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
|
complete usage semantics. Interaction request/response artifacts, selected
|
||||||
profiles, configuration provenance, and input snapshots are also debug-only.
|
profiles, configuration provenance, and input snapshots are also debug-only.
|
||||||
These files are diagnostic evidence, not an alternate ordinary report schema.
|
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,
|
when debug is off. With debug on, source/normalized snapshots, invocation,
|
||||||
effective settings, origins, profiles, initial chunk summary, correction ledger,
|
effective settings, origins, profiles, initial chunk summary, correction ledger,
|
||||||
indexed execution/utilization, and model interactions are written. Runner
|
indexed execution/utilization, and model interactions are written. Runner
|
||||||
candidate capture includes proposals later rejected or interrupted. Debug
|
candidate capture includes proposals later rejected or interrupted. Ledger
|
||||||
write failures reach the CLI through an explicit concurrency-safe diagnostic
|
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.
|
callback and produce redacted stderr messages without aborting processing.
|
||||||
Completed validator decisions returned with a fatal batch error remain available
|
Completed validator decisions returned with a fatal batch error remain available
|
||||||
to the debug ledger; they never authorize applying an incompletely validated edit.
|
to the debug ledger; they never authorize applying an incompletely validated edit.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Last updated:** 2026-09-25
|
**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`.
|
**Source baseline:** `a83a518` (feature roadmap); application baseline `0b6cd98`.
|
||||||
**Audience:** gpt-5.6-terra coding agent.
|
**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
|
**End state:** Debug evidence distinguishes a proposed/applied deletion from an
|
||||||
absent proposal or an edit that was never applied.
|
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
|
## Stage 4 — Verify the Integrated Contract and Finish Documentation
|
||||||
|
|
||||||
**Work**
|
**Work**
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ type CorrectionLedgerEntry struct {
|
|||||||
ProposalIndex int `json:"proposal_index"`
|
ProposalIndex int `json:"proposal_index"`
|
||||||
SegmentID int `json:"segment_id,omitempty"`
|
SegmentID int `json:"segment_id,omitempty"`
|
||||||
OriginalText string `json:"original_text,omitempty"`
|
OriginalText string `json:"original_text,omitempty"`
|
||||||
ProposedCorrectedText string `json:"proposed_corrected_text,omitempty"`
|
ProposedCorrectedText *string `json:"proposed_corrected_text,omitempty"`
|
||||||
AppliedCorrectedText string `json:"applied_corrected_text,omitempty"`
|
AppliedCorrectedText *string `json:"applied_corrected_text,omitempty"`
|
||||||
ReplacementPolicy string `json:"replacement_policy,omitempty"`
|
ReplacementPolicy string `json:"replacement_policy,omitempty"`
|
||||||
Disposition string `json:"disposition"`
|
Disposition string `json:"disposition"`
|
||||||
DispositionReasonCode string `json:"disposition_reason_code,omitempty"`
|
DispositionReasonCode string `json:"disposition_reason_code,omitempty"`
|
||||||
@@ -74,8 +74,8 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
|
|||||||
ProposalIndex: change.ProposalIndex,
|
ProposalIndex: change.ProposalIndex,
|
||||||
SegmentID: change.TargetSegmentID,
|
SegmentID: change.TargetSegmentID,
|
||||||
OriginalText: change.OriginalText,
|
OriginalText: change.OriginalText,
|
||||||
ProposedCorrectedText: change.CorrectedText,
|
ProposedCorrectedText: &change.CorrectedText,
|
||||||
AppliedCorrectedText: change.CorrectedText,
|
AppliedCorrectedText: &change.CorrectedText,
|
||||||
ReplacementPolicy: string(module.ReplacementPolicy),
|
ReplacementPolicy: string(module.ReplacementPolicy),
|
||||||
Disposition: correctionDispositionApplied,
|
Disposition: correctionDispositionApplied,
|
||||||
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false),
|
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false),
|
||||||
@@ -91,7 +91,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
|
|||||||
ProposalIndex: change.ProposalIndex,
|
ProposalIndex: change.ProposalIndex,
|
||||||
SegmentID: change.TargetSegmentID,
|
SegmentID: change.TargetSegmentID,
|
||||||
OriginalText: change.OriginalText,
|
OriginalText: change.OriginalText,
|
||||||
ProposedCorrectedText: change.CorrectedText,
|
ProposedCorrectedText: &change.CorrectedText,
|
||||||
ReplacementPolicy: string(module.ReplacementPolicy),
|
ReplacementPolicy: string(module.ReplacementPolicy),
|
||||||
Disposition: correctionDispositionSkipped,
|
Disposition: correctionDispositionSkipped,
|
||||||
DispositionReasonCode: string(change.SkipReason),
|
DispositionReasonCode: string(change.SkipReason),
|
||||||
@@ -109,7 +109,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
|
|||||||
ProposalIndex: rejection.ProposalIndex,
|
ProposalIndex: rejection.ProposalIndex,
|
||||||
SegmentID: rejection.TargetSegmentID,
|
SegmentID: rejection.TargetSegmentID,
|
||||||
OriginalText: rejection.OriginalText,
|
OriginalText: rejection.OriginalText,
|
||||||
ProposedCorrectedText: rejection.CorrectedText,
|
ProposedCorrectedText: &rejection.CorrectedText,
|
||||||
ReplacementPolicy: string(module.ReplacementPolicy),
|
ReplacementPolicy: string(module.ReplacementPolicy),
|
||||||
Disposition: correctionDispositionRejected,
|
Disposition: correctionDispositionRejected,
|
||||||
DispositionReasonCode: rejection.ReasonCode,
|
DispositionReasonCode: rejection.ReasonCode,
|
||||||
@@ -150,7 +150,7 @@ func BuildCorrectionLedger(input CorrectionLedgerInput) []CorrectionLedgerEntry
|
|||||||
entries = append(entries, CorrectionLedgerEntry{
|
entries = append(entries, CorrectionLedgerEntry{
|
||||||
RunID: runID, ModuleKey: candidate.ModuleKey, ModuleInstance: candidate.ModuleInstance,
|
RunID: runID, ModuleKey: candidate.ModuleKey, ModuleInstance: candidate.ModuleInstance,
|
||||||
ProposalIndex: candidate.ProposalIndex, SegmentID: candidate.TargetSegmentID,
|
ProposalIndex: candidate.ProposalIndex, SegmentID: candidate.TargetSegmentID,
|
||||||
OriginalText: candidate.OriginalText, ProposedCorrectedText: candidate.CorrectedText,
|
OriginalText: candidate.OriginalText, ProposedCorrectedText: &candidate.CorrectedText,
|
||||||
ReplacementPolicy: policy, Disposition: correctionDispositionInterrupted,
|
ReplacementPolicy: policy, Disposition: correctionDispositionInterrupted,
|
||||||
DispositionReasonCode: "not_applied", DispositionMessage: "candidate did not reach application",
|
DispositionReasonCode: "not_applied", DispositionMessage: "candidate did not reach application",
|
||||||
DeterministicValidatorResults: filterLedgerDecisions(decisions, false),
|
DeterministicValidatorResults: filterLedgerDecisions(decisions, false),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package processreport
|
package processreport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||||
@@ -40,7 +41,7 @@ func TestBuildCorrectionLedgerClassifiesValidatorDecisionsFromCanonicalMetadata(
|
|||||||
t.Fatalf("expected one ledger entry, got %d", len(ledger))
|
t.Fatalf("expected one ledger entry, got %d", len(ledger))
|
||||||
}
|
}
|
||||||
entry := ledger[0]
|
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)
|
t.Fatalf("unexpected applied ledger entry: %+v", entry)
|
||||||
}
|
}
|
||||||
if len(entry.DeterministicValidatorResults) != 1 || entry.DeterministicValidatorResults[0].ValidatorKey != "proposal_shape" {
|
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"])
|
t.Fatalf("unexpected skipped ledger entry: %+v", byDisposition["skipped"])
|
||||||
}
|
}
|
||||||
if byDisposition["rejected"].DispositionReasonCode != "protected_term" ||
|
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"])
|
t.Fatalf("unexpected rejected ledger entry: %+v", byDisposition["rejected"])
|
||||||
}
|
}
|
||||||
if byDisposition["failed"].DispositionReasonCode != "module_failed" ||
|
if byDisposition["failed"].DispositionReasonCode != "module_failed" ||
|
||||||
@@ -133,3 +134,51 @@ func TestCorrectionLedgerIncludesUnappliedCandidateAfterFatalValidation(t *testi
|
|||||||
}
|
}
|
||||||
t.Fatal("candidate missing")
|
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