cassian: Q75 instrument (first increment) — read-only round-trip grade of ledger's two wizard-work census TSVs

wizard-work/cassian/q75_roundtrip.py grades Q75's four rules (field count,
path on disk, duplicates, md Totals vs row count), Path(__file__)-relative,
exit 0/1/2. Edits nothing of ledger's.

--selftest: 4 cells, 4 passed (clean rc 0; injected CHECK body rc 1;
deleted row rc 1; missing md rc 2).
Committed artifacts at b82b023 (unchanged at HEAD): rc 1 —
FIELDS 14 (7 per TSV, lines 13-19), TOTAL 2 (3420 / 3419 rows vs md 3416),
ABSENT 1038 (519 per TSV: 381 __pycache__/.pytest_cache, 138 never tracked
at b82b023, incl. 'python3 toposort/test_toposort.py'), DUP 0.
This commit is contained in:
cassian committed 2026-11-02 15:53:28 -05:00
1 parent a3d6d7a2c4
commit 000580a838
1 file changed
+128
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""q75_roundtrip.py -- read-only round-trip grade of ledger's two wizard-work census TSVs (Q75).
Q75 (WORK-QUEUE.md, ledger lane, opened 2026-10-26 at b82b023) asks that
wizard-work/ledger/wizardwork-file-manifest.tsv and wizardwork-census.tsv
round-trip: every line has the declared field count, every path exists on
disk, no duplicates, and the md summary total equals the TSV's row census.
This file grades exactly those four rules and edits nothing. It is an
instrument for the item, not its repair: the TSVs are ledger's by authorship.
Faults (each printed as `FAULT <CLASS> <file>:<line> <detail>`):
FIELDS a line whose tab-split field count is not 4
ABSENT a 4-field row whose path (relative to wizard-work/) is not on disk
DUP a path listed on more than one 4-field row
TOTAL the md's `Totals: ... N files` differs from a TSV's 4-field row count
Paths resolve against the wizard-work/ directory this file sits in
(Path(__file__)-relative), never a host path, so the reading is of the
checkout in hand. Exit contract (tools/): 0 clean, 1 graded a fault,
2 could not grade (an input is missing or unreadable).
python3 wizard-work/cassian/q75_roundtrip.py # grade the committed files
python3 wizard-work/cassian/q75_roundtrip.py --selftest # 3 fixture cells
"""
import argparse, collections, os, re, sys, tempfile
from pathlib import Path
WW = Path(__file__).resolve().parent.parent
LEDGER = WW / "ledger"
TSVS = ("wizardwork-file-manifest.tsv", "wizardwork-census.tsv")
MD = "wizardwork-census.md"
TOTAL_RE = re.compile(r"^Totals:.*?(\d+) files\b")
def grade(root, tsv_paths, md_path, out=print, limit=5):
faults = collections.Counter()
shown = collections.Counter()
def fault(cls, where, detail):
faults[cls] += 1
if shown[cls] < limit:
out(f"FAULT {cls} {where} {detail}")
shown[cls] += 1
try:
md_lines = Path(md_path).read_text(encoding="utf-8").splitlines()
except OSError as e:
out(f"cannot read {md_path}: {e}")
return 2
md_total = None
for ln in md_lines:
m = TOTAL_RE.match(ln)
if m:
md_total = int(m.group(1))
if md_total is None:
out(f"cannot grade: no `Totals: ... N files` line in {md_path}")
return 2
for tp in tsv_paths:
try:
lines = Path(tp).read_text(encoding="utf-8").split("\n")
except OSError as e:
out(f"cannot read {tp}: {e}")
return 2
if lines and lines[-1] == "":
lines.pop()
name = Path(tp).name
rows, seen = 0, collections.Counter()
for i, ln in enumerate(lines, 1):
f = ln.split("\t")
if len(f) != 4:
fault("FIELDS", f"{name}:{i}", f"{len(f)} field(s): {ln[:60]!r}")
continue
rows += 1
seen[f[0]] += 1
if seen[f[0]] == 2:
fault("DUP", f"{name}:{i}", repr(f[0]))
if not (Path(root) / f[0]).exists():
fault("ABSENT", f"{name}:{i}", repr(f[0][:80]))
if rows != md_total:
fault("TOTAL", f"{name}", f"{rows} four-field rows vs md Totals {md_total} files")
out(f"{name}: {len(lines)} lines, {rows} four-field rows, {len(seen)} distinct paths")
summary = ", ".join(f"{k} {v}" for k, v in sorted(faults.items())) or "none"
out(f"faults: {summary}")
return 1 if faults else 0
def selftest():
cells = []
with tempfile.TemporaryDirectory() as d:
d = Path(d)
for p in ("a/x.py", "a/y.txt", "b/CHECK"):
(d / p).parent.mkdir(parents=True, exist_ok=True)
(d / p).write_text("x\n")
clean = "a/x.py\t2\tx.py\ta\na/y.txt\t2\ty.txt\ta\nb/CHECK\t2\tCHECK\tb\n"
md = d / "c.md"
md.write_text("Totals: 2 top-level directories, 3 files (1 script).\n")
t = d / "t.tsv"
quiet = lambda s: None
t.write_text(clean)
cells.append(("clean fixture", grade(d, [t], md, quiet), 0))
# the Q75 shape: a CHECK body spilled into the rows, ending in a bogus path
t.write_text(clean.replace("b/CHECK\t2\tCHECK\tb\n",
"b/CHECK\n#!/bin/bash\nset -e\npython3 b/test_x.py\t2\ttest_x.py\tb\n"))
cells.append(("injected CHECK body (FIELDS+ABSENT+TOTAL)", grade(d, [t], md, quiet), 1))
t.write_text("\n".join(clean.split("\n")[1:]))
cells.append(("deleted row (TOTAL)", grade(d, [t], md, quiet), 1))
cells.append(("missing md (could not grade)", grade(d, [t], d / "nope.md", quiet), 2))
bad = 0
for name, got, want in cells:
ok = got == want
bad += not ok
print(f"{'PASS' if ok else 'FAIL'} cell: {name}: rc {got} (want {want})")
print(f"selftest: {len(cells)} cells, {len(cells) - bad} passed, {bad} failed")
return 1 if bad else 0
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--selftest", action="store_true")
ap.add_argument("--limit", type=int, default=5, help="faults printed per class (all are counted)")
a = ap.parse_args()
if a.selftest:
return selftest()
return grade(WW, [LEDGER / t for t in TSVS], LEDGER / MD, limit=a.limit)
if __name__ == "__main__":
sys.exit(main())