tools (volt): the check-in leg grades the whole archive against a ledger, not one day -- a backdated fault was rc 0 before, rc 1 now
The 09:49Z leg ran checkin_check --since <newest day>, on the reasoning that the
newest day is the only day a new fault can appear. Measured false on a clone of
e98a34b: one added checkins/2026-10-02-volt-b.md (BAD_NAME) is invisible to that
floor -- verify --fast rc 0 'checkin_check: 1 files, 0 with a fault', while the
whole-archive run on the same tree is rc 1 with 74 files / 36 faults.
tools/checkin_baseline.tsv (35 data rows, sha256 0f599c5e...) is the faults the
archive already carried; checkin_check --baseline splits each row's classes into
known/new, only a new class makes rc 1, per class not per path; --emit-baseline
prints it to stdout (no new write path); an unreadable ledger is rc 2; stale
entries are advisory. Refreshed leg: same clone, same file -> rc 1 naming exactly
that file; live tree -> 73 files, 0 new, 35 known, rc 0.
Cells: checkin_check 29 -> 35 (35/35), verify 35 -> 37 (37/37), sweep 19/19 +
17/17 rc 0, ruff F,E9 clean. One docstring count removed after
doc_census_check graded it STALE -- the ledger is now the artifact that carries it.
This commit is contained in:
1 parent
e98a34b0c1
commit
92f8934e15
5 files changed
+402
-58
No files matched your search
@@ -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 <newest day present>` 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 `<filename>\t<CLASS[,CLASS]>` 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
|
||||
+9
-9
@@ -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,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# check-in baseline -- the faults this archive already carried when the
|
||||
# naming rule was written. One row per file: <filename>\t<CLASS[,CLASS]>.
|
||||
# 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
|
||||
|
+265
-13
@@ -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*: `<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 # 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 `<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:
|
||||
@@ -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 <date>-<handle>.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 (<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:
|
||||
@@ -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:
|
||||
|
||||
+75
-36
@@ -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 <newest day> --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 `<filename>\t<CLASS[,CLASS]>` 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 <rev>:<path>`, 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 `<YYYY-MM-DD>` 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.
|
||||
|
||||
Reference in new issue
Block a user