Skip to content

feat(ast): add the role census, kind-coverage ratchet and bypass lint #1692

Closed

Description

@dekobon

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>.rs for every remaining LANG with every role pending(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 their Comment/DocComment cells are pending(R09) and StringLit pending(R08); only the roles they genuinely lack are absent with a reason.
  • utils/check-role-census.py: for every LANG and every role in R01: #1691's set, require rows, absent(role, reason), or pending(R0n); print the (language × role) matrix with pending cells marked; fail on a hole; a missing roles/<lang>.rs is a failure, not a skip. pending cells are a ratchet of their own (--update may 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's grammar.json — named rules and anonymous terminals (__volatile__, co_return) — resolved to runtime ids through the generated language_<lang>.rs enum; subtract kinds any row of any role names (raw sets — a kind claimed by one role is claimed) and kinds listed Excluded; 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. An Excluded(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; --update ratchets 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 read owns_/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 a matches!, a match arm, a kind_id() ==, a kind() == "…" string comparison, or a const array 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.py pinning both directions, as check-snapshot-anchors-test.py does.
  • Makefile targets; wire into lint, pre-commit, ci, .pre-commit-config.yaml, the lint CI job.

Tests and edge cases

  • Census: a table with neither Modifier rows nor absent(Modifier) nor pending fails naming language and role; a pending cell 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 under Branch; a cross-role self-test asserts a kind claimed by Container is not in the residue at all.
  • Bypass: kind_id() == Java::MethodInvocation under src/metrics/abc/ fails; the same under src/spaces/ (traversal) passes; an exempted one passes; a kind() == "call_expression" string dispatch inside the boundary fails.

Acceptance criteria

  • All three gates run in make pre-commit and make 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

  1. added
    refactorCode maintainability / tech-debt cleanup (no behaviour change)
    on Oct 23, 2026
  2. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    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.

  3. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    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.

  4. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    Commands and results on the final tree 5bb1890a

    • make role-census: OK — 25 languages × 29 roles, 547 pending, 178 declared or absent.
    • make role-coverage (with --require-observations): OK — 25 languages, 7842 baselined residue entries of 7927 runtime ids; observation ok, 1666 observed in the corpus; 15 languages reported as not sampled by the corpus.
    • make role-bypass: OK — 125 baselined sites across 169 boundary files; 6 reasoned exemptions.
    • Self-tests: census 21, coverage 23, bypass 32 unittest cases, all passing.
    • Perturbation sweeps (file-backed driver, python3 -B, no bytecode): 34 decision lines across the reader and the three gates, each failing at least one named test; the one line found unguarded during the sweep (a duplicated lang/file check) was consolidated and re-guarded.
    • Rust: python_reader_agrees_with_the_compiled_tables holds the reader to the compiled canonical() of all 25 tables; dropping a payer or flipping pays_never in the reader fails it. 20 role lib tests; clippy -D warnings, fmt, cargo doc -D warnings clean; enums-codegen-drift OK; feature-isolated -p builds OK; check-test-lang-gates OK and --compare against b9ea546 OK; make actionlint clean.
    • make pre-commit: BCA_GATE: pass (gate=pre-commit) on the round-1 and round-2 trees; the final squashed tree's run is in progress and its line will follow.
  5. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    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.

  6. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    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.

  7. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    make pre-commit on the final squashed tip 5bb1890a: BCA_GATE: pass (gate=pre-commit). Working tree clean, no snapshot drift, output submodule clean.

  8. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refactorCode maintainability / tech-debt cleanup (no behaviour change)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions