← Files APODICTIC Development EditorARCHIVED FILE
scripts/finding_trace.py
42.6 KB · Nov 3, 2026 · 18:57 UTC
#!/usr/bin/env python3
"""finding-trace — cross-artifact Finding Lifecycle ID integrity (Harness Engineering).
`validate.sh finding-trace <run_folder> [--strict]` (or explicit files) shells out here.
Every material finding carries a durable Finding Lifecycle ID (`apodictic.finding.v1.id`,
`F-<ORIGIN>-<NN>`). `structured-findings` owns intra-ledger ID hygiene and `softness-check`
owns severity fidelity (locked -> delivered) by ID. This validator owns the *un-owned*
dimension: cross-artifact REFERENTIAL INTEGRITY + sidecar lifecycle COHERENCE.
E1 dangling reference a letter HTML-comment / severity_calibration citation to an
F-... ID that is not in the ledger (typo / phantom).
E2 phantom sidecar execution.finding_states key that is not a ledger ID.
E3 invalid state finding_states value not in {locked, delivered, revised}.
E4 dangling revision a revision-plan / coaching artifact cites an F-... ID not in the ledger.
W1 lifecycle coverage once synthesis has cleared (sidecar phase >= run_synthesis),
a Must-Fix/Should-Fix ledger ID with no finding_states entry
(advisory; ERROR under --strict).
W2 revision coverage a Must-Fix ledger ID not referenced in any revision plan, when one
is present (advisory; ERROR under --strict).
E5 phantom completion a completed-revision artifact MENTIONS a ledger ID whose finding_states
is "revised" but carries no `<!-- resolved: F-... -->` marker for it (the
in-scope report and the rolling sidecar disagree; IDs not in the report
are out of scope — a finding resolved in an earlier round is left alone).
W3 completion coverage a ledger ID marked resolved in a completed-revision artifact whose
finding_states entry is not "revised" (advisory; ERROR under --strict).
E6 dangling retcon src a Retcon Plan retcon_item `source` finding-ref (Retcon Planning F3,
primarily a Pass-8 seed) that is not in the ledger.
Each artifact is optional; a missing one skips its dimension (no false failure).
Reuses apodictic_artifacts.parse_blocks (one block grammar). See docs/finding-lifecycle-ids.md.
finding_trace.py finding-trace <run_folder> [--strict]
finding_trace.py finding-trace <ledger.md> [<letter.md>] [<sidecar.json>] [--strict]
finding_trace.py --self-test
Exit: 0 clean / WARN-only, 1 ERROR (or WARN under --strict), 2 usage.
"""
import glob
import json
import os
import re
import sys
try:
import apodictic_artifacts as art
except ImportError:
art = None
def _has_block(text, btype):
"""True if `text` carries a real apodictic:<btype> block (a parsed carrier, not a prose mention).
Classifying on parsed blocks — not a raw substring — keeps a file that merely *names* the marker
in prose from being misrouted/skipped (the 2026-06-20 resolver-hardening sweep). Gated by
validate.sh validator-conventions (M2)."""
if art is None:
return ("apodictic:%s" % btype) in (text or "")
return any(bt == btype for bt, _o, _e in art.parse_blocks(text or ""))
_STATES = ("locked", "delivered", "revised")
_SYNTH_BOUND = ("Must-Fix", "Should-Fix")
# Synthesis has cleared (findings locked) once the gate frontier reaches a gated phase.
_SYNTH_CLEARED_PHASES = ("run_synthesis", "run_spot_check")
# Exact Lifecycle-ID token (so F-P5-01 != F-P5-011); mirrors honesty_check._id_present.
# Single-sourced from apodictic_artifacts.FID_RE (the host — dependency direction: this module
# imports art, never the reverse). The literal below is only the art-less degraded fallback;
# the apodictic_artifacts --self-test (fid_re_lockstep) pins it byte-equal to FID_RE.
_ID_RE = art.FID_RE if art is not None else re.compile(r"(?<![\w-])F-[A-Za-z0-9]+-[0-9]{2,}(?![\w-])")
_COMMENT_RE = re.compile(r"<!--(.*?)-->", re.DOTALL)
# Explicit resolution marker in a completed-revision artifact: `<!-- resolved: F-XX-NN -->`.
# A bare mention (e.g. a finding listed under "Flags still present") is NOT a resolution claim,
# so completion (E5/W3) keys on this marker, not on any F-... token in the report.
_RESOLVED_RE = re.compile(r"<!--\s*resolved:(.*?)-->", re.DOTALL | re.IGNORECASE)
# Editorial-letter filename globs (output-structure.md naming).
_LETTER_GLOBS = ("*_Core_DE_Synthesis_*.md", "*_Full_DE_*.md", "*_Editorial_Letter_*.md")
_LEDGER_GLOB = "*_Findings_Ledger_*.md"
# Revision-plan / coaching artifact globs (the lifecycle stage after the letter).
_REVISION_GLOBS = ("*_Session_Plan_*.md", "*_Revision_*.md")
# Completed-revision artifacts — a SUBSET of _REVISION_GLOBS: the Revision REPORT specifically
# (`*_Revision_Report_*.md`, state-lifecycle.md §Revision Round Output), where a finding's lifecycle
# advances to `revised`. Narrowed from `*_Revision_*.md` so a deadline-coaching `*_Revision_Calendar_*.md`
# (a revision-stage artifact, but not a completion) is excluded — matching the Increment-4a gate's
# `revision_report` key. Session plans express intent (plan coverage, W2); completion artifacts
# express done work (E5/W3).
_COMPLETION_GLOBS = ("*_Revision_Report_*.md",)
# Retcon Plan artifacts (Retcon Planning, F3): retcon_item blocks may carry a `source` finding-ref
# (primarily a Pass-8 Reveal-Economy finding) recording what the item was seeded from — E6 checks it
# resolves to a ledger finding.
_RETCON_GLOB = "*_Retcon_Plan_*.md"
def _read(path):
try:
with open(path, encoding="utf-8") as fh:
return fh.read()
except (OSError, UnicodeDecodeError):
return None
def ledger_inventory(ledger_text):
"""{id: severity} for the ledger's apodictic.finding.v1 blocks. The authoritative ID set."""
inv = {}
if not ledger_text or art is None:
return inv
for bt, obj, _err in art.parse_blocks(ledger_text):
if bt == "finding" and isinstance(obj, dict) and obj.get("id"):
# art.fid_key: a malformed ledger finding with a non-hashable id (list/object) must not crash
# this index key (the authoritative-ID-set sibling of the manifest finding_id crash class).
inv[art.fid_key(obj["id"])] = obj.get("severity")
return inv
def letter_cited_ids(letter_text):
"""F-... IDs cited in the letter — tokens inside HTML comments (the canonical citation
surface: `<!-- finding: F-XX-NN -->` near a finding, and apodictic:severity_calibration
blocks, which are themselves HTML comments). IDs never appear in author-facing prose."""
cited = set()
if not letter_text:
return cited
for body in _COMMENT_RE.findall(letter_text):
cited.update(_ID_RE.findall(body))
return cited
def revision_cited_ids(text):
"""F-... IDs referenced in a revision-plan / coaching artifact. These are working documents,
so IDs are matched ANYWHERE in the text (inline prose references count) — not comment-only."""
return set(_ID_RE.findall(text)) if text else set()
def resolved_cited_ids(text):
"""F-... IDs explicitly marked resolved in a completed-revision artifact via
`<!-- resolved: F-XX-NN -->` markers. A bare mention (a finding under "Flags still present"
or "New issues introduced") is NOT a resolution claim and is excluded — only an explicit
marker counts a finding as a completed revision."""
ids = set()
if not text:
return ids
for body in _RESOLVED_RE.findall(text):
ids.update(_ID_RE.findall(body))
return ids
def retcon_source_ids(text):
"""Well-formed F-... `source` refs declared on the retcon_item blocks of a Retcon Plan (F3).
Only schema-shaped F-refs are returned; a malformed `source` is the retcon-plan schema's job
(R1), not E6's, so this filters by the ID pattern to avoid double-reporting a format error."""
ids = set()
if not text or art is None:
return ids
for bt, obj, _err in art.parse_blocks(text):
if bt != "retcon_item" or not isinstance(obj, dict):
continue
src = obj.get("source")
if isinstance(src, str) and _ID_RE.fullmatch(src):
ids.add(src)
return ids
def sidecar_state(sidecar_text):
"""(finding_states dict, phase, parse_ok) from a Diagnostic_State.meta.json.
parse_ok is False when a *discovered* sidecar is present but not valid JSON — the
caller must treat that as an error, not a clean empty lifecycle state."""
try:
meta = json.loads(sidecar_text)
except (ValueError, TypeError):
return {}, None, False
ex = meta.get("execution", {}) if isinstance(meta, dict) else {}
fs = ex.get("finding_states") or {}
return (fs if isinstance(fs, dict) else {}), ex.get("phase"), True
def event_advanced_revised_ids(sidecar_text, run_folder, sidecar_path):
"""M1c — the event-scoped extension of E5's in-scope set (docs/finding-lifecycle-ids.md:112).
On a GOVERNED sidecar (state_version >= 2 with a gate_events[] log), return the ledger ids that
a CLEARING event of THIS run advanced to `revised` — i.e. events whose recorded run_folder
resolves to `run_folder` and whose finding_deltas set an id to `revised`. On an ungoverned
sidecar (no gate_events / state_version < 2), or when the run folder can't be matched, return an
EMPTY set — so trace() stays byte-identical to today for ungoverned inputs.
run_folder events store the path RELATIVE to the sidecar's dir (run_gate._rel_run_folder), so a
match is by realpath equality against the run folder under trace. This reads the log only; it
never re-derives finding_states (W1/E2/E3 still key on the folded execution.finding_states)."""
ids = set()
if not sidecar_text or run_folder is None or sidecar_path is None:
return ids
try:
meta = json.loads(sidecar_text)
except (ValueError, TypeError):
return ids
ex = meta.get("execution", {}) if isinstance(meta, dict) else {}
if not isinstance(ex, dict) or ex.get("state_version", 1) < 2:
return ids
events = ex.get("gate_events")
if not isinstance(events, list):
return ids
sc_dir = os.path.dirname(os.path.abspath(sidecar_path))
target = os.path.realpath(os.path.abspath(run_folder))
for e in events:
if not isinstance(e, dict):
continue
rf = e.get("run_folder")
deltas = e.get("finding_deltas")
if not isinstance(rf, str) or not isinstance(deltas, dict):
continue
if os.path.realpath(os.path.join(sc_dir, rf)) != target:
continue
for fid, st in deltas.items():
if st == "revised":
ids.add(fid)
return ids
def trace(ledger_text, letter_text, sidecar_text, revision_texts=None, completion_texts=None,
retcon_texts=None, strict=False, event_advanced=None):
"""Run the cross-artifact trace. Returns (code, lines).
revision_texts = ALL revision-stage artifacts (session plans + completed revisions) — the
surface for E4 (dangling) and W2 (plan coverage).
completion_texts = the completed-revision (*_Revision_Report_*.md) SUBSET — the surface for E5
(phantom completion) and W3 (completion follow-through). Callers keep the
invariant completion_texts ⊆ revision_texts.
retcon_texts = Retcon Plan artifacts (F3) — the surface for E6 (a retcon_item `source`
finding-ref that is not in the ledger).
event_advanced = M1c — ids advanced to `revised` by a CLEARING event of THIS run (governed
sidecars only; event_advanced_revised_ids). WIDENS E5's in-scope set from
comp_mentioned to comp_mentioned ∪ event_advanced. The E5 PREDICATE and the
resolved_ids union are unchanged — only the iteration set grows. None/empty
(the ungoverned default) leaves behaviour byte-identical.
"""
lines, errs, warns = [], [], []
inv = ledger_inventory(ledger_text)
if not inv:
return 0, ["finding-trace: no ledger findings found — nothing to trace"]
have_letter = letter_text is not None
have_sidecar = sidecar_text is not None
revision_texts = revision_texts or []
have_revision = bool(revision_texts)
completion_texts = completion_texts or []
have_completion = bool(completion_texts)
cited = letter_cited_ids(letter_text) if have_letter else set()
rev_cited = set()
for rt in revision_texts:
rev_cited |= revision_cited_ids(rt)
resolved_ids = set() # explicitly resolved (<!-- resolved: ID -->) in a completion artifact
comp_mentioned = set() # any F-... token (bare) in a completion artifact — E5's in-scope set
for ct in completion_texts:
resolved_ids |= resolved_cited_ids(ct)
comp_mentioned |= revision_cited_ids(ct)
retcon_texts = retcon_texts or []
have_retcon = bool(retcon_texts)
retcon_sources = set() # F-... `source` refs declared on retcon_item blocks (F3)
for rt in retcon_texts:
retcon_sources |= retcon_source_ids(rt)
finding_states, phase, sc_ok = sidecar_state(sidecar_text) if have_sidecar else ({}, None, True)
event_advanced = set(event_advanced or ()) # M1c: governed event-scope extension of E5 (empty by default)
# E1 — dangling reference (letter cites an ID not in the ledger)
if have_letter:
for cid in sorted(cited):
if cid not in inv:
errs.append("E1 dangling reference: letter cites %s — not in the ledger" % cid)
# E4 — dangling revision reference (revision plan cites an ID not in the ledger)
if have_revision:
for cid in sorted(rev_cited):
if cid not in inv:
errs.append("E4 dangling reference: revision plan cites %s — not in the ledger" % cid)
# E6 — dangling retcon source (a retcon_item `source` finding-ref not in the ledger, F3)
if have_retcon:
for sid in sorted(retcon_sources):
if sid not in inv:
errs.append("E6 dangling retcon source: retcon plan seeds an item from %s — "
"not in the ledger" % sid)
# E0 — a discovered sidecar that cannot be parsed is an ERROR, not a clean empty lifecycle:
# otherwise E2/E3/W1 are silently bypassed on the artifact that is supposed to carry the state.
if have_sidecar and not sc_ok:
errs.append("E0 unparseable sidecar: Diagnostic_State.meta.json is present but not valid "
"JSON — lifecycle coherence cannot be verified")
# E2 / E3 — sidecar coherence (only when the sidecar parsed)
if have_sidecar and sc_ok:
for fid in sorted(finding_states):
if fid not in inv:
errs.append("E2 phantom sidecar state: finding_states[%s] — not in the ledger" % fid)
if finding_states[fid] not in _STATES:
errs.append("E3 invalid state: finding_states[%s]=%r (expected %s)"
% (fid, finding_states[fid], "/".join(_STATES)))
# W1 — lifecycle coverage (only once synthesis has cleared, and the sidecar parsed)
synth_cleared = sc_ok and phase in _SYNTH_CLEARED_PHASES
if have_sidecar and sc_ok and synth_cleared:
for fid in sorted(inv):
if inv[fid] in _SYNTH_BOUND and fid not in finding_states:
warns.append("W1 coverage: %s (%s) locked but has no finding_states entry"
% (fid, inv[fid]))
# W2 — revision follow-through coverage (a Must-Fix not picked up by any revision plan)
if have_revision:
for fid in sorted(inv):
if inv[fid] == "Must-Fix" and fid not in rev_cited:
warns.append("W2 follow-through: Must-Fix %s not referenced in any revision plan" % fid)
# E5 — phantom completion: an in-scope contradiction — a completed-revision artifact *mentions*
# a `revised` ledger finding (bare, e.g. under "Flags still present") but carries no
# `<!-- resolved: ID -->` marker for it, so the report and the sidecar disagree. Scoped to IDs
# the current report actually mentions (comp_mentioned): the sidecar's finding_states is a
# rolling all-session map, so a finding resolved in an EARLIER (out-of-scope) round is durably
# `revised` but simply won't appear in this report — and must not be flagged (PR #32 review P1).
# M1c widens the SCOPE (not the predicate) on governed sidecars: an id advanced to `revised` by
# THIS run's clearing event (event_advanced) is in scope too — so a marker removed AFTER the
# clear (post-clear tampering, the incoherence E5 exists to catch) is surfaced even if the id is
# not bare-mentioned in the current report. The resolved_ids union across ALL completion
# artifacts of the run is unchanged, so a marker living in a SIBLING artifact still clears it.
if have_sidecar and sc_ok:
for fid in sorted(comp_mentioned | event_advanced):
if finding_states.get(fid) == "revised" and fid in inv and fid not in resolved_ids:
errs.append("E5 phantom completion: finding_states[%s]=revised but the completed-"
"revision artifact mentions it without a <!-- resolved: %s --> marker "
"— the report and the sidecar disagree" % (fid, fid))
# W3 — completion follow-through: a finding explicitly marked resolved in a completed round
# whose lifecycle was not advanced to `revised`. Keys on the resolved marker (so a
# still-present mention never triggers it). Needs a parseable sidecar to judge against.
if have_completion and have_sidecar and sc_ok:
for fid in sorted(resolved_ids):
if fid in inv and finding_states.get(fid) != "revised":
warns.append("W3 completion: %s marked resolved in a completed-revision artifact "
"but finding_states[%s]=%r (expected 'revised')"
% (fid, fid, finding_states.get(fid)))
# Per-ID trace report
if not have_sidecar:
sc_note = " (no sidecar — lifecycle trace skipped)"
elif not sc_ok:
sc_note = " (sidecar UNPARSEABLE — see E0)"
else:
sc_note = ""
lines.append("finding-trace: %d ledger finding(s)%s%s%s%s"
% (len(inv),
"" if have_letter else " (no letter — letter trace skipped)",
sc_note,
"" if have_revision else " (no revision plan — follow-through skipped)",
"; %d retcon source(s) traced" % len(retcon_sources) if have_retcon else ""))
for fid in sorted(inv):
state = ((finding_states.get(fid, "—") if sc_ok else "?") if have_sidecar else "n/a")
mark = ("cited" if fid in cited else "UNCITED") if have_letter else "n/a"
if not have_revision:
rev_mark = "n/a"
elif fid in resolved_ids:
rev_mark = "done" # explicitly marked resolved in a completed-revision artifact
elif fid in rev_cited:
rev_mark = "planned" # mentioned in a revision plan / report (not marked resolved)
else:
rev_mark = "—"
lines.append(" %-12s sev=%-9s state=%-9s letter=%-7s rev=%s" % (fid, inv[fid], state, mark, rev_mark))
for e in errs:
lines.append(" ERROR: %s" % e)
for w in warns:
lines.append(" %s: %s" % ("ERROR (--strict)" if strict else "WARN", w))
if errs or (strict and warns):
lines.append("finding-trace: FAIL (%d error(s)%s)"
% (len(errs), ", %d strict warn(s)" % len(warns) if (strict and warns) else ""))
return 1, lines
if warns:
lines.append("WARN: finding-trace: %d advisory coverage gap(s) — see W1 above" % len(warns))
else:
lines.append("finding-trace: PASS (referential integrity + sidecar coherence)")
return 0, lines
# ---------------------------------------------------------------- artifact resolution
def _walk_up_sidecar(start):
d = os.path.abspath(start if os.path.isdir(start) else os.path.dirname(start))
for _ in range(4):
sc = os.path.join(d, "Diagnostic_State.meta.json")
if os.path.exists(sc):
return sc
d = os.path.dirname(d)
return None
def _newest(paths):
return max(paths, key=os.path.getmtime) if paths else None
def resolve_run_folder(folder):
"""(ledger, letter, sidecar, [revisions], [completions], [retcons]) — newest per single
artifact; revisions = all session-plan + revision-stage matches; completions = the
*_Revision_Report_*.md subset; retcons = all Retcon Plan matches (F3)."""
ledger = _newest(glob.glob(os.path.join(folder, _LEDGER_GLOB)))
letter = None
for g in _LETTER_GLOBS:
letter = _newest(glob.glob(os.path.join(folder, g)))
if letter:
break
sidecar = _walk_up_sidecar(folder)
revisions = []
for g in _REVISION_GLOBS:
revisions += glob.glob(os.path.join(folder, g))
completions = []
for g in _COMPLETION_GLOBS:
completions += glob.glob(os.path.join(folder, g))
retcons = glob.glob(os.path.join(folder, _RETCON_GLOB))
return ledger, letter, sidecar, revisions, completions, retcons
def classify_files(paths):
"""Classify explicit file args into (ledger, letter, sidecar, [revisions], [completions],
[retcons])."""
ledger = letter = sidecar = None
revisions, completions, retcons = [], [], []
for p in paths:
base = os.path.basename(p)
if p.endswith(".json"):
sidecar = p
elif "_Retcon_Plan_" in base:
retcons.append(p)
elif _has_block(_read(p) or "", "finding"):
ledger = p
elif "_Session_Plan_" in base or "_Revision_" in base:
revisions.append(p)
if "_Revision_Report_" in base: # completion ⊂ revision-stage; calendars stay non-completion
completions.append(p)
else:
letter = p
return ledger, letter, sidecar, revisions, completions, retcons
def run(paths, strict=False):
if len(paths) == 1 and os.path.isdir(paths[0]):
run_folder = paths[0]
ledger, letter, sidecar, revisions, completions, retcons = resolve_run_folder(paths[0])
else:
ledger, letter, sidecar, revisions, completions, retcons = classify_files(paths)
# Explicit-file mode: the run folder under trace is the dir the ledger lives in (the
# completion artifacts sit beside it) — the anchor event_advanced matches events against.
run_folder = os.path.dirname(os.path.abspath(ledger)) if ledger else None
if not ledger:
return 2, ["finding-trace: no Findings Ledger found (need a *_Findings_Ledger_*.md or a "
"file with apodictic:finding blocks)"]
sidecar_text = None
if sidecar: # a sidecar was discovered — read it; unreadable counts as present-but-bad (E0)
sidecar_text = _read(sidecar)
if sidecar_text is None:
sidecar_text = ""
revision_texts = [t for t in (_read(r) for r in revisions) if t is not None]
completion_texts = [t for t in (_read(c) for c in completions) if t is not None]
retcon_texts = [t for t in (_read(r) for r in retcons) if t is not None]
# M1c — governed event-scope extension of E5 (empty for ungoverned sidecars → byte-identical).
event_advanced = event_advanced_revised_ids(sidecar_text, run_folder, sidecar) if sidecar else set()
return trace(_read(ledger), _read(letter) if letter else None, sidecar_text,
revision_texts=revision_texts, completion_texts=completion_texts,
retcon_texts=retcon_texts, strict=strict, event_advanced=event_advanced)
# ---------------------------------------------------------------- self-test
def run_self_test():
import tempfile
import shutil
rc = {"v": 0}
made = []
def check(name, cond):
print(" %s: %s" % (name, "OK" if cond else "FAIL"))
if not cond:
rc["v"] = 1
# non-UTF8 artifact: _read must degrade to the unreadable path (None), never
# a traceback (the disposition_check adjacent-exception class, swept repo-wide)
import tempfile as _tf
_fd, _nu = _tf.mkstemp(suffix=".md")
with os.fdopen(_fd, "wb") as _fh:
_fh.write(b"\xff\xfenot utf-8\xff")
check("non_utf8_read_returns_none", _read(_nu) is None)
os.unlink(_nu)
def finding(fid, sev="Must-Fix"):
return ('<!-- apodictic:finding\n{"schema":"apodictic.finding.v1","id":"%s",'
'"mechanism":"m","severity":"%s","confidence":"HIGH","evidence_refs":["c"],'
'"fix_class":"x","risk_if_fixed":"y"}\n-->' % (fid, sev))
ledger = "## Pass 5 — Ledger Entry\n" + finding("F-P5-01") + "\n" + finding("F-P5-02", "Should-Fix") + "\n"
# regression: a non-hashable ledger id must not crash the authoritative-ID index (fid_key SSoT)
check("ledger_inventory_nonhashable_id_no_crash",
isinstance(ledger_inventory("<!-- apodictic:finding\n" + json.dumps(
{"schema": "apodictic.finding.v1", "id": [1, 2], "severity": "Must-Fix", "mechanism": "m"})
+ "\n-->"), dict))
letter_clean = ("# Edit\n## What Needs Work\nThe pacing collapses in Chapter 9. "
"<!-- finding: F-P5-01 -->\nThe stakes stay abstract. <!-- finding: F-P5-02 -->\n"
'<!-- apodictic:severity_calibration\n{"schema":"apodictic.severity_calibration.v1",'
'"id":"F-P5-01","locked":"Must-Fix","delivered":"Must-Fix","direction":"unchanged",'
'"rationale":"held"}\n-->\n')
letter_dangling = letter_clean + "Also a typo'd ref. <!-- finding: F-P5-99 -->\n"
def sidecar(states, phase="run_synthesis"):
return json.dumps({"execution": {"phase": phase, "finding_states": states}})
sc_ok = sidecar({"F-P5-01": "locked", "F-P5-02": "locked"})
sc_phantom = sidecar({"F-P5-01": "locked", "F-XX-09": "locked"})
sc_invalid = sidecar({"F-P5-01": "frozen"})
sc_partial = sidecar({"F-P5-01": "locked"}) # F-P5-02 missing -> W1
sc_presynth = sidecar({}, phase="") # pre-synthesis -> W1 skipped
# clean trace
code, _ = trace(ledger, letter_clean, sc_ok)
check("clean_trace_passes", code == 0)
# E1 dangling letter reference
code, lines = trace(ledger, letter_dangling, sc_ok)
check("e1_dangling_ref", code == 1 and any("E1 dangling" in ln and "F-P5-99" in ln for ln in lines))
# E2 phantom sidecar state
code, lines = trace(ledger, letter_clean, sc_phantom)
check("e2_phantom_sidecar", code == 1 and any("E2 phantom" in ln and "F-XX-09" in ln for ln in lines))
# E3 invalid state
code, lines = trace(ledger, letter_clean, sc_invalid)
check("e3_invalid_state", code == 1 and any("E3 invalid" in ln for ln in lines))
# W1 coverage: advisory (exit 0) by default, ERROR under --strict
code_w, lines_w = trace(ledger, letter_clean, sc_partial)
check("w1_coverage_advisory", code_w == 0 and any("W1 coverage" in ln and "F-P5-02" in ln for ln in lines_w))
code_s, _ = trace(ledger, letter_clean, sc_partial, strict=True)
check("w1_coverage_strict_fails", code_s == 1)
# W1 skipped pre-synthesis (no false positive before findings are locked)
code, _ = trace(ledger, letter_clean, sc_presynth)
check("w1_skipped_presynthesis", code == 0)
# present-but-malformed sidecar is an ERROR (not a clean empty lifecycle state)
code, lines = trace(ledger, letter_clean, "{ not valid json")
check("malformed_sidecar_errors", code == 1 and any("E0 unparseable" in ln for ln in lines))
# E4 — dangling revision reference (revision plan cites an ID not in the ledger)
code, lines = trace(ledger, letter_clean, sc_ok, revision_texts=["Plan: address F-P5-99 next."])
check("e4_dangling_revision", code == 1 and any("E4 dangling" in ln and "F-P5-99" in ln for ln in lines))
# revision plan references the Must-Fix -> clean (no W2)
code, _ = trace(ledger, letter_clean, sc_ok, revision_texts=["Session 1: fix F-P5-01 (the pacing)."])
check("revision_covers_mustfix", code == 0)
# W2 follow-through: revision plan present but the Must-Fix not referenced -> advisory, strict ERROR
code_w, lines_w = trace(ledger, letter_clean, sc_ok, revision_texts=["Session 1: polish only."])
check("w2_followthrough_advisory",
code_w == 0 and any("W2 follow-through" in ln and "F-P5-01" in ln for ln in lines_w))
code_s, _ = trace(ledger, letter_clean, sc_ok, revision_texts=["Session 1: polish only."], strict=True)
check("w2_followthrough_strict_fails", code_s == 1)
# --- Increment 3: revision-completion (`revised`) lifecycle ---
sc_revised = sidecar({"F-P5-01": "revised", "F-P5-02": "locked"})
sc_delivered = sidecar({"F-P5-01": "delivered", "F-P5-02": "delivered"})
# Completion = an explicit `<!-- resolved: ID -->` marker (NOT a bare mention).
rev_done_01 = "# Revision Report\n## Flags resolved\nPacing now lands. <!-- resolved: F-P5-01 -->"
rev_done_02 = "# Revision Report\n## Flags resolved\nStakes reworked. <!-- resolved: F-P5-02 -->"
# A legitimate report naming a worked-but-unresolved finding (no resolved marker).
rev_still_present = ("# Revision Report\n## Flags still present\nF-P5-01 remains present; the "
"attempted fix did not land.\n")
# E5 phantom completion: the in-scope report MENTIONS F-P5-01 (still present, no resolved
# marker) but the sidecar marks it revised -> the report and the sidecar disagree.
rev_contradict = ("# Revision Report\n## Flags still present\nF-P5-01 remains present.\n"
"## Flags resolved\nStakes reworked. <!-- resolved: F-P5-02 -->")
code, lines = trace(ledger, letter_clean, sc_revised,
revision_texts=[rev_contradict], completion_texts=[rev_contradict])
check("e5_phantom_completion",
code == 1 and any("E5 phantom" in ln and "F-P5-01" in ln for ln in lines))
# E5 scope (PR #32 re-review P1): finding_states is a rolling all-session map. A finding
# revised in an EARLIER round, with a new report resolving only F-P5-02 and NOT mentioning
# F-P5-01, must NOT E5-fail F-P5-01 (it was resolved out of scope).
code, lines = trace(ledger, letter_clean, sc_revised,
revision_texts=[rev_done_02], completion_texts=[rev_done_02])
check("e5_no_falsepos_out_of_scope",
code == 0 and not any("E5 phantom" in ln for ln in lines))
# E5 needs a completion artifact mentioning the finding: revised state, no completion -> no E5
code, _ = trace(ledger, letter_clean, sc_revised)
check("e5_skipped_without_completion", code == 0)
# --- M1c: event-scoped E5 (docs/finding-lifecycle-ids.md:112) ---
# The widened set is comp_mentioned ∪ event_advanced; the predicate is unchanged.
# (i) POSITIVE — post-clear tampering. A governed clearing event advanced F-P5-01 to revised,
# its resolved marker has since been REMOVED, and the id is mentioned NOWHERE in the current
# report. Silent under the old comp-mention scope; caught once F-P5-01 is event_advanced.
rev_no_mention = "# Revision Report\n## Flags resolved\nStakes reworked. <!-- resolved: F-P5-02 -->"
code, lines = trace(ledger, letter_clean, sc_revised,
revision_texts=[rev_no_mention], completion_texts=[rev_no_mention],
event_advanced={"F-P5-01"})
check("m1c_e5_event_scope_positive",
code == 1 and any("E5 phantom" in ln and "F-P5-01" in ln for ln in lines))
# ...and WITHOUT event_advanced (ungoverned) the same inputs stay silent — byte-identical today.
code0, lines0 = trace(ledger, letter_clean, sc_revised,
revision_texts=[rev_no_mention], completion_texts=[rev_no_mention])
check("m1c_e5_ungoverned_silent",
code0 == 0 and not any("E5 phantom" in ln for ln in lines0))
# (ii) INTRA-RUN CROSS-ARTIFACT (pinned): F-P5-01 is event_advanced AND bare-mentioned in
# completion artifact B, but its <!-- resolved --> marker lives in SIBLING completion artifact A
# of the same run. The resolved_ids union across ALL completion artifacts sees the marker -> the
# id is in resolved_ids -> stays PASS even under the widened scope.
comp_a_marker = "# Revision Report A\n## Flags resolved\n<!-- resolved: F-P5-01 -->"
comp_b_mention = "# Revision Report B\n## Notes\nF-P5-01 came up again in discussion.\n"
code, lines = trace(ledger, letter_clean, sc_revised,
revision_texts=[comp_a_marker, comp_b_mention],
completion_texts=[comp_a_marker, comp_b_mention],
event_advanced={"F-P5-01"})
check("m1c_e5_intra_run_cross_artifact_pass",
code == 0 and not any("E5 phantom" in ln for ln in lines))
# (iii) event_advanced but state NOT revised (e.g. still delivered) -> predicate unchanged, no E5
code, lines = trace(ledger, letter_clean, sc_delivered,
revision_texts=[rev_no_mention], completion_texts=[rev_no_mention],
event_advanced={"F-P5-01"})
check("m1c_e5_event_scope_needs_revised_state",
code == 0 and not any("E5 phantom" in ln for ln in lines))
# event_advanced_revised_ids: ungoverned sidecar (no gate_events) -> empty set (byte-identity guard)
ung = json.dumps({"execution": {"finding_states": {"F-P5-01": "revised"}}})
check("m1c_event_advanced_ungoverned_empty",
event_advanced_revised_ids(ung, os.getcwd(), os.path.join(os.getcwd(), "Diagnostic_State.meta.json")) == set())
# revised + explicitly marked resolved -> clean (no E5, no W3)
code, _ = trace(ledger, letter_clean, sc_revised,
revision_texts=[rev_done_01], completion_texts=[rev_done_01])
check("revised_with_completion_clean", code == 0)
# W3 completion follow-through: marked resolved but state still `delivered`
code_w, lines_w = trace(ledger, letter_clean, sc_delivered,
revision_texts=[rev_done_01], completion_texts=[rev_done_01])
check("w3_completion_advisory",
code_w == 0 and any("W3 completion" in ln and "F-P5-01" in ln for ln in lines_w))
code_s, _ = trace(ledger, letter_clean, sc_delivered,
revision_texts=[rev_done_01], completion_texts=[rev_done_01], strict=True)
check("w3_completion_strict_fails", code_s == 1)
# REGRESSION (PR #32 review P1): a worked-but-unresolved finding ("Flags still present", no
# resolved marker) must NOT count as completed -> no W3, and must pass even under --strict.
code, lines = trace(ledger, letter_clean, sc_delivered,
revision_texts=[rev_still_present], completion_texts=[rev_still_present])
check("w3_skips_unresolved_mention", code == 0 and not any("W3 completion" in ln for ln in lines))
check("w3_strict_ok_on_still_present",
trace(ledger, letter_clean, sc_delivered, revision_texts=[rev_still_present],
completion_texts=[rev_still_present], strict=True)[0] == 0)
# ...and a still-present finding shows rev=planned (mentioned), not rev=done (resolved)
check("still_present_not_done",
any("F-P5-01" in ln and "rev=planned" in ln for ln in lines))
# W3 needs a parseable sidecar to judge advancement: completion present, no sidecar -> skipped
code, _ = trace(ledger, letter_clean, None,
revision_texts=[rev_done_01], completion_texts=[rev_done_01])
check("w3_skipped_without_sidecar", code == 0)
# graceful: ledger-only run skips letter + sidecar dimensions
code, lines = trace(ledger, None, None)
check("ledger_only_graceful", code == 0 and any("letter trace skipped" in ln for ln in lines))
# no ledger findings -> nothing to trace (exit 0)
code, _ = trace("# empty\n", letter_clean, sc_ok)
check("no_findings_noop", code == 0)
# exact-boundary ID match: F-P5-011 must NOT satisfy a citation of F-P5-01
led1 = "## L\n" + finding("F-P5-011") + "\n"
let1 = "x <!-- finding: F-P5-01 -->\n" # cites F-P5-01, ledger has only F-P5-011
code, lines = trace(led1, let1, None)
check("exact_boundary_ids", code == 1 and any("F-P5-01" in ln and "E1" in ln for ln in lines))
# E6 — dangling retcon source (Retcon Planning F3): a retcon_item `source` finding-ref must
# resolve to a ledger finding. Matched by id; a malformed source is retcon-plan's R1, not E6.
def retcon(source):
return ('# Proj Retcon Plan\n## Retcon Targets\n- T1: x\n'
'<!-- apodictic:retcon_item\n{"schema":"apodictic.retcon_item.v1","id":"RX-01",'
'"target_id":"T1","kind":"setup-debt","mutability":"free","retcon_type":"dramatic",'
'"intervention_class":"plant a detail","disposition":"author seeds it","source":"%s"}\n-->\n'
% source)
code, lines = trace(ledger, None, None, retcon_texts=[retcon("F-P5-01")])
check("e6_source_resolves_clean", code == 0 and any("retcon source(s) traced" in ln for ln in lines))
code, lines = trace(ledger, None, None, retcon_texts=[retcon("F-P8-77")])
check("e6_dangling_source",
code == 1 and any("E6 dangling retcon source" in ln and "F-P8-77" in ln for ln in lines))
code, lines = trace(ledger, None, None, retcon_texts=[retcon("F-P8-7")]) # one-digit suffix
check("e6_malformed_not_flagged", code == 0 and not any("E6" in ln for ln in lines))
code, _ = trace(ledger, None, None, retcon_texts=["# plan\nno retcon blocks\n"])
check("e6_no_source_noop", code == 0)
# run-folder resolution + explicit-file classification
d = tempfile.mkdtemp()
made.append(d)
with open(os.path.join(d, "Proj_Findings_Ledger_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(ledger)
with open(os.path.join(d, "Proj_Core_DE_Synthesis_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(letter_clean)
with open(os.path.join(d, "Diagnostic_State.meta.json"), "w", encoding="utf-8", newline="") as fh:
fh.write(sc_ok)
with open(os.path.join(d, "Proj_Session_Plan_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write("# Session 1\nAddress F-P5-01 (pacing) and F-P5-02 (stakes).\n")
with open(os.path.join(d, "Proj_Revision_Report_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write("# Revision Report\n## Flags resolved\nPacing now lands. <!-- resolved: F-P5-01 -->\n")
with open(os.path.join(d, "Proj_Retcon_Plan_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(retcon("F-P5-02")) # F3: source resolves to the ledger -> no E6
rc_code, rc_lines = run([d])
check("run_folder_resolution", rc_code == 0)
check("run_folder_rev_done", any("rev=done" in ln for ln in rc_lines)) # F-P5-01 resolved marker
check("run_folder_rev_planned", any("rev=planned" in ln for ln in rc_lines)) # F-P5-02 in plan only
check("run_folder_retcon_traced", any("retcon source(s) traced" in ln for ln in rc_lines))
check("explicit_files_classify",
run([os.path.join(d, "Proj_Findings_Ledger_run.md"),
os.path.join(d, "Proj_Core_DE_Synthesis_run.md"),
os.path.join(d, "Diagnostic_State.meta.json")])[0] == 0)
check("explicit_files_completion",
any("rev=done" in ln for ln in run([os.path.join(d, "Proj_Findings_Ledger_run.md"),
os.path.join(d, "Proj_Revision_Report_run.md")])[1]))
# (glob narrowing) a Revision *Calendar* carrying a resolved marker is NOT a completion —
# so its marker must not advance any finding to rev=done. Genuine regression guard: pre-fix
# the broad *_Revision_*.md glob classified a calendar as a completion.
with open(os.path.join(d, "Proj_Revision_Calendar_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write("# Revision Calendar\n<!-- resolved: F-P5-01 -->\n")
check("calendar_not_completion",
not any("rev=done" in ln for ln in run([os.path.join(d, "Proj_Findings_Ledger_run.md"),
os.path.join(d, "Proj_Revision_Calendar_run.md")])[1]))
check("missing_ledger_usage_error", run([os.path.join(d, "Diagnostic_State.meta.json")])[0] == 2)
# --- M1c integration: a governed run folder whose clearing event advanced F-P5-01 to revised,
# marker since removed and the id mentioned nowhere -> run([folder]) surfaces E5 (event-scoped).
dm = tempfile.mkdtemp()
made.append(dm)
with open(os.path.join(dm, "Proj_Findings_Ledger_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(ledger)
# governed sidecar: state_version 2 + a clearing revision_round event for THIS run folder ("."),
# finding_deltas advancing F-P5-01 to revised; folded finding_states carries revised.
gov = {"execution": {"state_version": 2, "phase": "revision_round",
"finding_states": {"F-P5-01": "revised"},
"gate_events": [
{"gate": "revision_round", "result": "passed", "provenance": "mechanical",
"run_folder": ".", "attested_contract": [], "attested_items": [],
"checks": [{"validator": "x", "result": "ok"}],
"finding_deltas": {"F-P5-01": "revised"}}]}}
with open(os.path.join(dm, "Diagnostic_State.meta.json"), "w", encoding="utf-8", newline="") as fh:
json.dump(gov, fh)
# a completion report that mentions F-P5-02 only (F-P5-01 marker removed, not mentioned)
with open(os.path.join(dm, "Proj_Revision_Report_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write("# Revision Report\n## Flags resolved\n<!-- resolved: F-P5-02 -->\n")
mc_code, mc_lines = run([dm])
check("m1c_run_folder_event_scope_e5",
mc_code == 1 and any("E5 phantom" in ln and "F-P5-01" in ln for ln in mc_lines))
# sibling artifact carrying the F-P5-01 marker clears it (resolved_ids union) -> back to PASS
with open(os.path.join(dm, "Proj_Revision_Report_sibling.md"), "w", encoding="utf-8", newline="") as fh:
fh.write("# Revision Report (sibling)\n<!-- resolved: F-P5-01 -->\n")
mc2_code, _ = run([dm])
check("m1c_run_folder_sibling_marker_clears", mc2_code == 0)
for d in made:
shutil.rmtree(d, ignore_errors=True)
print("Self-test: PASS" if rc["v"] == 0 else "Self-test: FAIL")
return rc["v"]
def main(argv):
if "--self-test" in argv:
return run_self_test()
args = [a for a in argv[1:] if a not in ("finding-trace",)]
strict = "--strict" in args
paths = [a for a in args if not a.startswith("--")]
if not paths:
print("Usage: finding_trace.py finding-trace <run_folder|files...> [--strict] | --self-test")
return 2
code, lines = run(paths, strict=strict)
for ln in lines:
print(ln)
return code
if __name__ == "__main__":
sys.exit(main(sys.argv))
SHA-256: 90699a1dd9d07ec1abf97b27e026db6a86df9cd2d479dad6ed5d09e1c8d57457