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:
@@ -0,0 +1,42 @@
|
||||
package com.correx.core.events.events
|
||||
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Per-stage reconciliation of a stage's produced artifacts against the locked plan's produces-slots,
|
||||
* recorded so plan adherence is observable and replayable. Emitted only on plan-driven (phase-2)
|
||||
* runs — a session whose log contains an [ExecutionPlanLockedEvent]; non-plan workflows emit nothing.
|
||||
*
|
||||
* The *passed* variant means every expected slot was produced: [producedArtifacts] is a superset of
|
||||
* [expectedArtifacts] (extra artifacts beyond the expected set do not fail the checkpoint).
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("StageCheckpointPassed")
|
||||
data class StageCheckpointPassedEvent(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val expectedArtifacts: Set<String>,
|
||||
val producedArtifacts: Set<String>,
|
||||
) : EventPayload
|
||||
|
||||
/**
|
||||
* Per-stage reconciliation of a stage's produced artifacts against the locked plan's produces-slots,
|
||||
* recorded so plan adherence is observable and replayable. Emitted only on plan-driven (phase-2)
|
||||
* runs — a session whose log contains an [ExecutionPlanLockedEvent]; non-plan workflows emit nothing.
|
||||
*
|
||||
* The *failed* variant accompanies the stage halt owned by `verifyProduces` and surfaces *why* the
|
||||
* plan diverged: [missingArtifacts] are the expected slots ([expectedArtifacts] minus
|
||||
* [producedArtifacts]) the stage never produced.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("StageCheckpointFailed")
|
||||
data class StageCheckpointFailedEvent(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val expectedArtifacts: Set<String>,
|
||||
val producedArtifacts: Set<String>,
|
||||
val missingArtifacts: Set<String>,
|
||||
) : EventPayload
|
||||
@@ -45,6 +45,8 @@ import com.correx.core.events.events.RepoMapComputedEvent
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
import com.correx.core.events.events.WorkspaceStateObservedEvent
|
||||
import com.correx.core.events.events.RiskAssessedEvent
|
||||
import com.correx.core.events.events.StageCheckpointFailedEvent
|
||||
import com.correx.core.events.events.StageCheckpointPassedEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.events.PreemptRedirectBlockedEvent
|
||||
import com.correx.core.events.events.PreemptRedirectEvent
|
||||
@@ -131,6 +133,8 @@ val eventModule = SerializersModule {
|
||||
subclass(IdeaCapturedEvent::class)
|
||||
subclass(IdeaDiscardedEvent::class)
|
||||
subclass(CritiqueOutcomeCorrelatedEvent::class)
|
||||
subclass(StageCheckpointPassedEvent::class)
|
||||
subclass(StageCheckpointFailedEvent::class)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.correx.core.events.serialization
|
||||
|
||||
import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.events.StageCheckpointFailedEvent
|
||||
import com.correx.core.events.events.StageCheckpointPassedEvent
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class StageCheckpointEventSerializationTest {
|
||||
|
||||
@Test
|
||||
fun `StageCheckpointPassedEvent round-trips as polymorphic EventPayload`() {
|
||||
val sample: EventPayload = StageCheckpointPassedEvent(
|
||||
sessionId = SessionId("s"),
|
||||
stageId = StageId("plan"),
|
||||
expectedArtifacts = setOf("plan.json"),
|
||||
producedArtifacts = setOf("plan.json", "notes.md"),
|
||||
)
|
||||
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
|
||||
assertTrue(encoded.contains("\"type\":\"StageCheckpointPassed\""), "SerialName must be present: $encoded")
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
|
||||
assertEquals(sample, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `StageCheckpointFailedEvent round-trips as polymorphic EventPayload`() {
|
||||
val sample: EventPayload = StageCheckpointFailedEvent(
|
||||
sessionId = SessionId("s"),
|
||||
stageId = StageId("plan"),
|
||||
expectedArtifacts = setOf("plan.json", "diff.patch"),
|
||||
producedArtifacts = setOf("plan.json"),
|
||||
missingArtifacts = setOf("diff.patch"),
|
||||
)
|
||||
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
|
||||
assertTrue(encoded.contains("\"type\":\"StageCheckpointFailed\""), "SerialName must be present: $encoded")
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
|
||||
assertEquals(sample, decoded)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user