Merge main into Rust qualified factories

This commit is contained in:
Gergo Magyar 2026-10-22 08:37:00 +00:00
commit 09c68df98c
33 changed files with 1807 additions and 185 deletions

View file

@ -389,7 +389,7 @@ Single interface a language implements to plug into the pipeline. Contract fully
| `fieldFallbackOnMethodLookup?` | Statically-typed languages turn this OFF — the heuristic over-connects (default on) |
| `elementTypeOf?` | `(containerType, via: {kind:'index'} \| {kind:'accessor',name}) → elementType \| undefined` — element type of a container, reached by subscript (`repos[0]`) or by a property-style collection view (`data.Values`). ONE hook for both routes (it replaced the split `unwrapCollectionAccessor` / `unwrapCollectionElement`, where implementing one silently answered nothing for the other). Consulted only where the source actually performed the access — never as a general type-name normalizer |
| `stripTypePreservingDecoration?` | `(typeName) → strippedName \| undefined` — strip ONE layer of TYPE-PRESERVING decoration (pointer, reference, `const`, nullable, borrow, sigil) so a receiver declared `*Host` still finds the `Host` binding (#2766). Never a container: unwrapping `Repo[]` here would fold `repos.find(x)` to `Repo.find` — that is `elementTypeOf`'s job, and only after a real subscript. Consulted only after every undecorated lookup fails, and only by receiver-chain base/step resolution — default off |
| `qualifiedTypeLookupSpelling?` | `(typeRef) → 'pkg.Type' \| undefined` — the namespace-qualified spelling of a declared type whose qualifier capture cut from `rawName` (`*models.User` stored as `User`), recovered from `declaredSpelling`. Receiver typing tries it first, grounded on the qualifier's import (no unique-name guess), and falls back to the bare `rawName` lookup on any miss, so two packages' same-named types stop colliding. The binding is not rewritten — default off |
| `qualifiedTypeLookupSpelling?` | `(typeRef, scopes?) → 'pkg.Type' \| null \| undefined` — the namespace-qualified spelling of a declared type whose qualifier capture cut from `rawName` (`*models.User` stored as `User`), recovered from `declaredSpelling`. Receiver typing tries it first, grounded on the qualifier's import (no unique-name guess). A failed written-qualified lookup stops instead of falling back to a bare type name. The optional scope indexes also let a provider expand a visible namespace alias; `null` refuses the written lookup and its fallback. The binding is not rewritten — default off |
| `collapseMemberCallsByCallerTarget?` | One CALLS edge per (caller, target) instead of per-site — default off |
| `populateNamespaceSiblings?` | Cross-file implicit visibility (compiler-implicit namespace sharing) — default off; ctx carries `treeCache` |
| `hoistTypeBindingsToModule?` | Walk up to Module scope when looking up a method's return-type typeBinding — default off; enable only when bindings are stored at module level |
@ -397,6 +397,8 @@ Single interface a language implements to plug into the pipeline. Contract fully
| `constructorCallTargetsClass?` | A constructor-form call `Type(...)` links to the Class def rather than its explicit Constructor def — default off; Swift and Dart opt in |
| `constructionSyntax?` | How the language spells construction, so an INLINE constructor receiver (`Service(db).m()`, `new Service(db).m()`, `Service.new.m()`) can be typed — `bare` / `keyword` / `selector`; default off, opt in per language only where measured to be needed (#2708) |
| `resolveQualifiedCallee?` | `(calleeText, inScope, …) → callable \| undefined` — resolve a qualified callee only the language can read (`rp::make` in `x = rp::make()` / `rp::make().m()`) so chain-follow types the call from its return annotation; default off |
| `resolveQualifiedTypeSpellings?` | A receiver's declared or return type written QUALIFIED (`rp::C x`, `f(a::Config c)`) that binds nothing through the scope chain resolves through `resolveQualifiedInheritanceBase` — qualifier matched against the candidate's namespace, refuse on a tie — instead of by its tail, which binds the nearer same-named class. Runs after every existing lookup and after the template-argument match; default off, C++ opts in |
| `splitQualifiedCallee?` | `(callee, callerScope?, scopes?, position?) → { receiver, member } \| undefined` — map a qualified callee spelling (`rp::make`) onto a receiver/member pair that retains the full written namespace qualifier, so a factory result (`auto x = rp::make(); x.m()`, `rp::make().m()`) is typed from the definition `resolveQualifiedReceiverMember` returns and that definition's own declared return type. Caller scope, indexes and source position let a provider reject shadowed or aliased namespace qualifiers only when visible at the factory expression. Shared code never splits on a separator; default off, C++ implements it |
### Per-language registration

View file

@ -1010,6 +1010,9 @@ Shape states remain unchanged; counts are 133 call drops and
### Rust qualified factories after #3653
Main `2b4456553` is merged normally. The receiver-drop guard preserves both qualified-factory evidence and the module-export exclusion. Fresh build, both TypeScript configurations and all 28 Rust boundary, capture-golden and CommonJS checks pass. The measured baseline has 135 call drops, 164 total drops, 29 reads and 0 writes.
### C++ qualified receivers after #3653
Main `2b4456553` is merged normally. The field lookup retains both the declared namespace purpose and provider-qualified type handling. Build and both TypeScript configurations pass, and all 59 focused C++ and CommonJS merge checks pass. The measured receiver baseline has 133 call drops, 160 total drops, 27 reads and 0 writes.
### After merging #3653 into the C# namespace branch
Main `2b4456553` is merged normally. The measured baseline has 130 call drops and 156 across all kinds, with 26 reads and zero writes. Main has 133 calls, 27 reads and zero writes (160 total). Every shape state matches main. The fresh build and both TypeScript configurations pass, as do all 68 namespace-boundary, capture, hook and CommonJS merge checks.
@ -1017,3 +1020,12 @@ Main `2b4456553` is merged normally. The measured baseline has 130 call drops an
### Rust qualified factories after #3655
Main `caacc3bac` is merged normally. The C# capture baseline retains main's fixture additions, and receiver counts are regenerated on the combined source: 132 call drops, 160 total, 28 reads and zero writes. All shape states match the preceding Rust baseline. Fresh build, both TypeScript configurations and 23 Rust factory, capture and CommonJS checks pass. Final hosted CI remains required.
### C++ qualified receivers after #3655
Main `caacc3bac` is merged normally. The regenerated combined baseline has 130 call drops,156 total,26 reads and zero writes; all shape states match. These counts include the C# namespace fix from main. The broad C++ resolver run was interrupted for the prioritized focused merge checks and is not reported as a pass. Fresh exact-head CI remains required.
The prioritized merge validation passes a fresh package build, both TypeScript configurations and 56 tests across six C++, CommonJS and C# namespace-boundary files.
### Rust qualified factories after #3656
Main `a09f76be3` is merged normally. The shared receiver pass keeps Rust's provider-qualified callee hook and C++'s position-aware qualified-member lookup. Fresh package build, both TypeScript configurations and 302 tests across 20 Rust, C++, Go, CommonJS and sealed-disk-scope files pass. All 16 capture scaling checks pass without changing their budgets. The regenerated receiver check passes with 132 call drops, 160 across all kinds, 28 reads and zero writes; shape states are unchanged. The corpus adds 32 Rust resolution records to main's 5,392, removes none, and totals 5,424. Exact-head hosted CI remains required.

View file

@ -11,11 +11,11 @@
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 3d4e32e7490c830516126e28931827949baa3594cb521f7a3d8dcfed95b6018a -> 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a; scaling 1.058 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: provider-owned callable assignment/copy/formal/argument/invoke facts with invocation/constructor-result suppression. Prior 09ecd94911b830f52fa8807560abcbd79f163d02a2072870c1a59297e9a326e1 -> 3d4e32e7490c830516126e28931827949baa3594cb521f7a3d8dcfed95b6018a; scaling 1.039 < 1.5.",
"_rebaselined": "#1976: F33 generic composite literal constructor inference adds generic_type captures in composite_literal patterns; fingerprint drift expected.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a -> 5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a -> 5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb.",
"_rebaselined_2766_go_pointer_receiver_fixture": "#2766: added test/fixtures/lang-resolution/go-pointer-receiver-field-chain/ (2 Go files) as the committed regression fixture for pointer-receiver base resolution. Go fixture_count 100 -> 102. Prior 5d6c59c2f2c0dd937c53bf5d736e0f8376b2899a381e488a33aec23524823efb -> 8cba537ff211fab3bac5fb4456cd1ffba14d6a2db75c40acae28ab8bf29f3d2e. FIXTURE-CORPUS GROWTH, NOT A CAPTURE CHANGE: the accompanying fix is a resolution-time lookup fallback (stripTypePreservingDecoration) and cannot move capture output; go was the ONLY language whose fingerprint drifted, and every other language matched its baseline on the same run.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 8cba537ff211fab3bac5fb4456cd1ffba14d6a2db75c40acae28ab8bf29f3d2e -> 8162272bb897b0b89472c406321cf8d88a5ae4ea83ea9e3c45f8e817041bff9f.",
"_rebaselined_2766_await_subscript_emission": "#2766: extractMixedChain now walks THROUGH await and subscript nodes and peels transparent wrappers at loop entry, so sites whose receiver is `repos[0]` or `(await f())` mint a receiver chain where they previously minted none. EMISSION CHANGE: more sites carry `@reference.receiver-chain`; no existing chain changed shape. Only go and kotlin drifted of 15 \u2014 the two whose fixture corpora contain such receivers. Prior 8162272bb897b0b89472c406321cf8d88a5ae4ea83ea9e3c45f8e817041bff9f -> c9c908f441e3be12fad2448120ed3ea35dc235a12b3f63b0ec532ffdae11d9e9.",
"_rebaselined_2766_phantom_callee_read_site": "#2766: Go's `@reference.read` pattern matches EVERY selector_expression, so a member call `h.dep.Work()` minted THREE sites \u2014 the call, the genuine `h.dep` field read, and a PHANTOM read on the callee `h.dep.Work`. The phantom resolved through findOwnedMember (which prefers methods over fields) and emitted an ACCESSES edge to the METHOD duplicating the CALLS edge at the same position; visible today on any receiver the text cascade can type (`RunFromValueReceiver -> DoWork`). The emitter now drops a read match whose selector is in FUNCTION position. FEWER capture matches for Go, no other language affected \u2014 go was the only fingerprint of 15 that moved. A method VALUE (`f := h.dep.Work`) is not in function position and is untouched. Prior c9c908f441e3be12fad2448120ed3ea35dc235a12b3f63b0ec532ffdae11d9e9 -> 7bb524a32a2eed57a15b454e3a33480e92a496c683e6856ef02179693c0e02e3.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 8cba537ff211fab3bac5fb4456cd1ffba14d6a2db75c40acae28ab8bf29f3d2e -> 8162272bb897b0b89472c406321cf8d88a5ae4ea83ea9e3c45f8e817041bff9f.",
"_rebaselined_2766_await_subscript_emission": "#2766: extractMixedChain now walks THROUGH await and subscript nodes and peels transparent wrappers at loop entry, so sites whose receiver is `repos[0]` or `(await f())` mint a receiver chain where they previously minted none. EMISSION CHANGE: more sites carry `@reference.receiver-chain`; no existing chain changed shape. Only go and kotlin drifted of 15 — the two whose fixture corpora contain such receivers. Prior 8162272bb897b0b89472c406321cf8d88a5ae4ea83ea9e3c45f8e817041bff9f -> c9c908f441e3be12fad2448120ed3ea35dc235a12b3f63b0ec532ffdae11d9e9.",
"_rebaselined_2766_phantom_callee_read_site": "#2766: Go's `@reference.read` pattern matches EVERY selector_expression, so a member call `h.dep.Work()` minted THREE sites — the call, the genuine `h.dep` field read, and a PHANTOM read on the callee `h.dep.Work`. The phantom resolved through findOwnedMember (which prefers methods over fields) and emitted an ACCESSES edge to the METHOD duplicating the CALLS edge at the same position; visible today on any receiver the text cascade can type (`RunFromValueReceiver -> DoWork`). The emitter now drops a read match whose selector is in FUNCTION position. FEWER capture matches for Go, no other language affected — go was the only fingerprint of 15 that moved. A method VALUE (`f := h.dep.Work`) is not in function position and is untouched. Prior c9c908f441e3be12fad2448120ed3ea35dc235a12b3f63b0ec532ffdae11d9e9 -> 7bb524a32a2eed57a15b454e3a33480e92a496c683e6856ef02179693c0e02e3.",
"_rebaselined_2766_callee_position_marker": "#2766 review fix: a call's callee selector is no longer DROPPED at capture. An earlier commit on this branch dropped it outright, which also deleted the genuine field read on a func-typed struct field (`h.dep.Work()` where `Work func() error`) - callback/hook/mock structs lost their only ACCESSES evidence. The match is now emitted carrying `@reference.callee-position`, and the phantom is suppressed at EMIT by the resolved target's kind instead. Go only: the other 14 languages' fingerprints are byte-identical, which is the check that this is not a cross-language capture change. Prior 7bb524a32a2eed57a15b454e3a33480e92a496c683e6856ef02179693c0e02e3 -> e47302079e17a5e73711bbed5416557b49327cb67e4932008700ec6b8fb468b3; scaling 1.001 < 1.5; fixtures 102 (unchanged), capture_groups_fp 2103.",
"_rebaselined_2813_interface_field_dispatch_fixture": "#2813: added test/fixtures/lang-resolution/go-interface-field-dispatch/ (8 Go files) as the committed regression fixture for calls through an interface-typed struct field. Go fixture_count 102 -> 110. FIXTURE-CORPUS GROWTH, NOT A CAPTURE CHANGE: the accompanying fixes are a detection-time method-set change (interface-impls.ts) and a resolution-time fan-out in the shared receiver pass, neither of which emits captures; go/query.ts and go/captures.ts are untouched. Go was the ONLY language whose fingerprint drifted, and every other language matched its baseline on the same run - the same check used for the #2766 fixture growth above. Prior e47302079e17a5e73711bbed5416557b49327cb67e4932008700ec6b8fb468b3 -> cffee41cadbf350855d99bd5aee7c015b1e8b31d1c343d02f113540abe86c765; scaling 1.074 < 1.5, capture_groups_fp 2303.",
"_rebaselined_2837": "#2837: Go struct/interface captures re-anchored from the type_declaration onto the type_spec (@scope.class/@declaration.struct/@declaration.interface in languages/go/query.ts, @definition.struct/@definition.interface in GO_QUERIES). A grouped `type (...)` block used to yield ONE scope and ONE node for every type in it, so each type after the first lost its field typeBindings and every field-receiver call in the file emitted nothing. Capture COUNT is unchanged; only ranges moved, plus the new go-grouped-type-decl fixture. Prior c27fb803598581fa4eb7ddf5ef6f8369b9e3a150082d11362e7aa3ec8faaa832 -> e386598526e502d131e52a17d219635b3a4196d94f1ebdd25922a2582c985d18; scaling 1.054 < 1.5.",
@ -24,11 +24,11 @@
},
"cobol": {
"fingerprint": "d77fe8c2e08894d40bfdfeb6df8f5eceefa9f870bdfce9d4b620e6ef298870c9",
"_rebaselined_2813_exact_method_sets": "#2813: Go embedded fields now emit `@reference.embedded-pointer` when spelled `*T` rather than `T`. A CAPTURE-EMISSION CHANGE, not fixture growth: fixture_count is unchanged at 110 and capture_groups_fp moves 2303 -> 2339 (+36), which is the new marker plus the WrongSigRepo/Recount rows added to two existing fixture files. The marker is required for exactness \u2014 Go gives `struct{ Base }` and `struct{ *Base }` different method sets, so structural interface satisfaction cannot be correct without knowing which was written (go.dev/ref/spec#Struct_types). Go was the ONLY language of 15 whose fingerprint moved, which is the check that this is a Go capture change and not a cross-language regression. Accompanied by SCHEMA_BUMP 39 -> 43 (skipping 40/41/42, taken by origin/main during review) so a warm cache cannot replay the pre-marker capture set. Prior cffee41cadbf350855d99bd5aee7c015b1e8b31d1c343d02f113540abe86c765 -> c27fb803598581fa4eb7ddf5ef6f8369b9e3a150082d11362e7aa3ec8faaa832; scaling 0.987 < 1.5.",
"_rebaselined_2813_exact_method_sets": "#2813: Go embedded fields now emit `@reference.embedded-pointer` when spelled `*T` rather than `T`. A CAPTURE-EMISSION CHANGE, not fixture growth: fixture_count is unchanged at 110 and capture_groups_fp moves 2303 -> 2339 (+36), which is the new marker plus the WrongSigRepo/Recount rows added to two existing fixture files. The marker is required for exactness — Go gives `struct{ Base }` and `struct{ *Base }` different method sets, so structural interface satisfaction cannot be correct without knowing which was written (go.dev/ref/spec#Struct_types). Go was the ONLY language of 15 whose fingerprint moved, which is the check that this is a Go capture change and not a cross-language regression. Accompanied by SCHEMA_BUMP 39 -> 43 (skipping 40/41/42, taken by origin/main during review) so a warm cache cannot replay the pre-marker capture set. Prior cffee41cadbf350855d99bd5aee7c015b1e8b31d1c343d02f113540abe86c765 -> c27fb803598581fa4eb7ddf5ef6f8369b9e3a150082d11362e7aa3ec8faaa832; scaling 0.987 < 1.5.",
"scaling_budget": 1.5,
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: COBOL procedure-pointer callable flow facts; multi-topic extraction now consumes each grouped scope/declaration match once instead of requiring a duplicate declaration-only match. Prior 68ee0e95eb9f86f2d92ca35f730f4c2d4d83abc1b5241ae767ff3437780ec8d1 -> d45bb091b0893d0de4fae2486b31ba21719c9377bf35a0908fd3a36fa1c3bf4e; scaling 0.853 < 1.5.",
"_note": "Updated for F17-F23 fixes (P2: TIMES guard, ADD GIVING, SQL AS alias). See PR #1959.",
"_rebaselined_2793_declaratives": "PR #2793: corpus-only re-baseline. `cobol-declaratives` was added to test/fixtures/lang-resolution to reproduce the `Namespace\u2192Record` analyze abort (DECLARATIVES / USE AFTER STANDARD ERROR ON <file>), and this bench globs `lang-resolution/cobol-*`, so the corpus grew 14 -> 15 files. Verified capture-neutral: with that one fixture moved aside the fingerprint is byte-identical to the prior d45bb091b0893d0de4fae2486b31ba21719c9377bf35a0908fd3a36fa1c3bf4e. No COBOL capture code changed in that PR. Scaling 0.677 < 1.5.",
"_rebaselined_2793_declaratives": "PR #2793: corpus-only re-baseline. `cobol-declaratives` was added to test/fixtures/lang-resolution to reproduce the `Namespace→Record` analyze abort (DECLARATIVES / USE AFTER STANDARD ERROR ON <file>), and this bench globs `lang-resolution/cobol-*`, so the corpus grew 14 -> 15 files. Verified capture-neutral: with that one fixture moved aside the fingerprint is byte-identical to the prior d45bb091b0893d0de4fae2486b31ba21719c9377bf35a0908fd3a36fa1c3bf4e. No COBOL capture code changed in that PR. Scaling 0.677 < 1.5.",
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. Provider semantic facts replace line-only declarations: exact name spans and qualified/binding identities, full executable ownership scopes, section namespaces, data-item properties and identifier reads/calls. The 13 changed fixture entries gain 49 property declarations, 150 reads and 11 call-reference groups; sections become 5 namespaces instead of callable functions. The fixture corpus is unchanged. Fixture entries 15; changed entries 13; capture groups 110 -> 320. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
},
"c": {
@ -37,17 +37,22 @@
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 57fee292147ae6d2db7062da1e07d17122cf355207c8967fa85fd2ec9ca398a4 -> 3418cded9f7072152f68992f0a426f43ae7d9d553579a47075fc0cab185848a5; scaling 1.073 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C function-pointer signatures plus direct-callee argument metadata and invocation-result suppression. Prior 75bcdbbf006bf9bd263c0f5857461b118f39b164e9f821cb0651ad0ec46ef6ae -> 57fee292147ae6d2db7062da1e07d17122cf355207c8967fa85fd2ec9ca398a4; scaling 1.035 < 1.5.",
"_rebaselined_callable_flow": "Callable-value-flow facts for C function pointers, copies, pointer-to-pointer cells, arguments, and indirect invokes. Prior 12a196b2d6249c8d86a931b12ecebc2a0cdf8d6f47683acdd0d8e9d8bc7657f5 -> 75bcdbbf006bf9bd263c0f5857461b118f39b164e9f821cb0651ad0ec46ef6ae; measured scaling ratio 0.980 < 1.5.",
"_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance \u2014 flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96.",
"_note": "#1983: + c-static-linkage-worker fixture (caller.c/lib.c/lib.h/local.c \u2014 worker-path static-linkage side-channel test). Pure fixture-corpus drift: no c/captures.ts or query change branch-vs-main, existing fixtures' captures byte-identical (c-captures.test.ts 45/45), scaling stays linear (~0.97). The baseline was missed when the fixture landed; regenerated here. fingerprint 0de009b->39f3a83.",
"_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance — flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96.",
"_note": "#1983: + c-static-linkage-worker fixture (caller.c/lib.c/lib.h/local.c — worker-path static-linkage side-channel test). Pure fixture-corpus drift: no c/captures.ts or query change branch-vs-main, existing fixtures' captures byte-identical (c-captures.test.ts 45/45), scaling stays linear (~0.97). The baseline was missed when the fixture landed; regenerated here. fingerprint 0de009b->39f3a83.",
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression).",
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 72 pointer-parameter and 43 field type bindings are added; 19 reference-only struct/union specifiers stop introducing class scopes; 53 callable groups gain C/C++-compatible parameter type classes and normalized type spelling. The existing fixture corpus is unchanged. Fixture entries 15; changed entries 9; capture groups 545 -> 641. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
},
"cpp": {
"_lexical_ownership_3532": "#3532: lexical ownership captures and fixture updates; fixture identities are relative to lang-resolution so serialized scope IDs are independent of checkout location. Full capture content remains hashed; scaling_budget is unchanged.",
"fingerprint": "dfd1de80f55d8e1bacde91a99951e7eee4e76ccdc8a7477c5a8ac98d804ce05a",
"fingerprint": "14dcc4038c95a874f8f7bd72cbd8797f2deb030ba080b0d124ebcb3117617d00",
"_rebaselined_qualified_receiver_typing_factory_callee": "Qualified factory callee: captures.ts now promotes `auto x = ns::f()` / `auto x = ns::T()` (a qualified_identifier callee whose innermost name is a plain identifier) from the `type: (_)` @type-binding.assignment match, which bound x to the literal type `auto`, to @type-binding.constructor with the whole qualified callee as @type-binding.type - the qualified twin of the bare `auto x = f()` rule. Template callees (`std::make_shared<T>()`) keep their dedicated rule. Compared canonicalized captures of the pre-change and post-change emitters over the same corpus: exactly ONE match changed outside the new fixture - cpp-method-enrichment/app.cpp `auto category = Animal::classify(\"dog\");`, @type-binding.assignment/type `auto` -> @type-binding.constructor/type `Animal::classify` - and none was added or removed; synthetic DAO-20 unchanged. interpret.ts additionally drops any leftover `auto` / `decltype(auto)` binding (no capture effect; over the corpus no such leftover remained). The fixture gains its qualified-factory callers (+33 groups). capture_groups_fp 4793 -> 4826, fixture_count 285 unchanged. Corpus A/B: no edge or drop outside the fixture moved (the changed binding names a class's static method, which the qualified-namespace lookup does not answer, so category stays untyped as before). Prior 2242a6b8085b27478cc8bf6f37da3f7b9b07df85fd863a3f633d7b0887ab7ac8 -> 13bfad4a8197dd9e3d252e28c6b591e08bb51e799d1c1a839f8a485d11e549eb; scaling 1.198 < 1.5.",
"_rebaselined_qualified_receiver_typing_inline_fixture": "cpp-qualified-receiver-typing/using.cpp gains two inline-receiver callers (`make().m()` and `makeWidget().m()` under a using-directive) for the compound resolver's definition-identity return-type fallback. CORPUS GROWTH ONLY: the accompanying fix is resolution-time (compound-receiver.ts) and no emitter changed; the new fixture is the only file whose captures moved (+14 groups). capture_groups_fp 4779 -> 4793, fixture_count 285 unchanged. Prior b21abc71298d184dd484647e6d36237bb56e66c82597ef7a47452a74526856cd -> 2242a6b8085b27478cc8bf6f37da3f7b9b07df85fd863a3f633d7b0887ab7ac8; scaling 1.167 < 1.5.",
"_rebaselined_qualified_receiver_typing_prototype_returns": "Header prototype return types: two @type-binding.return rules on a free-function PROTOTYPE (`D makeD();`, `D* makeDp();`), anchored on the `declaration` node; captures.ts drops a match whose declaration is not at namespace scope (translation unit or declaration_list, through preprocessor conditionals, extern \"C\" and template prefixes), so a block-scope `D x();` (most vexing parse) binds nothing. Compared canonicalized captures of the pre-change and post-change emitters over the same corpus: exactly ONE match added outside the new fixture - cpp-return-type/user.h `User getUser(const char* name);` (the header prototype whose same-file definition already bound the identical type) - and nothing removed or changed; synthetic DAO-20 unchanged (no free prototypes). Over the whole cpp corpus the gate drops exactly one candidate, the fixture's deliberate `E y();` in a function body. The fixture gains its prototype shapes and using.cpp (140 -> 242 groups, fixture_count 284 -> 285). capture_groups_fp 4676 -> 4779. Corpus A/B: no edge or drop outside the fixture moved. Prior a50c2b2313f34e03f8da44d34007250830b66af7c62690d55b1716219eae481e -> b21abc71298d184dd484647e6d36237bb56e66c82597ef7a47452a74526856cd; scaling 1.226 < 1.5.",
"_rebaselined_qualified_receiver_typing_declared_ns": "Qualified declarations without an initializer: two @type-binding.annotation rules matching the OUTER qualified_identifier (`rp::C x;` and `rp::C* x = nullptr;`), the qualified twins of the bare `T x;` / `T* x = ...` rules, so the local binds `rp::C` as written for the qualifier-aware resolver. Compared canonicalized captures of the pre-change and post-change emitters over the same corpus: 29 matches ADDED outside the new fixture, every one a @type-binding.annotation on a qualified local (21 x `ns::T v;` - audit::Event, data::Pair, lib::T, alpha::Token, lib::Foo - across 16 cpp-adl-* / cpp-inline-namespace-adl-participation / cpp-phase5-u2-u3-adl-from-derived files, plus 8 std::vector / std::array / std::map locals in cpp-adl-template-args, cpp-template-cross-file and cpp-template-overload); zero matches removed or changed, no other tag moved, synthetic DAO-20 unchanged (it declares no qualified local). The new fixture gains 2 callers (127 -> 140 groups). capture_groups_fp 4634 -> 4676. Corpus A/B: no CALLS/ACCESSES edge or receiver drop outside the fixture moved (ADL arguments are typed from the capture-time AST, not typeBindings). Prior 83c1009cd6c88f6997a0b7aa8c188bc7c44c03e9126a2ed684bfb5a21bb9f5f3 -> a50c2b2313f34e03f8da44d34007250830b66af7c62690d55b1716219eae481e; scaling 1.255 < 1.5.",
"_rebaselined_qualified_receiver_typing_brace_init": "Qualified receiver typing (resolveQualifiedTypeSpellings): the `auto x = ns::T{}` brace-init @type-binding.constructor rule now captures the WHOLE qualified_identifier instead of its type_identifier tail, so the resolver can match the qualifier (the tail bound a nearer same-named class: a global C through another include, or the enclosing namespace's own Config). Compared canonicalized captures of the pre-change and post-change emitters over the same corpus: exactly ONE existing match changed - cpp-scoped-brace-init/main.cpp `client = ns::HttpClient{}`, @type-binding.type text HttpClient -> ns::HttpClient (range 4:22-4:32 -> 4:18-4:32); no tag was added or removed anywhere. The rest is CORPUS growth: the new cpp-qualified-receiver-typing fixture (5 files, 127 capture groups). capture_groups_fp 4507 -> 4634, fixture_count 279 -> 284. The member field change (non-generic qualified members keep their qualifier) is in interpret.ts and does not touch captures. Prior dfd1de80f55d8e1bacde91a99951e7eee4e76ccdc8a7477c5a8ac98d804ce05a -> 83c1009cd6c88f6997a0b7aa8c188bc7c44c03e9126a2ed684bfb5a21bb9f5f3; scaling 1.479 < 1.5 on a host at load ~14.",
"scaling_budget": 1.5,
"_rebaselined_2833_qualified_member_fields": "#2833 follow-up: the six per-qualifier-depth `field_declaration` type-binding rules for a QUALIFIED generic member are replaced by three depth-agnostic ones that match the outer `qualified_identifier` itself, with the qualifier reduced to its top-level tail in `interpret.ts` (`cppQualifiedTail`). This is a CAPTURE-LOGIC change and it moves the fingerprint in two places at once. (1) A qualified NON-generic member (`ns::Address addr;`, `std::string name;`) was captured by nothing at all and now binds \u2014 that is the whole +24 on the fixture corpus, every one of them a `std::string` member. (2) Qualifier depth is no longer enumerated, so `a::b::c::Repo<User>` (depth 3+) is captured where the old rules stopped at 2. Capture-name histogram, cpp-* corpus (278 files): `@type-binding.field` 8 -> 32, `@type-binding.name` and `@type-binding.type` 401 -> 425; synthetic DAO-20: `@type-binding.field` 40 -> 60, `@type-binding.name` and `@type-binding.type` 61 -> 81 (= 20 entities x the one `std::string name;` member the DAO unit already declared). NO OTHER TAG MOVED in either set \u2014 not one `@declaration.*`, `@scope.*` or `@reference.*` count \u2014 which is the property that says three rules replaced six without widening what a field_declaration matches. Measured over the 13 cpp-* fixture repos whose sources gained a binding, the distinct CALLS edge set is byte-identical before and after (32 edges): a reduced tail that names no workspace class binds nothing. Prior bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a -> db1156d81b3e3341faf5e938a4a34417f4fd246588b6150b4686481823262529; scaling 1.04 < 1.5.",
"_rebaselined_2833_generic_member_fields": "#2833 review follow-up: the cpp DAO generator's unit gains two GENERIC member fields \u2014 `Repo<Entity_n> repo;` (bare template_type) and `std::vector<Entity_n> items;` (qualified_identifier wrapping a template_type) \u2014 plus the header declaring `template <typename T> class Repo`. CORPUS CHANGE, NOT A CAPTURE-LOGIC CHANGE: no extractor edit accompanies it. It exists because the corpus had ZERO template-typed member fields and, across 279 cpp-* fixtures, not one qualified generic member either, so BOTH rounds of new `field_declaration` type-binding rules landed with a byte-identical cpp fingerprint \u2014 the gate was structurally blind to the exact thing being changed. Measured under the new corpus, the three states now differ: pre-#2833 query 0e7cbda71360b7ff35dd76091c77f288d6af6a5cfa9185ad85a372aae8c85191 (4521 groups) -> the three template_type field rules de07d8b5300ed867b460918e16b4d80259c7eb6efc1034d32bebe9ff7cab126d (4541) -> the six qualified rules bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a (4561); under the OLD corpus all three were 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc. Capture-name histogram over the synthetic DAO-20: `@type-binding.field` 0 -> 40, `@declaration.field` 40 -> 80, `@type-binding.type`/`@type-binding.name` 20 -> 61, `@declaration.name` 104 -> 147 \u2014 40 = 20 entities x 2 fields, with the residual +1/+2/+3 attributable to the one-off header declaration; every `@reference.*` count is unchanged. Prior 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc -> bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a; scaling 1.058 < 1.5. `c` is unaffected (3418cded..., unchanged).",
"_rebaselined_2833_qualified_member_fields": "#2833 follow-up: the six per-qualifier-depth `field_declaration` type-binding rules for a QUALIFIED generic member are replaced by three depth-agnostic ones that match the outer `qualified_identifier` itself, with the qualifier reduced to its top-level tail in `interpret.ts` (`cppQualifiedTail`). This is a CAPTURE-LOGIC change and it moves the fingerprint in two places at once. (1) A qualified NON-generic member (`ns::Address addr;`, `std::string name;`) was captured by nothing at all and now binds — that is the whole +24 on the fixture corpus, every one of them a `std::string` member. (2) Qualifier depth is no longer enumerated, so `a::b::c::Repo<User>` (depth 3+) is captured where the old rules stopped at 2. Capture-name histogram, cpp-* corpus (278 files): `@type-binding.field` 8 -> 32, `@type-binding.name` and `@type-binding.type` 401 -> 425; synthetic DAO-20: `@type-binding.field` 40 -> 60, `@type-binding.name` and `@type-binding.type` 61 -> 81 (= 20 entities x the one `std::string name;` member the DAO unit already declared). NO OTHER TAG MOVED in either set — not one `@declaration.*`, `@scope.*` or `@reference.*` count — which is the property that says three rules replaced six without widening what a field_declaration matches. Measured over the 13 cpp-* fixture repos whose sources gained a binding, the distinct CALLS edge set is byte-identical before and after (32 edges): a reduced tail that names no workspace class binds nothing. Prior bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a -> db1156d81b3e3341faf5e938a4a34417f4fd246588b6150b4686481823262529; scaling 1.04 < 1.5.",
"_rebaselined_2833_generic_member_fields": "#2833 review follow-up: the cpp DAO generator's unit gains two GENERIC member fields — `Repo<Entity_n> repo;` (bare template_type) and `std::vector<Entity_n> items;` (qualified_identifier wrapping a template_type) — plus the header declaring `template <typename T> class Repo`. CORPUS CHANGE, NOT A CAPTURE-LOGIC CHANGE: no extractor edit accompanies it. It exists because the corpus had ZERO template-typed member fields and, across 279 cpp-* fixtures, not one qualified generic member either, so BOTH rounds of new `field_declaration` type-binding rules landed with a byte-identical cpp fingerprint — the gate was structurally blind to the exact thing being changed. Measured under the new corpus, the three states now differ: pre-#2833 query 0e7cbda71360b7ff35dd76091c77f288d6af6a5cfa9185ad85a372aae8c85191 (4521 groups) -> the three template_type field rules de07d8b5300ed867b460918e16b4d80259c7eb6efc1034d32bebe9ff7cab126d (4541) -> the six qualified rules bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a (4561); under the OLD corpus all three were 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc. Capture-name histogram over the synthetic DAO-20: `@type-binding.field` 0 -> 40, `@declaration.field` 40 -> 80, `@type-binding.type`/`@type-binding.name` 20 -> 61, `@declaration.name` 104 -> 147 — 40 = 20 entities x 2 fields, with the residual +1/+2/+3 attributable to the one-off header declaration; every `@reference.*` count is unchanged. Prior 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc -> bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a; scaling 1.058 < 1.5. `c` is unaffected (3418cded..., unchanged).",
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature/cv metadata. Prior dde874d2c30bda9f634f9799281a66de800cad9f76cf65e7c31839e2ae9da9ff -> 57860dd2a8d4b06c6d2dd0d854c08b781faee3da8f2b6c42ba0c68a9f70e5ccb; scaling 1.090 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C++ overload-aware function/reference/member-pointer flow facts with invocation/constructor-result suppression. Prior 3a503a1513e7eede3f7a223dcce0896c06d15bdfa920445224c9025848c0d710 -> dde874d2c30bda9f634f9799281a66de800cad9f76cf65e7c31839e2ae9da9ff; scaling 1.034 < 1.5.",
"_rebaselined_callable_flow": "Callable-value-flow facts for C++ function pointers/references, reference aliases, contextual arity, arguments, and member-pointer syntax. Prior 6ab657c8f9bfe988a3759098c2cffdcc0443def75ff263f1282b82c21d96e931 -> 3a503a1513e7eede3f7a223dcce0896c06d15bdfa920445224c9025848c0d710; measured scaling ratio 1.069 < 1.5.",
@ -55,17 +60,18 @@
"_note_1899_followup": "#1899 follow-up: braced-init metadata now carries element count, intentionally changing C++ capture output; CI benchmark scaling remains linear (1.129 < 1.5).",
"_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.",
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression). #2094: deleted C++ declarations retain @declaration.is-deleted metadata; deleted operator and pointer-return shapes plus the expanded deleted-overload fixture are included. Intended capture drift; scaling remains linear (1.139 < 1.5).",
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift \u2014 no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures \u2014 pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture \u2014 pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5). #1899: braced-init call arguments emit a conservative parameter-type capture; fixture_count 277, scaling remains linear (1.141 < 1.5).",
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5). #1899: braced-init call arguments emit a conservative parameter-type capture; fixture_count 277, scaling remains linear (1.141 < 1.5).",
"_rebaselined_2522_review_fixes": "PR #2522 review fixes: outermost-chain passing modes; ->* ERROR-recovery role order; member-store visibility. Prior 57860dd2a8d4b06c6d2dd0d854c08b781faee3da8f2b6c42ba0c68a9f70e5ccb -> f29bc3f7b1622954d6f6b7647bc9cf6c7a2629ffcc0fe00ac7918e4925876b65; scaling ratio re-verified within budget.",
"_rebaselined_2522_prototype_value_cells": "Plain function/method prototypes no longer index as callable value cells (only pointer/parenthesized variable declarators do) \u2014 removes the spurious indirect-invoke facts that leaked phantom CALLS past two-phase suppression. Prior f29bc3f7b1622954d6f6b7647bc9cf6c7a2629ffcc0fe00ac7918e4925876b65 -> a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1; scaling re-verified within budget.",
"_rebaselined_2522_prototype_value_cells": "Plain function/method prototypes no longer index as callable value cells (only pointer/parenthesized variable declarators do) — removes the spurious indirect-invoke facts that leaked phantom CALLS past two-phase suppression. Prior f29bc3f7b1622954d6f6b7647bc9cf6c7a2629ffcc0fe00ac7918e4925876b65 -> a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1; scaling re-verified within budget.",
"_rebaselined_receiver_chain_2747": "#2747: additionally adds the `cpp-receiver-chain-arrow` fixture, the behavioural proof for a `->` BASE receiver (`svc->getUser()->save()`) that the rollout fixed and that `cpp-chain-call/` could never catch because it uses the value `.` form. Prior a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1 -> 7e27aea46f3e17f33c41babbe0ddd982d1ab5920f143864763e0a1c6aef882a5.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 7e27aea46f3e17f33c41babbe0ddd982d1ab5920f143864763e0a1c6aef882a5 -> 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 7e27aea46f3e17f33c41babbe0ddd982d1ab5920f143864763e0a1c6aef882a5 -> 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc.",
"capture_groups_small": 5021,
"capture_groups_large": 16021,
"capture_groups_fp": 4601,
"fixture_count": 279,
"capture_groups_fp": 4826,
"fixture_count": 285,
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Only drift: `choice.select(1)` / `choice.select(1.5)` (cpp-deleted-overload/main.cpp) no longer mint an INDIRECT `@callable-flow.invoke` (callee-kind binding) plus its synthetic `@reference.call.free` - that invoke was gated only by the same-named free `select` binding while the site is a genuine method call already captured as `@reference.call.member`, so the free-call duplicate is gone. capture_groups_fp 4605 -> 4601 (-4: 2 invokes + 2 synthetic call.free). Prior bf3587674267be1759e7c45abef143c3b81fe8629cfd17da5f8af40e83cc39ec -> 3aaee42523f02718795ba348782d78ec1418f35eff2e1b602422b192544fed96.",
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 9 existing read matches gain receiver chains; 9 synthetic operator names now use their written operator-token range rather than the translation-unit range. Group counts are unchanged. Fixture entries 279; changed entries 10; capture groups 4507 -> 4507. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 9 existing read matches gain receiver chains; 9 synthetic operator names now use their written operator-token range rather than the translation-unit range. Group counts are unchanged. Fixture entries 279; changed entries 10; capture groups 4507 -> 4507. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged.",
"_rebaselined_3498_review": "Qualified callable returns retain namespace spelling. Function-template definitions publish their own parameter list, including pointer-returning declarators. Only C++ capture fingerprint changes; all sixteen unchanged scaling budgets pass. Native boundaries cover alias chains, namespace/class collisions, overloaded prototypes and independent callable return owners."
},
"csharp": {
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.",
@ -75,8 +81,8 @@
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C# method-group/delegate callable flow facts with invocation-result suppression. Prior 2bb5bc8c19cb8eb08c9590545ad8a1968a7152951f7e12746e2d7901d542fed9 -> f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a; scaling 1.115 < 1.5.",
"_note": "#2046: F35 qualified-constructor captures now emit @reference.qualified-name + a simple-name @reference.name on `new Ns.Foo()`/`new A.B.Foo()`; namespace_declaration/file_scoped_namespace_declaration now emit @declaration.namespace name captures (feeding the non-destructive namespacePrefix sidecar for `new B.Foo()` same-tail disambiguation). + csharp-interface-only-base and csharp-namespace-qualified-ctor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.11).",
"_rebaselined_2563_instance_ownership": "#2563: csharp-using-static adds same-file ownership, local-function, overload, partial-class, and cross-namespace same-name coverage. Prior 75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1 -> e05dc27456bde8175948586c9e7689033a378fa40e9ca4ce78cce41fbea0f2f8; scaling 1.058 < 1.5.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 05a85bae70cf9c94f42459c843cfc36e3e81c872e5dcc7d77bc42fbc390f4bfe -> 8a282254b93b3ef2ff34c2fdba819ebc95c53c4fcb09942cbad99f96d3687855.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 8a282254b93b3ef2ff34c2fdba819ebc95c53c4fcb09942cbad99f96d3687855 -> 476d98a7cc659951c315d63319c8077bbcf0e5f3ec12d32ed773992a1f3a2adc.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 05a85bae70cf9c94f42459c843cfc36e3e81c872e5dcc7d77bc42fbc390f4bfe -> 8a282254b93b3ef2ff34c2fdba819ebc95c53c4fcb09942cbad99f96d3687855.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 8a282254b93b3ef2ff34c2fdba819ebc95c53c4fcb09942cbad99f96d3687855 -> 476d98a7cc659951c315d63319c8077bbcf0e5f3ec12d32ed773992a1f3a2adc.",
"capture_groups_small": 4509,
"capture_groups_large": 14409,
"capture_groups_fp": 2841,
@ -90,18 +96,18 @@
"_lexical_ownership_3532": "#3532: lexical ownership captures and fixture updates; fixture identities are relative to lang-resolution so serialized scope IDs are independent of checkout location. Full capture content remains hashed; scaling_budget is unchanged.",
"fingerprint": "19045ed3d4ee9fcea36e949284b385f8d0a24e471cb05f77d8b35d8a1a8dd7fa",
"scaling_budget": 1.5,
"_rebaselined_generic_instantiation_2912": "#2912: RUST_SCOPE_QUERY tags trait-impl heritage with the instantiation the impl was written with (`impl Validator<String> for V`), so interface dispatch can prune implementors of an instantiation the receiver cannot hold. Additive capture text on existing impl matches \u2014 the same matches are minted, carrying one more field \u2014 so this is digest drift, not a capture-set change: capture_groups_fp (3556) and fixture_count (202) are both unchanged, which is the check that no match appeared or vanished. Prior 116a971fee0004f340477aff69fa110a1d92bd8ba882d7c926483c6b1e8ca2b9 -> e61653008ff2de506cfd47f905fa9eb22d82fbbfe94d2a1d8190c358211b57b7; scaling 1.018 < 1.5. Only rust and dart move; the other 13 languages are byte-identical.",
"_rebaselined_mod_node_identity_2745_review": "#2745 review: added rust-2742-mod-members, rust-2742-nested-mods and rust-2742-type-vs-module under lang-resolution for the container/owner-edge fix, nested inline modules, and the imported-type-vs-module precedence. emitRustScopeCaptures is unchanged \u2014 verified by removing ONLY those three fixture dirs and re-running, which reproduces the prior fingerprint exactly, so the shift is purely corpus growth (fixture_count 196 -> 202, capture_groups_fp 3432 -> 3556). Prior 90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5 -> 05acbaca48427e0d9e0793bcd0ce4057712d3716b5e7868189c12e05ef8dd300; scaling 1.022 local / 1.057 CI < 1.5. NOTE for the next fixture author: a new rust-* fixture drifts BOTH this bench baseline and the rust-captures-golden snapshot. Updating only the golden is how this reached CI red.",
"_rebaselined_generic_instantiation_2912": "#2912: RUST_SCOPE_QUERY tags trait-impl heritage with the instantiation the impl was written with (`impl Validator<String> for V`), so interface dispatch can prune implementors of an instantiation the receiver cannot hold. Additive capture text on existing impl matches — the same matches are minted, carrying one more field — so this is digest drift, not a capture-set change: capture_groups_fp (3556) and fixture_count (202) are both unchanged, which is the check that no match appeared or vanished. Prior 116a971fee0004f340477aff69fa110a1d92bd8ba882d7c926483c6b1e8ca2b9 -> e61653008ff2de506cfd47f905fa9eb22d82fbbfe94d2a1d8190c358211b57b7; scaling 1.018 < 1.5. Only rust and dart move; the other 13 languages are byte-identical.",
"_rebaselined_mod_node_identity_2745_review": "#2745 review: added rust-2742-mod-members, rust-2742-nested-mods and rust-2742-type-vs-module under lang-resolution for the container/owner-edge fix, nested inline modules, and the imported-type-vs-module precedence. emitRustScopeCaptures is unchanged — verified by removing ONLY those three fixture dirs and re-running, which reproduces the prior fingerprint exactly, so the shift is purely corpus growth (fixture_count 196 -> 202, capture_groups_fp 3432 -> 3556). Prior 90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5 -> 05acbaca48427e0d9e0793bcd0ce4057712d3716b5e7868189c12e05ef8dd300; scaling 1.022 local / 1.057 CI < 1.5. NOTE for the next fixture author: a new rust-* fixture drifts BOTH this bench baseline and the rust-captures-golden snapshot. Updating only the golden is how this reached CI red.",
"_rebaselined_dyn_trait_object_2604": "#2604: RUST_SCOPE_QUERY now captures function_signature_item (abstract trait methods, no body) as a scope + declaration, so a &dyn Trait receiver can dispatch a CALLS edge to the trait's own method. Additive capture shift across every bench fixture with a required trait method. Prior df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29 -> f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846; scaling 1.033 < 1.5.",
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c -> df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29; scaling 1.065 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Rust fn-value callable flow facts with invocation/constructor-result suppression. Prior ac610bbe97666bf285923479dd7b43a2fe4c5354aae8df1bcbafdc04fb220f82 -> 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c; scaling 1.024 < 1.5.",
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) \u2014 legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical. | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED \u2014 @declaration.macro/@reference.macro + MacroRegistry \u2192 USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures \u2014 pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f.",
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) — legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical. | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED — @declaration.macro/@reference.macro + MacroRegistry → USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures — pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f.",
"_rebaselined_import_disambiguation_2514": "#2514: added rust-import-* and rust-dup-* fixtures under lang-resolution for the range-binding ambiguity latch + import-disambiguated resolution (for-loops / struct destructuring across explicit/aliased/glob use imports). emitRustScopeCaptures is unchanged; the corpus fingerprint shifts purely because the fixture set grew (130 -> 174). Prior f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846 -> 655aed01cf1b6b84fa0c64d48dfb2526ecb67f47d90f0a91edabacd269a212db; scaling 1.06 < 1.5.",
"_rebaselined_self_type_binding_2714": "#2714: a Rust `Self` type binding now records the enclosing impl's type instead of the literal 'Self'. `let fresh = Self { .. }` inside `impl User` binds `fresh: User`; recorded verbatim it bound `fresh: Self`, which resolves to nothing. The type-env channel already substituted this (type-extractors/rust.ts findEnclosingImplType); the scope-resolution channel did not, so the two disagreed. The gap was invisible while lookupCore Step 1 still walked the lexical chain for NAMED receivers \u2014 the impl scope binds the method by name, so fresh.validate() resolved by accident \u2014 and became a lost CALLS edge when #2714 stopped that walk. Only the rust fingerprint moves; the other 14 languages are byte-identical.",
"_rebaselined_self_type_binding_2714": "#2714: a Rust `Self` type binding now records the enclosing impl's type instead of the literal 'Self'. `let fresh = Self { .. }` inside `impl User` binds `fresh: User`; recorded verbatim it bound `fresh: Self`, which resolves to nothing. The type-env channel already substituted this (type-extractors/rust.ts findEnclosingImplType); the scope-resolution channel did not, so the two disagreed. The gap was invisible while lookupCore Step 1 still walked the lexical chain for NAMED receivers — the impl scope binds the method by name, so fresh.validate() resolved by accident — and became a lost CALLS edge when #2714 stopped that walk. Only the rust fingerprint moves; the other 14 languages are byte-identical.",
"_rebaselined_module_tree_2730": "#2730 + #2741 review: RUST_SCOPE_QUERY captures mod_item as @declaration.namespace (a Rust module is an item, mirroring the C++ namespace_definition capture) and tags scoped call sites with @reference.qualified-name so the written path survives to resolution. Both are additive captures: every bench fixture holding a mod block or a Foo::bar() call gains groups, and the corpus also grew by the rust-2730-* fixtures added for the fix and its review (workspace-crates, type-qualified, gaps, samename-wrapper, crate-layout). Prior 7f1240b38457468f06b7931e0c2c578f218f922774d0dc7e2ee6ef3b08d4d689 -> 90fda086a4e13aa069a5981f63ed58ab1c71f1ed3da5e1480a080e1992b0d3e5; scaling 1.061 < 1.5; fixture_count 196. Only the rust fingerprint moves; the other 14 languages are byte-identical. The earlier revision of this note cited 655aed01... as the prior value, which was two rebaselines stale (it predates #2604 and #2714); the CI gate compares live fingerprints, not this prose, so nothing caught it.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 05acbaca48427e0d9e0793bcd0ce4057712d3716b5e7868189c12e05ef8dd300 -> 83812d82f0e2c3eb552f3246381ca3dd5ccd6783d63aba3325f1343e7772280c.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 83812d82f0e2c3eb552f3246381ca3dd5ccd6783d63aba3325f1343e7772280c -> 6174889b8c98e0af430fa54c268dc781989ca9a8172d690eebae37a95f77e809.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 05acbaca48427e0d9e0793bcd0ce4057712d3716b5e7868189c12e05ef8dd300 -> 83812d82f0e2c3eb552f3246381ca3dd5ccd6783d63aba3325f1343e7772280c.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 83812d82f0e2c3eb552f3246381ca3dd5ccd6783d63aba3325f1343e7772280c -> 6174889b8c98e0af430fa54c268dc781989ca9a8172d690eebae37a95f77e809.",
"capture_groups_small": 5507,
"capture_groups_large": 17607,
"capture_groups_fp": 3699,
@ -119,9 +125,9 @@
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior df7b1565f9115d66b1ae32e4a408d651afb2521b14e5ca615f3be426c29af618 -> 4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd; scaling 1.078 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: PHP first-class callable and variable-invocation flow facts with invocation-result suppression. Prior 31c9e3f3cb7094a2bf9021cf9db859036e002f8b44605cd993b470fc600e97cb -> df7b1565f9115d66b1ae32e4a408d651afb2521b14e5ca615f3be426c29af618; scaling 1.074 < 1.5.",
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04). | #2481/#2482: PHP imports carry a symbol-kind capture so function/constant imports resolve by declaring file; capture shape changes, scaling remains linear (~1.04).",
"_note": "PR #1931: F53 import multi-clause, F54 enum_case, F55 anonymous_class \u2014 fixture count 138\u2192140, fingerprint drift expected.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd -> 3745662053c76b6ae0a84a29aad319626ed5ccb88f7b9376c2680d3dc6502e28.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 3745662053c76b6ae0a84a29aad319626ed5ccb88f7b9376c2680d3dc6502e28 -> b213a872342da2d866b04681dede988770e4d3dfdc0d6e9f62212ec5b59cdc2c.",
"_note": "PR #1931: F53 import multi-clause, F54 enum_case, F55 anonymous_class — fixture count 138→140, fingerprint drift expected.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd -> 3745662053c76b6ae0a84a29aad319626ed5ccb88f7b9376c2680d3dc6502e28.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 3745662053c76b6ae0a84a29aad319626ed5ccb88f7b9376c2680d3dc6502e28 -> b213a872342da2d866b04681dede988770e4d3dfdc0d6e9f62212ec5b59cdc2c.",
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. The existing php-use-function-const/app/Config/constants.php fixture now captures its MAX_RETRIES declaration. Its one new declaration group is the entire delta. Fixture entries 143; changed entries 1; capture groups 2241 -> 2242. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
},
"ruby": {
@ -132,10 +138,10 @@
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior cff273ae6cb7232c977d9241581834a2a2fa8bcf6369f7bd8f2471cd4419a6ef -> bf50ec6a53c8c91680dc6feac63a8956e78b1059249232dc25a0cfed25f31236; scaling 1.103 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Ruby Method/Proc callable flow facts with invocation/constructor-result suppression. Prior b5ea93bb3d0469c3821a8c70f5d5991c6f326e41097c119ad691154301dcc753 -> cff273ae6cb7232c977d9241581834a2a2fa8bcf6369f7bd8f2471cd4419a6ef; scaling 1.086 < 1.5.",
"_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_note": "F62: + scope_resolution class/module declaration captures \u2014 fixture count 78\u219281, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) \u2014 pure fixture-corpus drift, scope-extractor captures unchanged; 81\u219282. #1991: + ruby-nested-mixin-tail-collision fixture (85\u219286). Recomputed on the #942 merge (fixture-comment rewording shifts capture byte-positions, capture LOGIC unchanged): bf6b13a -> b5ea93bb.",
"_note": "F62: + scope_resolution class/module declaration captures — fixture count 78→81, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) — pure fixture-corpus drift, scope-extractor captures unchanged; 81→82. #1991: + ruby-nested-mixin-tail-collision fixture (85→86). Recomputed on the #942 merge (fixture-comment rewording shifts capture byte-positions, capture LOGIC unchanged): bf6b13a -> b5ea93bb.",
"_rebaselined_2522_review_fixes": "PR #2522 review fixes: bare identifiers are calls, not callable references (bareNamesAreCalls). Prior bf50ec6a53c8c91680dc6feac63a8956e78b1059249232dc25a0cfed25f31236 -> 070e4e11502442998ddf4048c2981cf1b2b735a87362ff854c5d14d71f98f4e2; scaling ratio re-verified within budget.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior fea3edf82f521995147874b7f6c5f9e2eb88efdebf6365668f3260e913f0b558 -> fc81941b0a921074fa80dc448284de9a23bd07358ddc84d4894797cc08c3fe83.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior fc81941b0a921074fa80dc448284de9a23bd07358ddc84d4894797cc08c3fe83 -> 1c8c9c4b54036fa24c2a81e39ea530e938645c856d369075e5f437da78218c57.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior fea3edf82f521995147874b7f6c5f9e2eb88efdebf6365668f3260e913f0b558 -> fc81941b0a921074fa80dc448284de9a23bd07358ddc84d4894797cc08c3fe83.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior fc81941b0a921074fa80dc448284de9a23bd07358ddc84d4894797cc08c3fe83 -> 1c8c9c4b54036fa24c2a81e39ea530e938645c856d369075e5f437da78218c57.",
"_rebaselined_3354_callable_alternatives": "#3354 callable alternatives: a callable chosen by a value-selecting source now flows every branch it can yield (`a ?? b`, `a || b`, `a or b`, `c ? a : b`, statement `if`, elvis), and an operator branch (`x == f || g`) stays opaque instead of seeding a qualified name. Verified by running the BASE (merge-base 233ca2849) and HEAD emitters over the SAME HEAD fixture corpus: every added or removed match is an `@callable-flow.*` match on one of those sources, and the pre-existing corpus is byte-identical (all other languages: zero delta). The rest of the drift is corpus growth from this PR's regression fixture, which this bench globs. Corpus growth: ruby-callable-alternatives/app.rb (+1 file, +58 groups). Emitter delta: +5 / -0: seeds for single-statement `if` / `elsif` branches (run_then, run_else, run_sweep, run_a, run_b); the multi-statement branch contributes nothing. capture_groups_fp 1358 -> 1416, fixture_count 91 -> 92; synthetic counts unchanged; scaling 1.04 < 1.5. Prior 1c8c9c4b54036fa24c2a81e39ea530e938645c856d369075e5f437da78218c57 -> 45e65d9fa8a9e5e905ddb596b179b77c86ed13ab5139c6b17ccaaf7315dfe46a.",
"capture_groups_small": 3011,
"capture_groups_large": 9611,
@ -155,8 +161,8 @@
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Swift function-value callable flow facts with invocation-result suppression. Prior 180ac68e780bdf6f9089d53f51cbb9a66aed3e7774631cc3fcbaae5020213998 -> 5f923c6604d825d12b249f31c155b0f4d13a8379d532e5dde64a0f9b15cf4725; scaling 1.043 < 1.5.",
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression).",
"_rebaselined_2522_review_fixes": "PR #2522 review fixes: assignment target:/result: fields join the shared fallback. Prior 7687ee2466e16020a12440a03fbda53e63aa05f94b4481f6133c09867a0d560d -> 115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248; scaling ratio re-verified within budget.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248 -> a6fca5f052ae5ec635b56051e28a168c864a988b2221a3279ddd69807378ba0b.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior a6fca5f052ae5ec635b56051e28a168c864a988b2221a3279ddd69807378ba0b -> 2f04ae960123cf50138a49fabdc5a146c2963170cecf5755c552b23c9055a9e7.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248 -> a6fca5f052ae5ec635b56051e28a168c864a988b2221a3279ddd69807378ba0b.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior a6fca5f052ae5ec635b56051e28a168c864a988b2221a3279ddd69807378ba0b -> 2f04ae960123cf50138a49fabdc5a146c2963170cecf5755c552b23c9055a9e7.",
"_rebaselined_inferred_field_receiver_2807": "#2807: optional property annotations (`var a: Outer?`) now emit a type binding. The prior pattern required the `user_type` to be a DIRECT child of the annotation, so an `optional_type` wrapper meant an optional field was never typed at all and its receiver could not resolve. ADDS @type-binding.annotation captures on the optional form only; no capture is removed. Prior 2f04ae960123cf50138a49fabdc5a146c2963170cecf5755c552b23c9055a9e7 -> adef9284feaecd39cb490aebce83876e15b9150c7a04b00a396feb78b7e1e0a9; scaling 1.023 < 1.5.",
"_rebaselined_nested_constructor_3262": "#3262: the existing nested-constructor fixture now uses the qualified legal form `extension Outer.Container`, covering full extension-owner recovery. Fixture-only capture drift; no capture logic changed. Prior adef9284feaecd39cb490aebce83876e15b9150c7a04b00a396feb78b7e1e0a9 -> 3ac80b64776969effb75c8d32aa0bc38fc6a41c6c115a33829c502559fa404f9; scaling 1.025 < 1.5.",
"_rebaselined_3309_call_result_assignment": "#3309: the Swift protocol-extension regression fixture adds one file with an untyped local initialized from a helper call, plus a compound-field receiver call. The capture query is unchanged; this is fixture-corpus growth only. capture_groups_fp 1235 -> 1272 and fixture_count 62 -> 64; synthetic scale counts remain 5012/16012. Prior adef9284feaecd39cb490aebce83876e15b9150c7a04b00a396feb78b7e1e0a9 -> 4d32535d454cd79086f0fcaec3b75de855c1314e73630321460e569f19aa484f; measured scaling 1.020 < 1.5.",
@ -170,7 +176,7 @@
"fingerprint": "1c5b892a136318eddee3435225284896c2effb52dd51ca5717c92887ae4a39a3",
"scaling_budget": 1.5,
"_rebaselined_2963_package_imports": "#3369 package-import fixtures: dart-package-imports adds six .dart files. CORPUS GROWTH ONLY, NOT A CAPTURE CHANGE: parking that directory restores 795290a9524ee0dc38af635536e9744c401ec13828ec77822d39e59e1cd6e649 byte-for-byte. The six files contribute 33 capture groups (862 -> 895) and fixture_count 37 -> 43. Synthetic scale counts stay 5011/16011. Local scaling 1.091 < 1.5. Prior 795290a9524ee0dc38af635536e9744c401ec13828ec77822d39e59e1cd6e649 -> f4edcfedb6e97def5ffbb9d3227fce2549e5c6c6cc876a05bc3b68a46ed30c24.",
"_rebaselined_generic_instantiation_2912": "#2912: the Dart heritage marker carries a fourth field \u2014 the type arguments the clause was written with (`implements Validator<String>`) \u2014 so interface dispatch can prune implementors of a mismatched instantiation. Additive marker text on existing heritage matches rather than a new match, so this is digest drift only; a marker from a pre-#2912 cache simply has no fourth field and reads as unknown. Prior ba93c90dcd341259e8e088816bc8c76ad27882419f665e35c056dc22fa54cf73 -> 3a8ddabbeb1cba47a4757451d4f79d726ca230fd15e860772b11526fbb1c6687; scaling 1.027 < 1.5.",
"_rebaselined_generic_instantiation_2912": "#2912: the Dart heritage marker carries a fourth field — the type arguments the clause was written with (`implements Validator<String>`) — so interface dispatch can prune implementors of a mismatched instantiation. Additive marker text on existing heritage matches rather than a new match, so this is digest drift only; a marker from a pre-#2912 cache simply has no fourth field and reads as unknown. Prior ba93c90dcd341259e8e088816bc8c76ad27882419f665e35c056dc22fa54cf73 -> 3a8ddabbeb1cba47a4757451d4f79d726ca230fd15e860772b11526fbb1c6687; scaling 1.027 < 1.5.",
"_rebaselined_2538": "#2538: Dart extension type headers are preprocessed into normal extension declarations before scope capture, so extension type symbols and their methods are now emitted. Intentional Dart-only capture fingerprint drift; CI measured scaling 1.042 < 1.5.",
"_rebaselined_2538_implements": "#2538 tri-review follow-up: Dart extension type implements clauses now emit heritage markers and fixture coverage asserts IMPLEMENTS edges, including multi-arg generic interfaces. Prior committed baseline 66a46d5ff09f3d11b2771db0f48596fe7057e95c5bc8f56241fdb911137298c3 -> ba93c90dcd341259e8e088816bc8c76ad27882419f665e35c056dc22fa54cf73; scaling 0.945 < 1.5.",
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 29ce2bfe70b246b1c9d5e99c0ec11e850c22e9672737592207242b7f4cc824b8 -> 66a46d5ff09f3d11b2771db0f48596fe7057e95c5bc8f56241fdb911137298c3; scaling 1.054 < 1.5.",
@ -191,7 +197,7 @@
"_rebaselined_2900_record_heritage": "#2900 review follow-up: the Java scale unit now includes a record implementing Marker, so the record-declaration @reference.inherits path is fingerprinted and exercised at scale. Prior b29e263524f55151dcb7cfc4c929d3d1d7bb360355cee4e832158f927857f663 -> 36d689c58526c4482fbd701d1d9ca156623a3970734ead145717858712271ab5; scaling 1.042 < 1.5.",
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata; same-name lexical regions use an O(ancestor-depth) ID-set lookup. Prior d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a -> 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4; scaling 0.992 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Java method-reference/SAM callable flow facts with invocation-result suppression. Prior 062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada -> d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a; scaling 1.074 < 1.5.",
"_rebaselined": "#2357 (supersedes #2353): + java-cast-receiver, java-this-field-chain, java-this-dispatch fixtures (cast-wrapped receivers, this.field chains incl. initializer contexts, bare-this dispatch pinning). Drift is purely fixture-additive: with the three new dirs parked, the fingerprint reproduces the prior baseline byte-identically \u2014 no emit/capture change. #1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_rebaselined": "#2357 (supersedes #2353): + java-cast-receiver, java-this-field-chain, java-this-dispatch fixtures (cast-wrapped receivers, this.field chains incl. initializer contexts, bare-this dispatch pinning). Drift is purely fixture-additive: with the three new dirs parked, the fingerprint reproduces the prior baseline byte-identically — no emit/capture change. #1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_note": "#1928 / #2045: F35 adds qualified + qualified-generic constructor query captures (`new pkg.Foo()`, `new a.b.Foo()`, `new pkg.Box<T>()`); F38 synthesizes `@reference.call.constructor` on `super(...)`/`this(...)` explicit_constructor_invocation nodes; F41 generic-aware stripQualifier in interpret (type-binding normalization). + java-qualified-constructor and java-explicit-constructor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.06).",
"_rebaselined_2522_review_fixes": "PR #2522 review fixes: get/test dropped from callableProtocolMethods. Prior 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4 -> f3b4f4b6610e07c3ac90deb1c53d3572b6ad55a36e5d7134984876d30031ff67; scaling ratio re-verified within budget.",
"_rebaselined_2550_instance_model": "PR #2549 (#2550): anonymous class bodies emit synthesized @declaration.class/@declaration.name (Worker$N), an @reference.inherits to the constructed type, and receiver @type-binding.* captures; six new java-* fixtures joined the corpus. Prior f3b4f4b6610e07c3ac90deb1c53d3572b6ad55a36e5d7134984876d30031ff67 -> d79c3b92acfc866094981499b977388ca14f90839bca0c040342ab1cec00aa90; scaling 1.058 < 1.5.",
@ -199,8 +205,8 @@
"_rebaselined_2564_record_capture": "PR for #2564: JAVA_QUERIES gained a (record_declaration name: (identifier) @name) @definition.record capture, previously entirely missing (record_declaration had no structure-phase capture at all, unlike class/interface/enum) - a record's methods existed as ownerless Method nodes with no HAS_METHOD edge. Two new java-* fixtures (java-record-methods, java-new-expr-chain-call) joined the corpus. Prior 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca -> 85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537; scaling 1.059 < 1.5.",
"_rebaselined_2561_enum_constant_receiver": "PR for #2561: synthesizeJavaAnonymousClassDeclarations now emits a class-scope @type-binding.annotation/name/type per enum constant (constant simple name -> its E$N synthesized class when bodied, else the host enum) so E.CONST.method() resolves through the existing compound-receiver chain walk. Two drivers of the drift, both in the java-enum-constant-body fixture (this bench's corpus IS test/fixtures/lang-resolution): (1) one extra type-binding match per enum_constant from the capture change; (2) review follow-up added a body-less Plain.java enum + EnumConst.dispatchToConstant/dispatchInherited methods (bodied-override, inherited-via-MRO, and body-less dispatch call sites). The review's fail-safe hardening (bodied constant binds ONLY to E$N, never the host enum, when name synthesis fails on a malformed tree) is output-neutral on this well-formed corpus (verified: fingerprint identical with and without it). Prior 85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537 -> d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686; scaling < 1.5.",
"_rebaselined_2562_local_classes": "#2562: Java block-local classes, enums, records, and interfaces use source-type-relative JLS 13.1 Host$NLocal identities with javac-compatible per-(host, simple-name) numbering; anonymous numbering remains separate. Lexical aliases begin at each declaration and end with its immediate block. Expanded java-local-class-naming fixtures cover declaration order, disjoint blocks, initializers, lambdas, local type kinds, and recursive local/member/anonymous host chains. Prior d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686 -> 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197; scaling 1.204 < 1.5.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197 -> 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee -> a9943355e945e03ddb87c800f4cc1f62b3d04feefb3ec64c258d8e0bb3b3fcd9.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197 -> 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee -> a9943355e945e03ddb87c800f4cc1f62b3d04feefb3ec64c258d8e0bb3b3fcd9.",
"capture_groups_small": 6255,
"capture_groups_large": 20005,
"capture_groups_fp": 3586,
@ -214,8 +220,8 @@
"_rebaselined_2935_synthetic_declarations": "PR #2935 review follow-up: the local-type stress corpus includes synthesized anonymous declarations, which now carry the presence-only @declaration.is-synthetic sidecar. DIGEST DRIFT ONLY, NOT A CAPTURE-SET CHANGE. Prior 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633 -> 560734cd053fb4f4b23aa04bc7870c22089a8deedb0217fa9c1b4db689e02a97; CI scaling 1.002 < 1.5.",
"_rebaselined_2917_record_component_accessors": "#2917: the focused local-type fixture corpus contains local records, so their implicit component accessors add the same bounded scope/declaration captures as the general Java corpus. No local-type naming logic changed. Prior 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633 -> 3e22f368a4ee139be7cb91ff4fb77ddadf60c55efe8d66955ec81f366a46e460; scaling 1.032 < 1.5, capture_groups_fp 680. Re-measured on top of #2935's is-synthetic sidecar after merging origin/main: 560734cd053fb4f4b23aa04bc7870c22089a8deedb0217fa9c1b4db689e02a97 -> bdde823fa725e636e257940efb4c8655aa23124c1727cbaa8856d1ad8f71729e; scaling 0.997 < 1.5, capture_groups_fp 680.",
"_added": "#2562 performance follow-up: co-scales same-host, same-name local classes and anonymous classes to gate JLS binary-name ordinal allocation. Precomputed per-sequence ordinals reduce the focused 100->800 workload from 176->6655ms to 141->752ms; normalized 250->800 scaling is 1.054.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior a9ad88de21ca6747a923260dbdf677fb74a004abbf9d57781f745e3a9027530b -> 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236 -> 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior a9ad88de21ca6747a923260dbdf677fb74a004abbf9d57781f745e3a9027530b -> 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236 -> 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633.",
"capture_groups_fp": 680,
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 2 constructor-parameter type bindings are added and 38 duplicate qualified free-call matches are removed. Every removed free-call match retains a member-call match with the identical call anchor and name capture. Fixture entries 6; changed entries 6; capture groups 680 -> 644. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
},
@ -226,26 +232,26 @@
"_rebaselined_3446_mcp_tools_fixture": "#3446 adds typescript-mcp-tools/src/{handlers,server,tools}.ts: corpus growth only, with fixture_count 171 -> 174 and capture_groups_fp 2753 -> 2894. Excluding only that fixture directory restores the prior fingerprint 77c9b4ea654123a64972db8348190ec250b669472b150db65ea8c7f20b355467 exactly. The scope emitter, synthetic capture counts (4503/14403), scaling budget, and other language baselines are unchanged.",
"_rebaselined_3190": "Capture matches now retain explicit ESM export/private evidence, including synthesized default HOCs; CommonJS surfaces remain undecided. Capture group counts unchanged. Scaling budget unchanged.",
"scaling_budget": 1.5,
"_rebaselined_2934_import_type_only": "#2934: `import-decomposer.ts` attaches a presence-only `@import.type-only` synthetic capture to specifiers `tsc` erases, so `check --cycles` can stop counting type-only edges as initialization cycles. DIGEST DRIFT ONLY, NOT A CAPTURE-SET CHANGE \u2014 the tag is added to import matches that already existed, never a new match, the same shape as the #2747 receiver-chain rebaseline. Every count is unchanged: capture_groups_fp 2414, fixture_count 155, capture_groups_small/large 4503/14403 (those measure the SYNTHETIC scaling source, which has no imports at all). The fingerprint moves because `canonicalizeMatch` in measure.mjs hashes every TAG on every match, synthetics included, so one extra presence-only tag on an existing match rewrites that match's canonical string. Attribution is exact, not inferred: neutralizing ONLY the `m['@import.type-only'] = \u2026` assignment in import-decomposer.ts and re-running returns the fingerprint to c2fbf8a89e5686dd\u2026 byte-for-byte, so nothing else in the TypeScript capture stream moved. All 14 other languages report ok. Scaling 0.997 < 1.5. NOTE ON THE CONTROL: javascript did not move (2026993b\u2026, 43 fixtures), but it is a WEAK control here \u2014 `import type` is TypeScript-only syntax, so a JS corpus cannot express the construct and could not have drifted either way. It evidences no collateral damage, not the correctness of the TS change; the exact-attribution check above is what does that. Prior c2fbf8a89e5686dd1ff3659b20d41d8b05ebcc9790356e3653ee0c8ca5d365c8 -> f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f.",
"_rebaselined_2934_import_type_only": "#2934: `import-decomposer.ts` attaches a presence-only `@import.type-only` synthetic capture to specifiers `tsc` erases, so `check --cycles` can stop counting type-only edges as initialization cycles. DIGEST DRIFT ONLY, NOT A CAPTURE-SET CHANGE — the tag is added to import matches that already existed, never a new match, the same shape as the #2747 receiver-chain rebaseline. Every count is unchanged: capture_groups_fp 2414, fixture_count 155, capture_groups_small/large 4503/14403 (those measure the SYNTHETIC scaling source, which has no imports at all). The fingerprint moves because `canonicalizeMatch` in measure.mjs hashes every TAG on every match, synthetics included, so one extra presence-only tag on an existing match rewrites that match's canonical string. Attribution is exact, not inferred: neutralizing ONLY the `m['@import.type-only'] = …` assignment in import-decomposer.ts and re-running returns the fingerprint to c2fbf8a89e5686dd… byte-for-byte, so nothing else in the TypeScript capture stream moved. All 14 other languages report ok. Scaling 0.997 < 1.5. NOTE ON THE CONTROL: javascript did not move (2026993b…, 43 fixtures), but it is a WEAK control here — `import type` is TypeScript-only syntax, so a JS corpus cannot express the construct and could not have drifted either way. It evidences no collateral damage, not the correctness of the TS change; the exact-attribution check above is what does that. Prior c2fbf8a89e5686dd1ff3659b20d41d8b05ebcc9790356e3653ee0c8ca5d365c8 -> f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f.",
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 27f937bfb47d4bded316ea3c785ff659c8cd88a5761d928f113477a08c802c78 -> e05446620c5b80b7aae291cfdf32f693580fada2ae687124769b04a0c03bfe63; scaling 0.983 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: lexical callable bindings, direct-callee argument metadata, and invocation-result suppression. Prior db5933cc6760234ed7d495123410feba6de243646d583f20d43032b9459f81fd -> 27f937bfb47d4bded316ea3c785ff659c8cd88a5761d928f113477a08c802c78; scaling 0.975 < 1.5.",
"_rebaselined_callable_flow": "Callable assignment/copy/formal/argument/invoke facts (also consumed by Vue script blocks). Prior 25de86fd3377132c4e35d3d98f4f94a58e0cfeb7c22948a8ea3be4e793be74fd -> db5933cc6760234ed7d495123410feba6de243646d583f20d43032b9459f81fd; measured scaling ratio 0.951 < 1.5.",
"_rebaselined": "#1962: F44 (class scope@), F85 (enum member declarations), F87 (optional_parameter type annotations) add new captures \u2014 fingerprint drift expected.",
"_note": "#1968: F44, F85, F87 \u2014 fingerprint drift expected.",
"_rebaselined": "#1962: F44 (class scope@), F85 (enum member declarations), F87 (optional_parameter type annotations) add new captures — fingerprint drift expected.",
"_note": "#1968: F44, F85, F87 — fingerprint drift expected.",
"_rebaselined_2522": "#2522 intentional @reference.value-ref/property-key capture additions. GitHub Actions run 29553361660 job 87800394279: prior 3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9 -> 25de86fd3377132c4e35d3d98f4f94a58e0cfeb7c22948a8ea3be4e793be74fd; scaling ratio 0.987 < 1.5.",
"_rebaselined_2550_instance_model": "PR #2549 (#2545/#2551): object literals emit @scope.object (was unscoped, then @scope.block during development). Prior e05446620c5b80b7aae291cfdf32f693580fada2ae687124769b04a0c03bfe63 -> 3280b13d3f9378ab23eee31c2edc779b5a9ae1e7bb510c23a24855b44406d2f4; scaling 0.981 < 1.5.",
"_rebaselined_receiver_owner_2701": "#2701: every non-arrow function form now carries a `@receiver-owner.this` marker on the same node as `@scope.function`, so a scope that BINDS its own `this` can stop the receiver walk (`Scope.ownsReceivers`). Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus against 1d3088173f6f93827641b476d614d5d15cd4f3ea: the ONLY delta is @receiver-owner.this (typescript +143, javascript +32) \u2014 every other capture count is byte-identical, so no existing capture moved. Prior 3280b13d3f9378ab23eee31c2edc779b5a9ae1e7bb510c23a24855b44406d2f4 -> 281e95484203b481094729ca249ef0423c41273eac35e424cdfd032a0dac7699.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior cad25be9f81d6e021ebae8dcb166bc0af3a1ba8021f1506f6ca93fd4c2649000 -> 9e112415f1169f08576826c12ea1d137d1994e34b44c45986c9ffee83b8b4edc.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 9e112415f1169f08576826c12ea1d137d1994e34b44c45986c9ffee83b8b4edc -> cdefe88d3c275f31953216c676ef32c7bf5727d56b9c3840b81ee6bf85749dff.",
"_rebaselined_inferred_field_receiver_2807": "#2807: inference-typed class fields now emit a type binding \u2014 `public_field_definition` with a `new_expression` value, and `this.<field> = new ...` carrying a @type-binding.this-field marker. ADDS @type-binding.constructor captures only; no capture is removed, and the annotated form is unchanged because annotation outranks constructor-inferred in typeBindingStrength. Prior cdefe88d3c275f31953216c676ef32c7bf5727d56b9c3840b81ee6bf85749dff -> 248b56f0d7a0a6fc7a949dc7afb8611e135ed642bccc2631b96ebb9d686bb965; scaling 0.994 < 1.5.",
"_rebaselined_ts_heritage_2842": "#2842 review: TypeScript heritage capture now emits `@reference.inherits` for `interface_declaration` (bases on `extends_type_clause`) and `abstract_class_declaration` (bases on `class_heritage`), which were both silently skipped \u2014 so `interface B extends A` and `abstract class X implements I` produced no edge and every interface-dispatch walk dead-ended on a bodiless declaration. Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus (145 files) with and without the change: the ONLY deltas are @reference.inherits 17 -> 20 (+3) and its paired @reference.name 245 -> 248 (+3), emitted together by emitTsInheritanceBase. Every other capture count is byte-identical, so no existing capture moved. The +3 is the three `interface X extends BasePayload` declarations in typescript-generic-calls/src/{auth,admin,guest}.ts. javascript is unchanged (no interfaces in the language). Prior 248b56f0d7a0a6fc7a949dc7afb8611e135ed642bccc2631b96ebb9d686bb965 -> 7a960908031331360ce582f5b55b7681e1cd7f8a2eabfd73c00982cb17f2a949.",
"_rebaselined_receiver_owner_2701": "#2701: every non-arrow function form now carries a `@receiver-owner.this` marker on the same node as `@scope.function`, so a scope that BINDS its own `this` can stop the receiver walk (`Scope.ownsReceivers`). Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus against 1d3088173f6f93827641b476d614d5d15cd4f3ea: the ONLY delta is @receiver-owner.this (typescript +143, javascript +32) — every other capture count is byte-identical, so no existing capture moved. Prior 3280b13d3f9378ab23eee31c2edc779b5a9ae1e7bb510c23a24855b44406d2f4 -> 281e95484203b481094729ca249ef0423c41273eac35e424cdfd032a0dac7699.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior cad25be9f81d6e021ebae8dcb166bc0af3a1ba8021f1506f6ca93fd4c2649000 -> 9e112415f1169f08576826c12ea1d137d1994e34b44c45986c9ffee83b8b4edc.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 9e112415f1169f08576826c12ea1d137d1994e34b44c45986c9ffee83b8b4edc -> cdefe88d3c275f31953216c676ef32c7bf5727d56b9c3840b81ee6bf85749dff.",
"_rebaselined_inferred_field_receiver_2807": "#2807: inference-typed class fields now emit a type binding — `public_field_definition` with a `new_expression` value, and `this.<field> = new ...` carrying a @type-binding.this-field marker. ADDS @type-binding.constructor captures only; no capture is removed, and the annotated form is unchanged because annotation outranks constructor-inferred in typeBindingStrength. Prior cdefe88d3c275f31953216c676ef32c7bf5727d56b9c3840b81ee6bf85749dff -> 248b56f0d7a0a6fc7a949dc7afb8611e135ed642bccc2631b96ebb9d686bb965; scaling 0.994 < 1.5.",
"_rebaselined_ts_heritage_2842": "#2842 review: TypeScript heritage capture now emits `@reference.inherits` for `interface_declaration` (bases on `extends_type_clause`) and `abstract_class_declaration` (bases on `class_heritage`), which were both silently skipped — so `interface B extends A` and `abstract class X implements I` produced no edge and every interface-dispatch walk dead-ended on a bodiless declaration. Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus (145 files) with and without the change: the ONLY deltas are @reference.inherits 17 -> 20 (+3) and its paired @reference.name 245 -> 248 (+3), emitted together by emitTsInheritanceBase. Every other capture count is byte-identical, so no existing capture moved. The +3 is the three `interface X extends BasePayload` declarations in typescript-generic-calls/src/{auth,admin,guest}.ts. javascript is unchanged (no interfaces in the language). Prior 248b56f0d7a0a6fc7a949dc7afb8611e135ed642bccc2631b96ebb9d686bb965 -> 7a960908031331360ce582f5b55b7681e1cd7f8a2eabfd73c00982cb17f2a949.",
"capture_groups_small": 4503,
"capture_groups_large": 14403,
"capture_groups_fp": 2465,
"fixture_count": 167,
"_rebaselined_blind_spots_2856": "#2856 blind-spots series: the JS/TS SCOPE queries gained capture rules, so fingerprint drift is expected and additive. Verified before re-baselining by diffing the capture-name sets in both scope queries against origin/main: TypeScript gained exactly @reference.read.identifier (A2 bare-identifier reads in value positions) and @reference.type (R2-2 type references, so a declared contract stops reporting incoming:{}); JavaScript gained exactly @reference.read.identifier, @reference.read.destructured (R2-1c) and @reference.write.property-key (R2-1b record-construction writes). NOTHING was removed on either side \u2014 the delta is a pure superset, which is the check that no existing capture moved. capture_groups_small/large are unchanged (4503/14403) because those measure the SYNTHETIC scaling source, which this branch does not touch; only the fixture-corpus count moves. capture_groups_fp 2097 -> 2338 and fixture_count 146 -> 151 from 21 new lang-resolution fixtures. Scaling stayed linear and inside budget: typescript 1.116 < 1.5, javascript 1.010 < 1.5. Prior typescript ed92588e0fc7b28b3a0174339ac378b4dd85965fe007db1208dea97a65ce0571 -> f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7; prior javascript 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594 -> 2026993b81b873839dd2ef8797d9c14d9c48516b2b57b05ac17d8d43f2f4eba3.",
"_rebaselined_type_parameter_shadowing_w2_8": "W2-8: `@declaration.type-parameters` is now captured on generic FUNCTIONS, generator functions and type ALIASES, not only on class/interface declarations. NO NEW CAPTURE NAME \u2014 verified by diffing the capture-name sets against the wave-1 branch, which returns empty; the tag already existed and simply fires on more declarations. That is the whole delta: capture_groups_fp 2338 -> 2371 (+33 occurrences of an existing tag) and fixture_count 151 -> 152 (one new fixture, typescript-type-parameters). capture_groups_small/large unchanged at 4503/14403, since those measure the synthetic scaling source this does not touch. Scaling 1.06 < 1.5. JavaScript is untouched \u2014 it has no type parameters \u2014 and its fingerprint does not move, which is the check that this is the TS declaration rules and not something broader. Prior f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7 -> 62c7f1bfbe568eed927fb78f00061ed5e49d12511fd8260648b876df386f3b4c.",
"_rebaselined_2899_review_type_parameter_scope_fixtures": "PR #2899 review follow-up: FIXTURE-CORPUS GROWTH ONLY \u2014 no query rule changed and no capture name was added or removed. `typescript/query.ts` is byte-identical to the previous baseline; the type-parameter shadowing defect was fixed on the RESOLUTION side (`walkers.ts` gains a `declarationOpenedScope` gate so a declaration's `typeParameters` bind only inside the scope that declaration opened, and the `USES` guard moved from `graph-bridge/references-to-edges.ts` to `resolve-references.ts` where the spelled `site.name` is in hand). The fingerprint moves because measure.mjs fingerprints the whole `lang-resolution/typescript-*` fixture corpus and the regression tests add three files to `typescript-type-parameters/src/` (values.ts, aliased.ts, namespaced.ts) plus two scope-less generic aliases in shapes.ts. Per-file accounting sums exactly to the delta: shapes.ts 33->35 (+2), values.ts +11, aliased.ts +10, namespaced.ts +20 = +43. capture_groups_fp 2371 -> 2414; fixture_count 152 -> 155. capture_groups_small/large unchanged at 4503/14403 (they measure the SYNTHETIC scaling source, untouched). JAVASCRIPT IS THE CONTROL AND DID NOT MOVE (fingerprint 2026993b..., 43 fixtures) \u2014 which is the check that this is corpus growth and not a capture regression; all 14 other languages report `ok`. Scaling 0.976 < 1.5. Prior 62c7f1bfbe568eed927fb78f00061ed5e49d12511fd8260648b876df386f3b4c -> c2fbf8a89e5686dd1ff3659b20d41d8b05ebcc9790356e3653ee0c8ca5d365c8.",
"_rebaselined_blind_spots_2856": "#2856 blind-spots series: the JS/TS SCOPE queries gained capture rules, so fingerprint drift is expected and additive. Verified before re-baselining by diffing the capture-name sets in both scope queries against origin/main: TypeScript gained exactly @reference.read.identifier (A2 bare-identifier reads in value positions) and @reference.type (R2-2 type references, so a declared contract stops reporting incoming:{}); JavaScript gained exactly @reference.read.identifier, @reference.read.destructured (R2-1c) and @reference.write.property-key (R2-1b record-construction writes). NOTHING was removed on either side — the delta is a pure superset, which is the check that no existing capture moved. capture_groups_small/large are unchanged (4503/14403) because those measure the SYNTHETIC scaling source, which this branch does not touch; only the fixture-corpus count moves. capture_groups_fp 2097 -> 2338 and fixture_count 146 -> 151 from 21 new lang-resolution fixtures. Scaling stayed linear and inside budget: typescript 1.116 < 1.5, javascript 1.010 < 1.5. Prior typescript ed92588e0fc7b28b3a0174339ac378b4dd85965fe007db1208dea97a65ce0571 -> f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7; prior javascript 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594 -> 2026993b81b873839dd2ef8797d9c14d9c48516b2b57b05ac17d8d43f2f4eba3.",
"_rebaselined_type_parameter_shadowing_w2_8": "W2-8: `@declaration.type-parameters` is now captured on generic FUNCTIONS, generator functions and type ALIASES, not only on class/interface declarations. NO NEW CAPTURE NAME — verified by diffing the capture-name sets against the wave-1 branch, which returns empty; the tag already existed and simply fires on more declarations. That is the whole delta: capture_groups_fp 2338 -> 2371 (+33 occurrences of an existing tag) and fixture_count 151 -> 152 (one new fixture, typescript-type-parameters). capture_groups_small/large unchanged at 4503/14403, since those measure the synthetic scaling source this does not touch. Scaling 1.06 < 1.5. JavaScript is untouched — it has no type parameters — and its fingerprint does not move, which is the check that this is the TS declaration rules and not something broader. Prior f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7 -> 62c7f1bfbe568eed927fb78f00061ed5e49d12511fd8260648b876df386f3b4c.",
"_rebaselined_2899_review_type_parameter_scope_fixtures": "PR #2899 review follow-up: FIXTURE-CORPUS GROWTH ONLY — no query rule changed and no capture name was added or removed. `typescript/query.ts` is byte-identical to the previous baseline; the type-parameter shadowing defect was fixed on the RESOLUTION side (`walkers.ts` gains a `declarationOpenedScope` gate so a declaration's `typeParameters` bind only inside the scope that declaration opened, and the `USES` guard moved from `graph-bridge/references-to-edges.ts` to `resolve-references.ts` where the spelled `site.name` is in hand). The fingerprint moves because measure.mjs fingerprints the whole `lang-resolution/typescript-*` fixture corpus and the regression tests add three files to `typescript-type-parameters/src/` (values.ts, aliased.ts, namespaced.ts) plus two scope-less generic aliases in shapes.ts. Per-file accounting sums exactly to the delta: shapes.ts 33->35 (+2), values.ts +11, aliased.ts +10, namespaced.ts +20 = +43. capture_groups_fp 2371 -> 2414; fixture_count 152 -> 155. capture_groups_small/large unchanged at 4503/14403 (they measure the SYNTHETIC scaling source, untouched). JAVASCRIPT IS THE CONTROL AND DID NOT MOVE (fingerprint 2026993b..., 43 fixtures) — which is the check that this is corpus growth and not a capture regression; all 14 other languages report `ok`. Scaling 0.976 < 1.5. Prior 62c7f1bfbe568eed927fb78f00061ed5e49d12511fd8260648b876df386f3b4c -> c2fbf8a89e5686dd1ff3659b20d41d8b05ebcc9790356e3653ee0c8ca5d365c8.",
"_rebaselined_2953_workspace_fixture": "#2953 adds test/fixtures/lang-resolution/typescript-pnpm-workspace-imports, a pnpm monorepo of 12 .ts files, and the TypeScript capture corpus is collected from test/fixtures. CORPUS GROWTH ONLY, NOT A CAPTURE CHANGE: fixture_count 155 -> 167 and capture_groups_fp 2414 -> 2465 are the 12 new files' own matches; capture_groups_small/large are unchanged at 4503/14403 because those measure the SYNTHETIC scaling source, which the fixture corpus does not feed. Attribution is exact rather than inferred: moving that one fixture directory aside and re-running returns typescript to f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f byte-for-byte with fixture_count back at 155, and [scope-capture --check] PASSES for all 15 languages - so nothing in the TypeScript capture stream moved. #2953 changes import RESOLUTION, which runs after capture and feeds no capture tag. Prior f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f -> 05d1dadd6c9ef35c74079fa50f341b1b36e4fb02c9a89dd1b59f32b7cfd5e633.",
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Drift: `await svc.verify<GuestPayload>(token, ...)` (typescript-generic-calls/src/guest.ts, member call) and `initializer()(() => {...})` (typescript-hof-callbacks/src/store.ts, call-of-call) lose `direct-callee-name`. `await verifyToken<AdminPayload>(token, ...)` (admin.ts/auth.ts) KEEPS `direct-callee-name|verifyToken`: tree-sitter-typescript parses `await f<T>(x)` as call_expression(function: await_expression(f), type_arguments, ...), and wrappedExpression now unwraps `await_expression` so the direct designator survives as it does for the un-awaited spelling. capture_groups_fp 2465 (unchanged). Prior 05d1dadd6c9ef35c74079fa50f341b1b36e4fb02c9a89dd1b59f32b7cfd5e633 -> fed04ed1d5db112387781e405da208ae6b3ab803889773b0455be96f01b893ff.",
"_rebaselined_3354_callable_alternatives": "#3354 callable alternatives: a callable chosen by a value-selecting source now flows every branch it can yield (`a ?? b`, `a || b`, `a or b`, `c ? a : b`, statement `if`, elvis), and an operator branch (`x == f || g`) stays opaque instead of seeding a qualified name. Verified by running the BASE (merge-base 233ca2849) and HEAD emitters over the SAME HEAD fixture corpus: every added or removed match is an `@callable-flow.*` match on one of those sources, and the pre-existing corpus is byte-identical (all other languages: zero delta). The rest of the drift is corpus growth from this PR's regression fixture, which this bench globs. Corpus growth: typescript-callable-alternatives/{index,compare,worker,sweep/index}.ts (+4 files, +288 groups). Emitter delta: +18 / -14: `??` / `||` / `&&` / ternary seeds and copies now name each operand (`&&` its right operand only); the 14 removed matches are the whole-expression seeds and copies carrying a qualified name of the compound source, including the comparison branches in compare.ts that now stay opaque. capture_groups_fp 2465 -> 2753, fixture_count 167 -> 171; synthetic counts unchanged; scaling 0.963 < 1.5. Prior 60e75bbe846f7200005f12c4d95c32e82d9d3feae9f2fd594e484670762b82b0 -> 77c9b4ea654123a64972db8348190ec250b669472b150db65ea8c7f20b355467.",
@ -263,10 +269,10 @@
"_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_rebaselined_2522": "#2522 intentional @reference.value-ref/property-key capture additions. GitHub Actions run 29553361660 job 87800394279: prior d72f03c6c502235d2d4b74d66baa5c7d361f040d7a1b72e84acad61210d05ae8 -> 5567dd47e7ba29821a518c4a9852adc3b774e25ef3e7a6e2b3ecb7b59ddab73c; scaling ratio 1.031 < 1.5.",
"_rebaselined_2550_instance_model": "PR #2549 (#2545/#2551): object literals emit @scope.object. Prior 479927409bbdd9852a36172c8260aa56df260e99129a7a9c20a0d1903dd5538b -> f1ccf42a36895c8e34dcb724286f247d469835f2dcbb23ad3347190adc7fde1c; scaling 1.096 < 1.5.",
"_rebaselined_receiver_owner_2701": "#2701: every non-arrow function form now carries a `@receiver-owner.this` marker on the same node as `@scope.function`, so a scope that BINDS its own `this` can stop the receiver walk (`Scope.ownsReceivers`). Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus against 1d3088173f6f93827641b476d614d5d15cd4f3ea: the ONLY delta is @receiver-owner.this (typescript +143, javascript +32) \u2014 every other capture count is byte-identical, so no existing capture moved. Prior f1ccf42a36895c8e34dcb724286f247d469835f2dcbb23ad3347190adc7fde1c -> 90601494695b834d3a9af7ac4844eac603f4f432809a05554cc59de0674a4354.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 1c71ef628eb75a3b111afa8c2a7c351c16a7f5aab9fac2f098f82b2866312aa8 -> 83344b7cba093702f4528eeee44e438809c229d43b12e69ed288812ce7ffc7bc.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 83344b7cba093702f4528eeee44e438809c229d43b12e69ed288812ce7ffc7bc -> 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594.",
"_rebaselined_blind_spots_2856": "#2856 blind-spots series: the JS/TS SCOPE queries gained capture rules, so fingerprint drift is expected and additive. Verified before re-baselining by diffing the capture-name sets in both scope queries against origin/main: TypeScript gained exactly @reference.read.identifier (A2 bare-identifier reads in value positions) and @reference.type (R2-2 type references, so a declared contract stops reporting incoming:{}); JavaScript gained exactly @reference.read.identifier, @reference.read.destructured (R2-1c) and @reference.write.property-key (R2-1b record-construction writes). NOTHING was removed on either side \u2014 the delta is a pure superset, which is the check that no existing capture moved. capture_groups_small/large are unchanged (4503/14403) because those measure the SYNTHETIC scaling source, which this branch does not touch; only the fixture-corpus count moves. capture_groups_fp 2097 -> 2338 and fixture_count 146 -> 151 from 21 new lang-resolution fixtures. Scaling stayed linear and inside budget: typescript 1.116 < 1.5, javascript 1.010 < 1.5. Prior typescript ed92588e0fc7b28b3a0174339ac378b4dd85965fe007db1208dea97a65ce0571 -> f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7; prior javascript 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594 -> 2026993b81b873839dd2ef8797d9c14d9c48516b2b57b05ac17d8d43f2f4eba3."
"_rebaselined_receiver_owner_2701": "#2701: every non-arrow function form now carries a `@receiver-owner.this` marker on the same node as `@scope.function`, so a scope that BINDS its own `this` can stop the receiver walk (`Scope.ownsReceivers`). Verified before re-baselining by diffing the capture-name histogram over this same fixture corpus against 1d3088173f6f93827641b476d614d5d15cd4f3ea: the ONLY delta is @receiver-owner.this (typescript +143, javascript +32) — every other capture count is byte-identical, so no existing capture moved. Prior f1ccf42a36895c8e34dcb724286f247d469835f2dcbb23ad3347190adc7fde1c -> 90601494695b834d3a9af7ac4844eac603f4f432809a05554cc59de0674a4354.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 1c71ef628eb75a3b111afa8c2a7c351c16a7f5aab9fac2f098f82b2866312aa8 -> 83344b7cba093702f4528eeee44e438809c229d43b12e69ed288812ce7ffc7bc.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 83344b7cba093702f4528eeee44e438809c229d43b12e69ed288812ce7ffc7bc -> 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594.",
"_rebaselined_blind_spots_2856": "#2856 blind-spots series: the JS/TS SCOPE queries gained capture rules, so fingerprint drift is expected and additive. Verified before re-baselining by diffing the capture-name sets in both scope queries against origin/main: TypeScript gained exactly @reference.read.identifier (A2 bare-identifier reads in value positions) and @reference.type (R2-2 type references, so a declared contract stops reporting incoming:{}); JavaScript gained exactly @reference.read.identifier, @reference.read.destructured (R2-1c) and @reference.write.property-key (R2-1b record-construction writes). NOTHING was removed on either side — the delta is a pure superset, which is the check that no existing capture moved. capture_groups_small/large are unchanged (4503/14403) because those measure the SYNTHETIC scaling source, which this branch does not touch; only the fixture-corpus count moves. capture_groups_fp 2097 -> 2338 and fixture_count 146 -> 151 from 21 new lang-resolution fixtures. Scaling stayed linear and inside budget: typescript 1.116 < 1.5, javascript 1.010 < 1.5. Prior typescript ed92588e0fc7b28b3a0174339ac378b4dd85965fe007db1208dea97a65ce0571 -> f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7; prior javascript 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594 -> 2026993b81b873839dd2ef8797d9c14d9c48516b2b57b05ac17d8d43f2f4eba3."
},
"kotlin": {
"fingerprint": "026392f9a60d9808dd993fa6d26f3a2ca6240ed6ac63981d614058ae7acd29e2",
@ -278,13 +284,13 @@
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Kotlin callable-reference flow facts with invocation-result suppression. Prior 4900431791f2b9280009deb2b82659c26ead8aa6fb8731190a7c505dec5a9041 -> bddba25d5a88152bbbee8d70e82c944b5302accb4b625df782adb1d4f7a7ac12; scaling 0.880 < 1.5.",
"_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (: Base()); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.",
"_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0.",
"_rebaselined_2271": "PR #2271: re-vendored tree-sitter-kotlin 0.3.8 -> unreleased fwcd main c8ac3d26 for `fun interface` support + new kotlin-fun-interface fixture in the corpus. Drift is both corpus-additive (the fixture) and grammar-driven (the new grammar parses `fun interface` as a class_declaration, not an ERROR node). Baselined to the NEW grammar's fingerprint, so this --check passes only once the regenerated prebuilds land \u2014 until then CI loads the committed 0.3.8 binary and the bench is red, same as the kotlin fun-interface integration tests. scaling ~0.83 (linear).",
"_rebaselined_2271": "PR #2271: re-vendored tree-sitter-kotlin 0.3.8 -> unreleased fwcd main c8ac3d26 for `fun interface` support + new kotlin-fun-interface fixture in the corpus. Drift is both corpus-additive (the fixture) and grammar-driven (the new grammar parses `fun interface` as a class_declaration, not an ERROR node). Baselined to the NEW grammar's fingerprint, so this --check passes only once the regenerated prebuilds land — until then CI loads the committed 0.3.8 binary and the bench is red, same as the kotlin fun-interface integration tests. scaling ~0.83 (linear).",
"_rebaselined_2522_review_fixes": "PR #2522 review fixes: fieldless assignment nodes decomposed positionally. Prior e856951c2a779163d555dadc8e1bf59304a86caed78ac1f450d9caa2b50f63d1 -> 4b31f46cfb004ba769a96feeb06ae4ef109c77410f54e7aaab4a688df599b112; scaling ratio re-verified within budget.",
"_rebaselined_2550_instance_model": "PR #2549 (#2545): anonymous object expressions (object_literal) emit @scope.class, and the kotlin-object-literal-scope fixture joined the corpus. Prior 4b31f46cfb004ba769a96feeb06ae4ef109c77410f54e7aaab4a688df599b112 -> a6fce0dff00e88d41d85023eaf3f35016b5217c7e5225f24a598e4c70bb63091; scaling 0.951 < 1.5.",
"_rebaselined_2563_instance_ownership": "#2563: kotlin-instance-ownership adds unrelated, inherited, outer-instance, and anonymous-object coverage. Prior a6fce0dff00e88d41d85023eaf3f35016b5217c7e5225f24a598e4c70bb63091 -> 9f159f8810d342ef1c821f466efd6920dad9a190f06000056e6cd2815861b195; scaling 1.257 < 1.5.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged \u2014 the tag is added to existing call matches, never a new match \u2014 so this is digest drift only. Prior 9f159f8810d342ef1c821f466efd6920dad9a190f06000056e6cd2815861b195 -> d3c4d2fa0d82d248a2299cfc888b067187ad1faf2c87a97f93c6ed835eefc3f1.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior d3c4d2fa0d82d248a2299cfc888b067187ad1faf2c87a97f93c6ed835eefc3f1 -> c1f0cc9058ab11b7cd6fc8b440deb6db2b2f530f2eb21178923e68a3d0796c4b.",
"_rebaselined_2766_await_subscript_emission": "#2766: extractMixedChain now walks THROUGH await and subscript nodes and peels transparent wrappers at loop entry, so sites whose receiver is `repos[0]` or `(await f())` mint a receiver chain where they previously minted none. EMISSION CHANGE: more sites carry `@reference.receiver-chain`; no existing chain changed shape. Only go and kotlin drifted of 15 \u2014 the two whose fixture corpora contain such receivers. Prior c1f0cc9058ab11b7cd6fc8b440deb6db2b2f530f2eb21178923e68a3d0796c4b -> efd5dbf80ffcd3bab2834d1010f6fe2b239dcc5d58229938dea9cff8d0f380f2.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 9f159f8810d342ef1c821f466efd6920dad9a190f06000056e6cd2815861b195 -> d3c4d2fa0d82d248a2299cfc888b067187ad1faf2c87a97f93c6ed835eefc3f1.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior d3c4d2fa0d82d248a2299cfc888b067187ad1faf2c87a97f93c6ed835eefc3f1 -> c1f0cc9058ab11b7cd6fc8b440deb6db2b2f530f2eb21178923e68a3d0796c4b.",
"_rebaselined_2766_await_subscript_emission": "#2766: extractMixedChain now walks THROUGH await and subscript nodes and peels transparent wrappers at loop entry, so sites whose receiver is `repos[0]` or `(await f())` mint a receiver chain where they previously minted none. EMISSION CHANGE: more sites carry `@reference.receiver-chain`; no existing chain changed shape. Only go and kotlin drifted of 15 — the two whose fixture corpora contain such receivers. Prior c1f0cc9058ab11b7cd6fc8b440deb6db2b2f530f2eb21178923e68a3d0796c4b -> efd5dbf80ffcd3bab2834d1010f6fe2b239dcc5d58229938dea9cff8d0f380f2.",
"_rebaselined_2960_declared_package_fixture": "#2960 adds four Kotlin declared-package import-resolution fixture files. This is fixture-corpus growth only: fixture_count 137 -> 141 and capture_groups_fp 2334 -> 2367; the synthetic capture counts remain 4753/15203, no Kotlin scope-capture query or implementation changed, and package resolution runs after capture. Other language fingerprints matched their baselines in the same CI run. Prior a184f8ff0ae40d246db855b63f7ff26bda3afac03e5f4c76e4593c7e2cefce54 -> f98e7e936afbce0e99588285cfc603bf945fd58c5de45271860509a5d90eb832.",
"capture_groups_small": 5253,
"capture_groups_large": 16803,
@ -292,7 +298,7 @@
"fixture_count": 142,
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Only drift: `users.map { it.name }.forEach { name -> println(name) }` (kotlin-lambda-scopes/App.kt) loses `direct-callee-name|forEach` (member call). capture_groups_fp 2334 (unchanged). Prior a184f8ff0ae40d246db855b63f7ff26bda3afac03e5f4c76e4593c7e2cefce54 -> 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284.",
"_rebaselined_1432_rebase_onto_2960": "#1432 rebase onto main @ aac7515d: the kotlin fingerprint is a COMBINATION of two independent changes, so neither side of the merge conflict was correct on its own and resolving it by picking a side would have committed a fingerprint no run can reproduce. main's #2960 added four declared-package fixture files (fixture_count 137 -> 141, capture_groups_fp 2334 -> 2367); this branch's `_rebaselined_1432_member_call_callee_name` drops `direct-callee-name|forEach` from one member call. Recomputed under both: capture_groups_fp 2367 and fixture_count 141 match main's committed counts EXACTLY (this branch's change is emission-only and moves no count), capture_groups_small/large stay 4753/15203 (the SYNTHETIC scaling source, which neither change touches), scaling 1.003 < 1.5, and the other 14 languages report ok against their committed baselines in the same run - which is the check that the rebase replayed nothing else into the capture stream. Attribution is exact rather than inferred: moving test/fixtures/lang-resolution/kotlin-import-package-evidence aside and re-running returns kotlin to 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284 byte-for-byte with fixture_count back at 137 and capture_groups_fp back at 2334 - this branch's pre-rebase value - so the whole delta is #2960's corpus growth layered on top, with nothing else moving. Prior (this branch, pre-rebase) 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284 and (main) f98e7e936afbce0e99588285cfc603bf945fd58c5de45271860509a5d90eb832 -> 973d702510002dda76166e017c5eca90cae37a139f5a512877b7a1b04ad19dc5.",
"_rebaselined_1432_merge_main_2885": "#1432 merge of main @ 212e007a: the kotlin fingerprint is again a COMBINATION of two independent changes \u2014 main's #2885 JVM property accessors / interface-abstract (4753/15203 -> 5753/18403, capture_groups_fp 2367 -> 2563) and this branch's `_rebaselined_1432_member_call_callee_name` (drops `direct-callee-name|forEach` from one member call). Neither side's value reproduces under the merged tree. Recomputed under both: counts 5753/18403/2563 and fixture_count 141 match main's committed counts EXACTLY (this branch's change is emission-only), scaling 1.061 < 1.5, and csharp / cpp / typescript measure byte-for-byte at this branch's committed values (main did not touch them since the merge-base) while the other 11 languages report ok \u2014 the check that the merge replayed nothing else into the capture stream. Prior (main) aeafc7a87402c933786ef582b7c98683b1822b78fa909e605cb97552867fa0d5 and (this branch) 973d702510002dda76166e017c5eca90cae37a139f5a512877b7a1b04ad19dc5 -> a9d3f0db7547ff47856159debf15d2a6f427efca97a6af27b4004174ed432132.",
"_rebaselined_1432_merge_main_2885": "#1432 merge of main @ 212e007a: the kotlin fingerprint is again a COMBINATION of two independent changes — main's #2885 JVM property accessors / interface-abstract (4753/15203 -> 5753/18403, capture_groups_fp 2367 -> 2563) and this branch's `_rebaselined_1432_member_call_callee_name` (drops `direct-callee-name|forEach` from one member call). Neither side's value reproduces under the merged tree. Recomputed under both: counts 5753/18403/2563 and fixture_count 141 match main's committed counts EXACTLY (this branch's change is emission-only), scaling 1.061 < 1.5, and csharp / cpp / typescript measure byte-for-byte at this branch's committed values (main did not touch them since the merge-base) while the other 11 languages report ok — the check that the merge replayed nothing else into the capture stream. Prior (main) aeafc7a87402c933786ef582b7c98683b1822b78fa909e605cb97552867fa0d5 and (this branch) 973d702510002dda76166e017c5eca90cae37a139f5a512877b7a1b04ad19dc5 -> a9d3f0db7547ff47856159debf15d2a6f427efca97a6af27b4004174ed432132.",
"_rebaselined_3354_callable_alternatives": "#3354 callable alternatives: a callable chosen by a value-selecting source now flows every branch it can yield (`a ?? b`, `a || b`, `a or b`, `c ? a : b`, statement `if`, elvis), and an operator branch (`x == f || g`) stays opaque instead of seeding a qualified name. Verified by running the BASE (merge-base 233ca2849) and HEAD emitters over the SAME HEAD fixture corpus: every added or removed match is an `@callable-flow.*` match on one of those sources, and the pre-existing corpus is byte-identical (all other languages: zero delta). The rest of the drift is corpus growth from this PR's regression fixture, which this bench globs. Corpus growth: kotlin-callable-alternatives/App.kt (+1 file, +71 groups). Emitter delta: +8 / -2: elvis and `if` expression (bare and braced) seeds and copies now name each operand; the 2 removed matches are the whole-expression seed and copy carrying a qualified name of the compound source; the multi-statement `if` stays opaque. capture_groups_fp 2563 -> 2634, fixture_count 141 -> 142; synthetic counts unchanged; scaling 1.17 < 1.5. Prior a9d3f0db7547ff47856159debf15d2a6f427efca97a6af27b4004174ed432132 -> f3fefe058484823761b52df5715f41d65fa4ff0223843bcc86893d7b0a6b3de2.",
"_rebaselined_3486_semantic_rename": "#3486: Compared actual origin/main (191a954b), committed dd3bf and frozen provider source over the same unchanged corpus. 1 existing member-read match gains a receiver-chain tag; removing only that added tag restores the complete main capture set. No group is added or removed. Fixture entries 142; changed entries 1; capture groups 2634 -> 2634. Fingerprint regenerated from the runner's canonicalized capture records. Scaling budgets unchanged."
},
@ -300,6 +306,6 @@
"_lexical_ownership_3532": "#3532: lexical ownership captures and fixture updates; fixture identities are relative to lang-resolution so serialized scope IDs are independent of checkout location. Full capture content remains hashed; scaling_budget is unchanged.",
"fingerprint": "3f41a3c6d9161f95c0c906cc979e6ef853c25b39052c5cab274dba2aabf270ba",
"scaling_budget": 1.5,
"_added": "#3354 review: one TS `handler || w === \"k0\" || \u2026` chain, operand count = entity count (250 -> 800). Guards callable-flow value-alternative expansion (valueAlternatives + per-leaf visibility walks) against per-leaf cost that grows with chain depth: before the fix this measured 7.5-8.0 (super-linear; 8000 operands overflowed the stack), after it 1.11-1.22. Synthetic-only fingerprint (no fixture corpus); identical before and after the fix."
"_added": "#3354 review: one TS `handler || w === \"k0\" || …` chain, operand count = entity count (250 -> 800). Guards callable-flow value-alternative expansion (valueAlternatives + per-leaf visibility walks) against per-leaf cost that grows with chain depth: before the fix this measured 7.5-8.0 (super-linear; 8000 operands overflowed the stack), after it 1.11-1.22. Synthetic-only fingerprint (no fixture corpus); identical before and after the fix."
}
}

