The write-axis leg grades ONE surface per tool: `--json PATH`. Measured atc3ffee9with caller_write_flags.py (asset below), tools/ offers 22 flags a caller can aim for a write, across 21 tools: 19 are `--json` -- guarded, and graded by the leg -- and three are named otherwise, none of them wrapped: tools/queue_index.py :581 --write BARE tools/tool_map.py :510 --write BARE tools/doc_census_check.py :1831 --save-vocab BARE BARE = `open(path, "w")` with no try. Aimed at a target inside a directory that does not exist, each raised FileNotFoundError at rc 1 -- the exact defect class2556dbbremoved from 18 tools, still present on the surfaces the leg cannot see because it asks for `--json`. Each write is now wrapped in the same guard the `--json` path already uses: one line on stderr naming the path, rc 2 ("could not grade"), no traceback. Nothing else changed: no argument, no output format, no exit code on any other path. VERIFIED, both directions, in a throwaway worktree (never this tree): /root/.hermes/wizard-outputs/volt/2026-10-21-write-axis-bites/write_flag_probe.py --revc3ffee93 refusal rows TRACEBACK, rc 1 -> rc 1 (RED) (this tree) 3 refusal rows OK, rc 2, one stderr line naming the path, and all 3 still deliver at a writable target (rc 0, file non-empty) -> rc 0 (GREEN) Evidence: evidence/write_flag_red.json, evidence/write_flag_green.json. Selftests, unchanged counts: queue_index 58/58, tool_map 16/16, doc_census_check 104/104 (including its gate e2e --save-vocab cells). repo gate: 6 legs, 6 pass, 0 fault, rc 0 (selftest-sweep 21 tools 0 fatal). REMAINING BLIND SPOT, stated so it is not read as closed: write_probe.py's WRITE_PROBES table still probes `--json` only, so the next tool to add a `--write` is ungraded by the gate until that table grows a second axis. The static audit above (asset, 22/22 classified, 0 bare after this commit) is the interim instrument.
1862 lines
102 KiB
Python
1862 lines
102 KiB
Python
#!/usr/bin/env python3
|
|
"""doc_census_check.py -- does a tool's own docstring census match the census
|
|
that tool PRINTS?
|
|
|
|
WHY THIS EXISTS
|
|
This archive's three scanners each carry a census table in their module
|
|
docstring: counts that a reader cites, that no command checks. The docstring
|
|
is prose; prose is not executed. On 2026-10-16 the control plane returned
|
|
exactly this defect against a sibling scanner, in its own words:
|
|
|
|
"The scanner's own docstring carries a number that does not match the
|
|
census it prints -- the exact class the tool exists to catch."
|
|
|
|
tools/citation_resolve_check.py said `471 UNRESOLVED` on one line and
|
|
`441 UNRESOLVED` nine lines later, over a census the tool prints as 471.
|
|
Nothing was checking the docstring against the output beside it. That is this
|
|
tool's whole job.
|
|
|
|
WHAT IT MEASURES (read-only: this tool only writes --json)
|
|
Two independent verdicts per tool, and they do not share evidence.
|
|
|
|
1. INTERNAL SPLIT -- one unit, one scope, two values. Within one docstring,
|
|
every numeric claim of the form `<value> <unit>` is collected; two claims
|
|
of ONE unit that a single revision scope covers, carrying two different
|
|
values, is a split. This catches the 441/471 instance. A split is GRADED
|
|
only when a leg verified the unit (`CHECKABLE`: the run printed it with
|
|
exactly one value); every other split -- no oracle (`LABEL_ONLY`,
|
|
`--no-run`), a unit no leg prints (`NOT_A_CENSUS_UNIT`), a unit the tool
|
|
prints per section (`SECTION_SPLIT_BY_TOOL`) -- is printed with its values
|
|
as ADVISORY and never reaches the gate. 2026-10-18: the old build failed
|
|
`--fail --no-run` on the 471/441 split at fca3f2e (rc 1) while the full run
|
|
could not see either value, so rc 1 meant "could not check this".
|
|
|
|
2. ORACLE -- the tool is RUN, at the revision its own docstring names, and
|
|
each claim is compared against the value that run prints for that unit.
|
|
MATCHES claim == printed value
|
|
STALE claim != printed value, and the printed unit is not
|
|
declared clone-local -- a defect claim
|
|
CLONE_LOCAL claim != printed value but the tool's own output marks
|
|
that unit clone-local ("blobs in THIS clone's local object
|
|
store"), so drift is the documented expectation, not a
|
|
defect. Reported, never counted as one.
|
|
AMBIGUOUS the tool prints that unit with 2+ values at that rev
|
|
(e.g. `citations` prints once per section), so there is no
|
|
single oracle value; both are printed and no claim is made
|
|
NOT_PRINTED the unit does not appear in this tool's output at all
|
|
|
|
A claim is `<digits> <unitword>` with the unit word ADJACENT, which is the
|
|
archive's own notation. A bare number is not a claim and is not checked.
|
|
|
|
A WRAPPED PAIR IS A CLAIM ONLY WHEN A LEG CAN BIND IT. A number that ENDS one
|
|
line with its unit opening the next is the same claim the archive writes on
|
|
one line, and until 2026-10-20 this reader was line-local: a docstring reflow
|
|
retired the claim silently while every command exited 0 (measured -- the
|
|
pinned pair in `queue_claim_check.py` fell out of the census and the claim
|
|
count fell with it, with no verdict moving). The reader now joins the pair,
|
|
under the same refusals the adjacency reader already applies: a number welded
|
|
to a preceding `-`, `/`, `.`, `:`, `_` or letter is not standalone, so a
|
|
date's tail and `UTF-8` and `0/1` are not counts, and a unit with a hyphen
|
|
behind it is a compound adjective. Whether the joined pair is SPENT is a
|
|
second question, and it is answered by the census rather than by a lexicon of
|
|
English: the pair is graded only when some leg prints its unit UNDER THAT
|
|
SPELLING, and every pair no leg binds is printed as WRAP_UNBOUND and counted
|
|
in the banner, never dropped. The spelling is exact and that is deliberate: a
|
|
plural fold (`notes` -> `note`) is affordable for a one-line claim, because
|
|
both sides are census voice, but a wrap is this reader's hypothesis about two
|
|
lines of prose. Measured at HEAD 2026-10-20, the fold spent `the queue's own
|
|
reply 62 / notes it` and graded a correct sentence STALE 62-vs-7; with the
|
|
fold refused, that pair is WRAP_UNBOUND and the one wrapped claim a leg really
|
|
binds -- `1,062 CONTENT_SHA256` at `citation_hash_class.py:53` -- is the only
|
|
one graded. The other five candidates are named, not graded: `69 filenames`
|
|
(`checkin_check.py:14`), `18 cells`, `24 spendable` and `0 without` (all
|
|
three in this file -- its `--selftest`, fast-mode and `--fail` paragraphs),
|
|
and `62 notes` (`queue_claim_check.py:119`). With `--no-run` there is no leg, so
|
|
no wrap is spent and each one is reported unbound -- a mode that cannot see a
|
|
census cannot bind a claim.
|
|
|
|
TWO ORACLE LEGS, AND WHY THE SECOND ONE EXISTS
|
|
Leg 1 is the tool's own run at the block's revision, exactly as above. Leg 2
|
|
is the SAME TOOL'S `--selftest`, added 2026-10-16 after the control plane
|
|
returned a docstring claim of `13 cells` against a selftest that prints `18
|
|
cells` -- the defect class this tool exists to catch, sitting inside its own
|
|
default scope and still read as NOT_PRINTED, because no leg ever passed
|
|
`--selftest` and a cell count is printed by nothing else. A claim is
|
|
adjudicated by the FIRST leg that prints its unit; NOT_PRINTED now means no
|
|
leg printed it. A tool that takes no `--selftest` simply has one leg -- a fact
|
|
about the tool, not a defect in it.
|
|
|
|
A leg that exits non-zero having printed nothing is reported RUN_FAILED, never
|
|
read as an empty census, and it adjudicates nothing either: the block's claims
|
|
are still read off any surviving leg -- leg 2 -- and are left unadjudicated
|
|
only when no leg survives, because a failed run is not a reason to stop
|
|
looking. A revision that will not materialise fails the same way rather than
|
|
producing a census at all: an empty queue is a census of zeros, and reading it
|
|
as that revision's census is the phantom this rule refuses.
|
|
|
|
A HEX RUN INTRODUCED AS A DIGEST IS NOT A REVISION. A block that quotes a
|
|
truncated sha256 in its prose is a size, not a place: read as a rev it files
|
|
the block under a revision that cannot exist, fails that leg, and then
|
|
adjudicates every claim in the block again beside the real ones. Two signals
|
|
are read, both local to the token -- a digest word in front of it (`sha256
|
|
<hex>`, `hash <hex>`), or an ellipsis behind it (`<hex>…`), which is how this
|
|
archive writes a truncated digest. See `block_revs`.
|
|
|
|
The run leg is `<script> --repo R --rev REV`. A tool that takes no `--rev`
|
|
answers it with an argparse diagnosis and prints no census at all, so the leg
|
|
is re-run with the rejected flags DROPPED -- the exact tokens argparse named,
|
|
and only those -- and is tagged `no-rev`; a recipe's leg may carry the same tag
|
|
for the same reason. Such a tool measures the tree it is POINTED AT, so a claim
|
|
in a block that names a revision is reported REV_UNCHECKED -- never counted as
|
|
verified against that revision, and never counted as a defect -- while a block
|
|
that names no revision is checked normally, since the default is what the flag
|
|
means there.
|
|
|
|
THIS TOOL DOES NOT RUN ITSELF. The default scope is every `tools/*.py` in the
|
|
tree, and this tool is one of them. Running it on itself spawns it, which
|
|
spawns it again, without a floor -- measured, not reasoned, in the check-in of
|
|
2026-10-16. The self target is therefore excluded from the run leg and every
|
|
one of its claims is reported SELF_NOT_RUN. It is deliberately still listed in
|
|
the scanned set, so the exclusion is a printed row and not a silence.
|
|
|
|
SCOPE, STATED RATHER THAN IMPLIED
|
|
Only module docstrings of .py tools are scanned: a docstring is where a census
|
|
table lives. Only revisions NAMED IN THE BLOCK are used as oracles -- a block
|
|
naming no revision is checked against the tool's default (HEAD) and tagged, so
|
|
a stale rev is never silently attributed. A tool whose subject is not `--repo`
|
|
+ `--rev` is driven through a per-tool recipe declared below, with a temp file
|
|
that is removed: `queue_claim_check.py` takes a materialised QUEUE file and so
|
|
its leg IS the revision, while `append_only_owners.py` takes a tree scan that
|
|
no flag can pin to a revision, so its leg is tagged `no-rev`. A tool no recipe
|
|
covers, and that answers `--repo ... --rev ...` with an argparse diagnosis, is
|
|
re-run with the rejected flags dropped and tagged the same way -- measured at
|
|
HEAD on 2026-10-16 against `check_lint.py`, which takes neither flag and whose
|
|
default census is a real one.
|
|
|
|
WHAT THIS DOES NOT CHECK, and the measured size of the gap. A claim is only
|
|
adjudicated when the unit it names is a unit the tool PRINTS. Over the three
|
|
scanners at d355395 the run read ninety-two claims (with the pre-2026-10-18
|
|
separator rule, which read punctuation and list markers as claims): twelve
|
|
were verified against the run they point at, eight were in units the tool
|
|
prints with two values (SECTION_SPLIT_BY_TOOL -- no oracle), and seventy-one
|
|
were in units the tool never prints at all. So the oracle's reach is narrow
|
|
and its precision is the reason: it reported exactly one bad claim on that
|
|
corpus, and that one was the returned defect. A wider rule was tried first and
|
|
falsified by its own first run -- num_agreement_check.py flagged a hundred and
|
|
seventeen restatements across the archive, too coarse to be read -- so the
|
|
narrow rule is the one that ships. Two claim forms are read (`<value> <unit>` and
|
|
`<unit>: <value>`); a claim written in neither form is not seen, and that is a
|
|
stated gap, not a silent one.
|
|
|
|
NO CENSUS OF THIS TOOL'S OWN VERDICTS APPEARS IN THIS DOCSTRING, deliberately:
|
|
this scanner scans itself, so a count of its own output written here would
|
|
have to agree with the output of the run that the same commit changes. That is
|
|
the limit citation_hash_class.py records in the same words -- a commit cannot
|
|
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 whose unit a leg --
|
|
or a cached vocabulary -- verified,
|
|
or a STALE claim)
|
|
2 could not grade (usage, a --tools filter that matches
|
|
nothing tracked, an unreadable
|
|
--vocab, --save-vocab without a run)
|
|
|
|
THE TWO MODES GRADE THE SAME CLASS. `--no-run` runs nothing, so it has no
|
|
oracle and cannot prove any split is a census unit: it prints every split as
|
|
ADVISORY with its values, and `--fail --no-run` exits 0. That is the ruling
|
|
of reply 22 (2026-10-18), and it fixes a real asymmetry: `--fail --no-run`
|
|
returned 1 on `citation_resolve_check.py` at fca3f2e (471@82 / 441@89) while
|
|
the same units were advisory under a full run. Measured after the repair at
|
|
the same rev, one mode apart: `--fail --no-run` rc 0 with the pair printed as
|
|
ADVISORY; `--fail` (full) rc 1 on the same split, now CHECKABLE because the
|
|
run prints `UNRESOLVED 471` -- plus the STALE claim at :89. The teeth are
|
|
kept where they can be verified, which is the only place they mean anything.
|
|
|
|
A CACHED VOCABULARY PUTS THE TEETH BACK IN THE FAST MODE, without asking it
|
|
to invent an oracle. `--save-vocab PATH` (full run) writes the census
|
|
vocabulary the run OBSERVED -- every unit key some leg printed AND that a
|
|
claim in that tool's own docstring can spend, per tool -- and
|
|
`--no-run --vocab PATH` grades a split in a unit that vocabulary names as
|
|
CACHE_CHECKABLE. The two halves are both required: the leg proves the key is
|
|
something the tool actually prints, the docstring proves some claim could
|
|
ever ask about it. A key no claim can spend (`keys_for`'s expansion of every
|
|
claimed unit) is dropped, because a permission list is only auditable when
|
|
every entry in it can be exercised -- 422 observed keys at 30c8120, 24
|
|
spendable, the other 398 unspendable by construction. That scoping is
|
|
measured to be outcome-neutral (see THE FAST MODE REPORTS WHAT IT CANNOT
|
|
GRADE); it does NOT close the cache's one over-reach, which is a word the
|
|
docstring really does claim twice (below).
|
|
|
|
What the cache proves is narrow and enough: the unit is a
|
|
census unit for that tool (a leg printed it at `<vocab rev>`), and the
|
|
disagreement being graded is INTERNAL to the docstring, so the leg never has
|
|
to supply the value. What it cannot prove is a value, so a STALE claim is
|
|
still impossible without a run, and a unit the vocabulary does not name stays
|
|
ADVISORY -- the 39/12/1 `ACS-only` class is still never spent. Measured
|
|
2026-10-18 at 814acc4 on a stub whose docstring splits 471/441 over a census
|
|
printing 471: `--no-run --fail` rc 0, `--no-run --vocab V --fail` rc 1,
|
|
`--fail` rc 1. The vocabulary's rev is printed with every CACHE_CHECKABLE
|
|
row, because a reader is entitled to know how old the census behind the
|
|
label is; it is NOT required to equal the revision being checked (unit-hood
|
|
is a property of the unit word, and the graded disagreement is internal).
|
|
|
|
THE FAST MODE REPORTS WHAT IT CANNOT GRADE. Before 2026-10-18 a runless split
|
|
in a lowercase word vanished: `unit_scope` answered UNCHECKED_NO_ORACLE, and
|
|
the printer showed only the LABEL_ONLY class, so a reader of `--no-run` saw
|
|
nothing at all for it (`vocab-precision` case E: rc 0, no line). It is now
|
|
printed as ADVISORY with its values and never graded -- silent was the only
|
|
part of that behaviour that was wrong, and silence is not a verdict. Scoping
|
|
the saved vocabulary to spendable keys is measured OUTCOME-NEUTRAL on the
|
|
same fixture (A..E still [1,1,0,0,0]) and on the tree (fast gate rc 0 before
|
|
and after; the fca3f2e tooth still rc 1 from the cache), which is the honest
|
|
description: it makes the permission list auditable, it does not change a
|
|
verdict, and it does not close the over-reach this paragraph opened with.
|
|
|
|
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] [--fail] [--no-run] [--vocab PATH] [--save-vocab PATH]
|
|
[--selftest] # selftest: 104 cells, one per claim class
|
|
"""
|
|
import argparse
|
|
import ast
|
|
import collections
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
DEFAULT_REPO = "/root/troupe-standup"
|
|
|
|
# A claim, in either of the two forms this archive writes:
|
|
# `<value> <unit>` -- "471 UNRESOLVED", "1,612 citations"
|
|
# `<unit> [: =] <value>` -- "ACS = 72", "overlap = 20" (markdown bold allowed)
|
|
# A bare number is not a claim. Deliberately not "any word": `471 cannot be
|
|
# resolved` is prose, and a checker that flags prose is a checker nobody reads.
|
|
#
|
|
# THE SEPARATOR IS SPACE, OR A CLOSING DELIMITER AND A SPACE -- tightened
|
|
# 2026-10-18 after the census was read off every tool at HEAD (140 claims) and
|
|
# the separator vocabulary was printed: `''` (11 claims, a hex run read as
|
|
# digits+word: `83acffe0` -> 83 `acffe0`), `'. '` (5, an ordinal or a sentence
|
|
# end: `1. INTERNAL`, `615. Cite`), `'-'` (5, a compound adjective: `120-char`,
|
|
# `1-based`), `' '` (4, an aligned list marker: `0 ran`), and the punctuation
|
|
# spills `'; '` `' "'` `'+ '` `') '`. Only 95 of the 140 were a count and its
|
|
# unit. The loose class fired two STALE verdicts in the full run at 4ce1aed
|
|
# against `checkin_check.py:59` -- "Never 1, never a traceback", where the digit
|
|
# is a code, not a census -- which is a false defect claim of exactly the kind
|
|
# 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.
|
|
#
|
|
# A COLON BEFORE THE NUMBER IS A LINE CITE, NOT A COUNT, made 2026-10-19 after
|
|
# the --fail gate went red at HEAD on the same tool it went red on for the
|
|
# hyphen: `queue_claim_check.py`'s docstring records its own past behaviour as
|
|
# "graded `:7303` CLOSED on Mercury's `:9869`" and "printed `open set:
|
|
# Q37:5278`", and the reader took `7303` for a count of CLOSED rows (printed
|
|
# 13) and of open rows (printed 2). Both are queue LINE NUMBERS in backticks,
|
|
# the same shape `CLAIM_EQ` already refuses one rule over ("`WORK-QUEUE.md:8586`
|
|
# is a citation"); the value/unit form had no such guard because no docstring
|
|
# had yet written `:NNNN <state word>`. Measured on every tracked docstring at
|
|
# HEAD: eleven of the hundred and twenty-three claims read carry a ':' immediately
|
|
# before the number, and every one is a line cite across four tools --
|
|
# `citation_hash_class.py` (:8467 defect),
|
|
# `citation_resolve_check.py` (four: :8003 cites, :6348 column, :9 reads, :8530
|
|
# Mercury), `queue_claim_check.py` (five, the two false STALEs among them) and
|
|
# `queue_index.py` (:7246 prose). Not one names a unit the tool prints. The
|
|
# invariant that licenses this tightening is that no MATCHES row moves, the
|
|
# same invariant the hyphen rule shipped under; the cost is that a count
|
|
# written `:13 claims` is no longer read, and no scanner writes one.
|
|
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(?!-)")
|
|
CLAIM_EQ = re.compile(
|
|
r"(?P<unit>[A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*\**\s*(?P<num>[0-9]+(?:,[0-9]{3})*)")
|
|
|
|
# A WRAPPED PAIR: the number ENDS one line and its unit OPENS the next. Until
|
|
# 2026-10-20 this reader was line-local, so a claim that a docstring's 79-column
|
|
# wrap cut in two was not a claim at all. That is not cosmetic: `queue_claim_check
|
|
# .py`'s pinned pair `1,229,541 B / 9,953 lines` was broken onto two lines at
|
|
# `70fa8b2` and the census silently fell from seventeen claims to sixteen with no
|
|
# verdict moving -- the archive's own instrument, blind to the archive's own
|
|
# notation, while every command exited 0. Measured at HEAD 2026-10-20 by running
|
|
# this reader over all sixteen tracked `tools/*.py` MODULE docstrings -- the only
|
|
# scope docstring_span reads: SIX
|
|
# number-ends-a-line candidates -- FOUR real counts with their unit
|
|
# (`69 filenames` at checkin_check.py:14, `1,062 CONTENT_SHA256` at
|
|
# citation_hash_class.py:53, `18 cells` and `24 spendable` in this file's
|
|
# `--selftest` and fast-mode paragraphs) and TWO words that merely open a
|
|
# sentence (`0 without` in the `--fail` paragraph here, `62 notes` at
|
|
# queue_claim_check.py:119). The same reader at `70fa8b2`, where the break was
|
|
# introduced, reads seven -- the same six plus the pinned `9,953 lines` pair --
|
|
# and spends it, so that docstring reads sixteen claims against the old reader
|
|
# and seventeen against this one. `4e6b2ef`, the commit that put the pair back on
|
|
# one line, is the control on the other side: it reads six candidates and
|
|
# seventeen claims on BOTH readers, so the six is a measurement of HEAD's prose
|
|
# and not a fallback the join quietly invents. A twelve-row artifact was produced
|
|
# that day by a looser net that counted every number ending a line with any word
|
|
# after it, non-counts included; the reader itself reads six.
|
|
# The same measurement over FUNCTION and CLASS docstrings -- which this tool does
|
|
# not read at all, module docstrings only -- finds two more real counts, `29
|
|
# literal` in append_only_audit.py and `74 files` in checkin_check.py. They are
|
|
# named here rather than quietly excluded: a scope the reader cannot see is
|
|
# worth knowing, and the join would spend either one if the scope ever widened.
|
|
#
|
|
# WHY THERE IS NO WORD LIST HERE. The obvious reader is "join the wrap unless the
|
|
# unit is a verb", and it needs an English lexicon that would refuse a real claim
|
|
# the day one is written in an unlucky voice. The discrimination is made where
|
|
# this tool already makes it: A WRAPPED PAIR IS SPENT ONLY WHEN A LEG PRINTS ITS
|
|
# UNIT (see check_tool). At HEAD that spends exactly one of the six --
|
|
# `1,062 CONTENT_SHA256`, whose census prints the label -- and the other five
|
|
# (`69 filenames`, `18 cells`, `24 spendable`, `0 without`, `62 notes`) are
|
|
# printed unbound and never graded. The structural half below is the part that
|
|
# must not be guessed at, and it is the same half the adjacency reader already
|
|
# enforces: a number welded to a preceding `-`, `/`, `.`, `:`, `_` or
|
|
# alphanumeric is not standalone (`2026-10-18`, `UTF-8` and `0/1` are three of
|
|
# the strings this refuses, each one a refusal cell in the selftest below), and
|
|
# a unit with a hyphen behind it is a compound adjective, not a unit noun.
|
|
WRAP_LEAD = re.compile(r"(?:^|[\s(\[{*`'\"])(?P<num>[0-9]+(?:,[0-9]{3})*)[ \t]*$")
|
|
WRAP_UNIT = re.compile(r"^[ \t]*(?P<unit>[A-Za-z_][A-Za-z0-9_]*|[A-Z][A-Z_]{2,})\b(?!-)")
|
|
REV_LONG = re.compile(r"\b[0-9a-f]{7,40}\b")
|
|
|
|
# A hex run introduced as a DIGEST is not a revision, and reading it as one costs
|
|
# more than a wrong label: the block is filed under a revision that cannot exist,
|
|
# its leg fails, and then every claim in the block is adjudicated once per
|
|
# spurious rev beside the real ones. Measured 2026-10-16 at HEAD on
|
|
# `tools/queue_claim_check.py`, whose UNITS block quotes the queue's size as
|
|
# "/ sha256 `fbe51445a89f81ab…`": three claims emitted FOUR times, once per
|
|
# REV_LONG token, and two `git show <sha256-prefix>:WORK-QUEUE.md` leg failures.
|
|
# Two signals, both local to the token: a digest word in front of it (sha256,
|
|
# sha1, md5, digest, hash), or an ellipsis behind it -- the archive's own way of
|
|
# writing a truncated digest. A revision cited in the ordinary way carries
|
|
# neither, and a FULL sha256 is already invisible to REV_LONG: the run has no
|
|
# word boundary to bite on.
|
|
DIGEST_BEFORE = re.compile(
|
|
r"(?:sha[-\s]?256|sha[-\s]?1|md5|digest|hash)\s*[:=]?\s*[`'\"]?\s*$", re.I)
|
|
DIGEST_AFTER = re.compile(r"^[`'\"]?\s*(?:…|\.\.\.)")
|
|
|
|
|
|
def block_revs(text):
|
|
"""The revisions a block names -- hex runs read as digests excluded."""
|
|
out = set()
|
|
for m in REV_LONG.finditer(text):
|
|
if DIGEST_BEFORE.search(text[:m.start()]) or \
|
|
DIGEST_AFTER.match(text[m.end():]):
|
|
continue
|
|
out.add(m.group(0))
|
|
return sorted(out)
|
|
|
|
# Function words and units this tool does not adjudicate. `at` in "2026-10-15 at
|
|
# rev 9188dd3" is not a census quantity; neither is `B` (bytes differ by
|
|
# revision by design, and "1,201,550 B" is a size, not a count). Stated so the
|
|
# exclusion is a rule rather than a habit.
|
|
STOPWORDS = {
|
|
"at", "the", "of", "in", "is", "are", "was", "and", "or", "to", "for", "on",
|
|
"by", "as", "it", "its", "that", "this", "these", "those", "with", "from",
|
|
"no", "not", "one", "two", "three", "all", "any", "be", "been", "has",
|
|
"have", "had", "his", "her", "our", "your", "their", "but", "if", "then",
|
|
"than", "so", "up", "out", "off", "over", "into", "run", "runs", "may",
|
|
"can", "same", "each", "both", "per", "via", "b", "kb", "mb", "gb",
|
|
}
|
|
|
|
# How to run each tool at a revision. Any tool not listed here is run as
|
|
# `<script> --repo R --rev REV`. A recipe returns `(argv, tag)` and the tag says
|
|
# whether the recipe could materialise the revision the block names.
|
|
#
|
|
# queue_claim_check.py takes no --rev: its subject is a FILE, and the file is
|
|
# materialised at the revision and handed to it -- so the leg IS that rev.
|
|
# A revision that will NOT materialise is a raised failure, never an empty
|
|
# file: an empty queue is a census of zeros, and a census of zeros reads as
|
|
# that revision's census. Measured 2026-10-16 -- `git show
|
|
# c3b4a290dc64bd24:WORK-QUEUE.md` (a sha256 prefix REV_LONG read as a rev)
|
|
# exits 128, and the leg it fed printed `0 opening records` at rc 0.
|
|
# append_only_owners.py takes a positional AUDIT.json and nothing else: its
|
|
# input is a tree scan, which its sibling produces over the WORKING TREE
|
|
# (`append_only_audit.py` has no --rev either). This scanner is read-only, so
|
|
# it will not add a git worktree to the repository it is only allowed to
|
|
# read: the leg is tagged no-rev and every claim in a block that names a
|
|
# revision is reported REV_UNCHECKED, never adjudicated against a tree it
|
|
# did not stand on.
|
|
def _recipe_queue(repo, rev, tmpdir):
|
|
dst = os.path.join(tmpdir, f"WORK-QUEUE.{rev}.md")
|
|
p = subprocess.run(["git", "-C", repo, "show", f"{rev}:WORK-QUEUE.md"],
|
|
capture_output=True)
|
|
if p.returncode != 0:
|
|
raise RuntimeError("git show %s:WORK-QUEUE.md -> rc=%d: %s"
|
|
% (rev, p.returncode,
|
|
p.stderr.decode("utf-8", "replace").strip()[:120]))
|
|
with open(dst, "wb") as fh:
|
|
fh.write(p.stdout)
|
|
return ["--queue", dst, "--census"], ""
|
|
|
|
|
|
def _recipe_owners(repo, rev, tmpdir):
|
|
dst = os.path.join(tmpdir, f"append-only-audit.{rev}.json")
|
|
subprocess.run([sys.executable,
|
|
os.path.join(repo, "tools", "append_only_audit.py"),
|
|
"--repo", repo, "--json", dst], capture_output=True)
|
|
return [dst], "no-rev"
|
|
|
|
|
|
RECIPES = {"queue_claim_check.py": _recipe_queue,
|
|
"append_only_owners.py": _recipe_owners}
|
|
|
|
|
|
def git(repo, *a):
|
|
return subprocess.run(["git", "-C", repo, *a], capture_output=True).stdout
|
|
|
|
|
|
def docstring_span(path, text):
|
|
try:
|
|
tree = ast.parse(text)
|
|
except SyntaxError:
|
|
return None
|
|
if not tree.body or not isinstance(tree.body[0], ast.Expr):
|
|
return None
|
|
node = tree.body[0]
|
|
if not isinstance(node.value, ast.Constant) or not isinstance(node.value.value, str):
|
|
return None
|
|
return node.lineno, node.end_lineno
|
|
|
|
|
|
def blocks(lines, lo, hi):
|
|
"""Blank-line-delimited paragraphs -- the same scope the sibling scanners
|
|
use (citation_resolve_check.py for inheritance, num_agreement_check.py for
|
|
quantity agreement), so one paragraph is one scope in all three."""
|
|
start, buf = lo, []
|
|
for ln in range(lo, hi + 1):
|
|
raw = lines[ln - 1]
|
|
if raw.strip() == "":
|
|
if buf:
|
|
yield start, buf
|
|
buf, start = [], ln + 1
|
|
else:
|
|
buf.append((ln, raw))
|
|
if buf:
|
|
yield start, buf
|
|
|
|
|
|
# ------------------------------------------------- printed census -> oracle
|
|
def parse_printed(stdout):
|
|
"""stdout -> {unit: {value: [line, ...]}} plus the set of clone-local units.
|
|
|
|
Four line shapes are read, all of them the scanners' own:
|
|
` RESOLVES 113` indented label then count (family rows)
|
|
` citations: 410` label then count
|
|
`corpus: 307 files at ...` count then unit
|
|
`overlap A&B = 20 ; ...` inline assignments, split on the separators
|
|
A unit whose printed line (or the line above it, for a wrapped phrase) says
|
|
"clone" is clone-local: its count is expected to move with the clone.
|
|
"""
|
|
units = collections.defaultdict(lambda: collections.defaultdict(list))
|
|
clone = set()
|
|
lines = stdout.split("\n")
|
|
|
|
def add(unit, value, ln, cloneline):
|
|
u = unit.strip().lower()
|
|
if len(u) < 3 or not re.fullmatch(r"[a-z_][a-z0-9_]*", u) or u in STOPWORDS:
|
|
return
|
|
units[u][value].append(ln)
|
|
if "clone" in cloneline.lower():
|
|
clone.add(u)
|
|
|
|
for i, raw in enumerate(lines, 1):
|
|
ctx = raw + " " + (lines[i - 2] if i >= 2 else "")
|
|
m = re.match(r"^\s{2,}([A-Za-z_][A-Za-z0-9_]*)\s+([0-9][0-9,]*)\s*$", raw)
|
|
if m: # family row: label count
|
|
add(m.group(1), int(m.group(2).replace(",", "")), i, ctx)
|
|
continue
|
|
m = re.match(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*([0-9][0-9,]*)\b", raw)
|
|
if m: # label: count
|
|
add(m.group(1), int(m.group(2).replace(",", "")), i, ctx)
|
|
for m in re.finditer(r"([0-9][0-9,]*)\s+(?:[a-z]+ )?([A-Za-z_][A-Za-z0-9_]*)\b", raw):
|
|
add(m.group(2), int(m.group(1).replace(",", "")), i, ctx)
|
|
for seg in re.split(r"[;]", raw): # inline `X = N`
|
|
m = re.match(r"^\s*(.*?)\s*[:=]\s*([0-9][0-9,]*)\s*$", seg)
|
|
if not m or not m.group(1):
|
|
continue
|
|
words = re.findall(r"[A-Za-z_][A-Za-z0-9_]*", m.group(1))
|
|
if not words:
|
|
continue
|
|
val = int(m.group(2).replace(",", ""))
|
|
add("_".join(w.lower() for w in words), val, i, ctx) # whole phrase
|
|
add(words[-1], val, i, ctx) # the nearest label: `overlap A&B`
|
|
# the scanners' own unit declaration: `... 6961 (unit: occurrences)`.
|
|
# The unit is named AFTER its number, which no other rule reads.
|
|
for m in re.finditer(r"\(\s*unit\s*:\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)", raw):
|
|
left = raw[:m.start()]
|
|
nums = re.findall(r"([0-9][0-9,]*)\s*$", left.rstrip())
|
|
if nums:
|
|
add(m.group(1), int(nums[-1].replace(",", "")), i, ctx)
|
|
return {k: dict(v) for k, v in units.items()}, clone
|
|
|
|
|
|
def ordinal_at(raw, pos):
|
|
"""True when `pos` is the number that OPENS this line and 2+ spaces follow.
|
|
|
|
That is a list marker -- the exit-code block's `0 ran; nothing to report`,
|
|
`1 ran, and graded ...`, `2 could not grade` -- and the word after it is
|
|
prose, not a unit. The same shape mid-line (`WORK-QUEUE.md:8003 cites`) is
|
|
a citation's line number, not a count, and is excluded by the same rule.
|
|
"""
|
|
m = re.match(r"[ \t]*([0-9][0-9,]*)([ \t]{2,})", raw)
|
|
return bool(m) and m.start(1) == pos
|
|
|
|
|
|
def claims_in(blk):
|
|
"""[(line, value, unit, token, offset, wrapped)] for one block.
|
|
|
|
Both claim forms are read and a claim found by both is reported once. The
|
|
offset is measured on the block text so a RESTATEMENT (far apart) can be
|
|
told from an ENUMERATION of neighbouring sub-counts, the same
|
|
discrimination num_agreement_check.py applies.
|
|
|
|
`wrapped` is True for a claim whose number ends one line and whose unit opens
|
|
the next -- see WRAP_LEAD. It is a reading decision only: whether such a pair
|
|
is SPENT is decided in check_tool, by whether a leg prints its unit.
|
|
"""
|
|
out, off, starts = [], 0, []
|
|
for ln, raw in blk:
|
|
starts.append(off)
|
|
found = {}
|
|
for m in CLAIM.finditer(raw):
|
|
u = m.group("unit")
|
|
if len(u) < 3 or u.lower() in STOPWORDS or re.fullmatch(r"[qQ]\d+", u):
|
|
continue
|
|
if ordinal_at(raw, m.start()):
|
|
continue
|
|
found[m.start()] = (int(m.group("num").replace(",", "")), u, m.group(0))
|
|
for m in CLAIM_EQ.finditer(raw):
|
|
u = m.group("unit")
|
|
if (len(u) < 3 or u.lower() in STOPWORDS
|
|
or re.fullmatch(r"[qQ]\d+", u) or m.start() in found):
|
|
continue
|
|
if m.start() and re.match(r"[0-9A-Za-z_.]", raw[m.start() - 1]):
|
|
continue # `WORK-QUEUE.md:8586` is a citation
|
|
found[m.start()] = (int(m.group("num").replace(",", "")), u, m.group(0))
|
|
for pos in sorted(found):
|
|
val, u, tok = found[pos]
|
|
out.append((ln, val, u, tok, off + pos, False))
|
|
off += len(raw) + 1
|
|
for i in range(len(blk) - 1):
|
|
ln, raw = blk[i]
|
|
m = WRAP_LEAD.search(raw)
|
|
if not m:
|
|
continue
|
|
um = WRAP_UNIT.match(blk[i + 1][1])
|
|
if not um:
|
|
continue
|
|
u = um.group("unit")
|
|
if len(u) < 3 or u.lower() in STOPWORDS or re.fullmatch(r"[qQ]\d+", u):
|
|
continue
|
|
if ordinal_at(raw, m.start("num")):
|
|
continue
|
|
num = m.group("num")
|
|
out.append((ln, int(num.replace(",", "")), u, f"{num} {u}",
|
|
starts[i] + m.start("num"), True))
|
|
out.sort(key=lambda r: r[4])
|
|
return out
|
|
|
|
|
|
def is_label(unit):
|
|
"""An uppercase census label -- the vocabulary a split is checked in when
|
|
the tool has not been run (no oracle available)."""
|
|
return len(unit) >= 3 and re.fullmatch(r"[A-Z][A-Z_]*", unit) is not None
|
|
|
|
|
|
def keys_for(unit, printed):
|
|
"""Candidate oracle keys for a claimed unit word."""
|
|
cands = [unit, unit.lower(), unit.lower().rstrip("s"), unit.upper()]
|
|
seen, out = set(), []
|
|
for c in cands:
|
|
if c.lower() in printed and c.lower() not in seen:
|
|
seen.add(c.lower())
|
|
out.append(c.lower())
|
|
return out
|
|
|
|
|
|
def oracle(printed, clone, unit, value, tag=""):
|
|
"""(verdict, detail) for one claim; `printed` is one revision's map.
|
|
|
|
`tag` names the leg a verdict came from when it is not leg 1 ("no-rev",
|
|
"selftest"): a reader can then tell which census the number was read off.
|
|
"""
|
|
keys = keys_for(unit, printed)
|
|
vals = set()
|
|
for k in keys:
|
|
vals |= set(printed[k])
|
|
if not vals:
|
|
return "NOT_PRINTED", ""
|
|
if len(vals) > 1:
|
|
return "AMBIGUOUS", "printed: " + ", ".join(str(v) for v in sorted(vals))
|
|
printed_val = vals.pop()
|
|
if printed_val == value:
|
|
return "MATCHES", ""
|
|
if any(k in clone for k in keys):
|
|
return "CLONE_LOCAL", f"printed {printed_val}" + (f" [{tag}]" if tag else "")
|
|
return "STALE", f"printed {printed_val}" + (f" [{tag}]" if tag else "")
|
|
|
|
|
|
def adjudicate(legs, unit, value, block_revs):
|
|
"""The first leg that prints the unit adjudicates the claim.
|
|
|
|
`legs` is [(printed, clone, tag)] in priority order: leg 1 (the run) first,
|
|
then the selftest leg. NEITHER a `no-rev` leg NOR the selftest leg is a
|
|
verifier for a block that names a revision: one measured a different tree,
|
|
the other measured the tool's own fixtures. Such a claim is reported
|
|
REV_UNCHECKED -- visible, counted as neither a match nor a defect.
|
|
"""
|
|
for printed, clone, tag in legs:
|
|
if not printed or not keys_for(unit, printed):
|
|
continue
|
|
if block_revs and tag == "no-rev":
|
|
return "REV_UNCHECKED", ("block names %s; the tool takes no --rev, so this "
|
|
"leg measured the working tree"
|
|
% ",".join(block_revs))
|
|
if block_revs and tag == "selftest":
|
|
return "REV_UNCHECKED", ("block names %s; the selftest leg prints this "
|
|
"tool's own fixtures, not a census at that "
|
|
"revision" % ",".join(block_revs))
|
|
return oracle(printed, clone, unit, value, tag)
|
|
return "NOT_PRINTED", ""
|
|
|
|
|
|
# --------------------------------------------------------------- self-test
|
|
# Synthetic stdout, so the parsers and the verdicts are executed, not asserted.
|
|
FAKE_STDOUT = """\
|
|
corpus: 297 markdown files at babdadc in /root/troupe-standup
|
|
--- EXPLICIT `<path>:<line>` -- a CLAIM (unit: citations) ---
|
|
citations: 410
|
|
RESOLVES 113
|
|
STALE_LINE 0
|
|
--- BARE `:<line>` -- a CANDIDATE, never a claim (unit: citations) ---
|
|
citations: 1612
|
|
UNRESOLVED 471
|
|
file mentions with no line: 6961 (unit: occurrences) -- these seed the scope
|
|
index: 871 blobs (unit: blobs in THIS clone's local object store), 680 commits
|
|
overlap A&B = 20 ; ACS-only = 52
|
|
"""
|
|
FAKE_DOC = """\
|
|
THE CENSUS AT HEAD, WITH ITS UNITS (measured 2026-10-16, rev babdadc, 297 .md)
|
|
BARE 1612 citations: 255 EXTERNAL_NAMED / 471 UNRESOLVED.
|
|
Read every figure as a citation count, never as a defect count: 441 UNRESOLVED
|
|
is not 441 errors.
|
|
|
|
A SECOND BLOCK, no revision named, quoting 471 UNRESOLVED and nothing else.
|
|
"""
|
|
|
|
# Leg 2's shape: a selftest summary line, and nothing else. The cell count is
|
|
# printed here and nowhere else, which is why a claim about it needs this leg.
|
|
FAKE_SELFTEST = """\
|
|
c1 a NEW byte PASS
|
|
selftest: 18 cells, 18 passed, 0 failed
|
|
"""
|
|
|
|
|
|
def _fixture_checks():
|
|
printed, clone = parse_printed(FAKE_STDOUT)
|
|
ok = True
|
|
cells, bad = [0], [0]
|
|
|
|
def check(name, got, want):
|
|
nonlocal ok
|
|
good = got == want
|
|
cells[0] += 1
|
|
bad[0] += 0 if good else 1
|
|
ok = ok and good
|
|
print(f"{'PASS' if good else 'FAIL'} {name}")
|
|
if not good:
|
|
print(f" got {got}")
|
|
print(f" want {want}")
|
|
|
|
check("parse: UNRESOLVED single", sorted(printed.get("unresolved", {})), [471])
|
|
check("parse: citations ambiguous", sorted(printed.get("citations", {})), [410, 1612])
|
|
check("parse: count-then-unit", sorted(printed.get("occurrences", {})), [6961])
|
|
check("parse: one word skipped", sorted(printed.get("files", {})), [297])
|
|
check("parse: clone-local marked", "blobs" in clone, True)
|
|
# the phrase rule: `ACS-only = 52` must NOT register as the unit `acs`
|
|
check("parse: inline phrase key", sorted(printed.get("acs_only", {})), [52])
|
|
|
|
lines = FAKE_DOC.split("\n")
|
|
got, first_block, revs_seen = [], [], set()
|
|
for first, blk in blocks(lines, 1, len(lines)):
|
|
revs = block_revs("\n".join(r for _, r in blk))
|
|
revs_seen |= set(revs)
|
|
for ln, val, unit, _tok, _pos, _w in claims_in(blk):
|
|
got.append((unit, val))
|
|
if first == 1:
|
|
first_block.append((unit, val))
|
|
# the fixture that matters: one unit, one rev scope, two values -> a split
|
|
check("claims: both UNRESOLVED values in one block",
|
|
[uv for uv in first_block if uv[0] == "UNRESOLVED"],
|
|
[("UNRESOLVED", 471), ("UNRESOLVED", 441)])
|
|
check("claims: `471 cannot be resolved` is not a claim",
|
|
any(u in ("cannot", "resolved") for u, _ in got), False)
|
|
# the tightened separator, one cell per form it now refuses, plus the
|
|
# positive control -- a rule that refuses everything proves nothing
|
|
def claims_of(text):
|
|
return [(u, v) for _ln, v, u, _t, _p, _w in claims_in([(1, text)])]
|
|
|
|
check("claims: a welded digit-and-letter run is not a claim",
|
|
claims_of("sha 83acffe0 / rev 9188dd3 at HEAD"), [])
|
|
check("claims: an ordinal is not a claim",
|
|
claims_of("1. INTERNAL SPLIT -- no oracle, no run"), [])
|
|
check("claims: a list marker is not a claim",
|
|
claims_of("0 ran; nothing to report"), [])
|
|
check("claims: a sentence end is not a claim",
|
|
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)])
|
|
# the colon rule, 2026-10-19: a queue line number in backticks followed by a
|
|
# state word is a CITE; the positive control shows the guard is only the
|
|
# character immediately before the number, not a blanket refusal
|
|
check("claims: a line cite is not a claim",
|
|
claims_of("graded `:7303` CLOSED on Mercury's `:9869`"), [])
|
|
check("claims: a count beside a line cite is still a claim",
|
|
claims_of("open set: Q37:5278 while 13 rows close"), [("rows", 13)])
|
|
check("claims: the block's rev is read", "babdadc" in revs_seen, True)
|
|
|
|
# THE WRAPPED PAIR, 2026-10-20. A count that ends one line with its unit
|
|
# opening the next is the same claim the archive writes on one line, and
|
|
# every ADMIT text below is a real wrap copied out of the docstring it was
|
|
# measured in -- not an invented example. The four ADMIT cells are the four
|
|
# counts at HEAD; the fifth candidate, `62 notes`, is a verb and is settled
|
|
# by binding (wrap_bound, the FOLD cells below) rather than by these cells.
|
|
# The four refusal cells are the structural half, synthesized rather than
|
|
# copied because at HEAD no welded or hyphenated string survives to the
|
|
# candidate stage for a text to be lifted from.
|
|
def claims_lines(text):
|
|
blk = [(i + 1, raw) for i, raw in enumerate(text.split("\n"))]
|
|
return [(u, v, w) for _ln, v, u, _t, _p, w in claims_in(blk)]
|
|
|
|
check("wrap: a number ending a line and its unit opening the next is a claim",
|
|
claims_lines("Twenty-five of the 69\nfilenames are not the form"),
|
|
[("filenames", 69, True)])
|
|
check("wrap: a thousands separator survives the join",
|
|
claims_lines("594 BLOB_OID / 1,062\nCONTENT_SHA256 and the rest"),
|
|
[("BLOB_OID", 594, False), ("CONTENT_SHA256", 1062, True)])
|
|
check("wrap: a backtick before the number and a closing one after the unit",
|
|
claims_lines("a selftest that prints `18\ncells` -- the defect class"),
|
|
[("cells", 18, True)])
|
|
check("wrap: an adjective is a unit when it is the count's own word",
|
|
claims_lines("observed keys at 30c8120, 24\nspendable, the other 398"),
|
|
[("spendable", 24, True)])
|
|
check("wrap: a claim that does not wrap is not tagged",
|
|
claims_lines("a selftest that prints 18 cells"), [("cells", 18, False)])
|
|
check("wrap: the same text wrapped and unwrapped is two claims, not three",
|
|
claims_lines("prints 18 cells\nand 18\ncells"),
|
|
[("cells", 18, False), ("cells", 18, True)])
|
|
# the four refusals, one cell each -- the structural half no candidate at
|
|
# HEAD exercises, because the reader refuses these strings before the join
|
|
check("wrap: a date's tail is welded to its hyphens, not a count",
|
|
claims_lines("--today 2026-10-18\npython3 tools/x.py"), [])
|
|
check("wrap: `UTF-8` ends a line and is still not a count",
|
|
claims_lines("a malformed name, non-UTF-8\nbytes are refused"), [])
|
|
check("wrap: a slash-separated pair (`0/1`) is not a count",
|
|
claims_lines("exit 0/1\npython3 tools/x.py"), [])
|
|
check("wrap: a hyphen behind the joined unit is a compound adjective",
|
|
claims_lines("the queue read 65 same-party / 9\nother-party. Neither"),
|
|
[])
|
|
# THE FOLD, as a unit rule: keys_for folds a plural (it must -- a one-line
|
|
# claim and a printed label are both census voice), wrap_bound does not.
|
|
check("wrap: a plural verb is bound by no fold -- `notes` is not `note`",
|
|
wrap_bound("notes", [({"note": {7}}, set(), "")]), False)
|
|
check("wrap: and a unit printed under its own spelling IS bound",
|
|
wrap_bound("notes", [({"notes": {62}}, set(), "")]), True)
|
|
check("wrap: an uppercase label binds to the lowercase printed key",
|
|
wrap_bound("CONTENT_SHA256",
|
|
[({"content_sha256": {1062}}, set(), "")]), True)
|
|
# the digest rule, one signal per cell, and the negative control between
|
|
# them -- the text of each is the text of the defect, measured at HEAD
|
|
check("revs: `sha256 <hex>` is a digest, not the block's revision",
|
|
block_revs("queue 1,170,483 B / sha256 `c3b4a290dc64bd24`"), [])
|
|
check("revs: an ellipsis-truncated hex run is a digest, not a revision",
|
|
block_revs("at `28ca8e5` (1,193,681 B / `c3b4a290dc64bd24…`)"),
|
|
["28ca8e5"])
|
|
check("revs: an ordinarily cited revision is still the block's revision",
|
|
block_revs("Run 37 read (`29a4872`, queue 1,170,483 B) and `28ca8e5`"),
|
|
["28ca8e5", "29a4872"])
|
|
check("oracle: 471 matches", oracle(printed, clone, "UNRESOLVED", 471),
|
|
("MATCHES", ""))
|
|
check("oracle: 441 is stale", oracle(printed, clone, "UNRESOLVED", 441),
|
|
("STALE", "printed 471"))
|
|
check("oracle: ambiguous unit makes no claim",
|
|
oracle(printed, clone, "citations", 1612),
|
|
("AMBIGUOUS", "printed: 410, 1612"))
|
|
check("oracle: clone-local is not a defect",
|
|
oracle(printed, clone, "blobs", 1234), ("CLONE_LOCAL", "printed 871"))
|
|
check("oracle: unprinted unit", oracle(printed, clone, "wombats", 3),
|
|
("NOT_PRINTED", ""))
|
|
|
|
# ---- leg 2 (--selftest) and the leg rules. The first check is the defect
|
|
# the leg was added for: a docstring cell count the run leg cannot see.
|
|
s2, s2c = parse_printed(FAKE_SELFTEST)
|
|
check("selftest leg: the cell count is a printed unit", sorted(s2.get("cells", {})), [18])
|
|
legs = [(printed, clone, "")] + [(s2, s2c, "selftest")]
|
|
check("adjudicate: a cell count leg 1 cannot see is STALE, not NOT_PRINTED",
|
|
adjudicate(legs, "cells", 13, []), ("STALE", "printed 18 [selftest]"))
|
|
check("adjudicate: the corrected cell count matches",
|
|
adjudicate(legs, "cells", 18, []), ("MATCHES", ""))
|
|
check("adjudicate: leg 1 wins when both legs print the unit",
|
|
adjudicate([(s2, s2c, ""), ({**s2, "cells": {99: [1]}}, s2c, "selftest")],
|
|
"cells", 99, []), ("STALE", "printed 18"))
|
|
check("adjudicate: a no-rev leg does not verify a block that names a revision",
|
|
adjudicate([(s2, s2c, "no-rev")], "cells", 18, ["f5403b2"]),
|
|
("REV_UNCHECKED", "block names f5403b2; the tool takes no --rev, so this "
|
|
"leg measured the working tree"))
|
|
check("adjudicate: the selftest leg is not a verifier at a named revision",
|
|
adjudicate([(s2, s2c, "selftest")], "cells", 18, ["f5403b2"]),
|
|
("REV_UNCHECKED", "block names f5403b2; the selftest leg prints this "
|
|
"tool's own fixtures, not a census at that revision"))
|
|
check("adjudicate: a no-rev leg IS an oracle for a block naming no revision",
|
|
adjudicate([(s2, s2c, "no-rev")], "cells", 13, []),
|
|
("STALE", "printed 18 [no-rev]"))
|
|
check("adjudicate: no leg prints the unit -> NOT_PRINTED",
|
|
adjudicate([(printed, clone, "")], "wombats", 3, []), ("NOT_PRINTED", ""))
|
|
|
|
# ---- the recipe contract, exercised rather than asserted: run_tool takes a
|
|
# recipe's (argv, tag) and the tag must survive to the leg, because the
|
|
# tag is what decides whether a claim may be adjudicated at all. A stub
|
|
# stands in for a tool so this stays hermetic and instant; the real
|
|
# owners recipe costs a 107 s tree scan and is exercised by the run.
|
|
with tempfile.TemporaryDirectory(prefix="doccensus-selftest-") as td:
|
|
stub = os.path.join(td, "stub.py")
|
|
with open(stub, "w") as fh:
|
|
fh.write("print('cells: 7')\n")
|
|
for want_tag in ("no-rev", ""):
|
|
RECIPES["stub.py"] = (lambda t: lambda repo, rev, tmp: (["--probe"], t))(want_tag)
|
|
try:
|
|
out, rc, tag = run_tool(td, stub, "HEAD", td)
|
|
finally:
|
|
RECIPES.pop("stub.py", None)
|
|
check(f"recipe: run_tool carries a recipe's tag {want_tag!r} to the leg",
|
|
(rc, tag, "cells: 7" in out), (0, want_tag, True))
|
|
|
|
def _boom(repo_, rev_, tmp_):
|
|
raise RuntimeError("boom")
|
|
|
|
RECIPES["stub.py"] = _boom
|
|
raised = ""
|
|
try:
|
|
try:
|
|
run_tool(td, stub, "HEAD", td)
|
|
except RuntimeError as exc:
|
|
raised = str(exc)
|
|
finally:
|
|
RECIPES.pop("stub.py", None)
|
|
check("recipe: an unmaterialisable revision raises, never an empty census",
|
|
raised, "boom")
|
|
|
|
# A tool that rejects `--repo`/`--rev` but has a perfectly good default
|
|
# census. The retry must drop exactly the tokens argparse named: the
|
|
# first cut re-ran it as `<script> --repo R`, which was rejected again,
|
|
# and a working tool was reported RUN_FAILED. Both halves are pinned --
|
|
# the retry happens, and it happens only for an argparse rejection.
|
|
apstub = os.path.join(td, "stub_argparse.py")
|
|
with open(apstub, "w") as fh:
|
|
fh.write("import argparse\n"
|
|
"ap = argparse.ArgumentParser()\n"
|
|
'ap.add_argument("--selftest", action="store_true")\n'
|
|
"ap.parse_args()\n"
|
|
"print('cells: 7')\n")
|
|
out, rc, tag = run_tool(td, apstub, "HEAD", td)
|
|
check("run_tool: argparse rejection retried with the rejected flags dropped",
|
|
(rc, tag, "cells: 7" in out), (0, "no-rev", True))
|
|
|
|
rcstub = os.path.join(td, "stub_rc2.py")
|
|
with open(rcstub, "w") as fh:
|
|
fh.write("import sys\n"
|
|
"sys.stderr.write('boom\\n')\n"
|
|
"sys.exit(2)\n")
|
|
out, rc, tag = run_tool(td, rcstub, "HEAD", td)
|
|
check("run_tool: a non-argparse failure is not re-tagged no-rev",
|
|
(rc, tag, "rc=2" in out), (2, "", True))
|
|
# ---- a failed run leg does not silence its block. This path lives in
|
|
# check_tool and needs a subprocess, so it is exercised end to end with
|
|
# a stub tool rather than asserted: the stub rejects every census
|
|
# invocation with an argparse diagnosis and answers only `--selftest`.
|
|
with tempfile.TemporaryDirectory(prefix="doccensus-selftest-") as td:
|
|
stub = os.path.join(td, "stub_failing_run.py")
|
|
with open(stub, "w") as fh:
|
|
fh.write("'''stub: a run leg that fails, a selftest that answers.\n"
|
|
"\n7 cells\n'''\n"
|
|
"import sys\n"
|
|
'if "--selftest" in sys.argv:\n'
|
|
' print("selftest: 7 cells, 7 passed, 0 failed")\n'
|
|
"else:\n"
|
|
' sys.stderr.write("usage: stub_failing_run.py [-h] [--selftest]\\n")\n'
|
|
" sys.exit(2)\n")
|
|
rep = check_tool(td, stub, "stub_failing_run.py", {}, run=True, tmpdir=td)
|
|
check("check_tool: a failed run leg is reported, and leg 2 still adjudicates",
|
|
([f["verdict"] for f in rep["failures"]],
|
|
[(v["unit"], v["claim"], v["verdict"]) for v in rep["verdicts"]]),
|
|
(["RUN_FAILED"], [("cells", 7, "MATCHES")]))
|
|
|
|
# ---- the banner's arithmetic, which is what a reader actually reads: a
|
|
# RUN_FAILED leg and a STALE claim are counted in two columns, so four
|
|
# failed runs cannot read as four bad claims.
|
|
synthetic = {"tool": "t", "splits": [], "unchecked": {}, "selftest_leg": True,
|
|
"failures": [{"verdict": "RUN_FAILED"}],
|
|
"verdicts": [{"verdict": "MATCHES"}, {"verdict": "STALE"}]}
|
|
check("counts: a leg failure is not a claim defect", counts(synthetic)[:4],
|
|
(0, 1, 1, 2))
|
|
check("counts: an absent leg 2 is noted",
|
|
counts({**synthetic, "selftest_leg": False})[4],
|
|
["no --selftest: leg 2 absent"])
|
|
|
|
# --tools: a filter that matches nothing must not read as a clean scan.
|
|
check("tools: a bare basename is reported missing, not scanned as zero",
|
|
resolve_tools(["checkin_check.py"], {"tools/checkin_check.py"}),
|
|
(["checkin_check.py"], ["checkin_check.py"]))
|
|
check("tools: no filter means every tracked tool and nothing missing",
|
|
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))
|
|
|
|
# ---- the mode symmetry (reply 22, 2026-10-18). A split is graded only when
|
|
# a leg verified its unit; without a run there is no oracle, so the
|
|
# same docstring that fails a full run must NOT fail `--no-run`. Both
|
|
# halves are pinned: the advisory side, and the teeth that survive.
|
|
check("mode: --no-run can only label a split, never verify it",
|
|
unit_scope("UNRESOLVED", [], True), "LABEL_ONLY")
|
|
check("mode: a unit no leg prints is not a census unit",
|
|
unit_scope("WOMBATS", [({"widgets": {7: [1]}}, {}, "")], False),
|
|
"NOT_A_CENSUS_UNIT")
|
|
with tempfile.TemporaryDirectory(prefix="doccensus-mode-") as td:
|
|
split_stub = os.path.join(td, "stub_split.py")
|
|
with open(split_stub, "w") as fh:
|
|
fh.write('"""A stub whose docstring splits one unit.\n'
|
|
"\n471 UNRESOLVED at the head.\n" + "pad " * 80 + "\n"
|
|
'441 UNRESOLVED at the tail.\n"""\n'
|
|
"print('UNRESOLVED: 471')\n")
|
|
rep_norun = check_tool(td, split_stub, "stub_split.py", {}, run=False,
|
|
tmpdir=td)
|
|
rep_run = check_tool(td, split_stub, "stub_split.py", {}, run=True,
|
|
tmpdir=td)
|
|
check("mode: --no-run prints the split, graded as none",
|
|
([s["unit"] for s in rep_norun["splits"]],
|
|
[(s["unit"], s["how"]) for s in rep_norun["advisory"]]),
|
|
([], [("UNRESOLVED", "LABEL_ONLY")]))
|
|
check("mode: the same split is CHECKABLE once a leg prints the unit",
|
|
[(s["unit"], s["how"]) for s in rep_run["splits"]],
|
|
[("UNRESOLVED", "CHECKABLE")])
|
|
check("mode: --fail cannot fail on a split it could not verify",
|
|
gate_rc([rep_norun], True), 0)
|
|
check("mode: and still fails on the verified one", gate_rc([rep_run], True), 1)
|
|
# a cached vocabulary grades unit-hood without a run -- and only when it
|
|
# names the unit, which is the whole difference between a proof and a
|
|
# guess (reply 22, option 2)
|
|
check("vocab: a cached census unit is recognised",
|
|
vocab_has("UNRESOLVED", {"unresolved", "citations"}), True)
|
|
check("vocab: a unit the vocabulary does not name is not",
|
|
vocab_has("WOMBATS", {"unresolved"}), False)
|
|
rep_cached = check_tool(td, split_stub, "stub_split.py", {}, run=False,
|
|
tmpdir=td, vocab={"unresolved"}, vocab_rev="abc1234")
|
|
check("vocab: the same runless split grades as CACHE_CHECKABLE",
|
|
[(s["unit"], s["how"], s["oracle_rev"]) for s in rep_cached["splits"]],
|
|
[("UNRESOLVED", "CACHE_CHECKABLE", "abc1234")])
|
|
check("vocab: and the gate spends it", gate_rc([rep_cached], True), 1)
|
|
rep_other = check_tool(td, split_stub, "stub_split.py", {}, run=False,
|
|
tmpdir=td, vocab={"wombats"}, vocab_rev="abc1234")
|
|
check("vocab: a vocabulary naming another unit leaves it advisory",
|
|
([s["unit"] for s in rep_other["splits"]], gate_rc([rep_other], True)),
|
|
([], 0))
|
|
|
|
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")
|
|
with open(os.path.join(td, "tools", "stub_split.py"), "w") as fh:
|
|
fh.write('"""A stub whose docstring splits one unit.\n'
|
|
"\n471 UNRESOLVED at the head.\n" + "pad " * 80 + "\n"
|
|
'441 UNRESOLVED at the tail.\n"""\n'
|
|
"print('UNRESOLVED: 471')\n")
|
|
# THE WRAPPED PAIR, three stubs that differ only in whether a leg can
|
|
# bind the unit: agreeing, disagreeing, and a wrap that is prose.
|
|
with open(os.path.join(td, "tools", "stub_wrap_ok.py"), "w") as fh:
|
|
fh.write('"""A stub whose claim wraps onto the next line.\n'
|
|
"\nWe measured 7\n"
|
|
'widgets at HEAD.\n"""\n'
|
|
"print('widgets: 7')\n")
|
|
with open(os.path.join(td, "tools", "stub_wrap_stale.py"), "w") as fh:
|
|
fh.write('"""A stub whose wrapped claim disagrees with its census.\n'
|
|
"\nWe measured 9\n"
|
|
'widgets at HEAD.\n"""\n'
|
|
"print('widgets: 7')\n")
|
|
with open(os.path.join(td, "tools", "stub_wrap_unbound.py"), "w") as fh:
|
|
fh.write('"""A stub whose wrap is prose, not a census.\n'
|
|
"\nIt exited 0\n"
|
|
'without a census.\n"""\n'
|
|
"print('widgets: 7')\n")
|
|
# THE FOLD, end to end -- the false fault measured on the real archive at
|
|
# HEAD (queue_claim_check.py:119, `reply 62 / notes it`). `notes` folds to
|
|
# the printed key `note`, which the loose printed reader picked out of a
|
|
# PASS line's prose, so a plural fold would spend this sentence and grade
|
|
# it STALE. The pair must land as unbound instead.
|
|
with open(os.path.join(td, "tools", "stub_wrap_fold.py"), "w") as fh:
|
|
fh.write('"""A stub whose wrap is a verb that folds onto a key.\n'
|
|
"\nThe queue's own reply 62\n"
|
|
'notes it).\n"""\n'
|
|
"print('note: 7')\n")
|
|
# ...and the boundary: the SAME text on ONE line is still a claim, still
|
|
# folded, still graded -- the narrowing is the wrap's price, not a new
|
|
# rule for the adjacency reader.
|
|
with open(os.path.join(td, "tools", "stub_note_adjacent.py"), "w") as fh:
|
|
fh.write('"""A stub whose one-line claim folds onto a key.\n'
|
|
"\nThe queue's own reply 62 notes it).\n\"\"\"\n"
|
|
"print('note: 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)
|
|
# the mode pair, as PROCESS exit codes: same tree, same split, the two
|
|
# modes must not disagree about what they verified
|
|
check("gate e2e: --fail --no-run on a split is rc 0 (no oracle, advisory)",
|
|
run_gate("--tools", "tools/stub_split.py", "--no-run", "--fail"), 0)
|
|
check("gate e2e: --fail with the run grades that same split (rc 1)",
|
|
run_gate("--tools", "tools/stub_split.py", "--fail"), 1)
|
|
# THE WRAPPED PAIR, END TO END. Three stubs whose only difference is
|
|
# whether a leg prints the unit the wrap names, so these cells grade the
|
|
# SPENDING rule and not just the reading rule above.
|
|
check("gate e2e: a wrapped claim a leg binds, agreeing, is rc 0",
|
|
run_gate("--tools", "tools/stub_wrap_ok.py", "--fail"), 0)
|
|
check("gate e2e: and its wrapped VALUE is graded -- disagreeing is rc 1",
|
|
run_gate("--tools", "tools/stub_wrap_stale.py", "--fail"), 1)
|
|
check("gate e2e: a wrapped pair no leg binds is not a claim (rc 0)",
|
|
run_gate("--tools", "tools/stub_wrap_unbound.py", "--fail"), 0)
|
|
check("gate e2e: --no-run spends no wrap and fails on none (rc 0)",
|
|
run_gate("--tools", "tools/stub_wrap_stale.py", "--no-run",
|
|
"--fail"), 0)
|
|
# THE FOLD: a plural verb must not be bound to a singular printed key by
|
|
# a wrap, and the same text on one line must still be graded.
|
|
check("gate e2e: a wrapped verb is NOT bound by a plural fold (rc 0)",
|
|
run_gate("--tools", "tools/stub_wrap_fold.py", "--fail"), 0)
|
|
check("gate e2e: the same words on one line ARE folded and graded (rc 1)",
|
|
run_gate("--tools", "tools/stub_note_adjacent.py", "--fail"), 1)
|
|
# the process exit code through the vocabulary, both directions, plus
|
|
# the two ways a vocabulary can fail to be usable at all
|
|
vpath = os.path.join(td, "vocab-names-unit.json")
|
|
with open(vpath, "w") as fh:
|
|
json.dump({"rev": "abc1234",
|
|
"tools": {"tools/stub_split.py": ["unresolved"]}}, fh)
|
|
check("gate e2e: --no-run --vocab grades a split the vocabulary names (rc 1)",
|
|
run_gate("--tools", "tools/stub_split.py", "--no-run",
|
|
"--vocab", vpath, "--fail"), 1)
|
|
vother = os.path.join(td, "vocab-other-unit.json")
|
|
with open(vother, "w") as fh:
|
|
json.dump({"rev": "abc1234",
|
|
"tools": {"tools/stub_split.py": ["wombats"]}}, fh)
|
|
check("gate e2e: a vocabulary that does not name the unit leaves rc 0",
|
|
run_gate("--tools", "tools/stub_split.py", "--no-run",
|
|
"--vocab", vother, "--fail"), 0)
|
|
check("gate e2e: an unreadable --vocab is rc 2, not 0 and not 1",
|
|
run_gate("--tools", "tools/stub_split.py", "--no-run",
|
|
"--vocab", os.path.join(td, "nope.json"), "--fail"), 2)
|
|
check("gate e2e: --save-vocab without a run is rc 2",
|
|
run_gate("--tools", "tools/stub_split.py", "--no-run",
|
|
"--save-vocab", os.path.join(td, "v0.json"), "--fail"), 2)
|
|
# the round trip a gate runner would actually run: a full run writes the
|
|
# vocabulary it observed, the fast mode reads it back and still fails
|
|
vrt = os.path.join(td, "vocab-roundtrip.json")
|
|
check("gate e2e: a full run writes the vocabulary it observed (rc 1)",
|
|
run_gate("--tools", "tools/stub_split.py", "--save-vocab", vrt,
|
|
"--fail"), 1)
|
|
with open(vrt) as fh:
|
|
check("gate e2e: the written vocabulary names the unit the leg printed",
|
|
sorted(json.load(fh)["tools"].get("tools/stub_split.py", [])),
|
|
["unresolved"])
|
|
check("gate e2e: the round trip fails the fast mode on the same split",
|
|
run_gate("--tools", "tools/stub_split.py", "--no-run",
|
|
"--vocab", vrt, "--fail"), 1)
|
|
|
|
# ---- the saved vocabulary is SCOPED to what a claim can spend, and the
|
|
# fast mode reports the splits it cannot grade (2026-10-18). The
|
|
# first three cells pin the matcher identity that makes the scoping
|
|
# safe; the last six pin the process, because a helper returning a
|
|
# set proves nothing about the file a runner reads.
|
|
check("vocab scope: a claimed unit expands the way keys_for reads it",
|
|
sorted(spendable_keys(["citations"])), ["citation", "citations"])
|
|
check("vocab scope: every key it admits is one vocab_has can spend",
|
|
all(vocab_has("citations", {k})
|
|
for k in spendable_keys(["citations"])), True)
|
|
check("vocab scope: a digest no docstring claims is not spendable",
|
|
"abcdef12" in spendable_keys(["UNRESOLVED", "citations"]), False)
|
|
with open(os.path.join(td, "tools", "stub_vocab.py"), "w") as fh:
|
|
fh.write('"""A stub that prints its census and a digest.\n\n'
|
|
'It found 7 widgets.\n"""\n'
|
|
"print('widgets: 7')\nprint('abcdef12: 3')\n")
|
|
with open(os.path.join(td, "tools", "stub_lower.py"), "w") as fh:
|
|
fh.write('"""A stub whose docstring splits a lowercase word.\n\n'
|
|
"We did 5 above the fold.\n" + "pad " * 80 + "\n"
|
|
'Then 7 above the fold.\n"""\nprint(\'above: 3\')\n')
|
|
for sub in (["add", "-A"], ["commit", "-qm", "vocab scope"]):
|
|
subprocess.run(["git", "-C", td] + sub, capture_output=True, env=genv)
|
|
vscope = os.path.join(td, "vocab-scope.json")
|
|
check("gate e2e: --save-vocab on a stub printing a digest (rc 0)",
|
|
run_gate("--tools", "tools/stub_vocab.py", "--save-vocab", vscope,
|
|
"--fail"), 0)
|
|
with open(vscope) as fh:
|
|
check("gate e2e: the saved vocabulary holds the claimed unit only",
|
|
json.load(fh)["tools"].get("tools/stub_vocab.py"), ["widgets"])
|
|
vlow = os.path.join(td, "vocab-lower.json")
|
|
with open(vlow, "w") as fh:
|
|
json.dump({"rev": "abc1234",
|
|
"tools": {"tools/stub_lower.py": ["above"]}}, fh)
|
|
check("gate e2e: a claimed lowercase word IS spendable -- the over-reach",
|
|
run_gate("--tools", "tools/stub_lower.py", "--no-run",
|
|
"--vocab", vlow, "--fail"), 1)
|
|
|
|
def run_gate_out(*extra):
|
|
p = subprocess.run([sys.executable, os.path.abspath(__file__),
|
|
"--repo", td, "--rev", "HEAD"] + list(extra),
|
|
capture_output=True, env=genv)
|
|
return p.returncode, p.stdout.decode("utf-8", "replace")
|
|
|
|
rc_low, out_low = run_gate_out("--tools", "tools/stub_lower.py",
|
|
"--no-run", "--fail")
|
|
check("gate e2e: a runless lowercase split is rc 0 (nothing verified)",
|
|
rc_low, 0)
|
|
check("gate e2e: and it is PRINTED as advisory, not swallowed",
|
|
[l.strip() for l in out_low.splitlines()
|
|
if l.strip().startswith("ADVISORY above (no census label")],
|
|
["ADVISORY above (no census label and no cached vocabulary): "
|
|
"5@3, 7@5 -- reported, not graded"])
|
|
# A WRAP IS NEVER SILENT: the banner says how many pairs were read
|
|
# across a break and how many were spent, on both outcomes.
|
|
_rc, out_wb = run_gate_out("--tools", "tools/stub_wrap_ok.py")
|
|
check("gate e2e: the banner says the wrap was read and spent",
|
|
[l.strip() for l in out_wb.splitlines()
|
|
if l.strip().startswith("wrapped:")],
|
|
["wrapped: 1 pair(s) read across a line break, 1 spent, "
|
|
"0 not a claim (no leg prints the unit)"])
|
|
_rc, out_wu = run_gate_out("--tools", "tools/stub_wrap_unbound.py")
|
|
check("gate e2e: an unbound wrap is counted and named, not dropped",
|
|
[l.strip() for l in out_wu.splitlines()
|
|
if l.strip().startswith(("wrapped:", "WRAP_UNBOUND"))],
|
|
["wrapped: 1 pair(s) read across a line break, 0 spent, "
|
|
"1 not a claim (no leg prints the unit)",
|
|
"WRAP_UNBOUND tools/stub_wrap_unbound.py:3 0 without -- the "
|
|
"unit is printed by no leg"])
|
|
_rc, out_wf = run_gate_out("--tools", "tools/stub_wrap_fold.py")
|
|
check("gate e2e: a folded verb is named UNBOUND, not graded STALE",
|
|
[l.strip() for l in out_wf.splitlines()
|
|
if l.strip().startswith(("wrapped:", "WRAP_UNBOUND", "STALE"))],
|
|
["wrapped: 1 pair(s) read across a line break, 0 spent, "
|
|
"1 not a claim (no leg prints the unit)",
|
|
"WRAP_UNBOUND tools/stub_wrap_fold.py:3 62 notes -- the unit "
|
|
"is printed by no leg"])
|
|
|
|
print(f"selftest: {cells[0]} cells, {cells[0] - bad[0]} passed, {bad[0]} failed")
|
|
return 0 if ok else 1
|
|
|
|
|
|
# ------------------------------------------------------------------- driver
|
|
def run_tool(repo, script, rev, tmpdir, timeout=600):
|
|
"""(stdout, rc, tag) for the tool's own run at `rev`.
|
|
|
|
`tag` is "" when the leg stood on the revision it was asked for and "no-rev"
|
|
when it did not: either the tool rejected `--rev` and was re-run without it,
|
|
or a recipe in RECIPES declared that it could not materialise the revision.
|
|
Such a leg measures the tree it is pointed at, so a claim in a block that
|
|
names a revision is reported REV_UNCHECKED rather than adjudicated (see
|
|
check_tool). stdout is returned; stderr is only folded in when there is no
|
|
stdout at all, so an argparse diagnosis is visible as a failure instead of
|
|
parsed as a census.
|
|
"""
|
|
recipe = RECIPES.get(os.path.basename(script), None)
|
|
if recipe is None:
|
|
argv, tag = [sys.executable, script, "--repo", repo, "--rev", rev], ""
|
|
else:
|
|
args, tag = recipe(repo, rev, tmpdir)
|
|
argv = [sys.executable, script] + args
|
|
p = subprocess.run(argv, capture_output=True, timeout=timeout)
|
|
out = p.stdout.decode("utf-8", "replace")
|
|
err = p.stderr.decode("utf-8", "replace")
|
|
# A tool that takes neither `--repo` nor `--rev` answers with an argparse
|
|
# diagnosis naming the flags it rejected, and NOTHING else: measured
|
|
# 2026-10-16, `check_lint.py --repo R --rev HEAD` exits 2 with stdout empty
|
|
# and `unrecognized arguments: --repo R --rev HEAD`, and the first cut of
|
|
# this rule re-ran it as `<script> --repo R` -- still rejected, still rc 2,
|
|
# so a tool with a perfectly good default census was reported RUN_FAILED.
|
|
# The retry therefore drops the tokens argparse itself named, and only
|
|
# those; a tool whose values collide with a dropped flag is a stated
|
|
# limitation, not a silent one.
|
|
m = re.search(r"unrecognized arguments:\s*(.+)$", err, re.M)
|
|
if tag != "no-rev" and p.returncode != 0 and m:
|
|
drop = set(m.group(1).split())
|
|
argv = [x for x in argv if x not in drop]
|
|
p = subprocess.run(argv, capture_output=True, timeout=timeout)
|
|
out = p.stdout.decode("utf-8", "replace")
|
|
err = p.stderr.decode("utf-8", "replace")
|
|
tag = "no-rev"
|
|
if not out.strip():
|
|
out = "<no stdout; rc=%d> %s" % (p.returncode, err[:200].replace("\n", " "))
|
|
return out, p.returncode, tag
|
|
|
|
|
|
def run_selftest(script, timeout=300):
|
|
"""Leg 2: the tool's own `--selftest`, or None if it takes no such flag.
|
|
|
|
A cell count is printed here and nowhere else, which is the whole reason
|
|
this leg exists. `None` means the tool has no selftest to offer -- reported
|
|
as one leg, not as a failure.
|
|
"""
|
|
try:
|
|
p = subprocess.run([sys.executable, script, "--selftest"],
|
|
capture_output=True, timeout=timeout)
|
|
except subprocess.TimeoutExpired:
|
|
return "<--selftest timed out>"
|
|
out = p.stdout.decode("utf-8", "replace")
|
|
err = p.stderr.decode("utf-8", "replace")
|
|
if "unrecognized arguments" in err and "--selftest" in err:
|
|
return None
|
|
if not out.strip():
|
|
return "<no stdout; rc=%d> %s" % (p.returncode, err[:200].replace("\n", " "))
|
|
return out
|
|
|
|
|
|
def unit_scope(unit, legs, no_run):
|
|
"""Is a split in this unit checkable, and why not if it is not?
|
|
|
|
With the oracle legs in hand a unit is checkable when some leg prints it
|
|
with exactly ONE value: the docstring and the output then talk about one
|
|
number. A unit printed with 2+ values is SECTION-SPLIT BY THE TOOL'S OWN
|
|
DESIGN (`citations` prints once under EXPLICIT and once under BARE), so a
|
|
docstring that mirrors both is correct and is reported as such, not flagged.
|
|
A unit no leg prints is not a census unit here. Without a run, only
|
|
uppercase census labels are checked -- and a cached vocabulary (see
|
|
vocab_has) can widen that, which is why the cache decision is taken in
|
|
check_tool, not here: this function answers about the legs it was given.
|
|
"""
|
|
if no_run or not legs:
|
|
return ("LABEL_ONLY" if is_label(unit) else "UNCHECKED_NO_ORACLE")
|
|
saw_split = False
|
|
for printed, clone, _tag in legs:
|
|
vals = set()
|
|
for k in keys_for(unit, printed or {}):
|
|
vals |= set((printed or {})[k])
|
|
if not vals:
|
|
continue
|
|
if len(vals) > 1:
|
|
saw_split = True
|
|
continue
|
|
return "CHECKABLE"
|
|
if saw_split:
|
|
return "SECTION_SPLIT_BY_TOOL"
|
|
return "NOT_A_CENSUS_UNIT"
|
|
|
|
|
|
def wrap_bound(unit, legs):
|
|
"""True when some leg prints this unit under its OWN spelling.
|
|
|
|
This is keys_for WITHOUT the plural fold, and the narrowing is the whole
|
|
point. `keys_for` tries `unit.lower().rstrip("s")` because a one-line claim
|
|
and a printed label are both hand-written census voice (`18 claims` vs
|
|
`claim: 18`), so folding the plural costs nothing there. A WRAP IS NOT
|
|
HAND-WRITTEN CENSUS VOICE -- it is two lines this reader hypothesised into
|
|
one claim, and the word that opens the second line is a word in a sentence
|
|
far more often than it is a label: `the queue's own reply 62 / notes it`
|
|
folds `notes` onto a printed key `note`, and the tool printed that key only
|
|
because `parse_printed`'s loose reader took it out of the prose of its own
|
|
PASS line (`PASS v2 loose key: a Q7 continuation note is not an opening
|
|
record`). Measured 2026-10-20: that fold spent the pair and graded the
|
|
docstring STALE 62-vs-7 -- a false fault about a correct sentence.
|
|
So a wrap binds on the unit's own spelling or not at all; a pair that only a
|
|
fold would bind is printed as WRAP_UNBOUND and never graded, which is the
|
|
same landing place as a pair no leg touches.
|
|
"""
|
|
u = unit.lower()
|
|
return any(u in (printed or {}) for printed, _clone, _tag in legs)
|
|
|
|
|
|
def vocab_has(unit, vocab):
|
|
"""True when a cached census vocabulary names this unit word.
|
|
|
|
The vocabulary is a set of the unit KEYS a full run observed (`unresolved`,
|
|
`citations`, `acs_only` ...), and `keys_for` is the same matcher the oracle
|
|
uses -- so a cache hit means exactly "a leg printed this unit under this
|
|
key", no letter of the rule re-invented for the cache.
|
|
"""
|
|
return bool(keys_for(unit, {u: [0] for u in (vocab or ())}))
|
|
|
|
|
|
def spendable_keys(units):
|
|
"""The vocabulary keys a claim on one of `units` can actually spend.
|
|
|
|
`vocab_has` answers `keys_for(unit, ...)`, so the set of keys any claim in a
|
|
docstring can reach is fixed: exactly the four spellings `keys_for` tries,
|
|
lowercased, for the units that docstring claims. A saved vocabulary entry
|
|
outside that set can never be consulted -- it is a permission nobody can
|
|
use -- so `--save-vocab` writes the intersection of what the legs observed
|
|
with this set. Same matcher on both sides, no second rule invented.
|
|
"""
|
|
out = set()
|
|
for u in units or ():
|
|
for c in (u, u.lower(), u.lower().rstrip("s"), u.upper()):
|
|
out.add(c.lower())
|
|
return out
|
|
|
|
|
|
def check_tool(repo, script, name, cache, run=True, tmpdir=".", isolation=240,
|
|
vocab=None, vocab_rev=None):
|
|
text = open(script, encoding="utf-8", errors="replace").read()
|
|
span = docstring_span(script, text)
|
|
if span is None:
|
|
return None
|
|
lines = text.split("\n")
|
|
lo, hi = span
|
|
blk_claims = []
|
|
for first, blk in blocks(lines, lo, hi):
|
|
btext = "\n".join(raw for _, raw in blk)
|
|
revs = block_revs(btext)
|
|
cl = claims_in(blk)
|
|
if cl:
|
|
blk_claims.append((first, revs, cl, btext))
|
|
|
|
# ---- the legs first. Leg 1: the tool's own run, once per revision the
|
|
# docstring names. Leg 2: its --selftest, once. The tool under test is
|
|
# never this tool: running this scanner on itself spawns it, which
|
|
# spawns it again, without a floor.
|
|
is_self = os.path.basename(script) == os.path.basename(os.path.abspath(__file__))
|
|
printed_by_rev, verdicts, failures = {}, [], []
|
|
selftest_leg = None
|
|
if run and not is_self:
|
|
for first, revs, cl, _b in blk_claims:
|
|
for rev in (revs or ["HEAD"]):
|
|
key = (name, rev)
|
|
if key not in cache:
|
|
try:
|
|
out, rc, tag = run_tool(repo, script, rev, tmpdir)
|
|
cache[key] = (parse_printed(out), rc, out, tag)
|
|
except Exception as exc: # noqa: BLE001
|
|
cache[key] = (None, None, f"<run failed: {exc}>", "")
|
|
printed_by_rev.setdefault((first, rev), cache[key])
|
|
skey = (name, "<selftest>")
|
|
if skey not in cache:
|
|
try:
|
|
sout = run_selftest(script)
|
|
except Exception as exc: # noqa: BLE001
|
|
sout = f"<selftest failed: {exc}>"
|
|
cache[skey] = (parse_printed(sout) if sout else None,
|
|
0 if sout else None, sout or "", "selftest")
|
|
selftest_leg = cache[skey]
|
|
|
|
def legs_for(first, rev):
|
|
"""Priority-ordered [(printed, clone, tag)] for one block scope."""
|
|
out = []
|
|
entry = printed_by_rev.get((first, rev))
|
|
if entry and entry[0]:
|
|
out.append((entry[0][0], entry[0][1], entry[3]))
|
|
if selftest_leg and selftest_leg[0]:
|
|
out.append((selftest_leg[0][0], selftest_leg[0][1], "selftest"))
|
|
return out
|
|
|
|
# A WRAPPED PAIR IS SPENT ONLY WHEN A LEG CAN BIND ITS UNIT. Joining a line
|
|
# break is a reading decision and it is the only place in this tool where a
|
|
# claim is manufactured out of two lines instead of found inside one, so it
|
|
# gets the tool's own test for census-hood rather than a lexicon of English:
|
|
# a unit some leg prints IS a unit; a word that merely opens a sentence is
|
|
# not, and the difference is measured on the tool's output rather than
|
|
# asserted in a word list. Measured at HEAD, spent: `1,062 CONTENT_SHA256`
|
|
# (citation_hash_class.py:53). Measured at HEAD, NOT spent and named in the
|
|
# banner: `69 filenames` (checkin_check.py:14, no leg prints `filenames`),
|
|
# `18 cells`, `24 spendable` and `0 without` (doc_census_check.py self), and
|
|
# `62 notes` (queue_claim_check.py:119, `reply 62 / notes it` -- prose, and
|
|
# only a plural fold would have bound it; see wrap_bound). Nothing is dropped
|
|
# silently -- every unbound pair is counted and printed (the `wrapped:` line
|
|
# in the banner), because a candidate that vanishes is the defect this rule
|
|
# exists to close. With no run there is no leg, so no wrap is spent and every
|
|
# one is reported unbound: a mode that cannot see a census cannot bind a
|
|
# claim, which is the same contract unit_scope already applies to a split.
|
|
wrap_candidates, wrap_unbound = 0, []
|
|
if any(w for _f, _r, cl, _b in blk_claims for _ln, _v, _u, _t, _p, w in cl):
|
|
kept_blocks = []
|
|
for first, revs, cl, btext in blk_claims:
|
|
rows = []
|
|
for ln, val, unit, tok, pos, wrapped in cl:
|
|
if not wrapped:
|
|
rows.append((ln, val, unit, tok, pos, wrapped))
|
|
continue
|
|
wrap_candidates += 1
|
|
bound = any(wrap_bound(unit, legs_for(first, r))
|
|
for r in (revs or ["HEAD"]))
|
|
if bound:
|
|
rows.append((ln, val, unit, tok, pos, wrapped))
|
|
else:
|
|
wrap_unbound.append({"file": name, "line": ln, "unit": unit,
|
|
"claim": val, "token": tok})
|
|
if rows:
|
|
kept_blocks.append((first, revs, rows, btext))
|
|
blk_claims = kept_blocks
|
|
|
|
# 1. INTERNAL SPLIT: one unit, one revision scope, two values, far enough
|
|
# apart to be a restatement rather than a neighbouring sub-count.
|
|
splits, advisory, unchecked = [], [], collections.Counter()
|
|
by_unit = collections.defaultdict(list)
|
|
for first, revs, cl, _b in blk_claims:
|
|
scope = tuple(revs)
|
|
for ln, val, unit, _t, pos, _w in cl:
|
|
by_unit[(unit.upper() if unit.isupper() else unit.lower(), scope)].append(
|
|
(first, ln, val, pos))
|
|
for (unit, scope), items in sorted(by_unit.items()):
|
|
vals = {v for _, _, v, _ in items}
|
|
if len(vals) < 2:
|
|
continue
|
|
# a restatement is a pair far apart; an enumeration is neighbours
|
|
far = any(abs(b[3] - a[3]) >= isolation
|
|
for a, b in [(x, y) for x in items for y in items]
|
|
if a[1] != b[1] and a[2] != b[2])
|
|
if not far:
|
|
continue
|
|
rev_for_oracle = (items[0][0], scope[0] if len(scope) == 1 else "HEAD")
|
|
why = ("SELF_NOT_RUN" if is_self and run
|
|
else unit_scope(unit, legs_for(*rev_for_oracle), not run))
|
|
oracle_rev = None
|
|
# A CACHED VOCABULARY WIDENS WHAT A RUNLESS MODE MAY GRADE, and only in
|
|
# the one direction it can prove: a unit some leg printed at the
|
|
# vocabulary's rev is a census unit, and the disagreement here is
|
|
# internal to this docstring, so the leg's VALUE is never needed. It is
|
|
# consulted only when nothing ran -- with a run, a unit the live leg
|
|
# does not print is evidence AGAINST census-hood and the cache must not
|
|
# overrule it. A STALE claim never enters through here: `why` only
|
|
# decides splits.
|
|
if why in ("LABEL_ONLY", "UNCHECKED_NO_ORACLE") and not run \
|
|
and vocab_has(unit, vocab):
|
|
why, oracle_rev = "CACHE_CHECKABLE", vocab_rev
|
|
row = {"unit": unit, "revs": list(scope), "values": sorted(vals),
|
|
"how": why, "oracle_rev": oracle_rev,
|
|
"rows": [{"line": ln, "value": v, "block": b}
|
|
for b, ln, v, _ in items]}
|
|
# THE ONLY DOOR INTO `splits`. A split is graded only when a leg ran and
|
|
# printed its unit with exactly one value (CHECKABLE), or when a cached
|
|
# vocabulary proves the unit is a census unit and no run happened
|
|
# (CACHE_CHECKABLE): then the docstring and the census are talking about
|
|
# one number and a disagreement is a defect the gate can verify. Every
|
|
# other reason -- no oracle at all (LABEL_ONLY, --no-run), a unit no leg
|
|
# prints (NOT_A_CENSUS_UNIT), a unit the tool itself prints per section
|
|
# (SECTION_SPLIT_BY_TOOL), this tool reading itself (SELF_NOT_RUN) -- is
|
|
# reported with its values and never reaches the gate. Decided 2026-10-18
|
|
# (reply 22) after `--fail --no-run` returned 1 on the 471/441 split at
|
|
# fca3f2e while the full run returned 0 for the same units it could not
|
|
# see: rc 1 must never mean "I could not check this".
|
|
if why not in ("CHECKABLE", "CACHE_CHECKABLE"):
|
|
unchecked[why] += 1
|
|
advisory.append(row)
|
|
continue
|
|
splits.append(row)
|
|
|
|
# 2. ORACLE: every claim compared against what the tool prints, leg 1 first.
|
|
if run:
|
|
for first, revs, cl, _b in blk_claims:
|
|
for rev in (revs or ["HEAD"]):
|
|
if is_self:
|
|
for ln, val, unit, tok, _pos, _w in cl:
|
|
verdicts.append({"file": name, "block": first, "line": ln,
|
|
"rev": rev, "unit": unit, "claim": val,
|
|
"token": tok, "verdict": "SELF_NOT_RUN",
|
|
"detail": "the tool under test is this tool"})
|
|
continue
|
|
pr, rc, out, _tag = printed_by_rev[(first, rev)]
|
|
printed, clone = pr if pr else (None, None)
|
|
legs = legs_for(first, rev)
|
|
# A leg with no census -- raised, or exited non-zero having
|
|
# printed nothing -- adjudicates nothing either: drop it and let
|
|
# whichever leg survives answer instead. Only when none survives
|
|
# is the block left unadjudicated.
|
|
no_census = pr is None or (rc not in (0, None)
|
|
and not any(printed.values()))
|
|
if pr is None:
|
|
failures.append({"file": name, "block": first, "rev": rev,
|
|
"unit": "-", "claim": None,
|
|
"verdict": "RUN_FAILED", "detail": str(out)[:90]})
|
|
elif no_census:
|
|
failures.append({"file": name, "block": first, "rev": rev,
|
|
"unit": "-", "claim": None,
|
|
"verdict": "RUN_FAILED",
|
|
"detail": "rc=%s, nothing printed: %s"
|
|
% (rc, str(out)[:80])})
|
|
if no_census:
|
|
legs = [l for l in legs if l[2] == "selftest"]
|
|
if not legs:
|
|
continue
|
|
for ln, val, unit, tok, _pos, _w in cl:
|
|
v, detail = adjudicate(legs, unit, val, revs)
|
|
verdicts.append({"file": name, "block": first, "line": ln,
|
|
"rev": rev, "unit": unit, "claim": val,
|
|
"token": tok, "verdict": v, "detail": detail})
|
|
return {"tool": name, "splits": splits, "advisory": advisory,
|
|
"unchecked": dict(unchecked),
|
|
"claim_units": sorted({(u.upper() if u.isupper() else u.lower())
|
|
for _f, _r, cl, _b in blk_claims
|
|
for _ln, _v, u, _t, _p, _w in cl}),
|
|
"failures": failures, "verdicts": verdicts,
|
|
"wrap_candidates": wrap_candidates, "wrap_unbound": wrap_unbound,
|
|
"self": is_self and run, "selftest_leg": (not run) or bool(selftest_leg and selftest_leg[0]),
|
|
"blocks_with_claims": len(blk_claims)}
|
|
|
|
|
|
def counts(rep):
|
|
"""(splits, claim_defects, leg_failures, claims, notes) -- the banner's rows.
|
|
|
|
A LEG FAILURE IS NOT A CLAIM DEFECT. RUN_FAILED says no census came back and
|
|
such a leg adjudicates nothing (see check_tool); STALE says a claim was read
|
|
against a census and disagreed. The first live run of the two-leg tool mixed
|
|
them in one `N STALE/failed` column, so four failed runs read as four bad
|
|
claims -- measured 2026-10-16 at HEAD, check_lint.py 2 and
|
|
queue_claim_check.py 2, none of them a claim disagreement. Two columns, so
|
|
the distinction survives the summary a reader actually reads.
|
|
"""
|
|
stale = [v for v in rep["verdicts"] if v["verdict"] == "STALE"]
|
|
notes = []
|
|
if rep.get("self"):
|
|
notes.append("self: no leg run (SELF_NOT_RUN)")
|
|
elif not rep.get("selftest_leg"):
|
|
notes.append("no --selftest: leg 2 absent")
|
|
return (len(rep["splits"]), len(stale), len(rep["failures"]),
|
|
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
|
|
basename instead of a repo-relative path) scanned zero tools and exited 0,
|
|
which reads exactly like a clean pass. Measured 2026-10-18 at 8932d06."""
|
|
if not requested:
|
|
return sorted(p for p in tree
|
|
if p.startswith("tools/") and p.endswith(".py")), []
|
|
return list(requested), [t for t in requested if t not in tree]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--repo", default=DEFAULT_REPO)
|
|
ap.add_argument("--rev", default="HEAD")
|
|
ap.add_argument("--tools", nargs="*")
|
|
ap.add_argument("--json")
|
|
ap.add_argument("--no-run", action="store_true",
|
|
help="label-only: no subprocess, no oracle, so every split "
|
|
"it prints is ADVISORY and --fail cannot fail on one -- "
|
|
"rc 1 would mean 'could not check this'")
|
|
ap.add_argument("--fail", action="store_true",
|
|
help="rc 1 if any tool reports an INTERNAL SPLIT a leg "
|
|
"VERIFIED (CHECKABLE) or a STALE claim; unverified "
|
|
"splits and leg failures are reported, never spent as "
|
|
"defects (rc 2 stays 'could not grade')")
|
|
ap.add_argument("--vocab", metavar="PATH",
|
|
help="a census vocabulary written by --save-vocab: under "
|
|
"--no-run, a split in a unit the vocabulary names is "
|
|
"GRADED (CACHE_CHECKABLE) instead of labelled. The "
|
|
"vocabulary proves unit-hood, never a value, so no "
|
|
"STALE claim can enter this way")
|
|
ap.add_argument("--save-vocab", metavar="PATH",
|
|
help="write the census vocabulary THIS RUN observed (unit "
|
|
"keys per tool) for a later --no-run --vocab; needs "
|
|
"the run, so it is refused with --no-run (rc 2)")
|
|
ap.add_argument("--selftest", action="store_true")
|
|
a = ap.parse_args()
|
|
|
|
if a.selftest:
|
|
return _fixture_checks()
|
|
|
|
if a.save_vocab and a.no_run:
|
|
print("cannot save a vocabulary: --save-vocab needs the census a run "
|
|
"prints, and --no-run runs nothing", file=sys.stderr)
|
|
return 2
|
|
vocab, vocab_rev = {}, None
|
|
if a.vocab:
|
|
try:
|
|
with open(a.vocab, encoding="utf-8") as fh:
|
|
vdoc = json.load(fh)
|
|
vocab = {t: set(u) for t, u in (vdoc.get("tools") or {}).items()}
|
|
vocab_rev = vdoc.get("rev")
|
|
except (OSError, ValueError, AttributeError, TypeError) as exc:
|
|
print(f"cannot grade: --vocab {a.vocab}: {exc}", file=sys.stderr)
|
|
return 2
|
|
if not a.no_run:
|
|
print("note: --vocab is read only by the runless mode; a live leg is "
|
|
"the oracle when the tool runs", file=sys.stderr)
|
|
|
|
store = git(a.repo, "ls-tree", "-r", "--name-only", a.rev)
|
|
tree = {p for p in store.decode("utf-8", "replace").split("\n") if p}
|
|
tools, missing = resolve_tools(a.tools, tree)
|
|
if not tools:
|
|
print(f"no tools found at {a.rev}", file=sys.stderr)
|
|
return 2
|
|
if missing:
|
|
print(f"cannot grade: --tools names nothing tracked at {a.rev}: "
|
|
f"{', '.join(missing)}", file=sys.stderr)
|
|
print(" names are repo-relative, e.g. --tools tools/checkin_check.py; "
|
|
"a filter that matches nothing is not a clean scan", file=sys.stderr)
|
|
return 2
|
|
|
|
cache, reports = {}, []
|
|
with tempfile.TemporaryDirectory(prefix="doccensus-") as tmpdir:
|
|
for t in tools:
|
|
src = subprocess.run(["git", "-C", a.repo, "show", f"{a.rev}:{t}"],
|
|
capture_output=True).stdout
|
|
local = os.path.join(tmpdir, os.path.basename(t))
|
|
with open(local, "wb") as fh:
|
|
fh.write(src)
|
|
try:
|
|
rep = check_tool(a.repo, local, t, cache, run=not a.no_run,
|
|
tmpdir=tmpdir, vocab=vocab.get(t),
|
|
vocab_rev=vocab_rev)
|
|
except subprocess.TimeoutExpired:
|
|
rep = {"tool": t, "splits": [], "blocks_with_claims": 0,
|
|
"verdicts": [{"file": t, "verdict": "TIMEOUT", "rev": "-",
|
|
"unit": "-", "claim": None, "block": 0}]}
|
|
if rep:
|
|
reports.append(rep)
|
|
|
|
print(f"tools scanned at {a.rev}: {len(reports)} "
|
|
f"({'internal-split only, label vocabulary' if a.no_run else 'docstring vs printed census'})")
|
|
if a.vocab:
|
|
print(f"vocabulary: {a.vocab} -- census at {vocab_rev}, "
|
|
f"{len(vocab)} tool(s), {sum(len(v) for v in vocab.values())} unit(s)"
|
|
+ ("" if a.no_run else " (not read: a live leg is the oracle)"))
|
|
tot = collections.Counter()
|
|
for rep in reports:
|
|
bad = rep["failures"] + [v for v in rep["verdicts"] if v["verdict"] == "STALE"]
|
|
nsplits, nstale, nfail, nclaims, notes = counts(rep)
|
|
print(f"\n{rep['tool']}: {nsplits} INTERNAL SPLIT, "
|
|
f"{nstale} STALE claim(s), {nfail} leg failure(s), "
|
|
f"{nclaims} claims checked"
|
|
+ (" [" + "; ".join(notes) + "]" if notes else ""))
|
|
if rep.get("wrap_candidates"):
|
|
print(f" wrapped: {rep['wrap_candidates']} pair(s) read across a "
|
|
f"line break, {rep['wrap_candidates'] - len(rep.get('wrap_unbound') or [])} "
|
|
f"spent, {len(rep.get('wrap_unbound') or [])} not a claim "
|
|
f"(no leg prints the unit)")
|
|
for u in rep.get("wrap_unbound") or []:
|
|
print(f" WRAP_UNBOUND {u['file']}:{u['line']} "
|
|
f"{u['token']} -- the unit is printed by no leg")
|
|
for s in rep["splits"]:
|
|
vals = ", ".join(f"{r['value']}@{r['line']}" for r in s["rows"])
|
|
scope = ",".join(s["revs"]) or "no rev named"
|
|
prov = (f", vocabulary at {s['oracle_rev']}"
|
|
if s.get("oracle_rev") else "")
|
|
print(f" SPLIT {s['unit']} [{scope}] ({s['how']}{prov}): {vals}")
|
|
for k, c in sorted(rep["unchecked"].items()):
|
|
# in the fast mode the UNCHECKED_NO_ORACLE rows are printed one by
|
|
# one above, so the count would be a second, worse copy of them
|
|
if a.no_run and k == "UNCHECKED_NO_ORACLE":
|
|
continue
|
|
print(f" (not flagged: {c} split(s) in units {k})")
|
|
# A --no-run split is the one advisory class a reader of the fast mode
|
|
# actually needs to SEE: it is what the old build failed the gate on.
|
|
# Printed with its values, labelled advisory, never counted above.
|
|
#
|
|
# BOTH runless classes print (2026-10-18). The LABEL_ONLY class (an
|
|
# uppercase census label with no oracle) was shown here; a lowercase
|
|
# word answered UNCHECKED_NO_ORACLE and was shown NOWHERE, so a runless
|
|
# split in a lowercase word read as no split at all. Not graded either
|
|
# way -- the fix is to the reporting, not the verdict.
|
|
for s in rep.get("advisory") or []:
|
|
if s["how"] == "LABEL_ONLY":
|
|
why = "LABEL_ONLY, no oracle"
|
|
elif s["how"] == "UNCHECKED_NO_ORACLE" and a.no_run:
|
|
why = "no census label and no cached vocabulary"
|
|
else:
|
|
continue
|
|
vals = ", ".join(f"{r['value']}@{r['line']}" for r in s["rows"])
|
|
print(f" ADVISORY {s['unit']} ({why}): {vals}"
|
|
f" -- reported, not graded")
|
|
for v in bad:
|
|
print(f" {v['verdict']:10s} {v['file']}:{v.get('line', '-')} "
|
|
f"{v['unit']} claimed {v['claim']} at {v['rev']} -- {v['detail']}")
|
|
for k in sorted({v["verdict"] for v in rep["verdicts"]}):
|
|
if k in ("REV_UNCHECKED", "SELF_NOT_RUN", "CLONE_LOCAL"):
|
|
n = sum(1 for v in rep["verdicts"] if v["verdict"] == k)
|
|
print(f" ({n} {k}: reported, never counted as a defect)")
|
|
for k, c in collections.Counter(v["verdict"] for v in rep["verdicts"]).items():
|
|
tot[k] += c
|
|
print("\n--- verdict totals (unit: claims) ---")
|
|
for k in sorted(tot):
|
|
print(f" {k:12s} {tot[k]}")
|
|
if a.json:
|
|
try:
|
|
with open(a.json, "w") as fh:
|
|
json.dump({"rev": a.rev, "repo": a.repo, "reports": reports}, fh, indent=1)
|
|
except OSError as exc:
|
|
print("cannot write --json " + a.json + ": " + str(exc),
|
|
file=sys.stderr)
|
|
return 2
|
|
print(f"wrote {a.json}")
|
|
if a.save_vocab:
|
|
# The vocabulary is READ OFF THE RUN, not off the docstrings: only the
|
|
# keys `parse_printed` saw in a leg's stdout count as census units, so a
|
|
# tool whose leg failed contributes nothing and cannot hand the fast
|
|
# mode a vocabulary it never printed.
|
|
#
|
|
# AND IT IS SCOPED TO WHAT A CLAIM CAN SPEND (2026-10-18). The leg says
|
|
# the key exists; the tool's own docstring says whether any claim will
|
|
# ever ask about it. Digests (`abc1234`) and diffstat words (`added`,
|
|
# `gone`) come off a leg's stdout but no docstring claims them, so they
|
|
# were 398 of 422 permissions nobody could use. Dropping them changes no
|
|
# verdict -- see spendable_keys and the offline check in the artifact --
|
|
# and it makes the file a list a reader can audit line by line.
|
|
per_tool = {r["tool"]: r for r in reports if r}
|
|
units = collections.defaultdict(set)
|
|
seen = collections.defaultdict(set)
|
|
for (name, rev), entry in cache.items():
|
|
if rev == "<selftest>" or not entry[0]:
|
|
continue
|
|
seen[name].update(entry[0][0])
|
|
keep = spendable_keys((per_tool.get(name) or {}).get("claim_units"))
|
|
units[name].update(k for k in entry[0][0] if k in keep)
|
|
observed = sum(len(v) for v in seen.values())
|
|
kept = sum(len(v) for v in units.values())
|
|
try:
|
|
with open(a.save_vocab, "w", encoding="utf-8") as fh:
|
|
json.dump({"rev": a.rev, "repo": a.repo,
|
|
"tools": {k: sorted(v) for k, v in units.items()}},
|
|
fh, indent=1, sort_keys=True)
|
|
except OSError as exc:
|
|
print("cannot write --save-vocab " + a.save_vocab + ": " + str(exc),
|
|
file=sys.stderr)
|
|
return 2
|
|
print(f"wrote vocabulary {a.save_vocab}: {len(units)} tool(s), "
|
|
f"{kept} unit(s) at {a.rev} ({observed} key(s) observed, "
|
|
f"{observed - kept} unspendable by any claim)")
|
|
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}"
|
|
+ (" (graded on a cached census vocabulary: --no-run --vocab)"
|
|
if a.no_run else ""))
|
|
return 1
|
|
print(f"\nOK: 0 INTERNAL SPLIT, 0 STALE claim(s) at {a.rev}"
|
|
+ (" (no oracle in this mode: nothing was verified, so nothing is "
|
|
"graded -- ADVISORY rows above are labels, not faults"
|
|
+ ("; the vocabulary graded any split in a unit it names)"
|
|
if a.vocab else ")")
|
|
if a.no_run else ""))
|
|
return gate_rc(reports, a.fail)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|