feat(recovery): one bounded post-failure diagnostic before terminal FAILED (#294)
At the terminal boundary (repair ladder spent, or a non-recoverable gate exhaustion), run exactly one tool-free diagnostic inference per terminal failure fingerprint over recorded facts only. A validated — materially new, confident, recovery-stage-available — RecoveryProposal routes once into the existing recovery stage via the ticket machinery, bypassing the spent route budget but bounded by a one-diagnosis-per-fingerprint dedupe so no loop is possible. Otherwise the run stays terminal FAILED (safe degrade when no diagnoser is wired). - New PostFailureDiagnosedEvent + nested RecoveryProposal (registered in eventModule); every observation/proposal/decision/route recorded for replay. - PostFailureDiagnoser seam (nullable, mirrors SalvageJudge) + DiagnosisInput built from the event log only (no fresh workspace observation). - diagnosisMinConfidence tuning knob. - Hooked at both terminal boundaries: routeToRecovery ladder-exhausted and decideGateExhaustion. Tests: RecoveryRoutingTest (route-once-then-bounded, low-confidence stays terminal), EventsTest serialization round-trip (proposal + null). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+46
@@ -3,6 +3,8 @@ package com.correx.testing.contracts.model.orchestration
|
||||
import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.PostFailureDiagnosedEvent
|
||||
import com.correx.core.events.events.RecoveryProposal
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||
import com.correx.core.events.events.WorkflowFailedEvent
|
||||
@@ -45,6 +47,50 @@ class EventsTest {
|
||||
assertEquals(event, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PostFailureDiagnosedEvent survives round-trip with a proposal`() {
|
||||
// Guards the #294 event AND its nested RecoveryProposal against the silent-registration trap.
|
||||
val event: EventPayload = PostFailureDiagnosedEvent(
|
||||
sessionId = SessionId("s1"),
|
||||
stageId = StageId("stage-a"),
|
||||
gate = "execution",
|
||||
fingerprint = "fp-abc",
|
||||
proposal = RecoveryProposal(
|
||||
diagnosis = "type mismatch",
|
||||
citedEvidence = "App.tsx: TS2322",
|
||||
recoveryAction = "narrow the return type",
|
||||
expectedFingerprint = "fp-xyz",
|
||||
confidence = 0.9,
|
||||
noRecovery = false,
|
||||
),
|
||||
decision = "ROUTE",
|
||||
routed = true,
|
||||
)
|
||||
|
||||
val json = eventJson.encodeToString(EventPayload.serializer(), event)
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), json)
|
||||
|
||||
assertEquals(event, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PostFailureDiagnosedEvent survives round-trip with a null proposal`() {
|
||||
val event: EventPayload = PostFailureDiagnosedEvent(
|
||||
sessionId = SessionId("s1"),
|
||||
stageId = StageId("stage-a"),
|
||||
gate = "execution",
|
||||
fingerprint = "fp-abc",
|
||||
proposal = null,
|
||||
decision = "TERMINAL_NO_PROPOSAL",
|
||||
routed = false,
|
||||
)
|
||||
|
||||
val json = eventJson.encodeToString(EventPayload.serializer(), event)
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), json)
|
||||
|
||||
assertEquals(event, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WorkflowFailedEvent survives round-trip`() {
|
||||
val event: EventPayload = WorkflowFailedEvent(
|
||||
|
||||
Reference in New Issue
Block a user