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 atbacdead, 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 onfca3f2e: 1 split, 1 STALE, rc 1.
This commit is contained in:
1 parent
bacdead9c3
commit
26056d2a21
1 file changed
+147
-3
+147
-3
@@ -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__":
|
||||
|
||||
Reference in new issue
Block a user