tools (volt): the check-in contract gets an instrument -- 25 of 69 names violate README, 10 days carry 2-8 files, 57 of 69 heading-incomplete

This commit is contained in:
volt committed 2026-10-18 00:19:25 +00:00
1 parent 07441fa557
commit 588784d7ea
2 files changed
+591 -2

No files matched your search

+579
View File
@@ -0,0 +1,579 @@
#!/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 52 of 70 files faulty -- noise that buries the 12 real
name faults. It is printed, counted, and carried in --json, and
the summary says which rule it is.
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, or a file this host cannot
read as UTF-8. Never 1, never a traceback.
READ-ONLY: the only write path is `--json PATH`. It renames nothing -- the
12 BAD_NAME 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 --dir /tmp/fake/checkins --roster volt,zephyrus --today 2026-10-18
python3 checkin_check.py --selftest # 24 cells, names, dates, roster, refusals
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
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*$")
# 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")
ADVISORY = ("MISSING_HEADINGS", "GAP")
# 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):
seen = set()
for line in text.splitlines():
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
out.append({"handle": h, "files": 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
# --------------------------------------------------------------------- reporting
def banner(text: str) -> str:
return f"== {text} =="
def run(repo: str, path: str, roster, today: date, show_gaps: bool, quiet=False):
"""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]
if not files:
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"]]
rep = {
"dir": path, "checked": len(files), "today": today.isoformat(),
"roster_size": len(roster), "skipped": skipped,
"faults": {c: sum(1 for r in rows if c in r["class"]) 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": sum(1 for r in rows if r["headings_missing"]),
"headings_ok": sum(1 for r in rows
if not r["headings_missing"] and "EMPTY" not in r["class"]),
"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']}")
for n in rep.get("skipped") or []:
print(f" SKIP {n} (the directory's own README: not a check-in)")
fault_rows = [r for r in rep["rows"] if r["class"] or r["duplicate"]]
if not fault_rows:
print("\nno file violates the filename, date, emptiness or duplicate rule")
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 = list(r["class"])
if r["duplicate"]:
cls.append("DUPLICATE")
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 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(f" -- A1: the four-heading rule postdates the archive; grade it where it applies")
if show_gaps:
print(f"\n{'handle':<12} {'files':>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['first']:>10} {g['last']:>10} "
f"{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)
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(repo, 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")))
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
jd = os.path.join(td, "j.json")
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']}")
print(f"selftest: 24 cells, {24 - 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("--json", help="write the full report here")
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.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")
try:
roster, src = load_roster(repo, a.roster)
rep = run(repo, path, roster, today, a.gaps)
except CannotGrade as exc:
print(str(exc), file=sys.stderr)
return 2
rep["roster_source"] = src
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())
+12 -2
View File
@@ -124,12 +124,16 @@ READ_AXIS_UNIT = 2
FATAL_READ = ("TRACEBACK", "RC_NOT_2", "SILENT", "NO_TOOL") FATAL_READ = ("TRACEBACK", "RC_NOT_2", "SILENT", "NO_TOOL")
NO_PROBE = "NO_PROBE" # advisory: a tool with no probe defined NO_PROBE = "NO_PROBE" # advisory: a tool with no probe defined
# {nope} = a path that does not exist; {bin} = a file that is not UTF-8. # {nope} = a path that does not exist; {bin} = a file that is not UTF-8;
# {bindir} = a directory holding one non-UTF-8 *.md (added 2026-10-18 with
# checkin_check.py, whose input is a directory of UTF-8 files -- a {bin} file
# would only have proven it refuses a non-directory).
READ_PROBES = ( READ_PROBES = (
("append_only_audit.py", ["--repo", "{nope}"]), ("append_only_audit.py", ["--repo", "{nope}"]),
("append_only_owners.py", ["{nope}/audit.json"]), ("append_only_owners.py", ["{nope}/audit.json"]),
("census_row_delta.py", ["{nope}/a.json", "{nope}/b.json"]), ("census_row_delta.py", ["{nope}/a.json", "{nope}/b.json"]),
("check_lint.py", ["--log", "{nope}/log.txt"]), ("check_lint.py", ["--log", "{nope}/log.txt"]),
("checkin_check.py", ["--dir", "{nope}", "--roster", "volt"]),
("citation_hash_class.py", ["--repo", "{nope}"]), ("citation_hash_class.py", ["--repo", "{nope}"]),
("citation_resolve_check.py", ["--repo", "{nope}"]), ("citation_resolve_check.py", ["--repo", "{nope}"]),
("doc_census_check.py", ["--repo", "{nope}", "--no-run"]), ("doc_census_check.py", ["--repo", "{nope}", "--no-run"]),
@@ -141,6 +145,7 @@ READ_PROBES = (
NOT_UTF8_PROBES = ( NOT_UTF8_PROBES = (
("append_only_owners.py", ["{bin}"]), ("append_only_owners.py", ["{bin}"]),
("census_row_delta.py", ["{bin}", "{bin}"]), ("census_row_delta.py", ["{bin}", "{bin}"]),
("checkin_check.py", ["--dir", "{bindir}", "--roster", "volt"]),
("queue_claim_check.py", ["--queue", "{bin}"]), ("queue_claim_check.py", ["--queue", "{bin}"]),
("queue_index.py", ["--queue", "{bin}", "--check", "{bin}"]), ("queue_index.py", ["--queue", "{bin}", "--check", "{bin}"]),
) )
@@ -377,8 +382,13 @@ def run_read_probe(path: str, argv, timeout: int):
binpath = os.path.join(td, "not-utf8.bin") binpath = os.path.join(td, "not-utf8.bin")
with open(binpath, "wb") as fh: with open(binpath, "wb") as fh:
fh.write(b"\xff\xfe\x00binary\x00\n") fh.write(b"\xff\xfe\x00binary\x00\n")
bindir = os.path.join(td, "not-utf8-dir")
os.makedirs(bindir, exist_ok=True)
with open(os.path.join(bindir, "2026-01-01-volt.md"), "wb") as fh:
fh.write(b"\xff\xfe\x00binary\x00\n")
nope = os.path.join(td, "there-is-no-such-path") nope = os.path.join(td, "there-is-no-such-path")
real = [a.replace("{nope}", nope).replace("{bin}", binpath) for a in argv] real = [a.replace("{nope}", nope).replace("{bin}", binpath)
.replace("{bindir}", bindir) for a in argv]
try: try:
p = subprocess.run([sys.executable, path] + real, cwd=td, p = subprocess.run([sys.executable, path] + real, cwd=td,
capture_output=True, text=True, errors="replace", capture_output=True, text=True, errors="replace",