Preserve empty deletion spans in debug ledger
This commit is contained in:
@@ -88,3 +88,11 @@ groups backend capacity and timing observations while preserving known and
|
||||
complete usage semantics. Interaction request/response artifacts, selected
|
||||
profiles, configuration provenance, and input snapshots are also debug-only.
|
||||
These files are diagnostic evidence, not an alternate ordinary report schema.
|
||||
Each candidate-backed ledger entry includes `proposed_corrected_text`, the
|
||||
literal replacement span, even when it is `""` for a deletion. Applied entries
|
||||
also include `applied_corrected_text`, likewise `""` for an applied deletion.
|
||||
Rejected, skipped, and interrupted entries omit the applied field; a module
|
||||
failure without a candidate omits both. These fields are replacement spans,
|
||||
not full resulting-segment text. Deterministic decision records now identify
|
||||
the blank-result guard as `non_empty_resulting_segment` (formerly
|
||||
`non_empty_corrected_text`); its `empty_resulting_segment` reason is unchanged.
|
||||
|
||||
@@ -32,8 +32,11 @@ under `diagnostics.work_dir`. CLI passes empty diagnostics paths and nil writers
|
||||
when debug is off. With debug on, source/normalized snapshots, invocation,
|
||||
effective settings, origins, profiles, initial chunk summary, correction ledger,
|
||||
indexed execution/utilization, and model interactions are written. Runner
|
||||
candidate capture includes proposals later rejected or interrupted. Debug
|
||||
write failures reach the CLI through an explicit concurrency-safe diagnostic
|
||||
candidate capture includes proposals later rejected or interrupted. Ledger
|
||||
mapping uses optional string pointers so an empty proposed replacement is
|
||||
serialized for every candidate and an empty applied replacement appears only
|
||||
when that candidate applied; see the [public artifact contract](../integrations/output-files.md#debug-only-files).
|
||||
Debug write failures reach the CLI through an explicit concurrency-safe diagnostic
|
||||
callback and produce redacted stderr messages without aborting processing.
|
||||
Completed validator decisions returned with a fatal batch error remain available
|
||||
to the debug ledger; they never authorize applying an incompletely validated edit.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Last updated:** 2026-09-25
|
||||
|
||||
**Status:** In progress; Stages 1–2 complete.
|
||||
**Status:** In progress; Stages 1–3 complete.
|
||||
**Source baseline:** `a83a518` (feature roadmap); application baseline `0b6cd98`.
|
||||
**Audience:** gpt-5.6-terra coding agent.
|
||||
|
||||
@@ -271,6 +271,12 @@ existing configured-secret redaction and debug-off persistence coverage.
|
||||
**End state:** Debug evidence distinguishes a proposed/applied deletion from an
|
||||
absent proposal or an edit that was never applied.
|
||||
|
||||
**Completed 2026-09-25.** Candidate-backed ledger entries now preserve an
|
||||
explicit empty proposed replacement, and applied entries preserve an explicit
|
||||
empty applied replacement. Serialized disposition coverage and the specified
|
||||
focused package tests passed; the artifact contract documents the fields and
|
||||
validator identity change.
|
||||
|
||||
## Stage 4 — Verify the Integrated Contract and Finish Documentation
|
||||
|
||||
**Work**
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user