epic-12: after epic audit and init commit
This commit is contained in:
+66
@@ -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
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
+91
@@ -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
|
||||
)
|
||||
+13
@@ -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()
|
||||
)
|
||||
+23
@@ -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
|
||||
)
|
||||
+138
@@ -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)
|
||||
}
|
||||
}
|
||||
+99
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
+12
@@ -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
|
||||
)
|
||||
+11
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
subprojects {
|
||||
group = "com.correx.core"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
+76
@@ -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()
|
||||
}
|
||||
}
|
||||
+14
@@ -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() }
|
||||
}
|
||||
+14
@@ -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>
|
||||
}
|
||||
+63
@@ -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
|
||||
}
|
||||
+11
@@ -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,
|
||||
)
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
+16
@@ -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,
|
||||
)
|
||||
+11
@@ -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
|
||||
}
|
||||
+15
@@ -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
|
||||
}
|
||||
+14
@@ -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
Reference in New Issue
Block a user