Repository navigation
Description
Objective
Give each semantic fact about a node — call, string literal, comparison, short-circuit operator, modifier, declaration keyword, function or space opener, decision, exit, doc target — one declaration per language, in big-code-analysis-ast, that Checker, Getter and every per-language metric dispatcher read rather than restate. Fail CI when a language has no declaration for a role, and when a grammar bump or a new language introduces a kind no declaration claims.
Status (2026-10-26): R01–R03 merged to main in #1705 (bdd70eb9, 20 commits, fast-forwarded). The role_table! DSL, the three gates and the first consumer migration are live; the Halstead keyword roles (Modifier, DeclKeyword, Primitive) are read from the rows in all 25 languages with output byte-identical to before. R04 (#1694, recorded go) and R05 (#1695) are done and merged to main (f23fb7bc..c65df636 plus the post-close fixes through 7425561f; the branch was rebased onto the 955da8d5..44184ad7 getter batch and fast-forwarded, so the hashes in the earlier comments are dangling): all seven paying roles — Comparison, ShortCircuit, EagerLogical, Ternary, CaseArm, Guard, NullSafe — are declared in every language and read by every consumer, with every ops and metrics output byte-identical (0 of 6,638 corpus and fixture outputs; 0 of 4,388 recovery-sweep outputs). The census has no pending cell for any of the seven. R06 (#1696), R08 (#1698) and #1599 are merged to main in PR #1717 (e2fd7193, 13 commits on 167cb506, fast-forwarded; the branch hashes in the comments below are dangling): the four space-tree roles, Member, Call and StringLit are declared in all 25 languages and read by every checker, getter, the walk's promotion, the member / boundary consumers, find / count, every getter's interpolation set and the dump's string flattening, with callable / callable_when / opens_by, CALLABLE, is_callable_kind, lists_kind and TEXT_GATED added to the DSL, Ancestors::of_children giving a body's members their own chain, and #1599 closed through Checker::keys_operator_by_text. Zero drift against 7425561f: 0 of 27,024 corpus outputs and 0 of 129,042 recovery outputs. Census 272 pending (none of the six roles), bypass baseline 84 → 10 (Elixir's byte-less kind table, #275, and the Exit consumer, R07), coverage 6,536. The PR carried the simplify-rust, audit-tests and rust-optimize passes and five reviews with two astra-review rounds on the fixes; lesson 103 records the one new mechanism (a chain is the queried node's own, or it is the wrong chain). Evidence at ~/bca-evidence/1696/ on the author's host, summarised on #1696. Next: R09 (#1699, on R03 + R06).
Status (2026-10-31): R07 (#1697) is implemented on refactor/1697-control-flow-roles (20 commits over 96a45849, tip fa3f316f), awaiting the output-submodule push and the PR: Branch, Switch, Loop, Catch, ElseIf, Exit declared in all 25 languages (census 287 → 157 lines, every remaining pending R09's), cyclomatic through one impl_cyclomatic_rows! for 19 languages plus four hand-written row readers, cognitive / nexits (impl_exit_from_row!) / ABC / Halstead (paying_roles!(…, control_flow)) / Checker::is_else_if (impl_else_if_from_row!, compile-time !TEXT_GATED pin) on the rows, the three impl_is_else_if_* macros and impl_exit_match_kinds! deleted. Tcl and Elixir classify their generic command / call once per visited node through a WordSlot cache on FoldCache. Zero drift for the migration (0 of 345,128 corpus outputs at its last commit); the branch then carries two approved behaviour changes as their own commits, #1720 (Rust type-syntax for, 201 outputs) and #1602 (Elixir keyword sections, 6), and nothing else differs at the tip. Bypass baseline 10 → 4 (Elixir's #275 table), coverage 6,561 → 6,303; bench-scaling 93/93 with five new probes; patch coverage 99.73 % vs a 98.94 % floor. Evidence at ~/bca-evidence/1697/, summarised on #1697.
Status (2026-11-01): R09 (#1699) is implemented on refactor/1699-doc-roles (two commits on fdd12ad4: the bypass-gate self-test case and the migration), awaiting the PR: Comment, DocComment, Docstring, DocTarget, DocRunOpener, DocRunDirective, HoldsDocInBody declared in all 25 languages — the census has no pending cell — and read by Checker::is_comment / is_doc_comment / is_doc_target / holds_doc_comment_in_body / is_doc_run_opener / is_doc_run_directive and Python's python_documentation_string_run. Doxygen's string-keyed doxygen_entry list is id-keyed fragments with an exact-alias test per dialect; the context a row cannot see (tsc's export wrapper, Ruby's then and YARD scope, Rust's tuple slot, Doxygen's ERROR, Lua's run content) stays a named fn in checker/<lang>.rs under allow(DocTarget). Zero drift on the final tree against fdd12ad4: 0 of 46,592 corpus, 0 of 952 fixture and 0 of 107,106 recovery outputs, a 7,675,318-node per-node is_doc_target oracle byte-identical; one migration defect (a destructuring TypeScript export documenting) was caught by the row tests before any capture and the base answer restored. Bypass baseline unchanged at 4; coverage 6,303 → 5,999; bench-scaling 99/99; patch coverage 99.49 % vs 98.79 %. #1655's touchpoints are edited; follow-ups on #1700 / #1701. Evidence summarised on #1699.
This is the umbrella for a reviewed design. Planning IDs R00: #1690–R11: #1701 identify the work packages; their GitHub sub-issues and native blocked-by relationships are the execution tracker. The design was derived from an exploratory inventory of the tree at f3527dfd (2026-10-22), 84 review entries across three overlapping trails, and independent astra-review passes over the design and issue texts. The evidence and its limits are summarised here so the issue stands alone. Recheck current source before implementing — the dispatch files move daily.
Terms used throughout
- Role: a semantic fact a consumer asks of a node (
Comparison,Modifier,FuncOpener, …). The full set is fixed in R01: #1691. - Row: one language's declaration for one role. A row names owners — the node kinds that are the construct, which a slot predicate such as ABC's
scores_itselfreceives — and payers — the token kinds a counting arm fires on — plus an optional projection from an owner to the child that pays (BinaryExpression→ itsoperatorfield). The declared owner and payer lists of one row are disjoint; a kind the grammar gives no token (InstanceofExpression) is declared once as a self-paying owner and appears in both effective sets. Where two consumers pay at different nodes of one construct — Python ABC pays at theComparisonOperatorwrapper, Halstead at its operator leaf (getter/python.rs) — the row carries one projection per occurrence named structurally —pays_at(Wrapper)andpays_at(Leaf)— and each root consumer chooses which it reads (ABC the wrapper, Halstead the leaf); R01: #1691 fixes the shape. The AST crate names no metric. - Peel: ABC's existing walk from a boolean slot down through transparent wrappers (
(…),!, a cast) to the operand that is tested;count_boolean_slotinsrc/metrics/abc.rs. Unchanged by this series. - Census: a gate asserting every (language, role) pair is declared — rows,
absent(reason), orpending(Rnn)during migration. - Ratchet: a checked-in baseline of debt that may only decrease, in the style of
.rustfmt-bail-baseline.txt. - Bypass lint: a gate failing when a kind a row claims is matched outside
roles/without a reasoned exemption. - Policy expression: in the root crate, which roles a metric counts —
cyclomatic = pays_CaseArm ∪ owns_Branch ∪ …— so a documented per-metric difference is a term present or absent, not a second kind list.
Why
The measured problem
The maintainer supplied six consecutive monthly created/closed counts: 389/377, 440/460, 66/35, 159/149, 71/61 and 128/96. These sum to 1,253 created and 1,178 closed; the separately reported tracker total was 1,315. The monthly figures were not independently reproduced for this plan. Three trails were read through gh issue view and their entries classified:
| trail | relevant | (a) omitted membership | (b) two encodings disagree | (c) genuine policy | (d) grammar |
|---|---|---|---|---|---|
| ABC condition slot / comparison, #1297 → #1540 | 37 of 45 read | 21 | 9 | 6 | 1 |
| Halstead "getter/ bills no X", #1544 → #1600 | 27 of 31 read | 17 | 0 | 8 | 2 |
| Checker predicate vs consuming metric: #1456 #1474 #1580 #1599 #1598 #1470 #1313 #1261 | 8 of 8 | 0 | 8 | 0 | 0 |
| total review entries | 72 classifications from 84 entries; 12 unclassified | 38 | 17 | 14 | 3 |
(a) means the kind existed and a sibling language or another consumer already handled it; (b) means two lists encoding one fact drifted. Together they account for 55 of the 72 classifications. These are review-entry totals, not distinct-issue counts: #1470 and #1474 appear in two trails, and the classified records include bundled PR #1313. They are qualitative evidence of recurrence, not a measured preventability rate. (c) is real and concentrated in text-keyed languages and Halstead billing policy; the design keeps room for it rather than pretending it away.
The chains are the sharpest evidence. The subject-less branch-arm double count was fixed in five languages across five issues (#1383 C# → #1421 Kotlin → #1453 Ruby → #1523 Go → #1527 Elixir), and each fix's sibling sweep cleared the next victim: #1383 said Ruby was clean; #1453 said Go and Elixir were clean. The modifier-keyword class took five issues with no shared table (#1558 → #1563 → #1565 → #1578 → #1579), after which review found Volatile2 missing from all four hand-copied C-family arms (#1598). The declaration-keyword class had one genuinely measured nine-language sweep (#1552) and still needed four follow-ups within a day, because the sweep's boundary was the issue's own table, not the role.
Where membership lives today
An exploratory inventory over checker/*.rs, getter/*.rs and src/metrics/{abc,cognitive,cyclomatic,loc,npa,npm}/*.rs used regex over comment-stripped match bodies, with spot-checks on six languages. Its candidate-site means are below. These are investigation leads, not a validated census of independent encodings: whole getter bodies can mix roles, macros and forwarding helpers need manual resolution, and bare variants such as Public/Private/Class were undercounted. R01: #1691/R03: #1693 must validate the membership they migrate.
| role | heuristic candidate-site mean / language | sites |
|---|---|---|
| short-circuit operator | 4.3 | cyclomatic, cognitive, abc scores_itself, abc compute, getter |
| function opener | 3.7 | is_func, is_closure, get_space_kind, nexits |
| space opener | 3.0 | is_func_space, get_space_kind, npm, npa |
| decision | 2.8 | cyclomatic, cognitive, abc token arm |
| modifier; doc comment | 2.2 | getter, npm, npa; is_doc_comment, is_comment, loc |
| call, string, primitive, else-if, exit, comparison | 1.7–2.0 | checker + getter (+ abc branches, alterator) |
The exploratory comparison of ABC's scores_itself, ABC counting arms and cyclomatic identified important node/token pairings: the slot predicate names an owning node (SpaceshipExpression, TryExpression, LetChain, BinopExpr) while a counting arm may name its operator token (LTEQGT, Try, EQEQ). The checks performed did not establish a new live drift; neither did the regex comparison prove all languages free of drift. In particular, the agreement required by scores_itself is with ABC's own counting paths, not equality with cyclomatic. That agreement is stated in src/metrics/abc.rs: "must agree with those arms exactly". #1529 and #1540 illustrate its maintenance cost. The proposed recognition/projection boundary is intended to reduce that cost.
What prior consolidations did and did not do
| consolidation | centralised | left per caller | trail afterwards |
|---|---|---|---|
kind_sets.rs |
5 C# alias unions, 1 Go set | every role list; never served getter/* |
#1598, #1484 |
Getter::string_operand_type (#420) |
the interpolation-skip rule | one or two interpolation kinds per language | #454, then stopped |
count_boolean_slot + peel (#1526) |
"slot pays unless an arm did" | scores_itself, wrapper_operand per language, agreeing with a third list by comment |
#1529, #1533, #1527, #1537 within two days |
_with_code forwarding defaults |
nothing — a default that hides the one override | all membership | #1130 |
slot_pays_tests.rs, tests/parity/* |
cyclomatic as oracle; cross-language equality on chosen fixtures | the fixture: exhaustive on LANG, not on kinds |
every (a) issue |
generic_angles.rs (#1559) |
the membership — lists / openers / closers per language |
only is_misparse |
#1567 was a three-row edit |
Sharing membership reduced #1567's repair to three table entries; it did not prevent that omission. The earlier rule-sharing work also helped, but left membership choices at individual callers. This umbrella combines shared mechanisms with explicit membership and independent behavioral checks rather than promising to eliminate language-specific decisions.
What this does and does not claim
It closes two failure modes mechanically: a language with no declaration for a role, and a kind newly unclaimed after a grammar bump or new language. It makes every remaining per-language decision live in one file per language, with a row comment, and makes a measured sibling sweep a diff of the census and ratchet rather than a hand-typed table.
It does not: derive Java::Class ∈ DeclKeyword from Python::Class ∈ DeclKeyword (kinds are per language); catch a kind claimed by the wrong role; catch an incomplete entry (#1556 — Kotlin's declaration lacking interface); catch a dead walker path (#1537), the wrong slot driver (#1527) or a missing projection parent (#1567); or replace #1458's measured sibling sweeps — the existing unclaimed kinds in the ratchet baseline are enumerated debt, not covered. The 76% above classifies trails; it is not a measured "preventable by this design" rate.
Architecture
big-code-analysis-ast/src/roles/<lang>.rs one role_table! per language; dialect clones share a template
| emits owns_<role>(node, code, ancestors), pays_<role>(..), opens_space(..) -> SpaceKind, const kind slices
|
+-- checker/<lang>.rs, getter/<lang>.rs call the table; keep no role kind lists of their own
|
`-- root src/metrics/* policy expressions over roles; mechanisms unchanged
cyclomatic = pays_CaseArm ∪ owns_Branch ∪ owns_Loop ∪ pays_ShortCircuit ∪ owns_Ternary ∪ owns_Guard
abc.scores_itself = owns_Comparison (projected) ∪ owns_ShortCircuit ∪ owns_Ternary ∪ per-language extras
(ConstraintReach, count_boolean_slot, the peel: kept as mechanisms, fed from the rows)
Shared contracts
- A row declares a construct, not a kind list (owners, payers, projections; declared owners ∩ declared payers = ∅ asserted by the census). Recognition is per language; which occurrence pays is per consumer, and where consumers differ the row carries a structurally named projection per paying node. A consumer states which of
owns_/pays_at(Wrapper|Leaf)it reads, so one consumer counting both a wrapper and its child is a visible choice, not an accident (grammar-dispatch.md§5). - Recognising a closure and opening a space are two declarations.
Closuresays what is a closure (nom, nargs, cognitive need it);opens_spacesays whether it gets aSpaceKind(Python'sLambdais a closure and opens nothing —checker/python.rs,getter/python.rs). The space-tree parity test compares promotion againstopens_space, never against recognition. - No logic in the macro body. Gates and projections are named
fns beside the table, each with its own comment; row rationale sits on the row (.claude/rules/macro-comments.md). Role identifiers are a fixed set the macro maps to generated names. Equal unconditional bodies and equalSpaceKindmappings coalesce into one arm soclippy::match_same_armsis not tripped. Macro-source formatting is verified withcheck-rustfmt-bail.py --show; clean expanded Rust proves nothing about the macro body (.claude/rules/formatting.md). - One canonical entry-point spelling. The table emits
(node, code, ancestors). The byte-lessChecker::is_*andGetter::get_space_kindstay as hand-written legacy wrappers with a documented limitation (kind-only; Elixir containers and every text-gated row read false). Callers still using them are audited and moved one by one; several already use the contextual form. Elixir'spromotes_to_func_space_with_codestays the single combined lookup.make bench-scalingandmake chain-auditrun from the first contextual migration (lesson 82). R03 made the first move:Getter::get_keyword_role(node, code, ancestors)is a required method every language implements from its rows, andkeyword_or_op_typereads it ahead of the byte-lessget_op_typeon every path the walk takes, so noget_op_typelists a keyword a row owns. - Dialect clones declare through one template (c/cpp/mozcpp/objc, javascript/mozjs/typescript/tsx, tcl/irules) with per-dialect extras. A per-language table alone would keep refactor(getter/c-family): one kind table for the four copied modifier lists #1598's four copies. Mozcpp owns no file extension, so its parity is pinned against Cpp (
tests/parity/cpp_mozcpp_parity.rs), not the corpus. R03 settled the shape: the shared list is a..name!()kind fragment (roles/c_family.rs,roles/js_family.rs) spliced into each dialect's own row, so a dialect adds its extras beside the shared kinds and a gate stays on the dialect's row. The TS/TSX and Cpp/Mozcpp rows are still full copies; folding them is R04's. - Text-keyed roles are declared, not exempted — Elixir call targets, Ruby visibility, Perl barewords, Tcl command names, Groovy/ObjC
is_primitive— throughtext_gated_by fn(&Node, &[u8], Ancestors) -> bool(lesson 22;grammar-dispatch.md§9–§10). - Policy is an expression in the root crate. Preserve each language's existing consumer policy rather than treating this diagram as a universal scoring formula. Python ABC handles
BooleanOperatorthrough its operands while cyclomatic counts itsand/ortokens; Bash&&/||are not ABC conditions (fix(metrics): Perl xor and Bash -a/-o miss metric parity #1536). Kotlin infixand/or/xorand Perl/PHPxorare eager: decision-counting consumers add no chain increment, but Halstead classification remains language-specific (Perl/PHP operator; Kotlin identifier operand). Optional navigation and null coalescing need not have identical consumers; JS-family cognitive includes??and??=.ConstraintReach(C++requiressuppresses decisions throughout its subtree — fix(cyclomatic/cpp): rvalue refs and operator&& count as decisions #1525),count_boolean_slotand the peel stay as mechanisms. Nothing in the AST crate names a metric. - Coverage is keyed by runtime kind id, not spelling. Aliased kinds share a
kind()string and differ in id (grammar-dispatch.md§1);enums/stays authoritative,grammar.jsonis only candidate discovery. A row'sexcludes [K "why"]records which role's author considered and rejected a kind (R01 chose the row attribute over a standaloneExcluded(role, reason)entry); the coverage guarantee itself is global (claimed, excluded or baselined), not per-role correctness. - Zero drift per refactor. A migration preserves
bca opsvocabularies, metric counts and space trees byte-for-byte on the integration corpus; both snapshot forms are captured underINSTA_FORCE_PASS=1 --no-fail-fast, then a plain run. A behaviour change found en route is its own commit with the self-scan baseline refresh, accepted integration snapshots pushed and the parent gitlink, never folded into a migration (lesson 59). Each package's CHANGELOG entry lands with that package. - Gates and API. Role modules are unconditional like
checker/*.rs; only derived tests are cfg-gated (TSX undertypescript), verified with-pisolated builds andcheck-test-lang-gates-compare. The AST crate is outsideSTABILITY.md; no new root re-exports;SpaceKind/TokenRoleoutput contracts unchanged.
Concrete source touchpoints
big-code-analysis-ast/src/checker.rs,checker/*.rs:is_call,is_string,is_primitive,is_func,is_closure,is_func_space,is_else_if,is_doc_target,is_doc_comment.big-code-analysis-ast/src/getter.rs,getter/*.rs,getter/js_family.rs,getter/generic_angles.rs:get_op_typearms,get_space_kind.macros/kind_sets.rsfolds into rows.src/metrics/abc.rs,abc/*.rs:*_condition_scores_itself,*_count_token_condition,*_wrapper_operand.src/metrics/cyclomatic.rs(impl_cyclomatic_*),cyclomatic/*.rs,cognitive/*.rs,nexits.rs(impl_exit_match_kinds!),npm/*.rs,npa/*.rs.src/spaces/compute.rs,src/ops.rs,src/function.rs,src/suppression.rs,big-code-analysis-ast/src/parser.rs: entry-point callers.utils/: census, coverage and bypass gates wired intomake pre-commitandmake ci.
Sub-issue roadmap
- R01: #1691 — design(ast): fix the role inventory and prove the role-table macro under the gates
- R02: #1692 — feat(ast): add the role census, kind-coverage ratchet and bypass lint
- R03: #1693 — refactor(getter): declare Modifier, DeclKeyword and Primitive through role tables
- R04: #1694 — refactor(abc,cyclomatic): declare Comparison and ShortCircuit in Java, Python and C++ with every consumer — go/no-go
- R05: #1695 — refactor(abc,cyclomatic): declare the remaining paying constructs by role across languages
- R06: #1696 — refactor(checker,getter): declare FuncOpener, Closure, Container and opens_space; migrate entry-point callers
- R07: #1697 — refactor(cognitive,nexits): declare Branch, Switch, Loop, Catch, ElseIf and Exit
- R07's close-out reviews moved three items onto R11: #1701 (named
pays []mode read byrole_tables.py; theSwitchbody projection decision; tier-1 homes for the drift drivers) and two onto R10: #1700 (the Elixir rows-vs-consumer test as a retirement candidate; the 26 R07 fixtures as corpus rows). Outside the epic: refactor(ast): key the word slot on the walk's visited node, or fold it into a typed NodeMemo #1737 (word-slot keying) and feat(cli): path-scoped threshold overrides in bca.toml, replacing 34 identical dispatcher suppressions #1738 (path-scoped thresholds).
- R07's close-out reviews moved three items onto R11: #1701 (named
- R08: #1698 — refactor(checker,getter): declare Call and StringLit; fold the alterator and find/count consumers
- R09: #1699 — refactor(checker): declare DocTarget, DocComment and Docstring rows ahead of the documentation epic
- R10: #1700 — test(parity): derive cross-consumer parity from the role tables beside the hand-written suites (implemented on
fix/issue-1700, three commits on7daa6079; awaiting the PR) - R11: #1701 — docs(ast): document the role tables and close out (PR docs(ast): document the role tables and close out the series (#1701) #1803, merged 2026-11-04 as
c1506be3)
Dependency and completion policy
R01 -> R02 -> R03 (pilot)
R03 -> R04 (vertical slice; ARCHITECTURAL GO/NO-GO — nothing below starts before R04 records "go")
R04 -> R05, R06, R08
R05 + R06 -> R07
R03 + R06 -> R09, which must close before #1655 freezes its contract (R09 does not wait for R07)
R05 + R06 + R07 + R08 + R09 -> R10
all -> R11 (the census must show zero `pending` cells here, not earlier)
R03: #1693 proves the mechanism on the easiest case (one consumer, byte-comparable vocabularies); R04: #1694 proves it on the hard ones (a projection, an owner-only construct, a subtree suppression, three consumers) before the remaining packages are committed to. R09: #1699 is sequenced early so the documentation epic can build on rows.
Relation to open issues
Closed by R03: #1598 (shared list as the C-family fragments; test helpers merged), #1484 (superseded), #1600 (guard kept; C# recorded as the directive-keyword outlier in the book). Filed by the series and left open as output-moving fixes, each with the row/gate shape its fix should take recorded on the issue: #1703 (Java string-template wrapper — the StringLit row's wraps set is declared, R08 wires it), #1704 (Groovy var, excluded on its DeclKeyword row), #1644 (Perl __END__ — a gated_by on SpecialLiteral plus excludes for the terminator kinds, under R08). Partial: #1599 (text-keyed classification and key-selection hook, R06: #1696), #1299 (shared recognition may reduce duplicate classification; template-payload traversal and identifier widening remain a separate fix), #1644. Mechanism for: #1458's measured sweeps (R02: #1692, R10: #1700) — not a replacement. Consumers: #1654–#1682 (R09: #1699 precedes #1655). Other work includes text-keyed policy (#1602, #1401, #1399, #1604), aggregation (#1284), test infrastructure (#1480) and grammar defects (#1445, #1408).
Measured so far
R01–R03 (#1705), against the "what this does and does not claim" section above:
- The census is at 25 languages × 29 roles, 501
pendingcells, 224 declared orabsent; everyModifier/DeclKeyword/Primitivecell is declared orabsent. The coverage ratchet holds 7,350 baselined kind ids, down 492 from R02's bootstrap, every change remove-only. The bypass baseline is 125 pre-migration sites, unchanged; R03 added 29role-bypass: allowexemptions, none for membership. - Zero drift held per commit:
bca opsandbca metricsJSON over 2,938 corpus and fixture files, 25 languages, byte-identical before and after each migration commit. - The refactor(getter/c-family): one kind table for the four copied modifier lists #1598 case the design was motivated by now fails mechanically: dropping
Volatile2fromc_family_modifiers!failsmake role-coveragenaming__volatile__in C, Cpp, Mozcpp and Objc, plus three spelling tests. Thirty-three single-edit row and gate perturbations each fail a test; the eleven row kinds no corpus file spelled gained per-language spelling tests when a review pass found them. - Cost: the Halstead walk is inside run-to-run noise (six interleaved criterion rounds;
corpus/walk/all21.4 → 21.3 ms); each language's row predicates compile to oneswitch i16with gates reached only on the gated kinds;make bench-scalingpasses, everyhalstead/probe at 0.98–1.08. - Two of the "does not claim" items showed up as predicted and were handled by review, not by a gate: the TS/TSX and Cpp/Mozcpp rows are byte-level copies (contract 5, R04), and a claimed kind's reading — Groovy
varas a declaration keyword, Pythonglobalas a scope statement — is a judgement the row now records withexcludes, which no gate could have made.
Verification and completion evidence
Every sub-issue records: the commands run and their output per language (a sibling row without output is an assertion, not a measurement — #1458); the BCA_GATE: line of a captured make pre-commit log; zero-drift evidence (corpus bca ops diff, .snap.new and .pending-snap counts, then a plain run); and, for walk-adjacent packages, bench-scaling exponents and a chain-audit pass.
Activity
dekobon commentedon Oct 23, 2026
Tracking is ready
The master issue now links all eleven work packages in execution order. Each sub-issue has its native parent and blocked-by relationships configured. Start with R01: #1691; R04: #1694 requires a recorded go/no-go decision before its dependents proceed. R09: #1699 is also a completion prerequisite for #1655, so the documentation epic can use the shared recognition definitions without duplicating the kind lists.
This publication establishes the work plan; it does not complete any implementation or close an existing defect report. Each package must record its own source checks, behavior-preservation evidence and validation results before closure.
dekobon commentedon Oct 23, 2026
R01, R02 and R03 are merged to
mainin #1705 (bdd70eb9). The body is updated: the roadmap ticks the three, a status line names R04 as the go/no-go that gates everything below, contracts 4, 5 and 8 record what the packages settled (the requiredget_keyword_role, kind fragments for the dialect families,excludesas a row attribute), the open-issue relation lists #1598 / #1484 / #1600 as closed and #1703 / #1704 / #1644 as the output-moving fixes the series filed and left for later, and a "Measured so far" section carries the census, ratchet, zero-drift, perturbation and cost numbers. Per-package evidence is on each sub-issue's completion comment.3 remaining items
dekobon commentedon Oct 25, 2026
Status correction. R04/R05 are on
main(rebased and fast-forwarded;f23fb7bc..c65df636+ post-close fixes through7425561f);refactor/1694-1695-paying-rolesno longer exists. Re-verified on that base before starting R06/R08 — evidence on #1695. R06 (#1696) and R08 (#1698) proceed onrefactor/1696-1698-recognition-rolesfrom7425561f.dekobon commentedon Oct 25, 2026
R06 (#1696) done.
FuncOpener,Closure,Container,Memberandopens_spacedeclared in every language and read by every checker, getter, member walk and cognitive boundary / lambda arm, onrefactor/1696-1698-recognition-roles(f38eeb54, ten commits on7425561f, not pushed, no PR). #1599 closed through theChecker::keys_operator_by_texthook; #1299's residue named and left. Zero drift on every population and on the recovery sweeps; every gate passes on every commit. The completion record, specification corrections, entry-point audit table and review dispositions are on #1696.Three things R07–R10 should take from it:
FuncOpeneris "opens a space" and the named-function subset is acallable/callable_when fmark — a third fact the two-declaration framing of contract 2 needed; each table'sCALLABLEis gates-aside, so a consumer whose ancestors can carry a gated-away spelling (C#'s bodied property) keeps its own stop list; and a byte-less wrapper must forward with a known-empty chain, neverAncestors::unknown(), which climbs — the oracles cannot see that spelling, only a direct pin can. R08 starts now.dekobon commentedon Oct 25, 2026
R08 (#1698) done.
CallandStringLitdeclared in every language and read byis_call/is_string(both spellings), every getter's interpolation set, the dump's string flattening and the ABC branch count, onrefactor/1696-1698-recognition-roles(f58ade41, twelve commits on7425561f, not pushed, no PR). Zero drift on every population and the recovery sweeps; every gate passes on every commit; the bypass baseline is down from 84 to 10, all of them Elixir's documented byte-less kind table (#275) and theExitconsumer that R07 takes. The completion record, the alias findings per grammar, the exemption list and the review disposition are on #1698.For R07 / R09 / R10: the byte-less
is_call/is_stringread the row's kind table (lists_kind), not a known-empty chain — the two differ where a gate reads "no parent" as something (the Tcl body), so choose per row and pin the disagreement; a row-forwarding macro must bind one kind to one row for the bypass gate to recognise it; and a scripted migration over sibling arms needs a per-language guard test before the sweep, since the C alterator arm the pass skipped changed nothing any gate measures.dekobon commentedon Oct 26, 2026
R06 / R08 post-landing review pass (
18ee3c98)Five reviewers read the branch after it closed #1696 and #1698 (the
reviewchecklist, performance / recursion, least-reviewed code, acceptance criteria against this epic's contracts, astra-review). What changed against the contracts here:(node, code, ancestors)) gains a pin. Each row emitsTEXT_GATED; the byte-lessnargs/nomreaders assert the rows they read stay without a text gate, so refactor(checker,getter): declare FuncOpener, Closure, Container and opens_space; migrate entry-point callers #1696 AC 3's exemptions are enforced rather than argued.npa/npmhanded a body's chain to the row for each member;Ancestors::of_children(one allocation per body) now builds the member's chain at all 20 sites, which also retires the lastAncestors::unknown()on those walks. A gate aMember/FuncOpenerrow grows later reads the right parent.StringLitrows are declared,impl_simple_is_string!is gone, census 272 pending / coverage 6536 / bypass 10 (remove-only).nom/wide-arrow-arraynow measures that shape.The #1696 body's step 1 (ObjC
Primitiveis kind-only) and step 5 (TemplateDeclarationstays with #1299) are corrected; #1698's "all twenty-five" and "text_gated_by perl_bareword_is_call" (it isgated_by) are noted there.refactor(checker,getter,npa): declare Java's space tree and members b…
refactor(roles,metrics,checker): take the post-landing review passes …
dekobon commentedon Oct 26, 2026
R06, R08 and #1599 merged — PR #1717 (
e2fd7193)Rebase-merged to
mainas a fast-forward of 13 commits on167cb506; all 52 checks green. The branch was rebased before merge, so every hash in the R06 / R08 comments above is dangling —mainnow holds: seven per-language-family space-tree commits, the #1599 hook, the R06 completion review, the R08 call / string rows, one commit for the post-landing review passes, lesson 103, and a coverage commit that took the patch from 94.7 % to the three documented-unreachable guards.Gates on
mainat the merge: role-census 272 pending (no cell ofFuncOpener,Closure,Container,Member,Call,StringLitin any language), role-bypass 10 baselined sites, role-coverage 6,536;make pre-commitpass; chain-audit pass; bench 88 / 88. Zero drift against7425561f: 0 of 27,024 corpus outputs and 0 of 129,042 recovery outputs.Open bugs touched by the change were updated: #1401 (the Ruby npm walk now reads the row under the argument's own chain; the two gaps are two places it does not yet ask), #1706 (
keys_operator_by_textunblocks option 1), #1299 (the fix lands once incpp_family_npa!).The roadmap's next edges are R07 (#1697, on R05 + R06) and R09 (#1699, on R03 + R06, which does not wait for R07).
dekobon commentedon Oct 31, 2026
R07 (#1697) is implemented on
refactor/1697-control-flow-rolesand ticked above; the body's status paragraph carries the summary and #1697's completion comment the evidence. Two things worth noting for the remaining packages:LEAF_KINDSandpays_at_token_kind; thepays []split row (owners, no payers,pays_itselfowners paying at the leaf) is the shape every dedicated-clauseElseIfuses, andelse_if_parent_clause_gate!/else_if_prev_sibling_gate!inroles/control_flow.rsare the two shared gate emitters — R09's rows will not need either but the pattern is there.WordSlotonFoldCache,Ancestors::classified); a future row that reads acommand/calltarget should go throughtcl_command_word/elixir_call_targetrather than the uncached readers, unless it asks about an enclosing node (where the cached read would only evict the visited one's entry — seeelixir_call_keyword's doc).Merge prerequisites: the output-submodule push (
af5c6222, #1720's snapshots) and the PR.dekobon commentedon Oct 31, 2026
R07 close-out: where its follow-ups went
The two final reviews of R07: #1697 (alignment against this epic's ten contracts; architecture on its own terms) found a small set of drift, all toward the AST crate, all fixed on the branch. What they left were follow-ups, sorted by whether they belong to the series:
Epic steps (added to the package bodies, with a comment on each):
pays []split-empty row mode and havecanonical()andutils/role_tables.pyemit it, sopython_reader_agrees_with_the_compiled_tablescan see it (23 rows today); decide theSwitchrow's body projection (is_switch_body_kindin Java and Groovy: DSLbodyattribute or documented convention); tier-1utils/homes for the drift drivers (oracle.py,compare.py,recovery_oracle.py,perturb.py,coverage_diff.py,ir_check.sh, the verbatim-oracle source) that R09 and R10 will need again.elixir_consumer_word_policy_agrees_with_the_control_flow_rowsis one hand-written instance of R10's derived test and its retirement candidate (perturb both sides first); R07's 21 per-language control-flow fixtures and 5 Tcl/Elixir C5b fixtures become the corpus rows for the six control-flow roles.Standalone issues (parse-layer cache design and a CLI feature; neither is a role declaration, so not gating the umbrella):
WordSlotcache on the walk's visited node, or fold it into a typedNodeMemo<T>, so the "which call sites may use the cached read" judgement stops being a convention. R07's read-budget tests pin it in the meantime.[[thresholds.path]]inbca.toml, replacing the 34 identicalsuppress(cyclomatic, halstead)dispatcher markers (16 from R07) with one path-scoped limit.Roadmap line for R07 updated in the body to point at these.
refactor(roles,cyclomatic,cognitive,nexits,abc,getter): declare Branc…
dekobon commentedon Nov 1, 2026
R09 (#1699) is implemented and reviewed on
refactor/1699-doc-roles, awaiting the PR; the body's R09 box is ticked and a dated status paragraph added. Summary and the evidence record are on #1699 (completion comment); DSL follow-ups on #1701, parity rows on #1700. Next: R10 (#1700), which R09 was the last prerequisite for.refactor(roles,checker): declare the documentation rows and read them…
dekobon commentedon Nov 4, 2026
Series complete: R01–R11 landed
Every package closed with its own evidence; this comment is the umbrella's, per the acceptance criteria on #1701, each with the command that produced it. Taken on
fix/issue-1701atc1506be3(= the merged tree), 2026-11-04.1. Census: zero
pending./utils/check-role-census.py --matrix --quiet— 25 languages × 29 roles, 725 cells declared orabsent, 0pending;.role-census-baseline.txtholds no entries.2. Coverage residue, per language
rg -c '^[0-9]+ ' .role-coverage-baseline.txt— 5,998 records (5,999 before R11 recorded Groovy'sswitch_blockunderexcludes). Per language (awk '/^\[/{if(sec!="")print sec, n; sec=$1; n=0; next} /^[0-9]+ /{n++} END{print sec, n}' .role-coverage-baseline.txt):Mozcpp 500 · Objc 443 · Csharp 429 · Cpp 405 · Php 332 · Perl 325 · Tsx 287 · Typescript 271 · C 265 · Rust 264 · Ruby 264 · Bash 232 · Java 230 · Python 218 · Groovy 209 · Elixir 204 · Mozjs 197 · Kotlin 197 · Javascript 196 · Go 170 · Irules 119 · Lua 112 · Tcl 85 · Preproc 32 · Ccomment 12.
Every entry is a kind id no row claims, excluded by none, under the pinned grammar version; the ratchet only removes.
3. Bypass baseline at zero
rg -v '^#|^\s*$' .role-bypass-baseline.txt— empty. The four pre-migration entries were Elixir's byte-lessis_func_space/get_space_kind, reclassified as three documented subset exemptions (// role-bypass: allow(…), the lint's own clause for a consumer-chosen subset of a row's owners), not removed as duplicated lists; the derived forward-parity test is what sees a kind added inside those spans. 308allowexemptions remain in the boundary, each naming a role that claims the kind.4.
kind_id()reads inside the consumer boundaryLiteral
kind_id()-matching lines over the 17BOUNDARYpaths ofutils/check-role-bypass.py:95-113, counted withrg -c 'kind_id\(\)'per path and summed: 815 atf3527dfd(the design's inventory tree, 2026-10-22) → 511 atc1506be3, −304 (37%). Two caveats: it counts matching lines, not dispatch sites (a line with two calls counts once, a comment namingkind_id()counts); and many survivors are the intended shape — onelet kind = node.kind_id();feeding severalowns_kind(kind, …)row reads.5. Issues, by provenance (every one CLOSED, re-checked 2026-11-04)
varas a declaration keyword #1704, fix(halstead): Perl __END__ data section counted as one operand #1644.6. What stays manual
The one list is the "What the mechanism cannot see" section of
big-code-analysis-ast/src/roles/mod.rs's module doc, with the manual check for each item; AGENTS.md,.claude/rules/and the book link to it. Its headings: a wrong-role claim (review against abca dump, every(kind, parent)pair given a row or a stated exclusion; a per-language spelling test for a kind no corpus spells); an incomplete entry (the §1 alias inventory —role-coveragefails only on an id nobody claims); a forward whose dropped context no gate reads yet (TEXT_GATEDpins the declared owner byte gates; nothing pins the chain, #1793); a consumer reading the wrong projection, or nothing, and the text-keyed policy no kind spells (§7's cross-walk, §11's one input per path); and the shapes the bypass lint does not scan. Lesson 115 records the series' conclusion: sharing the rule left the list at every caller; sharing the list leaves the judgement.Not landed: R09's per-node
doxygen-oraclecrate (one-off; its claim is on #1699 with counts).dekobon commentedon Nov 4, 2026
R01–R11 all closed; the completion comment above carries the evidence per the R11 acceptance criteria. Closing the umbrella.