diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index d9db35b..9d5d6a8 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -2,7 +2,7 @@ **Updated:** 2026-11-11 -**Status:** 4 of nine findings resolved. Findings refer to implementation revision +**Status:** 5 of nine findings resolved. Findings refer to implementation revision `aa8073c`. See the [implementation plan](implementation.md). ## Review Result @@ -184,6 +184,8 @@ scanning and allocation in the feedback path. ### R5 — Index Initial Findings and Compact Filtering Once (P3) +**Completed.** Initial findings are grouped by proposal in validator order. Superseded filtering records are removed once, including on partial returns. Runner and report tests pass. + **Affected plan:** Stage 6, items 1 and 5. **Evidence:** [repairCandidates](../../internal/framework/runner/repair.go#L59) diff --git a/internal/framework/runner/repair.go b/internal/framework/runner/repair.go index 7986f48..3e5db13 100644 --- a/internal/framework/runner/repair.go +++ b/internal/framework/runner/repair.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "gitea.maximumdirect.net/eric/audita/v3/internal/core/redaction" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/contracts" @@ -61,29 +62,44 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput, if input.Spec.Settings.LLM.CorrectionRepairAttempts <= 0 { return initial, nil, nil } + initialFindings := make(map[int][]ValidatorRejectedChange) + for _, decision := range initial.rejected { + initialFindings[decision.ProposalIndex] = append(initialFindings[decision.ProposalIndex], decision) + } + superseded := make(map[int]bool) var branches []RepairOutcome + finish := func(err error) (validateSectionCandidatesResult, []RepairOutcome, error) { + if len(superseded) > 0 { + initial.filtered = slices.DeleteFunc(initial.filtered, func(item outcomes.FilteredProposal) bool { + return superseded[item.ProposalIndex] + }) + } + return initial, branches, err + } + for position, original := range input.SectionEnriched { - if !actionable(original.ProposalIndex, initial.rejected) { + rejected := initialFindings[original.ProposalIndex] + if !actionable(original.ProposalIndex, rejected) { continue } branch := RepairOutcome{ProposalIndex: original.ProposalIndex, Original: original, Final: original, Outcome: "interrupted"} if err := ctx.Err(); err != nil { branches = append(branches, branch) - return initial, branches, err + return finish(err) } if input.Spec.RepairClient == nil || input.Exchange == nil || promptID == "" { branches = append(branches, branch) - return initial, branches, fmt.Errorf("correction repair setup is unavailable") + return finish(fmt.Errorf("correction repair setup is unavailable")) } localRef := 0 if position < len(references) { localRef = references[position] - } else if input.WorkingTranscript != nil { - for i, s := range input.WorkingTranscript.Segments { - if s.ID == original.TargetSegmentID { - localRef = i + 1 - break - } + } else { + if input.Section == nil { + input.Section = validators.NewSectionLookup(input.WorkingTranscript) + } + if pos, ok := input.Section.Position(original.TargetSegmentID); ok { + localRef = pos + 1 } } current := original @@ -95,13 +111,13 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput, } return findings } - findings := safeFindings(initial.rejected) + findings := safeFindings(rejected) replay := &structuredoutput.Replay{Exchange: input.Exchange} for attempt := 0; attempt < input.Spec.Settings.LLM.CorrectionRepairAttempts; attempt++ { payload, err := (proposal_generation.RepairRequest{ProposalRef: position + 1, Correction: proposal_generation.StructuredCorrectionProposal{SegmentReference: localRef, OriginalText: current.OriginalText, CorrectedText: current.CorrectedText, Confidence: current.Confidence}, Findings: findings}).JSON() if err != nil { branches = append(branches, branch) - return initial, branches, err + return finish(err) } var response proposal_generation.RepairResponse proposalIndex, attemptNumber := original.ProposalIndex, attempt+1 @@ -145,7 +161,7 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput, break } branches = append(branches, branch) - return initial, branches, callErr + return finish(callErr) } decoded, err := proposal_generation.DecodeRepairResponse(completion.Content, position+1) if err != nil { @@ -197,26 +213,21 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput, initial.operations = append(initial.operations, validated.operations...) if validationErr != nil { branches = append(branches, branch) - return initial, branches, validationErr + return finish(validationErr) } if len(validated.approved) == 1 { branch.Attempts[len(branch.Attempts)-1].Approved = true branch.Outcome = "approved" initial.approved = append(initial.approved, current) - retained := initial.filtered[:0] - for _, item := range initial.filtered { - if item.ProposalIndex != original.ProposalIndex { - retained = append(retained, item) - } - } - initial.filtered = retained + superseded[original.ProposalIndex] = true break } - findings = safeFindings(validated.rejected) if actionable(original.ProposalIndex, validated.rejected) { if attempt+1 == input.Spec.Settings.LLM.CorrectionRepairAttempts { branch.Outcome = "exhausted" + break } + findings = safeFindings(validated.rejected) continue } branch.Outcome = "unavailable" @@ -230,7 +241,7 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput, } branches = append(branches, branch) } - return initial, branches, nil + return finish(nil) } func repairOperations(input validateSectionCandidatesInput, branches []RepairOutcome) []outcomes.Operation {