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