Repository navigation
Description
Objective
Add the three gates that make an omission visible — the per-(language, role) census, the kind-coverage ratchet keyed by runtime kind id, and the bypass lint — with self-tests, wired into make pre-commit and make ci. State in each what it cannot detect.
Umbrella: R00: #1690 · Planning ID: R02: #1692
Completion prerequisites: R01: #1691
Status: implemented on branch refactor/1691-role-table-proof as two commits on R01's tip b9ea546f: 9c03629a (every language's table, the kind-flags sidecar, the shared reader) and 92d7ff74 (the three gates, baselines, integration). Not pushed; the one pull request will carry R01 and R02 together.
Adjustments to this body, recorded before implementation. R01 fixed the DSL by compiling it, and it differs from the illustrative syntax below: absent(role, reason) is Role { absent "reason" } emitting role::ABSENT; pending(R0n) is Role { pending Rnn } emitting role::PENDING; Excluded(role, reason) is a row-level excludes [Kind "reason"] emitting role::EXCLUDED under the row that rejected the kind. R01 shipped three proof tables (Java, Python, Elixir), not two. A plan review (astra-review, before any code) also moved four things stated below: baselines are exact record sets rather than counts, so a swap cannot hide inside an unchanged total; the bypass boundary includes the parent dispatcher modules and the macros that generate consumers (Java's cyclomatic membership lives in cyclomatic.rs's impl_cyclomatic_java_like!, outside the per-language files); named/visible labels come from the runtime via a generated KIND_FLAGS sidecar in each language_<lang>.rs, not from node-types.json; and no kind category is excluded from debt implicitly — _repeatN / _tokenN internals and _-prefixed kinds (C::Volatile2 spells __volatile__) are baselined like any other until a row claims or excludes them.
A second review pass (the review and rust-optimize skills, a performance agent, and astra-review on the least-reviewed surfaces, then astra-review on every fix) moved three more things: make fmt / make fmt-check now reach the excluded enums/ and fuzz/ crates by manifest path, since cargo fmt --all had never seen the sidecar's test file; corpus staleness is judged against the index gitlinks so a corpus bump staged with its re-observation passes pre-commit; and --migrate-pin is repeatable with an all spelling because the five vendored grammars share one pin that every release bumps (RELEASING.md gains the step). Four more absent cells became pending (C Catch — the pinned grammar emits seh_except_clause; Tcl and iRules DeclKeyword — their grammars emit proc / namespace / when leaves; Tcl Closure — apply is a recognition boundary, not an absence).
Why this exists
A table nobody is forced to use decays into another copy — kind_sets.rs has existed since May and never served getter/* (umbrella, "What prior consolidations did"). And a table that records only what someone remembered to list has today's blind spot: grammar-dispatch.md §1 says an arm list is a coverage claim the compiler cannot check. These gates are that check, modelled on check-snapshot-anchors.py and check-rustfmt-bail.py because a ratchet that enumerates existing debt is honest about what is and is not covered, where a one-shot "classify everything first" would stall the migration or be padded with blanket exclusions.
Two scope limits come from the design review and must be stated in the gate output, not just here: the ratchet catches newly unclaimed kinds and is not a substitute for #1458's measured sweeps; and grammar.json is not the id table — aliased kinds share a spelling and differ in id (grammar-dispatch.md §1), so identity is the runtime id from enums/.
Implementation steps
- Create
roles/<lang>.rsfor every remainingLANGwith every rolepending(R0n)naming the package that will declare it, so the census has a complete input from this package on. The two helper grammars are not blanket-absent: Ccomment and Preproc recognise comments and string literals (checker/ccomment.rs,checker/preproc.rs), so theirComment/DocCommentcells arepending(R09)andStringLitpending(R08); only the roles they genuinely lack areabsentwith a reason. -
utils/check-role-census.py: for everyLANGand every role in R01: #1691's set, require rows,absent(role, reason), orpending(R0n); print the (language × role) matrix withpendingcells marked; fail on a hole; a missingroles/<lang>.rsis a failure, not a skip.pendingcells are a ratchet of their own (--updatemay only remove them) and must be empty when R11: #1701 closes the umbrella. -
utils/check-role-coverage.py: per language, candidates are the grammar's full symbol list from the pinned crate'sgrammar.json— named rules and anonymous terminals (__volatile__,co_return) — resolved to runtime ids through the generatedlanguage_<lang>.rsenum; subtract kinds any row of any role names (raw sets — a kind claimed by one role is claimed) and kinds listedExcluded; the residue per language is compared against.role-coverage-baseline.txt. The guarantee is exactly "every grammar kind is either claimed by some role, excluded with a reason, or in the baseline" — global coverage, not per-role correctness. AnExcluded(role, reason)entry's role names whose author considered and rejected the kind, printed in the report so a reviewer sees which rejection is relied on; it asserts nothing about other roles, and a kind may carry several; increases fail;--updateratchets down. A column marks which residue kinds the integration corpus emits at this pin, so a reviewer can tell live from latent. -
utils/check-role-bypass.py: scope is the consumer boundary per role — the files that readowns_/pays_for that role (abc/*,cyclomatic/*,cognitive/*,getter/*,checker/*,npm/*,npa/*,nexits.rs). Inside that boundary, a kind in a row's raw set appearing in amatches!, a match arm, akind_id() ==, akind() == "…"string comparison, or aconstarray fails unless under// role-bypass: allow(<role>) — <reason>. Outside the boundary (naming, traversal, doc attachment, tests) it is not checked, and the gate's header says so. Own ratchet,.role-bypass-baseline.txt, during migration. - Each gate carries a
*-test.pypinning both directions, ascheck-snapshot-anchors-test.pydoes. - Makefile targets; wire into
lint,pre-commit,ci,.pre-commit-config.yaml, thelintCI job.
Tests and edge cases
- Census: a table with neither
Modifierrows norabsent(Modifier)norpendingfails naming language and role; apendingcell passes and is counted. - Coverage: a stub enum + flags pair (not
grammar.json— identity is the enum, see the adjustments) + enum pair where two kinds share a spelling with different ids — claiming one must leave the other in the residue. Adding an anonymous terminal raises the residue;Excluded(Branch, …)on it removes it from the residue and is reported underBranch; a cross-role self-test asserts a kind claimed byContaineris not in the residue at all. - Bypass:
kind_id() == Java::MethodInvocationundersrc/metrics/abc/fails; the same undersrc/spaces/(traversal) passes; an exempted one passes; akind() == "call_expression"string dispatch inside the boundary fails.
Acceptance criteria
- All three gates run in
make pre-commitandmake ci; none skips silently. - Baseline files committed with headers explaining an entry, the ratchet direction, and what the gate cannot see (wrong-role claims, incomplete entries, dead walker paths).
- With R01: #1691's two partial tables plus the pending tables for every other language, the census passes with every undeclared cell marked
pending; the coverage residue per language is in the baseline, not hidden.
Verification and completion evidence
- Self-test output for each gate and one real run;
BCA_GATE:line.
Activity
dekobon commentedon Oct 23, 2026
Tracking is ready
The body now links the master issue and the work packages it references. Native parent and blocked-by relationships match the listed completion prerequisites. Implementation and acceptance checkboxes remain open; prerequisite closure and the R04: #1694 go decision govern execution order.
dekobon commentedon Oct 23, 2026
Implemented
Branch refactor/1691-role-table-proof, two commits on R01's tip b9ea546: c0f6bd53 (every language's table, kind-flags sidecar, shared reader) and 5bb1890a (the three gates, baselines, integration). Not pushed; no PR. The body records the interface adjustments made before any code and ticks every implementation, test and acceptance item.
Design decisions, each from the plan review (astra-review) and reasoned in the issue body: identity is the (language, id) pair from the generated enum under the pinned grammar version, never a spelling, with named/visible read from a KIND_FLAGS sidecar the enums generator now emits, so node-types.json and cargo metadata are not on the lint path; no kind category is forgiven — hidden rules, auxiliary _repeatN symbols and volatile-style underscore terminals are baselined like any other until a row claims or excludes them; all three baselines are exact record sets (the bypass one numbering repeated sites within an item), so a swap or an addition cannot hide behind an unchanged count; the bypass boundary includes the dispatcher modules and resolves three macro shapes from their headers, and any other kind-reading macro is reported as unsupported rather than missed; legitimate naming and doc-attachment sites carry reasoned exemptions rather than baseline entries; the observation artifact is produced by a command that builds its own bca, records corpus SHAs, pins and per-language file counts, and is judged stale against the gitlinks in HEAD so the CI lint job's submodule-less checkout works.
dekobon commentedon Oct 23, 2026
Commands and results on the final tree 5bb1890a
dekobon commentedon Oct 23, 2026
Review
astra-review critiqued the plan before any code (seven findings, all accepted and recorded in the body) and then reviewed the implementation over four rounds. Round 1 found eleven issues, four blocking: language rows could silently vanish from the mk_langs! reader; multi-line match arms were not scanned, so the live Java getter's sites were absent from the baseline; kind-set and $lang-parameterised macros escaped; and the bypass key dropped multiplicity. Also: the node-header regex lost brace and colon kinds, failed observation runs passed, id-to-kind drift under an unchanged pin passed, import scope was file-wide and string compares were guessed from spelling, literal recovery read comments and missed raw strings, several absent reasons contradicted the role inventory, and exemptions needed no reason. All fixed with tests and re-bootstrapped baselines. Round 2 found three residual escapes (macro equality and string dispatch, raw strings with several hashes, a headerless run relabelled not-sampled); round 3 one (reversed-order macro equality with a metavariable receiver); round 4: no actionable findings remain.
dekobon commentedon Oct 23, 2026
Limitations
Stated in every gate's output and baseline header: none judges whether a row's membership is right, which role should claim a kind, whether a row is complete, or whether a consumer reads the right projection; the coverage ratchet is accounting, not a substitute for #1458's measured sweeps. The bootstrap guard refuses a file that exists or is tracked at HEAD, which does not see a deletion already committed; a base-branch comparison in CI would, and is not part of this package. The bypass lint resolves three macro shapes and reports others as unsupported; a kind reached through a helper function or a kind() compared to a variable is outside what it can see. Fifteen languages have no file in the integration corpus, so their residue is labelled unknown rather than unobserved.
dekobon commentedon Oct 23, 2026
make pre-commit on the final squashed tip 5bb1890a: BCA_GATE: pass (gate=pre-commit). Working tree clean, no snapshot drift, output submodule clean.
dekobon commentedon Oct 23, 2026
Second review pass and the final tree
Four reviewers ran against the squashed commits: the review skill (19 findings), the rust-optimize skill (one gate-invisible misformat in the excluded enums crate plus two test tweaks), a performance agent (bypass gate 2.16 s to 1.36 s from two one-line changes; the sidecar and canonical() confirmed absent from shipped binaries), and astra-review on the least-reviewed surfaces (six findings). Every finding was fixed, then astra-review confirmed the fix set over two further rounds (two residual P2s in the first - alias shadowing across import syntaxes, nested inline-module resolution of out-of-line test files - closed in the second). The simplify-rust and audit-tests passes followed; audit-tests found the two determinism tests could not fail (same inputs twice), and they now reorder rows and languages and pin the sorted entry order, with a perturbation of the sort failing exactly that test.
Material changes from this pass are in the issue body. Final tree: 9c03629 + 92d7ff7 on b9ea546; make pre-commit: BCA_GATE: pass (gate=pre-commit) on 92d7ff7; working tree clean, no snapshot drift, output submodule clean. Self-tests: census 21, coverage 31, bypass 38; 50 perturbations recorded in all, each failing a named test. Gates on the tree: census 551 pending / 174 declared-or-absent; coverage 7842 residue of 7927 ids, 1666 observed; bypass 125 sites across 167 files.
feat(ast): declare every language's roles and emit kind flags
feat(ast): add the role census, kind-coverage ratchet and bypass lint
feat(ast): declare every language's roles and emit kind flags
feat(ast): add the role census, kind-coverage ratchet and bypass lint