checkin 2026-10-17 (volt): six tools -- unreadable input no longer reads as clean

Five tracebacked on input they could not read (rc 1, the code this corpus uses for
"a finding"); two -- citation_hash_class.py, citation_resolve_check.py -- printed a
clean zero census over a missing repo or bogus --rev and exited 0, because git() in
both discards the return code. Those two are the gates a reviewer runs to decide
whether a citation defect exists, so a green that never looked is the hazard.

queue_index.py / queue_claim_check.py: read errors -> rc 3 (the claim tool's docstring
already promised `3 queue unreadable` while its code raised UnicodeDecodeError).
citation_hash_class.py / citation_resolve_check.py: precondition(repo, rev) -> rc 2,
matching doc_census_check.py and landing_check.py. check_lint.py / census_row_delta.py:
named message, no traceback, rc 1 kept (filed: usage error and graded fault still
share that code).

175 insertions(+), 14 deletions(-). Selftests 54/54, 16/16, 37/37, 33/33, 19/19, 11/11;
sweep 11 tools/10 OK/0 fatal; --check rc 0 at 9 nested ids; 9 broken-input probes ->
0 tracebacks; copy-vs-copy A/B 7 SAME / 2 DIFFERS (both intended added cells).
run.md's earlier "7 SAME, 0 DIFFERS" claim was wrong and is corrected in the artifact.
This commit is contained in:
timmy committed 2026-10-17 19:58:45 +00:00
1 parent 676be46ef8
commit 016d26eae7
7 files changed
+231 -9

No files matched your search

