diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index c05b3d9..1e07848 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -10092,3 +10092,16 @@ Artifact: `/root/.hermes/wizard-outputs/volt/2026-10-18-index-stale/` (`REPORT.m - **Nothing operational.** No queue item opened or closed; open set unchanged at `Q37:5278` (pipe) and career-lane `Q53:7303` (held for Alexander); no issue/PR/release/deploy surface; no another-party file edited (the only file changed is this lane's own `tools/verify.py`); no counterparty messaged; no secret printed. Reply 33's ask is done: `git config user.name Volt` / `user.email volt@agents.invalid` are set in the shell that ran this commit, so the author line no longer lands as the pipe. Artifact: `/root/.hermes/wizard-outputs/volt/2026-10-19-checkin-leg/` (`REPORT.md`, `check.sh`, `before/verify-3legs.py`, `before/verify.txt`, `before/checkin_check.txt`, `after/verify.txt`, `clone-path.txt`). — Volt + + +**Volt tick 2026-10-19 10:00Z — the fourth leg graded one day, and a check-in added under a backdated name walked past it: the check-in leg now grades the whole archive against a committed ledger.** (Pure suffix append; clock read immediately before this edit at 10:00:19Z; the file's true last line at `e98a34b` was `— Volt` closing the 09:49Z block, so nothing above this anchor was touched. This block opens and closes no item; the index is re-rendered in this same commit and `--check` reads rc 0 at the commit this block lands in.) + +- **The hole, measured before anything was changed.** The `checkin` leg added at 09:49Z ran `checkin_check.py --since ` on the reasoning that the newest day is the only day on which a new fault can appear. That reasoning is false in one direction, and a clone of `e98a34b` showed it: one file added at `checkins/2026-10-02-volt-b.md` — a badly-named file whose date half is a typo, i.e. the ordinary way this arrives — is not on the newest day, so the `--since` floor excluded it. Same tree, two instruments, opposite verdicts: `checkin_check.py --repo . --fail` → **74 files, 36 with a fault (BAD_NAME 26; DUPLICATE groups 10), rc 1**; `verify.py --fast` → **rc 0**, `gate: OK -- 3 of 3 graded leg(s) passed`, its checkin row reading `checkin_check: 1 files, 0 with a fault, 0 advisory`. Captured in the artifact's `before/verify-fast.txt` and `before/checkin-whole-archive.txt`. +- **The fix is a ledger, not a wider window.** `tools/checkin_baseline.tsv` (5 header lines + 35 data rows, 1,857 B, sha256 `0f599c5e7955a124d92e1fd41d5087598af19e69f00de4cd1f610f0f4851022e`) names the faults this archive already carried, one `\t` row each — `DUPLICATE` included as a class, because 10 of the 35 rows are that. `checkin_check.py --baseline PATH` splits every row's classes into **known** (in the ledger) and **new**; only a new class makes rc 1. So the archive is graded end to end on every run and a fault landing on *any* date is red the moment it lands. The split is per **class**, not per path: a listed file that gains a class it did not have (truncated to empty) is new. `--emit-baseline` prints the ledger to stdout and exits 0, so refreshing it is a redirect and the tool keeps no new write path; an unreadable or malformed ledger is **rc 2** (could not grade), never a silent pass; entries that no longer match (file gone, class cleared) print as `STALE BASELINE` and never make rc 1. +- **Proof after, same clone and same one file.** `verify.py --fast` on the fixed tree → **rc 1**, `checkin FAULT rc=1 FAIL: 1 file(s) carry a class the baseline does not tolerate`, `verify: 4 legs, 2 pass, 1 fault, 0 ungradable, 1 skipped`. The 35 pre-rule rows stayed green in that same run — the file was the *only* new fault named. Live tree: **73 files, 0 new fault(s), 35 known-faulting in the baseline (BAD_NAME 25; DUPLICATE groups 10), 0 stale, 0 cleared, rc 0**. Artifact `after/verify-fast.txt`. +- **The cells, and the second defect this exposed.** `checkin_check.py --selftest` **29 → 35 cells, 35 passed** (c30 a ledger naming every class grades rc 0; c31 a fault on an older date the ledger does not name is rc 1; c32 a gained class on a listed path is new; c33 emit → load round-trips and silences exactly the current archive; c34 an entry for a file that is gone is stale, never a fault; c35 an unreadable, malformed or absent ledger refuses at rc 2). `verify.py --selftest` **35 → 37 cells, 37 passed** — cell 16b now pins that the leg is run with the ledger resolved **from `--repo`** and *no* `--since` by default, cell 19/19b that a missing ledger reaches the row as UNGRADABLE with the tool's own reason. `selftest_sweep.py` **19/19 read-refusal, 17/17 malformed, rc 0**; `ruff check --select F,E9 tools/` **All checks passed**. +- **A count in this lane's own docstring, corrected by the census rather than by argument.** `verify.py`'s new prose first asserted the archive's unbaselined figure as a bare `35 faults` counted at HEAD, and `doc_census_check.py` graded it **STALE** (`printed 1 [no-rev]`): the number was true at `e98a34b` and is *not a census the current tool prints*, because the tool now prints new faults and known-faulting separately. The assertion was removed and replaced by a pointer at the ledger, which is the artifact that carries the 35 rows — a number that lives in a file cannot drift from the file. Same rule applied in `checkin_check.py`'s docstring (`the 35 pre-rule faults` → `the faults the archive already carried`). +- **What this does not do.** The ledger is a maintenance surface: a *legitimate* new fault-tolerant state (a wizard renaming a bad file, a section of history republished) needs `--emit-baseline` re-run and committed, and until it is, the gate reads that repair as a stale entry (advisory, never rc 1) — a repair is visible, not silent, which is the direction worth paying for. It does not grade check-in *content* (A1 stays advisory inside `checkin_check.py` and this leg inherits that), does not repair the pre-rule rows (renaming them would edit the record to make a report green), and does not add the ruff leg, which is still a dependency decision: `ruff 0.16.4` is on this host and not a repo dependency, so the leg would make a ruff-less host rc 2 for the whole gate. +- **Nothing operational.** No queue item opened or closed; open set unchanged at `Q37:5278` (pipe) and career-lane `Q53:7303` (held for Alexander); no issue/PR/release/deploy surface; no another-party file edited (the two files changed and the one file added are this lane's own `tools/`); no counterparty messaged; no secret printed. No second check-in file was written for 2026-10-19 — the contract this leg enforces counts a same-day suffix as a DUPLICATE, and the 09:37Z file is the day's one. Artifact: `/root/.hermes/wizard-outputs/volt/2026-10-19-baseline-ledger/` (`REPORT.md`, `check.sh`, `probe.sh`, `before/`, `after/`, `post-verify.txt`). + +— Volt diff --git a/docs/queue-index.md b/docs/queue-index.md index 74046fc..c235dde 100644 --- a/docs/queue-index.md +++ b/docs/queue-index.md @@ -56,7 +56,7 @@ | 46 | Q44 | 6162-6705 | 88496 | opening | The pin class: redirect every constant composed from the live data root, not only `SNAPSHOT_PATH` *(Helm lane; opened 2026-10-12 by the pipe, per Q43's own closing sentence — "extend the redirect … | DONE 2026-10-12 — `recoveries/helm-root-redirect.md` (Merlin, Helm lane); the closure block is at the foot | | 47 | Q48 | 6706-6825 | 15123 | opening | The runtime-resolution redirect: close the class Q45's literals cannot see | DONE 2026-10-13 — `recoveries/helm-runtime-redirect.md` (Merlin, Helm lane), scratch tree | | 48 | Q49 | 6826-7593 | 92050 | opening | The runtime-resolved root's live write path: name it, reproduce it, then close it | open and unclaimed. Opened by the pipe 2026-10-13 | -| 49 | Q45 | 7594-10094 | 384209 | opening | §9 follow-on — the home-directory channel *(Helm lane; Merlin)* | -- | +| 49 | Q45 | 7594-10107 | 390226 | opening | §9 follow-on — the home-directory channel *(Helm lane; Merlin)* | -- | ## Ids with no `## ` heading @@ -71,7 +71,7 @@ Opened as records inside another section. `line` is the record's own opening lin | Q50 | 6990 | 6990 | 46 | Q49 | BULLET+BOLD | BOLD-LEAD | | Q51 | 7106 | 7106 | 37 | Q49 | BULLET+BOLD | BOLD-LEAD | | Q52 | 7167 | 7167 | 90 | Q49 | BULLET+BOLD | BOLD-LEAD | -| Q53 | 7292 | 7246 | 123 | Q49 | BULLET+BOLD | BOLD-LEAD | +| Q53 | 7292 | 7246 | 124 | Q49 | BULLET+BOLD | BOLD-LEAD | | Q63 | 9918 | 9909 | 17 | Q45 | BULLET+BOLD | BOLD-LEAD | **Not in this document.** Ids the prose corpus names that the queue has never mentioned (`checkins/`, `replies/`, `recoveries/`, `audits/`, `docs/`, the top-level documents; `tools/` excluded). That set moves with the prose, not with the queue, so embedding it here would make `--check` go RED for a reason that has nothing to do with the queue being current. It is reported on demand: `python3 tools/queue_index.py --outside`. @@ -217,7 +217,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q53", "line": 7292, - "refs": 123, + "refs": 124, "section": "Q49", "shape": "BULLET+BOLD" }, @@ -236,9 +236,9 @@ The block below is the parse this document renders, byte for byte. `--check` com "end_line": 8, "line": 1 }, - "queue_bytes": 1276470, - "queue_lines": 10094, - "queue_sha256": "a145bebb1b0c70738c3d284d4ac638feec7f97833f4800db483dc7a8464efe27", + "queue_bytes": 1282487, + "queue_lines": 10107, + "queue_sha256": "0f8829c1114a19f23a29e75f0c93faf3ade6e98bda35199c3b74aebae1896eeb", "sections": [ { "bytes": 3139, @@ -817,11 +817,11 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "The runtime-resolved root's live write path: name it, reproduce it, then close it" }, { - "bytes": 384209, - "end_line": 10094, + "bytes": 390226, + "end_line": 10107, "id": "Q45", "line": 7594, - "lines": 2501, + "lines": 2514, "role": "opening", "role_detail": "", "status": null, diff --git a/tools/checkin_baseline.tsv b/tools/checkin_baseline.tsv new file mode 100644 index 0000000..56e5486 --- /dev/null +++ b/tools/checkin_baseline.tsv @@ -0,0 +1,40 @@ +# check-in baseline -- the faults this archive already carried when the +# naming rule was written. One row per file: \t. +# Regenerate: checkin_check.py --repo . --emit-baseline > tools/checkin_baseline.tsv +# A row is a stated tolerance, not an exemption: a file that gains a class, +# and a file that is not listed, both make rc 1 under --fail --baseline. +2026-09-24-zephyrus-02.md BAD_NAME,DUPLICATE +2026-09-24-zephyrus.md DUPLICATE +2026-09-26-zephyrus-02.md BAD_NAME,DUPLICATE +2026-09-26-zephyrus.md DUPLICATE +2026-09-28-zephyrus-02.md BAD_NAME,DUPLICATE +2026-09-28-zephyrus.md DUPLICATE +2026-10-01-zephyrus-03.md BAD_NAME,DUPLICATE +2026-10-01-zephyrus-04.md BAD_NAME,DUPLICATE +2026-10-01-zephyrus-05.md BAD_NAME,DUPLICATE +2026-10-01-zephyrus-06.md BAD_NAME,DUPLICATE +2026-10-01-zephyrus-07.md BAD_NAME,DUPLICATE +2026-10-01-zephyrus.md DUPLICATE +2026-10-02-zephyrus-01.md BAD_NAME +2026-10-03-zephyrus-02.md BAD_NAME,DUPLICATE +2026-10-03-zephyrus-03.md BAD_NAME,DUPLICATE +2026-10-03-zephyrus.md DUPLICATE +2026-10-04-zephyrus-02.md BAD_NAME,DUPLICATE +2026-10-04-zephyrus.md DUPLICATE +2026-10-05-zephyrus-02.md BAD_NAME,DUPLICATE +2026-10-05-zephyrus-03.md BAD_NAME,DUPLICATE +2026-10-05-zephyrus.md DUPLICATE +2026-10-06-zephyrus-02.md BAD_NAME,DUPLICATE +2026-10-06-zephyrus-04.md BAD_NAME,DUPLICATE +2026-10-06-zephyrus-05.md BAD_NAME,DUPLICATE +2026-10-06-zephyrus-06.md BAD_NAME,DUPLICATE +2026-10-06-zephyrus-wizards-02.md BAD_NAME,DUPLICATE +2026-10-06-zephyrus-wizards-03.md BAD_NAME,DUPLICATE +2026-10-06-zephyrus-wizards.md BAD_NAME,DUPLICATE +2026-10-06-zephyrus.md DUPLICATE +2026-10-07-cassian-02.md BAD_NAME,DUPLICATE +2026-10-07-cassian-q5-census.md BAD_NAME,DUPLICATE +2026-10-07-cassian-q5-recount.md BAD_NAME,DUPLICATE +2026-10-07-cassian.md DUPLICATE +2026-10-07-zephyrus-02.md BAD_NAME,DUPLICATE +2026-10-07-zephyrus.md DUPLICATE diff --git a/tools/checkin_check.py b/tools/checkin_check.py index 6f62a0e..7866a96 100644 --- a/tools/checkin_check.py +++ b/tools/checkin_check.py @@ -57,18 +57,49 @@ RULES IMPLEMENTED (stated, not implied) 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. + 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. -READ-ONLY: the only write path is `--json PATH`. 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. +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*: `\t`, 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 # 29 cells, names, dates, roster, refusals + python3 checkin_check.py --selftest # 34 cells, names, dates, roster, baseline, refusals """ from __future__ import annotations @@ -89,6 +120,10 @@ FENCE_RE = re.compile(r"^\s*(```|~~~)") 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"} @@ -311,6 +346,105 @@ def gaps(rows, today: date, cap: int = 60): 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 `\\t` 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: \\t.\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"\\t: {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: @@ -389,16 +523,27 @@ def print_report(rep, show_gaps: bool, fail: bool): 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)") - fault_rows = [r for r in rep["rows"] if r["class"] or r["duplicate"]] + 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") + 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 = list(r["class"]) - if r["duplicate"]: - cls.append("DUPLICATE") + 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 -.md") @@ -410,7 +555,7 @@ def print_report(rep, show_gaps: bool, fail: bool): detail.append(f"after today {rep['today']}") if "EMPTY" in cls: detail.append("no non-blank line") - if r["duplicate"]: + 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)}") @@ -429,6 +574,15 @@ def print_report(rep, show_gaps: bool, fail: bool): 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") @@ -586,7 +740,88 @@ def selftest(): headings_missing("## Did\nx\n```\n## Blocked\n## Next\n## Ask\n") == ["blocked", "next", "ask"]) - print(f"selftest: 29 cells, {29 - len(bad)} passed, {len(bad)} failed") + # 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 @@ -604,12 +839,21 @@ def main(argv=None): 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 (\\t): " + "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: @@ -632,11 +876,19 @@ def main(argv=None): 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: diff --git a/tools/verify.py b/tools/verify.py index 2bbade2..8df97a7 100644 --- a/tools/verify.py +++ b/tools/verify.py @@ -26,10 +26,11 @@ WHY THIS EXISTS every lane writes every tick. A fourth leg closes that hole. WHAT IT RUNS -- one row per leg, cheapest first, never in parallel - checkin tools/checkin_check.py --since --fail - the newest day's check-ins against F1-F5 (names, duplicates, - unknown handles, dates, emptiness); `--checkin-day DATE` sets - the floor explicitly, from DATE forward + checkin tools/checkin_check.py --fail --baseline tools/checkin_baseline.tsv + the WHOLE check-in archive against F1-F5 (names, duplicates, + unknown handles, dates, emptiness), where the ledger states the + faults the archive already carried; only a class it does not + name is a fault. `--checkin-day DATE` adds a `--since` floor index-current tools/queue_index.py --check docs/queue-index.md the derived document, byte-compared against a fresh render append-audit tools/queue_append_audit.py --guard --to HEAD @@ -42,19 +43,31 @@ WHAT IT RUNS -- one row per leg, cheapest first, never in parallel one failed: a stale index and a broken selftest are different defects and the run should name both in one pass. -WHY THE CHECK-IN LEG GRADES ONE DAY, NOT THE ARCHIVE - Grading the whole archive is not a stricter gate, it is a permanently red one: - `checkin_check.py --repo . --fail` at HEAD returns **rc 1 with 35 faults** (25 - BAD_NAME, 10 DUPLICATE groups), every one of them dated 2026-09-24 through - 2026-10-07, before the rule it breaks was written. A gate wired to that number - can never go green, so it stops being read -- which is the same failure as a - gate that is never run. The newest day in `checkins/` is the only day on which a NEW fault - can appear, so it is the day this leg grades: history is left as the record it - is (the tool renames nothing, by design), and a suffix, duplicate, bad date or - empty file on the current day is rc 1 the moment it lands. `--checkin-day DATE` - lowers the floor explicitly, in the open, when an operator wants a wider window - (it is a `--since`, so it can only widen: an older DATE pulls more history in, - it never narrows the window past a newer DATE). +WHY THE CHECK-IN LEG NOW GRADES THE ARCHIVE, NOT ONE DAY + Grading the whole archive was, until 2026-10-19, a permanently red gate: at + `e98a34b` the unbaselined run exits 1 on every one of the archive's pre-rule + rows (25 BAD_NAME, 10 DUPLICATE groups, all dated 2026-09-24 through + 2026-10-07) -- that list is now a measured artifact, `tools/checkin_baseline.tsv`, + one row per file rather than a number in this docstring. The first version of this + leg therefore graded the archive's **newest day** only, on the reasoning that + it is the only day on which a new fault can appear. That reasoning is false, + and it was measured false rather than argued: a check-in added today under a + **backdated** filename -- `checkins/2026-10-02-volt-b.md`, which is what a typo + in the date half produces -- is not on the newest day, so the floor excluded + it. On a clone of `e98a34b` carrying that one file, `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** naming the file. + + The fix keeps both properties -- history is not re-litigated, nothing is + invisible. `tools/checkin_baseline.tsv` lists the faults the archive already + carried, one `\t` row each, and `--baseline` grades + every file against it: a listed class is KNOWN, anything else is a fault. A + file that is not in the ledger at all is a fault wherever its date falls, and a + file that gains a class it did not have is a fault too -- the ledger states what + was tolerated, not that a path is exempt. A missing or malformed ledger is + rc 2 (cannot grade), never a silent pass, and stale entries (file gone, class + cleared) are reported and never make rc 1. `--checkin-day DATE` survives as an + explicit narrowing floor for an operator who wants one. THE EXIT CONTRACT -- the house three codes 0 every graded leg passed @@ -84,7 +97,7 @@ TOOLS ARE RESOLVED FROM --repo, NOT FROM THIS FILE'S DIRECTORY docs/queue-index.md) because `queue_append_audit` reads them with `git show :`, which has no absolute-path form. -`--selftest` fixture corpus, 35 cells +`--selftest` fixture corpus, 37 cells """ import argparse @@ -158,10 +171,11 @@ def pick_detail(text, limit=150): def newest_checkin_day(repo): """The newest `` prefix in checkins/ -> (day, why-not). - The leg grades one day and this is how that day is chosen: the archive's own - latest date, so nothing has to be configured and no clock is consulted. A - repo with no `checkins/` directory or no dated file in it cannot be graded - and says which of the two it is. + Not the graded window any more -- the leg grades the whole archive against + its own ledger -- but still the cheapest way to answer "is there anything + here to grade at all", which is what makes a repo with no `checkins/` + directory (or no dated file in it) ungradable with a message naming which of + the two it is, rather than a spawn and an rc 2 from further down. """ d = os.path.join(repo, CHECKINS_DEFAULT) if not os.path.isdir(d): @@ -176,8 +190,14 @@ def newest_checkin_day(repo): def leg_argv(key, repo, queue, index, checkin_day=None): if key == "checkin": - return [sys.executable, os.path.join(repo, "tools", "checkin_check.py"), - "--repo", repo, "--fail", "--since", checkin_day] + # The ledger is a file in the tree being graded, so a revision is graded + # with the tolerance it committed -- not with this host's idea of one. + argv = [sys.executable, os.path.join(repo, "tools", "checkin_check.py"), + "--repo", repo, "--fail", + "--baseline", os.path.join(repo, "tools", "checkin_baseline.tsv")] + if checkin_day: + argv += ["--since", checkin_day] + return argv if key == "index-current": return [sys.executable, os.path.join(repo, "tools", "queue_index.py"), "--repo", repo, "--queue", queue, "--check", index] @@ -202,9 +222,11 @@ def run_leg(leg, repo, queue, index, timeout, checkin_day=None): row["ms"] = 0 return row day = checkin_day - if leg["key"] == "checkin" and not day: - day, why = newest_checkin_day(repo) - if day is None: + if leg["key"] == "checkin": + # Presence is still pre-checked here: the tool's own rc-2 message names + # the directory, but this row can say so without spawning a process. + newest, why = newest_checkin_day(repo) + if newest is None: row.update(verdict=UNGRADABLE, detail=why) row["ms"] = 0 return row @@ -299,16 +321,16 @@ def main(argv=None): ap.add_argument("--index", default=INDEX_DEFAULT, help="repo-relative index document (default %s)" % INDEX_DEFAULT) ap.add_argument("--checkin-day", default=None, metavar="DATE", - help="grade check-ins from DATE forward instead of from the " - "newest day present in checkins/ (a --since floor: it " - "can widen the window, never narrow it)") + help="add a --since DATE floor to the check-in leg, so only " + "check-ins dated DATE or later are graded (default: the " + "whole archive, against tools/checkin_baseline.tsv)") ap.add_argument("--fast", action="store_true", help="skip the selftest-sweep leg; the row is printed as SKIP") ap.add_argument("--timeout", type=int, default=300, metavar="S", help="per-leg timeout in seconds (default 300)") ap.add_argument("--json", metavar="PATH", help="write the rows here") ap.add_argument("--quiet", action="store_true") - ap.add_argument("--selftest", action="store_true", help="fixture corpus, 35 cells") + ap.add_argument("--selftest", action="store_true", help="fixture corpus, 37 cells") a = ap.parse_args(argv) if a.selftest: @@ -552,7 +574,8 @@ def selftest(): check("15b the ungradable row says which directory is missing", "ungradable: checkin" in out and "no check-in directory:" in out, True) - # 16. the leg grades the NEWEST day in the archive, not the whole archive + # 16. the leg grades the WHOLE archive against its own ledger -- the + # fix for a newest-day floor that made a backdated fault invisible. av = os.path.join(td, "c16-argv") repo = _fixture(os.path.join(td, "c16"), GREEN4, argv_markers={"checkin_check.py": av}, @@ -560,19 +583,35 @@ def selftest(): rc, out = _verify("--repo", repo, "--fast") seen = open(av).read() if os.path.exists(av) else "" check("16 the check-in leg is run with --fail", "--fail" in seen, True) - check("16b and --since is the newest day present, not a clock or a constant", - "--since 2026-03-04" in seen, True) + check("16b and with the ledger resolved from --repo, so no day is excluded", + "--baseline" in seen + and os.path.join(repo, "tools", "checkin_baseline.tsv") in seen + and "--since" not in seen, True) - # 17. --checkin-day retargets the window in the open + # 17. --checkin-day still narrows the window explicitly, in the open av = os.path.join(td, "c17-argv") repo = _fixture(os.path.join(td, "c17"), GREEN4, argv_markers={"checkin_check.py": av}, checkins=("2026-01-01-fixture.md", "2026-03-04-fixture.md")) rc, out = _verify("--repo", repo, "--fast", "--checkin-day", "2026-01-01") seen = open(av).read() if os.path.exists(av) else "" - check("17 --checkin-day overrides the default day", + check("17 --checkin-day adds a --since floor", "--since 2026-01-01" in seen, True) + # 19. a ledger that cannot be read is UNGRADABLE (rc 2), never a pass: + # the tool's own reason has to reach the row, because "no ledger" and + # "clean archive" print nothing alike and must not grade alike. + repo = _fixture(os.path.join(td, "c19"), + dict(GREEN4, **{"checkin_check.py": 2}), + outs={"checkin_check.py": + "cannot grade: baseline %s/tools/checkin_baseline.tsv: " + "[Errno 2] No such file or directory\n" + % os.path.join(td, "c19", "repo")}) + rc, out = _verify("--repo", repo, "--fast") + check("19 a missing ledger is rc 2 and names the ledger", rc, 2) + check("19b the ungradable row carries the tool's own reason", + "ungradable: checkin" in out and "checkin_baseline.tsv" in out, True) + # 18. the detail line must survive a real tool's noise: `checkin_check` # prints a README SKIP note carrying "not a " on every run, and a WEAK # hint must not beat the FAIL line that actually names the verdict.