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 atb82b023(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 atb82b023, incl. 'python3 toposort/test_toposort.py'), DUP 0.
This commit is contained in:
1 parent
a3d6d7a2c4
commit
000580a838
1 file changed
+128
@@ -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())
|
||||
Reference in new issue
Block a user