epic-12: after epic audit and init commit

This commit is contained in:
2026-05-16 11:42:00 +04:00
commit c77277af0b
461 changed files with 28958 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation(project(":core:events"))
implementation(project(":core:sessions"))
testImplementation(project(":infrastructure:persistence"))
}
@@ -0,0 +1,10 @@
package com.correx.core.approvals
import com.correx.core.approvals.model.ApprovalState
import com.correx.core.events.events.StoredEvent
import com.correx.core.sessions.projections.Projection
class ApprovalProjector(private val reducer: ApprovalReducer) : Projection<ApprovalState> {
override fun initial(): ApprovalState = ApprovalState()
override fun apply(state: ApprovalState, event: StoredEvent): ApprovalState = reducer.reduce(state, event)
}
@@ -0,0 +1,8 @@
package com.correx.core.approvals
import com.correx.core.approvals.model.ApprovalState
import com.correx.core.events.events.StoredEvent
interface ApprovalReducer {
fun reduce(state: ApprovalState, event: StoredEvent): ApprovalState
}
@@ -0,0 +1,81 @@
package com.correx.core.approvals
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalDecision
import com.correx.core.approvals.model.ApprovalGrant
import com.correx.core.approvals.model.DomainApprovalRequest
import com.correx.core.approvals.model.ApprovalScopeIdentity
import com.correx.core.approvals.model.ApprovalState
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ApprovalGrantExpiredEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.sessions.ApprovalMode
class DefaultApprovalReducer : ApprovalReducer {
override fun reduce(state: ApprovalState, event: StoredEvent): ApprovalState {
return when (val payload = event.payload) {
is ApprovalRequestedEvent -> {
val request = DomainApprovalRequest(
id = payload.requestId,
tier = payload.tier,
validationReportId = payload.validationReportId,
riskSummaryId = payload.riskSummaryId,
timestamp = event.metadata.timestamp,
causationId = event.metadata.causationId,
correlationId = event.metadata.correlationId,
userSteering = payload.userSteering
)
state.copy(requests = state.requests + (request.id to request))
}
is ApprovalDecisionResolvedEvent -> {
val decisionId = payload.decisionId
// The event does not carry context info, so we reconstruct a partial identity
// with a default mode; stageId and projectId are unavailable at this point.
val contextSnapshot = ApprovalContext(
identity = ApprovalScopeIdentity(
sessionId = event.metadata.sessionId,
stageId = null,
projectId = null
),
mode = ApprovalMode.PROMPT,
causationId = event.metadata.causationId,
correlationId = event.metadata.correlationId
)
val decision = ApprovalDecision(
id = decisionId,
requestId = payload.requestId,
outcome = payload.outcome,
state = payload.status,
tier = payload.tier,
contextSnapshot = contextSnapshot,
resolutionTimestamp = payload.resolutionTimestamp,
reason = payload.reason,
userSteering = payload.userSteering
)
state.copy(decisions = state.decisions + (decisionId to decision))
}
is ApprovalGrantCreatedEvent -> {
val grant = ApprovalGrant(
id = payload.grantId,
scope = payload.scope,
permittedTiers = payload.permittedTiers,
reason = payload.reason,
timestamp = event.metadata.timestamp,
expiresAt = payload.expiresAt
)
state.copy(grants = state.grants + (grant.id to grant))
}
is ApprovalGrantExpiredEvent -> {
state.copy(grants = state.grants - payload.grantId)
}
else -> state
}
}
}
@@ -0,0 +1,12 @@
package com.correx.core.approvals
import com.correx.core.approvals.model.ApprovalState
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.replay.EventReplayer
class DefaultApprovalRepository(
private val replayer: EventReplayer<ApprovalState>
) {
fun getApprovalState(sessionId: SessionId): ApprovalState =
replayer.rebuild(sessionId)
}
@@ -0,0 +1,16 @@
package com.correx.core.approvals.domain
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalDecision
import com.correx.core.approvals.model.ApprovalGrant
import com.correx.core.approvals.model.DomainApprovalRequest
import kotlinx.datetime.Instant
interface ApprovalEngine {
fun evaluate(
request: DomainApprovalRequest,
context: ApprovalContext,
grants: List<ApprovalGrant>,
now: Instant
): ApprovalDecision
}
@@ -0,0 +1,91 @@
package com.correx.core.approvals.domain
import com.correx.core.approvals.Tier
import com.correx.core.approvals.isAtMost
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalDecision
import com.correx.core.approvals.model.ApprovalGrant
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.model.DomainApprovalRequest
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.GrantScope
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.sessions.ApprovalMode
import kotlinx.datetime.Instant
@Suppress("ReturnCount")
class DefaultApprovalEngine : ApprovalEngine {
override fun evaluate(
request: DomainApprovalRequest,
context: ApprovalContext,
grants: List<ApprovalGrant>,
now: Instant
): ApprovalDecision {
val decisionId = ApprovalDecisionId("decision:${request.id.value}")
val matchingGrant = grants.firstOrNull { grant ->
!isExpired(grant, now)
&& scopeMatches(grant.scope, context)
&& request.tier in grant.permittedTiers
}
if (matchingGrant != null) {
return approved(decisionId, request, context, now, reason = "grant:${matchingGrant.id.value}")
}
if (context.mode == ApprovalMode.YOLO) {
return approved(decisionId, request, context, now, reason = null)
}
val threshold = when (context.mode) {
ApprovalMode.DENY -> Tier.T0
ApprovalMode.PROMPT -> Tier.T1
ApprovalMode.AUTO -> Tier.T2
ApprovalMode.YOLO -> error("unreachable")
}
return if (request.tier.isAtMost(threshold)) {
approved(decisionId, request, context, now, reason = null)
} else {
ApprovalDecision(
id = decisionId,
requestId = request.id,
outcome = null,
state = ApprovalStatus.PENDING,
tier = request.tier,
contextSnapshot = context,
resolutionTimestamp = null,
reason = null,
userSteering = request.userSteering
)
}
}
private fun approved(
id: ApprovalDecisionId,
request: DomainApprovalRequest,
context: ApprovalContext,
now: Instant,
reason: String?
) = ApprovalDecision(
id = id,
requestId = request.id,
outcome = ApprovalOutcome.AUTO_APPROVED,
state = ApprovalStatus.COMPLETED,
tier = request.tier,
contextSnapshot = context,
resolutionTimestamp = now,
reason = reason,
userSteering = request.userSteering
)
private fun isExpired(grant: ApprovalGrant, now: Instant): Boolean =
grant.expiresAt != null && grant.expiresAt <= now
private fun scopeMatches(scope: GrantScope, context: ApprovalContext): Boolean = when (scope) {
is GrantScope.SESSION -> true
is GrantScope.STAGE -> context.identity.stageId == scope.stageId
is GrantScope.PROJECT -> context.identity.projectId == scope.projectId
}
}
@@ -0,0 +1,33 @@
package com.correx.core.approvals.domain
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.Tier
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalDecision
import com.correx.core.approvals.model.ApprovalGrant
import com.correx.core.approvals.model.DomainApprovalRequest
import kotlinx.datetime.Instant
/**
* No-op implementation of [ApprovalEngine] used during deterministic replay.
* Replay reconstructs approval decisions from the event log — live approval
* evaluation must never run on the replay path.
*/
class NoOpApprovalEngine : ApprovalEngine {
override fun evaluate(
request: DomainApprovalRequest,
context: ApprovalContext,
grants: List<ApprovalGrant>,
now: Instant,
): ApprovalDecision = ApprovalDecision(
id = null,
requestId = request.id,
outcome = ApprovalOutcome.AUTO_APPROVED,
state = ApprovalStatus.COMPLETED,
tier = Tier.T0,
contextSnapshot = context,
resolutionTimestamp = now,
reason = "replay — reconstructed from event log",
)
}
@@ -0,0 +1,14 @@
package com.correx.core.approvals.model
import com.correx.core.events.types.CausationId
import com.correx.core.events.types.CorrelationId
import com.correx.core.sessions.ApprovalMode
import kotlinx.serialization.Serializable
@Serializable
data class ApprovalContext(
val identity: ApprovalScopeIdentity,
val mode: ApprovalMode,
val causationId: CausationId? = null,
val correlationId: CorrelationId? = null
)
@@ -0,0 +1,26 @@
package com.correx.core.approvals.model
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.Tier
import com.correx.core.approvals.UserSteering
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class ApprovalDecision(
val id: ApprovalDecisionId?,
val requestId: ApprovalRequestId,
val outcome: ApprovalOutcome?,
val state: ApprovalStatus,
val tier: Tier,
val contextSnapshot: ApprovalContext,
val resolutionTimestamp: Instant?,
val reason: String?,
val userSteering: UserSteering? = null
) {
val isFinal: Boolean get() = state != ApprovalStatus.PENDING
val isApproved: Boolean get() = outcome == ApprovalOutcome.APPROVED || outcome == ApprovalOutcome.AUTO_APPROVED
}
@@ -0,0 +1,17 @@
package com.correx.core.approvals.model
import com.correx.core.approvals.GrantScope
import com.correx.core.approvals.Tier
import com.correx.core.events.types.GrantId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class ApprovalGrant(
val id: GrantId,
val scope: GrantScope,
val permittedTiers: Set<Tier>,
val reason: String,
val timestamp: Instant,
val expiresAt: Instant? = null
)
@@ -0,0 +1,13 @@
package com.correx.core.approvals.model
import com.correx.core.events.types.ProjectId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.SessionId
import kotlinx.serialization.Serializable
@Serializable
data class ApprovalScopeIdentity(
val sessionId: SessionId,
val stageId: StageId?,
val projectId: ProjectId?
)
@@ -0,0 +1,13 @@
package com.correx.core.approvals.model
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.GrantId
import kotlinx.serialization.Serializable
@Serializable
data class ApprovalState(
val requests: Map<ApprovalRequestId, DomainApprovalRequest> = emptyMap(),
val decisions: Map<ApprovalDecisionId, ApprovalDecision> = emptyMap(),
val grants: Map<GrantId, ApprovalGrant> = emptyMap()
)
@@ -0,0 +1,23 @@
package com.correx.core.approvals.model
import com.correx.core.approvals.Tier
import com.correx.core.approvals.UserSteering
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.CausationId
import com.correx.core.events.types.CorrelationId
import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.ValidationReportId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class DomainApprovalRequest(
val id: ApprovalRequestId,
val tier: Tier,
val validationReportId: ValidationReportId,
val riskSummaryId: RiskSummaryId?,
val timestamp: Instant,
val causationId: CausationId? = null,
val correlationId: CorrelationId? = null,
val userSteering: UserSteering? = null
)
@@ -0,0 +1,138 @@
package com.correx.core.approvals
import com.correx.core.approvals.model.ApprovalState
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ApprovalGrantExpiredEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolInvokedEvent
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.events.types.EventId
import com.correx.core.events.types.GrantId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.ValidationReportId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class DefaultApprovalReducerTest {
private val reducer = DefaultApprovalReducer()
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
private val sessionId = SessionId("session-1")
private fun storedEvent(payload: com.correx.core.events.events.EventPayload, sequence: Long = 1L): StoredEvent {
return StoredEvent(
metadata = EventMetadata(
eventId = EventId("event-$sequence"),
sessionId = sessionId,
timestamp = timestamp,
schemaVersion = 1,
causationId = null,
correlationId = null
),
sequence = sequence,
payload = payload
)
}
@Test
fun `ApprovalRequestedEvent adds request to state`() {
val requestId = ApprovalRequestId("req-1")
val payload = ApprovalRequestedEvent(
requestId = requestId,
tier = Tier.T2,
validationReportId = ValidationReportId("report-1"),
riskSummaryId = null,
sessionId = sessionId,
stageId = null,
projectId = null
)
val state = reducer.reduce(ApprovalState(), storedEvent(payload))
assertTrue(state.requests.containsKey(requestId))
assertEquals(requestId, state.requests[requestId]?.id)
assertEquals(Tier.T2, state.requests[requestId]?.tier)
}
@Test
fun `ApprovalGrantCreatedEvent adds grant to state`() {
val grantId = GrantId("grant-1")
val payload = ApprovalGrantCreatedEvent(
grantId = grantId,
scope = GrantScope.SESSION,
permittedTiers = setOf(Tier.T1, Tier.T2),
reason = "user approved",
expiresAt = null,
sessionId = sessionId,
stageId = null,
projectId = null
)
val state = reducer.reduce(ApprovalState(), storedEvent(payload))
assertTrue(state.grants.containsKey(grantId))
assertEquals(grantId, state.grants[grantId]?.id)
assertEquals(GrantScope.SESSION, state.grants[grantId]?.scope)
}
@Test
fun `ApprovalGrantExpiredEvent removes grant from state`() {
val grantId = GrantId("grant-2")
val addPayload = ApprovalGrantCreatedEvent(
grantId = grantId,
scope = GrantScope.SESSION,
permittedTiers = setOf(Tier.T1),
reason = "granted",
expiresAt = null,
sessionId = sessionId,
stageId = null,
projectId = null
)
val removePayload = ApprovalGrantExpiredEvent(grantId = grantId)
val stateAfterAdd = reducer.reduce(ApprovalState(), storedEvent(addPayload, sequence = 1L))
assertTrue(stateAfterAdd.grants.containsKey(grantId))
val stateAfterRemove = reducer.reduce(stateAfterAdd, storedEvent(removePayload, sequence = 2L))
assertFalse(stateAfterRemove.grants.containsKey(grantId))
}
@Test
fun `ApprovalDecisionResolvedEvent adds decision to state`() {
val decisionId = ApprovalDecisionId("decision-1")
val requestId = ApprovalRequestId("req-1")
val payload = ApprovalDecisionResolvedEvent(
decisionId = decisionId,
requestId = requestId,
outcome = ApprovalOutcome.APPROVED,
status = ApprovalStatus.COMPLETED,
tier = Tier.T2,
resolutionTimestamp = timestamp,
reason = null
)
val state = reducer.reduce(ApprovalState(), storedEvent(payload))
assertTrue(state.decisions.containsKey(decisionId))
assertEquals(requestId, state.decisions[decisionId]?.requestId)
assertEquals(ApprovalOutcome.APPROVED, state.decisions[decisionId]?.outcome)
}
@Test
fun `unknown events pass state through unchanged`() {
val initial = ApprovalState()
val state = reducer.reduce(initial, storedEvent(ToolInvokedEvent("tool")))
assertEquals(initial, state)
}
}
@@ -0,0 +1,99 @@
package com.correx.core.approvals
import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ApprovalGrantExpiredEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.GrantId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.ValidationReportId
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class DefaultApprovalRepositoryTest {
private val store = InMemoryEventStore()
private val replayer = DefaultEventReplayer(store, ApprovalProjector(DefaultApprovalReducer()))
private val repository = DefaultApprovalRepository(replayer)
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
private fun metadata(sessionId: SessionId, eventId: String): EventMetadata = EventMetadata(
eventId = EventId(eventId),
sessionId = sessionId,
timestamp = timestamp,
schemaVersion = 1,
causationId = null,
correlationId = null
)
@Test
fun `unknown session returns empty ApprovalState`() {
val state = repository.getApprovalState(SessionId("unknown-session"))
assertEquals(com.correx.core.approvals.model.ApprovalState(), state)
}
@Test
fun `after ApprovalRequestedEvent state contains the request`() {
val sessionId = SessionId("session-requested")
val requestId = ApprovalRequestId("req-1")
store.append(
NewEvent(
metadata = metadata(sessionId, "event-1"),
payload = ApprovalRequestedEvent(
requestId = requestId,
tier = Tier.T2,
validationReportId = ValidationReportId("report-1"),
riskSummaryId = null,
sessionId = sessionId,
stageId = null,
projectId = null
)
)
)
val state = repository.getApprovalState(sessionId)
assertEquals(1, state.requests.size)
assertTrue(state.requests.containsKey(requestId))
}
@Test
fun `after ApprovalGrantCreatedEvent then ApprovalGrantExpiredEvent grants list is empty`() {
val sessionId = SessionId("session-grant-expired")
val grantId = GrantId("grant-1")
store.append(
NewEvent(
metadata = metadata(sessionId, "event-1"),
payload = ApprovalGrantCreatedEvent(
grantId = grantId,
scope = GrantScope.SESSION,
permittedTiers = setOf(Tier.T1, Tier.T2),
reason = "approved",
expiresAt = null,
sessionId = sessionId,
stageId = null,
projectId = null
)
)
)
store.append(
NewEvent(
metadata = metadata(sessionId, "event-2"),
payload = ApprovalGrantExpiredEvent(grantId = grantId)
)
)
val state = repository.getApprovalState(sessionId)
assertTrue(state.grants.isEmpty())
}
}