+61
View File
@@ -466,3 +466,64 @@ did `outside, corpus = outside_ids(...)` → `TypeError: cannot unpack non-itera
new cells hold it: `--index outside a git work tree exits 0` and `… still summarizes` →
**selftest 46 cells, 46 passed, 0 failed**, sweep `11 tools, 10 OK, 0 fatal`, `--check` still
rc 0 at 9 nested ids. This is a second commit on top of `e79cbca`, not a rewrite of it.
## Tick 4 — unreadable input must not read as "clean" (six tools, one class)
Picked up an unfinished item: six tracked verifier tools mishandled input they could not
read, in two directions. Five crashed with a Python traceback under an exit code that this
corpus uses for "a finding"; two reported a **clean census over an input they never opened**
— `citation_hash_class.py --repo /nope` printed `corpus: 0 files`, `flags: 0 rows` and
exited **0**, and `citation_resolve_check.py` did the same for `citations: 0 / stale: 0`.
The second direction is the dangerous one: a green gate that never looked is
indistinguishable from a green gate that looked and found nothing, and these are the two
tools a reviewer runs to decide whether a citation defect exists. Both were found by probe,
not by reading — every one of the six was already green on its own selftest.
Cause, measured: `git()` in both scanners returns `subprocess.run(...).stdout` and discards
the return code, so a missing repo or a mistyped `--rev` yields an empty tree, an empty
corpus and a zero count. `queue_claim_check.py`'s docstring already promised
`3 queue unreadable` while its code raised `UnicodeDecodeError` on non-UTF-8 — doc/code
mismatch closed rather than widened.
**Changed** — six files, `175 insertions(+), 14 deletions(-)`:
tools/queue_index.py 66/6 read errors -> rc 3; whole-input guard on --check
tools/queue_claim_check.py 45/5 UnicodeDecodeError -> rc 3, + 2 read-error cells
tools/citation_hash_class.py 24/0 precondition(repo, rev) -> rc 2
tools/citation_resolve_check.py 21/0 same guard, same code
tools/check_lint.py 8/2 unreadable --log -> message, no traceback
tools/census_row_delta.py 11/1 non-census JSON named, no traceback
Exit codes follow the tools that already had a convention: 3 = queue unreadable (claim_check
docstring), 2 = precondition failed / did not measure (`doc_census_check.py`,
`landing_check.py`). No `except Exception` — every guard names `OSError`,
`UnicodeDecodeError` or `ValueError`, the classes actually observed.
**Verified, this host, this work tree:**
- selftests: `queue_index` 54/54, `queue_claim_check` 16/16 (10 claim-check, 4 unit,
2 read-error), `check_lint` 37/37, `census_row_delta` 33/33, `citation_hash_class` 19/19,
`citation_resolve_check` 11/11 — 0 failed everywhere.
- `tools/selftest_sweep.py` → `sweep: 11 tools, 10 OK, 0 fatal`; the admitted `--selftest`
claims now match what the tools print (the one non-OK, `doc_census_check.py NO_CLAIM`, is
advisory and pre-existing).
- `queue_index.py --check docs/queue-index.md` → rc 0, `49 sections, 45 ids,
4 continuation sections, 9 nested ids, 9937 lines / 1224324 bytes`.
- nine broken-input probes → **0 tracebacks**. `queue_index --queue <missing>` rc 3,
`--queue <non-UTF-8>` rc 3, `queue_claim_check --queue <non-UTF-8>` rc 3,
both citation scanners on a missing repo **rc 2** (and rc 2 on `--rev deadbeef`),
`check_lint --log <missing>` and `census_row_delta <non-census>` message-then-rc 1.
- no-behaviour-change A/B, HEAD copy vs working copy on one work tree, nine invocations:
**7 SAME, 2 DIFFERS**, and both DIFFERS are the intended added cells (54 vs 46, 16 vs 14).
**A correction I owe, against my own draft.** My run note first claimed "7 SAME, 0 DIFFERS
over three selftests". That is false — a selftest that gains cells cannot print the same
bytes — and it contradicted its own table. Re-measured copy-vs-copy it is 7 SAME / 2
DIFFERS. The trap is recorded in the artifact: the citation scanners census *this clone's
local object store*, so an A/B done with `git stash` adds stash objects to the very store
being counted; copy-vs-copy is the valid method. Filed, not fixed: `check_lint.py` and
`census_row_delta.py` still use rc 1 for both "a graded fault" and "I could not read your
file" — the message disambiguates, the code does not, and renumbering is a lane decision.
Artifact: `/root/.hermes/wizard-outputs/volt/2026-10-17-input-error-class/run.md`.
+11 -1
View File
@@ -76,8 +76,18 @@ def load(path):
`rows` is every surviving verdict dict, each carrying its tool name; keying
happens in `keyed()` so one load serves every key shape.
An unreadable or non-census file is a usage error, not a traceback: this
used to raise the json decoder's error or a bare `KeyError: 'reports'`, with
rc 1. The message names the file and what was expected instead.
"""
d = json.load(open(path, encoding="utf-8"))
try:
with open(path, encoding="utf-8") as fh:
d = json.load(fh)
except (OSError, ValueError) as exc:
raise SystemExit("cannot read census %s: %s" % (path, exc))
if not isinstance(d, dict) or not isinstance(d.get("reports"), list):
raise SystemExit("not a doc_census_check census (no 'reports' list): %s" % path)
rows, fails, per = [], [], {}
for rep in d["reports"]:
for v in rep["verdicts"]:
+6
View File
@@ -511,8 +511,14 @@ def main(argv=None) -> int:
return 1 if (a.fail_on_fault and primary != "SOUND") else 0
if a.log:
try:
with open(a.log, encoding="utf-8", errors="replace", newline="") as fh:
text = fh.read()
except OSError as exc:
# A log that is not there is a usage error, not a grading failure:
# it used to escape as a FileNotFoundError traceback with rc 1,
# which is the code this tool uses for "the check has a fault".
raise SystemExit("cannot read log: %s" % exc)
cmd = extract_check(text)
primary, faults, detail = classify(cmd)
if not a.quiet:
+24
View File
@@ -117,6 +117,21 @@ def git(repo, *a):
return subprocess.run(["git", "-C", repo, *a], capture_output=True).stdout
def precondition(repo, rev):
"""None when `repo` is a git work tree that carries `rev`, else why not.
The caller turns this into exit 2. It exists because every reader below
swallows git's exit status: an unreadable repo or a mistyped revision came
back as an empty tree, and the census printed zeros over it with rc 0.
"""
p = subprocess.run(["git", "-C", repo, "rev-parse", "--verify",
rev + "^{commit}"], capture_output=True)
if p.returncode == 0:
return None
why = p.stderr.decode("utf-8", "replace").strip().splitlines()
return ("cannot read %s at %s: %s" % (repo, rev, why[-1] if why else "git rev-parse failed"))
def build_index(repo):
"""One pass over the whole object store: blob oid -> (content sha256, size)."""
meta, idx = {}, collections.defaultdict(list)
@@ -351,6 +366,15 @@ def main():
if a.selftest:
return selftest(a.repo)
# A repo or revision git cannot read is a precondition failure, never a
# clean census. `git()` returns stdout only, so `--repo /nonexistent` used to
# print "corpus: 0 files ... 0 flags" and exit 0 -- a silent green on the one
# input this tool cannot see. Exit 2 says "did not measure", which is true.
problem = precondition(a.repo, a.rev)
if problem:
print(problem, file=sys.stderr)
return 2
corpus = a.corpus or [p for p in git(a.repo, "ls-tree", "-r", "--name-only", a.rev)
.decode('utf-8', 'replace').split('\n') if p]
IDX = build_index(a.repo)
+21
View File
@@ -144,6 +144,22 @@ def git(repo, *a):
return subprocess.run(["git", "-C", repo, *a], capture_output=True).stdout
def precondition(repo, rev):
"""None when `repo` is a git work tree that carries `rev`, else why not.
Every reader below swallows git's exit status, so an unreadable repo or a
mistyped revision came back as an empty tree and the census printed
"citations: 0 / stale: 0" with rc 0 -- a green gate over an input it never
saw. The caller turns this into exit 2.
"""
p = subprocess.run(["git", "-C", repo, "rev-parse", "--verify", rev + "^{commit}"],
capture_output=True)
if p.returncode == 0:
return None
why = p.stderr.decode("utf-8", "replace").strip().splitlines()
return "cannot read %s at %s: %s" % (repo, rev, why[-1] if why else "git rev-parse failed")
class Store:
"""File text and line counts at one revision, read once per path."""
@@ -348,6 +364,11 @@ def main():
if a.selftest:
return selftest(a.repo)
problem = precondition(a.repo, a.rev)
if problem:
print(problem, file=sys.stderr)
return 2
store = Store(a.repo, a.rev)
corpus = a.corpus or sorted(p for p in store.tree if p.endswith(".md"))
rows, stats = scan(a.repo, a.rev, corpus, store)
+45 -5
View File
@@ -104,7 +104,7 @@ USAGE
python3 tools/queue_claim_check.py --queue PATH --json OUT.json
python3 tools/queue_claim_check.py --validate # + the hand-read answers
python3 tools/queue_claim_check.py --git # resolve closure revisions
python3 tools/queue_claim_check.py --selftest # 14 fixtures, exit 0/1
python3 tools/queue_claim_check.py --selftest # 16 fixtures, exit 0/1
python3 tools/queue_claim_check.py --census # both closure units + the axis table
Exit codes: 0 clean · 1 self-test failure · 2 stale opening line (--fail-on-stale)
@@ -112,11 +112,14 @@ Exit codes: 0 clean · 1 self-test failure · 2 stale opening line (--fail-on-st
"""
import argparse
import contextlib
import io
import json
import os
import re
import subprocess
import sys
import tempfile
# --------------------------------------------------------------------------
# matching primitives
@@ -524,10 +527,43 @@ def selftest(verbose=True):
if not ok:
failures.append(name)
# A read failure is rc 3, not a traceback. `--queue` at a missing path or at
# bytes that are not UTF-8 escaped main() as FileNotFoundError /
# UnicodeDecodeError; the first was already caught, the second was not, and
# rc 1 is what this corpus uses for "a check failed". Two cells, because the
# exit code is the whole contract an operator sees.
n_read = 0
with tempfile.TemporaryDirectory() as td:
bad = os.path.join(td, 'not-utf8.md')
with open(bad, 'wb') as fh:
fh.write(b'\xff\xfe\x80\n')
for name, path, want in (
('read cell 1: a missing queue path exits 3, not a traceback',
os.path.join(td, 'nope.md'), 'No such file'),
('read cell 2: a queue that is not UTF-8 exits 3, not a traceback',
bad, 'utf-8')):
n_read += 1
err = io.StringIO()
with contextlib.redirect_stderr(err):
rc = main(['--queue', path])
msg = err.getvalue()
ok = rc == 3 and 'cannot read queue:' in msg
if ok:
ok = want.lower() in msg.lower()
if verbose:
total = len(FIXTURES) + n_unit
print('\nself-test: %d fixtures (%d claim-check, %d unit), %d passed, %d failed'
% (total, len(FIXTURES), n_unit, total - len(failures), len(failures)))
print('%s %s' % ('PASS' if ok else 'FAIL', name))
if not ok:
print(' expected rc 3 and `cannot read queue: ...%s...`\n'
' got rc %d, stderr %r' % (want, rc, msg.strip()[:160]))
if not ok:
failures.append(name)
if verbose:
total = len(FIXTURES) + n_unit + n_read
print('\nself-test: %d fixtures (%d claim-check, %d unit, %d read-error), '
'%d passed, %d failed'
% (total, len(FIXTURES), n_unit, n_read,
total - len(failures), len(failures)))
return failures
@@ -558,7 +594,11 @@ def main(argv=None):
path = args.queue or os.path.join(root, 'WORK-QUEUE.md')
try:
text = open(path, encoding='utf-8').read()
except OSError as exc:
except (OSError, UnicodeDecodeError) as exc:
# UnicodeDecodeError is not an OSError: a binary or mis-encoded file at
# --queue used to escape main() as a traceback with rc 1, which is the
# code the fixture corpus uses for "a check failed". A read failure is
# rc 3 with the same message, whatever the reason.
print('cannot read queue: %s' % exc, file=sys.stderr)
return 3
+63 -3
View File
@@ -78,7 +78,12 @@ WHAT IT DOES NOT DO
about the queue's currency and not about the last person to write a checkin.
`--check` exit codes: 0 current, 1 stale (with the delta printed), 2 the
document carries no parseable pinned block.
document carries no parseable pinned block, 3 the queue or the document could
not be read at all (missing path, or bytes that are not UTF-8). An unreadable
input is a precondition failure, never a stale index and never a traceback:
`--queue` at a missing path or at a binary used to raise FileNotFoundError /
UnicodeDecodeError out of `run()` with rc 1, which reads like "stale" to every
caller that only checks the exit code.
READ-ONLY except `--json PATH` and `--write PATH`.
@@ -88,7 +93,7 @@ USAGE
python3 tools/queue_index.py --json out.json
python3 tools/queue_index.py --write docs/queue-index.md
python3 tools/queue_index.py --check docs/queue-index.md
python3 tools/queue_index.py --selftest # fixture corpus, 46 cells
python3 tools/queue_index.py --selftest # fixture corpus, 54 cells
Re-run `--write` after any append to the queue. The document carries the
queue's sha256, so a stale copy is a RED check, never a silent lie.
@@ -480,12 +485,27 @@ def delta(pinned, fresh):
def run(args):
repo = args.repo
queue_path = os.path.join(repo, args.queue)
# A read failure is an exit code, not a traceback. `--queue` pointed at a
# missing path or at bytes that are not UTF-8 raised FileNotFoundError /
# UnicodeDecodeError straight out of run(), so the operator got a stack and
# rc 1 while every other reader in this repo (`queue_claim_check.py:558-563`)
# says `cannot read queue:` and exits 3. The same contract is owed to the
# --check document: a gate that cannot read its input has verified nothing,
# and must not report that as a stale index (rc 1) or a crash.
try:
with open(queue_path, encoding="utf-8") as fh:
text = fh.read()
except (OSError, UnicodeDecodeError) as exc:
print("cannot read queue: %s" % exc, file=sys.stderr)
return 3
payload = parse(text)
if args.check:
try:
doc = open(args.check, encoding="utf-8").read()
except (OSError, UnicodeDecodeError) as exc:
print("cannot read index document: %s" % exc, file=sys.stderr)
return 3
pinned = extract_pinned(doc)
if pinned is None:
print("UNVERIFIABLE: no parseable pinned block in %s" % args.check)
@@ -749,6 +769,46 @@ def selftest():
check("no pin -> exit 2", rc, 2)
check("no pin -> says so", "UNVERIFIABLE" in buf.getvalue(), True)
# --- a read error is an exit code, not a traceback -----------------------
# `--queue` at a missing path or at bytes that are not UTF-8 used to raise
# straight out of run() (FileNotFoundError / UnicodeDecodeError, rc 1). The
# repo's other reader exits 3 with `cannot read queue:`; this tool now does
# the same, and owes the --check document the same answer: an unreadable
# gate input is rc 3, never the rc 1 that means "stale".
bad_q = os.path.join(root, "bad-utf8.md")
with open(bad_q, "wb") as fh:
fh.write(b"\xff\xfe\x80\n")
err = io.StringIO()
with contextlib.redirect_stderr(err):
rc = run(argparse.Namespace(repo=root, queue="missing-queue.md", index=False,
json=None, write=None, check=None, outside=False))
check("missing queue -> exit 3", rc, 3)
check("missing queue -> says cannot read queue",
err.getvalue().startswith("cannot read queue:"), True)
err = io.StringIO()
with contextlib.redirect_stderr(err):
rc = run(argparse.Namespace(repo=root, queue="bad-utf8.md", index=False,
json=None, write=None, check=None, outside=False))
check("non-UTF-8 queue -> exit 3", rc, 3)
check("non-UTF-8 queue -> says cannot read queue",
"cannot read queue:" in err.getvalue(), True)
err = io.StringIO()
with contextlib.redirect_stderr(err):
rc = run(argparse.Namespace(repo=repo, queue="WORK-QUEUE.md", index=False,
json=None, write=None,
check=os.path.join(repo, "missing-index.md"),
outside=False))
check("missing index document -> exit 3", rc, 3)
check("missing index document -> names the document",
"cannot read index document:" in err.getvalue(), True)
err = io.StringIO()
with contextlib.redirect_stderr(err):
rc = run(argparse.Namespace(repo=repo, queue="WORK-QUEUE.md", index=False,
json=None, write=None, check=bad_q, outside=False))
check("non-UTF-8 index document -> exit 3", rc, 3)
check("non-UTF-8 index document -> names the document",
"cannot read index document:" in err.getvalue(), True)
print("selftest: %d cells, %d passed, %d failed" % (cells[0], cells[0] - bad[0], bad[0]))
return 1 if bad[0] else 0
@@ -765,7 +825,7 @@ def main(argv=None):
ap.add_argument("--check", metavar="PATH", help="re-derive and compare a rendered index")
ap.add_argument("--outside", action="store_true",
help="report ids the prose corpus names that the queue never mentions")
ap.add_argument("--selftest", action="store_true", help="fixture corpus, 46 cells")
ap.add_argument("--selftest", action="store_true", help="fixture corpus, 54 cells")
args = ap.parse_args(argv)
if args.selftest:
return selftest()