917 lines
42 KiB
Python
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())
|