From 6b724fd3ccf6a17045933d8495619ac8b2ebc053 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 11 Nov 2026 12:29:42 +0000 Subject: [PATCH] Index applied changes for repair outcomes --- docs/roadmap/remediation.md | 4 +++- internal/framework/runner/repair_test.go | 25 +++++++++++++++--------- internal/framework/runner/runner.go | 14 ++++++++----- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 9d5d6a8..365731b 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -2,7 +2,7 @@ **Updated:** 2026-11-11 -**Status:** 5 of nine findings resolved. Findings refer to implementation revision +**Status:** 6 of nine findings resolved. Findings refer to implementation revision `aa8073c`. See the [implementation plan](implementation.md). ## Review Result @@ -216,6 +216,8 @@ filtering evidence for candidates that remain excluded. ### R6 — Index Application Results for Repair Outcomes (P3) +**Completed.** Application outcomes use a stage-local index built once. The runner test covers an original approval, a skipped revision, and an applied revision in the same stage. + **Affected plan:** Stage 6, item 5, and Stage 7, item 3. **Evidence:** After applying the stage, [Runner.Run](../../internal/framework/runner/runner.go#L244) diff --git a/internal/framework/runner/repair_test.go b/internal/framework/runner/repair_test.go index c2a1997..3c0a334 100644 --- a/internal/framework/runner/repair_test.go +++ b/internal/framework/runner/repair_test.go @@ -114,22 +114,29 @@ type repairModule struct{ outcomeModule } func (m repairModule) RepairPromptID() string { return "repair" } func TestRunnerAppliesRevisionsInOriginalOrder(t *testing.T) { - sectionIndex := 0 - candidates := []proposals.CorrectionProposal{{TargetSegmentID: 1, OriginalText: "cat", CorrectedText: "dog", Confidence: .9}, {TargetSegmentID: 1, OriginalText: "cat", CorrectedText: "fox", Confidence: .1}} - client := &repairClient{outputs: []string{`{"proposal_ref":2,"action":"revise","correction":{"id":1,"original_text":"cat","corrected_text":"fox","confidence":0.9}}`}} + candidates := []proposals.CorrectionProposal{{TargetSegmentID: 1, OriginalText: "cat", CorrectedText: "dog", Confidence: .9}, {TargetSegmentID: 1, OriginalText: "cat", CorrectedText: "fox", Confidence: .1}, {TargetSegmentID: 1, OriginalText: "bird", CorrectedText: "owl", Confidence: .1}} + client := &repairClient{outputs: []string{`{"proposal_ref":2,"action":"revise","correction":{"id":1,"original_text":"cat","corrected_text":"fox","confidence":0.9}}`, + `{"proposal_ref":3,"action":"revise","correction":{"id":1,"original_text":"bird","corrected_text":"owl","confidence":0.9}}`, + }} module := repairModule{outcomeModule{fakeModule: fakeModule{key: "grammar", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{validators.ProposalShapeValidator{}, validators.NoEffectValidator{}, validators.OriginalTextPresenceValidator{}, validators.ConfidenceThresholdValidator{}}}, proposeResult: func(req contracts.ProposalRequest) (contracts.ProposalResult, error) { - return contracts.ProposalResult{Proposals: candidates, SegmentReferences: []int{1, 1}, Exchange: &structuredoutput.Exchange{PromptID: "proposal"}}, nil + return contracts.ProposalResult{Proposals: candidates, SegmentReferences: []int{1, 1, 1}, Exchange: &structuredoutput.Exchange{PromptID: "proposal"}}, nil }}} runner := New(fakeFactory{modules: map[string]contracts.TranscriptModule{"grammar": module}}) - out, err := runner.Run(context.Background(), RunInput{Transcript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "cat"}}}, ModuleSpecs: []contracts.ModuleRunSpec{{ModuleKey: "grammar", InstanceName: "grammar", RepairClient: client, Settings: config.Stage{ConfidenceThreshold: .8, LLM: config.LLM{CorrectionRepairAttempts: 1}}}}}) + out, err := runner.Run(context.Background(), RunInput{Transcript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "cat and bird"}}}, ModuleSpecs: []contracts.ModuleRunSpec{{ModuleKey: "grammar", InstanceName: "grammar", RepairClient: client, Settings: config.Stage{ConfidenceThreshold: .8, LLM: config.LLM{CorrectionRepairAttempts: 1}}}}}) if err != nil { t.Fatal(err) } - if out.FinalTranscript.Segments[0].Text != "dog" || len(out.WorkUnits) != 1 || len(out.WorkUnits[0].Repairs) != 1 || out.WorkUnits[0].Repairs[0].Outcome != "approved_skipped" || out.WorkUnits[0].CorrectionsApplied != 1 || len(out.ModuleResults[0].SkippedChanges) != 1 || out.ModuleResults[0].SkippedChanges[0].ProposalIndex != 1 { - t.Fatalf("output=%+v section=%d", out, sectionIndex) + if out.FinalTranscript.Segments[0].Text != "dog and owl" || len(out.WorkUnits) != 1 || len(out.WorkUnits[0].Repairs) != 2 || out.WorkUnits[0].Repairs[0].Outcome != "approved_skipped" || out.WorkUnits[0].Repairs[1].Outcome != "applied" || out.WorkUnits[0].CorrectionsApplied != 2 || len(out.ModuleResults[0].SkippedChanges) != 1 || out.ModuleResults[0].SkippedChanges[0].ProposalIndex != 1 { + t.Fatalf("output=%+v", out) } - if len(out.WorkUnits[0].Operations) != 1 || out.WorkUnits[0].Operations[0].Reference.Kind != outcomes.OperationCorrectionRepair || len(out.WorkUnits[0].Operations[0].RecoveryReasons) != 1 || out.WorkUnits[0].Operations[0].RecoveryReasons[0] != outcomes.RecoveryCorrectionRevised { - t.Fatalf("repair operations: %+v", out.WorkUnits[0].Operations) + operations := out.WorkUnits[0].Operations + if len(operations) != 2 { + t.Fatalf("repair operations: %+v", operations) + } + for _, op := range operations { + if op.Reference.Kind != outcomes.OperationCorrectionRepair || len(op.RecoveryReasons) != 1 || op.RecoveryReasons[0] != outcomes.RecoveryCorrectionRevised { + t.Fatalf("repair operation: %+v", op) + } } } diff --git a/internal/framework/runner/runner.go b/internal/framework/runner/runner.go index c505797..62ae30f 100644 --- a/internal/framework/runner/runner.go +++ b/internal/framework/runner/runner.go @@ -241,6 +241,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) { unit.State = outcomes.WorkCompleted unit.CorrectionsApplied = appliedBySection[sectionPos] } + var appliedIndices map[int]struct{} for i := range workUnits[moduleIndex*len(sections) : (moduleIndex+1)*len(sections)] { unit := &workUnits[moduleIndex*len(sections)+i] for j := range unit.Repairs { @@ -248,13 +249,16 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) { if branch.Outcome != "approved" { continue } - branch.Outcome = "approved_skipped" - for _, change := range applied { - if change.ProposalIndex == branch.ProposalIndex { - branch.Outcome = "applied" - break + if appliedIndices == nil { + appliedIndices = make(map[int]struct{}, len(applied)) + for _, change := range applied { + appliedIndices[change.ProposalIndex] = struct{}{} } } + branch.Outcome = "approved_skipped" + if _, ok := appliedIndices[branch.ProposalIndex]; ok { + branch.Outcome = "applied" + } } } attributeApplyFiltering(sections, pipelineResult.Approved, skipped, policy, workUnits[moduleIndex*len(sections):(moduleIndex+1)*len(sections)])