← Files APODICTIC Development EditorARCHIVED FILE
scripts/disposition_check.py
44.7 KB · Nov 3, 2026 · 18:57 UTC
#!/usr/bin/env python3
"""disposition-check — engine-level finding-disposition integrity (docs/finding-dispositions.md).
`validate.sh disposition-check <run_folder|sidecar> [readiness_assessment] [--strict]` shells out
here. A finding disposition (`declined`/`deferred`) is an author decision recorded as an OVERLAY on
the finding lifecycle — the sidecar map `execution.finding_dispositions` mirroring the pinned
durable markers (`<!-- declined: F-… — reason -->` / `<!-- deferred: F-… until: trigger — reason
-->`, grammar SSoT: `apodictic_artifacts.parse_disposition_markers`). This validator owns the
artifact-side audit; `run_gate.py --check-state` owns the log-side one for governed projects
(the finding-trace / gate-state ownership split).
DP0 record shape every `finding_dispositions` record validates against
apodictic.finding_disposition.v1 (closed-key); `trigger` present iff
`deferred`; `reason` non-empty; map key == record id. ERROR
DP1 caveat teeth when a readiness assessment (*Submission_Readiness_Assessment_*.md) is
present and an ACTIVE (not superseded — see DP2.6) declined/deferred
Must-Fix exists (sidecar map or markers), the assessment must carry the
pinned caveat line(s) — `**Declined Must-Fixes:** …` / `**Deferred
Must-Fixes:** …` — naming EVERY such id (a mechanical per-family id-set
comparison, not a substring check). Missing line or missing id -> a
verdict is absorbing a set-aside finding. ERROR
DP2 no laundering 2.1 the same run's completion artifacts carry both `<!-- resolved: F-X -->`
and an active declined/deferred marker for F-X (each direction launders
the other); ERROR
2.2 a `finding_dispositions` key that is not a ledger finding id (the
finding-trace E2 pattern applied to the new map); ERROR
2.3 a dispositioned id whose ledger severity token differs from the
letter's Severity Calibration `locked` value (read-only; the
softness-check ID-match approach); ERROR
2.4 `triage_summary.<sev>` LESS THAN the ledger tally at a severity with a
dispositioned finding — a disposition write that "improved" the deficit
count (equality overall stays structured-findings' job; the tally is
structured_findings.severity_tally, the exact logic it audits); ERROR
2.5 bidirectional marker/sidecar sync — every active marker has a record
and every active record has a marker in some home (WARN; ERROR under
--strict; a governed project's marker awaiting its next revision_round
fold is lag, not divergence — exempt).
2.6 uncorroborated supersedence — a dispositioned id whose finding_states
reads 'revised' with no <!-- resolved: F-… --> marker in any reachable
completion artifact (run folder, project root, runs/* archives).
Supersedence is RECOMPUTED, never trusted (the PR #161 recorded-field
rule applied to an exemption gate): an uncorroborated 'revised' leaves
the disposition ACTIVE everywhere — DP1 still owes its caveat, DP2.5
still audits sync (WARN; ERROR under --strict).
Marker homes: the project Diagnostic_State.md Coaching Log, `*_Revision_Report_*.md`, and
`*_Feedback_Triage_*.md`. Each artifact is optional; a missing one skips its dimension (no false
failure). What deliberately reads dispositions NOWHERE: softness-check, deficit-lock,
severity-floor, structured-findings, finding-trace, regression-diff — a disposition is not an
override marker and grants no honesty-gate relief (the laundering firewall). The `/ready` workflow
runs `validate.sh disposition-check <sidecar> <assessment>` BEFORE delivering a verdict; a DP1
ERROR blocks the verdict. Degrades to an advisory WARN without python3 (the bash dispatcher).
Supersedence trust model (DP2.6, docs/disposition-supersedence-recompute.md): corroboration
accepts any regex-matched <!-- resolved: F-id --> marker in a reachable *_Revision_Report_*.md —
evidence artifacts are NOT schema-validated or provenance-checked (markers are the canonical
record, docs/finding-dispositions.md §Schema 3; the recompute raises the fabrication bar from one
JSON field edit to forging a completion artifact — the artifact-forgery floor every
marker-canonical surface shares, by design). Corroboration deliberately accepts markers quoted
in code spans / fenced blocks: resolved_cited_ids is the same raw scan the governed fold WRITER
uses (run_gate._finding_deltas), so the checker is exactly as strict as the producer — any
hardening belongs at that shared SSoT, never locally here. Evidence is gathered from the project
root and its runs/* archives whenever a sidecar was LOCATED (a run-folder arg's walk-up or an
explicit .json arg both count); only a no-sidecar invocation gathers none — remediate by
invoking via the run folder, project root, or sidecar.
disposition_check.py disposition-check <run_folder|sidecar|files...> [--strict]
disposition_check.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
try:
import finding_trace as ft
except ImportError:
ft = None
try:
import structured_findings as sf
except ImportError:
sf = None
_SCHEMA_ID = "apodictic.finding_disposition.v1"
_LEDGER_GLOB = "*_Findings_Ledger_*.md"
_REPORT_GLOB = "*_Revision_Report_*.md"
_TRIAGE_GLOB = "*_Feedback_Triage_*.md"
_ASSESS_GLOB = "*Submission_Readiness_Assessment_*.md"
_STATE_MD = "Diagnostic_State.md"
# Pinned caveat-line grammar (submission-readiness.md §Output Template): a line-anchored family
# header, then `; `-separated `F-… — …` entries; ids extracted with the boundary-guarded id regex
# (art.FID_RE — the finding_trace._ID_RE pattern) and compared as ID SETS per family.
_CAVEAT_LINE_RE = re.compile(r"^\*\*(Declined|Deferred) Must-Fixes:\*\*(.*)$", re.MULTILINE)
_SEV_KEY = {"Must-Fix": "must_fix", "Should-Fix": "should_fix", "Could-Fix": "could_fix"}
def _read(path):
# UnicodeDecodeError included: a non-UTF8 artifact (e.g. an archived evidence report on the
# DP2.6 surface) must degrade to the honest skip/fail-closed path, never a traceback.
try:
with open(path, encoding="utf-8") as fh:
return fh.read()
except (OSError, UnicodeDecodeError):
return None
def _newest(paths):
return max(paths, key=os.path.getmtime) if paths else None
def _glob_multi(dirs, pattern):
out = []
for d in dirs:
out.extend(glob.glob(os.path.join(d, pattern)))
return sorted(set(out))
def _resolve(paths):
"""Resolve inputs to (sidecar, state_md, ledger, letter, reports, triages, assessment,
evidence_reports).
One dir arg = a run folder (walk up for the sidecar; the Coaching Log home is the sidecar's
SIBLING Diagnostic_State.md). One .json arg = the sidecar itself (project-root mode: artifacts
are globbed from the project root and its runs/*/ archives — the /ready invocation shape).
Explicit .md files classify by the output-structure.md naming convention.
evidence_reports = every completion artifact reachable from the located sidecar (project root
+ runs/*/ archives) — the DP2.6 supersedence-corroboration surface ONLY. It never feeds the
primary report set, so DP2.1's same-run scoping and DP2.5's marker homes are untouched by the
wider glob. No sidecar located (explicit-files mode) -> empty; the caller controls the
evidence surface."""
sidecar = state_md = ledger = letter = assessment = None
reports, triages = [], []
dirs = []
for p in paths:
base = os.path.basename(p)
if os.path.isdir(p):
dirs.append(p)
elif p.endswith(".json"):
sidecar = p
elif base == _STATE_MD:
state_md = p
elif "Submission_Readiness_Assessment_" in base:
assessment = p
elif "_Feedback_Triage_" in base:
triages.append(p)
elif "_Revision_Report_" in base:
reports.append(p)
elif "_Findings_Ledger_" in base or _has_finding_block(p):
ledger = p
else:
letter = p
if dirs:
folder = dirs[0]
sidecar = sidecar or (ft._walk_up_sidecar(folder) if ft else None)
scan = [folder]
elif sidecar:
scan = [os.path.dirname(sidecar) or "."]
scan += sorted(glob.glob(os.path.join(scan[0], "runs", "*")))
else:
scan = []
if scan:
ledger = ledger or _newest(_glob_multi(scan, _LEDGER_GLOB))
if letter is None and ft is not None:
for g in ft._LETTER_GLOBS:
letter = _newest(_glob_multi(scan, g))
if letter:
break
reports = reports or _glob_multi(scan, _REPORT_GLOB)
triages = triages or _glob_multi(scan, _TRIAGE_GLOB)
assessment = assessment or _newest(_glob_multi(scan, _ASSESS_GLOB))
if state_md is None and sidecar:
cand = os.path.join(os.path.dirname(sidecar) or ".", _STATE_MD)
if os.path.exists(cand):
state_md = cand
evidence = []
if sidecar:
root = os.path.dirname(sidecar) or "."
evidence = _glob_multi([root] + sorted(glob.glob(os.path.join(root, "runs", "*"))),
_REPORT_GLOB)
return sidecar, state_md, ledger, letter, reports, triages, assessment, evidence
def _has_finding_block(path):
"""Classify a candidate ledger on PARSED blocks (the _has_block idiom, meta-linter M2)."""
if art is None:
return False
text = _read(path) or ""
return any(bt == "finding" for bt, _o, _e in art.parse_blocks(text))
def _validate_record(fid, rec, where, schema):
"""DP0 for one record: schema shape + the trigger-iff-deferred conditional + non-empty reason
+ map-key/id agreement. The artifact-side twin of run_gate check_state's log-side checks."""
errs = []
if not isinstance(rec, dict):
return ["%s: not a disposition record object" % where]
errs.extend(art.validate_obj(rec, schema, where))
if rec.get("id") != fid:
errs.append("%s: record id %r does not match its map key" % (where, rec.get("id")))
if rec.get("disposition") == "deferred":
if not (isinstance(rec.get("trigger"), str) and rec["trigger"].strip()):
errs.append("%s: deferred requires a non-empty 'trigger' — a deferral without a "
"recorded trigger is a decline with extra steps" % where)
elif "trigger" in rec:
errs.append("%s: 'trigger' only valid on a deferred disposition" % where)
if not (isinstance(rec.get("reason"), str) and rec["reason"].strip()):
errs.append("%s: 'reason' must be non-empty" % where)
return errs
def check(sidecar_obj, ledger_text, letter_text, report_texts, triage_texts, state_text,
assessment_text, strict=False, sidecar_parse_ok=True, *, evidence_texts=None):
"""Run the disposition audit over already-read inputs. Returns (code, lines).
evidence_texts = archived completion artifacts (project root + runs/*) read for DP2.6
supersedence corroboration ONLY — never merged into report_texts (DP2.1/DP2.5 scoping)."""
lines, errs, warns, notes = [], [], [], []
if art is None or ft is None:
return 2, ["disposition-check: apodictic_artifacts/finding_trace unavailable — cannot run"]
if not sidecar_parse_ok:
errs.append("DP0 unparseable sidecar: Diagnostic_State.meta.json is present but not valid "
"JSON — the disposition map cannot be verified")
sidecar_obj = None
ex = (sidecar_obj or {}).get("execution") or {}
records = ex.get("finding_dispositions") or {}
if not isinstance(records, dict):
errs.append("DP0: execution.finding_dispositions must be an object (map F-id -> record)")
records = {}
finding_states = ex.get("finding_states") or {}
if not isinstance(finding_states, dict):
finding_states = {}
# Governed = the sidecar carries a non-empty gate_events log (state-lifecycle.md's dual-writer
# split). Non-empty, so the template's default empty list does not read as governed.
governed = bool(ex.get("gate_events"))
triage_summary = (sidecar_obj or {}).get("triage_summary")
# Marker scan — the three homes, via the shared grammar SSoT (never a local regex).
marker_homes = ([("coaching-log", state_text)]
+ [("revision-report", t) for t in (report_texts or [])]
+ [("feedback-triage", t) for t in (triage_texts or [])])
marker_homes = [(h, t) for h, t in marker_homes if t is not None]
markers = {} # fid -> last-seen marker dict (kind/trigger/reason)
report_marker_ids = set() # dispositions asserted by the run's completion artifacts (DP2.1)
for home, text in marker_homes:
for m in art.parse_disposition_markers(text):
markers[m["id"]] = dict(m, home=home)
if home == "revision-report":
report_marker_ids.add(m["id"])
resolved_ids = set()
for t in (report_texts or []):
resolved_ids |= ft.resolved_cited_ids(t)
# Supersedence corroboration surface: the run's own resolved markers PLUS the archived
# completion artifacts (evidence_texts — corroboration-only; never folded into resolved_ids
# or report_marker_ids, so DP2.1's same-run scoping stays exact).
corroborated = set(resolved_ids)
for t in (evidence_texts or []):
corroborated |= ft.resolved_cited_ids(t)
def active(fid):
# Supersedence (read-time precedence): finding_states is checked FIRST, everywhere — but
# RECOMPUTED, never trusted (the PR #161 recorded-field rule applied to an exemption
# gate): a sidecar 'revised' supersedes only when a <!-- resolved: fid --> marker
# corroborates it in some reachable completion artifact. Uncorroborated 'revised' leaves
# the disposition ACTIVE (fail-closed; DP2.6 surfaces the mismatch).
return not (finding_states.get(fid) == "revised" and fid in corroborated)
inv = ft.ledger_inventory(ledger_text) if ledger_text else {}
have_ledger = bool(inv)
# Severity lookup: ledger blocks first, the sidecar findings[] mirror as fallback.
severity = dict(inv)
for el in ((sidecar_obj or {}).get("findings") or []):
if isinstance(el, dict) and el.get("id") and el["id"] not in severity:
severity[art.fid_key(el["id"])] = el.get("severity")
# ---- DP0 — record shape --------------------------------------------------------------
schema = art.load_schema(_SCHEMA_ID)
for fid in sorted(records):
errs.extend("DP0 %s" % e for e in
_validate_record(fid, records[fid], "finding_dispositions[%s]" % fid, schema))
# ---- DP1 — declined/deferred Must-Fix caveat (the /ready teeth) ----------------------
# Active set = sidecar records ∪ markers (governed lag means a marker can precede its record).
dispositioned = {}
for fid, rec in records.items():
if isinstance(rec, dict) and rec.get("disposition") in ("declined", "deferred"):
dispositioned[fid] = rec["disposition"]
for fid, m in markers.items():
dispositioned.setdefault(fid, m["kind"])
active_dispo = {fid: kind for fid, kind in dispositioned.items() if active(fid)}
if assessment_text is None:
notes.append("no readiness assessment present — DP1 caveat check skipped")
else:
captured = {"declined": set(), "deferred": set()}
for fam, rest in _CAVEAT_LINE_RE.findall(assessment_text):
captured[fam.lower()].update(art.FID_RE.findall(rest))
for fid in sorted(active_dispo):
kind = active_dispo[fid]
sev = severity.get(fid)
if sev is None:
warns.append("DP1 severity unresolvable for dispositioned %s (no ledger block or "
"sidecar findings[] entry) — caveat coverage cannot be verified" % fid)
continue
if sev != "Must-Fix":
continue # lower tiers are informational (Diagnostic Summary), not caveat material
# Per-family id-set comparison: active set ⊆ captured set. Deliberately ONE-directional
# (subset-legal): an EXTRA id on a caveat line is over-disclosure, never laundering —
# only an absorbed (missing) id has teeth.
if fid not in captured[kind]:
errs.append("DP1 %s Must-Fix %s absent from the readiness caveat — a verdict is "
"absorbing a %s finding (pinned line: '**%s Must-Fixes:** …')"
% (kind, fid, kind, kind.capitalize()))
# ---- DP2 — no severity laundering / no deficit improvement ---------------------------
# 2.1 contradiction, scoped to the run's COMPLETION artifacts: the same report set both
# resolves F-X and carries an active disposition marker for it — each direction launders the
# other. (A Coaching-Log decline later resolved by a NEW round's report is legitimate
# supersedence and is NOT this class — hence the report-only scope.) By the same design,
# a live contradiction whose disposition marker homes in the Coaching Log or a triage
# artifact (resolved in the report, dispositioned elsewhere) is deliberately OUT of DP2.1's
# scope — both resolved_ids and report_marker_ids gather from the report home only. The
# completion-side consistency of that cross-home case is backstopped by finding-trace E5
# (phantom completion) / W3 (completion follow-through), and the residual marker/sidecar
# divergence by DP2.5.
for fid in sorted(resolved_ids & report_marker_ids):
if active(fid):
errs.append("DP2.1 contradiction: the run's completion artifacts carry both "
"<!-- resolved: %s --> and an active %s marker for it" % (fid, markers[fid]["kind"]))
# 2.2 phantom map key (the finding-trace E2 pattern applied to the new map). Sidecar-scoped
# LEDGER-integrity only: DP2.2 never reads triage artifacts — the triage-artifact side of
# decline reconciliation belongs to feedback-triage W3 (the ownership boundary; see
# feedback_triage.py W3 for the matching comment).
if have_ledger:
for fid in sorted(records):
if fid not in inv:
errs.append("DP2.2 phantom: finding_dispositions[%s] — not in the ledger" % fid)
elif records:
notes.append("no findings ledger present — DP2.2 phantom / DP2.3 severity / DP2.4 tally skipped")
# 2.3 severity immutability: the record schema is closed-key with NO severity field (a
# disposition structurally cannot carry one); additionally, a dispositioned id's ledger
# severity must equal the letter's Severity Calibration `locked` value — a post-hoc ledger
# edit is the remaining channel. Read-only; a legitimate override-marked DELIVERED downgrade
# stays softness-check's business.
if have_ledger and letter_text:
calib = {}
for bt, obj, _e in art.parse_blocks(letter_text):
if bt == "severity_calibration" and isinstance(obj, dict) and obj.get("id"):
calib[art.fid_key(obj["id"])] = obj.get("locked")
for fid in sorted(dispositioned):
if fid in inv and fid in calib and inv[fid] != calib[fid]:
errs.append("DP2.3 severity mismatch: dispositioned %s is %r in the ledger but "
"locked %r in the letter's Severity Calibration" % (fid, inv[fid], calib[fid]))
# 2.4 tally guard: a disposition write that "improved" the deficit count. Recompute the ledger
# tally with structured_findings.severity_tally — the EXACT logic structured-findings audits
# triage_summary with, factored shared so this guard cannot drift from it. DP2 owns only the
# disposition-shaped DECREMENT (<); overall equality stays structured-findings' job.
if have_ledger and isinstance(triage_summary, dict) and sf is not None and dispositioned:
ledger_objs = [obj for bt, obj, _e in art.parse_blocks(ledger_text)
if bt == "finding" and isinstance(obj, dict)]
ledger_tally = sf.severity_tally(ledger_objs)
dispo_sevs = {severity.get(fid) for fid in dispositioned} & set(_SEV_KEY)
for sev in sorted(dispo_sevs):
key = _SEV_KEY[sev]
try:
got = int(triage_summary.get(key, 0))
except (TypeError, ValueError):
continue # a malformed triage_summary value is structured-findings' E, not DP2.4's
if got < ledger_tally[key]:
errs.append("DP2.4 tally decrement: triage_summary.%s=%d but the ledger carries %d "
"%s finding(s) — a disposition never decrements a count" % (key, got, ledger_tally[key], sev))
# 2.5 bidirectional marker/sidecar sync (active only; WARN, ERROR under --strict).
if not marker_homes:
notes.append("no marker homes present (Coaching Log / Revision Report / Feedback Triage) "
"— DP2.5 sync check skipped")
else:
for fid in sorted(markers):
if active(fid) and fid not in records:
if governed:
notes.append("DP2.5: active %s marker for %s awaits its revision_round fold "
"(governed lag, not divergence)" % (markers[fid]["kind"], fid))
else:
warns.append("DP2.5 marker without record: active %s marker for %s has no "
"finding_dispositions entry" % (markers[fid]["kind"], fid))
for fid in sorted(records):
if active(fid) and fid not in markers:
warns.append("DP2.5 record without marker: active finding_dispositions[%s] has no "
"disposition marker in any home — the markers are the canonical source" % fid)
# 2.6 uncorroborated supersedence — the recompute backstop for the active() exemption (the
# PR #161 recorded-field rule): a dispositioned id whose finding_states says 'revised' with no
# <!-- resolved --> marker in any reachable completion artifact. Fail-closed: active() already
# treats it as ACTIVE above (DP1 caveat owed, DP2.5 audited); this WARN names the mismatch
# even when no assessment is present (no verdict to launder yet) or the caveat is already
# over-disclosed. Dispositioned ids only — an undispositioned 'revised' consumes no exemption.
for fid in sorted(dispositioned):
if finding_states.get(fid) == "revised" and fid not in corroborated:
warns.append("DP2.6 uncorroborated supersedence: finding_states[%s]=revised but no "
"<!-- resolved: %s --> marker in any reachable completion artifact — "
"the disposition is treated as ACTIVE (DP1 caveat still owed, DP2.5 sync "
"still audited). If genuinely revised, make the completion report "
"reachable (run folder, project root, or runs/ archives)" % (fid, fid))
# ---- report --------------------------------------------------------------------------
lines.append("disposition-check: %d record(s), %d marker(s)%s"
% (len(records), len(markers),
"" if sidecar_obj is not None or not sidecar_parse_ok else " (no sidecar)"))
for fid in sorted(set(list(records) + list(markers))):
rec = records.get(fid) if isinstance(records.get(fid), dict) else {}
kind = rec.get("disposition") or markers.get(fid, {}).get("kind", "?")
lines.append(" %-12s %-9s sev=%-9s state=%-9s %s"
% (fid, kind, severity.get(fid, "?"), finding_states.get(fid, "—"),
"superseded" if not active(fid) else "active"))
for n in notes:
lines.append(" note: %s" % n)
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("disposition-check: 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: disposition-check: %d advisory warning(s) — see the WARN lines above" % len(warns))
else:
lines.append("disposition-check: PASS (record shape + caveat coverage + no-laundering)")
return 0, lines
def run(paths, strict=False):
if art is None or ft is None:
return 2, ["disposition-check: apodictic_artifacts/finding_trace unavailable — cannot run"]
sidecar, state_md, ledger, letter, reports, triages, assessment, evidence = _resolve(paths)
if not sidecar and not (reports or triages or state_md):
return 2, ["disposition-check: nothing to check — need a run folder, a "
"Diagnostic_State.meta.json sidecar, or disposition-marker artifacts"]
sidecar_obj, sc_ok = None, True
if sidecar:
raw = _read(sidecar)
try:
sidecar_obj = json.loads(raw) if raw is not None else None
sc_ok = raw is not None and isinstance(sidecar_obj, dict)
except ValueError:
sidecar_obj, sc_ok = None, False
return check(sidecar_obj,
_read(ledger) if ledger else None,
_read(letter) if letter else None,
[t for t in (_read(r) for r in reports) if t is not None],
[t for t in (_read(t_) for t_ in triages) if t is not None],
_read(state_md) if state_md else None,
_read(assessment) if assessment else None,
strict=strict, sidecar_parse_ok=sc_ok,
evidence_texts=[t for t in (_read(e) for e in evidence) if t is not None])
# ---------------------------------------------------------------- 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 (this module ORIGINATED the disposition_check adjacent-exception
# class in PR #162; its own _read was left OSError-only until the class sweep)
_fd, _nu = tempfile.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 = ("## Ledger\n" + finding("F-P5-01") + "\n" + finding("F-DP-02") + "\n"
+ finding("F-P5-03", "Should-Fix") + "\n")
def rec(fid, kind="declined", **kw):
r = {"schema": _SCHEMA_ID, "id": fid, "disposition": kind, "reason": "held on purpose",
"source": "author", "session": 3, "ts": "2026-07-01T00:00:00Z"}
if kind == "deferred":
r["trigger"] = "beta reads back"
r.update(kw)
return r
def sidecar(dispositions, states=None, triage=None, governed=False):
ex = {"finding_states": states or {}, "finding_dispositions": dispositions}
if governed:
ex["gate_events"] = [{"gate": "run_synthesis", "result": "mechanical-passed",
"provenance": "mechanical", "ts": "t"}]
sc = {"project": "P", "execution": ex}
if triage is not None:
sc["triage_summary"] = triage
return sc
coaching = ("## Coaching Log\n"
"<!-- declined: F-P5-01 — abstraction is the register -->\n"
"<!-- deferred: F-DP-02 until: beta reads back — waiting on reader signal -->\n")
states_delivered = {"F-P5-01": "delivered", "F-DP-02": "delivered", "F-P5-03": "delivered"}
both = {"F-P5-01": rec("F-P5-01"), "F-DP-02": rec("F-DP-02", "deferred")}
caveat = ("## Readiness Verdict: CONDITIONALLY VIABLE\n"
"**Declined Must-Fixes:** F-P5-01 — abstraction is the register (declined, session 3)\n"
"**Deferred Must-Fixes:** F-DP-02 — until: beta reads back — waiting on reader signal\n")
def go(dispositions=None, states=None, triage=None, governed=False, ledger_text=ledger,
letter=None, reports=(), triages=(), state=coaching, assessment=None, strict=False,
no_sidecar=False, evidence=()):
sc = None if no_sidecar else sidecar(dispositions if dispositions is not None else both,
states if states is not None else dict(states_delivered),
triage, governed)
return check(sc, ledger_text, letter, list(reports), list(triages), state, assessment,
strict=strict, evidence_texts=list(evidence))
# clean: records + markers in sync, no assessment (DP1 skipped with a note)
code, lines = go()
check_("clean_sync_passes", code == 0 and any("DP1 caveat check skipped" in ln for ln in lines))
# ---- DP0 ----
bad = dict(both); bad["F-P5-01"] = rec("F-P5-01", source="editor")
check_("dp0_bad_enum", go(dispositions=bad)[0] == 1)
bad = dict(both); bad["F-P5-01"] = rec("F-P5-01", reason=" ")
code, lines = go(dispositions=bad)
check_("dp0_empty_reason", code == 1 and any("'reason' must be non-empty" in ln for ln in lines))
bad = dict(both); bad["F-DP-02"] = {k: v for k, v in rec("F-DP-02", "deferred").items() if k != "trigger"}
code, lines = go(dispositions=bad)
check_("dp0_deferred_without_trigger_refused",
code == 1 and any("deferred requires a non-empty 'trigger'" in ln for ln in lines))
bad = dict(both); bad["F-P5-01"] = rec("F-P5-01", trigger="stray")
code, lines = go(dispositions=bad)
check_("dp0_trigger_on_declined_refused",
code == 1 and any("'trigger' only valid on a deferred" in ln for ln in lines))
bad = dict(both); bad["F-P5-01"] = rec("F-P5-01", severity="Could-Fix")
code, lines = go(dispositions=bad)
check_("dp0_no_severity_field_closed_key",
code == 1 and any("unknown field 'severity'" in ln for ln in lines))
bad = dict(both); bad["F-P5-01"] = rec("F-DP-02", "deferred")
code, lines = go(dispositions=bad)
check_("dp0_key_id_mismatch", code == 1 and any("does not match its map key" in ln for ln in lines))
# ---- DP1 ----
code, lines = go(assessment=caveat)
check_("dp1_caveat_present_clean", code == 0)
code, lines = go(assessment="## Readiness Verdict: READY\nNo caveats.\n")
check_("dp1_caveat_absent_fails",
code == 1 and any("DP1 declined Must-Fix F-P5-01" in ln for ln in lines)
and any("DP1 deferred Must-Fix F-DP-02" in ln for ln in lines))
# one id missing from its family line
code, lines = go(assessment="**Declined Must-Fixes:** F-P5-01 — held\n")
check_("dp1_missing_one_id",
code == 1 and any("F-DP-02" in ln and "DP1" in ln for ln in lines)
and not any("DP1 declined Must-Fix F-P5-01" in ln for ln in lines))
# an id on the WRONG family line is still absent from its own family
code, lines = go(assessment="**Declined Must-Fixes:** F-P5-01; F-DP-02\n")
check_("dp1_wrong_family_still_fails", code == 1 and any("DP1 deferred Must-Fix F-DP-02" in ln for ln in lines))
# an EXTRA caveat id is subset-legal over-disclosure — never an error
code, _ = go(assessment=caveat + "**Declined Must-Fixes:** F-ZZ-77 — over-disclosed\n")
check_("dp1_extra_id_subset_legal", code == 0)
# a superseded (revised) id is exempt — no caveat owed. The revised state carries its
# corroborating completion artifact (a <!-- resolved --> report): supersedence is recomputed
# from evidence, never trusted (docs/disposition-supersedence-recompute.md).
sup_states = dict(states_delivered, **{"F-P5-01": "revised"})
code, lines = go(states=sup_states, reports=["<!-- resolved: F-P5-01 -->\n"],
assessment="**Deferred Must-Fixes:** F-DP-02 — until: beta reads back\n")
check_("dp1_superseded_exempt", code == 0)
# KEYSTONE (the exploit repro, pre-fix exit 0): the SAME shape with the corroborating report
# withheld — a fabricated finding_states 'revised' alone must NOT waive the caveat.
code, lines = go(states=sup_states, assessment="**Deferred Must-Fixes:** F-DP-02 — until: beta reads back\n")
check_("dp1_fabricated_supersedence_still_owes_caveat",
code == 1 and any("DP1 declined Must-Fix F-P5-01" in ln for ln in lines)
and any("DP2.6" in ln for ln in lines))
# archived-round evidence (project root / runs/* — the rolling-map shape): the resolved
# marker arrives via evidence_texts only -> exempt, no DP2.6
code, lines = go(states=sup_states, evidence=["# Old report\n<!-- resolved: F-P5-01 -->\n"],
assessment="**Deferred Must-Fixes:** F-DP-02 — until: beta reads back\n")
check_("dp1_archived_evidence_exempt",
code == 0 and not any("DP2.6" in ln for ln in lines))
# a Should-Fix disposition needs no caveat (lower tiers are informational)
only_should = {"F-P5-03": rec("F-P5-03")}
code, _ = go(dispositions=only_should, state="<!-- declined: F-P5-03 — style call -->",
assessment="## Readiness Verdict: READY\n")
check_("dp1_should_fix_no_caveat_needed", code == 0)
# marker-only active disposition (no record yet) still forces the caveat — the laundering
# attempt "keep it out of the sidecar" has no purchase
code, lines = go(dispositions={}, governed=True, assessment="## Readiness Verdict: READY\n")
check_("dp1_marker_only_still_forces_caveat",
code == 1 and any("DP1 declined Must-Fix F-P5-01" in ln for ln in lines))
# severity unresolvable -> honest WARN, not silent pass, not false ERROR
code, lines = go(ledger_text=None, assessment="## Readiness Verdict: READY\n")
check_("dp1_severity_unresolvable_warns",
code == 0 and any("severity unresolvable" in ln for ln in lines))
# ---- DP2 ----
report_contra = ("# Revision Report\n<!-- resolved: F-P5-01 -->\n"
"<!-- declined: F-P5-01 — but also resolved? -->\n")
code, lines = go(reports=[report_contra])
check_("dp2_1_contradiction",
code == 1 and any("DP2.1 contradiction" in ln and "F-P5-01" in ln for ln in lines))
# legitimate supersedence: an OLD Coaching-Log decline + a new report resolving it (state revised)
code, lines = go(states=dict(states_delivered, **{"F-P5-01": "revised"}),
reports=["# Revision Report\n<!-- resolved: F-P5-01 -->\n"])
check_("dp2_1_supersedence_not_contradiction",
code == 0 and not any("DP2.1" in ln for ln in lines))
phantom = dict(both); phantom["F-ZZ-99"] = rec("F-ZZ-99")
code, lines = go(dispositions=phantom,
state=coaching + "<!-- declined: F-ZZ-99 — phantom -->\n")
check_("dp2_2_phantom", code == 1 and any("DP2.2 phantom" in ln and "F-ZZ-99" in ln for ln in lines))
letter_cal = ('# Letter\n<!-- apodictic:severity_calibration\n'
'{"schema":"apodictic.severity_calibration.v1","id":"F-P5-01","locked":"Should-Fix",'
'"delivered":"Should-Fix","direction":"unchanged","rationale":"r"}\n-->\n')
code, lines = go(letter=letter_cal)
check_("dp2_3_severity_mismatch",
code == 1 and any("DP2.3 severity mismatch" in ln and "F-P5-01" in ln for ln in lines))
letter_ok = letter_cal.replace('"locked":"Should-Fix"', '"locked":"Must-Fix"')
check_("dp2_3_matching_clean", go(letter=letter_ok)[0] == 0)
code, lines = go(triage={"must_fix": 1, "should_fix": 1, "could_fix": 0})
check_("dp2_4_tally_decrement",
code == 1 and any("DP2.4 tally decrement" in ln and "must_fix=1" in ln for ln in lines))
check_("dp2_4_honest_tally_clean", go(triage={"must_fix": 2, "should_fix": 1, "could_fix": 0})[0] == 0)
# DP2.5 marker without record (non-governed) — WARN by default, ERROR under --strict
code, lines = go(dispositions={"F-P5-01": rec("F-P5-01")})
check_("dp2_5_marker_without_record_warns",
code == 0 and any("DP2.5 marker without record" in ln and "F-DP-02" in ln for ln in lines))
check_("dp2_5_strict_escalates", go(dispositions={"F-P5-01": rec("F-P5-01")}, strict=True)[0] == 1)
# governed-lag exemption: same desync on a governed sidecar is a note, not a WARN
code, lines = go(dispositions={"F-P5-01": rec("F-P5-01")}, governed=True)
check_("dp2_5_governed_lag_exempt",
code == 0 and any("governed lag" in ln for ln in lines)
and not any("DP2.5 marker without record" in ln for ln in lines))
code, lines = go(state="## Coaching Log\n<!-- declined: F-P5-01 — abstraction is the register -->\n")
check_("dp2_5_record_without_marker_warns",
code == 0 and any("DP2.5 record without marker" in ln and "F-DP-02" in ln for ln in lines))
check_("dp2_5_record_without_marker_strict",
go(state="## Coaching Log\n<!-- declined: F-P5-01 — abstraction is the register -->\n",
strict=True)[0] == 1)
# ---- DP2.6 (supersedence recompute — docs/disposition-supersedence-recompute.md) ----
# uncorroborated 'revised' on a dispositioned id: WARN by default (the mismatch is named even
# with no assessment present), ERROR under --strict
code, lines = go(states=sup_states)
check_("dp2_6_uncorroborated_warns",
code == 0 and any("DP2.6 uncorroborated supersedence" in ln and "F-P5-01" in ln for ln in lines))
check_("dp2_6_strict_escalates", go(states=sup_states, strict=True)[0] == 1)
code, lines = go(states=sup_states, reports=["<!-- resolved: F-P5-01 -->\n"])
check_("dp2_6_corroborated_clean", code == 0 and not any("DP2.6" in ln for ln in lines))
# a fabricated 'revised' must NOT silence the DP2.5 sync audit (pre-fix it did): F-DP-02's
# record has no marker anywhere AND its state is fabricated 'revised' -> both warns fire
fab_states = dict(states_delivered, **{"F-DP-02": "revised"})
code, lines = go(states=fab_states,
state="## Coaching Log\n<!-- declined: F-P5-01 — abstraction is the register -->\n")
check_("dp2_5_not_suppressed_by_fabricated_revised",
code == 0 and any("DP2.5 record without marker" in ln and "F-DP-02" in ln for ln in lines)
and any("DP2.6" in ln and "F-DP-02" in ln for ln in lines))
# corroboration is evidence-ONLY: a resolved marker present only in evidence_texts must not
# enter resolved_ids -> no DP2.1 contradiction with a same-run disposition marker (DP2.1's
# same-run scoping stays exact; with a leak this exact shape would ERROR)
code, lines = go(reports=["# R\n<!-- declined: F-P5-01 — still declining -->\n"],
evidence=["<!-- resolved: F-P5-01 -->\n"])
check_("dp2_1_evidence_not_leaked", code == 0 and not any("DP2.1" in ln for ln in lines))
# ---- gardening (state-lifecycle.md §State Gardening) ----
# active disposition markers survive gardening and must still be FOUND in a gardened state
# file; a SUPERSEDED disposition compressed to the one-line archive form must NOT parse as an
# active marker (it is prose, not an HTML comment).
gardened = ("## Coaching Log\n"
"<!-- declined: F-P5-01 — abstraction is the register -->\n"
"<!-- deferred: F-DP-02 until: beta reads back — waiting on reader signal -->\n"
"## Archived\n"
"- F-P5-03: declined session 2, later revised session 5. "
"Full record: Diagnostic_State_Archive_2026-06-01T10-00.md\n")
code, lines = go(state=gardened)
check_("gardening_active_markers_still_found", code == 0)
found = art.parse_disposition_markers(gardened)
check_("gardening_archive_line_not_a_marker",
len(found) == 2 and not any(m["id"] == "F-P5-03" for m in found))
# unparseable sidecar is an ERROR, not a clean empty map
code, lines = check(None, ledger, None, [], [], coaching, None, sidecar_parse_ok=False)
check_("unparseable_sidecar_errors", code == 1 and any("DP0 unparseable sidecar" in ln for ln in lines))
# ---- file/run-folder resolution (end-to-end) ----
d = tempfile.mkdtemp()
made.append(d)
with open(os.path.join(d, "P_Findings_Ledger_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(ledger)
with open(os.path.join(d, "Diagnostic_State.meta.json"), "w", encoding="utf-8", newline="") as fh:
json.dump(sidecar(both, dict(states_delivered)), fh)
with open(os.path.join(d, _STATE_MD), "w", encoding="utf-8", newline="") as fh:
fh.write(coaching)
with open(os.path.join(d, "Submission_Readiness_Assessment_2026-07-01.md"), "w",
encoding="utf-8", newline="") as fh:
fh.write(caveat)
check_("run_folder_resolution", run([d])[0] == 0)
check_("sidecar_plus_assessment_resolution",
run([os.path.join(d, "Diagnostic_State.meta.json"),
os.path.join(d, "Submission_Readiness_Assessment_2026-07-01.md")])[0] == 0)
# the /ready laundering arm end-to-end: strip the caveat -> DP1 blocks
with open(os.path.join(d, "Submission_Readiness_Assessment_2026-07-01.md"), "w",
encoding="utf-8", newline="") as fh:
fh.write("## Readiness Verdict: READY\nLooks great.\n")
check_("run_folder_dp1_blocks", run([d])[0] == 1)
check_("usage_error_on_nothing", run([os.path.join(d, "nope", "nothing.xyz")])[0] == 2)
# ---- supersedence-recompute end-to-end (run-folder mode + runs/* archive evidence) ----
d2 = tempfile.mkdtemp()
made.append(d2)
cur = os.path.join(d2, "runs", "cur")
old = os.path.join(d2, "runs", "old")
os.makedirs(cur)
os.makedirs(old)
with open(os.path.join(d2, "Diagnostic_State.meta.json"), "w", encoding="utf-8", newline="") as fh:
json.dump(sidecar(both, dict(sup_states)), fh)
with open(os.path.join(d2, _STATE_MD), "w", encoding="utf-8", newline="") as fh:
fh.write(coaching)
with open(os.path.join(cur, "P_Findings_Ledger_run.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(ledger)
with open(os.path.join(cur, "Submission_Readiness_Assessment_2026-07-02.md"), "w",
encoding="utf-8", newline="") as fh:
fh.write("**Deferred Must-Fixes:** F-DP-02 — until: beta reads back\n")
old_report = os.path.join(old, "P_Revision_Report_old.md")
with open(old_report, "w", encoding="utf-8", newline="") as fh:
fh.write("# Revision Report\n<!-- resolved: F-P5-01 -->\n")
# the archived earlier-round report corroborates the 'revised' state -> exempt (rolling map)
check_("run_folder_archived_evidence_exempt", run([cur])[0] == 0)
# delete the archive -> the same tree is the fabrication shape: DP1 blocks + DP2.6 names it
os.unlink(old_report)
code, lines2 = run([cur])
check_("run_folder_fabricated_supersedence_blocks",
code == 1 and any("DP1 declined Must-Fix F-P5-01" in ln for ln in lines2)
and any("DP2.6" in ln for ln in lines2))
# a non-UTF8 archived report on the evidence surface degrades to the skip/fail-closed path
# (unreadable evidence = no corroboration), never a traceback (the adjacent-exception class)
with open(old_report, "wb") as fh:
fh.write(b"\xff\xfenot utf-8\xff")
code, lines2 = run([cur])
check_("evidence_non_utf8_fails_closed",
code == 1 and any("DP2.6" in ln for ln in lines2))
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 != "disposition-check"]
strict = "--strict" in args
paths = [a for a in args if not a.startswith("--")]
if not paths:
print("Usage: disposition_check.py disposition-check <run_folder|sidecar|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: cfd40dfa799c960cfdde9746516ab33d2eb08b5ecf9466f104828c62a406901d