#!/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 -- no oracle, no run. Within one docstring, every numeric claim of the form ` ` is collected; two claims of ONE unit that a single revision scope covers, carrying two different values, is a split. This is the verdict that catches the 441/471 instance, and it is why the tool is useful with no subprocess at all. 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 ` ` with the unit word ADJACENT, which is the archive's own notation. A bare number is not a claim and is not checked. 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 that cannot be run at a revision (queue_claim_check.py has no --rev; the queue is materialised from git and handed to it by --queue) is driven through a per-tool recipe declared below, with a temp file that is removed. 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: 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 (` ` and `: `); 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. USAGE python3 doc_census_check.py [--repo DIR] [--rev HEAD] [--tools NAME...] [--json OUT] [--no-run] [--selftest] """ 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: # ` ` -- "471 UNRESOLVED", "1,612 citations" # ` [: =] ` -- "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. CLAIM = re.compile( r"(?[0-9][0-9,]*)(?P[^A-Za-z0-9\n]{0,2})" r"(?P[A-Za-z_][A-Za-z0-9_]*|[A-Z][A-Z_]{2,})\b") CLAIM_EQ = re.compile( r"(?P[A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*\**\s*(?P[0-9][0-9,]*)") REV_LONG = re.compile(r"\b[0-9a-f]{7,40}\b") # 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. `queue_claim_check.py` takes no --rev: its # subject is a FILE, so the file is materialised at the revision and handed to # it. Any tool not listed here is run as `