tools (volt): doc_census_check -- the gate, and a hyphenated unit is not a unit

`--fail` moves the verdict into the exit code: rc 0 ran and graded no fault,
rc 1 ran and graded a STALE claim or an INTERNAL SPLIT, rc 2 could not grade
(usage, a --tools filter matching nothing tracked, unreadable input). Before
this, the process exited 0 while printing STALE rows, so nothing could gate on
the one class the tool exists to catch.

Armed at HEAD, the gate went red on its own first run -- on a false claim, not
a defect: `queue_claim_check.py`'s axis list says `39 ACS-only lines carry no
Q<digits> token` (and 12, and 1) and the token read as a unit was the
adjective `ACS` in `ACS-only`; with no oracle to test the label against,
--no-run split 39/12/1. The reader now refuses a unit immediately followed by
`-`, the same refusal it already made for a hyphen behind the number
(`120-char`).

Measured cost of the tightening at bacdead, from --json before/after: 6 claims
no longer read -- queue_claim_check 23 -> 19, checkin_check 8 -> 7,
queue_index 10 -> 9 -- 5 NOT_PRINTED and 1 REV_UNCHECKED, every one of them the
first half of a compound (`ACS-only`, `check-in`, `self-test`, `follow-on`).
The invariant that licenses it holds: MATCHES 26 before and after, identical
rows. A genuinely hyphenated unit (`citations-per-block`) is no longer read;
the tool states that limit in its docstring rather than discovering it.

selftest 41 -> 52 cells (5 decision cells, 4 process-exit cells that prove a
helper returning 1 is not a gate); sweep 12/12 OK, read-refusal 17/17,
malformed 15/15; `--fail` at HEAD: 0 INTERNAL SPLIT, 0 STALE, rc 0; the same
gate on fca3f2e: 1 split, 1 STALE, rc 1.
This commit is contained in:
volt committed 2026-10-18 07:35:53 +00:00
1 parent bacdead9c3
commit 26056d2a21
1 file changed
+147 -3
+147 -3
View File
@@ -122,9 +122,44 @@ SCOPE, STATED RATHER THAN IMPLIED
measure the file it introduces -- and here it is avoided rather than
discovered.
FAILING ON THE CLASS IT CATCHES (`--fail`)
Without it the process exits 0 while printing STALE rows, so no gate could
act on the one class this tool exists to catch without parsing its text.
`--fail` makes the exit code carry the verdict. The cost of the old silence
is measured twice, and the two are NOT the same measurement:
- STRUCTURAL -- reproducible right here, no archive needed. A stub tree
whose docstring says `99 widgets` beside a printed `widgets: 7` exits 0
without `--fail` and 1 with it (`gate e2e` cells in `--selftest`).
- HISTORICAL -- true only of the pre-tightening separator (`ac0e211^`).
That build at rev 4ce1aed, `--tools tools/checkin_check.py`, exits 0
with two STALE verdicts on `checkin_check.py:59`: `Never claimed 8` and
`never claimed 1`, both `printed 2 [selftest]` -- the digits in "Never 1,
never a traceback". Reproduced 2026-10-18 from this host. The tightened
separator stops reading those digits as a census: the same rev now
adjudicates 7 claims, 0 STALE. So the historical run is cited with its
build and never as current behaviour -- a claim that says "rc 0 with two
STALE" without naming the build goes stale the moment the reader is
tightened, which is the failure this tool grades other files for.
The code means:
0 ran, and graded no fault (every claim adjudicated agrees)
1 ran, and graded a fault (an INTERNAL SPLIT or a STALE claim)
2 could not grade (usage, a --tools filter that matches
nothing tracked, unreadable input)
rc 2 keeps its meaning from the tools/ contract (6ee8fce) and is decided
before the gate runs, so "could not grade" never reads as "graded clean" or
as "graded a fault". A LEG FAILURE (RUN_FAILED) is reported in its own column
and does not fail the gate, for the reason counts() states: a leg that
produced no census adjudicated no claim. A gate that counted it would fail
runs it never checked.
USAGE
python3 doc_census_check.py [--repo DIR] [--rev HEAD] [--tools NAME...]
[--json OUT] [--no-run] [--selftest] # selftest: 41 cells, one per claim class
[--json OUT] [--no-run] [--fail] [--selftest] # selftest: 52 cells, one per claim class
"""
import argparse
import ast
@@ -157,9 +192,22 @@ DEFAULT_REPO = "/root/troupe-standup"
# this archive grades lanes for. A hex run is welded to its letters and has no
# separator; an ordinal's separator is a period; a compound adjective's is a
# hyphen. None of the three is `<value> <unit>`.
#
# A HYPHEN AFTER THE UNIT IS THE SAME REFUSAL, made 2026-10-18 after the
# --fail gate went red at HEAD on a false claim: `queue_claim_check.py`'s axis
# list says `39 ACS-only lines carry no Q<digits> token`, `12 ACS-only lines
# are indented`, `1 ACS-only line carries a BQ exclusion phrase` -- three
# sub-counts of ONE population of 52, and the token read as a unit was the
# adjective `ACS` in `ACS-only`. With no oracle to test the label against, a
# `--no-run` split fired on 39/12/1. A hyphen immediately behind the unit word
# marks a compound adjective, not a unit noun, exactly as a hyphen behind the
# number does (`120-char`). The cost is stated: a genuinely hyphenated unit
# (`citations-per-block`) is no longer read, and the invariant that licenses
# this tightening is that the 26 MATCHES do not move -- measured before and
# after, same 26 rows.
CLAIM = re.compile(
r"(?<![0-9A-Za-z_.])(?P<num>[0-9]+(?:,[0-9]{3})*)(?P<sep>\s{1,2}|[)\]}\"'`*]\s)"
r"(?P<unit>[A-Za-z_][A-Za-z0-9_]*|[A-Z][A-Z_]{2,})\b")
r"(?P<unit>[A-Za-z_][A-Za-z0-9_]*|[A-Z][A-Z_]{2,})\b(?!-)")
CLAIM_EQ = re.compile(
r"(?P<unit>[A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*\**\s*(?P<num>[0-9]+(?:,[0-9]{3})*)")
REV_LONG = re.compile(r"\b[0-9a-f]{7,40}\b")
@@ -530,6 +578,12 @@ def _fixture_checks():
claims_of("read as UTF-8. Never 1, never a traceback"), [])
check("claims: a compound adjective is not a claim",
claims_of("a 120-char window and a 1-based index"), [])
# the same refusal on the unit side, which is where the 2026-10-18 false
# split came from: `ACS-only` is a compound adjective, not a unit
check("claims: a hyphenated unit word is a compound adjective, not a claim",
claims_of("39 ACS-only lines carry no Q<digits> token"), [])
check("claims: the same unit unhyphenated is still a claim",
claims_of("39 ACS lines carry no Q<digits> token"), [("ACS", 39)])
check("claims: the tightening keeps a count and its unit",
claims_of("471 UNRESOLVED and 1,612 citations"),
[("UNRESOLVED", 471), ("citations", 1612)])
@@ -680,6 +734,55 @@ def _fixture_checks():
resolve_tools(None, {"tools/b.py", "tools/a.py", "README.md"}),
(["tools/a.py", "tools/b.py"], []))
# ---- the gate. Before `--fail` this tool exited 0 while printing STALE
# rows -- the two historical STALEs at 4ce1aed require the
# pre-tightening reader, `ac0e211^`, so they are cited with that build
# and not as current behaviour (see FAILING ON THE CLASS IT CATCHES) --
# so the one class it exists to catch could not fail anything. The
# first five cells pin the decision; the last four pin the PROCESS exit
# code, because a helper returning 1 proves nothing about a gate.
stale_rep = {"tool": "t", "splits": [], "unchecked": {}, "failures": [],
"verdicts": [{"verdict": "STALE"}], "selftest_leg": True}
split_rep = {**stale_rep, "splits": [{"unit": "UNRESOLVED", "rows": []}],
"verdicts": [{"verdict": "MATCHES"}]}
# a leg failure alone: reported, never spent as a defect
clean_rep = {**stale_rep, "failures": [{"verdict": "RUN_FAILED"}],
"verdicts": [{"verdict": "MATCHES"},
{"verdict": "NOT_PRINTED"},
{"verdict": "AMBIGUOUS"},
{"verdict": "REV_UNCHECKED"}]}
check("gate: off is rc 0 even with a STALE claim", gate_rc([stale_rep], False), 0)
check("gate: on and a STALE claim -> rc 1", gate_rc([stale_rep], True), 1)
check("gate: on and an INTERNAL SPLIT alone -> rc 1", gate_rc([split_rep], True), 1)
check("gate: on and a leg failure alone -> rc 0 (reported, not spent)",
gate_rc([clean_rep], True), 0)
check("gate: the two columns sum across tools", gate_faults([stale_rep, split_rep]),
(1, 1))
with tempfile.TemporaryDirectory(prefix="doccensus-gate-") as td:
os.makedirs(os.path.join(td, "tools"))
for name, claim in (("stub_stale.py", 99), ("stub_ok.py", 7)):
with open(os.path.join(td, "tools", name), "w") as fh:
fh.write(f'"""A stub whose docstring says {claim} widgets."""\n'
"print('widgets: 7')\n")
genv = dict(os.environ, GIT_AUTHOR_NAME="t", GIT_AUTHOR_EMAIL="t@t",
GIT_COMMITTER_NAME="t", GIT_COMMITTER_EMAIL="t@t")
for sub in (["init", "-q"], ["add", "-A"], ["commit", "-qm", "fixture"]):
subprocess.run(["git", "-C", td] + sub, capture_output=True, env=genv)
def run_gate(*extra):
return subprocess.run([sys.executable, os.path.abspath(__file__),
"--repo", td, "--rev", "HEAD"] + list(extra),
capture_output=True, env=genv).returncode
check("gate e2e: a STALE claim fails the process (rc 1)", run_gate("--fail"), 1)
check("gate e2e: the same tree without --fail is rc 0 (the old silence)",
run_gate(), 0)
check("gate e2e: a matching claim passes the gate (rc 0)",
run_gate("--tools", "tools/stub_ok.py", "--fail"), 0)
check("gate e2e: a --tools filter matching nothing is rc 2, not 1",
run_gate("--tools", "tools/nope.py", "--fail"), 2)
print(f"selftest: {cells[0]} cells, {cells[0] - bad[0]} passed, {bad[0]} failed")
return 0 if ok else 1
@@ -927,6 +1030,34 @@ def counts(rep):
len(rep["verdicts"]), notes)
def gate_faults(reports):
"""(splits, stale) -- the two things `--fail` refuses to let pass.
The class this tool exists to catch is a docstring claim that disagrees
with the census printed beside it (STALE), and two claims of one unit that
disagree with each other (INTERNAL SPLIT). A LEG FAILURE IS NOT IN THIS
COUNT, for the reason counts() gives: a leg that produced no census
adjudicated no claim, so counting it would fail a run on a claim never
checked. rc 2, decided in main(), covers "could not grade" separately.
"""
splits = stale = 0
for rep in reports:
splits += len(rep.get("splits") or [])
stale += sum(1 for v in (rep.get("verdicts") or [])
if v.get("verdict") == "STALE")
return splits, stale
def gate_rc(reports, fail):
"""0 when the gate is off or nothing was graded faulty; 1 when it is on and
a defect claim was graded. Never 2: "could not grade" is main()'s call,
made before any report exists, and this function cannot see it."""
if not fail:
return 0
splits, stale = gate_faults(reports)
return 1 if (splits or stale) else 0
def resolve_tools(requested, tree):
"""(tools, missing). A requested name that is not tracked at the rev is
REPORTED, never silently dropped: `--tools checkin_check.py` (a bare
@@ -947,6 +1078,10 @@ def main():
ap.add_argument("--json")
ap.add_argument("--no-run", action="store_true",
help="internal-split verdicts only; no subprocess")
ap.add_argument("--fail", action="store_true",
help="rc 1 if any tool reports an INTERNAL SPLIT or a STALE "
"claim; leg failures are reported, never spent as "
"defects (rc 2 stays 'could not grade')")
ap.add_argument("--selftest", action="store_true")
a = ap.parse_args()
@@ -1016,7 +1151,16 @@ def main():
with open(a.json, "w") as fh:
json.dump({"rev": a.rev, "repo": a.repo, "reports": reports}, fh, indent=1)
print(f"wrote {a.json}")
return 0
if a.fail:
nsplit, nstale = gate_faults(reports)
if nsplit or nstale:
print(f"\nFAIL: {nsplit} INTERNAL SPLIT + {nstale} STALE claim(s) "
f"at {a.rev}"
+ (" (internal-split only: --no-run)" if a.no_run else ""))
return 1
print(f"\nOK: 0 INTERNAL SPLIT, 0 STALE claim(s) at {a.rev}"
+ (" (internal-split only: --no-run)" if a.no_run else ""))
return gate_rc(reports, a.fail)
if __name__ == "__main__":