From 2140314d40320fd5974007acdd69ef1c430a0ce1 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 10 Nov 2026 20:49:38 +0000 Subject: [PATCH] Index interrupted ledger evidence by stage and proposal --- .../processreport/correction_ledger.go | 51 +++++++++---------- .../processreport/correction_ledger_test.go | 38 +++++++++----- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/internal/framework/processreport/correction_ledger.go b/internal/framework/processreport/correction_ledger.go index a32ce76..8ce32de 100644 --- a/internal/framework/processreport/correction_ledger.go +++ b/internal/framework/processreport/correction_ledger.go @@ -1,7 +1,6 @@ package processreport import ( - "fmt" "path/filepath" "sort" @@ -62,16 +61,31 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry } entries := make([]CorrectionLedgerEntry, 0) - seen := make(map[string]bool) - identity := func(instance string, index int) string { return instance + "\x00" + fmt.Sprint(index) } + type proposalIdentity struct { + instance string + index int + } + type stageEvidence struct { + index int + id *string + policy string + decisions map[int][]runner.ValidatorDecisionRecord + } + seen := make(map[proposalIdentity]bool) + stages := make(map[string]stageEvidence, len(runOutput.ModuleResults)) for _, module := range runOutput.ModuleResults { decisionsByProposal := make(map[int][]runner.ValidatorDecisionRecord) for _, decision := range module.ValidatorDecisions { decisionsByProposal[decision.ProposalIndex] = append(decisionsByProposal[decision.ProposalIndex], decision) } + if _, exists := stages[module.ModuleInstance]; !exists { + stages[module.ModuleInstance] = stageEvidence{ + index: module.StageIndex, id: module.StageID, policy: string(module.ReplacementPolicy), decisions: decisionsByProposal, + } + } for _, change := range module.AppliedChanges { - seen[identity(module.ModuleInstance, change.ProposalIndex)] = true + seen[proposalIdentity{module.ModuleInstance, change.ProposalIndex}] = true entries = append(entries, CorrectionLedgerEntry{ StageIndex: module.StageIndex, StageID: module.StageID, RunID: runID, @@ -89,7 +103,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry }) } for _, change := range module.SkippedChanges { - seen[identity(module.ModuleInstance, change.ProposalIndex)] = true + seen[proposalIdentity{module.ModuleInstance, change.ProposalIndex}] = true entries = append(entries, CorrectionLedgerEntry{ StageIndex: module.StageIndex, StageID: module.StageID, RunID: runID, @@ -112,7 +126,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry if rejection.Disposition == validators.DispositionUnavailable { disposition = correctionDispositionUnavailable } - seen[identity(module.ModuleInstance, rejection.ProposalIndex)] = true + seen[proposalIdentity{module.ModuleInstance, rejection.ProposalIndex}] = true entries = append(entries, CorrectionLedgerEntry{ StageIndex: module.StageIndex, StageID: module.StageID, RunID: runID, @@ -143,31 +157,16 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry } } for _, candidate := range runOutput.Candidates { - if seen[identity(candidate.ModuleInstance, candidate.ProposalIndex)] { + if seen[proposalIdentity{candidate.ModuleInstance, candidate.ProposalIndex}] { continue } - var decisions []runner.ValidatorDecisionRecord - var stageIndex int - var stageID *string - policy := "" - for _, module := range runOutput.ModuleResults { - if module.ModuleInstance != candidate.ModuleInstance { - continue - } - stageIndex, stageID = module.StageIndex, module.StageID - policy = string(module.ReplacementPolicy) - for _, decision := range module.ValidatorDecisions { - if decision.ProposalIndex == candidate.ProposalIndex { - decisions = append(decisions, decision) - } - } - break - } + stage := stages[candidate.ModuleInstance] + decisions := stage.decisions[candidate.ProposalIndex] entries = append(entries, CorrectionLedgerEntry{ - StageIndex: stageIndex, StageID: stageID, RunID: runID, ModuleKey: candidate.ModuleKey, ModuleInstance: candidate.ModuleInstance, + StageIndex: stage.index, StageID: stage.id, RunID: runID, ModuleKey: candidate.ModuleKey, ModuleInstance: candidate.ModuleInstance, ProposalIndex: candidate.ProposalIndex, SegmentID: candidate.TargetSegmentID, OriginalText: candidate.OriginalText, ProposedCorrectedText: &candidate.CorrectedText, - ReplacementPolicy: policy, Disposition: correctionDispositionInterrupted, + ReplacementPolicy: stage.policy, Disposition: correctionDispositionInterrupted, DispositionReasonCode: "not_applied", DispositionMessage: "candidate did not reach application", DeterministicValidatorResults: filterLedgerDecisions(decisions, false), LLMValidatorResults: filterLedgerDecisions(decisions, true), diff --git a/internal/framework/processreport/correction_ledger_test.go b/internal/framework/processreport/correction_ledger_test.go index 8bfae66..ed98a96 100644 --- a/internal/framework/processreport/correction_ledger_test.go +++ b/internal/framework/processreport/correction_ledger_test.go @@ -116,25 +116,35 @@ func TestBuildCorrectionLedgerPreservesDispositionPolicy(t *testing.T) { } func TestCorrectionLedgerIncludesUnappliedCandidateAfterFatalValidation(t *testing.T) { + stageID := "second grammar pass" output := &runner.RunOutput{ - Candidates: []proposals.EnrichedCorrectionProposal{{CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: 7, OriginalText: "old", CorrectedText: "new"}, ProposalMetadata: proposals.ProposalMetadata{ModuleKey: "grammar", ModuleInstance: "grammar", ProposalIndex: 2}}}, - ModuleResults: []runner.ModuleResult{{ModuleKey: "grammar", ModuleInstance: "grammar", Status: runner.ModuleStatusFailed, - ValidatorDecisions: []runner.ValidatorDecisionRecord{{ValidatorName: "spoken_form_plausibility", ProposalIndex: 2, Approved: false, Disposition: validators.DispositionUnavailable, ReasonCode: "transport_failed"}}}}, + Candidates: []proposals.EnrichedCorrectionProposal{ + {CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: 7, OriginalText: "old", CorrectedText: "other"}, ProposalMetadata: proposals.ProposalMetadata{ModuleKey: "grammar", ModuleInstance: "grammar_2", ProposalIndex: 3}}, + {CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: 7, OriginalText: "old", CorrectedText: "new"}, ProposalMetadata: proposals.ProposalMetadata{ModuleKey: "grammar", ModuleInstance: "grammar_2", ProposalIndex: 2}}, + }, + ModuleResults: []runner.ModuleResult{ + {ModuleKey: "grammar", ModuleInstance: "grammar_1", Status: runner.ModuleStatusSuccess, + AppliedChanges: []proposals.AppliedChange{{ProposalIndex: 2, TargetSegmentID: 7, OriginalText: "before", CorrectedText: "old"}}, + ValidatorDecisions: []runner.ValidatorDecisionRecord{{ValidatorName: "editorial_review", ProposalIndex: 2, Approved: true, Disposition: validators.DispositionApproved}}}, + {StageIndex: 1, StageID: &stageID, ModuleKey: "grammar", ModuleInstance: "grammar_2", + ReplacementPolicy: proposals.ReplacementPolicyRequireUnique, Status: runner.ModuleStatusFailed, + ValidatorDecisions: []runner.ValidatorDecisionRecord{{ValidatorName: "spoken_form_plausibility", ProposalIndex: 2, Disposition: validators.DispositionUnavailable, ReasonCode: "transport_failed"}}}, + }, } ledger := BuildCorrectionLedger(CorrectionLedgerInput{RunOutput: output}) - if len(ledger) != 2 { - t.Fatalf("missing candidate: %+v", ledger) + if len(ledger) != 4 || ledger[0].Disposition != correctionDispositionApplied || ledger[1].Disposition != correctionDispositionFailed || + ledger[2].ProposalIndex != 2 || ledger[3].ProposalIndex != 3 { + t.Fatalf("ledger order or entries changed: %+v", ledger) } - for _, entry := range ledger { - if entry.ProposalIndex != 2 { - continue - } - if entry.Disposition != correctionDispositionInterrupted || entry.OriginalText != "old" || len(entry.LLMValidatorResults) != 1 || entry.LLMValidatorResults[0].Disposition != validators.DispositionUnavailable { - t.Fatalf("candidate evidence lost: %+v", entry) - } - return + entry := ledger[2] + if entry.Disposition != correctionDispositionInterrupted || entry.StageIndex != 1 || entry.StageID == nil || *entry.StageID != stageID || + entry.ModuleInstance != "grammar_2" || entry.ReplacementPolicy != string(proposals.ReplacementPolicyRequireUnique) || + entry.OriginalText != "old" || len(entry.LLMValidatorResults) != 1 || entry.LLMValidatorResults[0].Disposition != validators.DispositionUnavailable { + t.Fatalf("candidate evidence lost: %+v", entry) + } + if ledger[3].Disposition != correctionDispositionInterrupted || len(ledger[3].LLMValidatorResults) != 0 { + t.Fatalf("decisions leaked between candidates: %+v", ledger[3]) } - t.Fatal("candidate missing") } func TestCorrectionLedgerSerializesEmptyReplacementByDisposition(t *testing.T) {