← Files APODICTIC Development EditorARCHIVED FILE
scripts/regression_diff.py
30.3 KB · Nov 3, 2026 · 18:57 UTC
#!/usr/bin/env python3
"""regression-diff — Draft-over-Draft Structural Regression Testing (cross-round Findings-Ledger diff).
Diffs the Findings Ledger across two revision rounds: did this revision resolve what it claimed,
and did it break anything that was working? Finding IDs are *unique per run* (renumbered each round),
so cross-round identity is established by a DETERMINISTIC heuristic match — every regression signal is
therefore a CANDIDATE for editor judgment, not a mechanical verdict. Prints classification + candidates
to stdout (the diff-validator precedent — timeline-diff / state-card-diff persist nothing); the
human-readable Regression Report is orchestrator-written at round-close.
Match rule (pinned, deterministic): a round-N finding matches a round-(N-1) finding iff same ORIGIN
code AND equal chapter token (apodictic_artifacts.chapter_token; chapter-less bins to 'unplaced' and
matches only 'unplaced') AND >=1 shared normalized mechanism token. Score = shared-token count.
Assignment is greedy + stable + one-to-one: prior findings in ascending-id order each take the
highest-scoring unconsumed current finding (tie -> lowest current NN, then lexical id).
Classes (all candidates): persisted, resolved-and-held, recurrence-candidate, new, new-in-quiet-chapter.
Validator (run-folder pair):
R1 (ERROR) round linkage — both ledgers resolve, parse, are non-empty, and are distinct rounds.
W1 (WARN; ERROR --strict) recurrence-candidate(s): a resolved/'revised' prior finding matched in N.
W2 (WARN; ERROR --strict) new-in-quiet-chapter(s): a current finding in a chapter the prior round
left quiet (zero findings on record), unadjudicated. Override: `<!-- override: regression-cleared
<runlabel>:<chapter> — investigated, not fix-induced -->`.
W3 (WARN; ERROR --strict) unexplained-drop: a prior finding with no current match and no resolution.
Only R1 (the diff's structural contract) is a hard error; the rest are advisory (re-diagnosing a
changed manuscript legitimately shifts findings) — the finding-trace / state-card-diff posture.
Usage:
regression_diff.py regression-diff <prior_run_folder> <this_run_folder> [--strict]
regression_diff.py regression-diff <run_folder> # single round: nothing to diff (PASS)
regression_diff.py --self-test
Exit: 0 clean / WARN-only, 1 ERROR (or WARN under --strict), 2 usage.
"""
import glob
import os
import re
import sys
from override_marker import override_payloads # SSoT: code-span-stripped, boundary-matched override scan
try:
import apodictic_artifacts as art
except ImportError:
art = None
try:
import finding_trace as ft
except ImportError:
ft = None
_LEDGER_GLOB = "*_Findings_Ledger_*.md"
# Round N-1's resolution claims live in its completed-revision artifacts (state-lifecycle.md
# §Revision Round Output) and/or inline in its ledger — scan both for `<!-- resolved: F-… -->`.
_REVISION_GLOBS = ("*_Revision_Report_*.md", "*_Revision_*.md", "*_Session_Plan_*.md")
# `<!-- resolved: F-XX-NN -->` marker (fallback if finding_trace is unavailable).
_RESOLVED_RE = re.compile(r"<!--\s*resolved:(.*?)-->", re.DOTALL | re.IGNORECASE)
# Exact Lifecycle-ID token, single-sourced from apodictic_artifacts.FID_RE (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-])")
# W2 adjudication: `<!-- override: regression-cleared <runlabel>:<chapter> — <rationale> -->`.
# Read via the shared override_marker SSoT (override_payloads) — code spans stripped, slug
# boundary-matched — so a backtick'd example is not honored as a live adjudication.
_ID_SPLIT_RE = re.compile(r"^F-(.+)-([0-9]{2,})$")
# The classes `classify` keys by the PRIOR finding's id (vs. `new` / `new-in-quiet-chapter[ (cleared)]`,
# keyed by the CURRENT id). `crossref_classes` returns only these — the reanchor manifest carries prior ids.
_PRIOR_KEYED_CLASSES = frozenset(("persisted", "resolved-and-held", "recurrence-candidate", "unexplained-drop"))
# Mechanism tokenization: lowercase alnum tokens, drop a small fixed stopword set + tokens < 3 chars.
_STOP = frozenset((
"the", "a", "an", "of", "to", "in", "is", "it", "so", "and", "that", "this", "for", "on", "at",
"as", "but", "or", "its", "with", "by", "be", "are", "was", "not", "no", "into", "out", "up",
"her", "his", "their", "they", "she", "he", "than", "then", "too", "via", "per",
))
def _read(path):
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 _origin_nn(fid):
# A non-string id can't be origin/NN-parsed; re.match would traceback on it (sibling of the
# _mech_tokens non-string P2). A malformed id contributes no origin/number.
if not isinstance(fid, str):
return ("", 0)
m = _ID_SPLIT_RE.match(fid)
if not m:
return (fid or "", 0)
return (m.group(1), int(m.group(2)))
def _mech_tokens(mechanism):
# Only a STRING mechanism yields match tokens. A malformed list/dict would otherwise be str()'d
# into "['foo', ...]" / "{'k': 'v'}" and split into FABRICATED tokens (foo, k, v …) that falsely
# match against other findings (Codex P2). A non-string mechanism contributes no tokens.
if not isinstance(mechanism, str):
return set()
toks = set()
for raw in re.split(r"[^A-Za-z0-9]+", mechanism.lower()):
if len(raw) >= 3 and raw not in _STOP:
toks.add(raw)
return toks
def _chapter_of(evidence_refs):
"""The finding's chapter token ('Ch N') from its first chapter-bearing ref, else 'unplaced'."""
if art is None:
return "unplaced"
for ref in (evidence_refs if isinstance(evidence_refs, list) else []):
tok = art.chapter_token(ref)
if tok:
return tok
return "unplaced"
def findings_of(ledger_text):
"""[{id, origin, nn, mechanism, severity, evidence_refs, chapter, tokens}] for the ledger's
apodictic.finding.v1 blocks (parsed via the shared apodictic_artifacts.parse_blocks)."""
out = []
if not ledger_text or art is None:
return out
for bt, obj, _err in art.parse_blocks(ledger_text):
if bt != "finding" or not isinstance(obj, dict) or not obj.get("id"):
continue
# A non-string id is dropped at the source: parse_blocks is raw json.loads (no id-type
# validation), so a JSON list/dict id is truthy and survives the `not obj.get("id")`
# filter, but it is stored verbatim and then used as a dict key / set member downstream
# (match(): `c["id"] in consumed` / `consumed.add(...)`; classify(): `{p["id"]: p}`) where
# an unhashable list/dict raises TypeError. Guarding only _origin_nn's re.match (the prior
# fix) leaves those hashing sites open — same _mech_tokens/id sibling lesson, repeated.
if not isinstance(obj.get("id"), str):
continue
origin, nn = _origin_nn(obj["id"])
refs = obj.get("evidence_refs") or []
out.append({
"id": obj["id"], "origin": origin, "nn": nn,
"mechanism": obj.get("mechanism") or "", "severity": obj.get("severity"),
"evidence_refs": refs, "chapter": _chapter_of(refs),
"tokens": _mech_tokens(obj.get("mechanism")),
})
return out
def _resolved_ids(text):
if ft is not None:
return ft.resolved_cited_ids(text or "")
ids = set()
for body in _RESOLVED_RE.findall(text or ""):
ids.update(_ID_RE.findall(body))
return ids
def _cleared_chapters(text):
"""Chapter tokens cleared by `<!-- override: regression-cleared <runlabel>:<chapter> — … -->`.
The chapter is the `Ch N` token in the `<runlabel>:<chapter>` head (before the em-dash rationale);
chapter_token scans for it and ignores the runlabel prefix, so the canonical spaced form `Ch 3`
parses correctly (splitting on whitespace would fragment `Ch`/`3` and match neither)."""
chaps = set()
if art is None:
return chaps
for body in override_payloads(text or "", "regression-cleared"):
head = body.split("—")[0] # drop the rationale so a chapter named there isn't swept in
tok = art.chapter_token(head)
if tok:
chaps.add(tok)
return chaps
def match(prior, current):
"""Deterministic greedy one-to-one match. Returns {prior_id: current_finding}."""
matched = {}
consumed = set()
for p in sorted(prior, key=lambda f: f["id"]):
best = None # (score, nn, id, finding)
for c in current:
if c["id"] in consumed:
continue
if c["origin"] != p["origin"] or c["chapter"] != p["chapter"]:
continue
shared = len(p["tokens"] & c["tokens"])
if shared < 1:
continue
cand = (shared, -c["nn"], c["id"]) # high score, then low nn, then lexical id
if best is None or cand[:2] > best[0][:2] or (cand[:2] == best[0][:2] and cand[2] < best[0][2]):
best = (cand, c)
if best is not None:
matched[p["id"]] = best[1]
consumed.add(best[1]["id"])
return matched
def classify(prior, current, prior_resolved, cleared_chaps):
"""-> (rows, recurrence[], quiet[], drops[]). rows: (chapter, id, klass, basis, severity)."""
matched = match(prior, current)
pri_by_id = {p["id"]: p for p in prior}
prior_chapters = set(p["chapter"] for p in prior if p["chapter"] != "unplaced")
matched_current_ids = set(c["id"] for c in matched.values())
rows, recurrence, quiet, drops = [], [], [], []
for p in sorted(prior, key=lambda f: (f["chapter"], f["id"])):
c = matched.get(p["id"])
if c is not None:
shared = sorted(p["tokens"] & c["tokens"])
basis = "origin=%s chapter=%s shared=[%s]" % (p["origin"], p["chapter"], ",".join(shared))
if p["id"] in prior_resolved:
rows.append((p["chapter"], p["id"], "recurrence-candidate", basis, p["severity"]))
recurrence.append((p, c, p["severity"], basis))
else:
rows.append((p["chapter"], p["id"], "persisted", basis, p["severity"]))
else:
if p["id"] in prior_resolved:
rows.append((p["chapter"], p["id"], "resolved-and-held", "no current match", p["severity"]))
else:
rows.append((p["chapter"], p["id"], "unexplained-drop", "no current match", p["severity"]))
drops.append(p)
for c in sorted(current, key=lambda f: (f["chapter"], f["id"])):
if c["id"] in matched_current_ids:
continue
if c["chapter"] != "unplaced" and c["chapter"] not in prior_chapters:
if c["chapter"] in cleared_chaps:
rows.append((c["chapter"], c["id"], "new-in-quiet-chapter (cleared)", "override", c["severity"]))
else:
rows.append((c["chapter"], c["id"], "new-in-quiet-chapter", "quiet on prior record", c["severity"]))
quiet.append(c)
else:
rows.append((c["chapter"], c["id"], "new", "", c["severity"]))
return rows, recurrence, quiet, drops
def diff_rounds(prior_text, current_text, prior_resolved, cleared_chaps, strict=False):
lines, errs, warns = [], [], []
prior = findings_of(prior_text)
current = findings_of(current_text)
# R1 — round linkage (the one mechanical invariant).
if not prior:
errs.append("R1 round linkage: prior round has no parseable findings")
if not current:
errs.append("R1 round linkage: current round has no parseable findings")
if errs:
return _finish(["regression-diff: cross-round diff"], errs, warns, strict, "")
rows, recurrence, quiet, drops = classify(prior, current, prior_resolved, cleared_chaps)
lines.append("regression-diff: %d prior finding(s) -> %d current finding(s)"
% (len(prior), len(current)))
for chapter, fid, klass, basis, sev in rows:
lines.append(" regression-diff:%s %s [%s%s]%s"
% (klass, fid, chapter, "" if sev is None else " %s" % sev,
" — %s" % basis if basis else ""))
for p, c, sev, basis in recurrence:
warns.append("W1 recurrence-candidate: prior %s (%s) matched by current %s — reverts to %s; %s"
% (p["id"], sev, c["id"], sev, basis))
for c in quiet:
warns.append("W2 quiet-chapter breakage candidate: current %s in %s (quiet on prior record) — "
"adjudicate or `<!-- override: regression-cleared %s — investigated -->`"
% (c["id"], c["chapter"], c["chapter"]))
for p in drops:
warns.append("W3 unexplained-drop candidate: prior %s (%s) has no current match and no resolution marker"
% (p["id"], p["severity"]))
return _finish(lines, errs, warns, strict,
"%d persisted/new, no regression candidates" % len(rows))
def _finish(lines, errs, warns, strict, ok_msg):
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("regression-diff: 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: regression-diff: %d regression candidate(s) — see W1-W3 above" % len(warns))
else:
lines.append("regression-diff: PASS (%s)" % ok_msg)
return 0, lines
def _ledger_path(p):
"""A dir -> its newest ledger; a file -> itself."""
if os.path.isdir(p):
return _newest(glob.glob(os.path.join(p, _LEDGER_GLOB)))
return p if os.path.isfile(p) else None
def _resolved_and_cleared(folder_or_file, want_resolved):
"""(resolved_ids, cleared_chapters) gathered from a round's artifacts. A dir scans its ledger AND
revision artifacts (so a `<!-- resolved -->` / `regression-cleared` marker is found whether it lives
inline in the ledger or in the Revision Report); resolved_ids computed only when want_resolved."""
texts = []
if os.path.isdir(folder_or_file):
paths = list(glob.glob(os.path.join(folder_or_file, _LEDGER_GLOB)))
for g in _REVISION_GLOBS:
paths += glob.glob(os.path.join(folder_or_file, g))
for p in paths:
t = _read(p)
if t:
texts.append(t)
else:
t = _read(folder_or_file)
if t:
texts.append(t)
joined = "\n".join(texts)
return (_resolved_ids(joined) if want_resolved else set()), _cleared_chapters(joined)
def resolve(paths):
"""-> (prior, current) folder/file paths. A lone arg is single-round (no prior)."""
if len(paths) >= 2:
return paths[0], paths[1]
if len(paths) == 1:
return None, paths[0]
return None, None
def run(paths, strict=False):
prior, current = resolve(paths)
if not current:
return 2, ["regression-diff: usage: regression-diff <prior_run_folder> <this_run_folder>"]
cur_ledger = _ledger_path(current)
if not cur_ledger:
return 2, ["regression-diff: no %s in %s" % (_LEDGER_GLOB, current)]
if prior is None:
# Single round: nothing to diff. Confirm the ledger parses, then PASS.
n = len(findings_of(_read(cur_ledger) or ""))
return 0, ["regression-diff: single round (%d finding(s)); no prior round to diff — PASS" % n]
pri_ledger = _ledger_path(prior)
if not pri_ledger:
return 2, ["regression-diff: no %s in %s" % (_LEDGER_GLOB, prior)]
if os.path.abspath(pri_ledger) == os.path.abspath(cur_ledger):
return 1, ["regression-diff: cross-round diff", " ERROR: R1 round linkage: prior and current "
"are the same round (%s)" % os.path.basename(cur_ledger),
"regression-diff: FAIL (1 error(s))"]
prior_resolved, _ = _resolved_and_cleared(prior, want_resolved=True)
_, cleared = _resolved_and_cleared(current, want_resolved=False)
return diff_rounds(_read(pri_ledger), _read(cur_ledger), prior_resolved, cleared, strict=strict)
def crossref_classes(prior, current):
"""{prior_finding_id: class} for the PRIOR-round-keyed regression classes (persisted, resolved-and-held,
recurrence-candidate, unexplained-drop) — the map `reanchor crossref` joins against by finding_id (the
re-anchored manifest carries the prior round's ids). Current-only classes (new / new-in-quiet-chapter)
have no prior id to join on and are omitted. Best-effort: returns {} when a ledger is missing/unparseable
(the join simply finds no matches — it never raises), so it's safe to call from the orchestrator glue."""
if prior is None or current is None:
return {}
pri_ledger, cur_ledger = _ledger_path(prior), _ledger_path(current)
if not pri_ledger or not cur_ledger:
return {}
prior_findings = findings_of(_read(pri_ledger))
if not prior_findings:
return {}
prior_resolved, _ = _resolved_and_cleared(prior, want_resolved=True)
_, cleared = _resolved_and_cleared(current, want_resolved=False)
rows, _rec, _quiet, _drops = classify(prior_findings, findings_of(_read(cur_ledger)),
prior_resolved, cleared)
# Keep only the PRIOR-keyed rows (their `id` is the prior finding's id — the join key). Filter by the
# known prior-keyed class set, NOT by id membership: per-run renumbering means a current-only `new`
# finding can share an id string with a prior finding (both rounds start at `-01`), and `classify`
# emits the `new` row AFTER the prior rows — an id-membership filter would let that collision overwrite
# the prior class. The class set is collision-proof and order-independent.
out = {}
for _chapter, fid, klass, _basis, _sev in rows:
if klass in _PRIOR_KEYED_CLASSES:
out[fid] = klass
return out
# ---------------------------------------------------------------- self-test
def run_self_test():
import json as _j
import tempfile
import shutil
rc = {"v": 0}
def chk(name, cond):
print(" %s: %s" % (name, "OK" if cond else "FAIL"))
if not cond:
rc["v"] = 1
def finding(fid, mech, refs, sev="Should-Fix"):
return '<!-- apodictic:finding\n%s\n-->' % _j.dumps(
{"schema": "apodictic.finding.v1", "id": fid, "mechanism": mech, "severity": sev,
"confidence": "HIGH", "evidence_refs": list(refs), "fix_class": "x", "risk_if_fixed": "y"})
def ledger(*blocks):
return "# Ledger\n" + "\n".join(blocks) + "\n"
# round 1: a flat-affect beat in Ch 7 (Must-Fix), a pacing seam in Ch 9.
r1 = ledger(
finding("F-P5-01", "the want never forces a sacrifice so stakes stay abstract", ["Ch 7"], "Must-Fix"),
finding("F-RR-01", "three days collapse into one paragraph, a continuity seam", ["Ch 9"], "Should-Fix"),
)
# round 2: F-P5 recurs (renumbered, same origin/chapter, shared mechanism tokens); a NEW finding in
# Ch 3 which round 1 left quiet; the Ch 9 seam is gone (matched -> not present -> drop/held).
r2 = ledger(
finding("F-P5-01", "the want still never forces a sacrifice; stakes remain abstract", ["Ch 7"], "Must-Fix"),
finding("F-P5-02", "a new voice-drift sentence breaks POV discipline", ["Ch 3"], "Should-Fix"),
)
f1 = findings_of(r1)
chk("findings_parsed", len(f1) == 2 and f1[0]["origin"] == "P5" and f1[0]["chapter"] == "Ch 7")
chk("mech_tokens_drop_stopwords", "want" in f1[0]["tokens"] and "the" not in f1[0]["tokens"])
# a malformed (non-string) mechanism yields NO tokens — never fabricated from a list/dict repr (Codex P2)
chk("mech_tokens_nonstring_empty",
_mech_tokens(["fabricated", "tokens"]) == set() and _mech_tokens({"k": "v"}) == set()
and _mech_tokens(None) == set())
# a non-string finding id must not crash _origin_nn's re.match — sibling of the mechanism P2
chk("origin_nn_nonstring",
_origin_nn(5) == ("", 0) and _origin_nn(["a"]) == ("", 0) and _origin_nn(3.14) == ("", 0))
# An INT id is hashable, so findings_of keeps it without crashing the downstream key/set sites;
# but a NON-HASHABLE list/dict id (valid JSON, truthy, survives the id filter) would crash
# match()/classify() at `c["id"] in consumed` / `{p["id"]: p}`. findings_of now drops any
# non-string id at the source, so the WHOLE downstream path is guarded, not just _origin_nn.
chk("findings_of_nonstring_id_no_crash",
len(findings_of(ledger(finding(5, "m", ["Ch 1"], "Must-Fix")))) == 0)
# the load-bearing regression: a non-hashable id driven through the full diff_rounds path
# (-> classify -> match) must not raise TypeError: unhashable type. Pre-fix this crashed.
_badl = ledger(finding(["a"], "the want forces a sacrifice", ["Ch 7"], "Must-Fix"))
_badd = ledger(finding({"k": "v"}, "the want forces a sacrifice", ["Ch 7"], "Must-Fix"))
chk("findings_of_unhashable_id_dropped",
findings_of(_badl) == [] and findings_of(_badd) == [])
def _no_crash(prior_text, current_text):
try:
diff_rounds(prior_text, current_text, set(), set())
return True
except TypeError:
return False
# a malformed (non-hashable id) finding paired with a valid one — exercises match()/classify().
_good = ledger(finding("F-P5-01", "the want forces a sacrifice", ["Ch 7"], "Must-Fix"))
chk("diff_rounds_unhashable_list_id_no_crash", _no_crash(_badl, _good) and _no_crash(_good, _badl))
chk("diff_rounds_unhashable_dict_id_no_crash", _no_crash(_badd, _good) and _no_crash(_good, _badd))
# matcher: F-P5-01(r1) <-> F-P5-01(r2) by origin+chapter+shared tokens; F-RR-01 has no r2 match.
m = match(f1, findings_of(r2))
chk("match_one_to_one", m.get("F-P5-01", {}).get("id") == "F-P5-01" and "F-RR-01" not in m)
# classify without resolution: F-P5 persisted, F-RR-01 unexplained-drop, F-P5-02 new-in-quiet-chapter.
rows, rec, quiet, drops = classify(f1, findings_of(r2), set(), set())
klass = {fid: k for (_ch, fid, k, _b, _s) in rows}
chk("persisted", klass.get("F-P5-01") == "persisted")
chk("unexplained_drop", klass.get("F-RR-01") == "unexplained-drop" and len(drops) == 1)
chk("new_in_quiet_chapter", klass.get("F-P5-02") == "new-in-quiet-chapter" and len(quiet) == 1)
chk("no_recurrence_without_resolution", not rec)
# classify WITH F-P5-01 marked resolved in round 1 -> recurrence-candidate (the regression).
rows2, rec2, _q2, _d2 = classify(f1, findings_of(r2), {"F-P5-01"}, set())
klass2 = {fid: k for (_ch, fid, k, _b, _s) in rows2}
chk("recurrence_candidate", klass2.get("F-P5-01") == "recurrence-candidate" and len(rec2) == 1)
chk("recurrence_reverts_severity", rec2[0][2] == "Must-Fix")
# a resolved finding with NO current match is resolved-and-held (the win).
rows3, _r3, _q3, _d3 = classify(f1, findings_of(r2), {"F-RR-01"}, set())
klass3 = {fid: k for (_ch, fid, k, _b, _s) in rows3}
chk("resolved_and_held", klass3.get("F-RR-01") == "resolved-and-held")
# diff_rounds: W1 (recurrence) + W2 (quiet chapter) advisory by default, ERROR under --strict.
code, lines = diff_rounds(r1, r2, {"F-P5-01"}, set())
chk("default_advisory", code == 0 and any("W1 recurrence" in x for x in lines)
and any("W2 quiet-chapter" in x for x in lines))
chk("strict_fails", diff_rounds(r1, r2, {"F-P5-01"}, set(), strict=True)[0] == 1)
# determinism: identical inputs -> identical output lines.
chk("deterministic", diff_rounds(r1, r2, {"F-P5-01"}, set())[1] == diff_rounds(r1, r2, {"F-P5-01"}, set())[1])
# W2 override clears the quiet-chapter candidate.
code, lines = diff_rounds(r1, r2, set(), {"Ch 3"})
chk("w2_override_clears", code == 0 and not any("W2 quiet-chapter breakage" in x for x in lines)
and any("new-in-quiet-chapter (cleared)" in x for x in lines))
# _cleared_chapters parses the CANONICAL spaced override form end-to-end (regression: a per-token
# whitespace split would fragment `Ch 3` and clear nothing).
chk("cleared_canonical_spaced",
_cleared_chapters("<!-- override: regression-cleared 2026-02-01_m:Ch 3 — investigated, not fix-induced -->")
== {"Ch 3"})
chk("cleared_runlabel_not_swept", "Ch 3" in
_cleared_chapters("<!-- override: regression-cleared codex54:Ch 3 -->"))
chk("cleared_none_when_absent", _cleared_chapters("no override markers here") == set())
# the real marker, driven through _cleared_chapters into diff_rounds, clears W2.
_ov = "<!-- override: regression-cleared 2026-02-01_m:Ch 3 — investigated -->"
code, lines = diff_rounds(r1, r2, set(), _cleared_chapters(_ov))
chk("w2_canonical_override_end_to_end",
code == 0 and not any("W2 quiet-chapter breakage" in x for x in lines))
# unplaced findings never classify as new-in-quiet-chapter.
r2u = ledger(finding("F-P8-01", "a reveal lands flat with no felt beat", []))
rows_u, _ru, quiet_u, _du = classify(f1, findings_of(r2u), set(), set())
klass_u = {fid: k for (_ch, fid, k, _b, _s) in rows_u}
chk("unplaced_is_new_not_quiet", klass_u.get("F-P8-01") == "new" and not quiet_u)
# R1: a round with no parseable findings is a hard error.
chk("r1_empty_prior_errors", diff_rounds("# empty\n", r2, set(), set())[0] == 1)
# run(): folder pair, single-round, same-round.
d = tempfile.mkdtemp()
try:
d1, d2 = os.path.join(d, "r1"), os.path.join(d, "r2")
os.makedirs(d1)
os.makedirs(d2)
with open(os.path.join(d1, "P_Findings_Ledger_2026-01-01_m.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(r1)
with open(os.path.join(d1, "P_Revision_Report_2026-01-02_m.md"), "w", encoding="utf-8", newline="") as fh:
fh.write("# Revision Report\n<!-- resolved: F-P5-01 -->\n")
with open(os.path.join(d2, "P_Findings_Ledger_2026-02-01_m.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(r2)
# prior resolved F-P5-01 -> recurrence -> WARN (exit 0), ERROR under --strict.
chk("run_pair_advisory", run([d1, d2])[0] == 0)
chk("run_pair_strict_fails", run([d1, d2], strict=True)[0] == 1)
chk("run_single_round_pass", run([d2])[0] == 0)
chk("run_same_round_r1_fails", run([d2, d2])[0] == 1)
chk("run_missing_ledger_usage", run([os.path.join(d, "nope"), d2])[0] == 2)
# current-round override in a REVISION REPORT (not the ledger) is read and clears W2.
d3 = os.path.join(d, "r3")
os.makedirs(d3)
with open(os.path.join(d3, "P_Findings_Ledger_2026-03-01_m.md"), "w", encoding="utf-8", newline="") as fh:
fh.write(r2)
with open(os.path.join(d3, "P_Revision_Report_2026-03-02_m.md"), "w", encoding="utf-8", newline="") as fh:
fh.write("# Revision Report\n<!-- override: regression-cleared 2026-03-01_m:Ch 3 — investigated -->\n")
_c3, l3 = run([d1, d3])
chk("run_override_in_revision_report_clears_w2",
not any("W2 quiet-chapter breakage" in x for x in l3)
and any("new-in-quiet-chapter (cleared)" in x for x in l3))
# crossref_classes: the prior-keyed class map the reanchor crossref glue joins against by id.
# In d1->d2: F-P5-01 was resolved in d1 and recurs in d2 (recurrence-candidate); F-RR-01 has no
# d2 match and no resolution (unexplained-drop). New/current-only classes carry no prior id and
# are omitted; a missing-ledger folder yields {} (best-effort, never raises).
cc = crossref_classes(d1, d2)
chk("crossref_classes_recurrence", cc.get("F-P5-01") == "recurrence-candidate")
chk("crossref_classes_drop", cc.get("F-RR-01") == "unexplained-drop")
chk("crossref_classes_no_current_only", "F-P5-02" not in cc)
chk("crossref_classes_missing_safe", crossref_classes(os.path.join(d, "nope"), d2) == {})
# id-collision guard: per-run renumbering can make a current-only `new` finding SHARE an id with a
# prior finding. The prior-keyed class must win (it's the join key the reanchor manifest carries) —
# a class-set filter is collision-proof where an id-membership filter would let the later `new` row
# overwrite it. Here d1's F-RR-01 (Ch 9, unexplained-drop) and a NEW Ch 2 finding both id'd F-RR-01.
dcol = os.path.join(d, "rcol")
os.makedirs(dcol)
with open(os.path.join(dcol, "P_Findings_Ledger_2026-04-01_m.md"), "w",
encoding="utf-8", newline="") as fh:
# a recurrence of F-P5 (Ch 7) + a NEW finding in Ch 2 that reuses the id string F-RR-01.
fh.write(ledger(
finding("F-P5-01", "the want still never forces a sacrifice; stakes remain abstract",
["Ch 7"], "Must-Fix"),
finding("F-RR-01", "a brand-new continuity wobble unrelated to the prior seam", ["Ch 2"])))
ccol = crossref_classes(d1, dcol)
# prior F-RR-01 (Ch 9) had no Ch-2 match -> unexplained-drop; the class set keeps THAT, not the
# current-only `new` Ch-2 row that happens to reuse the id.
chk("crossref_classes_id_collision_prior_wins", ccol.get("F-RR-01") == "unexplained-drop")
finally:
shutil.rmtree(d, ignore_errors=True)
# regression: a non-string mechanism / non-list evidence_refs must not crash (2026-06-20 sweep)
chk("crash_nondict_mechanism", _mech_tokens(42) == set() and _mech_tokens(["a", "b"]) == set())
chk("crash_string_evidence_refs", _chapter_of("Ch 5") == "unplaced" and _chapter_of(["Ch 5"]) == "Ch 5")
print("Self-test: %s" % ("PASS" if rc["v"] == 0 else "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 != "regression-diff"]
strict = "--strict" in args
paths = [a for a in args if not a.startswith("--")]
if not paths:
print("Usage: regression_diff.py regression-diff <prior_run_folder> <this_run_folder> [--strict] "
"| <run_folder> | --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: a72607494e1f72555682da78fb6762f3ee4313ac3ecaabdb7ab5e3187bd9000c