Repository navigation
Description
Objective
Declare FuncOpener, Closure, Container and Member per language, with a separate opens_space mapping; route is_func, is_closure, is_func_space, get_space_kind and the npm/npa member walks through them; audit every byte-less entry-point caller and migrate the ones that are actually byte-less.
Umbrella: R00: #1690 · Planning ID: R06: #1696
Completion prerequisites: R04: #1694 Completion of R04: #1694 includes a recorded go decision.
Why this exists
The space tree is built from three predicates and one mapping that must agree, and the failure when they do not is silent: a node the walker promotes but the getter leaves Unknown is not a member scope, so its space serialises no npm/npa block, which looks exactly like a language with no containers (grammar-dispatch.md §7). Today the agreement is held by a book paragraph (metrics.md: "In Python, Java, Kotlin and C++ a lambda opens no space") and tested nowhere — tests/parity/ covers ops-vs-metrics, not checker-vs-getter. Measured: a C++ lambda's if is charged to the enclosing function with no anonymous space; Rust and Go open one. Python recognises Lambda as a closure (checker/python.rs) and maps it to Unknown (getter/python.rs) — recognition and space-opening are two facts, and the rows keep them apart (umbrella contract 2).
The entry-point audit lives here because these predicates are the ones whose byte-less spelling is wrong for Elixir (defmodule/def are Call nodes told apart by target text, #275) and whose forwarding default hid that from bca ops for a release (#1130). A Node carries no bytes or chain, so the byte-less form cannot be made correct by forwarding; it stays as a documented legacy wrapper and each remaining byte-less caller moves.
Implementation steps
- Rows per language:
FuncOpener,Closure,Container,Member(fields/properties npm/npa count), each withopens_space => SpaceKind | None. Gates as named fns: C#IndexerDeclaration/PropertyDeclarationoncsharp_member_has_accessors(fix(nom/wmc): C# indexer triple-counted as a function (IndexerDeclaration + get/set accessors) #464, fix(nom/wmc): C# expression-bodied property undercounts (0 vs npm 1) #472 — lesson 65: gate, never delete); JavaObjectCreationExpressiononhas_class_body(fix(spaces): anonymous class/object bodies open no func-space (Kotlin object_literal, Java/Groovy anonymous classes) #463); C++StructSpecifier/ClassSpecifieroncpp_specifier_has_body(fix(cpp): forward declarations open struct/class spaces #1555); RubyBlockon parent-not-Lambda; Elixirtext_gated_byon the call target; GroovyPrimitiveastext_gated_by; ObjC'sPrimitiveis kind-only, its text-keyed words are theModifierrow's gatedidentifierplus thekeys_operator_by_texthook (refactor(checker): is_primitive answers true for every Groovy/ObjC identifier #1599). -
Checker::is_func_with_code/is_closure/is_func_space_with_codereadowns_FuncOpener/owns_Closure/opens_space != None;Getter::get_space_kind_with_codereadsopens_space;promotes_to_func_space_with_codestays as the one combined Elixir lookup. - Audit each listed caller —
big-code-analysis-ast/src/parser.rs,src/ops.rs,src/spaces/compute.rs,src/function.rs,src/suppression.rs,src/metrics/nargs.rs(16 call sites),halstead.rs(14),nom.rs,nexits.rs,loc.rs,npm.rs— enumerate every call first (AGENTS.md public-API rule); several already use the contextual form (parser.rsfind/count,ops.rs); migrate only the byte-less ones and list them. - For refactor(checker): is_primitive answers true for every Groovy/ObjC identifier #1599, migrate Halstead's text-key selection away from the byte-less
is_primitivedependency to a code-aware hook. Adding the role alone is insufficient. Test distinct primitive spellings and ordinary identifiers in both Groovy and ObjC while preserving their current vocabularies. - npm/npa member walks read
owns_Container/owns_Member. The Npm/Npa disagreement on C++TemplateDeclarationnamed in fix(npa/cpp): templated static data members are invisible to npa #1299 is not aMember-row question and stays with fix(npa/cpp): templated static data members are invisible to npa #1299 (npm/cpp.rsstill reads the wrapper for templated member functions;npa/cpp.rscarries aFIXME(#1299)at the field-list walker). The rest of fix(npa/cpp): templated static data members are invisible to npa #1299 — template-payload traversal and plain-identifier counting innpa/cpp.rs— is likewise not supplied by a row.
Tests and edge cases
- Derived test per language: for every node in a fixture, the walker promotes it ⇔
opens_space != None— promotion against the space mapping, never againstClosurerecognition. -
tests/parity/ops_metrics_space_parity.rsunchanged and passing; the Elixir case specifically. - Lesson 65's childless variants per gated row (expression-bodied C# property,
new Foo()without a body, bodiless C++ specifier). -
make bench-scalingandmake chain-audit— these predicates sit on the walk's hot path and takeAncestors. - Zero drift on the corpus; both snapshot forms; plain run.
Acceptance criteria
- No opener/container/member kind matched inside the consumer boundary; bypass gate clean.
- The book paragraph on which languages' lambdas open a space is cross-checked against the
opens_spacemapping and says so. - Byte-less callers: audit table (file, line, byte-less or contextual, migrated or exempted with reason).
- Record the disposition of refactor(checker): is_primitive answers true for every Groovy/ObjC identifier #1599 and fix(npa/cpp): templated static data members are invisible to npa #1299. Close an original issue only after every item is implemented or explicitly superseded with rationale; otherwise retain it with named residue in both its body and a comment.
Verification and completion evidence
- Call-site audit; corpus diff;
bench-scalingexponents;chain-audit;BCA_GATE:line.
Activity
dekobon commentedon Oct 23, 2026
Tracking is ready
The body now links the master issue and the work packages it references. Native parent and blocked-by relationships match the listed completion prerequisites. Implementation and acceptance checkboxes remain open; prerequisite closure and the R04: #1694 go decision govern execution order.
dekobon commentedon Oct 25, 2026
Done —
FuncOpener,Closure,Container,Memberandopens_spacedeclared in every language; entry-point callers audited; #1599 closedBranch
refactor/1696-1698-recognition-rolesfrom7425561f(main), nine commits, not pushed, no PR. Per slice, one commit with its measured evidence in the message; a tenth takes the completion review's findings.803fceb2314a8c239b21b591456e5ee65a367017809f2726b173f3008bd5a700f38eeb54No
pending R06cell remains; the 40 remaining bypass sites are allCall/StringLit(R08). Coverage ratchet −182 ids over the package, every change remove-only.Representation (umbrella contract 2, §3 amendment).
FuncOpeneris "opens aFunctionspace"; its named functions carrycallable(whatis_funcanswers — a Javastatic { … }or a Kotlin accessor opens a space and is not one) orcallable_when f(a JS function expression is a function only where bound; theis_js_func!ancestor walk moved onto the rows asjs_family_binding_gates!, sixchecker.rsmacros deleted).opens_by flabels a kind that spells two containers (Kotlin'sclass_declaration). Each table emitsis_callable,CALLABLE(the marked kinds, gates aside) and theopens_spacefold;utils/role_tables.pyreads all three attributes; thedsl_gatesproof table and the census self-tests pin them.Memberrecognises the declaration — a field, a property, a Pythonself.x/cls.xwhere it is bound, an Elixirdefstruct, a Ruby@ivaras an assignment target — and leaves which body, what visibility and how many names to the consumer.Specification corrections, recorded on the rows:
FuncOpenerwas wider thanis_func(Java'sStaticInitializer, Elixir'sAnonymousFunction), which the plan review caught before the first slice;callableis the third fact the issue's two-declaration framing needed.CALLABLElists kinds gates aside, so a consumer whose ancestors can carry a gated-away spelling keeps its own list: C#'s cognitive stop list, where a bodied property is in the list and is not a boundary — the one drift the corpus found in flight (twoDeepSpeechfiles), fixed before landing and pinned bycsharp_bodied_property_is_not_a_function_boundary_above_its_accessor.blockanddo_block(the issue text named one).Functionfor a node the walk never promoted, the "pair that fails quietly" of grammar-dispatch §7, now recorded on the rows. The book's list gains Groovy, Mozcpp and Objective-C, and is read from the rows byclosures_that_open_a_space_are_what_the_book_says.quoteddeftoo (the row declines one, fix(elixir): WMC and NPM disagree on quoted def inside defmacro #310), and its method count counts a quoteddefmodule'sdefs.is_func_space/get_space_kindforward with a known-empty chain, notAncestors::unknown(), which climbs throughNode::parentand would answer a gated result the old kind table never did (Ruby's lambda-body block; found by the completion review, pinned byruby_byte_less_spellings_stay_the_kind_table).Entry-point audit (every non-test caller of the thirteen predicates outside
checker//getter/, enumerated by search; verdict per site):src/spaces/compute.rs:883,src/ops.rs:353promotes_to_func_space_with_code, real chainsrc/spaces/compute.rs:508,src/ops.rs:170get_space_kind_with_code, real chainsrc/spaces/compute.rs:476,src/ops.rs:129get_space_kind_with_code,Ancestors::unknown()src/function.rs:65,src/suppression.rs:762,src/metrics/nom.rs:241,nargs.rs:296,709is_func_with_code, real chainbig-code-analysis-ast/src/parser.rs:151,174,183is_call_with_code/is_string_with_code/is_func_with_code, real chainsrc/metrics/nargs.rs×12 (is_func/is_closure, Go, Kotlin, Lua, Tcl, iRules, Perl, Groovy, Java) andnom.rs:245FuncOpener/Closurerow reads bytes except Elixir's, whoseNArgsalready callsis_func_with_code; the chain is passed, so the JS and Ruby gates read itsrc/metrics/npm.rs:260direct_child_funcsis_func,Ancestors::unknown()src/metrics/cognitive.rs(JS)is_funcon a hand-listed setis_callable+CALLABLEsrc/metrics/nexits.rs:567is_callCall)src/metrics/halstead.rs:407is_primitivekeys_operator_by_text(node, code, ancestors)(#1599)big-code-analysis-ast/src/getter.rs:321,checker.rs:718,726,746#1599 disposition — closed.
Checker::keys_operator_by_text(node, code, ancestors)(defaultis_primitive) is what Halstead reads; Groovy answers it from thePrimitiverow's text gate, Objective-C from that row plusnullableand theModifierrow's gatedidentifier(#1579). ObjC'sis_primitivenow answers from the row's kinds (an identifier is no primitive); Groovy's byte-less one stays the kind table, since its primitives are told by text alone, and the test pins that limitation rather than claiming it away. #1299 disposition — untouched, residue named: theTemplateDeclarationNpm/Npa disagreement it describes is not a membership question theMemberrow answers (a templated static data member is atemplate_declaration > declaration, read by declarator), so its fix stays the separate behaviour change it is.Evidence. Zero drift against the base binary on every slice and the final tree: 25 populations (Mozcpp over the C++ files, MozJS over the JS files, ccomment / preproc over fixtures), CLI
ops/metrics/dump/find -t call|string|function/countplus a library-level verbatim record (raw bytes, Alterator export, find, count): 0 of 26,992 outputs differ. Recovery sweeps per slice (every fixture cut at each line, 16 offsets, after every operator punctuation and at every word boundary, CLI + verbatim): 6,480 + 21,930 + 27,138 + 47,808 + 5,382 + 12,402 + 7,902 = 129,042 outputs, 0 differing, 0 failed executions.roles/space_tree_tests.rspins the five answers per kind in every language independently of the classifiers that now read the rows (never-emitted aliases as drift markers);src/spaces/opens_space_parity_tests.rsruns the realanalyzewalk in all 23 grammar languages and holds its promotions to theopens_spacelabels. Perturbations: ~100 across the slices — a row owner, gate orcallableremoved or added, a consumer severed from its row, a consumer readingCALLABLEwhere it must not — each failing at least one named test; five lambda-channel / stop-list arms had no guarding test and gained one (Go, PHP, C#, Python, C#'s property).make chain-auditpasses;make bench-scaling87 of 87 probes within bound, the newnom/wide-arrow-array(0.96) pinning the sibling scan the review found quadratic in the first row-based spelling of the JS walk (4,000 arrows: 0.02 s → 4 s, exponent 2.00 on the regressed form; fixed to lazy evaluation). Single-feature-pbuilds (rust, go, elixir, tcl, kotlin) green in both crates;check-test-lang-gates-compare COMPARE_REF=origin/mainloses no build.make pre-commit:BCA_GATE: pass (gate=pre-commit)on every commit. Both snapshot forms clean; no.snap.newor.pending-snap; the output submodule untouched.Reviews (astra-review, fresh context each): the plan (nine findings, all taken: the
callablefact, the ObjC text-key set, Python'sself/clsand target-position recognition, the Elixir single lookup, Ruby's two block kinds, body discovery as traversal, consumer-connection perturbations, a verbatim-EOF oracle, 25 populations); checkpoint 2 on the Java slice (one P2:callableneeded an independently gated spelling →callable_when); checkpoint 3 on the whole package (one P1: the JS sibling scan evaluated eagerly — fixed with a probe; two P2: the Ruby byte-less climb, the Ruby visibility-wrappeddefbypassingis_callable— both fixed and perturbed; one P3: the Groovyis_primitiveclaim — corrected).Limitations.
CALLABLEis gates-aside by construction and the module doc says so; a consumer with gated-away ancestors keeps its own list. The byte-lessis_func_space/get_space_kindare the kind table and are documented as such; nothing on a supported output path reads them. Groovy's byte-lessis_primitiveremains every name. #1299's template-payload residue is unchanged.dekobon commentedon Oct 26, 2026
Post-landing review pass (five reviewers; fixes in
18ee3c98)After R06/R08 closed, the branch went through simplify-rust, audit-tests and rust-optimize passes and then five parallel reviews (the
reviewchecklist, an algorithmic-performance / recursion audit, a least-reviewed-code audit, an acceptance-criteria audit against this issue, #1698, #1599 and #1690, and astra-review). Everything they found is fixed in18ee3c98; what touches this issue's claims:Step 1 / step 5 wording corrected in the body. ObjC's
Primitiverow is kind-only — its text-keyed words are theModifierrow's gatedidentifierpluskeys_operator_by_text— and theTemplateDeclarationdisagreement named in #1299 is not aMember-row question and stays with #1299 (npa/cpp.rsnow carries aFIXME(#1299)).AC 3 (byte-less callers) is now pinned, not exempted by rationale. Each row emits
TEXT_GATED, true iff an owner istext_gated_by;nargs::tests::byte_less_spellings_read_only_rows_without_a_text_gateandnom::tests::byte_less_is_closure_reads_only_rows_without_a_text_gatefail the day a row the byte-lessis_func/is_closurereaders depend on grows a text gate, naming the impl that must move to_with_code. The byte-less spellings hand the walk's chain through, so a chain-reading gate (Ruby's) was never the hazard.Rows are asked under the member's own chain.
npa/npmhanded a body's chain to the row for each of its members ("the row reads neither"); a gate the row grew later would have read the wrong parent silently, and the JS/TSis_callableon class-body children already reached the chain-climbingbound_*gates that way.Ancestors::of_children(body)(oneVecper body, never per node) now gives each member its own chain at every such site — 20 of them acrossnpa*.rs/npm*.rs, including the twoAncestors::unknown()sites innpm/ruby.rsanddirect_child_funcs, which also moves tois_func_with_code. Pinned byof_children_names_the_body_as_each_member_s_parent.Two pre-existing performance defects the branch touched are fixed.
python_walk_target_elementswas recursive and overflowed the consumer thread's stack at ~8,000 nesting levels (bindshad already gone iterative in #1597); it is iterative now, pinned bypython_deeply_nested_unpacking_target_does_not_overflow_the_stackon a 512 KiB thread. The JS arrow binding gate'sproperty_identifiersibling scan wasO(width)per positional arrow, sorun(() => 1, …)with 4,000 arrows took 3.4 s; memoised per parent underMemoQuestion::NamesAnArrowit takes 0.01 s, andnom/wide-arrow-arraynow renders that positional form (it rendered a boundconst cb = [...], which never reached the scan) — exponent 1.02, 87/87 probes on HEAD.is_primitivereads the row everywhere. Kotlin's and C#'sis_primitivestill answered the Halstead key-selection question (C#'s listed seven modifier kinds); that moves tokeys_operator_by_textoverrides as Groovy's and ObjC's are, and C / C++ / Mozcpp / TS / TSX readprimitive::lists_kindwith their one text-keyed kind in the hook.Evidence on HEAD. Corpus oracle 0 differing of 27,008 compared (the two R05 fixture files the restart lost were reconstructed from their recorded exports and verified byte-identical through the base binary's dump; Kotlin's and Perl's could not be and are the 16 records short of 27,024); recovery sweep 0 of 124,488; chain-audit pass;
make bench-scaling87/87, worst exponent 1.12. Per-language populations:{"bash":146,"c":3,"cpp":1050,"mozcpp":1050,"ccomment":6,"preproc":6,"csharp":24,"elixir":3,"go":3,"groovy":3,"irules":3,"java":14,"javascript":392,"mozjs":392,"kotlin":3,"lua":3,"objc":3,"perl":3,"php":3,"python":75,"ruby":3,"rust":175,"tcl":3,"typescript":10,"tsx":2}— the populations of 3 are the fixture set only. The earlier comments' "26,992" was a miscount of the same 27,024-record runs; the recovery count is 129,042 (CHANGELOG said 128,062 and is corrected).dekobon commentedon Oct 26, 2026
Astra-review of
18ee3c98returned two findings, both fixed in3ea78b95and re-reviewed (approve, no findings): the members' chain was built before the body test in the fournpa.rs/npm.rsmacro walks and so ran per node (a 64,000-deep expression took 3–6 s in each metric; 0.04 s now, pinned by a newnpa/java-deep-expressionprobe), andTEXT_GATEDreadtext_gated_byonly in first position (the scan arms now precede the fold; the DSL test declares the word in three positions). Final gate on an idle host:make bench-scaling88 / 88, worst exponent 1.32;make pre-commitpass; zero drift unchanged.dekobon commentedon Oct 26, 2026
Lost-fixture gap closed; evidence moved off
/tmpThe two R05 fixtures the host restart lost (
f1.kt,f1.pl) are recovered byte-exact. Every token's text and span is in the recorded verbatim export, so the first pass rebuilt 20 of 22 from those alone; the two that failed parsed asERRORbecause the semicolons Kotlin's grammar emits as anonymous separators, and the$ascalar names Perl's grammar reports only on thescalar_variablewrapper, are not leaves of the export. Reading those from the recordeddump(which prints every node's text) filled the remaining bytes, and the base binary's dump of each rebuilt file is identical to the recorded one — the same check the other 20 passed.With all 22 in place, the oracle on HEAD (
3ea78b95) reads 0 differing of 27,024 corpus records and the recovery sweep 0 of 129,042 — the full populations the earlier comments cite, so the "27,008 compared, 16 short" and "124,488" caveats above no longer apply.The evidence is no longer only in
/tmp:~/bca-evidence/1696/on this host holds the fixtures (original and recovered), the base and HEAD outputs (out2-7425561f,out-final), the base and HEADbca/ verbatim-oracle binaries, the oracle, recovery and perturbation drivers, the verbatim-oracle source, the bench / chain-audit / pre-commit logs, and the acceptance-criteria report (6.9 G). Whether the per-language fixtures should become integration tests undertests/is a question for the PR.refactor(checker,getter,npa): declare Java's space tree and members b…
refactor(checker,getter,npm,npa,cognitive): declare the JS family's s…
refactor(checker,getter,cognitive,npm,npa): declare the kind-only lan…
refactor(checker,getter,cognitive,npm,npa): declare the C++ family's …
refactor(checker,getter,cognitive,npm,npa): declare C#'s space tree b…
refactor(checker,getter,cognitive,npm,npa): declare Kotlin's, Groovy'…
refactor(checker,getter,cognitive,npm,npa): declare Python's and Elix…
refactor(halstead,checker): key text-keyed operators through a code-a…
fix(roles): take the R06 completion review
refactor(roles,metrics,checker): take the post-landing review passes …
docs(lessons): a chain is the queried node's own, or it is the wrong …