Skip to content

refactor(ast): one role declaration per language for Checker, Getter and metric dispatchers #1690

Closed
11 of 11 issues completed

Description

@dekobon

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_itself receives — and payers — the token kinds a counting arm fires on — plus an optional projection from an owner to the child that pays (BinaryExpression → its operator field). 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 the ComparisonOperator wrapper, Halstead at its operator leaf (getter/python.rs) — the row carries one projection per occurrence named structurally — pays_at(Wrapper) and pays_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_slot in src/metrics/abc.rs. Unchanged by this series.
  • Census: a gate asserting every (language, role) pair is declared — rows, absent(reason), or pending(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

  1. 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).
  2. Recognising a closure and opening a space are two declarations. Closure says what is a closure (nom, nargs, cognitive need it); opens_space says whether it gets a SpaceKind (Python's Lambda is a closure and opens nothing — checker/python.rs, getter/python.rs). The space-tree parity test compares promotion against opens_space, never against recognition.
  3. 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 equal SpaceKind mappings coalesce into one arm so clippy::match_same_arms is not tripped. Macro-source formatting is verified with check-rustfmt-bail.py --show; clean expanded Rust proves nothing about the macro body (.claude/rules/formatting.md).
  4. One canonical entry-point spelling. The table emits (node, code, ancestors). The byte-less Checker::is_* and Getter::get_space_kind stay 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's promotes_to_func_space_with_code stays the single combined lookup. make bench-scaling and make chain-audit run 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, and keyword_or_op_type reads it ahead of the byte-less get_op_type on every path the walk takes, so no get_op_type lists a keyword a row owns.
  5. 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.
  6. Text-keyed roles are declared, not exempted — Elixir call targets, Ruby visibility, Perl barewords, Tcl command names, Groovy/ObjC is_primitive — through text_gated_by fn(&Node, &[u8], Ancestors) -> bool (lesson 22; grammar-dispatch.md §9–§10).
  7. 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 BooleanOperator through its operands while cyclomatic counts its and/or tokens; Bash &&/|| are not ABC conditions (fix(metrics): Perl xor and Bash -a/-o miss metric parity #1536). Kotlin infix and/or/xor and Perl/PHP xor are 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++ requires suppresses decisions throughout its subtree — fix(cyclomatic/cpp): rvalue refs and operator&& count as decisions #1525), count_boolean_slot and the peel stay as mechanisms. Nothing in the AST crate names a metric.
  8. 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.json is only candidate discovery. A row's excludes [K "why"] records which role's author considered and rejected a kind (R01 chose the row attribute over a standalone Excluded(role, reason) entry); the coverage guarantee itself is global (claimed, excluded or baselined), not per-role correctness.
  9. Zero drift per refactor. A migration preserves bca ops vocabularies, metric counts and space trees byte-for-byte on the integration corpus; both snapshot forms are captured under INSTA_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.
  10. Gates and API. Role modules are unconditional like checker/*.rs; only derived tests are cfg-gated (TSX under typescript), verified with -p isolated builds and check-test-lang-gates-compare. The AST crate is outside STABILITY.md; no new root re-exports; SpaceKind / TokenRole output 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_type arms, get_space_kind. macros/kind_sets.rs folds 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 into make pre-commit and make ci.

Sub-issue roadmap

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 pending cells, 224 declared or absent; every Modifier / DeclKeyword / Primitive cell is declared or absent. 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 29 role-bypass: allow exemptions, none for membership.
  • Zero drift held per commit: bca ops and bca metrics JSON 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 Volatile2 from c_family_modifiers! fails make role-coverage naming __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/all 21.4 → 21.3 ms); each language's row predicates compile to one switch i16 with gates reached only on the gated kinds; make bench-scaling passes, every halstead/ 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 var as a declaration keyword, Python global as a scope statement — is a judgement the row now records with excludes, 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

  1. added
    enhancementNew feature or request
    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 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.

  3. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    R01, R02 and R03 are merged to main in #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 required get_keyword_role, kind fragments for the dialect families, excludes as 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.

  4. 3 remaining items

  5. dekobon commented on Oct 25, 2026

    @dekobon
    OwnerAuthor

    Status correction. R04/R05 are on main (rebased and fast-forwarded; f23fb7bc..c65df636 + post-close fixes through 7425561f); refactor/1694-1695-paying-roles no longer exists. Re-verified on that base before starting R06/R08 — evidence on #1695. R06 (#1696) and R08 (#1698) proceed on refactor/1696-1698-recognition-roles from 7425561f.

  6. dekobon commented on Oct 25, 2026

    @dekobon
    OwnerAuthor

    R06 (#1696) done. FuncOpener, Closure, Container, Member and opens_space declared in every language and read by every checker, getter, member walk and cognitive boundary / lambda arm, on refactor/1696-1698-recognition-roles (f38eeb54, ten commits on 7425561f, not pushed, no PR). #1599 closed through the Checker::keys_operator_by_text hook; #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: FuncOpener is "opens a space" and the named-function subset is a callable / callable_when f mark — a third fact the two-declaration framing of contract 2 needed; each table's CALLABLE is 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, never Ancestors::unknown(), which climbs — the oracles cannot see that spelling, only a direct pin can. R08 starts now.

  7. dekobon commented on Oct 25, 2026

    @dekobon
    OwnerAuthor

    R08 (#1698) done. Call and StringLit declared in every language and read by is_call / is_string (both spellings), every getter's interpolation set, the dump's string flattening and the ABC branch count, on refactor/1696-1698-recognition-roles (f58ade41, twelve commits on 7425561f, 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 the Exit consumer 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_string read 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.

  8. dekobon commented on Oct 26, 2026

    @dekobon
    OwnerAuthor

    R06 / R08 post-landing review pass (18ee3c98)

    Five reviewers read the branch after it closed #1696 and #1698 (the review checklist, performance / recursion, least-reviewed code, acceptance criteria against this epic's contracts, astra-review). What changed against the contracts here:

    • Contract 4 (every emitted predicate takes (node, code, ancestors)) gains a pin. Each row emits TEXT_GATED; the byte-less nargs / nom readers 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.
    • Rows are asked under the queried node's own chain. npa / npm handed 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 last Ancestors::unknown() on those walks. A gate a Member / FuncOpener row grows later reads the right parent.
    • R08 is complete for 25 languages: Ccomment's and Preproc's StringLit rows are declared, impl_simple_is_string! is gone, census 272 pending / coverage 6536 / bypass 10 (remove-only).
    • Two pre-existing performance defects on touched paths: the Python attribute walk is iterative (stack overflow at ~8,000 nesting levels), and the JS arrow binding gate's sibling scan is memoised per parent (4,000 positional arrows: 3.4 s → 0.01 s); nom/wide-arrow-array now measures that shape.
    • Evidence on HEAD: corpus 0 differing of 27,008 compared (the earlier "26,992" was a miscount of 27,024-record runs; two R05 fixtures lost to a host restart were rebuilt from their recorded exports, two could not be), recovery 0 of 124,488, chain-audit pass, bench 87/87 (worst 1.12).

    The #1696 body's step 1 (ObjC Primitive is kind-only) and step 5 (TemplateDeclaration stays with #1299) are corrected; #1698's "all twenty-five" and "text_gated_by perl_bareword_is_call" (it is gated_by) are noted there.

  9. dekobon commented on Oct 26, 2026

    @dekobon
    OwnerAuthor

    R06, R08 and #1599 merged — PR #1717 (e2fd7193)

    Rebase-merged to main as a fast-forward of 13 commits on 167cb506; all 52 checks green. The branch was rebased before merge, so every hash in the R06 / R08 comments above is dangling — main now 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 main at the merge: role-census 272 pending (no cell of FuncOpener, Closure, Container, Member, Call, StringLit in any language), role-bypass 10 baselined sites, role-coverage 6,536; make pre-commit pass; chain-audit pass; bench 88 / 88. Zero drift against 7425561f: 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_text unblocks option 1), #1299 (the fix lands once in cpp_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).

  10. dekobon commented on Oct 31, 2026

    @dekobon
    OwnerAuthor

    R07 (#1697) is implemented on refactor/1697-control-flow-roles and 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:

    • The row DSL gained nothing new for R07 beyond LEAF_KINDS and pays_at_token_kind; the pays [] split row (owners, no payers, pays_itself owners paying at the leaf) is the shape every dedicated-clause ElseIf uses, and else_if_parent_clause_gate! / else_if_prev_sibling_gate! in roles/control_flow.rs are the two shared gate emitters — R09's rows will not need either but the pattern is there.
    • Text-classified languages now have a per-visited-node word cache (WordSlot on FoldCache, Ancestors::classified); a future row that reads a command / call target should go through tcl_command_word / elixir_call_target rather than the uncached readers, unless it asks about an enclosing node (where the cached read would only evict the visited one's entry — see elixir_call_keyword's doc).

    Merge prerequisites: the output-submodule push (af5c6222, #1720's snapshots) and the PR.

  11. dekobon commented on Oct 31, 2026

    @dekobon
    OwnerAuthor

    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):

    • R11: #1701 — name the pays [] split-empty row mode and have canonical() and utils/role_tables.py emit it, so python_reader_agrees_with_the_compiled_tables can see it (23 rows today); decide the Switch row's body projection (is_switch_body_kind in Java and Groovy: DSL body attribute or documented convention); tier-1 utils/ 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.
    • R10: #1700 — elixir_consumer_word_policy_agrees_with_the_control_flow_rows is 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):

    Roadmap line for R07 updated in the body to point at these.

  12. dekobon commented on Nov 1, 2026

    @dekobon
    OwnerAuthor

    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.

  13. dekobon commented on Nov 4, 2026

    @dekobon
    OwnerAuthor

    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-1701 at c1506be3 (= the merged tree), 2026-11-04.

    1. Census: zero pending

    ./utils/check-role-census.py --matrix --quiet — 25 languages × 29 roles, 725 cells declared or absent, 0 pending; .role-census-baseline.txt holds no entries.

                Call             StringLit        Comparison       ShortCircuit     EagerLogical     Ternary          NullSafe         CaseArm          Guard            Branch           Switch           Loop             Catch            ElseIf           Exit             Modifier         DeclKeyword      Primitive        FuncOpener       Closure          Container        Member           Comment          DocComment       Docstring        DocTarget        DocRunOpener     DocRunDirective  HoldsDocInBody 
    Javascript  rows             rows             rows             rows             absent           rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Mozjs       rows             rows             rows             rows             absent           rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Java        rows             rows             rows             rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           rows           
    Go          rows             rows             rows             rows             absent           absent           absent           rows             absent           rows             rows             rows             absent           rows             rows             absent           rows             absent           rows             rows             rows             rows             rows             absent           absent           rows             absent           rows             absent         
    Kotlin      rows             rows             rows             rows             rows             absent           rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           rows           
    Lua         rows             rows             rows             rows             absent           absent           absent           absent           absent           rows             absent           rows             absent           rows             rows             absent           rows             absent           rows             rows             rows             absent           rows             absent           absent           rows             rows             absent           absent         
    Rust        rows             rows             rows             rows             rows             absent           absent           rows             rows             rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Tcl         rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             rows             rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             absent           rows             absent           absent           absent           absent           absent           absent         
    Irules      rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             rows             rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             absent           rows             absent           absent           absent           absent           absent           absent         
    C           rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             absent           rows             absent           rows             rows             absent           rows             absent           absent           absent         
    Cpp         rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Mozcpp      rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Objc        rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Csharp      rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           rows           
    Elixir      rows             rows             rows             rows             absent           absent           absent           rows             rows             rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             rows             rows             rows             rows             absent           absent           absent           absent           absent           absent         
    Python      rows             rows             rows             rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           rows             rows             rows             rows             rows             absent           rows             absent           absent           absent           absent         
    Tsx         rows             rows             rows             rows             absent           rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Typescript  rows             rows             rows             rows             absent           rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Bash        rows             rows             rows             rows             rows             rows             absent           rows             absent           rows             rows             rows             absent           rows             rows             absent           rows             absent           rows             absent           rows             absent           rows             absent           absent           absent           absent           absent           absent         
    Ccomment    absent           rows             absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           rows             absent           absent           absent           absent           absent           absent         
    Preproc     absent           rows             absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           absent           rows             absent           absent           absent           absent           absent           absent         
    Perl        rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           rows             absent           rows             rows             absent           rows             absent           rows             rows             rows             absent           rows             rows             absent           absent           absent           absent           absent         
    Php         rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           absent         
    Ruby        rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           rows             rows             rows             rows             rows             absent           absent           rows             rows             rows             absent         
    Groovy      rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             rows             absent           rows             absent           absent           rows           
    

    2. Coverage residue, per language

    rg -c '^[0-9]+ ' .role-coverage-baseline.txt — 5,998 records (5,999 before R11 recorded Groovy's switch_block under excludes). 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-less is_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. 308 allow exemptions remain in the boundary, each naming a role that claims the kind.

    4. kind_id() reads inside the consumer boundary

    Literal kind_id()-matching lines over the 17 BOUNDARY paths of utils/check-role-bypass.py:95-113, counted with rg -c 'kind_id\(\)' per path and summed: 815 at f3527dfd (the design's inventory tree, 2026-10-22) → 511 at c1506be3, −304 (37%). Two caveats: it counts matching lines, not dispatch sites (a line with two calls counts once, a comment naming kind_id() counts); and many survivors are the intended shape — one let kind = node.kind_id(); feeding several owns_kind(kind, …) row reads.

    5. Issues, by provenance (every one CLOSED, re-checked 2026-11-04)

    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 a bca 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-coverage fails only on an id nobody claims); a forward whose dropped context no gate reads yet (TEXT_GATED pins 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-oracle crate (one-off; its claim is on #1699 with counts).

  14. dekobon commented on Nov 4, 2026

    @dekobon
    OwnerAuthor

    R01–R11 all closed; the completion comment above carries the evidence per the R11 acceptance criteria. Closing the umbrella.

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

    enhancementNew feature or requestrefactorCode maintainability / tech-debt cleanup (no behaviour change)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions