The 2026-10-28 and 2026-10-29 check-in censuses were red atd42f577for three reasons at once: both CHECKs were Python under a 'bash CHECK' line (rc 2, import: command not found), both graded the live tree (so every new check-in reddened a dated artifact), and both hardcoded /root/troupe-standup. The 10-28 table also carried a 101st row for checkins/2026-10-27-sentinel.md, a file in no revision. - pins beside each artifact, in the 10-27 format (ab7767d/b21bb10) - extractor and generator take --pin REV (git ls-tree/show, no disk), write nothing without --write; 10-28 regenerated at its pin (100 rows); 10-29 regenerated at its pin reproduces the committed CSV/JSON/MD byte for byte - CHECK = bash wrapper over a 7-leg (28) / 5-leg (29) grader that derives every cell from the pin, writes nothing, and answers rc 2 when the pin object is absent - CHECK-q151 runs both gates plus a mutant cell - q143 sweep: both rows re-taken 1 -> 0 with retake.py, CHECK-q151 added; q137 open-set pin moved with the record
328 lines
14 KiB
Python
Executable File
328 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Grade the 2026-10-28 check-in census against its PINNED revision.
|
|
|
|
WHAT IT GRADES -- seven legs, each re-deriving from `git show <pin>:checkins/...`
|
|
and comparing against the committed tables. Nothing is read from the working
|
|
tree, so a check-in that lands tomorrow cannot red this artifact, and nothing is
|
|
written, so a run cannot regenerate its own input.
|
|
|
|
1 scope the pin file beside this script: header, sortedness, uniqueness, names
|
|
2 pin the name list equals `checkins/` at the pinned commit (SKIP if the
|
|
object is not in this database -- the committed list is then the pin)
|
|
3 tsv every cell of the committed TSV against the re-derivation
|
|
4 md-inv the MD's full-inventory rows
|
|
5 md-head the MD's headline measures
|
|
6 md-late the MD's per-signer latest block
|
|
7 json the JSON's rows, headline counters and pin field
|
|
|
|
The re-derivation is deliberately a second implementation -- line-oriented
|
|
section finding, not the committed extractor's section regex -- and it never
|
|
imports that extractor, so the two cannot agree by sharing a bug.
|
|
|
|
EXIT CODES: 0 every leg passed; 1 a fault (including "the pin is not a usable
|
|
input", which is a refusal to grade rather than a fallback to today's disk);
|
|
2 cannot grade at all (no git, no python3 in the wrapper).
|
|
|
|
Usage: python3 grade.py [--repo DIR] (or `bash CHECK`, which runs this file)
|
|
"""
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
PIN_FILE = HERE / "2026-10-28-checkin-census.files"
|
|
TAG = "2026-10-28"
|
|
TSV = HERE / ("checkin-census-" + TAG + ".tsv")
|
|
MD = HERE / ("checkin-census-" + TAG + ".md")
|
|
JSON = HERE / ("checkin-census-" + TAG + ".json")
|
|
SECTIONS = ("## Did", "## Blocked", "## Next", "## Ask")
|
|
NAME_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-(.+)\.md$")
|
|
ITEM_RE = re.compile(r"^\s*### \d+\.\s+\S")
|
|
|
|
|
|
class Fault(Exception):
|
|
"""A graded leg failed; the message is the verdict line."""
|
|
|
|
|
|
class CannotGrade(Exception):
|
|
"""The instrument itself cannot run."""
|
|
|
|
|
|
def git(repo, *args):
|
|
try:
|
|
out = subprocess.run(["git", "-C", str(repo)] + list(args),
|
|
capture_output=True, timeout=180)
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
raise CannotGrade("git %s: %s" % (args[0], exc))
|
|
if out.returncode != 0:
|
|
raise Fault("git %s failed: %s" % (" ".join(args),
|
|
out.stderr.decode("utf-8", "replace").strip()))
|
|
return out.stdout
|
|
|
|
|
|
def find_repo():
|
|
for cand in (HERE, HERE.parent):
|
|
r = subprocess.run(["git", "-C", str(cand), "rev-parse", "--show-toplevel"],
|
|
capture_output=True, text=True)
|
|
if r.returncode == 0 and r.stdout.strip():
|
|
return Path(r.stdout.strip())
|
|
return HERE.parent.parent.parent
|
|
|
|
|
|
# --------------------------------------------------------------- the pin
|
|
def require_pin(repo, sha):
|
|
"""A revision this clone does not carry cannot grade a census of it."""
|
|
try:
|
|
git(repo, "cat-file", "-e", sha + "^{commit}")
|
|
except Fault:
|
|
raise CannotGrade("pin %s is not in this object database: a census of that "
|
|
"revision cannot be graded without it" % sha[:12])
|
|
|
|
|
|
def read_pin():
|
|
if not PIN_FILE.is_file() or PIN_FILE.stat().st_size == 0:
|
|
raise Fault("SCOPE NOT ESTABLISHED: pinned name list missing or empty: %s"
|
|
% PIN_FILE.name)
|
|
lines = PIN_FILE.read_text(encoding="utf-8").splitlines()
|
|
m = re.match(r"^# pin checkins@([0-9a-f]{40}) tree=([0-9a-f]{40})$", lines[0])
|
|
if not m:
|
|
raise Fault("SCOPE NOT ESTABLISHED: pin header not understood: %r" % lines[0])
|
|
names = lines[1:]
|
|
if not names:
|
|
raise Fault("SCOPE NOT ESTABLISHED: pin names no files")
|
|
bad = [n for n in names if not NAME_RE.match(n)]
|
|
if bad:
|
|
raise Fault("SCOPE NOT ESTABLISHED: %d pinned name(s) are not YYYY-MM-DD-*.md "
|
|
"(first: %r)" % (len(bad), bad[0]))
|
|
if len(set(names)) != len(names):
|
|
raise Fault("SCOPE NOT ESTABLISHED: duplicated name(s) in the pin")
|
|
if sorted(names) != names:
|
|
raise Fault("SCOPE NOT ESTABLISHED: pin names are not sorted (LC_ALL=C)")
|
|
return m.group(1), m.group(2), names
|
|
|
|
|
|
def leg_pin(repo, sha, tree, names):
|
|
real = git(repo, "rev-parse", ("%s:checkins" % sha)).decode().strip()
|
|
if real != tree:
|
|
raise Fault("pin tree %s is not %s:checkins (%s)" % (tree[:12], sha[:7], real[:12]))
|
|
out = git(repo, "ls-tree", "-r", "--name-only", sha, "--", "checkins/").decode("utf-8")
|
|
at_pin = sorted(p[len("checkins/"):] for p in out.splitlines()
|
|
if p.startswith("checkins/") and NAME_RE.match(p[len("checkins/"):]))
|
|
if at_pin != names:
|
|
only_pin = [n for n in at_pin if n not in set(names)]
|
|
only_list = [n for n in names if n not in set(at_pin)]
|
|
raise Fault("pin name list != checkins/ at %s (+%d at pin, +%d in pin): %s %s"
|
|
% (sha[:7], len(only_pin), len(only_list), only_pin[:3], only_list[:3]))
|
|
return "PASS pin: %d name(s) == checkins/ tree %s at %s" % (len(names), tree[:12], sha[:7])
|
|
|
|
|
|
# ------------------------------------------------------- the re-derivation
|
|
def blob(repo, sha, name):
|
|
return git(repo, "show", "%s:checkins/%s" % (sha, name)).decode("utf-8", "replace")
|
|
|
|
|
|
def analyse(text):
|
|
"""Line-oriented: flags, numbered items under ## Did, and the slice length."""
|
|
lines = text.split("\n")
|
|
flags = {s: False for s in SECTIONS}
|
|
start = None
|
|
for i, line in enumerate(lines):
|
|
bare = line.rstrip()
|
|
if bare in flags:
|
|
flags[bare] = True
|
|
if bare == "## Did" and start is None:
|
|
start = i
|
|
items, chars = 0, 0
|
|
if start is not None:
|
|
end = len(lines)
|
|
for j in range(start + 1, len(lines)):
|
|
if lines[j].rstrip() in flags:
|
|
end = j
|
|
break
|
|
body = lines[start:end]
|
|
items = len([l for l in body[1:] if ITEM_RE.match(l)])
|
|
chars = len("\n".join(body)) + (1 if end < len(lines) else 0)
|
|
return {
|
|
"has_done": flags["## Did"], "has_blocked": flags["## Blocked"],
|
|
"has_next": flags["## Next"], "has_ask": flags["## Ask"],
|
|
"did_items": items, "did_chars": chars,
|
|
}
|
|
|
|
|
|
def derive(repo, sha, names):
|
|
rows = []
|
|
for name in names:
|
|
m = NAME_RE.match(name)
|
|
rows.append(dict(date=m.group(1), signer=m.group(2), file=name,
|
|
**analyse(blob(repo, sha, name))))
|
|
return rows
|
|
|
|
|
|
def skipped_at_pin(repo, sha):
|
|
out = git(repo, "ls-tree", "-r", "--name-only", sha + ":checkins").decode("utf-8")
|
|
return [p for p in out.splitlines()
|
|
if p.endswith(".md") and not NAME_RE.match(p)]
|
|
|
|
|
|
# ------------------------------------------------------------- the tables
|
|
def leg_tsv(rows):
|
|
with open(TSV, encoding="utf-8", newline="") as fh:
|
|
reader = csv.reader(fh, delimiter="\t")
|
|
header = next(reader)
|
|
got = [[c.strip() for c in r] for r in reader]
|
|
want_header = ["date", "signer", "file", "has_done", "has_blocked", "has_next",
|
|
"has_ask", "did_items", "did_chars"]
|
|
if header != want_header:
|
|
raise Fault("TSV header %r != %r" % (header, want_header))
|
|
want = [[r["date"], r["signer"], r["file"], str(r["has_done"]), str(r["has_blocked"]),
|
|
str(r["has_next"]), str(r["has_ask"]), str(r["did_items"]), str(r["did_chars"])]
|
|
for r in rows]
|
|
if len(got) != len(want):
|
|
raise Fault("TSV has %d row(s); the pin derives %d" % (len(got), len(want)))
|
|
for i, (g, w) in enumerate(zip(got, want)):
|
|
if g != w:
|
|
raise Fault("TSV row %d: %r != pin %r" % (i, g, w))
|
|
return "PASS tsv: %d row(s) match the pin cell for cell" % len(got)
|
|
|
|
|
|
def md_block(text, header_cells):
|
|
"""Rows of the table whose header is exactly `header_cells`, as cell lists."""
|
|
lines = text.splitlines()
|
|
out = []
|
|
inside = False
|
|
for line in lines:
|
|
if not line.startswith("|"):
|
|
if inside:
|
|
break
|
|
continue
|
|
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
if not inside:
|
|
if cells == header_cells:
|
|
inside = True
|
|
continue
|
|
if set("".join(cells)) <= set("-: "):
|
|
continue
|
|
out.append(cells)
|
|
return out
|
|
|
|
|
|
def leg_md_inventory(text, rows):
|
|
want = sorted([[r["date"], r["signer"], r["file"],
|
|
"1" if r["has_done"] else "0", "1" if r["has_blocked"] else "0",
|
|
"1" if r["has_next"] else "0", "1" if r["has_ask"] else "0",
|
|
str(r["did_items"]), str(r["did_chars"])]
|
|
for r in rows], key=lambda x: (x[0], x[1]))
|
|
got = [[c.strip("`") for c in r] for r in md_block(
|
|
text, ["date", "signer", "file", "Did", "Blocked", "Next", "Ask",
|
|
"did items", "did chars"])]
|
|
if got != want:
|
|
bad = next((i for i in range(min(len(got), len(want))) if got[i] != want[i]),
|
|
min(len(got), len(want)))
|
|
raise Fault("MD inventory: %d row(s) vs pin %d; first difference at row %d "
|
|
"(md %r, pin %r)" % (len(got), len(want), bad,
|
|
got[bad] if bad < len(got) else None,
|
|
want[bad] if bad < len(want) else None))
|
|
return "PASS md-inventory: %d row(s) match the pin" % len(got)
|
|
|
|
|
|
def leg_md_headline(text, rows, skipped):
|
|
want = {
|
|
"dated check-in files": str(len(rows)),
|
|
"non-dated files skipped": str(len(skipped)),
|
|
"signers": str(len({r["signer"] for r in rows})),
|
|
"total did items": str(sum(r["did_items"] for r in rows)),
|
|
"total did chars": str(sum(r["did_chars"] for r in rows)),
|
|
}
|
|
got = {c[0]: c[1] for c in md_block(text, ["measure", "value"])}
|
|
misses = [(k, got.get(k), v) for k, v in want.items() if got.get(k) != v]
|
|
if misses:
|
|
raise Fault("MD headline disagrees with the pin: %s"
|
|
% "; ".join("%s md=%r pin=%r" % m for m in misses))
|
|
return "PASS md-headline: %d measure(s) match the pin" % len(want)
|
|
|
|
|
|
def leg_md_latest(text, rows):
|
|
latest = {}
|
|
for r in rows:
|
|
cur = latest.get(r["signer"])
|
|
if cur is None or r["date"] > cur["latest_date"]:
|
|
latest[r["signer"]] = {"signer": r["signer"], "latest_date": r["date"],
|
|
"file": r["file"]}
|
|
want = [[latest[s]["signer"], latest[s]["latest_date"], latest[s]["file"]]
|
|
for s in sorted(latest)]
|
|
got = [[c[0], c[1], c[2].strip("`")] for c in md_block(
|
|
text, ["signer", "latest check-in", "file"])]
|
|
if got != want:
|
|
raise Fault("MD per-signer latest block != pin (%d md row(s), %d derived)"
|
|
% (len(got), len(want)))
|
|
return "PASS md-latest: %d signer row(s) match the pin" % len(got)
|
|
|
|
|
|
def leg_json(rows, sha, skipped):
|
|
doc = json.loads(JSON.read_text(encoding="utf-8"))
|
|
if doc.get("pin") != sha:
|
|
raise Fault("JSON pin %r != %r" % (doc.get("pin"), sha))
|
|
got = [{k: r.get(k) for k in ("date", "signer", "file", "has_done", "has_blocked",
|
|
"has_next", "has_ask", "did_items", "did_chars")}
|
|
for r in doc.get("rows", [])]
|
|
want = [{k: r[k] for k in ("date", "signer", "file", "has_done", "has_blocked",
|
|
"has_next", "has_ask", "did_items", "did_chars")}
|
|
for r in rows]
|
|
if got != want:
|
|
bad = next((i for i in range(min(len(got), len(want))) if got[i] != want[i]),
|
|
min(len(got), len(want)))
|
|
raise Fault("JSON rows: %d vs pin %d; first difference at row %d"
|
|
% (len(got), len(want), bad))
|
|
h = doc.get("headline", {})
|
|
want_h = {"files": len(rows), "signers": len({r["signer"] for r in rows}),
|
|
"skipped_non_dated": len(skipped),
|
|
"total_did_items": sum(r["did_items"] for r in rows),
|
|
"total_did_chars": sum(r["did_chars"] for r in rows)}
|
|
misses = [(k, h.get(k), v) for k, v in want_h.items() if h.get(k) != v]
|
|
if misses:
|
|
raise Fault("JSON headline disagrees with the pin: %s"
|
|
% "; ".join("%s json=%r pin=%r" % m for m in misses))
|
|
return "PASS json: %d row(s) and %d headline counter(s) match the pin" % (
|
|
len(got), len(want_h))
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(description="grade the 2026-10-28 check-in census")
|
|
ap.add_argument("--repo", default=None, help="repository to grade (default: this tree's)")
|
|
args = ap.parse_args(argv)
|
|
repo = Path(args.repo) if args.repo else find_repo()
|
|
|
|
try:
|
|
sha, tree, names = read_pin()
|
|
require_pin(repo, sha)
|
|
print("PASS scope: %d pinned dated file(s), pin %s" % (len(names), sha[:7]))
|
|
lines = [leg_pin(repo, sha, tree, names)]
|
|
rows = derive(repo, sha, names)
|
|
skipped = skipped_at_pin(repo, sha)
|
|
text = MD.read_text(encoding="utf-8")
|
|
lines.append(leg_tsv(rows))
|
|
lines.append(leg_md_inventory(text, rows))
|
|
lines.append(leg_md_headline(text, rows, skipped))
|
|
lines.append(leg_md_latest(text, rows))
|
|
lines.append(leg_json(rows, sha, skipped))
|
|
except Fault as exc:
|
|
print("FAIL: %s" % exc)
|
|
return 1
|
|
except CannotGrade as exc:
|
|
print("CANNOT GRADE: %s" % exc)
|
|
return 2
|
|
|
|
for line in lines:
|
|
print(line)
|
|
print("VERDICT=PASS census-%s rows=%d files=%d pin=%s"
|
|
% (TAG, len(rows), len(rows), sha[:7]))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|