Preserve empty deletion spans in debug ledger

This commit is contained in:
2026-09-25 02:52:09 +00:00
parent 93f62c9095
commit 2a58db04c2
5 changed files with 78 additions and 12 deletions

View File

@@ -29,8 +29,8 @@ type CorrectionLedgerEntry struct {
ProposalIndex int `json:"proposal_index"`
SegmentID int `json:"segment_id,omitempty"`
OriginalText string `json:"original_text,omitempty"`
ProposedCorrectedText string `json:"proposed_corrected_text,omitempty"`
AppliedCorrectedText string `json:"applied_corrected_text,omitempty"`
ProposedCorrectedText *string `json:"proposed_corrected_text,omitempty"`
AppliedCorrectedText *string `json:"applied_corrected_text,omitempty"`
ReplacementPolicy string `json:"replacement_policy,omitempty"`
Disposition string `json:"disposition"`
DispositionReasonCode string `json:"disposition_reason_code,omitempty"`
@@ -74,8 +74,8 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
ProposalIndex: change.ProposalIndex,
SegmentID: change.TargetSegmentID,
OriginalText: change.OriginalText,
ProposedCorrectedText: change.CorrectedText,
AppliedCorrectedText: change.CorrectedText,
ProposedCorrectedText: &change.CorrectedText,
AppliedCorrectedText: &change.CorrectedText,
ReplacementPolicy: string(module.ReplacementPolicy),
Disposition: correctionDispositionApplied,
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false),
@@ -91,7 +91,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
ProposalIndex: change.ProposalIndex,
SegmentID: change.TargetSegmentID,
OriginalText: change.OriginalText,
ProposedCorrectedText: change.CorrectedText,
ProposedCorrectedText: &change.CorrectedText,
ReplacementPolicy: string(module.ReplacementPolicy),
Disposition: correctionDispositionSkipped,
DispositionReasonCode: string(change.SkipReason),
@@ -109,7 +109,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
ProposalIndex: rejection.ProposalIndex,
SegmentID: rejection.TargetSegmentID,
OriginalText: rejection.OriginalText,
ProposedCorrectedText: rejection.CorrectedText,
ProposedCorrectedText: &rejection.CorrectedText,
ReplacementPolicy: string(module.ReplacementPolicy),
Disposition: correctionDispositionRejected,
DispositionReasonCode: rejection.ReasonCode,
@@ -150,7 +150,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
entries = append(entries, CorrectionLedgerEntry{
RunID: runID, ModuleKey: candidate.ModuleKey, ModuleInstance: candidate.ModuleInstance,
ProposalIndex: candidate.ProposalIndex, SegmentID: candidate.TargetSegmentID,
OriginalText: candidate.OriginalText, ProposedCorrectedText: candidate.CorrectedText,
OriginalText: candidate.OriginalText, ProposedCorrectedText: &candidate.CorrectedText,
ReplacementPolicy: policy, Disposition: correctionDispositionInterrupted,
DispositionReasonCode: "not_applied", DispositionMessage: "candidate did not reach application",
DeterministicValidatorResults: filterLedgerDecisions(decisions, false),

View File

@@ -1,6 +1,7 @@
package processreport
import (
"encoding/json"
"testing"
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
@@ -40,7 +41,7 @@ func TestBuildCorrectionLedgerClassifiesValidatorDecisionsFromCanonicalMetadata(
t.Fatalf("expected one ledger entry, got %d", len(ledger))
}
entry := ledger[0]
if entry.RunID != "audita-run-id" || entry.Disposition != "applied" || entry.AppliedCorrectedText != "Jesters" {
if entry.RunID != "audita-run-id" || entry.Disposition != "applied" || entry.AppliedCorrectedText == nil || *entry.AppliedCorrectedText != "Jesters" {
t.Fatalf("unexpected applied ledger entry: %+v", entry)
}
if len(entry.DeterministicValidatorResults) != 1 || entry.DeterministicValidatorResults[0].ValidatorKey != "proposal_shape" {
@@ -103,7 +104,7 @@ func TestBuildCorrectionLedgerPreservesDispositionPolicy(t *testing.T) {
t.Fatalf("unexpected skipped ledger entry: %+v", byDisposition["skipped"])
}
if byDisposition["rejected"].DispositionReasonCode != "protected_term" ||
byDisposition["rejected"].ProposedCorrectedText != "after" {
byDisposition["rejected"].ProposedCorrectedText == nil || *byDisposition["rejected"].ProposedCorrectedText != "after" {
t.Fatalf("unexpected rejected ledger entry: %+v", byDisposition["rejected"])
}
if byDisposition["failed"].DispositionReasonCode != "module_failed" ||
@@ -133,3 +134,51 @@ func TestCorrectionLedgerIncludesUnappliedCandidateAfterFatalValidation(t *testi
}
t.Fatal("candidate missing")
}
func TestCorrectionLedgerSerializesEmptyReplacementByDisposition(t *testing.T) {
output := &runner.RunOutput{
ModuleResults: []runner.ModuleResult{
{
ModuleKey: "spoken_word", ModuleInstance: "spoken_word", ReplacementPolicy: proposals.ReplacementPolicyRequireUnique,
AppliedChanges: []proposals.AppliedChange{{ProposalIndex: 0, TargetSegmentID: 1, OriginalText: "uh ", CorrectedText: ""}},
ValidatorRejected: []runner.ValidatorRejectedChange{{ProposalIndex: 1, TargetSegmentID: 2, OriginalText: "uh ", CorrectedText: "", ReasonCode: "llm_rejected"}},
SkippedChanges: []proposals.SkippedChange{{ProposalIndex: 2, TargetSegmentID: 3, OriginalText: "uh ", CorrectedText: "", SkipReason: proposals.SkipReasonEmptyResultingText}},
},
{ModuleKey: "grammar", ModuleInstance: "grammar", Status: runner.ModuleStatusFailed, ErrorMessage: "failed"},
},
Candidates: []proposals.EnrichedCorrectionProposal{{CorrectionProposal: proposals.CorrectionProposal{TargetSegmentID: 4, OriginalText: "uh ", CorrectedText: ""}, ProposalMetadata: proposals.ProposalMetadata{ModuleKey: "spoken_word", ModuleInstance: "spoken_word", ProposalIndex: 3}}},
}
ledger := BuildCorrectionLedger(CorrectionLedgerInput{RunOutput: output})
byDisposition := make(map[string]map[string]json.RawMessage)
for _, entry := range ledger {
raw, err := json.Marshal(entry)
if err != nil {
t.Fatal(err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
t.Fatal(err)
}
byDisposition[entry.Disposition] = fields
}
for _, tc := range []struct {
disposition string
proposed, applied bool
}{
{correctionDispositionApplied, true, true},
{correctionDispositionRejected, true, false},
{correctionDispositionSkipped, true, false},
{correctionDispositionInterrupted, true, false},
{correctionDispositionFailed, false, false},
} {
fields, ok := byDisposition[tc.disposition]
if !ok {
t.Fatalf("missing %s entry: %+v", tc.disposition, byDisposition)
}
proposed, proposedPresent := fields["proposed_corrected_text"]
applied, appliedPresent := fields["applied_corrected_text"]
if proposedPresent != tc.proposed || appliedPresent != tc.applied || (proposedPresent && string(proposed) != `""`) || (appliedPresent && string(applied) != `""`) {
t.Fatalf("incorrect serialized replacement fields for %s: %+v", tc.disposition, fields)
}
}
}