View file

@ -224,6 +224,11 @@ export function emitCppScopeCaptures(
}
if (Object.keys(grouped).length === 0) continue;
if (grouped['@import.namespace-alias'] !== undefined) {
recordCppUsingDeclaration(filePath, grouped);
continue;
}
// ── Handle #include statements ──────────────────────────────────
// `@import.statement` is captured directly on the `preproc_include` node.
if (grouped['@import.statement'] !== undefined) {
@ -237,6 +242,19 @@ export function emitCppScopeCaptures(
}
}
const callable = nodeIfType(
nodeMap['@declaration.function'] ?? nodeMap['@declaration.method'],
'function_definition',
);
if (callable?.parent?.type === 'template_declaration') {
const parameters = callable.parent.childForFieldName('parameters');
if (parameters !== null)
grouped['@declaration.type-parameters'] = nodeToCapture(
'@declaration.type-parameters',
parameters,
);
}
// ── Handle using declarations (using namespace / using name) ────
// `@import.using-decl` is captured directly on the `using_declaration` node.
if (grouped['@import.using-decl'] !== undefined) {
@ -606,6 +624,17 @@ export function emitCppScopeCaptures(
grouped['@type-binding.type'] = nodeToCapture('@type-binding.type', fieldNode);
delete grouped['@type-binding.assignment'];
}
} else if (fnNode !== null && isCppQualifiedCallee(fnNode)) {
// auto x = rp::make() / auto x = rp::C() → bind the QUALIFIED
// callee, as `auto x = make()` binds `make` (the constructor rule
// in query.ts matches only a bare identifier callee). Left alone,
// the match bound `x` to the literal type `auto`. The resolver
// types `x` from the class the spelling names, or from the
// return type the callee's definition declares
// (ScopeResolver.splitQualifiedCallee).
grouped['@type-binding.constructor'] = anchor;
grouped['@type-binding.type'] = nodeToCapture('@type-binding.type', fnNode);
delete grouped['@type-binding.assignment'];
}
}
}
@ -613,6 +642,17 @@ export function emitCppScopeCaptures(
}
}
// A prototype's return type (`D makeD();`) is a module-level fact only at
// namespace scope. In a function body the same syntax declares a
// block-scope function — the most vexing parse, `D x();` — and its binding
// would be hoisted to the module and type every undeclared `x` in the file.
// Only the prototype rules anchor `@type-binding.return` on a `declaration`
// (definitions anchor on `function_definition`, in-class prototypes on
// `field_declaration`), and the match carries nothing else, so it is dropped
// whole.
const prototypeReturn = nodeIfType(nodeMap['@type-binding.return'], 'declaration');
if (prototypeReturn !== null && !isCppNamespaceScopeDeclaration(prototypeReturn)) continue;
const parameterNode = nodeMap['@type-binding.parameter'];
const parameterCapture = grouped['@type-binding.parameter'];
if (parameterNode !== undefined && parameterCapture !== undefined) {
@ -655,6 +695,48 @@ export function emitCppScopeCaptures(
return out;
}
/**
* A `qualified_identifier` callee naming a plain function or class — `rp::make`,
* `a::b::make`, `rp::C` — as opposed to a template call (`std::make_shared<T>`,
* typed by its own rule in query.ts), an operator or a destructor.
*/
function isCppQualifiedCallee(node: SyntaxNode): boolean {
if (node.type !== 'qualified_identifier') return false;
let name = node.childForFieldName('name');
while (name !== null && name.type === 'qualified_identifier') {
name = name.childForFieldName('name');
}
return name !== null && name.type === 'identifier';
}
/** Nodes a namespace-scope declaration may sit inside without leaving namespace
* scope: preprocessor conditionals (include guards nest every declaration of a
* guarded header in one), single-declaration `extern "C"`, and a `template`
* prefix. */
const CPP_NAMESPACE_SCOPE_WRAPPERS: ReadonlySet<string> = new Set([
'preproc_if',
'preproc_ifdef',
'preproc_else',
'preproc_elif',
'preproc_elifdef',
'linkage_specification',
'template_declaration',
]);
/**
* Is `node` declared at namespace scope — directly in the translation unit or in
* a namespace / `extern "C" {}` body (`declaration_list`), through any number of
* {@link CPP_NAMESPACE_SCOPE_WRAPPERS}? A function body, class body, friend
* declaration or anything else on the way up answers no.
*/
function isCppNamespaceScopeDeclaration(node: SyntaxNode): boolean {
for (let parent = node.parent; parent !== null; parent = parent.parent) {
if (parent.type === 'translation_unit' || parent.type === 'declaration_list') return true;
if (!CPP_NAMESPACE_SCOPE_WRAPPERS.has(parent.type)) return false;
}
return false;
}
/** Prototype and nested function-pointer parameter names do not bind the body. */
function isCppLexicalParameter(node: SyntaxNode): boolean {
for (let parent = node.parent; parent !== null; parent = parent.parent) {

View file

@ -29,7 +29,8 @@
* declaration transparently.
*/
import type { Callsite, ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared';
import type { Callsite, ParsedFile, ScopeId, SymbolDefinition, TypeRef } from 'gitnexus-shared';
import { isCppNamespaceQualifierShadowed } from './qualified-name-claims.js';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import {
isOverloadAmbiguousAfterNormalization,
@ -182,6 +183,7 @@ interface QualifiedNsMemberIndex {
* for are ever materialized, which is what keeps the deep-chain case linear.
*/
readonly membersByReceiver: Map<string, Map<string, readonly SymbolDefinition[]>>;
readonly ownerScopeByDefId: ReadonlyMap<string, ScopeId>;
}
/** One `Namespace` scope as the qualified-lookup walk sees it. */
@ -271,6 +273,7 @@ interface MemoizedIndex {
* pairs it already found. */
function buildQualifiedNsMemberIndex(parsedFiles: readonly ParsedFile[]): QualifiedNsMemberIndex {
const rootsByReceiver = new Map<string, QualifiedNsNode[]>();
const ownerScopeByDefId = new Map<string, ScopeId>();
for (const parsed of parsedFiles) {
const nodesByScope = new Map<ScopeId, MutableNsNode>();
@ -278,6 +281,8 @@ function buildQualifiedNsMemberIndex(parsedFiles: readonly ParsedFile[]): Qualif
for (const sc of parsed.scopes) {
if (sc.kind !== 'Namespace') continue;
const node: MutableNsNode = { ownMembers: bucketOwnMembers(sc), inlineChildren: [] };
for (const definitions of node.ownMembers.values())
for (const definition of definitions) ownerScopeByDefId.set(definition.nodeId, sc.id);
nodesByScope.set(sc.id, node);
namespaces.push([sc, node]);
}
@ -299,7 +304,7 @@ function buildQualifiedNsMemberIndex(parsedFiles: readonly ParsedFile[]): Qualif
roots.push(node);
}
}
return { rootsByReceiver, membersByReceiver: new Map() };
return { rootsByReceiver, membersByReceiver: new Map(), ownerScopeByDefId };
}
/** Bucket a namespace scope's own callable `ownedDefs` by member simple name.
@ -450,10 +455,50 @@ export function resolveCppQualifiedNamespaceMember(
receiverName: string,
memberName: string,
parsedFiles: readonly ParsedFile[],
_scopes: ScopeResolutionIndexes,
scopes: ScopeResolutionIndexes,
callsite?: Callsite,
callerScope?: ScopeId,
): SymbolDefinition | 'ambiguous' | undefined {
const allHits = qualifiedNsMembers(qualifiedNsMemberIndex(parsedFiles), receiverName, memberName);
const index = qualifiedNsMemberIndex(parsedFiles);
const qualifier = receiverName.replace(/^::/, '').replace(/::/g, '.');
const qualified = receiverName.includes('::');
const receiver = qualifier.slice(qualifier.lastIndexOf('.') + 1);
let allHits = qualifiedNsMembers(index, receiver, memberName);
if (qualified) {
const prefixes: string[] = [];
let cursor = callerScope === undefined ? undefined : scopes.scopeTree.getScope(callerScope);
const visited = new Set<ScopeId>();
while (cursor !== undefined && !visited.has(cursor.id)) {
visited.add(cursor.id);
if (cursor.kind === 'Namespace') {
const definition = findNamespaceDefInScope(cursor);
if (definition !== undefined) prefixes.unshift(simpleNameOf(definition));
}
cursor = cursor.parent === null ? undefined : scopes.scopeTree.getScope(cursor.parent);
}
const keys = new Set([qualifier]);
if (!receiverName.startsWith('::'))
for (let length = prefixes.length; length > 0; length--)
keys.add([...prefixes.slice(0, length), qualifier].join('.'));
allHits = allHits.filter((definition) => {
if (definition.namespacePrefix !== undefined && keys.has(definition.namespacePrefix))
return true;
const transparent: string[] = [];
let id = index.ownerScopeByDefId.get(definition.nodeId);
const seen = new Set<ScopeId>();
while (id !== undefined && !seen.has(id)) {
seen.add(id);
const scope = scopes.scopeTree.getScope(id);
if (scope === undefined) break;
if (scope.kind === 'Namespace' && !isCppInlineNamespaceScope(id)) {
const namespace = findNamespaceDefInScope(scope);
if (namespace !== undefined) transparent.unshift(simpleNameOf(namespace));
}
id = scope.parent ?? undefined;
}
return keys.has(transparent.join('.'));
});
}
if (allHits.length === 0) return undefined;
if (allHits.length === 1) return allHits[0];
@ -476,6 +521,47 @@ export function resolveCppQualifiedNamespaceMember(
return 'ambiguous';
}
/**
* `ScopeResolver.splitQualifiedCallee` for C++: the receiver/member pair the
* qualified-call captures produce for a callee spelling, so
* {@link resolveCppQualifiedNamespaceMember} answers it exactly as it answers
* the call. The full written qualifier is retained
* (`a::b::make()` → `a::b` / `make`) so another outer namespace cannot supply
* the result type. Separators inside template
* arguments are not split points (`ns::Box<a::B>::make` → `ns::Box<a::B>` / `make`,
* which names no namespace and resolves nothing). Unqualified → `undefined`.
*/
export function splitCppQualifiedCallee(
callee: string,
callerScope?: ScopeId,
scopes?: ScopeResolutionIndexes,
position?: TypeRef['lookupPosition'],
): { readonly receiver: string; readonly member: string } | undefined {
if (callerScope !== undefined && scopes !== undefined && !/^\s*::/.test(callee)) {
const root = callee.trim().split('::')[0]?.trim();
if (root !== undefined) {
if (isCppNamespaceQualifierShadowed(root, callerScope, scopes, position)) return undefined;
}
}
const separators: number[] = [];
let angleDepth = 0;
for (let i = 0; i < callee.length; i++) {
const ch = callee[i];
if (ch === '<') angleDepth++;
else if (ch === '>') {
if (angleDepth > 0) angleDepth--;
} else if (angleDepth === 0 && ch === ':' && callee[i + 1] === ':') {
separators.push(i);
i++;
}
}
const last = separators[separators.length - 1];
if (last === undefined) return undefined;
const receiver = callee.slice(0, last).trim();
const member = callee.slice(last + 2).trim();
return receiver.length > 0 && member.length > 0 ? { receiver, member } : undefined;
}
function findNamespaceDefInScope(scope: {
readonly ownedDefs: readonly SymbolDefinition[];
}): SymbolDefinition | undefined {

View file

@ -35,6 +35,9 @@ export function interpretCppImport(captures: CaptureMatch): ParsedImport | null
return { kind: 'wildcard', targetRaw: source, isSystem };
}
/** Placeholder type specifiers: the declared type is deduced, never named. */
const CPP_PLACEHOLDER_TYPES: ReadonlySet<string> = new Set(['auto', 'decltype(auto)']);
/**
* Interpret a C++ type-binding capture into a ParsedTypeBinding.
*
@ -51,6 +54,12 @@ export function interpretCppTypeBinding(captures: CaptureMatch): ParsedTypeBindi
const name = captures['@type-binding.name']?.text;
const type = captures['@type-binding.type']?.text;
if (name === undefined || type === undefined) return null;
// `auto` names no type: the deduced type is whatever the initializer yields,
// which the specific rules (constructor, alias, member access, qualified
// callee) capture directly. What reaches here as `auto` is a leftover —
// `auto n = 5;`, `[](auto v)` — and binding it records the literal type name
// `auto`, which types nothing and hides the variable from every other route.
if (CPP_PLACEHOLDER_TYPES.has(type.trim())) return null;
let source: TypeRef['source'] = 'annotation';
@ -85,17 +94,24 @@ export function interpretCppTypeBinding(captures: CaptureMatch): ParsedTypeBindi
// A member field's type is captured AS WRITTEN, qualifier and all
// (`ns::Repo<User>`), because the query matches the outer
// `qualified_identifier` — one depth-agnostic pattern per declarator shape
// instead of one per qualifier depth. The qualifier is dropped HERE; see the
// "Field type, QUALIFIED" block in query.ts for why the qualified spelling
// resolves to nothing and the tail resolves like the bare one.
// instead of one per qualifier depth.
//
// FIELDS ONLY. `@type-binding.parameter` and `@type-binding.assignment` also
// capture qualified spellings (their patterns use `type: (_)`), and reducing
// THOSE would newly bind every qualified local and parameter in the workspace
// — a far wider change than the member-field miss this closes, and not one
// anything here has measured.
// A NON-GENERIC qualified member (`a::Config cfg;`) keeps its qualifier, like
// a qualified local or parameter: the resolver matches the qualifier against
// the candidate's namespace (`ScopeResolver.resolveQualifiedTypeSpellings`).
// Its tail bound the NEAREST `Config` — inside a `namespace b` that declares
// one, `cfg.m()` reached `b::Config::m`, a confident wrong edge.
//
// A GENERIC qualified member is still reduced to its tail, here, because the
// template-argument match that picks a specialization (`Repo<User>` over the
// primary `Repo<T>`) keys on the bare base name; the qualified route is
// argument-blind and would refuse wherever a specialization exists. See the
// "Field type, QUALIFIED" block in query.ts. That keeps the tail hazard for
// generic members, and only for them.
const effectiveType =
captures['@type-binding.field'] === undefined ? type : cppQualifiedTail(type);
captures['@type-binding.field'] !== undefined && type.includes('<')
? cppQualifiedTail(type)
: type;
// The reduced spelling is also the AS-WRITTEN one, and saying so is load
// bearing. `collectTypeBindings` derives `TypeRef.declaredSpelling` from
// `@type-binding.type` whenever that text differs from `rawTypeName`, and it

View file

@ -0,0 +1,42 @@
import type { Range, ScopeId } from 'gitnexus-shared';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import { bindsTypeParameter, isClassLike } from '../../scope-resolution/scope/walkers.js';
/** Check lexical owners, excluding the finalize channel's hoisted declarations. */
export function isCppNamespaceQualifierShadowed(
name: string,
inScope: ScopeId,
indexes: ScopeResolutionIndexes,
position?: Pick<Range, 'startLine' | 'startCol'>,
): boolean {
if (bindsTypeParameter(inScope, name, indexes)) return true;
const seen = new Set<ScopeId>();
let id: ScopeId | null = inScope;
while (id !== null && !seen.has(id)) {
seen.add(id);
const scope = indexes.scopeTree.getScope(id);
if (scope === undefined) break;
if (
scope.bindings.get(name)?.some((binding) => {
if (
!isClassLike(binding.def.type) &&
binding.def.type !== 'Typedef' &&
binding.def.type !== 'TypeAlias'
)
return false;
const at =
binding.availableFrom ?? binding.declarationRange ?? binding.def.declarationRange;
return (
position === undefined ||
at === undefined ||
scope.kind === 'Class' ||
at.startLine < position.startLine ||
(at.startLine === position.startLine && at.startCol <= position.startCol)
);
})
)
return true;
id = scope.parent;
}
return false;
}

View file

@ -264,6 +264,10 @@ const CPP_SCOPE_QUERY = `
(type_definition
declarator: (type_identifier) @declaration.name) @declaration.typedef
(namespace_alias_definition
name: (namespace_identifier) @import.name
. (_) @import.source) @import.namespace-alias @import.statement
;; ─── Declarations — type alias (using Name = Type) ──────────────────
(alias_declaration
name: (type_identifier) @declaration.name) @declaration.typedef
@ -383,6 +387,17 @@ const CPP_SCOPE_QUERY = `
type: (template_type) @type-binding.type
declarator: (identifier) @type-binding.name) @type-binding.annotation
;; Covers: rp::C x; std::vector<User> users;
;; A QUALIFIED type is a qualified_identifier wrapping the type_identifier or
;; template_type, so the two rules above never matched it and rp::C x; bound
;; nothing — the receiver of x.m() was untyped. Matched on the OUTER node, one
;; pattern per declarator shape like the qualified field rules below (depth- and
;; genericity-agnostic, no alternation). The spelling is kept as written and
;; resolved by the qualifier-aware route (resolveQualifiedTypeSpellings).
(declaration
type: (qualified_identifier) @type-binding.type
declarator: (identifier) @type-binding.name) @type-binding.annotation
;; ─── Type bindings — pointer variable declaration ───────────────────
;; Covers: User* ptr = new User()
(declaration
@ -391,6 +406,13 @@ const CPP_SCOPE_QUERY = `
declarator: (pointer_declarator
declarator: (identifier) @type-binding.name))) @type-binding.annotation
;; Covers: rp::C* ptr = nullptr; (the qualified twin of the rule above)
(declaration
type: (qualified_identifier) @type-binding.type
declarator: (init_declarator
declarator: (pointer_declarator
declarator: (identifier) @type-binding.name))) @type-binding.annotation
;; ─── Type bindings — auto + constructor call ────────────────────────
;; Covers: auto user = User("alice")
;; AST: declaration > placeholder_type_specifier/auto > init_declarator > identifier + call_expression > identifier
@ -414,13 +436,18 @@ const CPP_SCOPE_QUERY = `
;; ─── Type bindings — auto + scoped brace-init (qualified) ───────────
;; Covers: auto client = ns::HttpClient{}
;; AST: compound_literal_expression > qualified_identifier > type_identifier
;; The WHOLE qualified spelling is captured, not its tail: \`ns::HttpClient\` is
;; resolved by the qualifier-aware route (\`resolveQualifiedTypeSpellings\`),
;; while the tail \`HttpClient\` bound whichever same-named class was nearer — a
;; global one through another include, or the enclosing namespace's own
;; (\`auto x = a::Config{}\` inside a \`namespace b\` declaring Config). Matching
;; the outer node is also depth-agnostic (\`a::b::T{}\`).
(declaration
type: (placeholder_type_specifier)
declarator: (init_declarator
declarator: (identifier) @type-binding.name
value: (compound_literal_expression
type: (qualified_identifier
name: (type_identifier) @type-binding.type)))) @type-binding.constructor
type: (qualified_identifier) @type-binding.type))) @type-binding.constructor
;; ─── Type bindings — auto + new expression ──────────────────────────
;; Covers: auto user = new User(name)
@ -488,20 +515,43 @@ const CPP_SCOPE_QUERY = `
;; Covers: User getUser() { ... }
;; AST: function_definition > type_identifier + function_declarator > identifier
(function_definition
type: (type_identifier) @type-binding.type
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (function_declarator
declarator: (identifier) @type-binding.name)) @type-binding.return
;; Return type — out-of-class method: User Class::getUser() { ... }
(function_definition
type: (type_identifier) @type-binding.type
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (function_declarator
declarator: (qualified_identifier
name: (identifier) @type-binding.name))) @type-binding.return
;; Return type — pointer return: User* getUser() { ... }
(function_definition
type: (type_identifier) @type-binding.type
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (pointer_declarator
declarator: (function_declarator
declarator: (identifier) @type-binding.name))) @type-binding.return
;; ─── Type bindings — free-function PROTOTYPE return type ─────────────
;; Covers: D makeD(); D* makeDp(); namespace rp { C make(); }
;; The usual layout is "header declares, .cpp defines", and a .cpp is never
;; included — so the header prototype is the only return-type fact an includer
;; can see. Only definitions carried one, which left \`auto x = makeD(); x.m()\`
;; and \`makeD().m()\` untyped in every file that merely includes the header.
;; Inside a function body the same syntax declares a BLOCK-scope function (the
;; most vexing parse, \`D x();\`), whose binding would be hoisted to the module
;; like any return type; captures.ts keeps only the matches at namespace scope
;; (isCppNamespaceScopeDeclaration). A reference return (\`D& f();\`) has no
;; rule here because such a function is not declared as a Function at all.
(declaration
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (function_declarator
declarator: (identifier) @type-binding.name)) @type-binding.return
;; Prototype pointer return: D* makeDp();
(declaration
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (pointer_declarator
declarator: (function_declarator
declarator: (identifier) @type-binding.name))) @type-binding.return
@ -509,13 +559,13 @@ const CPP_SCOPE_QUERY = `
;; ─── Type bindings — inline method return type ──────────────────────
;; Covers: class Foo { User getUser() { ... } };
(function_definition
type: (type_identifier) @type-binding.type
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (function_declarator
declarator: (field_identifier) @type-binding.name)) @type-binding.return
;; Inline method pointer return type: class Foo { User* lookup(int) { ... } };
(function_definition
type: (type_identifier) @type-binding.type
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (pointer_declarator
declarator: (function_declarator
declarator: (field_identifier) @type-binding.name))) @type-binding.return
@ -524,13 +574,13 @@ const CPP_SCOPE_QUERY = `
;; Covers: class User { User* lookup(int); std::string getName(); };
;; AST: field_declaration > function_declarator > field_identifier
(field_declaration
type: (type_identifier) @type-binding.type
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (function_declarator
declarator: (field_identifier) @type-binding.name)) @type-binding.return
;; Method prototype pointer return type: User* lookup(int id);
(field_declaration
type: (type_identifier) @type-binding.type
type: [(type_identifier) (qualified_identifier)] @type-binding.type
declarator: (pointer_declarator
declarator: (function_declarator
declarator: (field_identifier) @type-binding.name))) @type-binding.return
@ -599,18 +649,16 @@ const CPP_SCOPE_QUERY = `
;; a single node type in the field position, not an alternation — the
;; tree-sitter 0.21 hazard this repo has been bitten by before.
;;
;; The QUALIFIER IS THEN DROPPED, by cppQualifiedTail in interpret.ts, not
;; here — and dropping it was measured rather than assumed. Recording
;; ns::Repo<User> resolves to NOTHING: findClassBindingInScope's dotted-tail
;; fallback splits on "." and C++ writes "::", and resolveClassBindingForName's
;; generic branch then looks up the base ns::Repo, which is not a key either
;; because C++ emits no @declaration.qualified_name and indexes ns::Repo under
;; Repo. Reducing to the tail lands on exactly the path the BARE spelling
;; already takes — one class-like match or decline — so a qualified member field
;; behaves like the bare one instead of like nothing. A tail that names no
;; workspace class (std::string with no "class string" in the repo) binds
;; nothing and emits nothing, which is why this is a miss-closing change rather
;; than an edge-fabricating one.
;; For a GENERIC member the QUALIFIER IS THEN DROPPED, by cppQualifiedTail in
;; interpret.ts, not here. resolveClassBindingForName's template-argument match
;; (the one that prefers a Repo<User> specialization over the primary Repo<T>)
;; looks up the BASE name, and C++ indexes ns::Repo under Repo, so only the tail
;; reaches it. A NON-GENERIC member keeps its qualifier and resolves through the
;; qualifier-aware route (ScopeResolver.resolveQualifiedTypeSpellings): its tail
;; bound the NEAREST same-named class, which inside a namespace declaring its
;; own Config made a::Config cfg; type cfg as that one. A qualified spelling
;; that names no workspace class (std::string with no namespace std in the
;; repo) binds nothing and emits nothing.
;;
;; Like the six above, each requires the declarator to reach the field_identifier
;; DIRECTLY, so a method whose return type is qualified (ns::Thing method();)

View file

@ -1,4 +1,4 @@
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
import type { ParsedFile, SymbolDefinition, TypeRef } from 'gitnexus-shared';
import {
findClassBindingInScope,
findEnclosingClassDef,
@ -44,6 +44,7 @@ import {
clearCppInlineNamespaces,
populateCppInlineNamespaceScopes,
resolveCppQualifiedNamespaceMember,
splitCppQualifiedCallee,
} from './inline-namespaces.js';
import { populateCppRangeBindings } from './range-bindings.js';
import { cppConstraintCompatibility } from './constraint-filter.js';
@ -62,6 +63,7 @@ import {
clearCppUsingDeclarations,
populateCppIncludedUsingClaims,
populateCppUsingBindings,
cppQualifiedTypeLookupSpelling,
} from './using-bindings.js';
/** A pointee worth binding: a bare identifier, not `T**`, `T[]`, `A::B` or a
@ -231,6 +233,26 @@ export const cppScopeResolver: ScopeResolver = {
},
populateNamespaceSiblings: (parsedFiles, indexes) => {
// Name-keyed return facts cannot choose between free overloads whose
// return types differ. Keep their declaration identities, drop the guess.
for (const parsed of parsedFiles) {
for (const scope of parsed.scopes) {
if (scope.kind !== 'Module' && scope.kind !== 'Namespace') continue;
const returns = new Map<string, Set<string | undefined>>();
for (const definition of scope.ownedDefs) {
if (definition.type !== 'Function' || definition.qualifiedName === undefined) continue;
const name = definition.qualifiedName.slice(
definition.qualifiedName.lastIndexOf('.') + 1,
);
const types = returns.get(name) ?? new Set<string | undefined>();
types.add(definition.returnType);
returns.set(name, types);
}
for (const [name, types] of returns)
if (types.size > 1 && scope.typeBindings.get(name)?.source === 'return-annotation')
(scope.typeBindings as Map<string, TypeRef>).delete(name);
}
}
populateCppUsingBindings(parsedFiles, indexes);
populateCppIncludedOverloads(indexes);
},
@ -321,12 +343,18 @@ export const cppScopeResolver: ScopeResolver = {
conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
// Range-for element type inference: for (auto& user : users) → bind user to User
populateRangeBindings: populateCppRangeBindings,
// C++ method return-type bindings need to be visible from module scope
// for cross-file propagation and compound-receiver chain resolution.
// cppBindingScopeFor hoists @type-binding.return to Module scope.
hoistTypeBindingsToModule: true,
// Return bindings stay with their declaring class or namespace. An absent
// method fact must not borrow a same-name free function's return type.
// Enable receiver-bound explicit-`this` fallback only for C++.
resolveThisViaEnclosingClass: true,
// `rp::C x`, `void f(a::Config c)`: the capture keeps the qualifier, and
// neither `#include` (no namespace handle) nor the index (`rp::C` keyed as
// `C`) lets the shared receiver lookups read it. Resolve it the way a
// qualified base class already resolves — qualifier matched against the
// candidate's namespace, refuse on a tie — rather than by its tail, which
// binds the nearer same-named class the qualifier was written to exclude.
resolveQualifiedTypeSpellings: true,
qualifiedTypeLookupSpelling: cppQualifiedTypeLookupSpelling,
resolveReceiverMember: resolveCppReceiverMember,
// The `isFileLocalDef` hook on the global free-call fallback names
// file-local linkage historically, but semantically gates "logically
@ -396,9 +424,38 @@ export const cppScopeResolver: ScopeResolver = {
resolveQualifiedReceiverMember: (
receiverName,
memberName,
_callerScope,
callerScope,
scopes,
parsedFiles,
callsite,
) => resolveCppQualifiedNamespaceMember(receiverName, memberName, parsedFiles, scopes, callsite),
) =>
resolveCppQualifiedNamespaceMember(
receiverName,
memberName,
parsedFiles,
scopes,
callsite,
callerScope,
),
// `auto x = rp::make(); x.m()` / `rp::make().m()`: map the callee text back
// onto the receiver/member pair the call capture used, so the hook above
// answers the factory's definition the way it answered the call.
splitQualifiedCallee: (callee, callerScope, scopes, position) => {
// The namespace-member resolver does not bind aliases. Decline instead
// of treating an alias as an unrelated namespace with the same name.
if (
callerScope !== undefined &&
cppQualifiedTypeLookupSpelling(
{
rawName: callee,
declaredAtScope: callerScope,
source: 'assignment-inferred',
lookupPosition: position,
},
scopes,
) !== undefined
)
return undefined;
return splitCppQualifiedCallee(callee, callerScope, scopes, position);
},
};

View file

@ -21,17 +21,15 @@ export function cppBindingScopeFor(
innermost: Scope,
tree: ScopeTree,
): ScopeId | null {
// Hoist return-type bindings to Module scope so:
// 1. propagateImportedReturnTypes can mirror them across files
// 2. compound-receiver can find method return types via hoistTypeBindingsToModule
// Keep each callable's return fact with its declaring owner. In particular,
// a free prototype must not overwrite a same-name method's return type.
if (decl['@type-binding.return'] !== undefined) {
let cur: Scope | undefined = innermost;
while (cur !== undefined && cur.kind !== 'Module') {
const parentId: ScopeId | null = cur.parent ?? null;
if (parentId === null) break;
cur = tree.getScope(parentId);
let current: Scope | undefined = innermost;
while (current !== undefined) {
if (current.kind === 'Class' || current.kind === 'Namespace' || current.kind === 'Module')
return current.id;
current = current.parent === null ? undefined : tree.getScope(current.parent);
}
if (cur !== undefined && cur.kind === 'Module') return cur.id;
}
return null; // default auto-hoist for other bindings
}

View file

@ -8,14 +8,18 @@ import type {
Range,
Scope,
ScopeId,
TypeRef,
} from 'gitnexus-shared';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import { findClassBindingInScope, isClassLike } from '../../scope-resolution/scope/walkers.js';
import { isCppInlineNamespaceScope } from './inline-namespaces.js';
import { isCppNamespaceQualifierShadowed } from './qualified-name-claims.js';
/** Namespace lookup facts, deliberately distinct from preprocessor file imports. */
export interface CppUsingDeclaration {
readonly namespace: string;
readonly name?: string;
readonly alias?: string;
readonly range: Range;
}
@ -35,7 +39,12 @@ export function recordCppUsingDeclaration(filePath: string, capture: CaptureMatc
if (namespace === undefined || range === undefined) return;
const name = capture['@import.name']?.text;
const declarations = declarationsByFile.get(filePath) ?? [];
declarations.push({ namespace, range, ...(name === undefined ? {} : { name }) });
const alias = capture['@import.namespace-alias'] === undefined ? undefined : name;
declarations.push({
namespace,
range,
...(alias === undefined ? (name === undefined ? {} : { name }) : { alias }),
});
declarationsByFile.set(filePath, declarations);
}
@ -92,7 +101,7 @@ export function attachCppUsingClaims(captures: CaptureMatch[], filePath: string)
byOwner.set(owner.match, claims);
};
for (const declaration of collectCppUsingDeclarations(filePath)) {
if (declaration.name === undefined) continue;
if (declaration.alias !== undefined || declaration.name === undefined) continue;
add({
name: declaration.name,
range: declaration.range,
@ -276,6 +285,7 @@ export function populateCppIncludedUsingClaims(
for (const parsed of parsedFiles) {
const declarations = usingDeclarations(parsed).filter(
(declaration) =>
declaration.alias === undefined &&
declaration.name !== undefined &&
owningScope(parsed, declaration.range)?.id === parsed.moduleScope,
);
@ -371,16 +381,166 @@ export function populateCppIncludedUsingClaims(
}
}
interface NamespaceAliasTarget {
readonly target: string;
readonly availableFrom?: Pick<Range, 'startLine' | 'startCol'>;
}
const namespaceAliases = new WeakMap<
ScopeResolutionIndexes,
Map<ScopeId, Map<string, NamespaceAliasTarget>>
>();
const classScopeIds = new WeakMap<ScopeResolutionIndexes, ReadonlyMap<string, ScopeId>>();
function visibleNamespaceAlias(
name: string,
inScope: ScopeId,
indexes: ScopeResolutionIndexes,
position?: Pick<Range, 'startLine' | 'startCol'>,
): string | undefined {
let id: ScopeId | null = inScope;
const seen = new Set<ScopeId>();
while (id !== null && !seen.has(id)) {
seen.add(id);
const alias = namespaceAliases.get(indexes)?.get(id)?.get(name);
if (
alias !== undefined &&
(position === undefined ||
alias.availableFrom === undefined ||
compare(alias.availableFrom, position) <= 0)
)
return alias.target;
const scope = indexes.scopeTree.getScope(id);
if (
scope?.bindings
.get(name)
?.some(
(binding) =>
binding.def.type === 'Namespace' &&
(position === undefined ||
binding.declarationRange === undefined ||
compare(binding.declarationRange, position) <= 0),
)
)
break;
id = scope?.parent ?? null;
}
return undefined;
}
/** Expand only aliases visible from the binding's declaration scope. */
export function cppQualifiedTypeLookupSpelling(
ref: TypeRef,
indexes?: ScopeResolutionIndexes,
): string | null | undefined {
if (indexes === undefined) return undefined;
const separator = ref.rawName.indexOf('::');
if (separator <= 0) return undefined;
const head = ref.rawName.slice(0, separator).trim();
if (
isCppNamespaceQualifierShadowed(
head,
ref.declaredAtScope,
indexes,
ref.lookupPosition ?? ref.bindingRange,
)
) {
// A concrete class can still own the written nested type. A type alias or
// dependent qualifier with no class identity cannot borrow a namespace.
return findClassBindingInScope(ref.declaredAtScope, head, indexes, undefined, {
uniqueQualifiedNameFallback: false,
position: ref.lookupPosition ?? ref.bindingRange,
purpose: 'type',
}) === undefined
? null
: undefined;
}
const alias = visibleNamespaceAlias(
head,
ref.declaredAtScope,
indexes,
ref.lookupPosition ?? ref.bindingRange,
);
const expanded =
alias === undefined ? undefined : `${alias}::${ref.rawName.slice(separator + 2)}`;
const spelling = expanded ?? ref.rawName;
const last = spelling.lastIndexOf('::');
const qualifier = spelling.slice(0, last).replace(/::/g, '.');
const tail = spelling.slice(last + 2);
const qualifiers = new Set([qualifier]);
if (alias === undefined && !spelling.startsWith('::')) {
const enclosing: string[] = [];
let scope = indexes.scopeTree.getScope(ref.declaredAtScope);
const seen = new Set<ScopeId>();
while (scope !== undefined && !seen.has(scope.id)) {
seen.add(scope.id);
if (scope.kind === 'Namespace') {
const name = scope.ownedDefs.find((def) => def.type === 'Namespace')?.qualifiedName;
if (name !== undefined) enclosing.unshift(name.slice(name.lastIndexOf('.') + 1));
}
scope = scope.parent === null ? undefined : indexes.scopeTree.getScope(scope.parent);
}
for (let length = enclosing.length; length > 0; length--)
qualifiers.add([...enclosing.slice(0, length), qualifier].join('.'));
}
const matches = new Map<string, string>();
for (const candidateId of indexes.qualifiedNames.get(tail)) {
const classScopeId = classScopeIds.get(indexes)?.get(candidateId);
if (classScopeId === undefined) continue;
let parent = indexes.scopeTree.getScope(classScopeId)?.parent ?? null;
const visited = new Set<ScopeId>();
const full: string[] = [];
const transparent: string[] = [];
let hasInline = false;
while (parent !== null && !visited.has(parent)) {
visited.add(parent);
const scope = indexes.scopeTree.getScope(parent);
if (scope === undefined) break;
if (scope.kind === 'Namespace') {
const name = scope.ownedDefs.find((def) => def.type === 'Namespace')?.qualifiedName;
if (name !== undefined) {
const segment = name.slice(name.lastIndexOf('.') + 1);
full.unshift(segment);
if (isCppInlineNamespaceScope(scope.id)) hasInline = true;
else transparent.unshift(segment);
}
}
parent = scope.parent;
}
if (hasInline && qualifiers.has(transparent.join('.')))
matches.set(candidateId, [...full, tail].join('::'));
}
return matches.size === 1 ? matches.values().next().value : expanded;
}
/** Materialize namespace identities once, then augment only the using's owner. */
export function populateCppUsingBindings(
parsedFiles: readonly ParsedFile[],
indexes: ScopeResolutionIndexes,
): void {
const classes = new Map<string, ScopeId>();
for (const parsed of parsedFiles) {
for (const scope of parsed.scopes) {
if (scope.kind !== 'Class') continue;
const definition = scope.ownedDefs.find((def) => isClassLike(def.type));
if (definition !== undefined) classes.set(definition.nodeId, scope.id);
}
}
classScopeIds.set(indexes, classes);
if (!parsedFiles.some((parsed) => usingDeclarations(parsed).length > 0)) return;
const namespaceLookup = createNamespaceLookup(parsedFiles, (id) =>
indexes.scopeTree.getScope(id),
);
const { namespacePath } = namespaceLookup;
const aliases = new Map<ScopeId, Map<string, NamespaceAliasTarget>>();
namespaceAliases.set(indexes, aliases);
const installAlias = (scope: ScopeId, name: string, value: NamespaceAliasTarget): void => {
let bucket = aliases.get(scope);
if (bucket === undefined) {
bucket = new Map();
aliases.set(scope, bucket);
}
bucket.set(name, value);
};
const byFile = new Map(parsedFiles.map((parsed) => [parsed.filePath, parsed]));
const visibleFiles = createIncludeClosure((filePath) => {
const parsed = byFile.get(filePath);
@ -389,6 +549,38 @@ export function populateCppUsingBindings(
edge.targetFile !== null && edge.linkStatus !== 'unresolved' ? [edge.targetFile] : [],
);
});
const consumersByHeader = new Map<
string,
Map<ScopeId, Pick<Range, 'startLine' | 'startCol'> | undefined>
>();
const aliasFiles = new Set(
parsedFiles
.filter((parsed) =>
usingDeclarations(parsed).some((declaration) => declaration.alias !== undefined),
)
.map((parsed) => parsed.filePath),
);
if (aliasFiles.size > 0) {
for (const consumer of parsedFiles) {
for (const edge of indexes.imports.get(consumer.moduleScope) ?? []) {
if (edge.targetFile === null || edge.linkStatus === 'unresolved') continue;
for (const header of visibleFiles(edge.targetFile)) {
if (header === consumer.filePath || !aliasFiles.has(header)) continue;
const consumers = consumersByHeader.get(header) ?? new Map();
const previous = consumers.get(consumer.moduleScope);
const availableFrom = edge.atRange === undefined ? undefined : end(edge.atRange);
if (
!consumers.has(consumer.moduleScope) ||
availableFrom === undefined ||
(previous !== undefined && compare(availableFrom, previous) < 0)
) {
consumers.set(consumer.moduleScope, availableFrom);
}
consumersByHeader.set(header, consumers);
}
}
}
}
const augmentations = indexes.bindingAugmentations as Map<ScopeId, Map<string, BindingRef[]>>;
const append = (
scope: Scope,
@ -412,22 +604,72 @@ export function populateCppUsingBindings(
}
bindings.set(name, bucket);
};
const resolveDeclaration = (parsed: ParsedFile, declaration: CppUsingDeclaration) => {
const owner = owningScope(parsed, declaration.range);
if (owner === undefined) return undefined;
const parts = declaration.namespace.split('::').map((part) => part.trim());
const head = parts[0];
const alias =
head === undefined || head.length === 0
? undefined
: visibleNamespaceAlias(head, owner.id, indexes, declaration.range);
const resolved = resolveUsingMembers(
alias === undefined
? declaration
: { ...declaration, namespace: `::${[alias, ...parts.slice(1)].join('::')}` },
owner,
parsed.filePath,
visibleFiles(parsed.filePath),
namespaceLookup,
);
return resolved === undefined ? undefined : { owner, ...resolved };
};
// Includes are leaves-first. Retry only unresolved aliases inside cycles;
// successful aliases have canonical targets and never need expanding again.
const ordered = new Map<string, ParsedFile>();
for (const scc of indexes.sccs)
for (const file of scc.files) {
const parsed = byFile.get(file);
if (parsed !== undefined) ordered.set(file, parsed);
}
for (const parsed of parsedFiles)
if (!ordered.has(parsed.filePath)) ordered.set(parsed.filePath, parsed);
let pending = [...ordered.values()].flatMap((parsed) =>
usingDeclarations(parsed)
.filter((declaration) => declaration.alias !== undefined)
.map((declaration) => ({ parsed, declaration })),
);
while (pending.length > 0) {
const retry: typeof pending = [];
for (const item of pending) {
const resolved = resolveDeclaration(item.parsed, item.declaration);
const name = item.declaration.alias;
if (resolved === undefined || name === undefined) {
retry.push(item);
continue;
}
installAlias(resolved.owner.id, name, {
target: resolved.target,
availableFrom: item.declaration.range,
});
if (resolved.owner.id === item.parsed.moduleScope)
for (const [consumer, availableFrom] of consumersByHeader.get(item.parsed.filePath) ?? [])
installAlias(consumer, name, {
target: resolved.target,
availableFrom,
});
}
if (retry.length === pending.length) break;
pending = retry;
}
for (const parsed of parsedFiles) {
const facts = usingDeclarations(parsed);
if (facts.length === 0) continue;
const visible = visibleFiles(parsed.filePath);
for (const declaration of facts) {
const owner = owningScope(parsed, declaration.range);
if (owner === undefined) continue;
const resolved = resolveUsingMembers(
declaration,
owner,
parsed.filePath,
visible,
namespaceLookup,
);
if (declaration.alias !== undefined) continue;
const resolved = resolveDeclaration(parsed, declaration);
if (resolved === undefined) continue;
const { target, members } = resolved;
const { owner, target, members } = resolved;
for (const [name, refs] of members) {
if (declaration.name !== undefined) {
append(owner, name, refs, declaration);

View file

@ -1146,6 +1146,30 @@ export interface ScopeResolver {
*/
readonly resolveNamespaceChains?: boolean;
/**
* When true, a receiver's declared type written as a QUALIFIED name
* (`rp::C x`, `void f(a::Config c)`, a factory returning `rp::C`) that binds
* nothing through the scope chain is resolved through the same precise route
* a qualified inheritance base and a qualified type reference take
* (`resolveQualifiedInheritanceBase`): the qualifier must match the
* candidate's namespace, and a tie resolves nothing.
*
* The shared receiver lookups read a qualifier only as a `.` namespace-import
* handle, so a language whose qualifier is a namespace PATH with no import
* handle behind it typed none of these receivers. File includes
* binds no handle and its index keys `rp::C` under `C`. The alternative —
* reducing the spelling to its tail at capture — binds the nearest
* same-named class, which is the class the qualifier was written to exclude.
*
* Per-language rather than universal because PHP also keeps qualified
* (`\`-separated) declared types and resolves them through its own import
* channel; it was not measured under this route. Off ⇒ every receiver
* lookup is unchanged. It runs only after every existing lookup missed, and
* after the template-argument match, so it can add an edge but never
* retarget one.
*/
readonly resolveQualifiedTypeSpellings?: boolean;
/**
* How this language spells a construction expression, so the compound
* receiver resolver can type an INLINE constructor receiver — the
@ -1504,6 +1528,36 @@ export interface ScopeResolver {
callsite?: Callsite,
) => SymbolDefinition | 'ambiguous' | undefined;
/**
* Split a qualified CALLEE as written (`rp::make`, `a::b::make`) into the
* receiver/member pair this language's qualified call captures produce for
* the same spelling, or `undefined` when the callee is not qualified.
*
* A factory call types a receiver in two shapes — bound
* (`auto x = rp::make(); x.m()`, whose binding records the callee text) and
* inline (`rp::make().m()`) — and both reach the compound receiver resolver
* as callee TEXT, while the call itself was resolved from its captured
* receiver and member. This hook maps the text back onto that pair so the
* resolver can ask `resolveQualifiedReceiverMember` for the callee's
* definition and read the return type that definition declares (by
* definition identity, never by name). Splitting is the language's job: the
* separator and which segment counts as the receiver are grammar facts; a
* provider may capture `a::b::make` as `b` / `make`. The caller scope and
* indexes let it reject dependent or shadowed qualifiers before splitting.
* The optional position keeps later local declarations from hiding the
* factory expression; bound receivers pass their producer lookup position.
*
* Consulted only for a free-call head whose name binds no callable, and only
* when `resolveQualifiedReceiverMember` is implemented too. It is asked
* without call-site arity, so an `'ambiguous'` answer types nothing.
*/
readonly splitQualifiedCallee?: (
callee: string,
callerScope?: ScopeId,
scopes?: ScopeResolutionIndexes,
position?: TypeRef['lookupPosition'],
) => { readonly receiver: string; readonly member: string } | undefined;
/**
* Every receiver spelling under which a namespace import's target is
* reachable, and the file each spelling names (#2826). Returning
@ -1805,10 +1859,15 @@ export interface ScopeResolver {
* `rawName` has many readers, and the compound resolver reads a dotted one as
* a `value.member` alias path. Only receiver typing needs the distinction.
*
* Return a lookup-ready class name (no decoration, no type arguments). Leave
* undefined for languages whose `rawName` keeps its qualifier.
* Return a module-visible class spelling (no decoration or type arguments).
* Providers may also expand a namespace alias using the binding's declaration
* scope and the supplied indexes. Leave undefined when no expansion is needed.
* Return null to refuse the written qualified lookup and its fallback.
*/
readonly qualifiedTypeLookupSpelling?: (typeRef: TypeRef) => string | undefined;
readonly qualifiedTypeLookupSpelling?: (
typeRef: TypeRef,
scopes?: ScopeResolutionIndexes,
) => string | null | undefined;
/**
* Whether the compound-receiver resolver should strip C-style cast

View file

@ -31,14 +31,14 @@ import {
} from '../../utils/template-arguments.js';
import type { DecodedReceiverChain } from '../../utils/receiver-chain-codec.js';
import { decodeReceiverChain } from '../../utils/receiver-chain-codec.js';
import type { QualifiedCalleeResolver } from './imported-return-types.js';
import { namespaceCallReturnType, qualifiedCalleeReturnType } from './imported-return-types.js';
import { namespaceCallReturnType, producerTypeRef } from './imported-return-types.js';
import type { ClassBindingLookup, DecorationStripper } from '../scope/walkers.js';
import {
findClassBindingInScope,
lookupNameClaim,
hasExplicitNameClaim,
resolveClassBindingForName,
resolveQualifiedClassSpelling,
findEnclosingClassDef,
findExportedDef,
findExportedDefByName,
@ -187,16 +187,36 @@ interface ResolveCompoundReceiverOptions {
* by the shared lookup's other callers — see the contract's own note on why
* this is opt-in rather than global. */
readonly stripTypePreservingDecoration?: DecorationStripper;
/** `ScopeResolver.resolveQualifiedCallee`, bound to the pass. Types an
* undotted call no scope binds (`rp::make()`) from the return annotation of
* the callable the provider resolves it to. Absent ⇒ exactly as before. */
readonly resolveQualifiedCallee?: QualifiedCalleeResolver;
/** `ScopeResolver.resolveQualifiedTypeSpellings`: a qualified declared or
* return type (`rp::C`) that binds nothing is resolved through the precise
* qualified route. Absent ⇒ both lookups are exactly as before. */
readonly qualifiedTypeSpellings?: boolean;
/** The definition a QUALIFIED callee (`rp::make`) names, through
* `ScopeResolver.splitQualifiedCallee` + `resolveQualifiedReceiverMember`
* — the same answer the qualified call itself got. Consulted for a
* free-call head whose name binds no callable; absent ⇒ such a head types
* nothing, exactly as before. */
readonly resolveQualifiedCallee?: (
callee: string,
inScope: ScopeId,
position?: TypeRef['lookupPosition'],
) => SymbolDefinition | undefined;
/** `ScopeResolver.qualifiedTypeLookupSpelling`, tried before `rawName` by
* every {@link classOfDeclaredType} caller (member, head and fold lookups).
* Absent ⇒ `rawName` alone, exactly as before. */
readonly qualifiedTypeLookupSpelling?: ScopeResolver['qualifiedTypeLookupSpelling'];
}
/** The options the two declared-type lookups below read. */
type TypeLookupOptions = Pick<
ResolveCompoundReceiverOptions,
| 'recordReceiverType'
| 'onNameFallback'
| 'qualifiedTypeSpellings'
| 'qualifiedTypeLookupSpelling'
| 'namespaceMemberVisible'
>;
/** Is this hop the language's construction selector applied to the class
* itself — `Factory.new` — rather than an ordinary member named `new` on a
* value? Both call sites ask the identical question, so it is asked in one
@ -469,6 +489,7 @@ function classOfDeclaredType(
position: typeRef.lookupPosition,
purpose: typeRef.lookupPurpose ?? 'type',
onNameFallback: options.onNameFallback,
qualifiedTypeSpellings: options.qualifiedTypeSpellings,
};
// The report keeps the spelling it always had, whichever lookup answers: it
// is read only for type ARGUMENTS (#2912), and the qualified spelling has none.
@ -524,11 +545,12 @@ export function classOfQualifiedTypeSpelling(
): SymbolDefinition | null | undefined {
// Runs for every declared-type lookup in every language, so nothing is
// allocated until the provider has actually returned a qualified spelling.
const qualified = qualifiedTypeLookupSpelling?.(typeRef);
const qualified = qualifiedTypeLookupSpelling?.(typeRef, scopes);
if (qualified === null) return null;
if (qualified === undefined || qualified === typeRef.rawName) return undefined;
const importScope = moduleScopeIdOf(typeRef.declaredAtScope, scopes) ?? typeRef.declaredAtScope;
return (
findClassBindingInScope(importScope, qualified, scopes, undefined, {
resolveClassBindingForName(importScope, qualified, scopes, undefined, {
...lookup,
namespaceMemberVisible,
uniqueQualifiedNameFallback: false,
@ -570,22 +592,72 @@ function noteReceiverType(
* arguments intact. The spelling is built only once the lookup has actually
* found a class, because it is discarded otherwise — and every fold hop
* through a call reaches this, generic or not.
*
* A qualified return type (`rp::C make();`) binds nothing through the bare
* lookup for the same reason a qualified declared type does not, so it takes
* the same opt-in qualified route as a last resort (see
* `ScopeResolver.resolveQualifiedTypeSpellings`).
*/
function classOfReturnType(
retType: TypeRef,
scopes: ScopeResolutionIndexes,
record: ReceiverTypeRecorder | undefined,
onNameFallback?: () => void,
options: TypeLookupOptions,
): SymbolDefinition | undefined {
const def = findClassBindingInScope(retType.declaredAtScope, retType.rawName, scopes, undefined, {
position: retType.lookupPosition,
purpose: retType.lookupPurpose ?? 'type',
onNameFallback,
});
const qualified = classOfQualifiedTypeSpelling(
retType,
scopes,
options.qualifiedTypeLookupSpelling,
{
position: retType.lookupPosition,
purpose: retType.lookupPurpose ?? 'type',
onNameFallback: options.onNameFallback,
qualifiedTypeSpellings: options.qualifiedTypeSpellings,
},
options.namespaceMemberVisible,
);
const def =
qualified === undefined
? (findClassBindingInScope(retType.declaredAtScope, retType.rawName, scopes, undefined, {
position: retType.lookupPosition,
purpose: retType.lookupPurpose ?? 'type',
onNameFallback: options.onNameFallback,
}) ??
(options.qualifiedTypeSpellings === true
? resolveQualifiedClassSpelling(retType.declaredAtScope, retType.rawName, scopes)
: undefined))
: (qualified ?? undefined);
const record = options.recordReceiverType;
if (def === undefined || record === undefined) return def;
return noteReceiverType(record, erasedTypeApplication(retType) ?? retType.rawName, def);
}
/**
* The return type a free-call callee declares for ITSELF — read by definition
* identity (`producerTypeRef`), the way the chain follow types the bound form
* `auto x = make(); x.m()` — for when the name read finds no return binding
* under the callee's name. That happens whenever the callee arrived by a
* channel nothing mirrors a return binding through (a namespace directive), so
* `make().m()` dropped while the bound form beside it resolved.
*
* Only for a callee the name actually BINDS at the call. `findExportedDefByName`
* falls back to the first workspace callable of that name, and reading a
* return type off that guess typed a receiver from an unrelated module's
* same-named factory — a confident wrong edge where the name read declined.
*/
function boundCalleeReturnType(
fnDef: SymbolDefinition,
name: string,
inScope: ScopeId,
scopes: ScopeResolutionIndexes,
position: TypeRef['lookupPosition'],
): TypeRef | undefined {
const claim = lookupNameClaim(inScope, name, scopes, { position, purpose: 'value' });
const identities = new Set(claim.bindings.map((binding) => binding.def.nodeId));
return identities.size === 1 && identities.has(fnDef.nodeId)
? producerTypeRef(fnDef.nodeId, scopes)
: undefined;
}
function typeOfMemberOnClass(
owner: SymbolDefinition,
memberName: string,
@ -1100,37 +1172,28 @@ export function resolveCompoundReceiverClass(
purpose: 'value',
});
if (fnDef !== undefined) {
const retType = findReceiverTypeBinding(inScope, fnExpr, scopes, {
position: options.lookupPosition,
});
const retType =
findReceiverTypeBinding(inScope, fnExpr, scopes, {
position: options.lookupPosition,
}) ?? boundCalleeReturnType(fnDef, fnExpr, inScope, scopes, options.lookupPosition);
const viaReturn =
retType === undefined
? undefined
: classOfReturnType(
retType,
scopes,
options.recordReceiverType,
options.onNameFallback,
);
retType === undefined ? undefined : classOfReturnType(retType, scopes, options);
if (viaReturn !== undefined) return viaReturn;
} else {
// A callee path only the provider can read (`rp::make()`, #3498).
const returned = qualifiedCalleeReturnType(
// A qualified callee (`rp::make`, from `rp::make().m()` or the binding
// `auto x = rp::make()`) binds no name; its definition comes from the
// language's qualified-member lookup — the qualifier is the evidence —
// and its return type from that definition's own declaration.
const qualifiedDef = options.resolveQualifiedCallee?.(
fnExpr,
inScope,
scopes,
options.resolveQualifiedCallee,
options.lookupPosition,
);
const viaQualified =
returned === undefined
? undefined
: classOfReturnType(
returned,
scopes,
options.recordReceiverType,
options.onNameFallback,
);
if (viaQualified !== undefined) return viaQualified;
const retType =
qualifiedDef === undefined ? undefined : producerTypeRef(qualifiedDef.nodeId, scopes);
const viaReturn =
retType === undefined ? undefined : classOfReturnType(retType, scopes, options);
if (viaReturn !== undefined) return viaReturn;
}
// Inline construction — `Service(db).m()` / `new Service(db).m()`.
// The constructed value IS the receiver, so there is no binding to
@ -1163,14 +1226,7 @@ export function resolveCompoundReceiverClass(
// named import of that function (#3498).
const returned = namespaceCallReturnType(fnExpr, inScope, scopes, options.lookupPosition);
const viaReturn =
returned === undefined
? undefined
: classOfReturnType(
returned,
scopes,
options.recordReceiverType,
options.onNameFallback,
);
returned === undefined ? undefined : classOfReturnType(returned, scopes, options);
if (viaReturn !== undefined) return viaReturn;
return resolveConstructionExpressionClass(fnExpr, inScope, scopes, index, options);
}
@ -1272,7 +1328,7 @@ export function resolveCompoundReceiverClass(
}
if (retType === undefined) return undefined;
return classOfReturnType(retType, scopes, options.recordReceiverType, options.onNameFallback);
return classOfReturnType(retType, scopes, options);
}
// Mixed dotted + call chain: `obj.field.method().field.method()…`.

View file

@ -328,6 +328,12 @@ export function followChainPostFinalize(
if (next === undefined || claim.bindings.some((binding) => binding.origin !== 'local')) {
const candidates = new Map<string, TypeRef>();
for (const binding of claim.bindings) {
// A member exposed through an import bucket must not replace a
// return fact anchored on the module's own free function.
if (binding.def.type === 'Method' && next?.source === 'return-annotation') {
const owner = scopes.scopeTree.getScope(next.declaredAtScope);
if (owner?.kind === 'Module' && isModuleOwnReturn(next, owner, current.rawName)) continue;
}
const producer = producerTypeRef(binding.def.nodeId, scopes);
if (producer !== undefined) candidates.set(binding.def.nodeId, producer);
}

View file

@ -217,6 +217,8 @@ type ReceiverBoundProviderSubset = Pick<
| 'importsBindAtLexicalScope'
| 'resolveNamespaceChains'
| 'resolveQualifiedCallee'
| 'resolveQualifiedTypeSpellings'
| 'splitQualifiedCallee'
>;
/** A bare, undecorated identifier and nothing else — see {@link isBareTypeName}. */
@ -281,6 +283,10 @@ export function classifyReceiverOrigin(
* external. Same hook the three receiver-chain lookups in
* `compound-receiver.ts` already receive. */
readonly stripTypePreservingDecoration?: DecorationStripper;
/** `ScopeResolver.resolveQualifiedTypeSpellings`, so a base declared with a
* qualified type (`rp::C x`) is classified by the same lookup that typed
* (or failed to type) it. */
readonly qualifiedTypeSpellings?: boolean;
/** `LanguageProvider.isBuiltInName`, threaded through the pass options the
* same way `emitFreeCallFallback` receives it. THE only source of positive
* external evidence available here; languages that declare no built-in set
@ -304,8 +310,9 @@ export function classifyReceiverOrigin(
// arguments, so an in-program generic base (`Box<String> b; b.open()`)
// resolves instead of being mislabelled and dropped from the hedge.
if (
resolveClassBindingForName(binding.declaredAtScope, binding.rawName, scopes, strip) !==
undefined
resolveClassBindingForName(binding.declaredAtScope, binding.rawName, scopes, strip, {
qualifiedTypeSpellings: options.qualifiedTypeSpellings,
}) !== undefined
) {
return 'in-program';
}
@ -507,7 +514,7 @@ export function emitReceiverBoundCalls(
return def !== undefined && CALLABLE_TYPES.has(def.type);
});
};
const resolveQualifiedCallee = bindQualifiedCalleeResolver(
const resolveProviderQualifiedCallee = bindQualifiedCalleeResolver(
provider.resolveQualifiedCallee,
scopes,
index,
@ -515,7 +522,7 @@ export function emitReceiverBoundCalls(
options.allFilePaths,
);
const bindsQualifiedCallable = (typeRef: TypeRef): boolean => {
const definition = resolveQualifiedCallee?.(typeRef.rawName, typeRef.declaredAtScope);
const definition = resolveProviderQualifiedCallee?.(typeRef.rawName, typeRef.declaredAtScope);
return definition !== undefined && CALLABLE_TYPES.has(definition.type);
};
// A class-like def NESTED in `owner` (`A.Item` inside `A`): its qualified
@ -654,6 +661,32 @@ export function emitReceiverBoundCalls(
return undefined;
return uniqueClassAcross(files, tail);
};
// A qualified callee as the free-call head of a compound receiver
// (`rp::make().m()`, or `x` bound to `rp::make`): the definition the
// qualified CALL resolves to, so its declared return type can type the
// receiver. Asked without call-site arity, so an ambiguous answer is none.
const splitQualifiedCallee = provider.splitQualifiedCallee;
const resolveQualifiedReceiverMember = provider.resolveQualifiedReceiverMember;
const resolveQualifiedCallee =
resolveProviderQualifiedCallee ??
(splitQualifiedCallee === undefined || resolveQualifiedReceiverMember === undefined
? undefined
: (
callee: string,
inScope: ScopeId,
position?: TypeRef['lookupPosition'],
): SymbolDefinition | undefined => {
const parts = splitQualifiedCallee(callee, inScope, scopes, position);
if (parts === undefined) return undefined;
const def = resolveQualifiedReceiverMember(
parts.receiver,
parts.member,
inScope,
scopes,
parsedFiles,
);
return def === 'ambiguous' ? undefined : def;
});
const compoundOpts = {
fieldFallback,
elementTypeOf: provider.elementTypeOf,
@ -667,6 +700,7 @@ export function emitReceiverBoundCalls(
stripTypePreservingDecoration: provider.stripTypePreservingDecoration,
qualifiedTypeLookupSpelling: provider.qualifiedTypeLookupSpelling,
resolveThisViaEnclosingClass: provider.resolveThisViaEnclosingClass,
qualifiedTypeSpellings: provider.resolveQualifiedTypeSpellings === true,
resolveQualifiedCallee,
};
// Loop-invariant: both hooks come off the pass arguments, so the options bag
@ -674,6 +708,7 @@ export function emitReceiverBoundCalls(
const receiverOriginOpts = {
stripTypePreservingDecoration: provider.stripTypePreservingDecoration,
isBuiltInName: options.isBuiltInName,
qualifiedTypeSpellings: provider.resolveQualifiedTypeSpellings === true,
};
// Maps class-like graph ids back to ALL scope definitions that resolved to
@ -2382,6 +2417,7 @@ export function emitReceiverBoundCalls(
position: typeRef.lookupPosition,
purpose: typeRef.lookupPurpose ?? 'type',
onNameFallback,
qualifiedTypeSpellings: provider.resolveQualifiedTypeSpellings === true,
};
const qualifiedOwner = classOfQualifiedTypeSpelling(
typeRef,
@ -3110,7 +3146,11 @@ export function emitReceiverBoundCalls(
fieldDeclaredType.name,
scopes,
provider.stripTypePreservingDecoration,
{ onNameFallback, purpose: fieldDeclaredType.purpose },
{
onNameFallback,
purpose: fieldDeclaredType.purpose,
qualifiedTypeSpellings: provider.resolveQualifiedTypeSpellings === true,
},
);
if (receiverClass !== undefined) {
const chain = [

View file

@ -946,6 +946,13 @@ export type ClassBindingLookup = IndexedNameLookupOptions & {
* pass it is unchanged.
*/
readonly namespaceMemberVisible?: (def: SymbolDefinition) => boolean;
/**
* `ScopeResolver.resolveQualifiedTypeSpellings`. When true,
* {@link resolveClassBindingForName} answers a QUALIFIED spelling that bound
* nothing (`rp::C`, `a::Config`) through {@link resolveQualifiedClassSpelling}.
* Read by nothing else; absent (the default) leaves every lookup unchanged.
*/
readonly qualifiedTypeSpellings?: boolean;
};
export function findClassBindingInScope(
@ -1384,7 +1391,8 @@ export function resolveClassBindingForName(
//
// If a future case genuinely needs it, it must be gated on
// `bindsTypeParameter` and land with a test that fails without it.
if (!rawClassName.includes('<')) return undefined;
if (!rawClassName.includes('<'))
return qualifiedSpellingFallback(scopeId, rawClassName, scopes, lookup);
const baseName = stripTemplateArguments(rawClassName).replace(/\s+/g, '');
if (baseName.length === 0) return undefined;
@ -1425,8 +1433,61 @@ export function resolveClassBindingForName(
// ── Base-name route ────────────────────────────────────────────────────────
// Nothing matched the arguments as written, so what is left to find is the
// declaration written against its PARAMETERS — the one instantiation-agnostic
// declaration the erasure is entitled to reach.
return resolveErasedBaseName(scopeId, baseName, scopes, lexical);
// declaration the erasure is entitled to reach. A qualified base
// (`ns::Repo<User>`) is tried only after both, so a specialization the
// argument match can reach is never bypassed by the argument-blind
// qualified route.
return (
resolveErasedBaseName(scopeId, baseName, scopes, lexical) ??
qualifiedSpellingFallback(scopeId, rawClassName, scopes, lookup)
);
}
/** {@link resolveQualifiedClassSpelling}, for the providers that opted in. */
function qualifiedSpellingFallback(
scopeId: string,
rawClassName: string,
scopes: ScopeResolutionIndexes,
lookup: ClassBindingLookup | undefined,
): SymbolDefinition | undefined {
return lookup?.qualifiedTypeSpellings === true
? resolveQualifiedClassSpelling(scopeId, rawClassName, scopes)
: undefined;
}
/**
* The class a QUALIFIED declared-type spelling names — `rp::C`, `a::Config`,
* `rp::Box<int>` — through {@link resolveQualifiedInheritanceBase}, the route a
* qualified inheritance base and a qualified type reference already take.
*
* The receiver-typing lookups above understand a qualifier only as a `.`
* namespace-import handle, and a language whose qualifier is a namespace path
* (an include binds no handle, and the index keys `rp::C` under `C`) never
* has one, so a qualified local, parameter or factory result typed nothing.
* Reducing the spelling to its tail is NOT the fix: the tail binds the nearest
* same-named class, which is exactly the class the qualifier was written to
* rule out (`a::Config` inside a `namespace b` that declares its own Config).
* The qualified route matches the qualifier against each candidate's
* namespace and refuses on a tie, so it binds the named class or nothing.
*
* Declines for an unqualified spelling and for a qualifier that is a type
* parameter in scope (`T::type` is a dependent name, not a namespace path).
*/
export function resolveQualifiedClassSpelling(
scopeId: string,
rawClassName: string,
scopes: ScopeResolutionIndexes,
): SymbolDefinition | undefined {
const normalized = stripTrailingTypeArguments(normalizeQualifiedName(rawClassName));
const dot = normalized.indexOf('.');
if (dot <= 0) return undefined;
if (bindsTypeParameter(scopeId, normalized.slice(0, dot), scopes)) return undefined;
return resolveQualifiedInheritanceBase(
scopeId,
rawClassName,
scopes,
findEnclosingClassDef(scopeId, scopes),
);
}
/**

View file

@ -0,0 +1,29 @@
namespace impl { struct AliasC { int m(); }; }
namespace alias = impl;
struct Holder { alias::AliasC c; int alias_field() { return c.m(); } };
int alias_brace() { auto c = alias::AliasC{}; return c.m(); }
struct A { int m(); };
struct B { int m(); };
struct Factory { A make(); };
B make();
int prototype_collision() { Factory factory; return factory.make().m(); }
namespace one { A overloaded(); }
namespace two { B overloaded(int); }
using namespace one;
using namespace two;
int using_overload() { return overloaded(1).m(); }
namespace rp { A produce(); }
template<class rp> struct Client {
int dependent_bound() { auto x = rp::produce(); return x.m(); }
int dependent_inline() { return rp::produce().m(); }
};
namespace outer { namespace nested { A produce(); } }
template<class outer> struct DeepClient {
int dependent_outer() { return outer::nested::produce().m(); }
};
int qualified_positive() { return rp::produce().m(); }
struct AliasShadow {
struct alias { struct AliasC { int m(); }; };
int alias_shadowed() { auto value = alias::AliasC{}; return value.m(); }
};

View file

@ -0,0 +1,10 @@
#ifndef QRT_A_H
#define QRT_A_H
namespace a {
struct Config {
int m() { return 6; }
};
} // namespace a
#endif

View file

@ -0,0 +1,58 @@
#include "mod.h"
#include "other.h"
#include "a.h"
// Qualified type spellings: `rp::C` must reach rp::C, never the global C that
// is visible here through other.h.
int declared_init() { rp::C x = rp::make(); return x.m(); }
int param_ns(rp::C x) { return x.m(); }
int param_ns_ptr(rp::C* x) { return x->m(); }
int param_template(rp::Box<int> b) { return b.m(); }
int bare_global() { C x; return x.m(); }
// Qualified declarations with no initializer.
int declared_ns() { rp::C x; return x.m(); }
int declared_ns_ptr() { rp::C* x = nullptr; return x->m(); }
// Qualified brace-init and member field.
int brace_ns() { auto x = rp::C{}; return x.m(); }
namespace b {
struct Config {
int m() { return 7; }
};
int param_qualified(a::Config x) { return x.m(); }
int param_local(Config x) { return x.m(); }
int brace_qualified() { auto x = a::Config{}; return x.m(); }
struct Holder {
a::Config cfg;
int field_qualified() { return cfg.m(); }
};
} // namespace b
// Factories declared only by header prototypes (definitions live in mod.cpp,
// which nothing includes).
int auto_plain() { auto x = makeD(); return x.m(); }
int inline_plain() { return makeD().m(); }
int auto_ptr() { auto p = makeDp(); return p->m(); }
int global_factory() { auto y = make(); return y.m(); }
// A block-scope `E y();` declares a local function; it must not leak a
// file-level `y => E` binding to other functions.
int vexing() { E y(); return 0; }
int uses_vexing_name() { return y.m(); }
// Qualified factory callee.
int auto_ns() { auto x = rp::make(); return x.m(); }
int inline_ns() { return rp::make().m(); }
int ctor_ns() { auto x = rp::C(); return x.m(); }
// An unknown qualified factory types nothing, and must not let the inner `x`
// fall through to the outer `D x`.
int shadowed_auto() {
D x;
{
auto x = rp::unknown();
return x.m();
}
}

View file

@ -0,0 +1,8 @@
#include "mod.h"
namespace rp {
C make() { return C(); }
} // namespace rp
D makeD() { return D(); }
D* makeDp() { return nullptr; }

View file

@ -0,0 +1,34 @@
#ifndef QRT_MOD_H
#define QRT_MOD_H
namespace rp {
struct C {
int m() { return 1; }
};
template <typename T>
struct Box {
int m() { return 4; }
};
C make();
// Shares its tail with the global Widget below, in the same header.
struct Widget {
int m() { return 6; }
};
Widget makeWidget();
} // namespace rp
struct Widget {
int n() { return 7; }
};
struct D {
int m() { return 3; }
};
struct E {
int m() { return 5; }
};
D makeD();
D* makeDp();
#endif

View file

@ -0,0 +1,9 @@
#ifndef QRT_OTHER_H
#define QRT_OTHER_H
struct C {
int m() { return 2; }
};
C make();
#endif

View file

@ -0,0 +1,14 @@
#include "mod.h"
// `make` is reached through the using-directive; its return type is the
// namespace prototype's `C`, which must mean rp::C even though a global C
// exists elsewhere in the workspace.
int using_auto() { using namespace rp; auto x = make(); return x.m(); }
// The same call as an INLINE receiver: typed from the callee's definition, as
// the bound form above is, not from a return binding found by name.
int using_inline() { using namespace rp; return make().m(); }
// The prototype's `Widget` is rp::Widget, not the global Widget declared in the
// same header.
int using_same_file() { using namespace rp; auto x = makeWidget(); return x.m(); }
int using_inline_same_file() { using namespace rp; return makeWidget().m(); }

View file

@ -0,0 +1,71 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
describe('C++ header alias activation and factory ownership', () => {
let root: string;
let result: Awaited<ReturnType<typeof runPipelineFromRepo>>;
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cpp-header-alias-order-'));
await fs.writeFile(
path.join(root, 'provider.h'),
'namespace pkg { struct Cross { int m(); }; Cross make(); } namespace imported = pkg;',
);
await fs.writeFile(
path.join(root, 'main.cpp'),
`
struct Decoy { int m(); };
void before_include() { imported::Cross x; x.m(); }
#include "provider.h"
void after_include() { imported::Cross x; x.m(); }
void bound_factory() { auto x = pkg::make(); x.m(); }
void inline_factory() { pkg::make().m(); }
void typed_value(int pkg) { pkg::Cross x; x.m(); }
void auto_value(auto pkg) { pkg::Cross x; x.m(); }
void alias_type() { using pkg = Decoy; pkg::Cross x; x.m(); }
void later_alias() { pkg::Cross x; using pkg = Decoy; x.m(); }
void later_alias_bound() { auto x = pkg::make(); using pkg = Decoy; x.m(); }
void later_alias_inline() { pkg::make().m(); using pkg = Decoy; }
void later_class_bound() { auto x = pkg::make(); struct pkg {}; x.m(); }
void later_class_inline() { pkg::make().m(); struct pkg {}; }
void prior_alias_bound() { using pkg = Decoy; auto x = pkg::make(); x.m(); }
void prior_alias_inline() { using pkg = Decoy; pkg::make().m(); }
template<class pkg> void dependent_type() { typename pkg::Cross x; x.m(); }
`,
);
result = await runPipelineFromRepo(root, () => {}, {});
}, 120000);
afterAll(async () => {
if (root) await fs.rm(root, { recursive: true, force: true });
});
it.each([
['before_include', []],
['after_include', ['Method:provider.h:Cross.m#0']],
['bound_factory', ['Method:provider.h:Cross.m#0']],
['inline_factory', ['Method:provider.h:Cross.m#0']],
['typed_value', ['Method:provider.h:Cross.m#0']],
['auto_value', ['Method:provider.h:Cross.m#0']],
['alias_type', []],
['later_alias', ['Method:provider.h:Cross.m#0']],
['later_alias_bound', ['Method:provider.h:Cross.m#0']],
['later_alias_inline', ['Method:provider.h:Cross.m#0']],
['later_class_bound', ['Method:provider.h:Cross.m#0']],
['later_class_inline', ['Method:provider.h:Cross.m#0']],
['prior_alias_bound', []],
['prior_alias_inline', []],
['dependent_type', []],
] as const)('%s uses only the visible producer', (caller, targets) => {
expect(
[...result.graph.iterRelationships()]
.filter(
(edge) =>
edge.type === 'CALLS' &&
result.graph.getNode(edge.sourceId)?.properties.name === caller &&
result.graph.getNode(edge.targetId)?.properties.name === 'm',
)
.map((edge) => edge.targetId),
).toEqual(targets);
});
});

View file

@ -0,0 +1,55 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
describe('C++ namespace spelling and header order', () => {
let root: string;
let result: Awaited<ReturnType<typeof runPipelineFromRepo>>;
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cpp-namespace-order-'));
const sources: Record<string, string> = {
'z_provider.h': 'namespace one { struct FirstProduct { int m(); }; } namespace first = one;',
'a_consumer.h':
'#include "z_provider.h"\nnamespace second = first; struct FirstHolder { second::FirstProduct c; int first_use() { return c.m(); } };',
'a_provider.h':
'namespace two { struct SecondProduct { int m(); }; } namespace before = two;',
'z_consumer.h':
'#include "a_provider.h"\nnamespace after = before; struct SecondHolder { after::SecondProduct c; int second_use() { return c.m(); } };',
'main.cpp': `
#include "a_consumer.h"
#include "z_consumer.h"
namespace outer {
namespace lib { inline namespace v1 { struct RelativeProduct { int m(); }; } }
struct Decoy { int m(); };
void relative_brace() { auto x = lib::RelativeProduct{}; x.m(); }
struct RelativeHolder { lib::RelativeProduct field; void relative_field() { field.m(); } };
}
`,
};
for (const [name, source] of Object.entries(sources))
await fs.writeFile(path.join(root, name), source);
result = await runPipelineFromRepo(root, () => {}, {});
}, 120000);
afterAll(async () => {
if (root) await fs.rm(root, { recursive: true, force: true });
});
it.each([
['relative_brace', 'Method:main.cpp:RelativeProduct.m#0'],
['relative_field', 'Method:main.cpp:RelativeProduct.m#0'],
['first_use', 'Method:z_provider.h:FirstProduct.m#0'],
['second_use', 'Method:a_provider.h:SecondProduct.m#0'],
])('%s resolves its own namespace', (caller, target) => {
expect(
[...result.graph.iterRelationships()]
.filter(
(edge) =>
edge.type === 'CALLS' &&
result.graph.getNode(edge.sourceId)?.properties.name === caller &&
result.graph.getNode(edge.targetId)?.properties.name === 'm',
)
.map((edge) => edge.targetId),
).toEqual([target]);
});
});

View file

@ -0,0 +1,55 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
describe('C++ chained namespace alias receiver types', () => {
let root: string;
let result: Awaited<ReturnType<typeof runPipelineFromRepo>>;
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cpp-alias-chain-'));
await fs.writeFile(
path.join(root, 'main.cpp'),
`
namespace impl { struct C { int m(); }; }
namespace first = impl;
namespace second = first;
struct Decoy { int m(); };
struct Holder { second::C c; int field_call() { return c.m(); } };
int brace_call() { auto c = second::C{}; return c.m(); }
second::C make();
namespace factory { second::C make(); }
int alias_return_inline() { return make().m(); }
int alias_return_bound() { auto c = make(); return c.m(); }
int qualified_alias_return_inline() { return factory::make().m(); }
int qualified_alias_return_bound() { auto c = factory::make(); return c.m(); }
int consumer_shadow() { using second = Decoy; return make().m(); }
`,
);
result = await runPipelineFromRepo(root, () => {}, {});
}, 120000);
afterAll(async () => {
if (root) await fs.rm(root, { recursive: true, force: true });
});
it.each([
'field_call',
'brace_call',
'alias_return_inline',
'alias_return_bound',
'qualified_alias_return_inline',
'qualified_alias_return_bound',
'consumer_shadow',
])('%s resolves the canonical alias target', (caller) => {
expect(
[...result.graph.iterRelationships()]
.filter(
(edge) =>
edge.type === 'CALLS' &&
result.graph.getNode(edge.sourceId)?.properties.name === caller &&
result.graph.getNode(edge.targetId)?.properties.name === 'm',
)
.map((edge) => edge.targetId),
).toEqual(['Method:main.cpp:C.m#0']);
});
});

View file

@ -0,0 +1,71 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
describe('C++ qualified callee collisions', () => {
let root: string;
let result: Awaited<ReturnType<typeof runPipelineFromRepo>>;
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cpp-callee-collisions-'));
await fs.writeFile(
path.join(root, 'lib.h'),
`
struct FreeProduct { void m(); };
struct MemberProduct { void m(); };
struct Factory { MemberProduct get() { return MemberProduct(); } };
inline FreeProduct get() { return FreeProduct(); }
namespace real { struct AliasProduct { void m(); }; AliasProduct make(); }
namespace handle { struct WrongProduct { void m(); }; WrongProduct make(); }
`,
);
await fs.writeFile(
path.join(root, 'main.cpp'),
`
#include "lib.h"
void bound() { auto x = get(); x.m(); }
void inlined() { get().m(); }
namespace client {
namespace handle = real;
void alias_inline() { handle::make().m(); }
void alias_bound() { auto x = handle::make(); x.m(); }
}
void actual_namespace() { handle::make().m(); }
`,
);
result = await runPipelineFromRepo(root, () => {}, {});
}, 120000);
afterAll(async () => {
if (root) await fs.rm(root, { recursive: true, force: true });
});
function owners(caller: string): string[] {
const edges = [...result.graph.iterRelationships()];
return edges
.filter(
(edge) =>
edge.type === 'CALLS' &&
result.graph.getNode(edge.sourceId)?.properties.name === caller &&
result.graph.getNode(edge.targetId)?.properties.name === 'm',
)
.map((edge) => {
const owner = edges.find(
(candidate) => candidate.type === 'HAS_METHOD' && candidate.targetId === edge.targetId,
);
return String(owner && result.graph.getNode(owner.sourceId)?.properties.name);
})
.sort();
}
it.each(['bound', 'inlined'])('%s keeps the free function return type', (caller) => {
expect(owners(caller)).toEqual(['FreeProduct']);
});
it.each(['alias_inline', 'alias_bound'])(
'%s cannot borrow a shadowed namespace factory',
(caller) => {
expect(owners(caller)).toEqual([]);
},
);
it('keeps a real namespace factory with the same spelling', () => {
expect(owners('actual_namespace')).toEqual(['WrongProduct']);
});
});

View file

@ -0,0 +1,99 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
describe('C++ qualified factory review boundaries', () => {
let root: string;
let result: Awaited<ReturnType<typeof runPipelineFromRepo>>;
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cpp-factory-review-'));
await fs.writeFile(
path.join(root, 'products.h'),
`
namespace lib { inline namespace v1 { struct InlineProduct { int m(); }; } }
struct NoArg { int m(); };
struct OneArg { int m(); };
NoArg create();
OneArg create(int);
namespace app { namespace json { struct ParsedDoc { void dump(); }; ParsedDoc parse(); } }
namespace outer { struct Factory { static NoArg make(); }; }
namespace Factory { OneArg make(); }
`,
);
await fs.writeFile(
path.join(root, 'main.cpp'),
`
#include "products.h"
void inline_brace() { auto x = lib::InlineProduct{}; x.m(); }
struct Holder { lib::InlineProduct field; void field_call() { field.m(); } };
void overloaded_bound() { auto x = create(); x.m(); }
void overloaded_inline() { create().m(); }
void overloaded_argument_bound() { auto x = create(1); x.m(); }
void overloaded_argument_inline() { create(1).m(); }
void outer_mismatch_bound() { auto x = ext::json::parse(); x.dump(); }
void outer_mismatch_inline() { ext::json::parse().dump(); }
void outer_match_bound() { auto x = app::json::parse(); x.dump(); }
void outer_match_inline() { app::json::parse().dump(); }
namespace app { void relative_match() { json::parse().dump(); } }
void class_qualifier_collision() { outer::Factory::make().m(); }
void actual_factory_namespace() { Factory::make().m(); }
`,
);
result = await runPipelineFromRepo(root, () => {}, {});
}, 120000);
afterAll(async () => {
if (root) await fs.rm(root, { recursive: true, force: true });
});
function targets(caller: string): string[] {
const edges = [...result.graph.iterRelationships()];
return edges
.filter(
(edge) =>
edge.type === 'CALLS' &&
result.graph.getNode(edge.sourceId)?.properties.name === caller &&
['m', 'dump'].includes(String(result.graph.getNode(edge.targetId)?.properties.name)),
)
.map((edge) => {
const owner = edges.find(
(candidate) => candidate.type === 'HAS_METHOD' && candidate.targetId === edge.targetId,
);
return String(owner && result.graph.getNode(owner.sourceId)?.properties.name);
})
.sort();
}
it.each(['inline_brace', 'field_call'])('%s sees through an inline namespace', (caller) => {
expect(targets(caller)).toEqual(['InlineProduct']);
});
it.each([
'overloaded_bound',
'overloaded_inline',
'overloaded_argument_bound',
'overloaded_argument_inline',
])('%s leaves distinct prototype returns unresolved without overload selection', (caller) => {
// These calls are valid and distinguishable by arity in C++. Receiver
// typing currently lacks the selected overload's return identity.
expect(targets(caller)).toEqual([]);
});
it.each(['outer_mismatch_bound', 'outer_mismatch_inline'])(
'%s cannot borrow a different outer namespace',
(caller) => {
expect(targets(caller)).toEqual([]);
},
);
it.each(['outer_match_bound', 'outer_match_inline', 'relative_match'])(
'%s keeps a factory in the written namespace',
(caller) => {
expect(targets(caller)).toEqual(['ParsedDoc']);
},
);
it('leaves an unsupported class-qualified factory unresolved without borrowing a namespace', () => {
// This is valid C++ and should eventually target NoArg::m. The current
// namespace factory hook cannot resolve a class owner.
expect(targets('class_qualifier_collision')).toEqual([]);
});
it('keeps the real namespace that shares the class name', () => {
expect(targets('actual_factory_namespace')).toEqual(['OneArg']);
});
});

View file

@ -0,0 +1,61 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
describe('C++ qualified receiver boundaries', () => {
let root: string;
let result: Awaited<ReturnType<typeof runPipelineFromRepo>>;
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cpp-receiver-boundaries-'));
await fs.copyFile(
new URL('../fixtures/cpp-qualified-receiver-boundaries.cpp', import.meta.url),
path.join(root, 'main.cpp'),
);
result = await runPipelineFromRepo(root, () => {}, {});
}, 120000);
afterAll(async () => {
if (root) await fs.rm(root, { recursive: true, force: true });
});
function receiverOwners(caller: string, ids = false): string[] {
const edges = [...result.graph.iterRelationships()];
return edges
.filter(
(e) =>
e.type === 'CALLS' &&
result.graph.getNode(e.sourceId)?.properties.name === caller &&
result.graph.getNode(e.targetId)?.properties.name === 'm',
)
.map((e) => {
const owner = edges.find((x) => x.type === 'HAS_METHOD' && x.targetId === e.targetId);
return ids
? String(owner?.sourceId)
: String(owner && result.graph.getNode(owner.sourceId)?.properties.name);
})
.sort();
}
it.each(['alias_field', 'alias_brace'])('%s resolves the namespace alias', (caller) => {
expect(receiverOwners(caller)).toEqual(['AliasC']);
});
it('keeps the method return separate from a same-name free prototype', () => {
expect(receiverOwners('prototype_collision')).toEqual(['A']);
});
it('does not select a competing overload without call evidence', () => {
expect(receiverOwners('using_overload')).toEqual([]);
});
it.each(['dependent_bound', 'dependent_inline', 'dependent_outer'])(
'%s does not borrow a namespace factory',
(caller) => {
expect(receiverOwners(caller)).toEqual([]);
},
);
it('alias_shadowed cannot borrow the namespace alias through a local class', () => {
const namespaceTarget = receiverOwners('alias_field', true);
expect(namespaceTarget).toHaveLength(1);
expect(receiverOwners('alias_shadowed', true)).not.toContain(namespaceTarget[0]);
});
it('keeps the concrete namespace factory', () => {
expect(receiverOwners('qualified_positive')).toEqual(['A']);
});
});

View file

@ -0,0 +1,84 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
describe('C++ lexical qualified factory boundaries', () => {
let root: string;
let result: Awaited<ReturnType<typeof runPipelineFromRepo>>;
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cpp-template-review-'));
await fs.writeFile(
path.join(root, 'original.h'),
`
namespace original { struct C { int m(); }; }
namespace alias = original;
`,
);
await fs.writeFile(
path.join(root, 'inner.h'),
`
#include "original.h"
namespace inner {
namespace alias { struct C { int m(); }; }
int nearer(alias::C c) { return c.m(); }
}
`,
);
await fs.writeFile(
path.join(root, 'main.cpp'),
`
#include "inner.h"
namespace actual { struct Product { int m(); }; }
struct Wrong { int m(); };
struct Factory { actual::Product make(); };
struct AutoFactory { auto make() { return actual::Product{}; } };
Wrong make();
int qualified_method() { Factory f; return f.make().m(); }
int auto_method() { AutoFactory f; return f.make().m(); }
struct A { int m(); };
struct B { int m(); };
namespace rp { A produce(); }
struct Actual { static B produce(); };
template<class rp> int function_template() { return rp::produce().m(); }
template<class rp> A* pointer_template() { rp::produce().m(); return nullptr; }
struct Owner { template<class rp> int member_template() { return rp::produce().m(); } };
int concrete() { return rp::produce().m(); }
`,
);
result = await runPipelineFromRepo(root, () => {}, {});
}, 120000);
afterAll(async () => {
if (root) await fs.rm(root, { recursive: true, force: true });
});
function targets(caller: string): string[] {
return [...result.graph.iterRelationships()]
.filter(
(edge) =>
edge.type === 'CALLS' &&
result.graph.getNode(edge.sourceId)?.properties.name === caller &&
result.graph.getNode(edge.targetId)?.properties.name === 'm',
)
.map((edge) => edge.targetId)
.sort();
}
it('lets a nearer namespace hide an outer alias', () => {
expect(targets('nearer')).toEqual(['Method:inner.h:C.m#0']);
});
it('does not borrow a free prototype for a qualified method return', () => {
expect(targets('qualified_method')).toEqual(['Method:main.cpp:Product.m#0']);
});
it.each(['function_template', 'member_template', 'pointer_template'])(
'%s retains dependent lookup',
(caller) => {
expect(targets(caller)).toEqual([]);
},
);
it('does not borrow a free return annotation for an undeduced auto method', () => {
expect(targets('auto_method')).toEqual([]);
});
it('keeps the nondependent namespace factory', () => {
expect(targets('concrete')).toEqual(['Method:main.cpp:A.m#0']);
});
});

View file

@ -0,0 +1,99 @@
/**
* C++ receiver typing through qualified type spellings, header prototypes and
* qualified factory callees.
*
* Every row names a caller in the `cpp-qualified-receiver-typing` fixture and
* the exact `m` targets it must reach, as `<file>:<Owner>.m`. The fixture puts
* a global `C` (other.h) beside `rp::C` (mod.h), and `a::Config` beside a
* `b::Config` declared in the caller's own namespace, so a lookup that drops
* the qualifier and lands on the nearer same-tail class fails a row instead of
* passing it.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js';
const RP_C = 'mod.h:C.m';
const GLOBAL_C = 'other.h:C.m';
const D = 'mod.h:D.m';
describe('C++ qualified receiver typing', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-qualified-receiver-typing'),
() => {},
);
}, 60000);
/** `<file>:<Owner>.m` for every CALLS edge from `caller` to a method `m`. */
const memberTargets = (caller: string): string[] =>
getRelationships(result, 'CALLS')
.filter((edge) => edge.source === caller && edge.target === 'm')
.map((edge) => `${edge.targetFilePath}:${edge.rel.targetId.split(':')[2]?.split('#')[0]}`)
.sort();
it.each<[string, string[]]>([
// A qualified spelling reaches the namespace member it names, never the
// global `C` that other.h makes visible under the same tail.
['declared_init', [RP_C]],
['param_ns', [RP_C]],
['param_ns_ptr', [RP_C]],
['param_template', ['mod.h:Box.m']],
['bare_global', [GLOBAL_C]],
// `a::Config` written inside `namespace b`, which declares its own Config.
['param_qualified', ['a.h:Config.m']],
['param_local', ['callers.cpp:Config.m']],
])('qualified type spelling: %s -> %j', (caller, expected) => {
expect(memberTargets(caller)).toEqual(expected);
});
it.each<[string, string[]]>([
['declared_ns', [RP_C]],
['declared_ns_ptr', [RP_C]],
])('qualified declaration without initializer: %s -> %j', (caller, expected) => {
expect(memberTargets(caller)).toEqual(expected);
});
it.each<[string, string[]]>([
['brace_ns', [RP_C]],
['brace_qualified', ['a.h:Config.m']],
['field_qualified', ['a.h:Config.m']],
])('qualified brace-init and member field: %s -> %j', (caller, expected) => {
expect(memberTargets(caller)).toEqual(expected);
});
it.each<[string, string[]]>([
['auto_plain', [D]],
['inline_plain', [D]],
['auto_ptr', [D]],
['global_factory', [GLOBAL_C]],
// A namespace prototype's return type is looked up inside its namespace,
// never from the file's module scope where the global Widget sits.
['using_auto', [RP_C]],
['using_same_file', ['mod.h:Widget.m']],
// The block-scope `E y();` in `vexing` must not hoist a file-level `y`.
['uses_vexing_name', []],
])('header prototype return type: %s -> %j', (caller, expected) => {
expect(memberTargets(caller)).toEqual(expected);
});
it.each<[string, string[]]>([
['using_inline', [RP_C]],
['using_inline_same_file', ['mod.h:Widget.m']],
])('inline call receiver typed by definition identity: %s -> %j', (caller, expected) => {
expect(memberTargets(caller)).toEqual(expected);
});
it.each<[string, string[]]>([
['auto_ns', [RP_C]],
['inline_ns', [RP_C]],
['ctor_ns', [RP_C]],
// An unknown qualified factory types nothing, and the inner `x` must not
// fall through to the outer `D x`.
['shadowed_auto', []],
])('qualified factory callee: %s -> %j', (caller, expected) => {
expect(memberTargets(caller)).toEqual(expected);
});
});

View file

@ -615,6 +615,42 @@ describe('C++ pointer parameter — declared spelling', () => {
});
});
/**
* Which name a declaration binds, and to what type — for the shapes where the
* written declaration names no type directly (`auto`) or names it in a way only
* one syntactic position may claim (a function prototype).
*/
describe('C++ deduced and prototype type bindings', () => {
function rawTypeOf(src: string, name: string): string | undefined {
const parsed = extractParsedFile(cppProvider, src, 'test.cpp');
const refs = (parsed?.scopes ?? []).flatMap((scope) => {
const ref = scope.typeBindings.get(name);
return ref === undefined ? [] : [ref.rawName];
});
return refs[0];
}
it.each<[string, string, string | undefined]>([
// `auto` names no type; a leftover placeholder binds nothing.
['void f() { auto n = 5; }', 'n', undefined],
['auto g = [](auto v) { return v; };', 'v', undefined],
// A qualified callee is bound as written, like a bare one.
['void f() { auto x = make(); }', 'x', 'make'],
['void f() { auto x = rp::make(); }', 'x', 'rp::make'],
['void f() { auto x = a::b::make(); }', 'x', 'a::b::make'],
// A template factory keeps its dedicated rule (the template argument).
['void f() { auto x = std::make_shared<Dog>(); }', 'x', 'Dog'],
// A prototype records its return type at namespace scope, through an
// include guard, and never in a function body (the most vexing parse).
['D makeD();', 'makeD', 'D'],
['D* makeP();', 'makeP', 'D'],
['#ifndef G\n#define G\nnamespace rp { C make(); }\n#endif\n', 'make', 'C'],
['void f() { E y(); }', 'y', undefined],
])('%s binds %s to %s', (src, name, expected) => {
expect(rawTypeOf(src, name)).toBe(expected);
});
});
describe('written operator positions', () => {
it('keeps distinct synthetic operator references on their actual tokens', () => {
const source = 'int combine(A a, A b, A c) { return a + b + c; }';

View file

@ -23,6 +23,7 @@ import {
markCppInlineNamespaceRange,
populateCppInlineNamespaceScopes,
resolveCppQualifiedNamespaceMember,
splitCppQualifiedCallee,
} from '../../../../src/core/ingestion/languages/cpp/inline-namespaces.js';
const NO_SCOPES = {} as unknown as ScopeResolutionIndexes;
@ -331,3 +332,18 @@ describe('C++ qualified-namespace member index (#2788)', () => {
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', files, NO_SCOPES)).toBeUndefined();
});
});
describe('splitCppQualifiedCallee', () => {
// Keep the written namespace path so a factory result cannot borrow a
// same-named nested namespace from another outer namespace.
it.each<[string, { receiver: string; member: string } | undefined]>([
['rp::make', { receiver: 'rp', member: 'make' }],
['a::b::make', { receiver: 'a::b', member: 'make' }],
['ns::Box<a::B>::make', { receiver: 'ns::Box<a::B>', member: 'make' }],
['make', undefined],
['::make', undefined],
['Box<a::B>', undefined],
])('%s -> %j', (callee, expected) => {
expect(splitCppQualifiedCallee(callee)).toEqual(expected);
});
});