Make debug evidence opt-in and retain all candidates

This commit is contained in:
2026-09-24 17:41:10 +00:00
parent a80eed4745
commit 87b8d9ad5c
10 changed files with 246 additions and 213 deletions

View File

@@ -1,6 +1,7 @@
package processreport
import (
"fmt"
"path/filepath"
"sort"
@@ -9,10 +10,11 @@ import (
)
const (
correctionDispositionApplied = "applied"
correctionDispositionRejected = "rejected"
correctionDispositionSkipped = "skipped"
correctionDispositionFailed = "failed"
correctionDispositionApplied = "applied"
correctionDispositionRejected = "rejected"
correctionDispositionSkipped = "skipped"
correctionDispositionFailed = "failed"
correctionDispositionInterrupted = "interrupted"
)
type CorrectionLedgerInput struct {
@@ -55,6 +57,8 @@ 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) }
for _, module := range runOutput.ModuleResults {
decisionsByProposal := make(map[int][]runner.ValidatorDecisionRecord)
for _, decision := range module.ValidatorDecisions {
@@ -62,6 +66,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
}
for _, change := range module.AppliedChanges {
seen[identity(module.ModuleInstance, change.ProposalIndex)] = true
entries = append(entries, CorrectionLedgerEntry{
RunID: runID,
ModuleKey: module.ModuleKey,
@@ -78,6 +83,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
})
}
for _, change := range module.SkippedChanges {
seen[identity(module.ModuleInstance, change.ProposalIndex)] = true
entries = append(entries, CorrectionLedgerEntry{
RunID: runID,
ModuleKey: module.ModuleKey,
@@ -95,6 +101,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
})
}
for _, rejection := range module.ValidatorRejected {
seen[identity(module.ModuleInstance, rejection.ProposalIndex)] = true
entries = append(entries, CorrectionLedgerEntry{
RunID: runID,
ModuleKey: module.ModuleKey,
@@ -122,6 +129,34 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
})
}
}
for _, candidate := range runOutput.Candidates {
if seen[identity(candidate.ModuleInstance, candidate.ProposalIndex)] {
continue
}
var decisions []runner.ValidatorDecisionRecord
policy := ""
for _, module := range runOutput.ModuleResults {
if module.ModuleInstance != candidate.ModuleInstance {
continue
}
policy = string(module.ReplacementPolicy)
for _, decision := range module.ValidatorDecisions {
if decision.ProposalIndex == candidate.ProposalIndex {
decisions = append(decisions, decision)
}
}
break
}
entries = append(entries, CorrectionLedgerEntry{
RunID: runID, ModuleKey: candidate.ModuleKey, ModuleInstance: candidate.ModuleInstance,
ProposalIndex: candidate.ProposalIndex, SegmentID: candidate.TargetSegmentID,
OriginalText: candidate.OriginalText, ProposedCorrectedText: candidate.CorrectedText,
ReplacementPolicy: policy, Disposition: correctionDispositionInterrupted,
DispositionReasonCode: "not_applied", DispositionMessage: "candidate did not reach application",
DeterministicValidatorResults: filterLedgerDecisions(decisions, false),
LLMValidatorResults: filterLedgerDecisions(decisions, true),
})
}
sort.SliceStable(entries, func(i, j int) bool {
if entries[i].ModuleInstance != entries[j].ModuleInstance {

View File

@@ -126,3 +126,25 @@ func TestHasSkippedCorrectionsIncludesApplicationSkipsAndValidatorRejections(t *
t.Fatal("validator rejections should count as skipped corrections")
}
}
func TestCorrectionLedgerIncludesUnappliedCandidateAfterFatalValidation(t *testing.T) {
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, ReasonCode: "transport_failed"}}}},
}
ledger := BuildCorrectionLedger(CorrectionLedgerInput{RunOutput: output})
if len(ledger) != 2 {
t.Fatalf("missing candidate: %+v", ledger)
}
for _, entry := range ledger {
if entry.ProposalIndex != 2 {
continue
}
if entry.Disposition != correctionDispositionInterrupted || entry.OriginalText != "old" || len(entry.LLMValidatorResults) != 1 {
t.Fatalf("candidate evidence lost: %+v", entry)
}
return
}
t.Fatal("candidate missing")
}