Files
troupe-standup/tools/checkin_check.py
T

917 lines
42 KiB
Python

#!/usr/bin/env python3
"""checkin_check.py -- the gate on the check-in contract, which nothing measured.
WHY THIS EXISTS
Two files state the check-in contract and neither is instrumented.
`checkins/README.md` (in the tree since `07441fa`'s ancestry, 2026-09-24):
"One file per person per day: `YYYY-MM-DD-<handle>.md`."
`WIZARDS-TABLE.md` -- "The churn contract", item 1:
"One check-in in `checkins/YYYY-MM-DD-<name>.md` (Did / Blocked / Next / Ask)."
Read against `checkins/` at `07441fa` (69 check-in files, measured from this
host on 2026-10-18 with `python3 tools/checkin_check.py --repo . --gaps`),
both sentences are false of the archive they describe. Twenty-five of the 69
filenames are not `<date>-<handle>.md` (`-01`..`-07` suffixes, `zephyrus-wizards`,
`cassian-q5-census`, `cassian-q5-recount`), and those suffixes are not cosmetic:
ten (date, handle) days carry 2 to 8 files, the worst being 2026-10-06 for
zephyrus at 8. Of the 69, 57 do not carry all four of Did / Blocked / Next /
Ask; 12 do. A contract with no instrument is a preference, and this archive has
been running on two of them.
RULES IMPLEMENTED (stated, not implied)
F1 NAME a check-in filename is `<YYYY-MM-DD>-<handle>.md`, where <handle>
is a bare roster name: `[a-z][a-z0-9]*`. Any suffix -- `-02`,
`-wizards`, `-q5-census` -- is BAD_NAME. This is the README's
sentence, taken literally, because a rule read "generously"
(suffixes allowed, same person's day split across files) is
not the rule that was written.
F2 DUPLICATE two or more files resolving to one (date, handle) pair. The
handle is resolved by longest-roster-prefix on a `-` boundary,
so `zephyrus-02` resolves to zephyrus and `cassian-q5-census`
to cassian -- which is why F2 catches what F1 already flags:
the suffix is the mechanism, the duplicate is the damage.
F3 UNKNOWN a name that no roster handle prefixes. The roster is read from
`WIZARDS-TABLE.md`'s rows and `.mailmap`'s canonical names, so
a new wizard needs no edit here; `--roster` overrides it.
F4 DATE a date that is not a real calendar date (2026-02-30) or lies in
the future against `--today` (UTC). FUTURE is separate from
BAD_DATE because the two mean different things: a typo, versus
a clock this host cannot reproduce.
F5 EMPTY a file with no non-blank line. A filename is a claim that a
check-in is there.
A1 HEADINGS missing any of Did / Blocked / Next / Ask. ADVISORY, never a
fault and never rc 1: the four-heading sentence entered this
tree after the archive does, so grading history by it would
report 57 of 70 files faulty at `c374a0a` -- noise that buries
the 25 name faults. It is printed, counted, and carried in
--json, and the summary says which rule it is. A heading inside
a fenced block is quoted material, not structure, and does not
count (the same fence rule `queue_index.py` applies).
A2 GAP a day inside a handle's own span with no file, and days since
the newest file against --today. ADVISORY, per the table's own
words: "A lane that stops churning is a lane that needs a
decision, not silence." A missing day is not a repository
fault; it is a fact an operator acts on.
EXIT CODES (the repo's contract, `tools/selftest_sweep.py`)
0 ran; nothing to report, or faults reported without --fail
1 ran, and graded at least one F-fault, under --fail
2 could not grade: no checkins dir, no roster, a baseline ledger this host
cannot read or parse, or a file this host cannot read as UTF-8. Never 1,
never a traceback.
BASELINE: GRADING THE WHOLE ARCHIVE WITHOUT KEEPING IT RED
A `--since` floor is how this tool stayed adoptable: the faults the archive
already carried are the record and are not re-litigated every run. But a floor grades *one
side of the archive*, and a floor chosen from the archive's own newest date
grades **nothing that is added before that date**. Measured on a clone of
`e98a34b` on 2026-10-19: a new `checkins/2026-10-02-volt-b.md` (BAD_NAME)
added today is invisible to `--since 2026-10-19` -- `verify.py --fast` printed
`checkin_check: 1 files, 0 with a fault` and exited **0** -- while the same
tree's whole-archive run exited **1** with that file in the table. A backdated
filename is not a hypothetical: it is what a typo in the date half makes.
`--baseline PATH` replaces the floor with a ledger. The ledger is one row per
*known-faulting file*: `<filename>\t<CLASS[,CLASS]>`, with `DUPLICATE` as a
class like any other. A file's classes are then split into **known** (in the
ledger) and **new**; only a new class makes rc 1, so the archive can be
graded end to end every run and a fault that lands *anywhere*, on any date, is
red the moment it lands. The split is per **class**, not per path: an existing
file that gains a class it did not have (a check-in truncated to empty) is
new, because the ledger states what was tolerated, not that the path is
exempt. Entries that no longer match -- file gone, or class cleared -- are
reported as stale and never make rc 1: that is someone repairing the record,
which is the point.
`--emit-baseline` prints that ledger to stdout and exits 0, so refreshing it
is a redirect and the tool keeps no write path of its own. An unreadable or
malformed ledger is rc 2, never a silent pass: a baseline that cannot be read
cannot certify anything.
READ-ONLY: the only write paths are `--json PATH` and stdout. It renames
nothing -- the badly named files are the principals' history, and a tool that
renamed them would be editing the record to make its own report green.
USAGE:
python3 checkin_check.py --repo /root/troupe-standup
python3 checkin_check.py --repo . --gaps --json /tmp/checkins.json
python3 checkin_check.py --repo . --fail --baseline tools/checkin_baseline.tsv
python3 checkin_check.py --repo . --emit-baseline > tools/checkin_baseline.tsv
python3 checkin_check.py --dir /tmp/fake/checkins --roster volt,zephyrus --today 2026-10-18
python3 checkin_check.py --selftest # 35 cells, names, dates, roster, baseline, refusals
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from collections import Counter
from datetime import date, datetime, timedelta, timezone
NAME_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})-([a-z][a-z0-9]*)\.md$")
AMBIENT_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})-(.+)\.md$")
ROSTER_HANDLE_RE = re.compile(r"^[a-z][a-z0-9]*$")
HEADING_RE = re.compile(r"^#{1,6}\s+(.*\S)\s*$")
# A fenced block is quoted material, never structure (see headings_missing).
FENCE_RE = re.compile(r"^\s*(```|~~~)")
# The four headings are matched on the heading's own first word, case-folded.
WANT_HEADINGS = ("did", "blocked", "next", "ask")
FAULTS = ("BAD_NAME", "UNKNOWN_HANDLE", "BAD_DATE", "FUTURE", "EMPTY")
# Every class a baseline ledger may name. DUPLICATE is computed after grading
# (it is a property of a *group*), so it lives here rather than in FAULTS -- but
# a ledger that cannot name it could not record the worst rows in this archive.
BASELINE_CLASSES = frozenset(FAULTS) | {"DUPLICATE"}
# Not a check-in, and never silently dropped: the directory's own README is
# printed as SKIP so a reader can see it was seen and why it was not graded.
SKIP_NAMES = {"README.md"}
class CannotGrade(Exception):
"""Anything that stops the tool from grading: rc 2, one message."""
# --------------------------------------------------------------------- roster
def roster_from_table(text: str):
"""Handles from WIZARDS-TABLE.md's `| Wizard | ... |` rows: first token, lowered.
Skips the header row and the `|---|` separator. A row whose first cell is not
a bare name contributes nothing -- this reads the roster, it does not guess.
"""
out = set()
in_table = False
for line in text.splitlines():
s = line.strip()
if not s.startswith("|"):
in_table = False
continue
cells = [c.strip() for c in s.strip("|").split("|")]
if not cells or not cells[0]:
continue
if cells[0].lower() == "wizard":
in_table = True
continue
if not in_table or set(cells[0]) <= set("-: "):
continue
first = cells[0].split()[0].lower() if cells[0].split() else ""
if ROSTER_HANDLE_RE.match(first):
out.add(first)
return out
def roster_from_mailmap(text: str):
"""Handles from the canonical side of `.mailmap`: `Name <mail> other <mail>`."""
out = set()
for line in text.splitlines():
s = line.strip()
if not s or s.startswith("#") or "<" not in s:
continue
name = s.split("<")[0].strip()
# `Zephyrus Sparkborn <z@…>` -> the handle is the FIRST name, not the last.
first = name.split()[0].lower() if name.split() else ""
if ROSTER_HANDLE_RE.match(first):
out.add(first)
return out
def load_roster(repo: str, override: str | None):
"""(handles, source-string). Raises CannotGrade when there is nothing to read."""
if override:
hs = {h.strip().lower() for h in override.split(",") if h.strip()}
if not hs:
raise CannotGrade("cannot grade: --roster is empty")
return hs, "--roster"
sources, out = [], set()
table = os.path.join(repo, "WIZARDS-TABLE.md")
mailmap = os.path.join(repo, ".mailmap")
for path, fn, label in ((table, roster_from_table, "WIZARDS-TABLE.md"),
(mailmap, roster_from_mailmap, ".mailmap")):
if not os.path.exists(path):
continue
try:
with open(path, "r", encoding="utf-8") as fh:
got = fn(fh.read())
except (OSError, UnicodeDecodeError) as exc:
raise CannotGrade(f"cannot read roster {label}: {exc}")
if got:
out |= got
sources.append(f"{label} ({len(got)})")
if not out:
raise CannotGrade("cannot grade: no roster "
f"(looked for WIZARDS-TABLE.md and .mailmap in {repo})")
return out, " + ".join(sources)
# --------------------------------------------------------------------- grading
def resolve_handle(name: str, roster):
"""The longest roster handle that is `name` or a `-`-delimited prefix of it."""
best = ""
for h in roster:
if name == h or name.startswith(h + "-"):
if len(h) > len(best):
best = h
return best or None
def parse_date(triple):
"""'YYYY-MM-DD' -> date, or None when the calendar refuses it."""
try:
return date(int(triple[0]), int(triple[1]), int(triple[2]))
except ValueError:
return None
def read_text(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as fh:
return fh.read()
except UnicodeDecodeError as exc:
raise CannotGrade(f"cannot read {path}: not UTF-8 ({exc.reason} at byte {exc.start})")
except OSError as exc:
raise CannotGrade(f"cannot read {path}: {exc}")
def headings_missing(text: str):
"""The four contract words this file does NOT use as a heading.
A fenced block is quoted material, not structure: a check-in that shows the
contract itself (``` ## Did / ## Blocked / ## Next / ## Ask ```) was graded
as carrying all four while carrying none. `queue_index.py` already skips
fences for the same reason; this reader now does too. Measured over the 74
files at `331927f`: 0 files change grade (60 of 74 advisory-missing before
and after), so the fix is a sound for the quoted-heading form, not a
re-reading of the archive.
"""
seen, in_fence = set(), False
for line in text.splitlines():
if FENCE_RE.match(line):
in_fence = not in_fence
continue
if in_fence:
continue
m = HEADING_RE.match(line)
if m:
seen.add(m.group(1).split()[0].strip("*_`:").lower())
return [w for w in WANT_HEADINGS if w not in seen]
def grade_file(path: str, roster, today: date):
"""One row. Reads the file only when the name parsed far enough to need it."""
base = os.path.basename(path)
rec = {"file": base, "path": path, "handle": None, "date": None,
"class": [], "headings_missing": [], "lines": 0}
m = AMBIENT_RE.match(base)
if not m:
rec["class"].append("BAD_NAME")
rec["detail"] = "not <YYYY-MM-DD>-<handle>.md"
return rec
rec["date"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
name = m.group(4)
strict = NAME_RE.match(base)
rec["handle"] = resolve_handle(name, roster)
if not strict:
rec["class"].append("BAD_NAME")
if rec["handle"] is None:
rec["class"].append("UNKNOWN_HANDLE")
d = parse_date((m.group(1), m.group(2), m.group(3)))
if d is None:
rec["class"].append("BAD_DATE")
elif d > today:
rec["class"].append("FUTURE")
text = read_text(path)
rec["lines"] = len(text.splitlines())
if not text.strip():
rec["class"].append("EMPTY")
else:
rec["headings_missing"] = headings_missing(text)
return rec
def duplicates(rows):
"""{(date, handle): [files]} for groups with more than one file."""
groups = {}
for r in rows:
if r["handle"] and r["date"] and "BAD_DATE" not in r["class"]:
groups.setdefault((r["date"], r["handle"]), []).append(r["file"])
return {k: sorted(v) for k, v in groups.items() if len(v) > 1}
def gaps(rows, today: date, cap: int = 60):
"""Per-handle coverage + the missing days inside its own span (not after it)."""
by_handle = {}
for r in rows:
# A future-dated file is a clock fault, not coverage: counting it would
# stretch the span to the year 2099 and report 26k "missing" days.
if not r["handle"] or r["class"] and ("BAD_DATE" in r["class"] or "FUTURE" in r["class"]):
continue
by_handle.setdefault(r["handle"], set()).add(r["date"])
out = []
for h in sorted(by_handle):
days = sorted(by_handle[h])
first, last = date.fromisoformat(days[0]), date.fromisoformat(days[-1])
missing, run, longest, run_end = [], 0, 0, None
cur = first
while cur <= last:
if cur.isoformat() in by_handle[h]:
run = 0
else:
missing.append(cur.isoformat())
run += 1
if run > longest:
longest, run_end = run, cur
cur += timedelta(days=1)
since = (today - last).days
# `days` and `files` are different numbers: zephyrus has 44 files across
# 23 days, and a table that called the second one "files" would be the
# exact defect this archive keeps grading other people for.
nfiles = sum(1 for r in rows
if r["handle"] == h
and not ("BAD_DATE" in r["class"] or "FUTURE" in r["class"]))
out.append({"handle": h, "files": nfiles, "days": len(by_handle[h]),
"first": days[0], "last": days[-1],
"days_in_span": (last - first).days + 1,
"missing": missing[:cap], "missing_count": len(missing),
"longest_gap": longest,
"longest_gap_ends": run_end.isoformat() if run_end else None,
"days_since_last": since})
return out
# --------------------------------------------------------------------- baseline
def classes_of(rec):
"""A row's full class set, duplicates folded in: the ledger's unit."""
return set(rec["class"]) | ({"DUPLICATE"} if rec.get("duplicate") else set())
def emit_baseline(rep):
"""The ledger as text: one `<filename>\\t<CLASS,CLASS>` row per faulting file.
Basenames, not paths: `checkins/` is flat and the ledger has to survive a
checkout on another host. Sorted, so regenerating it on an unchanged archive
produces an unchanged file and the diff is only what moved. The header is
part of the output because the output IS the file: a ledger that has to be
explained somewhere else is a ledger nobody can read on its own.
"""
head = ("# check-in baseline -- the faults this archive already carried when the\n"
"# naming rule was written. One row per file: <filename>\\t<CLASS[,CLASS]>.\n"
"# Regenerate: checkin_check.py --repo . --emit-baseline > tools/checkin_baseline.tsv\n"
"# A row is a stated tolerance, not an exemption: a file that gains a class,\n"
"# and a file that is not listed, both make rc 1 under --fail --baseline.\n")
lines = []
for r in sorted(rep["rows"], key=lambda x: x["file"]):
cls = sorted(classes_of(r))
if cls:
lines.append(f"{r['file']}\t{','.join(cls)}")
return head + "\n".join(lines) + ("\n" if lines else "")
def load_baseline(path):
"""Read the ledger -> {filename: set(classes)}. Anything unreadable is rc 2.
A ledger is what turns "35 historical faults" from a reason not to grade
into a stated tolerance. If it cannot be read, the tool has no idea what was
tolerated, so it refuses rather than grading against an empty set (which
would read the whole archive as new) or skipping the check (which would read
nothing as new). Both of those are the silent-pass class this repo grades.
"""
try:
with open(path, "r", encoding="utf-8") as fh:
text = fh.read()
except OSError as exc:
raise CannotGrade(f"cannot grade: baseline {path}: {exc}")
except UnicodeDecodeError as exc:
raise CannotGrade(f"cannot grade: baseline {path} is not UTF-8: {exc}")
out = {}
for n, line in enumerate(text.splitlines(), 1):
s = line.rstrip("\r")
if not s.strip() or s.lstrip().startswith("#"):
continue
parts = s.split("\t")
if len(parts) != 2:
raise CannotGrade(f"cannot grade: baseline {path}:{n} is not "
f"<filename>\\t<CLASS[,CLASS]>: {s!r}")
name = parts[0].strip()
cls = [c.strip() for c in parts[1].split(",") if c.strip()]
bad = [c for c in cls if c not in BASELINE_CLASSES]
if not name or not cls or bad:
raise CannotGrade(f"cannot grade: baseline {path}:{n} names an "
f"empty filename, an empty class list, or an "
f"unknown class {bad}: {s!r}")
out[name] = set(cls)
return out
def apply_baseline(rep, ledger, path=""):
"""Split every row's classes into known and new, and count what is stale."""
current = {r["file"]: classes_of(r) for r in rep["rows"]}
known_files = 0
for r in rep["rows"]:
cls = current[r["file"]]
known = ledger.get(r["file"], set())
new = cls - known
r["baseline"] = {"known": sorted(cls & known), "new": sorted(new)}
if cls and not new:
known_files += 1
stale = sorted(f for f in ledger if f not in current)
cleared = sorted(f for f in ledger
if f in current and (ledger[f] - current[f]))
rep["baseline"] = {"path": path, "entries": len(ledger),
"known_files": known_files,
"new_faults": sum(1 for r in rep["rows"]
if r["baseline"]["new"]),
"stale": stale, "cleared": cleared}
return rep
def graded_faults(rep):
"""The rows that make rc 1.
Without a ledger: every faulting row, exactly as before. With one: only the
rows carrying a class the ledger does not name -- the ledger is a stated
tolerance, not an exemption, so "known" never silently becomes "fine".
"""
if rep.get("baseline"):
return [r for r in rep["rows"] if r.get("baseline", {}).get("new")]
return [r for r in rep["rows"] if r["class"] or r["duplicate"]]
# --------------------------------------------------------------------- reporting
def banner(text: str) -> str:
return f"== {text} =="
def run(path: str, roster, today: date, show_gaps: bool,
since: str | None = None):
"""Everything except printing the file table. Returns the report dict."""
if not os.path.isdir(path):
raise CannotGrade(f"cannot read checkins dir {path}: not a directory")
try:
names = sorted(os.listdir(path))
except OSError as exc:
raise CannotGrade(f"cannot read checkins dir {path}: {exc}")
files = [os.path.join(path, n) for n in names
if n.endswith(".md") and n not in SKIP_NAMES
and os.path.isfile(os.path.join(path, n))]
skipped = [n for n in names if n in SKIP_NAMES]
# --since grades forward only: the archive's 25 historical names are the
# record, and a gate that can only ever be red is a gate nobody adopts.
excluded_since = 0
if since:
keep = []
for p in files:
m = AMBIENT_RE.match(os.path.basename(p))
if m and f"{m.group(1)}-{m.group(2)}-{m.group(3)}" >= since:
keep.append(p)
else:
excluded_since += 1
files = keep
if not files and not since:
raise CannotGrade(f"cannot read checkins dir {path}: no *.md files")
rows = [grade_file(p, roster, today) for p in files]
dup = duplicates(rows)
dup_map = {}
for (d, h), fl in dup.items():
for f in fl:
dup_map[f] = (d, h, len(fl))
for r in rows:
if r["file"] in dup_map:
r["duplicate"] = {"date": dup_map[r["file"]][0],
"handle": dup_map[r["file"]][1],
"group_size": dup_map[r["file"]][2]}
else:
r["duplicate"] = None
fault_rows = [r for r in rows if r["class"] or r["duplicate"]]
# One pass over `rows` replaces seven `sum(1 for r in rows ...)` scans --
# five verdict passes (`faults`, one per FAULT class) plus two advisory
# passes (`missing_headings`, `headings_ok`). Counter buckets verdicts by
# presence: `set(r["class"])` folds any intra-row duplicate so a row counts
# once per class, matching `c in r["class"]`; the two advisory counters are
# incremented alongside, so each row is touched once, not seven times.
fault_counts = Counter()
missing_headings = 0
headings_ok = 0
for r in rows:
fault_counts.update(set(r["class"]))
if r["headings_missing"]:
missing_headings += 1
elif "EMPTY" not in r["class"]:
headings_ok += 1
rep = {
"dir": path, "checked": len(files), "today": today.isoformat(),
"roster_size": len(roster), "skipped": skipped,
"since": since, "excluded_since": excluded_since,
"faults": {c: fault_counts.get(c, 0) for c in FAULTS},
"fault_rows": len(fault_rows),
"duplicate_groups": [{"date": d, "handle": h, "files": fl}
for (d, h), fl in sorted(dup.items())],
"advisory": {
"missing_headings": missing_headings,
"headings_ok": headings_ok,
"note": "A1/A2 are advisory: the four-heading rule postdates the archive",
},
"rows": rows,
}
if show_gaps:
rep["gaps"] = gaps(rows, today)
return rep
def print_report(rep, show_gaps: bool, fail: bool):
print(banner(f"{rep['checked']} check-in files in {rep['dir']}"))
print(f"roster: {rep['roster_size']} handles; today {rep['today']}")
if rep.get("since"):
print(f"since {rep['since']}: {rep['checked']} graded, "
f"{rep['excluded_since']} older file(s) not graded")
for n in rep.get("skipped") or []:
print(f" SKIP {n} (the directory's own README: not a check-in)")
led = rep.get("baseline")
fault_rows = graded_faults(rep)
if led:
print(f"\nbaseline {led['path']}: {led['entries']} entries, "
f"{led['known_files']} file(s) known-faulting, "
f"{led['new_faults']} new fault(s), {len(led['stale'])} stale, "
f"{len(led['cleared'])} cleared")
for f in led["stale"]:
print(f" STALE BASELINE {f} (no longer in the directory: the record was repaired)")
for f in led["cleared"]:
print(f" STALE BASELINE {f} (no longer carries every class the ledger names)")
if not fault_rows:
print("\nno file violates the filename, date, emptiness or duplicate rule"
if not led else
"\nno file carries a class the baseline does not tolerate")
else:
width = max(len(r["file"]) for r in fault_rows)
print(f"\n{'file':<{width}} {'verdict':<22} detail")
for r in fault_rows:
cls = (r["baseline"]["new"] if led
else sorted(set(r["class"]) | ({"DUPLICATE"} if r["duplicate"] else set())))
detail = []
if "BAD_NAME" in cls:
detail.append("name is not <date>-<handle>.md")
if "UNKNOWN_HANDLE" in cls:
detail.append("no roster handle prefixes it")
if "BAD_DATE" in cls:
detail.append("not a calendar date")
if "FUTURE" in cls:
detail.append(f"after today {rep['today']}")
if "EMPTY" in cls:
detail.append("no non-blank line")
if "DUPLICATE" in cls and r["duplicate"]:
d = r["duplicate"]
detail.append(f"{d['group_size']} files for {d['handle']} on {d['date']}")
print(f"{r['file']:<{width}} {','.join(cls):<22} {'; '.join(detail)}")
adv = rep["advisory"]
print(f"\nadvisory (never rc 1): {adv['missing_headings']} of {rep['checked']} "
f"files lack a Did/Blocked/Next/Ask heading")
print(" -- A1: the four-heading rule postdates the archive; grade it where it applies")
if show_gaps:
print(f"\n{'handle':<12} {'files':>5} {'days':>5} {'first':>10} {'last':>10} "
f"{'span':>4} {'miss':>4} {'gap':>4} {'idle':>4}")
for g in rep["gaps"]:
print(f"{g['handle']:<12} {g['files']:>5} {g['days']:>5} {g['first']:>10} "
f"{g['last']:>10} {g['days_in_span']:>4} {g['missing_count']:>4} "
f"{g['longest_gap']:>4} {g['days_since_last']:>4}")
print(" -- A2: a missing day is not a repo fault; it is a decision an operator owes")
counts = ", ".join(f"{k} {v}" for k, v in rep["faults"].items() if v)
if led:
print(f"\ncheckin_check: {rep['checked']} files, {led['new_faults']} new fault(s), "
f"{led['known_files']} known-faulting in the baseline"
+ (f" ({counts}; DUPLICATE groups {len(rep['duplicate_groups'])})" if counts else "")
+ f", {adv['missing_headings']} advisory")
if fail and led["new_faults"]:
print(f"FAIL: {led['new_faults']} file(s) carry a class the baseline does not tolerate")
return 1
return 0
print(f"\ncheckin_check: {rep['checked']} files, {rep['fault_rows']} with a fault"
+ (f" ({counts}; DUPLICATE groups {len(rep['duplicate_groups'])})" if counts else "")
+ f", {adv['missing_headings']} advisory")
if fail and rep["fault_rows"]:
print(f"FAIL: {rep['fault_rows']} file(s) violate the check-in contract")
return 1
return 0
# --------------------------------------------------------------------- selftest
def _mk(d: str, name: str, body: str, encoding="utf-8"):
p = os.path.join(d, name)
with open(p, "w", encoding=encoding, errors="strict") as fh:
fh.write(body)
return p
GOOD = "# 2026-10-17 -- volt\n\n## Did\nlanded x\n\n## Blocked\nnothing\n\n## Next\ny\n\n## Ask\nno\n"
BARE = "# 2026-10-17 -- volt\n\nlanded x\n"
TABLE = """# The Wizards Table
| Wizard | Model | Lane |
|---|---|---|
| Timmy | Hermes | partner |
| Zephyrus Sparkborn | Mistral | archive |
| Volt | DeepSeek | workhorse |
| not a roster row
"""
def selftest():
bad = []
def cell(name, ok, detail=""):
print(f"{'PASS' if ok else 'FAIL'} {name}" + (f" [{detail}]" if detail and not ok else ""))
if not ok:
bad.append(name)
cell("c1 roster from the table's rows only",
roster_from_table(TABLE) == {"timmy", "zephyrus", "volt"},
str(roster_from_table(TABLE)))
cell("c2 roster from .mailmap's canonical names",
roster_from_mailmap("Volt <volt@a> volt <volt@b>\n"
"Zephyrus Sparkborn <z@s> <z@n>\n") == {"volt", "zephyrus"},
str(roster_from_mailmap("Volt <volt@a> volt <volt@b>\n")))
cell("c3 comment lines and blank lines in .mailmap are skipped",
roster_from_mailmap("# c\n\n") == set())
m4 = NAME_RE.match("2026-10-17-volt.md")
cell("c4 the strict name parses", m4 is not None and m4.group(4) == "volt")
cell("c5 a -02 suffix is not the strict name",
NAME_RE.match("2026-09-24-zephyrus-02.md") is None
and AMBIENT_RE.match("2026-09-24-zephyrus-02.md").group(4) == "zephyrus-02")
cell("c6 longest-prefix handle resolution",
(resolve_handle("zephyrus-02", {"zephyrus", "volt"}) == "zephyrus"
and resolve_handle("cassian-q5-census", {"cassian"}) == "cassian"
and resolve_handle("gandalf", {"volt"}) is None))
cell("c7 a hyphen boundary is required, not a bare prefix",
resolve_handle("voltmeter", {"volt"}) is None)
cell("c8 2026-02-30 is not a calendar date",
parse_date(("2026", "02", "30")) is None
and parse_date(("2026", "02", "28")) is not None)
cell("c9 headings are matched by first word, case-folded",
headings_missing(GOOD) == [] and headings_missing(BARE) == list(WANT_HEADINGS),
f"{headings_missing(GOOD)} / {headings_missing(BARE)}")
today = date(2026, 10, 18)
with __import__("tempfile").TemporaryDirectory() as td:
repo = os.path.join(td, "repo")
ck = os.path.join(repo, "checkins")
os.makedirs(ck)
_mk(repo, "WIZARDS-TABLE.md", TABLE)
_mk(ck, "2026-10-17-volt.md", GOOD)
_mk(ck, "2026-10-17-zephyrus.md", BARE)
_mk(ck, "2026-10-17-zephyrus-02.md", BARE)
_mk(ck, "2026-10-16-gandalf.md", GOOD)
_mk(ck, "2026-02-30-volt.md", GOOD)
_mk(ck, "2099-01-01-volt.md", GOOD)
_mk(ck, "2026-10-15-volt.md", "")
_mk(ck, "README.md", "# Check-ins\n\nOne file per person per day.\n")
roster, src = load_roster(repo, None)
cell("c10 roster is the union of table and mailmap, and says so",
roster == {"timmy", "zephyrus", "volt"} and "WIZARDS-TABLE.md" in src,
f"{roster} / {src}")
rep = run(ck, roster, today, True)
by = {r["file"]: r for r in rep["rows"]}
cell("c11 a conforming complete file has no class",
by["2026-10-17-volt.md"]["class"] == []
and by["2026-10-17-volt.md"]["headings_missing"] == [])
cell("c12 the -02 file is BAD_NAME and duplicates zephyrus",
by["2026-10-17-zephyrus-02.md"]["class"] == ["BAD_NAME"]
and by["2026-10-17-zephyrus-02.md"]["duplicate"] is not None,
str(by["2026-10-17-zephyrus-02.md"]))
cell("c13 an unknown handle is UNKNOWN_HANDLE",
by["2026-10-16-gandalf.md"]["class"] == ["UNKNOWN_HANDLE"],
str(by["2026-10-16-gandalf.md"]["class"]))
cell("c14 a false calendar date is BAD_DATE, not FUTURE",
by["2026-02-30-volt.md"]["class"] == ["BAD_DATE"])
cell("c15 a future date is FUTURE",
by["2099-01-01-volt.md"]["class"] == ["FUTURE"])
cell("c16 an empty file is EMPTY",
by["2026-10-15-volt.md"]["class"] == ["EMPTY"])
cell("c17 the duplicate group carries both files",
rep["duplicate_groups"] == [{"date": "2026-10-17", "handle": "zephyrus",
"files": ["2026-10-17-zephyrus-02.md",
"2026-10-17-zephyrus.md"]}],
str(rep["duplicate_groups"]))
cell("c18 the advisory counts, and does not enter the fault count",
rep["advisory"]["missing_headings"] == 2 and rep["fault_rows"] == 6,
f"{rep['advisory']} faults={rep['fault_rows']}")
g = {x["handle"]: x for x in rep["gaps"]}
cell("c19 the gap axis counts missing days inside a handle's span",
g["volt"]["missing_count"] == 1 and g["volt"]["longest_gap"] == 1,
str(g.get("volt")))
cell("c27 'files' and 'days' are two columns, not one",
g["zephyrus"]["files"] == 2 and g["zephyrus"]["days"] == 1,
str(g.get("zephyrus")))
rc_fail = print_report(rep, True, True)
cell("c20 --fail exits 1 when a fault exists", rc_fail == 1, str(rc_fail))
# refusals: rc 2, never a traceback, never 1
miss = os.path.join(td, "no-such-dir")
rc_missing = main(["--dir", miss, "--roster", "volt"])
cell("c21 a missing checkins dir refuses at rc 2", rc_missing == 2, str(rc_missing))
_mk(ck, "2026-10-14-volt.md", "not utf8")
with open(os.path.join(ck, "2026-10-14-volt.md"), "wb") as fh:
fh.write(b"\xff\xfe\x00bad\x00\n")
rc_bin = main(["--dir", ck, "--roster", "volt,timmy,zephyrus"])
cell("c22 a non-UTF-8 check-in refuses at rc 2", rc_bin == 2, str(rc_bin))
rc_noroster = main(["--dir", ck, "--repo", os.path.join(td, "empty")])
cell("c23 no roster at all refuses at rc 2", rc_noroster == 2, str(rc_noroster))
cell("c24 the directory's README is skipped, not graded as a check-in",
rep["skipped"] == ["README.md"]
and "README.md" not in [r["file"] for r in rep["rows"]]
and rep["checked"] == 7, f"{rep['skipped']} {rep['checked']}")
rep_since = run(ck, roster, today, False, since="2026-10-16")
cell("c25 --since grades forward only, and says how many it left out",
rep_since["checked"] == 5 and rep_since["excluded_since"] == 3
and "2026-10-15-volt.md" not in [r["file"] for r in rep_since["rows"]],
f"{rep_since['checked']}/{rep_since['excluded_since']}")
cell("c26 a malformed --since refuses at rc 2, never 1",
main(["--dir", ck, "--roster", "volt", "--since", "16-10-2026"]) == 2)
# The quoted-heading form: a fenced block is material, not structure. Both
# cells drive the real reader on inline text, not a fixture line number.
quoted = ("# 2026-10-17 -- volt\n\n## Did\nx\n\n## Blocked\nx\n\n"
"```\n## Next\ny\n## Ask\nno\n```\n")
cell("c28 a heading inside a fenced block is not a heading",
headings_missing(quoted) == ["next", "ask"],
str(headings_missing(quoted)))
cell("c29 an unclosed fence quotes to end of file, and says so",
headings_missing("## Did\nx\n```\n## Blocked\n## Next\n## Ask\n")
== ["blocked", "next", "ask"])
# c30-c35: the baseline ledger. The floor this tool was run under graded one
# side of the archive; these cells pin that a fault landing on ANY date is
# still graded, that the tolerance is per class rather than per path, and
# that an unreadable ledger refuses instead of certifying.
with __import__("tempfile").TemporaryDirectory() as td:
import contextlib
import io as _io
ck = os.path.join(td, "checkins")
os.makedirs(ck)
_mk(ck, "2026-10-17-volt.md", GOOD) # conforming
_mk(ck, "2026-10-14-volt-02.md", GOOD) # BAD_NAME
_mk(ck, "2026-10-13-volt-03.md", "") # BAD_NAME, EMPTY
_mk(ck, "2026-10-12-volt-04.md", "") # BAD_NAME, EMPTY -- "backdated"
btoday = date(2026, 10, 18)
broster = {"volt"}
def with_ledger(ledger_):
r_ = run(ck, broster, btoday, False)
apply_baseline(r_, ledger_, "fixture.tsv")
with contextlib.redirect_stdout(_io.StringIO()):
rc_ = print_report(r_, False, True)
return r_, sorted(r["file"] for r in graded_faults(r_)), rc_
everything = {"2026-10-14-volt-02.md": {"BAD_NAME"},
"2026-10-13-volt-03.md": {"BAD_NAME", "EMPTY"},
"2026-10-12-volt-04.md": {"BAD_NAME", "EMPTY"}}
_, g_none, rc_none = with_ledger(everything)
cell("c30 a ledger naming every class grades the whole archive rc 0",
g_none == [] and rc_none == 0, f"graded={g_none} rc={rc_none}")
partial = dict(everything)
del partial["2026-10-12-volt-04.md"]
_, g_new, rc_new = with_ledger(partial)
cell("c31 a fault on an older date that the ledger does not name is rc 1",
g_new == ["2026-10-12-volt-04.md"] and rc_new == 1,
f"graded={g_new} rc={rc_new}")
per_class = dict(everything, **{"2026-10-12-volt-04.md": {"BAD_NAME"}})
r_class, g_class, rc_class = with_ledger(per_class)
new_here = [r["baseline"]["new"] for r in r_class["rows"]
if r["file"] == "2026-10-12-volt-04.md"]
cell("c32 the ledger is per class, not per path: a gained class is new",
new_here == [["EMPTY"]] and rc_class == 1, f"{new_here} rc={rc_class}")
rep_emit = run(ck, broster, btoday, False)
bl_path = os.path.join(td, "ledger.tsv")
with open(bl_path, "w", encoding="utf-8") as fh:
fh.write(emit_baseline(rep_emit))
round_trip = load_baseline(bl_path)
_, g_rt, rc_rt = with_ledger(round_trip)
cell("c33 emit -> load round-trips, and silences exactly the current archive",
g_rt == [] and rc_rt == 0
and round_trip == {r["file"]: classes_of(r) for r in rep_emit["rows"]
if classes_of(r)},
f"{round_trip} graded={g_rt}")
gone = dict(everything, **{"2026-10-01-volt.md": {"BAD_NAME"}})
r_stale, g_stale, rc_stale = with_ledger(gone)
cell("c34 an entry for a file that is gone is stale, never a fault",
r_stale["baseline"]["stale"] == ["2026-10-01-volt.md"]
and g_stale == [] and rc_stale == 0, str(r_stale["baseline"]))
bad_ledgers, why = [], []
for label, body in (("no tab", "2026-10-13-volt-03.md\n"),
("unknown class", "2026-10-13-volt-03.md\tNOT_A_CLASS\n"),
("empty classes", "2026-10-13-volt-03.md\t\n")):
p = os.path.join(td, "bad-%s.tsv" % label.replace(" ", "-"))
_mk(td, os.path.basename(p), body)
try:
load_baseline(p)
bad_ledgers.append(label)
except CannotGrade:
why.append(label)
try:
load_baseline(os.path.join(td, "absent.tsv"))
bad_ledgers.append("missing file")
except CannotGrade:
why.append("missing file")
cell("c35 an unreadable, malformed or absent ledger refuses (rc-2 class)",
bad_ledgers == [], f"accepted: {bad_ledgers}")
print(f"selftest: 35 cells, {35 - len(bad)} passed, {len(bad)} failed")
return 1 if bad else 0
# --------------------------------------------------------------------- main
def main(argv=None):
ap = argparse.ArgumentParser(
description="checkin_check.py -- the gate on the check-in filename and date "
"contract, shipped with its tests.")
ap.add_argument("--repo", default=".", help="repository root (roster is read here)")
ap.add_argument("--dir", help="checkins dir (default <repo>/checkins)")
ap.add_argument("--roster", help="comma-separated handles, instead of the roster files")
ap.add_argument("--today", help="YYYY-MM-DD for the future-date rule (default: UTC today)")
ap.add_argument("--gaps", action="store_true", help="print per-handle coverage and gaps")
ap.add_argument("--since", help="grade only check-ins dated on/after this YYYY-MM-DD "
"(the archive's older names are its record)")
ap.add_argument("--json", help="write the full report here")
ap.add_argument("--baseline", help="known-fault ledger (<filename>\\t<CLASS[,CLASS]>): "
"only classes it does not name make rc 1, so the "
"whole archive can be graded every run")
ap.add_argument("--emit-baseline", action="store_true",
help="print the ledger for the current directory to stdout and exit 0")
ap.add_argument("--fail", action="store_true", help="exit 1 when any file has a fault")
ap.add_argument("--selftest", action="store_true", help="fixture corpus")
a = ap.parse_args(argv)
if a.selftest:
return selftest()
if a.emit_baseline and a.baseline:
print("cannot grade: --emit-baseline and --baseline are different jobs",
file=sys.stderr)
return 2
if a.today:
try:
today = date.fromisoformat(a.today)
except ValueError:
print(f"cannot grade: --today is not YYYY-MM-DD: {a.today}", file=sys.stderr)
return 2
else:
today = datetime.now(timezone.utc).date()
repo = os.path.abspath(a.repo)
path = os.path.abspath(a.dir) if a.dir else os.path.join(repo, "checkins")
since = a.since
if since:
try:
date.fromisoformat(since)
except ValueError:
print(f"cannot grade: --since is not YYYY-MM-DD: {since}", file=sys.stderr)
return 2
try:
roster, src = load_roster(repo, a.roster)
rep = run(path, roster, today, a.gaps, since=since)
if a.baseline:
rep = apply_baseline(rep, load_baseline(a.baseline), a.baseline)
except CannotGrade as exc:
print(str(exc), file=sys.stderr)
return 2
rep["roster_source"] = src
if a.emit_baseline:
sys.stdout.write(emit_baseline(rep))
if a.json:
with open(a.json, "w", encoding="utf-8") as fh:
json.dump(rep, fh, indent=1, sort_keys=False)
return 0
rc = print_report(rep, a.gaps, a.fail)
if a.json:
with open(a.json, "w", encoding="utf-8") as fh:
json.dump(rep, fh, indent=1, sort_keys=False)
print(f"wrote {a.json}")
return rc
if __name__ == "__main__":
sys.exit(main())