ADR-102: _retro_audit marker for in-place artifact mutation¶
- Status: Accepted
- Date: 2026-06-24
- Authors: Marko Dragoljevic, Claude (Opus 4.7)
- Related ADRs: ADR-101 (strict KG v2.0 / GI v3.0 schemas which routine in-place migrations now satisfy).
- Related RFC: RFC-097 (the v3.0 ontology whose post-pass enrichment surfaced this need).
Context¶
Two distinct surfaces have, over the last 30 days, mutated already-published artifacts in place:
-
Eval-run retroactive fingerprint backfill (
data/eval/runs/<run_id>/, 2026-06-22) — chunk-7 rerun used new prompt and config fields that weren't in the original fingerprint. Rather than reprocess each run, the rerun stamped each affectedpayload.jsonwith a_retro_auditentry recording the marker, the rerun timestamp, and theunknown_fieldslist. Documented indocs/guides/eval-reports/CHUNK7_RETRO_FINGERPRINT_AUDIT_2026_06_22.md. -
#1076 NER post-pass retro sweep (
.test_outputs/manual/prod-v2/corpus, 2026-06-24) — the spaCy NER pass was switched on for two airgapped profiles. To project the lift onto an existing corpus without re-running 99 episodes through the pipeline,enrich-edges --use-ner --retro-auditmutated each affected.gi.jsonin place and stamped_retro_auditwith the marker, applied-at, and per-edge counters.
The pattern works. It satisfies the operator-set constraint
([[feedback_never_mutate_historical_artifacts]]) that historical
artifacts are frozen-once-written; mutation is allowed only as an
explicit extraordinary measure with an audit trail. Without an ADR, the
two surfaces are using slightly different shapes (eval uses
unknown_fields, retro-sweep uses edges_added) and the trigger
conditions live in scattered docs.
The cost of not codifying: every future in-place mutation invents its
own shape, future readers can't tell whether a missing marker means
"never mutated" or "mutated by a non-audit-aware caller," and the
existing migration scripts (scripts/migrate_*.py) silently bypass the
pattern when they sweep through legacy corpora.
Decision¶
_retro_audit is the canonical marker for in-place mutation of any
artifact that was already produced + written to its source-of-truth
location. Greenfield writes (a fresh pipeline run, a brand-new eval
run, a first-time-emitted KG/GI artifact) do not stamp.
When required¶
A caller MUST stamp _retro_audit when it:
- Modifies an artifact at the same path it was originally written to,
- The artifact is in a directory the project treats as a published or
frozen-once-written source-of-truth (
data/eval/runs/*, prod corpora, viewer-validation corpora), AND - The mutation changes the artifact's observable content (edges added, fields backfilled, schema bumped).
A caller MAY stamp _retro_audit for less-load-bearing mutations (a
local dev experiment on a personal corpus) but is not required to.
Field shape¶
_retro_audit is a list of dict entries. List-valued so repeat
mutations stack chronologically:
{
"_retro_audit": [
{
"marker": "#1076-ner-2026-06-24",
"applied_at": "2026-06-24T11:02:25+00:00",
"scope": { "use_ner": true },
"changes": { "mentions": 2, "has_episode": 0, "spoken_by": 0 }
}
]
}
Required keys on each entry:
marker(string) — issue-prefixed, date-suffixed:"#<issue>-<short slug>-<YYYY-MM-DD>". Marker doubles as the summary-file basename (with#and/stripped:_retro_audit_1076-ner-2026-06-24.json).applied_at(ISO-8601 UTC string) —datetime.now(timezone.utc) .isoformat(timespec="seconds").
Recommended keys (use what fits the mutation):
scope— what the mutation was scoped to. For #1076: the flags that were live during the rewrite (use_ner: true).changes— per-artifact counters of what changed. Free-shape, chosen by the caller.unknown_fields— for fingerprint-style mutations, the list of cfg/prompt fields that were new at rerun time.
Sites are free to add other keys (e.g. rerun_id, validator_version)
as long as the required keys are present.
Summary file¶
Callers that mutate more than one artifact in a single pass SHOULD
also write a single <root>/_retro_audit_<sanitized-marker>.json
listing per-artifact changes and grand totals. The summary makes the
sweep auditable without reading every mutated file.
Required summary shape:
{
"marker": "#1076-ner-2026-06-24",
"applied_at": "...",
"scope": { "use_ner": true },
"root": "<absolute path>",
"totals": { "mentions": 46, "has_episode": 0, "spoken_by": 0 },
"per_episode": [
{ "gi_path": "feeds/.../episode.gi.json",
"marker": "...", "applied_at": "...",
"changes": { "mentions": 2, ... } }
]
}
Migration scripts¶
scripts/migrate_*.py currently rewrite legacy schema in place
without stamping _retro_audit. This is grandfathered because:
- The migration is a one-shot per artifact (idempotent on v3 input); the audit need is fundamentally different from an enrichment sweep.
- Stamping every migrated artifact would balloon the in-tree migration scripts when most callers only migrate scratch corpora.
- The migration target shape (
schema_version) already records that migration happened; no second marker is needed.
If a future in-place migration changes observable content beyond what
schema_version advertises, that migration MUST start stamping.
Consequences¶
Positive:
- Two existing surfaces converge on one shape.
- Future authors copy from this ADR instead of inventing.
- The
[[feedback_never_mutate_historical_artifacts]]rule has a concrete escape hatch the operator can audit.
Negative:
- A new top-level key in artifact files that downstream consumers
must tolerate. Validators currently accept extras
(
_minimal_validatechecks required keys only), so this is no-op for now. - Forgetting to stamp during a retro-sweep silently violates the pattern; CI can't detect "should have stamped but didn't."
Neutral:
- Migration scripts intentionally don't stamp. If the operator later decides migration audit is needed, that's a follow-up.
Validation¶
This ADR codifies existing behavior; nothing to validate beyond:
scripts/dev/revert_gi_v3_to_v2.pyreverts both content and_retro_auditcorrectly (verified 2026-06-24 on.test_outputs/manual/prod-v2/corpusin the AI-ML-improvements worktree).enrich-edges --use-ner --retro-auditproduces a well-formed marker + summary (verified 2026-06-24 on the FUTURE worktree's corpus copy).
Implementation references¶
src/podcast_scraper/search/cli_handlers.py:1505-1623— the enrich-edges retro-audit hook (reference implementation).docs/guides/eval-reports/CHUNK7_RETRO_FINGERPRINT_AUDIT_2026_06_22.md— earlier surface usingunknown_fieldsinstead ofchanges.scripts/dev/revert_gi_v3_to_v2.py— reverse path; strips the marker as part of reverting content.