Files
troupe-standup/wizard-work/ledger/2026-10-28-checkin-census/CHECK
T

182 lines
6.6 KiB
Python

#!/usr/bin/env python3
# Ledger — independent cross-derivation + comparison gate for the
# check-in census.
#
# Re-derives the census from the raw check-in markdown with a SECOND, naive
# implementation (plain string splitting, no section-regex) and compares it
# field-for-field against the persisted tables produced by
# extract_checkin_census_2026_10_28.py. Exit code IS the verdict:
# 0 = census matches the raw files on every row and headline invariant
# 1 = mismatch found
#
# A re-run of `python3 wizard-work/ledger/2026-10-28-checkin-census/CHECK`
# re-derives everything from the current tree, so the check stays green only
# while the census honestly reflects `checkins/`.
import csv
import datetime
import json
import re
import subprocess
import sys
from pathlib import Path
BASE = Path("/root/troupe-standup")
CHECKS = BASE / "checkins"
OUT = BASE / "wizard-work/ledger/2026-10-28-checkin-census"
EXTRACTOR = OUT / "extract_checkin_census_2026_10_28.py"
TSV = OUT / "checkin-census-2026-10-28.tsv"
JSON = OUT / "checkin-census-2026-10-28.json"
MD = OUT / "checkin-census-2026-10-28.md"
EXCLUDE = {"README.md"}
def naive_parse(path):
"""Second, independent implementation to cross-derive the census.
Uses plain string-finding on the raw text (no section regex, no line
splitting). Bounds the `## Did` section the same way the extractor does
(up to the next `## ` heading) and reports did_items / did_chars on that
exact slice, so both implementations must agree on every row.
"""
import re as _re
name = path.name
body = name[:-3]
m = _re.match(r"^(\d{4}-\d{2}-\d{2})-(.+)$", body)
if not m:
return None
date_str, signer = m.group(1), m.group(2)
try:
datetime.date.fromisoformat(date_str)
except ValueError:
return None
text = path.read_text(encoding="utf-8", errors="replace")
t = text.replace("\r\n", "\n").replace("\r", "\n")
def section(name):
# A heading may sit at file start (no preceding newline).
if t.startswith(name):
idx = 0
else:
idx = t.find("\n" + name)
if idx == -1:
return ""
idx += 1 # "\n" at idx; "##" one char later
nxt = t.find("\n## ", idx)
return t[idx:nxt if nxt != -1 else len(t)]
has_done = t.find("\n## Did") != -1 or t.startswith("## Did")
has_blocked = t.find("\n## Blocked") != -1 or t.startswith("## Blocked")
has_next = t.find("\n## Next") != -1 or t.startswith("## Next")
has_ask = t.find("\n## Ask") != -1 or t.startswith("## Ask")
did = section("## Did")
did_items = 0
for line in did.split("\n"):
s2 = line.strip()
if s2.startswith("### "):
rest = s2[len("### "):]
if len(rest) > 1 and rest[0].isdigit() and rest[1] == ".":
did_items += 1
did_chars = len(did)
return {"date": date_str, "signer": signer, "file": name,
"has_done": has_done, "has_blocked": has_blocked,
"has_next": has_next, "has_ask": has_ask,
"did_items": did_items, "did_chars": did_chars}
def rederive():
rows = []
for path in sorted(CHECKS.iterdir()):
if path.name in EXCLUDE or not path.is_file():
continue
r = naive_parse(path)
if r is not None:
rows.append(r)
return rows
def load_tsv():
with open(TSV, encoding="utf-8", newline="") as f:
return list(csv.DictReader(f, delimiter="\t"))
def load_json():
return json.loads(JSON.read_text(encoding="utf-8"))
def same(a, b):
return a == b
def main():
errors = []
# 1) The extractor must run clean.
proc = subprocess.run([sys.executable, str(EXTRACTOR), str(CHECKS), str(OUT)],
capture_output=True, text=True)
if proc.returncode != 0:
print("CHECK FAIL: extractor exited {!r}\nstd{}out{}end{}".format(
proc.returncode, "out:", proc.stdout, "end:", proc.stderr))
return 1
# 2) Independent re-derivation vs persisted TSV.
expect = rederive()
actual = load_tsv()
if len(expect) != len(actual):
errors.append("row count: re-derived {} != persisted {}".format(len(expect), len(actual)))
for i in range(max(len(expect), len(actual))):
e = expect[i] if i < len(expect) else None
a = actual[i] if i < len(actual) else None
if e is None or a is None:
errors.append("row {} side mismatch".format(i))
continue
if e["date"] != a["date"]:
errors.append("row {} date {} != {}".format(i, e["date"], a["date"]))
if e["signer"] != a["signer"]:
errors.append("row {} signer {} != {}".format(i, e["signer"], a["signer"]))
if e["has_done"] != (a["has_done"] == "True"):
errors.append("row {} has_done mismatch".format(i))
if e["did_items"] != int(a["did_items"]):
errors.append("row {} did_items {} != {}".format(i, e["did_items"], a["did_items"]))
if e["did_chars"] != int(a["did_chars"]):
errors.append("row {} did_chars mismatch".format(i))
# 3) Headline invariants from the JSON.
d = load_json()
h = d.get("headline", {})
if h.get("files") != len(expect):
errors.append("headline.files {} != re-derived {}".format(h.get("files"), len(expect)))
if h.get("skipped_non_dated") != 1:
errors.append("headline.skipped != 1")
signers = {r["signer"] for r in expect}
if h.get("signers") != len(signers):
errors.append("headline.signers {} != re-derived {}".format(h.get("signers"), len(signers)))
# Re-derived latest per signer must match JSON's latest_per_signer.
latest = {}
for r in expect:
cur = latest.get(r["signer"])
if cur is None or r["date"] > cur["date"]:
latest[r["signer"]] = {"signer": r["signer"], "date": r["date"], "file": r["file"]}
j_latest = {x["signer"]: x for x in h.get("latest_per_signer", [])}
if set(latest) != set(j_latest):
errors.append("latest signers mismatch: {} != {}".format(set(latest), set(j_latest)))
for s, rec in latest.items():
if s not in j_latest:
continue
if j_latest[s]["latest_date"] != rec["date"] or j_latest[s]["file"] != rec["file"]:
errors.append("latest {} mismatch: json {} vs re-derived {}".format(
s, j_latest[s], rec))
if errors:
print("CHECK FAIL:")
for e in errors:
print(" -", e)
return 1
print("CHECK PASS: {} dated check-ins, {} signers, 1 non-dated skipped, "
"all rows + headline re-derive clean".format(len(expect), len(signers)))
return 0
if __name__ == "__main__":
sys.exit(main())