From 8267eba6a576c28e6643345ef8183c02f62741a1 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 11 Nov 2026 12:25:15 +0000 Subject: [PATCH] Retain application skip reasons for repaired proposals --- docs/roadmap/remediation.md | 4 ++- .../processreport/correction_ledger.go | 10 +++++-- .../processreport/correction_ledger_test.go | 29 +++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 649f720..abca3a5 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -2,7 +2,7 @@ **Updated:** 2026-11-11 -**Status:** 1 of nine findings resolved. Findings refer to implementation revision +**Status:** 2 of nine findings resolved. Findings refer to implementation revision `aa8073c`. See the [implementation plan](implementation.md). ## Review Result @@ -75,6 +75,8 @@ defects. ### R2 — Retain Application Skip Reasons in Repaired Ledger Entries (P2) +**Completed.** The debug ledger joins the application skip reason and message by stage and proposal. Ledger and overlap tests pass. + **Affected plan:** Stage 6, item 5; Stage 8, item 3. **Evidence:** [BuildCorrectionLedger](../../internal/framework/processreport/correction_ledger.go) diff --git a/internal/framework/processreport/correction_ledger.go b/internal/framework/processreport/correction_ledger.go index a24783f..0437115 100644 --- a/internal/framework/processreport/correction_ledger.go +++ b/internal/framework/processreport/correction_ledger.go @@ -95,6 +95,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry } } seen := make(map[proposalIdentity]bool) + repairSkips := make(map[proposalIdentity]proposals.SkippedChange) stages := make(map[string]stageEvidence, len(runOutput.ModuleResults)) for _, module := range runOutput.ModuleResults { decisionsByProposal := make(map[int][]runner.ValidatorDecisionRecord) @@ -129,10 +130,12 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry }) } for _, change := range module.SkippedChanges { - if _, ok := repairs[proposalIdentity{module.ModuleInstance, change.ProposalIndex}]; ok { + identity := proposalIdentity{module.ModuleInstance, change.ProposalIndex} + if _, ok := repairs[identity]; ok { + repairSkips[identity] = change continue } - seen[proposalIdentity{module.ModuleInstance, change.ProposalIndex}] = true + seen[identity] = true entries = append(entries, CorrectionLedgerEntry{ StageIndex: module.StageIndex, StageID: module.StageID, RunID: runID, @@ -204,6 +207,9 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry entry.AppliedSegmentID = &segment entry.AppliedOriginalText = &before entry.AppliedCorrectedText = &after + } else if skipped, ok := repairSkips[identity]; ok && repair.Outcome == "approved_skipped" { + entry.DispositionReasonCode = string(skipped.SkipReason) + entry.DispositionMessage = skipped.Message } seen[identity] = true entries = append(entries, entry) diff --git a/internal/framework/processreport/correction_ledger_test.go b/internal/framework/processreport/correction_ledger_test.go index cd867ae..8afa508 100644 --- a/internal/framework/processreport/correction_ledger_test.go +++ b/internal/framework/processreport/correction_ledger_test.go @@ -226,6 +226,35 @@ func TestCorrectionLedgerCombinesRepairLineage(t *testing.T) { } } +func TestSkippedRepairLedgerKeepsApplicationReason(t *testing.T) { + const instance = "grammar_1" + original := proposals.EnrichedCorrectionProposal{ + CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: 1, OriginalText: "cat", CorrectedText: "fox"}, + ProposalMetadata: proposals.ProposalMetadata{ProposalIndex: 1, ModuleKey: "grammar", ModuleInstance: instance}, + } + revision := original + revision.Confidence = 0.99 + skip := proposals.SkippedChange{ProposalIndex: 1, SkipReason: proposals.SkipReasonMissingOriginalText, Message: "source span no longer matches"} + output := &runner.RunOutput{ + ModuleResults: []runner.ModuleResult{{ModuleInstance: instance, SkippedChanges: []proposals.SkippedChange{skip}}}, + WorkUnits: []runner.WorkUnitOutcome{{ModuleInstance: instance, Repairs: []runner.RepairOutcome{{ + ProposalIndex: 1, Outcome: "approved_skipped", Original: original, Final: revision, + Attempts: []runner.RepairAttempt{{Revision: &revision}}, + }}}}, + Candidates: []proposals.EnrichedCorrectionProposal{original}, + } + ledger := BuildCorrectionLedger(CorrectionLedgerInput{RunOutput: output}) + if len(ledger) != 1 { + t.Fatalf("ledger: %+v", ledger) + } + entry := ledger[0] + if entry.Disposition != "approved_skipped" || entry.DispositionReasonCode != string(skip.SkipReason) || + entry.DispositionMessage != skip.Message || entry.AppliedSegmentID != nil || entry.AppliedOriginalText != nil || + entry.AppliedCorrectedText != nil || len(entry.RepairAttempts) != 1 { + t.Fatalf("skipped revision: %+v", entry) + } +} + func TestInterruptedRepairLedgerKeepsEarlierDecisions(t *testing.T) { original := proposals.EnrichedCorrectionProposal{CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: 1, OriginalText: "cat", CorrectedText: "dog"}, ProposalMetadata: proposals.ProposalMetadata{ProposalIndex: 1, ModuleKey: "grammar", ModuleInstance: "grammar_1"}} revision := original