Index interrupted ledger evidence by stage and proposal

This commit is contained in:
2026-11-10 20:49:38 +00:00
parent 71a9b1f560
commit 2140314d40
2 changed files with 49 additions and 40 deletions

View File

@@ -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),

View File

@@ -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) {