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
+66
View File
@@ -0,0 +1,66 @@
.idea/
auto-import.
*.iml
*.ipr
cmake-build-*/
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
http-client.private.env.json
.DS_Store
/confluence/target
/dependencies/repo
/android.tests.dependencies
/dependencies/android.tests.dependencies
/dist
/local
/gh-pages
/ideaSDK
/clionSDK
/android-studio/sdk
/tmp
/intellij
workspace.xml
*.versionsBackup
/idea/testData/debugger/tinyApp/classes*
/jps-plugin/testData/kannotator
/js/js.translator/testData/out/
/js/js.translator/testData/out-min/
/js/js.translator/testData/out-pir/
.gradle/
build/
!**/src/**/build
!**/test/**/build
!**/tests-gen/**/build
!**/testData/**/*.iml
node_modules/
.rpt2_cache/
local.properties
buildSrcTmp/
distTmp/
outTmp/
/test.output
/kotlin-native/dist
kotlin-ide/
.kotlin/
.teamcity/
.npmrc
**/.junie/memory/
.claude/
.claudeignore
.ctx_cache.pkl
.opencode/
CLAUDE.md
scripts/
test_output.log
**/xcuserdata
.test-federation.*
bin/
/docs/future/
/docs/plans/
/docs/refactor.md
+12
View File
@@ -0,0 +1,12 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
id 'application'
}
application {
mainClass = 'com.correx.apps.cli.MainKt'
}
dependencies {
implementation "com.github.ajalt.clikt:clikt:5.0.1"
}
@@ -0,0 +1,5 @@
package com.correx.apps.cli
fun main() {
println("correx :: apps/cli")
}
+12
View File
@@ -0,0 +1,12 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
id 'application'
}
application {
mainClass = 'com.correx.apps.desktop.MainKt'
}
dependencies {
implementation "com.github.ajalt.clikt:clikt:5.0.1"
}
@@ -0,0 +1,5 @@
package com.correx.apps.desktop
fun main() {
println("correx :: apps/desktop")
}
+12
View File
@@ -0,0 +1,12 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
id 'application'
}
application {
mainClass = 'com.correx.apps.server.MainKt'
}
dependencies {
implementation "com.github.ajalt.clikt:clikt:5.0.1"
}
@@ -0,0 +1,5 @@
package com.correx.apps.server
fun main() {
println("correx :: apps/server")
}
+12
View File
@@ -0,0 +1,12 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
id 'application'
}
application {
mainClass = 'com.correx.apps.worker.MainKt'
}
dependencies {
implementation "com.github.ajalt.clikt:clikt:5.0.1"
}
@@ -0,0 +1,5 @@
package com.correx.apps.worker
fun main() {
println("correx :: apps/worker")
}
+126
View File
@@ -0,0 +1,126 @@
import io.gitlab.arturbosch.detekt.Detekt
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.0.21' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '2.0.21' apply false
id "io.gitlab.arturbosch.detekt" version "1.23.7"
id "org.jetbrains.kotlinx.kover" version "0.8.3"
}
ext {
kotlin_version = '2.0.21'
kotlinx_coroutines_version = '1.9.0'
kotlinx_serialization_version = '1.7.3'
kotlinx_datetime_version = '0.6.2'
junit_version = '5.11.0'
sqlite_jdbc_version = '3.47.1.0'
exposed_version = '0.57.0'
ktor_version = '3.0.3'
hoplite_version = '2.8.2'
sqlite_jdbc_version = '3.47.1.0'
}
allprojects {
group = 'com.correx'
version = '0.1.0'
repositories {
mavenCentral()
}
}
subprojects {
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
apply plugin: "io.gitlab.arturbosch.detekt"
apply plugin: "org.jetbrains.kotlinx.kover"
detekt {
toolVersion = "1.23.7"
config.setFrom("$rootDir/detekt.yml")
buildUponDefaultConfig = true
ignoreFailures = false
}
kover {
reports {
// verify {
// rule {
// minBound(80)
// }
// }
filters {
excludes {
classes("*Generated*")
}
}
}
}
tasks.named("check") {
dependsOn("test")
dependsOn("detekt")
dependsOn("koverVerify")
}
}
plugins.withId('org.jetbrains.kotlin.jvm') {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_version"
implementation "org.xerial:sqlite-jdbc:$sqlite_jdbc_version"
implementation "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinx_datetime_version"
testImplementation "org.junit.jupiter:junit-jupiter:$junit_version"
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
showStandardStreams = true
exceptionFormat = 'full'
}
afterSuite { desc, result ->
if (!desc.parent) { // Only log for the final root suite
def duration = (result.endTime - result.startTime) / 1000
println """
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
tests: ${result.testCount}
passed: ${result.successfulTestCount}
failed: ${result.failedTestCount}
skipped: ${result.skippedTestCount}
result: ${result.resultType}
time: ${duration}s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
}
}
}
tasks.withType(Detekt).configureEach {
reports {
html.required.set(true)
xml.required.set(true)
txt.required.set(false)
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
freeCompilerArgs.add("-Xcontext-receivers")
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
@@ -0,0 +1,3 @@
package com.correx.core.agents
object Module
+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())
}
}
+9
View File
@@ -0,0 +1,9 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation(project(":core:events"))
}
@@ -0,0 +1,24 @@
package com.correx.core.artifacts
import com.correx.core.events.events.StoredEvent
import com.correx.core.sessions.projections.Projection
import java.util.logging.Logger
class ArtifactProjector(private val reducer: ArtifactReducer) : Projection<ArtifactState> {
private val logger: Logger = Logger.getLogger(ArtifactProjector::class.java.name)
override fun initial(): ArtifactState = ArtifactState()
// Invalid lifecycle transitions must not crash the replayer. Log and leave state unchanged
// so replay continues; the event log remains the source of truth.
override fun apply(state: ArtifactState, event: StoredEvent): ArtifactState =
reducer.reduce(state, event).getOrElse { exception ->
if (exception !is IllegalStateException) throw exception
logger.warning(
"Invalid artifact lifecycle transition (event payload=${event.payload::class.simpleName}): " +
"${exception.message}. State unchanged."
)
state
}
}
@@ -0,0 +1,77 @@
package com.correx.core.artifacts
import com.correx.core.artifacts.model.ArtifactRelationship
import com.correx.core.events.events.ArtifactArchivedEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ArtifactRejectedEvent
import com.correx.core.events.events.ArtifactRelationshipAddedEvent
import com.correx.core.events.events.ArtifactSupersededEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.ArtifactLifecyclePhase
interface ArtifactReducer {
fun reduce(state: ArtifactState, event: StoredEvent): Result<ArtifactState>
}
class DefaultArtifactReducer : ArtifactReducer {
override fun reduce(state: ArtifactState, event: StoredEvent): Result<ArtifactState> =
when (val p = event.payload) {
is ArtifactCreatedEvent ->
Result.success(state.copy(phase = ArtifactLifecyclePhase.CREATED))
is ArtifactValidatingEvent ->
transition(state, ArtifactLifecyclePhase.VALIDATING, ArtifactLifecyclePhase.CREATED)
is ArtifactValidatedEvent ->
transition(state, ArtifactLifecyclePhase.VALIDATED, ArtifactLifecyclePhase.VALIDATING)
is ArtifactRejectedEvent ->
transition(state, ArtifactLifecyclePhase.REJECTED, ArtifactLifecyclePhase.VALIDATING)
is ArtifactSupersededEvent ->
transition(state, ArtifactLifecyclePhase.SUPERSEDED, ArtifactLifecyclePhase.VALIDATED)
is ArtifactArchivedEvent ->
transitionFromAny(
state,
ArtifactLifecyclePhase.ARCHIVED,
ArtifactLifecyclePhase.VALIDATED,
ArtifactLifecyclePhase.REJECTED,
)
is ArtifactRelationshipAddedEvent -> {
val rel = ArtifactRelationship(p.sourceId, p.targetId, p.relationshipType)
Result.success(
state.copy(lineage = state.lineage.copy(relationships = state.lineage.relationships + rel))
)
}
else -> Result.success(state)
}
private fun transition(
state: ArtifactState,
to: ArtifactLifecyclePhase,
from: ArtifactLifecyclePhase,
): Result<ArtifactState> =
if (state.phase == from) {
Result.success(state.copy(phase = to))
} else {
Result.failure(
IllegalStateException(
"Invalid artifact transition: ${state.phase}$to (expected $from)"
)
)
}
private fun transitionFromAny(
state: ArtifactState,
to: ArtifactLifecyclePhase,
vararg validFrom: ArtifactLifecyclePhase,
): Result<ArtifactState> =
if (state.phase in validFrom) {
Result.success(state.copy(phase = to))
} else {
Result.failure(
IllegalStateException(
"Invalid artifact transition: ${state.phase}$to (expected one of ${validFrom.toList()})"
)
)
}
}
@@ -0,0 +1,11 @@
package com.correx.core.artifacts
import com.correx.core.artifacts.model.ArtifactLineage
import com.correx.core.events.types.ArtifactLifecyclePhase
import kotlinx.serialization.Serializable
@Serializable
data class ArtifactState(
val phase: ArtifactLifecyclePhase = ArtifactLifecyclePhase.CREATED,
val lineage: ArtifactLineage = ArtifactLineage()
)
@@ -0,0 +1,3 @@
package com.correx.core.artifacts
object Module
@@ -0,0 +1,20 @@
package com.correx.core.artifacts.model
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ArtifactLifecyclePhase
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
sealed interface Artifact {
val id: ArtifactId
val sessionId: SessionId
val stageId: StageId
val schemaVersion: Int
val createdAt: Instant
val lifecyclePhase: ArtifactLifecyclePhase
val lineage: ArtifactLineage
val metadata: Map<String, String>
}
@@ -0,0 +1,12 @@
package com.correx.core.artifacts.model
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ValidationReportId
import kotlinx.serialization.Serializable
@Serializable
data class ArtifactLineage(
val parentIds: List<ArtifactId> = emptyList(),
val relationships: List<ArtifactRelationship> = emptyList(),
val validationReportIds: List<ValidationReportId> = emptyList()
)
@@ -0,0 +1,12 @@
package com.correx.core.artifacts.model
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ArtifactRelationshipType
import kotlinx.serialization.Serializable
@Serializable
data class ArtifactRelationship(
val sourceId: ArtifactId,
val targetId: ArtifactId,
val type: ArtifactRelationshipType
)
@@ -0,0 +1,11 @@
package com.correx.core.artifacts.serialization
import com.correx.core.artifacts.model.Artifact
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
val artifactModule: SerializersModule = SerializersModule {
polymorphic(Artifact::class) {
// concrete artifact subclasses registered here in future epics
}
}
+3
View File
@@ -0,0 +1,3 @@
subprojects {
group = "com.correx.core"
}
+5
View File
@@ -0,0 +1,5 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
@@ -0,0 +1,3 @@
package com.correx.core.config
object Module
+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:artifacts"))
implementation(project(":core:sessions"))
}
@@ -0,0 +1,13 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.events.StoredEvent
import com.correx.core.sessions.projections.Projection
class ContextProjector(
private val reducer: ContextReducer
) : Projection<ContextState> {
override fun initial(): ContextState = ContextState()
override fun apply(state: ContextState, event: StoredEvent): ContextState =
reducer.reduce(state, event)
}
@@ -0,0 +1,8 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.events.StoredEvent
interface ContextReducer {
fun reduce(state: ContextState, event: StoredEvent): ContextState
}
@@ -0,0 +1,23 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.events.ContextBuildingFailedEvent
import com.correx.core.events.events.ContextBuildingInterruptedEvent
import com.correx.core.events.events.ContextBuildingStartedEvent
import com.correx.core.events.events.ContextPackBuiltEvent
import com.correx.core.events.events.StoredEvent
class DefaultContextReducer : ContextReducer {
override fun reduce(state: ContextState, event: StoredEvent): ContextState =
when (val p = event.payload) {
is ContextBuildingStartedEvent -> state.copy(buildingInProgress = true, interrupted = false)
is ContextPackBuiltEvent -> state.copy(
buildingInProgress = false,
interrupted = false,
builtPackIds = state.builtPackIds + p.contextPackId,
)
is ContextBuildingFailedEvent -> state.copy(buildingInProgress = false, interrupted = false)
is ContextBuildingInterruptedEvent -> state.copy(buildingInProgress = false, interrupted = true)
else -> state
}
}
@@ -0,0 +1,12 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.replay.EventReplayer
class DefaultContextRepository(
private val replayer: EventReplayer<ContextState>
) {
fun getContextState(sessionId: SessionId): ContextState =
replayer.rebuild(sessionId)
}
@@ -0,0 +1,3 @@
package com.correx.core.context
object Module
@@ -0,0 +1,18 @@
package com.correx.core.context.builder
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
interface ContextPackBuilder {
fun build(
id: ContextPackId,
sessionId: SessionId,
stageId: StageId,
entries: List<ContextEntry>,
budget: TokenBudget
): ContextPack
}
@@ -0,0 +1,8 @@
package com.correx.core.context.builder
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextPack
interface DecisionPointBuilder {
fun build(pack: ContextPack): List<ContextEntry>
}
@@ -0,0 +1,76 @@
package com.correx.core.context.builder
import com.correx.core.context.compression.CompressionStrategy
import com.correx.core.context.compression.ContextCompressor
import com.correx.core.context.model.CompressionMetadata
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
class DefaultContextPackBuilder(
private val compressor: ContextCompressor
) : ContextPackBuilder {
private val neverDropSourceTypes = setOf("steeringNote", "eventHistory")
override fun build(
id: ContextPackId,
sessionId: SessionId,
stageId: StageId,
entries: List<ContextEntry>,
budget: TokenBudget
): ContextPack {
val (pinned, compressible) = entries.partition { it.sourceType in neverDropSourceTypes }
val pinnedTokens = pinned.sumOf { it.tokenEstimate }
var remainingTokens = (budget.limit - pinnedTokens).coerceAtLeast(0)
// Dispatch compression strategy by entry sourceType — applying Conversation
// strategy uniformly mangles tool logs and artifacts (their shape isn't conversational).
val strategiesUsed = linkedSetOf<String>()
val compressed = compressible
.groupBy { it.sourceType }
.flatMap { (sourceType, group) ->
val strategy = strategyFor(sourceType)
strategiesUsed += strategy::class.simpleName ?: "Unknown"
val groupBudget = TokenBudget(limit = remainingTokens.coerceAtLeast(0))
val result = compressor.compress(group, groupBudget, strategy)
remainingTokens -= result.sumOf { it.tokenEstimate }
result
}
val retained = pinned + compressed
val layers = retained.groupBy { it.layer }
val budgetUsed = retained.sumOf { it.tokenEstimate }
val droppedCount = entries.size - retained.size
val truncatedLayers = if (droppedCount > 0) {
entries.map { it.layer }.distinct().filter { layer ->
entries.count { it.layer == layer } > retained.count { it.layer == layer }
}
} else emptyList()
return ContextPack(
id = id,
sessionId = sessionId,
stageId = stageId,
layers = layers,
budgetUsed = budgetUsed,
budgetLimit = budget.limit,
compressionMetadata = CompressionMetadata(
appliedStrategies = strategiesUsed.toList().ifEmpty { listOf("Conversation") },
truncatedLayers = truncatedLayers,
entriesDropped = droppedCount
)
)
}
private fun strategyFor(sourceType: String): CompressionStrategy = when (sourceType) {
"toolLog" -> CompressionStrategy.ToolLog
"artifact" -> CompressionStrategy.Artifact
"steeringNote" -> CompressionStrategy.SteeringNote
"eventHistory" -> CompressionStrategy.EventHistory
else -> CompressionStrategy.Conversation()
}
}
@@ -0,0 +1,14 @@
package com.correx.core.context.builder
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.ContextPack
class DefaultDecisionPointBuilder : DecisionPointBuilder {
// L0 (live execution) and L1 (stage-local) are the only layers sent to the model for inference.
// L2+ are too large or already compressed into DecisionPoints.
private val inferenceLayers = setOf(ContextLayer.L0, ContextLayer.L1)
override fun build(pack: ContextPack): List<ContextEntry> =
inferenceLayers.flatMap { layer -> pack.layers[layer] ?: emptyList() }
}
@@ -0,0 +1,14 @@
package com.correx.core.context.compression
sealed interface CompressionStrategy {
// Tool log entries: deduplicate identical content, drop oldest when over budget
object ToolLog : CompressionStrategy
// Conversation turns: retain the last `keepLast` entries verbatim, drop oldest first when over budget. Default: 10.
data class Conversation(val keepLast: Int = 10) : CompressionStrategy
// Artifact entries: keep most recent entry per sourceId
object Artifact : CompressionStrategy
// Steering notes: always retained verbatim, never dropped
object SteeringNote : CompressionStrategy
// Event history DecisionPoints: always retained, already compressed facts
object EventHistory : CompressionStrategy
}
@@ -0,0 +1,12 @@
package com.correx.core.context.compression
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.TokenBudget
interface ContextCompressor {
fun compress(
entries: List<ContextEntry>,
budget: TokenBudget,
strategy: CompressionStrategy
): List<ContextEntry>
}
@@ -0,0 +1,63 @@
package com.correx.core.context.compression
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.TokenBudget
class DefaultContextCompressor : ContextCompressor {
// L0 (live execution), L3 (durable project memory), and L4 (archival) are never dropped — only L2 then L1.
private val dropOrder = listOf(ContextLayer.L2, ContextLayer.L1)
override fun compress(
entries: List<ContextEntry>,
budget: TokenBudget,
strategy: CompressionStrategy
): List<ContextEntry> = when (strategy) {
CompressionStrategy.SteeringNote,
CompressionStrategy.EventHistory -> entries
CompressionStrategy.Artifact -> compressArtifacts(entries, budget)
CompressionStrategy.ToolLog -> compressToolLogs(entries, budget)
is CompressionStrategy.Conversation -> compressConversation(entries, budget, strategy.keepLast)
}
// Keeps the last `keepLast` entries (oldest-first order preserved), then enforces budget.
// L0 is never truncated — overflow removes L1 from oldest first.
private fun compressConversation(
entries: List<ContextEntry>,
budget: TokenBudget,
keepLast: Int
): List<ContextEntry> {
val kept = if (entries.size > keepLast) entries.takeLast(keepLast) else entries
return trimToFit(kept, budget)
}
private fun compressArtifacts(entries: List<ContextEntry>, budget: TokenBudget): List<ContextEntry> {
val deduplicated = entries
.groupBy { it.sourceId }
.map { (_, group) -> group.last() }
return trimToFit(deduplicated, budget)
}
private fun compressToolLogs(entries: List<ContextEntry>, budget: TokenBudget): List<ContextEntry> {
val deduplicated = entries.distinctBy { it.content }
return trimToFit(deduplicated, budget)
}
private fun trimToFit(entries: List<ContextEntry>, budget: TokenBudget): List<ContextEntry> {
var total = entries.sumOf { it.tokenEstimate }
if (total <= budget.limit) return entries
val mutable = entries.toMutableList()
for (layer in dropOrder) {
if (total <= budget.limit) break
val layerEntries = mutable.filter { it.layer == layer }
for (entry in layerEntries) {
if (total <= budget.limit) break
mutable.remove(entry)
total -= entry.tokenEstimate
}
}
return mutable
}
}
@@ -0,0 +1,10 @@
package com.correx.core.context.model
import kotlinx.serialization.Serializable
@Serializable
data class CompressionMetadata(
val appliedStrategies: List<String> = emptyList(),
val truncatedLayers: List<ContextLayer> = emptyList(),
val entriesDropped: Int = 0
)
@@ -0,0 +1,14 @@
package com.correx.core.context.model
import com.correx.core.events.types.ContextEntryId
import kotlinx.serialization.Serializable
@Serializable
data class ContextEntry(
val id: ContextEntryId,
val layer: ContextLayer,
val content: String,
val sourceType: String,
val sourceId: String,
val tokenEstimate: Int
)
@@ -0,0 +1,12 @@
package com.correx.core.context.model
import kotlinx.serialization.Serializable
@Serializable
enum class ContextLayer {
L0, // live execution: current stage inputs/outputs
L1, // stage-local: recent artifacts and events within this stage
L2, // compressed session memory: summarised prior stages
L3, // durable project memory: cross-session retained context
L4 // archival history: stored externally, referenced by ID only
}
@@ -0,0 +1,17 @@
package com.correx.core.context.model
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class ContextPack(
val id: ContextPackId,
val sessionId: SessionId,
val stageId: StageId,
val layers: Map<ContextLayer, List<ContextEntry>> = emptyMap(),
val budgetUsed: Int,
val budgetLimit: Int,
val compressionMetadata: CompressionMetadata = CompressionMetadata()
)
@@ -0,0 +1,12 @@
package com.correx.core.context.model
data class TokenBudget(
val limit: Int,
val used: Int = 0
) {
val remaining: Int get() = limit - used
fun consume(tokens: Int): TokenBudget = copy(used = used + tokens)
fun canFit(tokens: Int): Boolean = tokens <= remaining
}
@@ -0,0 +1,11 @@
package com.correx.core.context.serialization
import com.correx.core.context.model.ContextPack
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
val contextModule: SerializersModule = SerializersModule {
polymorphic(ContextPack::class) {
// no subclasses — ContextPack is a concrete data class
}
}
@@ -0,0 +1,11 @@
package com.correx.core.context.state
import com.correx.core.events.types.ContextPackId
import kotlinx.serialization.Serializable
@Serializable
data class ContextState(
val builtPackIds: List<ContextPackId> = emptyList(),
val buildingInProgress: Boolean = false,
val interrupted: Boolean = false,
)
+9
View File
@@ -0,0 +1,9 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-datetime"
}
@@ -0,0 +1,10 @@
package com.correx.core.approvals
import kotlinx.serialization.Serializable
@Serializable
enum class ApprovalOutcome {
APPROVED,
REJECTED,
AUTO_APPROVED,
}
@@ -0,0 +1,10 @@
package com.correx.core.approvals
import kotlinx.serialization.Serializable
@Serializable
sealed interface ApprovalStatus {
@Serializable data object PENDING : ApprovalStatus
@Serializable data object COMPLETED : ApprovalStatus
@Serializable data object TIMED_OUT : ApprovalStatus
}
@@ -0,0 +1,12 @@
package com.correx.core.approvals
import com.correx.core.events.types.ProjectId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
sealed interface GrantScope {
@Serializable data object SESSION : GrantScope
@Serializable data class STAGE(val stageId: StageId) : GrantScope
@Serializable data class PROJECT(val projectId: ProjectId) : GrantScope
}
@@ -0,0 +1,16 @@
package com.correx.core.approvals
import kotlinx.serialization.Serializable
@Suppress("MagicNumber")
@Serializable
enum class Tier(val level: Int) {
T0(0),
T1(1),
T2(2),
T3(3),
T4(4)
}
fun Tier.isAtLeast(other: Tier): Boolean = this.level >= other.level
fun Tier.isAtMost(other: Tier): Boolean = this.level <= other.level
@@ -0,0 +1,12 @@
package com.correx.core.approvals
import com.correx.core.events.types.SessionId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class UserSteering(
val text: String,
val sessionId: SessionId,
val timestamp: Instant
)
@@ -0,0 +1,34 @@
package com.correx.core.events
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.NewEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.CausationId
import com.correx.core.events.types.CorrelationId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import kotlinx.datetime.Clock
import java.util.UUID
class EventDispatcher(private val eventStore: EventStore) {
suspend fun emit(
payload: EventPayload,
sessionId: SessionId,
causationId: CausationId? = null,
correlationId: CorrelationId? = null,
) {
val event = NewEvent(
metadata = EventMetadata(
eventId = EventId(UUID.randomUUID().toString()),
sessionId = sessionId,
timestamp = Clock.System.now(),
schemaVersion = 1,
causationId = causationId,
correlationId = correlationId,
),
payload = payload
)
eventStore.append(event)
}
}
@@ -0,0 +1,58 @@
package com.correx.core.events.events
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.GrantScope
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 com.correx.core.events.types.GrantId
import com.correx.core.events.types.ProjectId
import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ValidationReportId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class ApprovalRequestedEvent(
val requestId: ApprovalRequestId,
val tier: Tier,
val validationReportId: ValidationReportId,
val riskSummaryId: RiskSummaryId?,
val sessionId: SessionId,
val stageId: StageId?,
val projectId: ProjectId?,
val userSteering: UserSteering? = null
) : EventPayload
@Serializable
data class ApprovalDecisionResolvedEvent(
val decisionId: ApprovalDecisionId,
val requestId: ApprovalRequestId,
val outcome: ApprovalOutcome,
val status: ApprovalStatus,
val tier: Tier,
val resolutionTimestamp: Instant,
val reason: String?,
val userSteering: UserSteering? = null
) : EventPayload
@Serializable
data class ApprovalGrantCreatedEvent(
val grantId: GrantId,
val scope: GrantScope,
val permittedTiers: Set<Tier>,
val reason: String,
val expiresAt: Instant?,
val sessionId: SessionId,
val stageId: StageId?,
val projectId: ProjectId?
) : EventPayload
@Serializable
data class ApprovalGrantExpiredEvent(
val grantId: GrantId
) : EventPayload
@@ -0,0 +1,60 @@
package com.correx.core.events.events
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ArtifactRelationshipType
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class ArtifactValidatingEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class ArtifactValidatedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class ArtifactSupersededEvent(
val artifactId: ArtifactId,
val supersededById: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class ArtifactRelationshipAddedEvent(
val sourceId: ArtifactId,
val targetId: ArtifactId,
val relationshipType: ArtifactRelationshipType,
val sessionId: SessionId,
) : EventPayload
@Serializable
data class ArtifactRejectedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
) : EventPayload
@Serializable
data class ArtifactCreatedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
val schemaVersion: Int,
) : EventPayload
@Serializable
data class ArtifactArchivedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@@ -0,0 +1,51 @@
package com.correx.core.events.events
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class LayerTruncatedEvent(
val contextPackId: ContextPackId,
val layer: String,
val entriesDropped: Int,
val reason: String,
) : EventPayload
@Serializable
data class ContextBuildingStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class ContextBuildingFailedEvent(
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
) : EventPayload
// Emitted during replay when a session ended with buildingInProgress=true and no completion/failure event followed.
@Serializable
data class ContextBuildingInterruptedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class CompressionAppliedEvent(
val contextPackId: ContextPackId,
val layer: String,
val entriesRemoved: Int,
val strategyApplied: String,
) : EventPayload
@Serializable
data class ContextPackBuiltEvent(
val contextPackId: ContextPackId,
val sessionId: SessionId,
val stageId: StageId,
val budgetUsed: Int,
val budgetLimit: Int,
) : EventPayload
@@ -0,0 +1,16 @@
package com.correx.core.events.events
import kotlinx.serialization.Serializable
@Serializable
data class NewEvent(
val metadata: EventMetadata,
val payload: EventPayload
)
@Serializable
data class StoredEvent(
val metadata: EventMetadata,
val sequence: Long,
val payload: EventPayload
)
@@ -0,0 +1,18 @@
package com.correx.core.events.events
import com.correx.core.events.types.CausationId
import com.correx.core.events.types.CorrelationId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class EventMetadata(
val eventId: EventId,
val sessionId: SessionId,
val timestamp: Instant,
val schemaVersion: Int,
val causationId: CausationId?,
val correlationId: CorrelationId?
)
@@ -0,0 +1,6 @@
package com.correx.core.events.events
import kotlinx.serialization.Serializable
@Serializable
sealed interface EventPayload
@@ -0,0 +1,60 @@
package com.correx.core.events.events
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.ProviderId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.TokenUsage
import kotlinx.serialization.Serializable
@Serializable
data class InferenceStartedEvent(
val requestId: InferenceRequestId,
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
) : EventPayload
@Serializable
data class InferenceCompletedEvent(
val requestId: InferenceRequestId,
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
val tokensUsed: TokenUsage,
val latencyMs: Long,
) : EventPayload
@Serializable
data class InferenceFailedEvent(
val requestId: InferenceRequestId,
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
val reason: String, // human-readable; structured cause lives in CancellationReason
) : EventPayload
@Serializable
data class InferenceTimeoutEvent(
val requestId: InferenceRequestId,
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
val timeoutMs: Long,
) : EventPayload
@Serializable
data class ModelLoadedEvent(
val modelId: String,
val providerId: ProviderId,
val sessionId: SessionId,
) : EventPayload
@Serializable
data class ModelUnloadedEvent(
val modelId: String,
val providerId: ProviderId,
val sessionId: SessionId,
val cancellationReason: String? = null, // serialized label, not the sealed class
) : EventPayload
@@ -0,0 +1,50 @@
package com.correx.core.events.events
import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class WorkflowStartedEvent(
val sessionId: SessionId,
val startStageId: StageId,
val retryPolicy: RetryPolicy? = null,
) : EventPayload
@Serializable
data class WorkflowCompletedEvent(
val sessionId: SessionId,
val terminalStageId: StageId,
val totalStages: Int,
) : EventPayload
@Serializable
data class WorkflowFailedEvent(
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
val retryExhausted: Boolean,
) : EventPayload
@Serializable
data class OrchestrationPausedEvent(
val sessionId: SessionId,
val stageId: StageId,
val reason: String, // e.g. "APPROVAL_PENDING", "USER_REQUESTED"
) : EventPayload
@Serializable
data class OrchestrationResumedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class RetryAttemptedEvent(
val sessionId: SessionId,
val stageId: StageId,
val attemptNumber: Int,
val maxAttempts: Int,
val failureReason: String,
) : EventPayload
@@ -0,0 +1,17 @@
package com.correx.core.events.events
import com.correx.core.events.risk.RiskAction
import com.correx.core.events.risk.RiskLevel
import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class RiskAssessedEvent(
val sessionId: SessionId,
val stageId: StageId,
val riskSummaryId: RiskSummaryId,
val level: RiskLevel,
val action: RiskAction,
) : EventPayload
@@ -0,0 +1,34 @@
package com.correx.core.events.events
import com.correx.core.events.types.SessionId
import kotlinx.serialization.Serializable
@Serializable
data class SessionStartedEvent(
val sessionId: SessionId,
val initialContextId: String? = null
) : EventPayload
@Serializable
data class SessionPausedEvent(
val sessionId: SessionId,
val reason: String? = null
) : EventPayload
@Serializable
data class SessionResumedEvent(
val sessionId: SessionId,
) : EventPayload
@Serializable
data class SessionCompletedEvent(
val sessionId: SessionId,
val summary: String? = null
) : EventPayload
@Serializable
data class SessionFailedEvent(
val sessionId: SessionId,
val errorCode: String? = null,
val errorMessage: String? = null
) : EventPayload
@@ -0,0 +1,36 @@
package com.correx.core.events.events
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import kotlinx.serialization.Serializable
@Serializable
data class StageStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
val transitionId: TransitionId
) : EventPayload
@Serializable
data class StageCompletedEvent(
val sessionId: SessionId,
val stageId: StageId,
val transitionId: TransitionId
) : EventPayload
@Serializable
data class StageFailedEvent(
val sessionId: SessionId,
val stageId: StageId,
val transitionId: TransitionId,
val reason: String
) : EventPayload
@Serializable
data class TransitionExecutedEvent(
val sessionId: SessionId,
val from: StageId,
val to: StageId,
val transitionId: TransitionId
) : EventPayload
@@ -0,0 +1,54 @@
package com.correx.core.events.events
import com.correx.core.approvals.Tier
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import kotlinx.serialization.Serializable
@Serializable
data class ToolExecutionCompletedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val toolName: String,
val receipt: ToolReceipt,
) : EventPayload
@Serializable
data class ToolExecutionFailedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val toolName: String,
val reason: String,
) : EventPayload
@Serializable
data class ToolExecutionRejectedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val toolName: String,
val tier: Tier,
val reason: String,
) : EventPayload
@Serializable
data class ToolExecutionStartedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val toolName: String,
) : EventPayload
@Serializable
data class ToolInvocationRequestedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val stageId: StageId,
val toolName: String,
val tier: Tier,
val request: ToolRequest,
) : EventPayload
@Serializable
data class ToolInvokedEvent(
val toolId: String,
) : EventPayload
@@ -0,0 +1,21 @@
package com.correx.core.events.events
import com.correx.core.approvals.Tier
import com.correx.core.events.serialization.AnyMapSerializer
import com.correx.core.events.types.ToolInvocationId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class ToolReceipt(
val invocationId: ToolInvocationId,
val toolName: String,
val exitCode: Int,
val outputSummary: String,
@Serializable(with = AnyMapSerializer::class)
val structuredOutput: Map<String, Any> = emptyMap(),
val affectedEntities: List<String> = emptyList(),
val durationMs: Long,
val tier: Tier,
val timestamp: Instant
)
@@ -0,0 +1,17 @@
package com.correx.core.events.events
import com.correx.core.events.serialization.AnyMapSerializer
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import kotlinx.serialization.Serializable
@Serializable
data class ToolRequest(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val stageId: StageId,
val toolName: String,
@Serializable(with = AnyMapSerializer::class)
val parameters: Map<String, Any> = emptyMap()
)
@@ -0,0 +1,13 @@
package com.correx.core.events.execution
import kotlinx.serialization.Serializable
@Serializable
data class RetryPolicy(
val maxAttempts: Int,
val backoffMs: Long = 0L,
) {
init {
require(maxAttempts >= 1) { "maxAttempts must be >= 1" }
}
}
@@ -0,0 +1,16 @@
package com.correx.core.events.orchestration
import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class OrchestrationState(
val currentStageId: StageId? = null,
val status: OrchestrationStatus = OrchestrationStatus.IDLE,
val retryCount: Int = 0,
val pauseReason: String? = null,
val pendingApproval: Boolean = false,
val failureReason: String? = null,
val retryPolicy: RetryPolicy? = null,
)
@@ -0,0 +1,11 @@
package com.correx.core.events.orchestration
enum class OrchestrationStatus {
IDLE,
RUNNING,
PAUSED,
COMPLETED,
FAILED,
CANCELED,
;
}
@@ -0,0 +1,3 @@
package com.correx.core.events.risk
enum class RiskAction { PROCEED, PROMPT_USER, BLOCK }
@@ -0,0 +1,3 @@
package com.correx.core.events.risk
enum class RiskLevel { LOW, MEDIUM, HIGH, CRITICAL }
@@ -0,0 +1,8 @@
package com.correx.core.events.risk
sealed class RiskSignal {
data class CycleWithoutExit(val cycleId: String) : RiskSignal()
data class RepeatedFailure(val reason: String, val count: Int) : RiskSignal()
data class ValidationErrors(val errorCount: Int) : RiskSignal()
data class InferenceTimeout(val elapsedMs: Long) : RiskSignal()
}
@@ -0,0 +1,7 @@
package com.correx.core.events.risk
data class RiskSummary(
val level: RiskLevel,
val signals: List<RiskSignal>,
val recommendedAction: RiskAction,
)
@@ -0,0 +1,71 @@
package com.correx.core.events.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
private object AnySerializer : KSerializer<Any> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AnyValue")
override fun serialize(encoder: Encoder, value: Any) {
val jsonEncoder = encoder as? JsonEncoder
?: throw SerializationException("AnySerializer requires a JSON encoder")
jsonEncoder.encodeJsonElement(value.toJsonElement())
}
override fun deserialize(decoder: Decoder): Any {
val jsonDecoder = decoder as? JsonDecoder
?: throw SerializationException("AnySerializer requires a JSON decoder")
return jsonDecoder.decodeJsonElement().toAny()
}
}
private fun Any?.toJsonElement(): JsonElement = when (this) {
null -> JsonNull
is String -> JsonPrimitive(this)
is Number -> JsonPrimitive(this)
is Boolean -> JsonPrimitive(this)
is Map<*, *> -> JsonObject(entries.associate { (k, v) -> k.toString() to v.toJsonElement() })
is List<*> -> JsonArray(map { it.toJsonElement() })
else -> throw SerializationException("Unsupported type: ${this::class.simpleName}")
}
private fun JsonElement.toAny(): Any = when (this) {
is JsonNull -> throw SerializationException("null values are not supported in Map<String, Any>")
is JsonPrimitive -> {
val content = this.content
when {
content == "true" || content == "false" -> content == "true"
content == "null" -> throw SerializationException("null values are not supported in Map<String, Any>")
content.toDoubleOrNull() != null && content.contains('.') -> content.toDouble()
content.toLongOrNull() != null -> content.toLong()
else -> content // fallback to string
}
}
is JsonObject -> entries.associate { (k, v) -> k to v.toAny() }
is JsonArray -> map { it.toAny() }
}
object AnyMapSerializer : KSerializer<Map<String, Any>> {
private val delegate = MapSerializer(String.serializer(), AnySerializer)
override val descriptor: SerialDescriptor = delegate.descriptor
@Suppress("UNCHECKED_CAST")
override fun deserialize(decoder: Decoder): Map<String, Any> =
delegate.deserialize(decoder) as Map<String, Any>
override fun serialize(encoder: Encoder, value: Map<String, Any>) =
delegate.serialize(encoder, value)
}
@@ -0,0 +1,8 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
interface EventSerializer {
fun serialize(payload: EventPayload): String
fun deserialize(raw: String): EventPayload
}
@@ -0,0 +1,15 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import kotlinx.serialization.json.Json
class JsonEventSerializer(
private val json: Json = eventJson
) : EventSerializer {
override fun serialize(payload: EventPayload): String =
json.encodeToString(EventPayload.serializer(), payload)
override fun deserialize(raw: String): EventPayload =
json.decodeFromString(EventPayload.serializer(), raw)
}
@@ -0,0 +1,107 @@
package com.correx.core.events.serialization
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.ArtifactArchivedEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ArtifactRejectedEvent
import com.correx.core.events.events.ArtifactRelationshipAddedEvent
import com.correx.core.events.events.ArtifactSupersededEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.CompressionAppliedEvent
import com.correx.core.events.events.ContextBuildingFailedEvent
import com.correx.core.events.events.ContextBuildingInterruptedEvent
import com.correx.core.events.events.ContextBuildingStartedEvent
import com.correx.core.events.events.ContextPackBuiltEvent
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceFailedEvent
import com.correx.core.events.events.InferenceStartedEvent
import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.ModelLoadedEvent
import com.correx.core.events.events.ModelUnloadedEvent
import com.correx.core.events.events.LayerTruncatedEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.SessionCompletedEvent
import com.correx.core.events.events.SessionFailedEvent
import com.correx.core.events.events.SessionPausedEvent
import com.correx.core.events.events.SessionResumedEvent
import com.correx.core.events.events.SessionStartedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StageStartedEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolExecutionRejectedEvent
import com.correx.core.events.events.ToolExecutionStartedEvent
import com.correx.core.events.events.ToolInvokedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
val eventModule = SerializersModule {
polymorphic(EventPayload::class) {
// Legacy stub event — predates the full tool lifecycle model (ToolInvocationRequestedEvent et al.)
subclass(ToolInvokedEvent::class)
subclass(ToolInvocationRequestedEvent::class)
subclass(ToolExecutionStartedEvent::class)
subclass(ToolExecutionCompletedEvent::class)
subclass(ToolExecutionFailedEvent::class)
subclass(ToolExecutionRejectedEvent::class)
subclass(SessionStartedEvent::class)
subclass(SessionPausedEvent::class)
subclass(SessionResumedEvent::class)
subclass(SessionCompletedEvent::class)
subclass(SessionFailedEvent::class)
subclass(StageStartedEvent::class)
subclass(StageFailedEvent::class)
subclass(StageCompletedEvent::class)
subclass(TransitionExecutedEvent::class)
subclass(ApprovalRequestedEvent::class)
subclass(ApprovalDecisionResolvedEvent::class)
subclass(ApprovalGrantCreatedEvent::class)
subclass(ApprovalGrantExpiredEvent::class)
subclass(ContextBuildingStartedEvent::class)
subclass(ContextPackBuiltEvent::class)
subclass(CompressionAppliedEvent::class)
subclass(LayerTruncatedEvent::class)
subclass(ContextBuildingFailedEvent::class)
subclass(ContextBuildingInterruptedEvent::class)
subclass(ArtifactCreatedEvent::class)
subclass(ArtifactValidatingEvent::class)
subclass(ArtifactValidatedEvent::class)
subclass(ArtifactRejectedEvent::class)
subclass(ArtifactSupersededEvent::class)
subclass(ArtifactArchivedEvent::class)
subclass(ArtifactRelationshipAddedEvent::class)
subclass(InferenceFailedEvent::class)
subclass(InferenceCompletedEvent::class)
subclass(InferenceStartedEvent::class)
subclass(InferenceTimeoutEvent::class)
subclass(ModelLoadedEvent::class)
subclass(ModelUnloadedEvent::class)
subclass(OrchestrationResumedEvent::class)
subclass(OrchestrationPausedEvent::class)
subclass(WorkflowStartedEvent::class)
subclass(WorkflowFailedEvent::class)
subclass(WorkflowCompletedEvent::class)
subclass(RetryAttemptedEvent::class)
subclass(RiskAssessedEvent::class)
}
}
val eventJson = Json {
serializersModule = eventModule
}
@@ -0,0 +1,37 @@
package com.correx.core.events.stores
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.SessionId
interface EventStore {
/**
* Append a single event to the store.
* Must enforce:
* - monotonic per-session sequence
* - idempotency by eventId
* - causal consistency rules (if enforced here, otherwise in kernel)
*/
fun append(event: NewEvent): StoredEvent
/**
* Append multiple events atomically (same session preferred).
*/
fun appendAll(events: List<NewEvent>): List<StoredEvent>
/**
* Read events for a session in strict sequence order.
*/
fun read(sessionId: SessionId): List<StoredEvent>
/**
* Read events from a specific sequence offset (for replay/resume).
*/
fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent>
/**
* Get last sequence number for session.
*/
fun lastSequence(sessionId: SessionId): Long?
}
@@ -0,0 +1,13 @@
package com.correx.core.events.types
import kotlinx.serialization.Serializable
@Serializable
enum class ArtifactLifecyclePhase {
CREATED,
VALIDATING,
VALIDATED,
REJECTED,
SUPERSEDED,
ARCHIVED
}
@@ -0,0 +1,14 @@
package com.correx.core.events.types
import kotlinx.serialization.Serializable
@Serializable
enum class ArtifactRelationshipType {
PARENT,
CHILD,
SUPERSEDES,
DERIVED_FROM,
VALIDATED_BY,
APPROVED_BY,
GENERATED_FROM
}
@@ -0,0 +1,17 @@
package com.correx.core.events.types
object EventTypes {
const val SESSION_STARTED = "session.started"
const val SESSION_PAUSED = "session.paused"
const val SESSION_COMPLETED = "session.completed"
const val TOOL_INVOKED = "tool.invoked"
const val TOOL_COMPLETED = "tool.completed"
const val APPROVAL_REQUESTED = "approval.requested"
const val APPROVAL_DECISION_RESOLVED = "approval.decision_resolved"
const val APPROVAL_GRANT_CREATED = "approval.grant_created"
const val APPROVAL_GRANT_EXPIRED = "approval.grant_expired"
const val ARTIFACT_CREATED = "artifact.created"
}
@@ -0,0 +1,42 @@
package com.correx.core.events.types
import com.correx.core.utils.TypeId
// Events types
typealias EventId = TypeId
typealias SessionId = TypeId
typealias CorrelationId = TypeId
typealias CausationId = TypeId
// Transitions types
typealias StageId = TypeId
typealias TransitionId = TypeId
// Artifacts types
typealias ArtifactId = TypeId
// Context types
typealias ContextPackId = TypeId
typealias ContextEntryId = TypeId
// Approvals types
typealias ValidationReportId = TypeId
typealias RiskSummaryId = TypeId
typealias GrantId = TypeId
typealias ApprovalRequestId = TypeId
typealias ProjectId = TypeId
typealias ApprovalDecisionId = TypeId
// Tools types
typealias ToolInvocationId = TypeId
// Inference
typealias InferenceRequestId = TypeId
typealias ProviderId = TypeId
@@ -0,0 +1 @@
package com.correx.core.events.types
@@ -0,0 +1,11 @@
package com.correx.core.inference
import kotlinx.serialization.Serializable
@Serializable
data class TokenUsage(
val promptTokens: Int,
val completionTokens: Int,
) {
val totalTokens: Int get() = promptTokens + completionTokens
}
@@ -0,0 +1,14 @@
package com.correx.core.sessions.projections
import com.correx.core.events.events.StoredEvent
class DefaultStateBuilder<S>(
private val projection: Projection<S>
) : StateBuilder<S> {
override fun build(events: List<StoredEvent>): S {
return events.fold(projection.initial()) { state, event ->
projection.apply(state, event)
}
}
}

Some files were not shown because too many files have changed in this diff Show More