feat(kernel): stage-level plan checkpointing (C-A2)

- StageCheckpointPassed/FailedEvent (registered + serialization test): per-stage
  reconciliation of produced artifacts vs the locked plan's produces-slots
- StageCheckpointReconciler (pure) + tests
- emitStageCheckpoint wired in after verifyProduces, runs regardless of its
  pass/fail (verifyProduces still owns the halt); gated on an ExecutionPlanLocked
  in the session log so non-plan workflows are unaffected

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 10:53:41 +00:00
parent 1df7af5b85
commit 1a1b5cc5ed
6 changed files with 192 additions and 1 deletions
@@ -53,6 +53,8 @@ import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.StageCheckpointFailedEvent
import com.correx.core.events.events.StageCheckpointPassedEvent
import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
@@ -549,7 +551,9 @@ abstract class SessionOrchestrator(
is StageExecutionResult.Success -> {
emitToolArtifacts(sessionId, stageId, stageConfig)
emitLlmArtifacts(sessionId, stageId, stageConfig)
when (val produced = verifyProduces(sessionId, stageId, stageConfig)) {
val produced = verifyProduces(sessionId, stageId, stageConfig)
emitStageCheckpoint(sessionId, stageId, stageConfig)
when (produced) {
is StageExecutionResult.Success ->
when (val grounded = groundBriefReferences(sessionId, stageId, stageConfig, effectives)) {
is StageExecutionResult.Success ->
@@ -1360,6 +1364,33 @@ abstract class SessionOrchestrator(
)
}
/**
* Stage-level plan checkpointing (BACKLOG §C-A2): make plan adherence observable and replayable
* by emitting a first-class checkpoint event per stage, reconciling produced-vs-expected against
* the confirmed (locked) plan. Only runs on plan-driven (phase-2) sessions — i.e. a log that
* holds an [ExecutionPlanLockedEvent]; non-plan workflows emit nothing. The halt itself is owned
* by [verifyProduces]; this method only EMITS — a [StageCheckpointFailedEvent] accompanies that
* halt and surfaces *why* the plan diverged.
*/
private suspend fun emitStageCheckpoint(
sessionId: SessionId,
stageId: StageId,
stageConfig: StageConfig,
) {
if (eventStore.read(sessionId).none { it.payload is ExecutionPlanLockedEvent }) return
val expected = stageConfig.produces.map { it.name.value }.toSet()
if (expected.isEmpty()) return
val produced = eventStore.read(sessionId)
.mapNotNull { (it.payload as? ArtifactCreatedEvent)?.artifactId?.value }
.toSet()
val cp = StageCheckpointReconciler.reconcile(expected, produced)
if (cp.passed) {
emit(sessionId, StageCheckpointPassedEvent(sessionId, stageId, expected, produced))
} else {
emit(sessionId, StageCheckpointFailedEvent(sessionId, stageId, expected, produced, cp.missing))
}
}
/**
* Entity grounding (role-reliability §3): for a stage that opted in (`ground_references`),
* check every workspace-relative file path its brief named against the filesystem. Existence
@@ -0,0 +1,23 @@
package com.correx.core.kernel.orchestration
/**
* The reconciliation of a stage's [produced] artifacts against the [expected] produces-slots of the
* locked plan. [missing] is the expected slots that were never produced; [passed] holds when none
* are missing. Extra produced artifacts beyond [expected] do not fail the checkpoint.
*/
internal data class StageCheckpoint(
val expected: Set<String>,
val produced: Set<String>,
) {
val missing: Set<String> get() = expected - produced
val passed: Boolean get() = missing.isEmpty()
}
/**
* Pure reconciler for stage-level plan checkpointing: compares the artifacts a stage produced
* against the artifacts the locked plan expected it to produce. Side-effect free and replay-safe.
*/
internal object StageCheckpointReconciler {
fun reconcile(expected: Set<String>, produced: Set<String>): StageCheckpoint =
StageCheckpoint(expected = expected, produced = produced)
}
@@ -0,0 +1,49 @@
package com.correx.core.kernel.orchestration
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 StageCheckpointReconcilerTest {
@Test
fun `all expected produced - passed with no missing`() {
val cp = StageCheckpointReconciler.reconcile(
expected = setOf("plan.json", "diff.patch"),
produced = setOf("plan.json", "diff.patch"),
)
assertTrue(cp.passed)
assertTrue(cp.missing.isEmpty())
}
@Test
fun `one missing - not passed with correct missing set`() {
val cp = StageCheckpointReconciler.reconcile(
expected = setOf("plan.json", "diff.patch"),
produced = setOf("plan.json"),
)
assertFalse(cp.passed)
assertEquals(setOf("diff.patch"), cp.missing)
}
@Test
fun `empty expected - passed`() {
val cp = StageCheckpointReconciler.reconcile(
expected = emptySet(),
produced = setOf("plan.json"),
)
assertTrue(cp.passed)
assertTrue(cp.missing.isEmpty())
}
@Test
fun `extra produced beyond expected - still passed`() {
val cp = StageCheckpointReconciler.reconcile(
expected = setOf("plan.json"),
produced = setOf("plan.json", "notes.md", "scratch.txt"),
)
assertTrue(cp.passed)
assertTrue(cp.missing.isEmpty())
}
}