Commit graph

27 commits

Author SHA1 Message Date
samuel mbabhazi
9772bcd24f
feat(api_impact): list the callers of the requesting function as indirect consumers (#3720)
* feat(api_impact): list the callers of the requesting function as indirect consumers

The FETCHES edge is file-level, so api_impact saw only the file that sends
the request. The files that call its request function never mention the URL
and were missing from the report (#3644).

The edge's reason now names the callables whose spans contain the request
sites, one `|via:<id>` field each, under the containment rule the R3-6 sink
set already uses; that rule moves into EnclosingCallableIndex so both stay
one. Several sites of one file for one route now share the edge instead of
colliding on its id. api_impact follows the CALLS edges into those callables
one level up and reports the callers as `indirectConsumers`, apart from the
direct consumers and outside the risk level. Direct consumer files and the
handler file are left out. An index built before the attribution carries no
field. Expo navigation sites carry their real line so they attribute to the
component that contains them.

Closes #3644

* fix(api_impact): attribute a request site by its AST enclosing callable

A helper defined on the line of the request has the smaller span, so the
line-only lookup named it and the callers of the real requesting function
vanished from indirectConsumers. The parser now records the enclosing
callable of each fetch site and of each wrapper call (through the call's
position), and attribution prefers that identity; the span lookup remains
for sites found by text scan. The same preference applies to the R3-6 sink
set. A real analyze of the fixture followed by api_impact pins the helper
case and the wrapper case.

* test(bench): rebaseline receiver resolution for the indirect-consumers fixture

The api-impact-indirect-consumers fixture adds three receiver-unresolved
sites to the corpus: two `.then(...)` calls on the untyped return of
`fetch(...)` and of the local wrapper `apiFetch(...)`, and one
`req.query.path` read inside an inline Express handler with an untyped
`req`. Call drops 138 -> 140, reads 30 -> 31, all kinds 168 -> 171. No
shape-arm state and no existing site changed.

* fix(api_impact): preserve module-scope request attribution

Keep AST-confirmed module scope distinct from missing attribution for direct
fetches, HTTP clients, and wrapper calls. Prevent overlapping callable spans
from inventing indirect consumers or process sinks.

Preserve span fallback for callable forms the generic identity walk cannot
name. Add eight regression cases, including real analyze/api_impact coverage.
Rebaseline receiver-resolution counts for the two then calls and one property
read added by this PR's existing fixtures; shape expectations are unchanged.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-10-25 18:29:09 +00:00
Gergő Magyar
48f108a58a
docs: simplify guides and verify receiver gaps (#3694)
* docs: align resolution architecture and roadmap

* docs: add language ingestion gap inventories

* test: audit receiver gap targets and coverage

Check exact declaration IDs, expose the Dart await wrong-owner edge, and execute previously excluded C++, Rust, PHP, Dart and Vue shapes. Refresh the baseline and all language inventories from fresh measurements; call-drop counts remain unchanged.

* docs: focus roadmap and shorten project README

Keep the resolution roadmap limited to future work and acceptance criteria. Move detailed editor, CLI, MCP, deployment and wiki instructions into linked guides, preserve their command examples, and correct stale onboarding claims.

* docs: correct setup and audit descriptions

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-23 09:06:03 +01:00
Gergő Magyar
bbbc1b49e3
test(bench): type the JS, Ruby and PHP receiver shapes; mark C dispatch N/A (#3498) (#3663)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Classify release event (push) Waiting to run
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* test(bench): type the JS, Ruby and PHP receiver shapes; mark C dispatch N/A

Several shape-arm rows were measuring their fixtures instead of the receiver
spelling.

The JavaScript and Ruby rows had no types anywhere, so each chain cell said
"this code is untyped" and could only flip by guessing from a method name.
They now carry JSDoc and YARD types, the way plain JS and Ruby carry types.
Each row keeps one untyped control as a new extra id, `untypedChain`, so
`plainChain` and the other canonical ids mean the same thing in every row.
Ruby names each function's parameter after its cell, because YARD `@param`
bindings land on the Module scope. A probe showed an untyped `svc` resolving
through a sibling's tag.

PHP `plainDeepChain` and `optionalChain` went through the unannotated
`getUser()`, so they measured the missing return type rather than their own
spelling. They now use `getUserTyped()`, and `arrowCallChain` stays the
unannotated control. `indexElement` gains PHPDoc `@param User[] $repos`.

The C structs lived in `models.h`, which the C++ provider owns, so C never saw
them. They now live in `main.c`. All three measured C cells called through a
function-pointer field. That field has no graph node and no static callee, so
the cells could never resolve and are now N/A.

Shape-arm movement, every target checked:
- javascript: plainChain, plainDeepChain, optionalChain, awaitParen
  VISIBLE-GAP -> RESOLVES; untypedChain new, VISIBLE-GAP; indexElement
  stays VISIBLE-GAP (no JS elementTypeOf)
- ruby: plainChain, optionalChain VISIBLE-GAP -> RESOLVES; plainDeepChain
  VISIBLE-GAP -> INVISIBLE-GAP (base types, untyped attr_reader step);
  untypedChain new, VISIBLE-GAP; indexElement stays INVISIBLE-GAP
- php: plainDeepChain, optionalChain INVISIBLE-GAP -> RESOLVES
- c: plainDeepChain, indexElement VISIBLE-GAP -> N/A; fieldReceiverCall
  INVISIBLE-GAP -> N/A

The count arm and every other language's cells are unchanged. No resolver
code changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: correct typed shape benchmark states

* docs: qualify Ruby typed shape owner claims (#3663)

The receiver shape gate matches member names only, so the Ruby
plainChain/optionalChain owner (User.save) is a probe observation, as
already stated for the JavaScript and PHP rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: drop unverified #3609 tracking claims (#3663)

#3609 does not document the Ruby YARD @param scope leak or C header
struct visibility, so say both are not yet filed separately.

Also carry the BASELINE.md notes that came from the pre-rebase merge commits
(main c30449394 and stack parent #3667), which the linear replay onto the
rebased walker drops: the post-#3659 and #3667-stack integration notes and
the Ruby indexElement wording on the walker parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 00:05:10 +00:00
Gergő Magyar
09b7a5b174
fix(ingestion): read more receiver-chain spellings (#3498) (#3667)
* fix(ingestion): name chain steps without their explicit type arguments

C#, C++ and Rust put explicit type arguments inside the callee node, not
beside it as TypeScript, Java and Kotlin do. The receiver-chain walk read
the callee's text, so `svc.GetTyped<User>().Save()` built the step
`GetTyped<User>`, which matches no member, and C++ built `getTyped<User>`.
Rust's `svc.get_typed::<User>()` wraps the whole field access in a
`generic_function`, so the walk found neither a name nor the inner
receiver and built no chain at all.

The walk now peels these callee nodes (C# `generic_name`, C++
`template_method`, `template_function` and `dependent_name`, Rust
`generic_function`) before it reads the step name and the receiver. It is
a node-type table next to the existing ones; nothing branches on a
language. Type arguments choose an instantiation, never a member, so the
name loses nothing.

Shape arm: csharp and rust `explicitTypeArgs` go from VISIBLE-GAP to
RESOLVES, to `User.Save`/`User.save`. cpp `explicitTypeArgs` keeps its drop: the step
is now `getTyped`, but `template <typename T> T* getTyped()` has no
return type until `T` is substituted, which is separate work.

baseline.json and BASELINE.md record the move; the count arm and a
corpus A/B against main are unchanged.

The chain wire format is unchanged, so the receiver-chain codec version
stays at 2. Parse caches need no manual bump: the parse-cache identity
hashes every file under `core/ingestion`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ingestion): read receiver chains through C# `!` and Swift `(x)`

The chain walk peels wrappers that denote exactly their operand, so the
chain's base is `svc` rather than `svc!` or `(svc)`. Two spellings of that
were missing.

C#'s null-forgiving `svc!` is a `postfix_unary_expression`, so
`svc!.GetUser().Save()` built `2|svc!|cGetUser`, whose base binds nothing.
It now joins the `!`-gated entry Swift and Kotlin already use. The same
node type carries `x++` and `x--`, and the `!` gate leaves those alone.

Swift has no parenthesized-expression node: `(await svc.getUserAsync())`
and `(holder)` parse as a one-element `tuple_expression`, and the walk
stopped there. `(await svc.getUserAsync()).save()` built no chain, and
`(holder).repo.save()` built one whose base `(holder)` binds nothing. A new gate peels a tuple only when it is exactly `(`,
one element, `)`. That excludes Rust's real one-element tuple `(svc,)`,
Swift's labelled `(x: svc)`, and every tuple of two or more.

Shape arm: csharp `nonNullAssert` goes from VISIBLE-GAP to RESOLVES
(`User.Save`). Swift
`awaitParen` now builds `2|svc|cgetUserAsync|a`, but it was already a
VISIBLE-GAP and stays one: Swift return types live on the Module scope,
and the fold looks there only after the Swift/Dart hoist change lands.
Swift `(holder).repo.save()` now resolves to `User.save`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ingestion): walk C# null-conditional access in receiver chains

C# parses `svc?.GetUser()` as a `conditional_access_expression` whose
receiver sits in a `condition` field and whose member sits in a
`member_binding_expression` child. The chain walk read it like any other
callee: it named the step `.GetUser` and found no inner receiver, so
`svc?.GetUser().Save()` built no chain and could never resolve. As a
receiver (`user?.Address.Save()`, `repos?[0].Save()`) the node stopped
the walk, and its whole text became the base.

A node-type table now says where a conditional access keeps its receiver.
As a callee, the walk names the step from the member binding; an element
binding (`handlers?[0]()`) names no member. As a receiver, a member
binding becomes a field step and an element binding becomes an index step.

Shape arm: csharp `optionalChain` goes from VISIBLE-GAP to RESOLVES
(`User.Save`). `svc?.GetTyped<User>().Save()` also resolves, through the
type-argument peel from the earlier commit. `repos?[0].Save()` now
records its miss, which was silent before: `User[] repos` has no type
binding yet. `user?.Address.Save()` already resolved through the
receiver-text cascade; a resolver row pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ingestion): read Ruby and Swift subscripts as index steps

Two grammars spell a subscript as something the chain walk did not
recognise.

Ruby's `repos[0]` is an `element_reference`, which was missing from the
subscript table, so the walk stopped there. The receiver has no `.` or
`(` either, so the call vanished: no edge and no drop.

Swift has no subscript node at all. `repos[0]` parses as a
`call_expression` whose `call_suffix` holds `value_arguments`, the same
as a call, except that the list is delimited by `[`/`]` instead of
`(`/`)`. Walked as a call, it named no member, and `holder.users[0]`
named `users` as if it were a method. A call node whose suffix argument
list opens with `[` is now read as an index step. The opening token is
the only thing that tells the two apart, and `svc.getUser(0)` stays a
call.

Shape arm: ruby `indexElement` goes from INVISIBLE-GAP to VISIBLE-GAP.
`repos` is untyped, so there is nothing to fold, but the miss is now
counted. swift `indexElement` builds `2|repos|i` and records its miss
too, going from INVISIBLE-GAP to VISIBLE-GAP; resolving it needs a
Swift `elementTypeOf` hook from a separate change. So does
`holder.users[0].save()`, which now builds `2|holder|fusers|i` instead
of a call step named `users`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: clarify receiver walker review limits

* test: pin receiver-chain walker control spellings (#3667)

- Kotlin calls share Swift's `call_suffix → value_arguments`; pin that a
  parenthesized and a generic Kotlin call stay call steps, never index steps.
- Pin that a Swift trailing-closure call stays a call step.
- Pin that C# `GetTyped<T>()` and C++ `ns::getTyped<T>()` without a member
  receiver are named for the member and mint no chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: skip Swift chain rows when its grammar is unavailable (#3667)

The walker rows loaded the optional vendored Swift grammar at module scope,
so a host without its prebuild failed to collect every other language's
test in the file. Gate Swift on isLanguageAvailable and report its rows
as skipped; every other row still runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: expect Swift awaitParen through hoisted returns (#3667)

Rebased onto #3666, whose hoisted Swift return types let the walker's
`(x)` chain resolve awaitParen to User.save. This carries the content the
replaced merge commits held: the walker row now expects that edge and no
drop, the receiver baseline marks swift awaitParen RESOLVES, and
BASELINE.md keeps the measured main and stack integration notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(bench): label parent-only awaitParen note and drop issue claim (#3667)

The #3666 section said Swift awaitParen still needed a fix. That holds for
#3666 alone; combined with this walker it resolves to User.save with no
drop, while dart nonNullAssert stays unsupported. Also stop calling the
generic-arity limitation tracked by #3609, a general triage tracker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:33:50 +00:00
Gergő Magyar
cc0b5d54f7
fix(swift,dart): type chained receivers through hoisted return types (#3498) (#3666)
* fix(swift,dart): type chained receivers through hoisted return types (#3498)

Swift and Dart hoist every method's return-type binding out of the class
body onto the Module scope, as C# does, but neither set
hoistTypeBindingsToModule. The receiver walk therefore never looked
there, and svc.getUser().save() had no receiver type. Both now set it.

The flag alone was not safe. The hoisted bindings are keyed by bare
method name, so with class A { getUser() -> User } and
class B { getUser() -> Admin } in one file the Module scope keeps only
B's entry, and a.getUser().save() resolved to Admin.save. C# and
TypeScript already do this on main.

So there is a new ScopeResolver toggle, hoistedTypeBindingsRequireOwner,
which Swift and Dart also set. With it, the walk accepts a hoisted
binding only when its declaration anchor encloses the declaration of a
member of that name owned by the receiver's class. Otherwise it keeps
climbing, and declines if nothing passes. The four walk-ups in
compound-receiver.ts now share one helper.

The toggle is opt-in. With the guard on everywhere, TypeScript loses
correct edges where two same-named methods happen to return the same
type (11 rows in inferred-field-receiver-matrix), because the Module map
cannot hold both. Swift and Dart never had those edges.

Bench: swift plainChain, plainDeepChain, optionalChain and nonNullAssert
(INVISIBLE-GAP) and dart plainChain, plainDeepChain and optionalChain
(VISIBLE-GAP) now RESOLVE with correct targets. No other cell and no
count-arm figure moves. The inferred-field matrix gains correct
Inner.compute edges for Swift (3 rows) and Dart (7 Alien rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): guard hoisted return ownership

* fix(scope): gate hoisted return replay checks

* fix(scope): preserve parameterized return replay

Keep exact callee return facts when ancestor candidates have matching
known parameter types, while refusing same-owner overload ties.

Refs #3666

* fix(scope): preserve known variadic return replay

Compare complete captured parameter vectors across distinct ancestor
owners, including variadic methods with unknown numeric arity. Keep
unknown Swift parameter slots instead of compressing them away.

Refs #3666

* test(swift): retain reviewed receiver fixtures

* fix(scope): retain unknown overload candidates

Keep unknown parameter slots possible without hiding conversion overloads.
Check return-owner precision without requiring valid calls to stay unresolved.

Refs #3498

* fix(scope): preserve conversion suppression

Keep explicit empty conversion results final when parameter metadata is
unknown. Add exact positive controls for receiver-fixture liveness.

Refs #3498

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:52:11 +00:00
Gergő Magyar
673dc6968e
fix(rust): see through Box, Rc and Arc on struct field types (#3498) (#3665)
* fix(rust): see through Box, Rc and Arc on struct field types (#3498)

The struct-field normalizer in rust/range-binding.ts cut a type at its
first `<`. A `repo: Box<User>` field was typed `Box`, so `self.repo.save()`
bound nothing. It also stripped a leading `&` but not a lifetime, so a
borrowed field, which always carries one (`&'a User`), came out as
`'a User`.

Box, Rc and Arc auto-deref on a method call, so the field now takes their
first type argument: bare or path-qualified (`std::rc::Rc<User>`), and
nested (`Rc<Box<User>>`). Borrows with a lifetime or `mut` are stripped,
and `Box<dyn Trait>` reduces to the trait through the stripDynBound helper
that parameters already use (#2604).

RefCell, Cell, Mutex, RwLock, Option and Vec are deliberately left alone.
A method on the inner type needs `.borrow()`, `.get()`, `.lock()`,
`.read()`, a match or an index first, so the receiver really is the
wrapper, and a direct call through one must not bind.

Shape arm: rust `decoratedFieldType` moves INVISIBLE-GAP -> RESOLVES, with
the edge on `User::save`. The corpus has no smart-pointer or borrowed
struct field, so the count arm and every other cell are unchanged, and the
corpus edge (2838) and drop (192) sets match main exactly. Scope-capture
fingerprints are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(rust): keep opaque pointer fields wrapped

* fix(rust): refuse opaque macro expansions

* test(rust): pin pointer wrapper targets and opacity limits (#3665)

- Assert a same-file Box keeps its own save target while User.save stays
  unbound (review thread on custom same-named wrappers).
- Pin the documented workspace-wide recall limit: an unrelated unresolved
  import or macro_rules! keeps an otherwise valid pointer field wrapped.
- Drop per-case logging and group the opaque/expansion cases in describe
  blocks.
- Collapse the stacked pointer-field receipts in BASELINE.md into one
  current section listing every workspace-wide refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: preserve Rust integration notes

* test(rust): assert complete pointer targets

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:19:27 +00:00
Gergő Magyar
af5fd9665c
fix(csharp): bind nullable field types for receiver resolution (#3498) (#3664)
* fix(csharp): bind nullable field types for receiver resolution (#3498)

A C# field written `User? repo;` got no type binding. Its type is a
`nullable_type` node, and the field_declaration rules only matched
identifier, generic_name and qualified_name types. Parameters, properties,
method returns and foreach variables already have a nullable_type rule, so
fields were the odd one out: the class scope had no binding for `repo`,
and `this.repo.Save()` was dropped as receiver-unresolved.

Add the missing field rule. interpretCsharpTypeBinding already strips the
`?`, so the field binds `User`, as the property spelling does.

Shape arm: csharp `decoratedFieldType` moves VISIBLE-GAP -> RESOLVES, with
the edge on `User.Save`. No corpus fixture declares a nullable field, so
the count arm and every other cell are unchanged, and the corpus edge
(2838) and drop (192) sets match main exactly. Scope-capture fingerprints
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(csharp): preserve nullable field ownership

* fix(csharp): refuse unowned nullable types

* test(csharp): guard nullable-member fixture cleanup (#3664)

Address nexus-check review on resolvers/csharp.test.ts:1471: skip the
temp-root removal when mkdtempSync never ran, so a setup failure is not
masked by a secondary rmSync(undefined) TypeError. Matches the guard in
csharp-nullable-field-review.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(bench): scope nullable wrapper claim to what the code does (#3664)

The ownership-controls note said the nullable-field marker preserves
custom generic wrappers. Its only control, Custom.List<User>?, passes
through the qualified-type refusal instead. State that unqualified
wrappers keep the wrapper type (no native control yet) and that
qualified spellings, custom wrappers included, stay unbound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: preserve C# integration notes

* fix(csharp): preserve generic argument qualifiers

Ignore comments when checking outer nullable-field qualification.

Refs #3664

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:05:33 +00:00
Abhigyan Patwari
36e16434ea
fix(php): type locals from namespaced-function factory calls (#3498) (#3662)
* fix(php): type locals from namespaced-function factory calls

`namespace App; use App\Mod as rp; $x = rp\make(); $x->m()` left `$x`
untyped although `make` is declared `: C` in `namespace App\Mod`, and the
inline `rp\make()->m()` recorded a drop. The query matched only an
unqualified callee (`function: (name)`), so a `qualified_name` callee minted
neither a call site nor a result binding; `\App\Mod\make()`, `Mod\make()`
and `namespace\make()` failed the same way, and nothing could have expanded
the path anyway: functions were never indexed by fully qualified name.

- A `qualified_name` / `relative_name` callee is captured as a free call
  (tail name, written path as `@reference.qualified-name`) and as a
  call-result binding that keeps the whole path, leading `\` included.
- New `php/qualified-function.ts` expands that path by PHP's name rules:
  `\` is absolute, `namespace\` is the file's namespace, and a qualified
  head goes through the file's class/namespace `use` table, else the file's
  namespace. It indexes every `Function` def by lower-cased FQN per pass and
  answers only when exactly one def declares the name.
- PHP implements `resolveQualifiedCallee` and `resolveQualifiedFreeCall`
  with that one resolution, so a call's type and its CALLS edge agree.
  `Qff\Decoy\make` in the fixture is never picked.
- A receiver bound to an in-program namespaced function with no return type
  (`$x = rp\untyped()`) now records a `receiver-unresolved` drop.

A full lang-resolution A/B against the parent removes no CALLS/ACCESSES edge
and no drop. It adds 20 edges, all inside the new
php-qualified-function-factory fixture, and 2 drops (its two untyped sites).
The PHP capture golden gains the new fixture only. Existing fixtures' digests
are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(php): bind use-function imports outside PSR-4 by fully qualified name

`use function App\Mod\make; $x = make(); $x->m()` typed nothing, and the
call itself had no CALLS edge, when `make` is declared outside any PSR-4
path: loaded through composer `autoload.files`, or in a project with no
composer.json. Import resolution finds a function only through a PSR-4
directory or a file named after it, so the import bound nothing (or, with a
PSR-4 directory that holds other files, resolved to one that declares no
`make`). In the new fixture, `namedCall` even picked up another `make`'s
return type, mirrored by name from a sibling file, and emitted a wrong
`QffGadget.ping` edge.

After namespaces are known, a `use function A\f` whose module scope binds
nothing for its local name is bound to the one workspace function named
`A\f`, through the same per-pass FQN index as qualified callees. The fully
qualified name is the function's identity, so this guesses nothing; a
resolved import or a local declaration is left alone, and two declarations
of one name bind nothing. The IMPORTS edge is unchanged: finalize has no
namespaces yet.

Lang-resolution A/B against the parent: one wrong edge removed
(`namedCall -> QffGadget.ping`), two correct fixture edges added
(`namedCall -> make`, `namedCall -> QffWidget.ping`), and four correct
edges the combined corpus lacked because its merged composer configs map
`App\` to many fixtures: php-calls `create_user -> write_audit`,
php-overload-dispatch `run -> format_text` and `run -> format_text_padded`,
php-use-function-const `Calculator.process -> formatName`. Each FQN is
declared once in the corpus. Drops are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline php captures and receiver counts

- receiver-resolution: the new php-qualified-function-factory fixture's two
  untyped sites (`$x = rp\untyped(); $x->ping()` and inline
  `rp\untyped()->ping()`) are each one call drop: call drops 154 -> 156,
  all kinds 224 -> 226. No other drop moves in a same-corpus A/B, and
  shape-arm cells are unchanged.
- scope-capture (php): corpus growth only. The query now captures
  qualified function callees, but no existing fixture has one: parking the
  new fixture reproduces the prior fingerprint byte-for-byte. It adds 96
  capture groups and 9 fixture entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(php): fail closed on qualified calls the path cannot resolve

A qualified call site was named by its tail (`count` for `\count($x)`),
so when `resolveQualifiedFreeCall` refused the path, the free-call pass
fell through to its simple-name tiers and could bind a same-named
function in the caller's namespace: `\count($x)` -> a user `Qff\Ext\count`,
`\Vendor\Pkg\make()` -> `Qff\Ext\make`, and a twice-declared
`\Qff\Dup\make()` the same way. Before this branch such calls minted no
site at all, so these were new wrong edges.

The site is now named by its whole written path, the same text it carries
as `@reference.qualified-name`. The provider hook still resolves it by
PHP's name rules. Every simple-name tier (scope chain, implicit `this`,
global unique-name fallback) looks up a name with a `\` in it and finds
nothing, so a refused path (builtin, uninstalled package, ambiguous name,
multi-namespace file) leaves the call unresolved. This change is PHP-only.

Lang-resolution A/B against the parent: no edge or drop removed. Added
edges: the three new fixture functions' `new QffWidget()` calls. No edge
from builtinCall, vendorCall or ambiguousCall. Versus the pre-branch base:
29 edges added (25 in the fixture, plus the 4 `use function` edges from
4781ace51), 0 removed, 2 drops added. Receiver counts unchanged. The PHP
golden changes only the new fixture's files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline php captures for fail-closed qualified calls

scope-capture (php): a qualified function call site is now named by its
whole path, and the php-qualified-function-factory fixture gains four
files (src/Ext/Calls.php, src/Ext/Shadows.php, lib/dup-a.php,
lib/dup-b.php). Only that fixture moves. Parking it reproduces the
pre-branch fingerprint byte-for-byte. fixture_count 152 -> 156,
capture_groups_fp 2338 -> 2381. receiver-resolution is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(php): preserve qualified identifier bytes

Keep PHP labels and ASCII-only name matching through qualified factories.
Decline names shortened by native non-ASCII whitespace parsing.
Correct fixture comments and give parameterized cases unique audit IDs.

* fix(php): preserve reviewed merge resolutions

* test(php): align golden with fixture comment

The corrected factory annotation comment is included in captured function
text; keep its fine golden digest aligned with the reviewed fixture.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:55:04 +00:00
Abhigyan Patwari
e7ef82dfcb
fix(kotlin): type locals from package-qualified factory calls (#3660)
* fix(scope): follow namespace factory return types and record untyped drops

`import mod as rp; x = rp.make(); x.m()` lost the receiver type even when
`make` is declared `-> C`: `x` was bound to the raw dotted name `rp.make`,
and nothing followed a dotted name to a function's return type. The inline
`rp.make().m()` failed the same way in the compound resolver, which read
`ns.f()` only as a construction.

Resolve `ns.f` to `f`'s return annotation when `ns` is a namespace import,
the fact `propagateImportedReturnTypes` already mirrors for a named import.
The post-finalize chain follow uses it for bindings and the compound
resolver for inline calls. Only sites that emitted nothing change: the
full lang-resolution corpus emits the identical 2,838 CALLS/ACCESSES edges.

Record the drops that left `impact` at "exact":
- a plain receiver bound to a namespace function with no usable return type
  (`x = ns.untyped()`);
- an outer call whose `file:line:col` key a resolved inner site already
  claimed (`ns.untyped().m()`, `s.repo.Delete(id)`). Only a duplicate
  capture of the same member hides a drop now.

Receiver baseline: call drops 128 -> 151, reads 44 -> 48, ten shape-arm
cells INVISIBLE-GAP -> VISIBLE-GAP; every new drop has no CALLS edge to its
member (BASELINE.md lists them). The python.factory-qualified accuracy gap
is removed; the python-scope fingerprint and capture golden gain the new
fixture only.

Fixes #3498

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(dart): type locals from prefixed-import factory calls

`import 'mod.dart' as rp; final x = rp.make(); x.m()` gave `x` no type:
local call inference accepted only a bare callee (`Callee()`), and the
prefix-stripping type normalizer would have reduced `rp.make` to `make`.

Bind a local initialized by exactly `prefix.member(...)` (direct or
awaited) to the dotted callee and keep its prefix, so the shared chain
follow types it from the namespace function's return annotation. Only
call-derived bindings keep the prefix; annotated `prefix.Type` spellings
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): type locals from namespace-module factory calls

`const rp = @import("mod.zig"); const x = rp.make(); x.m()` bound `x` to
the namespace handle `rp` itself: the call-return rule reads a non-value
head as the type being constructed (`Counter.init()`), and import bindings
are not values. The member name was gone before the shared chain follow
could use the new namespace return-type lookup.

For a module-level handle on a lowercase file (a namespace module, by Zig
convention), bind the dotted callee `rp.make` so the shared chain follow
types it from `make`'s return annotation. File-struct handles
(`@import("Page.zig")`, any spelling of the handle) keep naming the
struct, so `Page.init()` is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline captures and receiver counts for Dart and Zig

- receiver-resolution: the Dart fixture's untyped factory adds one call
  drop (151 -> 152); every typed form in the Dart and Zig fixtures resolves.
- scope-capture (dart): two existing dart-method-enrichment locals
  initialized by `obj.method()` / `Type.method()` gain a call-derived
  binding, plus the new fixture's captures. A full lang-resolution A/B
  emits the identical edges outside the new fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): bind same-namespace inheritance bases before sibling visibility exists

Cause: same-package / same-namespace visibility (`populateNamespaceSiblings`)
is populated after the inheritance pre-pass and `buildMro`. A base written
without an import (`class C extends B`, B in another file of the package)
could bind only through the workspace-unique simple-name fallback, which any
other package declaring a `B` defeats: no EXTENDS edge, an empty MRO, and
lost calls through inherited members. In the combined lang-resolution corpus
`java-grandparent-resolution` `c.greet().save()` dropped because
`java-unrelated-method-collision` declares default-package A and B.

Moving the sibling hook ahead of the pre-pass was measured and rejected. It
retargeted correct edges and minted wrong ones, because the sibling channels
carry candidates the pre-pass must not see: C# global-namespace types are
consulted at every scope (same-file `Cat : Animal` bound another fixture's
global Animal), namespace buckets merge across source roots with no
proximity order (PHP, C#), and a qualified `extends app.base.Box` bound
through a same-package `app.Box`.

Fix: an optional `ScopeResolver.resolveImplicitInheritanceBase`, consulted by
the pre-pass only when the shared chain bound nothing, the base was written
bare (no qualifier, name capture at the start of the base expression), and
no import claims the name. It adds edges and never retargets one.
`resolveSameNamespaceInheritanceBase` picks the candidate in the deriving
class's namespace, nearest by shared path prefix, refusing a tie. Java and
Kotlin answer from their package facts, C# from the exact `namespacePrefix`.

Corpus A/B (main vs this, same corpus): EXTENDS/IMPLEMENTS +42, all within
their own fixture, 0 removed. CALLS: six `super.save()` edges keep their
target but are now MRO-dispatched (`global`, as in isolation), one moves off
another fixture's BaseModel.Save onto its own, and 7 are new (both
grandparent `greet` calls, a default-method `validate`, the new fixtures).
Receiver bench: call drops 128 -> 127, all kinds 192 -> 191, shape arm
unchanged. Scope-capture fingerprints re-baselined for the added fixtures
only (parked, --check passes on the old ones).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(java): resolve a duplicated package+type import to the importer's own root

Cause: `buildJavaPackageIndex` kept the first file declaring each
package+type. One repository can hold several build modules or sample
projects that each declare `models.User`, so every `import models.User;`
bound whichever copy was parsed first, a wrong IMPORTS edge at full
confidence that carries the receiver types behind it. In the combined
lang-resolution corpus 24 fixtures declare `models.User`, and e.g.
`java-heritage` UserService.processUser called
`java-assignment-chain/models/User.save`.

Fix: the index keeps every declaring file in a sparse `duplicateTypes` map
(only colliding pairs, so a normal workspace pays nothing), and a type import
resolves to the declaring file sharing the longest leading path with the
importer (its own module or source root). A tie, or an unknown importer,
refuses rather than guessing. Wildcard imports are unchanged.

Corpus A/B (previous commit vs this, same corpus): 40 CALLS edges that
crossed into another fixture's User/Repo/Address/UserService are gone, 55
CALLS/ACCESSES edges now land in their own fixture (0 cross-fixture), and
`java-parent-resolution` User IMPLEMENTS Serializable moves off
`java-heritage` onto its own interface. Receiver-unresolved drops unchanged;
import-target and receiver benches pass. Scope-capture java fingerprint
re-baselined for the added fixture only (parked, --check passes on the old
one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(scope): expose the position a receiver-chain fold reaches

`foldReceiverChain` answers "which class", so a chain whose last step is
declared as a type the program does not contain (`Dictionary<string,
User>`, `String`) reads only as a miss. That miss is informative to the
receiver-origin classifier: every hop was typed by a program declaration
and the last one names a library type.

Extract the fold loop into `foldReceiverChainState` and export
`foldReceiverChainPosition`, which returns the final position including
one whose declared type named no class, and reports nothing to
`recordReceiverType`. `foldReceiverChain` keeps its behaviour: it now
wraps the same loop and makes the receiver-type report from the final
state, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): classify library-rooted receiver drops from provider evidence

`classifyReceiverOrigin` had one source of positive external evidence,
`LanguageProvider.isBuiltInName`. That set also filters callee names out
of the call graph, so it could not grow to cover host globals, stdlib
imports or library-typed values without dropping real edges, and every
such drop hedged `impact()` to `lower-bound`. It also read a base name as
evidence even where the program bound that name itself: a parameter
named `console` with an unresolved type, or an unresolved import of a
built-in spelling from the program's own module, came back `external`.

Add two provider data hooks read only by the classifier:
`externalReceiverNames` (globals, platform roots, library types) and
`isExternalModuleSpecifier` (import specifiers reserved for the runtime
or stdlib). The classifier now answers `external` when:
- the base is unbound at the site and is a built-in or listed name;
- the base is bound only by imports that resolved to no program file and
  all name a library module;
- the receiver chain folds, through declarations each owner makes, to a
  position declared as a library type (`this.data.Keys` with
  `Dictionary<string, User> data`), and the program declares nothing by
  the dropped member's name (an extension method or trait impl could be
  the real target). The fold skips ancestor-hoisted bindings, which can
  lend another class's member type to the step.
A base the program binds (a parameter, a local, a resolved import, an
import from a non-library module) never takes a name-based verdict. The
in-program arms are unchanged and still win first. No drop is added or
removed and no edge changes.

Java supplies its reserved `java` package root for both hooks: the JVM
refuses to define classes under `java.*`, so `java.util.Optional.of(...)`
and `import java.util.Map` are rooted in the platform. Without the
specifier hook, the stricter import rule would have demoted the existing
`import java.util.List; List.of(...)` verdict to unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(typescript): name JavaScript host globals as the program boundary

`document.body.appendChild(...)`, `window.history.pushState(...)` and
`Buffer.from(raw).toString()` were recorded `unknown`: none of
`document`, `window`, `Buffer`, `process` or `globalThis` is in
`BUILT_INS`, and they cannot be added there because that set also
filters callee names, so `process` would drop every call to a program
function named `process`. Each drop hedged `impact()` to `lower-bound`
although no program node could have been the target.

List the host objects in `EXTERNAL_RECEIVER_NAMES` and supply it to the
TypeScript, JavaScript and Vue providers as `externalReceiverNames`. A
name counts only where the program binds nothing by it and declares
nothing by it, so a `document: Doc` parameter, an untyped `window`
parameter, a local `navigator` and a program `class Buffer` keep their
verdicts. `location`, `history`, `self` and `crypto` stay out: they are
ordinary local names.

Corpus: 5 call drops move from unknown to external. `process.env.*`
(3 reads) stays in-program there because three fixtures declare a
top-level `function process()`, one of them in a global script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(python): name standard-library imports as the program boundary

`os.environ.setdefault(...)` after `import os`, and
`Path(__file__).resolve()` after `from pathlib import Path`, were
recorded `unknown`. The imports resolve to no file in the program, but an
unresolved import alone cannot be told apart from an in-program module
the resolver missed, and nothing said that `os` and `pathlib` belong to
the interpreter.

Supply `isExternalModuleSpecifier` from a curated set of standard-library
modules, matched on the specifier's first segment (`os.path` -> `os`).
A relative specifier never matches. The classifier still requires the
import to have resolved to no program file, so a local `os.py` that the
resolver finds wins, and a parameter named `os` is the program's own.
Generic nouns an application plausibly uses for its own modules
(`types`, `queue`, `signal`, `secrets`, `string`, `email`, `http`, ...)
are left out. Third-party packages are not listed: an unresolved
`fastapi` import stays `unknown`.

Corpus: 4 call drops move from unknown to external
(`os.environ.setdefault` x3, `Path(__file__).resolve()`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): name @import("std") and @import("builtin") as the program boundary

`std.sort.pdq(...)` and `const Allocator = std.mem.Allocator;` were
recorded `unknown`: `std` is bound by `const std = @import("std")`, an
import the resolver deliberately leaves external, and nothing told the
classifier that the module is the standard library rather than an
in-program file it failed to find.

Supply `isExternalModuleSpecifier` for the two module names Zig
reserves: `std` and the compiler-generated `builtin`. Exact names only,
so `@import("std.zig")` (a file path) stays `unknown`, as does `root`
(the program's own root module) and any module `build.zig` names.

Corpus: 9 drops move from unknown to external: `std.sort.pdq` (2 calls,
2 reads), `std.mem.Allocator` (1 read), and the
`zig-qualified-literal` literals `std.Thread.Mutex{}` and
`std.mem.Allocator{...}` (2 calls, 2 reads). The last four are that
fixture's edge negative control (`useExternal` must emit no edge, which
still holds); their receiver is std, so `external` is the right origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(csharp): name the System namespace and Dictionary as the program boundary

`System.Console.WriteLine(...)` was recorded `unknown`: `Console` is in
`BUILT_INS` but the chain base is `System`, which is not, and `this.data.Keys`
with `Dictionary<string, User> data` was recorded `in-program` because
the base `this` resolves to a program class even though the member lives
on a BCL type.

Supply `externalReceiverNames` with `System` (the BCL root namespace),
`Dictionary` (the one collection capture does not reduce to its element,
because it takes two type arguments) and `string` (a keyword alias that
no program can declare). The base check covers `System.Console`; the
chain fold reaches `Dictionary<string, User>` through the program's own
field declaration. A program `class System`, a field of a program type,
and an extension method the program declares on `Dictionary` all keep
their verdicts.

Corpus: 3 call drops move from unknown to external
(`System.Console.WriteLine`). The two `this.data.Keys/Values` reads in
`csharp-nested-member-foreach` stay in-program there: the fixture's
global-namespace `App` is one of several in the merged corpus, so `this`
binds another fixture's `App` and the fold declines. Analysed alone,
both read `external`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kotlin): name java.lang.System and the platform roots as the program boundary

`System.out.println(...)` in Kotlin was recorded `unknown`: Kotlin's
`BUILT_INS` lists stdlib function names, not `System`, which Kotlin/JVM
imports by default from `java.lang`.

Supply `externalReceiverNames` with `System` and the package roots no
program can declare into (`java`, refused by the JVM; `kotlin`, reserved
by the compiler for the standard library), and `isExternalModuleSpecifier`
for imports under those roots. A program `object System` keeps the
receiver `in-program`.

Corpus: 1 call drop moves from unknown to external
(`kotlin-object-literal-scope` `System.out.println`, whose decoy
`println` on the enclosing object still gets no edge).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline receiver-origin buckets for library receivers

The preceding commits label library- and runtime-rooted drops `external`
instead of `unknown`. Call drops by origin move from 55 in-program, 44
external and 29 unknown to 55, 62 and 11. Total drops (192), call drops
(128), every per-extension and per-shape count, and every shape-arm
state are unchanged; a same-corpus A/B against main keeps all 2,838
CALLS/ACCESSES edges byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(rust): type locals from module-qualified factory calls

`mod modx; use crate::modx as rp; let x = rp::make(); x.m()` left `x`
untyped although `make` is declared `-> C`. The call-return rule kept only
the text before the first `::` (the `Foo::new()` idiom), so `x` was bound
to the module `rp` and the function name was lost. `modx::make()` and
`crate::modx::make()` failed the same way, and the inline `rp::make().m()`
failed in the compound resolver, which cannot split a `::` path. With a
`let modx = Other;` in scope, `modx::make()` even typed `x` as `Other` and
emitted a wrong `Other.m` edge.

- Rust keeps the whole callee path when every segment is lower-case: a
  module function, by rustc's lint-enforced casing. `Foo::new()` and
  `Self::new()` still read as the type.
- New `ScopeResolver.resolveQualifiedCallee` hook. The shared chain follow,
  the compound call branch and the drop recorder hand a callee text no
  scope binds to the provider, which returns the callable it names; the
  shared side reads that callable's return annotation by definition
  identity and never splits the path. Rust answers with the module-tree
  walk behind `resolveQualifiedFreeCall`, so a call's type and its CALLS
  edge come from the same resolution.
- A receiver bound to an in-program module function with no return
  annotation (`x = rp::untyped()`) records a `receiver-unresolved` drop,
  as #3498 does for `ns.untyped()`.
- `rp::make()` did not resolve as a call either: the module-name filter
  rejected the alias `rp`, and under Cargo the import binds the module's
  `mod modx;` declaration rather than `modx.rs`. The `use` candidate now
  admits an alias that names a module and reads a module from its
  top-level declaration.

A full lang-resolution A/B against the parent emits identical CALLS and
ACCESSES edges outside the new fixture, except one new correct edge:
`rust-ambiguous` `main -> models::Handler.handle`, typed from
`let h = services::create_handler()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline rust captures and receiver counts

- receiver-resolution: the new rust-qualified-factory-return fixture's two
  untyped sites (`x = rp::untyped(); x.m()` and inline
  `modx::untyped().m()`) are each a call and a callee-position read drop:
  call drops 152 -> 154, reads 48 -> 50, all kinds 220 -> 224. No other
  drop moves in a same-corpus A/B, and shape-arm cells are unchanged.
- scope-capture (rust): corpus growth only. emitRustScopeCaptures is
  untouched, and parking the new fixture reproduces the prior fingerprint
  byte-for-byte; it adds 131 capture groups and 3 fixture entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): ask resolveQualifiedCallee about dotted callee paths

A dotted call-return binding (`x -> com.acme.mod.make`) whose head no
namespace import binds came back from followChainPostFinalize untouched.
The dotted branch tried only namespaceCallReturnType, which needs a
two-segment path headed by a namespace import. The inline
`com.acme.mod.make().m()` failed the same way in the compound resolver: once
the object part resolved to no class and named no namespace import, the call
branch returned nothing. A language whose package paths are dotted and never
imported (Kotlin) could type neither form.

Both sites now pass the dotted text to the provider's resolveQualifiedCallee
after their own lookups miss, as they already did for undotted callees, and
read the returned callable's return annotation by definition identity. The
contract documents the two new consult points, and says that a provider must
refuse a path whose head the call site can see as a value or type.

No provider answers dotted text yet: Rust's hook reads only `::` paths. No
edge moves, and the Rust, Python, Dart and Zig resolver suites pass
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kotlin): type locals from package-qualified factory calls

`val x = kqfmod.make(); x.m()` left `x` untyped although `make` is declared
`: C`. `com.acme.mod.make()` failed the same way, and the inline
`kqfmod.make().m()` dropped. Three gaps lined up:

- the constructor-inferred query matches only a bare callee;
- the navigation-call inference handles only typed locals and same-file
  classes;
- nothing resolved a package name, because Kotlin imports declarations, not
  packages, so no import edge names `kqfmod`.

The named-import form (`import kqfmod.make; make()`) already worked.

- The local-assignment synthesis binds `x` to the whole callee path when the
  callee is a dotted path of plain names. The head must be lower-case and
  must not be declared or imported in the file as a value or type. A
  `@type.qualified-callee` marker stops interpretKotlinTypeBinding from
  reducing the path to the function name. Typed-local and same-file-class
  receivers (`repo.find()`, `Logger.create()`) are tried first, so they keep
  their meaning.
- New Kotlin resolveQualifiedCallee. The package is everything before the
  last segment. A package spans files, and across them it must declare
  exactly one top-level function with that name. The index of package facts
  and module bindings is built once per pass. The hook refuses overloads. It
  also refuses a head the call site can see: anything the lexical claim walk
  finds, plus top-level names in same-package sibling files and star-imported
  packages, which that walk misses.
- An untyped package function (`val x = kqfmod.untyped()`) now records a
  receiver-unresolved drop instead of an exact miss.

New fixture kotlin-package-qualified-factory covers:

- single- and multi-segment packages, and inline calls;
- the untyped drop and the named-import control;
- local, parameter, property, import, star-import and same-package values
  named like the package;
- overloads, and companion/object member calls.

A full lang-resolution A/B against the parent emits identical CALLS/ACCESSES
edges and drops outside the new fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline kotlin captures and receiver counts

- receiver-resolution: the new kotlin-package-qualified-factory fixture
  records six drops, all in app/Callers.kt. Two are call drops on untyped
  receivers: `x.m()` on `val x = kqfmod.untyped()` and the inline
  `kqfmod.untyped().m()`. The other four come from `com.kqfacme.deep.make()`
  in deepCall and inlineDeep: each is a call drop plus a read drop on the
  `deep` receiver path. Call drops 154 -> 158, reads 50 -> 52, all kinds
  224 -> 230. A same-corpus A/B shows no other drop change. Shape-arm cells
  are unchanged.
- scope-capture (kotlin): moving the new fixture out of the corpus
  reproduces the prior fingerprint byte-for-byte. Base and new emitters
  differ by exactly six matches over the same corpus, all of them in the
  new fixture. Synthetic counts are unchanged, and scaling is 1.039 < 1.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(scope): reuse qualified call file sets

* docs(scope): clarify finalized type refinement

* test(kotlin): sync measured capture counts

* fix(rust): scope generic alias parameters

* fix(scope): respect base candidate visibility

* test(scope): preserve library member collision hedge

* fix(scope): cover more library receiver evidence

* fix(vue): classify imported runtime receivers

* fix(scope): match written JVM base arity

* fix(kotlin): refuse unproven package receivers

* fix(kotlin): prove companion receiver headers

* test(kotlin): complete JVM scope fixtures

* fix(kotlin): unify package receiver shadow checks

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:49:06 +00:00
Abhigyan Patwari
c30449394b
fix(scope): bind same-package inheritance bases when the simple name is reused elsewhere (#3659)
* fix(scope): bind same-namespace inheritance bases before sibling visibility exists

Cause: same-package / same-namespace visibility (`populateNamespaceSiblings`)
is populated after the inheritance pre-pass and `buildMro`. A base written
without an import (`class C extends B`, B in another file of the package)
could bind only through the workspace-unique simple-name fallback, which any
other package declaring a `B` defeats: no EXTENDS edge, an empty MRO, and
lost calls through inherited members. In the combined lang-resolution corpus
`java-grandparent-resolution` `c.greet().save()` dropped because
`java-unrelated-method-collision` declares default-package A and B.

Moving the sibling hook ahead of the pre-pass was measured and rejected. It
retargeted correct edges and minted wrong ones, because the sibling channels
carry candidates the pre-pass must not see: C# global-namespace types are
consulted at every scope (same-file `Cat : Animal` bound another fixture's
global Animal), namespace buckets merge across source roots with no
proximity order (PHP, C#), and a qualified `extends app.base.Box` bound
through a same-package `app.Box`.

Fix: an optional `ScopeResolver.resolveImplicitInheritanceBase`, consulted by
the pre-pass only when the shared chain bound nothing, the base was written
bare (no qualifier, name capture at the start of the base expression), and
no import claims the name. It adds edges and never retargets one.
`resolveSameNamespaceInheritanceBase` picks the candidate in the deriving
class's namespace, nearest by shared path prefix, refusing a tie. Java and
Kotlin answer from their package facts, C# from the exact `namespacePrefix`.

Corpus A/B (main vs this, same corpus): EXTENDS/IMPLEMENTS +42, all within
their own fixture, 0 removed. CALLS: six `super.save()` edges keep their
target but are now MRO-dispatched (`global`, as in isolation), one moves off
another fixture's BaseModel.Save onto its own, and 7 are new (both
grandparent `greet` calls, a default-method `validate`, the new fixtures).
Receiver bench: call drops 128 -> 127, all kinds 192 -> 191, shape arm
unchanged. Scope-capture fingerprints re-baselined for the added fixtures
only (parked, --check passes on the old ones).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(java): resolve a duplicated package+type import to the importer's own root

Cause: `buildJavaPackageIndex` kept the first file declaring each
package+type. One repository can hold several build modules or sample
projects that each declare `models.User`, so every `import models.User;`
bound whichever copy was parsed first, a wrong IMPORTS edge at full
confidence that carries the receiver types behind it. In the combined
lang-resolution corpus 24 fixtures declare `models.User`, and e.g.
`java-heritage` UserService.processUser called
`java-assignment-chain/models/User.save`.

Fix: the index keeps every declaring file in a sparse `duplicateTypes` map
(only colliding pairs, so a normal workspace pays nothing), and a type import
resolves to the declaring file sharing the longest leading path with the
importer (its own module or source root). A tie, or an unknown importer,
refuses rather than guessing. Wildcard imports are unchanged.

Corpus A/B (previous commit vs this, same corpus): 40 CALLS edges that
crossed into another fixture's User/Repo/Address/UserService are gone, 55
CALLS/ACCESSES edges now land in their own fixture (0 cross-fixture), and
`java-parent-resolution` User IMPLEMENTS Serializable moves off
`java-heritage` onto its own interface. Receiver-unresolved drops unchanged;
import-target and receiver benches pass. Scope-capture java fingerprint
re-baselined for the added fixture only (parked, --check passes on the old
one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): respect base candidate visibility

* fix(scope): match written JVM base arity

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:45:11 +00:00
Abhigyan Patwari
169aa5d960
fix(scope): label library-rooted receiver drops as external from provider evidence (#3658)
* refactor(scope): expose the position a receiver-chain fold reaches

`foldReceiverChain` answers "which class", so a chain whose last step is
declared as a type the program does not contain (`Dictionary<string,
User>`, `String`) reads only as a miss. That miss is informative to the
receiver-origin classifier: every hop was typed by a program declaration
and the last one names a library type.

Extract the fold loop into `foldReceiverChainState` and export
`foldReceiverChainPosition`, which returns the final position including
one whose declared type named no class, and reports nothing to
`recordReceiverType`. `foldReceiverChain` keeps its behaviour: it now
wraps the same loop and makes the receiver-type report from the final
state, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): classify library-rooted receiver drops from provider evidence

`classifyReceiverOrigin` had one source of positive external evidence,
`LanguageProvider.isBuiltInName`. That set also filters callee names out
of the call graph, so it could not grow to cover host globals, stdlib
imports or library-typed values without dropping real edges, and every
such drop hedged `impact()` to `lower-bound`. It also read a base name as
evidence even where the program bound that name itself: a parameter
named `console` with an unresolved type, or an unresolved import of a
built-in spelling from the program's own module, came back `external`.

Add two provider data hooks read only by the classifier:
`externalReceiverNames` (globals, platform roots, library types) and
`isExternalModuleSpecifier` (import specifiers reserved for the runtime
or stdlib). The classifier now answers `external` when:
- the base is unbound at the site and is a built-in or listed name;
- the base is bound only by imports that resolved to no program file and
  all name a library module;
- the receiver chain folds, through declarations each owner makes, to a
  position declared as a library type (`this.data.Keys` with
  `Dictionary<string, User> data`), and the program declares nothing by
  the dropped member's name (an extension method or trait impl could be
  the real target). The fold skips ancestor-hoisted bindings, which can
  lend another class's member type to the step.
A base the program binds (a parameter, a local, a resolved import, an
import from a non-library module) never takes a name-based verdict. The
in-program arms are unchanged and still win first. No drop is added or
removed and no edge changes.

Java supplies its reserved `java` package root for both hooks: the JVM
refuses to define classes under `java.*`, so `java.util.Optional.of(...)`
and `import java.util.Map` are rooted in the platform. Without the
specifier hook, the stricter import rule would have demoted the existing
`import java.util.List; List.of(...)` verdict to unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(typescript): name JavaScript host globals as the program boundary

`document.body.appendChild(...)`, `window.history.pushState(...)` and
`Buffer.from(raw).toString()` were recorded `unknown`: none of
`document`, `window`, `Buffer`, `process` or `globalThis` is in
`BUILT_INS`, and they cannot be added there because that set also
filters callee names, so `process` would drop every call to a program
function named `process`. Each drop hedged `impact()` to `lower-bound`
although no program node could have been the target.

List the host objects in `EXTERNAL_RECEIVER_NAMES` and supply it to the
TypeScript, JavaScript and Vue providers as `externalReceiverNames`. A
name counts only where the program binds nothing by it and declares
nothing by it, so a `document: Doc` parameter, an untyped `window`
parameter, a local `navigator` and a program `class Buffer` keep their
verdicts. `location`, `history`, `self` and `crypto` stay out: they are
ordinary local names.

Corpus: 5 call drops move from unknown to external. `process.env.*`
(3 reads) stays in-program there because three fixtures declare a
top-level `function process()`, one of them in a global script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(python): name standard-library imports as the program boundary

`os.environ.setdefault(...)` after `import os`, and
`Path(__file__).resolve()` after `from pathlib import Path`, were
recorded `unknown`. The imports resolve to no file in the program, but an
unresolved import alone cannot be told apart from an in-program module
the resolver missed, and nothing said that `os` and `pathlib` belong to
the interpreter.

Supply `isExternalModuleSpecifier` from a curated set of standard-library
modules, matched on the specifier's first segment (`os.path` -> `os`).
A relative specifier never matches. The classifier still requires the
import to have resolved to no program file, so a local `os.py` that the
resolver finds wins, and a parameter named `os` is the program's own.
Generic nouns an application plausibly uses for its own modules
(`types`, `queue`, `signal`, `secrets`, `string`, `email`, `http`, ...)
are left out. Third-party packages are not listed: an unresolved
`fastapi` import stays `unknown`.

Corpus: 4 call drops move from unknown to external
(`os.environ.setdefault` x3, `Path(__file__).resolve()`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): name @import("std") and @import("builtin") as the program boundary

`std.sort.pdq(...)` and `const Allocator = std.mem.Allocator;` were
recorded `unknown`: `std` is bound by `const std = @import("std")`, an
import the resolver deliberately leaves external, and nothing told the
classifier that the module is the standard library rather than an
in-program file it failed to find.

Supply `isExternalModuleSpecifier` for the two module names Zig
reserves: `std` and the compiler-generated `builtin`. Exact names only,
so `@import("std.zig")` (a file path) stays `unknown`, as does `root`
(the program's own root module) and any module `build.zig` names.

Corpus: 9 drops move from unknown to external: `std.sort.pdq` (2 calls,
2 reads), `std.mem.Allocator` (1 read), and the
`zig-qualified-literal` literals `std.Thread.Mutex{}` and
`std.mem.Allocator{...}` (2 calls, 2 reads). The last four are that
fixture's edge negative control (`useExternal` must emit no edge, which
still holds); their receiver is std, so `external` is the right origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(csharp): name the System namespace and Dictionary as the program boundary

`System.Console.WriteLine(...)` was recorded `unknown`: `Console` is in
`BUILT_INS` but the chain base is `System`, which is not, and `this.data.Keys`
with `Dictionary<string, User> data` was recorded `in-program` because
the base `this` resolves to a program class even though the member lives
on a BCL type.

Supply `externalReceiverNames` with `System` (the BCL root namespace),
`Dictionary` (the one collection capture does not reduce to its element,
because it takes two type arguments) and `string` (a keyword alias that
no program can declare). The base check covers `System.Console`; the
chain fold reaches `Dictionary<string, User>` through the program's own
field declaration. A program `class System`, a field of a program type,
and an extension method the program declares on `Dictionary` all keep
their verdicts.

Corpus: 3 call drops move from unknown to external
(`System.Console.WriteLine`). The two `this.data.Keys/Values` reads in
`csharp-nested-member-foreach` stay in-program there: the fixture's
global-namespace `App` is one of several in the merged corpus, so `this`
binds another fixture's `App` and the fold declines. Analysed alone,
both read `external`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kotlin): name java.lang.System and the platform roots as the program boundary

`System.out.println(...)` in Kotlin was recorded `unknown`: Kotlin's
`BUILT_INS` lists stdlib function names, not `System`, which Kotlin/JVM
imports by default from `java.lang`.

Supply `externalReceiverNames` with `System` and the package roots no
program can declare into (`java`, refused by the JVM; `kotlin`, reserved
by the compiler for the standard library), and `isExternalModuleSpecifier`
for imports under those roots. A program `object System` keeps the
receiver `in-program`.

Corpus: 1 call drop moves from unknown to external
(`kotlin-object-literal-scope` `System.out.println`, whose decoy
`println` on the enclosing object still gets no edge).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline receiver-origin buckets for library receivers

The preceding commits label library- and runtime-rooted drops `external`
instead of `unknown`. Call drops by origin move from 55 in-program, 44
external and 29 unknown to 55, 62 and 11. Total drops (192), call drops
(128), every per-extension and per-shape count, and every shape-arm
state are unchanged; a same-corpus A/B against main keeps all 2,838
CALLS/ACCESSES edges byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(scope): preserve library member collision hedge

* fix(scope): cover more library receiver evidence

* fix(vue): classify imported runtime receivers

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:14:28 +01:00
Abhigyan Patwari
65b101407e
fix(rust): type locals from module-qualified factory calls (#3657)
* fix(scope): follow namespace factory return types and record untyped drops

`import mod as rp; x = rp.make(); x.m()` lost the receiver type even when
`make` is declared `-> C`: `x` was bound to the raw dotted name `rp.make`,
and nothing followed a dotted name to a function's return type. The inline
`rp.make().m()` failed the same way in the compound resolver, which read
`ns.f()` only as a construction.

Resolve `ns.f` to `f`'s return annotation when `ns` is a namespace import,
the fact `propagateImportedReturnTypes` already mirrors for a named import.
The post-finalize chain follow uses it for bindings and the compound
resolver for inline calls. Only sites that emitted nothing change: the
full lang-resolution corpus emits the identical 2,838 CALLS/ACCESSES edges.

Record the drops that left `impact` at "exact":
- a plain receiver bound to a namespace function with no usable return type
  (`x = ns.untyped()`);
- an outer call whose `file:line:col` key a resolved inner site already
  claimed (`ns.untyped().m()`, `s.repo.Delete(id)`). Only a duplicate
  capture of the same member hides a drop now.

Receiver baseline: call drops 128 -> 151, reads 44 -> 48, ten shape-arm
cells INVISIBLE-GAP -> VISIBLE-GAP; every new drop has no CALLS edge to its
member (BASELINE.md lists them). The python.factory-qualified accuracy gap
is removed; the python-scope fingerprint and capture golden gain the new
fixture only.

Fixes #3498

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(dart): type locals from prefixed-import factory calls

`import 'mod.dart' as rp; final x = rp.make(); x.m()` gave `x` no type:
local call inference accepted only a bare callee (`Callee()`), and the
prefix-stripping type normalizer would have reduced `rp.make` to `make`.

Bind a local initialized by exactly `prefix.member(...)` (direct or
awaited) to the dotted callee and keep its prefix, so the shared chain
follow types it from the namespace function's return annotation. Only
call-derived bindings keep the prefix; annotated `prefix.Type` spellings
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): type locals from namespace-module factory calls

`const rp = @import("mod.zig"); const x = rp.make(); x.m()` bound `x` to
the namespace handle `rp` itself: the call-return rule reads a non-value
head as the type being constructed (`Counter.init()`), and import bindings
are not values. The member name was gone before the shared chain follow
could use the new namespace return-type lookup.

For a module-level handle on a lowercase file (a namespace module, by Zig
convention), bind the dotted callee `rp.make` so the shared chain follow
types it from `make`'s return annotation. File-struct handles
(`@import("Page.zig")`, any spelling of the handle) keep naming the
struct, so `Page.init()` is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline captures and receiver counts for Dart and Zig

- receiver-resolution: the Dart fixture's untyped factory adds one call
  drop (151 -> 152); every typed form in the Dart and Zig fixtures resolves.
- scope-capture (dart): two existing dart-method-enrichment locals
  initialized by `obj.method()` / `Type.method()` gain a call-derived
  binding, plus the new fixture's captures. A full lang-resolution A/B
  emits the identical edges outside the new fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(rust): type locals from module-qualified factory calls

`mod modx; use crate::modx as rp; let x = rp::make(); x.m()` left `x`
untyped although `make` is declared `-> C`. The call-return rule kept only
the text before the first `::` (the `Foo::new()` idiom), so `x` was bound
to the module `rp` and the function name was lost. `modx::make()` and
`crate::modx::make()` failed the same way, and the inline `rp::make().m()`
failed in the compound resolver, which cannot split a `::` path. With a
`let modx = Other;` in scope, `modx::make()` even typed `x` as `Other` and
emitted a wrong `Other.m` edge.

- Rust keeps the whole callee path when every segment is lower-case: a
  module function, by rustc's lint-enforced casing. `Foo::new()` and
  `Self::new()` still read as the type.
- New `ScopeResolver.resolveQualifiedCallee` hook. The shared chain follow,
  the compound call branch and the drop recorder hand a callee text no
  scope binds to the provider, which returns the callable it names; the
  shared side reads that callable's return annotation by definition
  identity and never splits the path. Rust answers with the module-tree
  walk behind `resolveQualifiedFreeCall`, so a call's type and its CALLS
  edge come from the same resolution.
- A receiver bound to an in-program module function with no return
  annotation (`x = rp::untyped()`) records a `receiver-unresolved` drop,
  as #3498 does for `ns.untyped()`.
- `rp::make()` did not resolve as a call either: the module-name filter
  rejected the alias `rp`, and under Cargo the import binds the module's
  `mod modx;` declaration rather than `modx.rs`. The `use` candidate now
  admits an alias that names a module and reads a module from its
  top-level declaration.

A full lang-resolution A/B against the parent emits identical CALLS and
ACCESSES edges outside the new fixture, except one new correct edge:
`rust-ambiguous` `main -> models::Handler.handle`, typed from
`let h = services::create_handler()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): rebaseline rust captures and receiver counts

- receiver-resolution: the new rust-qualified-factory-return fixture's two
  untyped sites (`x = rp::untyped(); x.m()` and inline
  `modx::untyped().m()`) are each a call and a callee-position read drop:
  call drops 152 -> 154, reads 48 -> 50, all kinds 220 -> 224. No other
  drop moves in a same-corpus A/B, and shape-arm cells are unchanged.
- scope-capture (rust): corpus growth only. emitRustScopeCaptures is
  untouched, and parking the new fixture reproduces the prior fingerprint
  byte-for-byte; it adds 131 capture groups and 3 fixture entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(scope): reuse qualified call file sets

* docs(scope): clarify finalized type refinement

* fix(rust): scope generic alias parameters

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:05:48 +00:00
Abhigyan Patwari
a09f76be39
fix(cpp): type receivers declared or produced through qualified names (#3656)
* fix(cpp): resolve qualified receiver types by namespace, not by tail

Cause: a C++ receiver declared with a qualified type (`rp::C x = ...`,
`void f(a::Config c)`, `rp::Box<int> b`) typed nothing. The capture keeps
the qualifier, but the shared receiver lookups read a qualifier only as a
`.` namespace-import handle, and C++ has neither: `#include` binds no
handle and the index keys `rp::C` under `C`. The two places that did type
qualified spellings — the `auto x = ns::T{}` brace-init rule and qualified
member fields — reduced them to their tail, which binds the NEAREST
same-named class: `auto x = rp::C{}` reached a global `C` through another
include, and `a::Config` inside a `namespace b` declaring its own Config
reached `b::Config` (both confident wrong CALLS edges).

Fix: a ScopeResolver opt-in, `resolveQualifiedTypeSpellings`, that C++
sets. When a qualified declared or return type binds nothing,
`resolveClassBindingForName` and the compound resolver's return-type lookup
fall back to `resolveQualifiedInheritanceBase` — the route qualified base
classes and qualified type references already take: the qualifier is
matched against the candidate's namespace and a tie resolves nothing. It
runs after every existing lookup and after the template-argument match, so
it adds edges and never retargets one. The brace-init rule now captures the
whole qualified_identifier, and non-generic qualified member fields keep
their qualifier, so both resolve through the same route; generic members
stay tail-reduced because specialization matching keys on the bare base
name.

Corpus A/B (lang-resolution, all languages): no edge outside the new
fixture moved; drops unchanged. bench/scope-capture cpp re-baselined (one
brace-init capture text, plus the new fixture).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cpp): bind qualified declarations that have no initializer

Cause: `rp::C x;` and `rp::C* x = nullptr;` emitted no type binding at
all. The plain-declaration annotation rules accept only a bare
`type_identifier` or `template_type`, and tree-sitter-cpp parses a
qualified type as a `qualified_identifier` wrapping one of those, so the
receiver of `x.m()` was untyped (no edge, and no drop either). The
initialized form `rp::C x = ...` was already captured by the `type: (_)`
assignment rule.

Fix: two @type-binding.annotation rules on the OUTER qualified_identifier
(plain declarator, and pointer inside an init_declarator) — the qualified
twins of the bare rules, one pattern per declarator shape like the
qualified member-field rules, with no node-type alternation. The spelling
is kept as written and resolved by the qualifier-aware route from the
previous commit.

Capture diff over the cpp corpus: 29 added @type-binding.annotation
matches on qualified locals, nothing removed or changed. Corpus A/B: no
edge or drop outside the new fixture moved. bench/scope-capture cpp
re-baselined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cpp): record return types of header function prototypes

Cause: only function DEFINITIONS (and in-class method prototypes)
recorded a return type. A free-function prototype — `D makeD();`,
`namespace rp { C make(); }` — recorded none, and the usual layout is
"header declares, .cpp defines": the .cpp is never included, so an
includer saw no return fact at all and `auto x = makeD(); x.m()` /
`makeD().m()` typed nothing (the inline form recorded a drop, the bound
form not even that).

Fix: @type-binding.return rules for a prototype with a plain or pointer
return. In 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 and type every undeclared `x` in the file, so
captures.ts keeps a prototype match only at namespace scope — the
translation unit or a namespace / extern "C" body, through preprocessor
conditionals (include guards), single-declaration extern "C" and
template prefixes. A prototype declared in a namespace keeps its binding
in that namespace instead of the module: the binding's scope is also
where its type is looked up, and from the module a global `Widget` in
the same header won over `rp::Widget`. Callers read namespace members by
definition identity, which joins in any scope, and namespace members are
never mirrored to includers by bare name anyway.

A reference return (`D& f();`) gets no rule: such a function is not
declared as a Function at all (no declaration pattern), so a return fact
would have nothing to attach to.

Capture diff over the cpp corpus: one added match outside the new
fixture (cpp-return-type's header prototype, same type as its
definition); the gate drops only the fixture's deliberate `E y();`.
Corpus A/B: no edge or drop outside the fixture moved.
bench/scope-capture cpp re-baselined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): type an inline call receiver from its callee's own return type

Cause: the compound resolver's free-call branch (`make().m()`) read the
callee's return type BY NAME from the caller's scope chain, while the
chain follow that types the bound form (`auto x = make(); x.m()`) reads
it BY DEFINITION IDENTITY (`producerTypeRef`). A callee that arrives by a
channel nothing mirrors a return binding through — a C++ using-directive
— therefore typed the bound form and dropped the inline one.

Fix: when the name read finds nothing, fall back to the return type the
callee's definition declares, via `producerTypeRef` (now exported from
imported-return-types.ts; the only change there). 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 is not evidence. Measured: without the binding check the corpus
gained 15 non-C++ edges (Go range/chain factories, Swift call results),
all reached through that workspace-name guess and one of them provably
wrong (swift-return-type's `user.save()` bound to another fixture's
User); with it, no edge outside the new fixture moves.

Corpus A/B: edges and drops outside the fixture identical to main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cpp): type receivers produced by a qualified factory callee

Cause: `auto x = rp::make(); x.m()` bound `x` to the literal type `auto`
— the bare `auto x = f()` constructor rule matches only an identifier
callee, so the generic assignment rule captured the placeholder — and
`rp::make().m()` reached the compound resolver as the free call
`rp::make`, whose name binds nothing. Neither had any route from the
qualified callee to its definition, so both typed nothing (`auto x =
rp::C()` likewise).

Fix:
- captures.ts promotes `auto x = ns::f()` / `auto x = ns::T()` (a
  qualified_identifier whose innermost name is a plain identifier) to
  @type-binding.constructor with the whole callee as the type, the
  qualified twin of the bare rule; template factories keep their own
  rule. interpret.ts drops any leftover `auto` / `decltype(auto)` binding,
  which named no type.
- New ScopeResolver hook `splitQualifiedCallee` maps a callee spelling onto
  the receiver/member pair the language's qualified CALL captures use (C++:
  innermost scope segment, `a:🅱️:make` -> `b` / `make`), so shared code
  never splits on `::`. The compound resolver's free-call branch, when the
  callee's name binds nothing, resolves the definition through the existing
  `resolveQualifiedReceiverMember` hook — the answer the call itself got —
  and reads that definition's own return type by identity
  (`producerTypeRef`). `auto x = rp::C()` resolves through the
  qualified-type route instead.

Capture diff over the cpp corpus: one changed match outside the new
fixture (cpp-method-enrichment `auto category = Animal::classify(...)`
now binds the callee instead of `auto`; a class static method, which the
namespace lookup does not answer, so it stays untyped as before). Corpus
A/B: no edge or drop outside the fixture moved. bench/scope-capture cpp
re-baselined. The new cpp-captures rows also pin the namespace-scope gate
on prototype return types from the earlier commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cpp): bind qualified receiver owners

Preserve complete namespace qualifiers and declaration-owned return facts. Resolve inline namespace and chained alias types without borrowing unrelated functions or classes; cover review collision and header-order cases.

* fix(cpp): honor qualified type visibility

Carry header alias activation positions through include chains. Refuse unresolved type-owned qualifiers without rejecting value names, and align the lexical-ownership fixture with the scope-index contract.

* style(cpp): format inline namespace lookup

* fix(cpp): preserve factory lookup position

Keep later local type declarations from hiding an earlier namespace
factory. Name overload and class-factory recall limits explicitly.

Refs #3656

* fix(cpp): expand factory return aliases

Apply the provider qualified-type hook in producer-return lookup.
Keep authoritative refusals and the original declaration context.

Refs #3656

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:21:37 +00:00
Abhigyan Patwari
caacc3bac2
fix(csharp): rank global-namespace types below the file's own namespaces (#3655)
* fix(csharp): rank global-namespace types below the file's own namespaces

C# global-namespace types were stored in the scope-independent
workspaceFqnBindings channel. lookupNameClaim returns that channel at
every scope, while namespace siblings are consulted only at the module
scope. A type name looked up inside a method therefore stopped at the
first global type with that name. The walk never reached the file's own
or same-namespace declaration. On the merged lang-resolution corpus this
caused four field-chain drops in csharp-field-types and
csharp-deep-field-chain. It also bound calls to other fixtures' global
types. For example, csharp-hello's Demo.Greeter.Main called
csharp-optional-params' Greeter.

The C# hook now stores global types in namespaceFqnBindings under the
'' key. Each file's accessibility list follows C# name lookup: the
declared namespace, the using targets of that declaration, the enclosing
namespaces, then the global namespace. The compilation unit's using
targets come after it. A using after a file-scoped namespace belongs to
that namespace. Shared walker logic is unchanged (comments only); PHP's
FQN keys still use workspaceFqnBindings.

Corpus A/B against main: the four drops resolve and no drop is added.
Wrong cross-fixture edges are retargeted or removed. One global-namespace
fixture now types `this` as its own class and reaches a duplicated global
User. Receiver bench call drops 128 -> 125, all-kind 192 -> 188
(baseline and BASELINE.md updated; the old note that these sites
declined on ambiguity was wrong).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(csharp): initialize voucher fixture payee

* test(csharp): refresh initialized payee captures

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:35:08 +00:00
Abhigyan Patwari
2b44565534
fix(resolution): correct receiver drops and property ownership (#3653)
Some checks are pending
Gitleaks / gitleaks (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
* fix(js): withdraw receiver drops for object-literal key writes a later pass answers

The JS capture makes the enclosing object literal the receiver of each key
it writes (`return { wickRatio: row.a }`). Case 0 of the receiver-bound pass
treats any receiver text holding `.` or `(` as a compound expression, and a
literal holding `row.a`, `0.5` or `...seed` does, so the fold failed and the
site was recorded as a `receiver-unresolved` drop. The return-shape and
unique-name passes run later and answered 17 of these 19 writes with an
ACCESSES edge, but nothing withdrew the earlier drop.

Both passes now report the sites they emitted an edge for, keyed by file,
position, site kind and member name (position alone is shared by `a.b` and
`a.b.c()`), and run.ts withdraws the matching `receiver-unresolved` drops
once they have run. A site they leave unanswered keeps its drop.

The other two writes, in `SignalService.make()`, had no edge at all: the
own-return-shape tier read the caller's owner qualifier as
`SignalService.make#0`, so it never matched the method's keys
(`SignalService.make.secretFlag`) and three same-named candidates were
refused. The qualifier is now the qualified name after `<Label>:<file>:`
without the `#<arity>` suffix. It also stops cutting a positioned id
(`outer.parse@25:2`) down to its column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(js): type a static field from its initializer for class-level receivers

`Config.DEFAULT.validate()` with `static DEFAULT = new Config()` produced no
CALLS edge. JS and TS keep a static field's type binding out of the class
scope on purpose: the scope has one binding map for static and instance
members, so the static binding would retype an instance field of the same
name. The class-level receiver route (`declaredTypeOfClassLevelField`) reads
the static Property node instead, but only its annotation-derived
`declaredType`, and an unannotated field has none. The type had nowhere to
live.

The field extractor now records `initializerType` for a field that declares
no type and is initialized by `new X()` / `new ns.X()` (the same two shapes
the scope queries bind), the parse worker stamps it on the Property node, and
the class-level route falls back to it after `declaredType`. The binding still
never enters the shared class-scope map, so the instance-twin hazard does not
return.

This resolves one hop: `Host.q.m()` now reaches `Alien.m`. A longer chain off
the field still goes through the compound fold, which types a field step only
from the class-scope map, so it stays unresolved. The matrix known-gap row
`static-read-without-a-twin-loses-its-type` flips to a positive row for TS,
with a JS twin added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(js): keep CommonJS module.exports declarations out of receiver drops

`module.exports.arrowRequest = (url) => fetch(url)` declares this module's
`arrowRequest` export; the CJS capture rules already bind it like
`exports.memberRequest = …`. The same line also yields a write reference
whose receiver is `module.exports`, and because that text has a dot the
receiver-bound pass folded it as a compound value, failed, and recorded the
declaration as a `receiver-unresolved` write. The bare `exports.x = …`
spelling never reached that path.

A new ScopeResolver hook, `isModuleExportAssignment`, lets a language say a
write site is a module-export declaration. The receiver-bound pass consults
it only at the drop guard, after every case has failed, so it records no
drop and changes nothing else: the site is not marked handled, no edge is
added, and the existing module-level write edge stays. The JS and TS
resolvers implement it for a receiver of exactly `module.exports` (from the
minted chain, or the text when no chain was minted) when `module` resolves
to nothing in scope. A `module` parameter, as in a UMD wrapper, or a write
into a value hanging off the export object still records its drop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ts): record an owned-`this` chain receiver as owned-but-unbound

`this.route.go()` inside an object-literal method was filed as a generic
`receiver-unresolved` drop (or, depending on site order, as nothing). The
method scope owns `this` and gives it no type, which the pass detects with
`isReceiverOwnedButUnbound`, but that check was handed the whole receiver
text `this.route`. No scope owns a chain, so the call fell through to Case 0,
whose fold cannot type an owned `this` either.

The check now receives the receiver's base (from the minted chain, else the
text before the first `.`/`(`), the same base the `suppressReceiverLookup`
check already used. The call is recorded as `receiver-owned-but-unbound`,
its real reason, and still emits no edge: the dynamic-this negative control
holds. On the fixture corpus this moves only that site; no edge changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(resolution): preserve receiver ownership

Bind a return key's declaration write by exact file and position so
unrelated input accesses keep their ordinary candidate ranking. Stop at
untyped static overrides and resolve constructor initializers as values.

Preserve the combined drop guards after rebasing onto main, cover the
binding regressions, and refresh the receiver baseline after #3654.

Key answered sites by their full range to retain repeated-name outer
reads. Keep owned-but-unbound calls in the persisted completeness signal.

* fix(resolution): address feedback (#3653)

Classify module-export declarations using captured module kind before
suppressing receiver drops. Match same-named field metadata by source
position, including declarations on one line.

Require reference kinds in suppression assertions and guard temporary
fixture cleanup. Cover the reported failures with regression tests.

* perf(parser): index repeated field declarations

Sort private field buckets once and use binary search to avoid
quadratic scans while preserving declaration disambiguation.

* fix(ingestion): preserve capture metadata

Record module kind from the parsed tree on worker and main-thread paths.
Match Ruby field metadata at the accessor call instead of its argument.

* fix(ingestion): harden capture metadata

Keep Ruby reader/writer properties writable. Walk module-kind ASTs iteratively so deep expressions cannot abort extraction.

* test(ingestion): clarify worker metadata scope

* docs(ingestion): state capture metadata effects

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:59:59 +01:00
Abhigyan Patwari
f6e42092b3
fix(zig): resolve import-alias, forwarding-constructor, anonymous-container and type-application receivers (#3652)
* fix(zig): bind the module of a one-member @import alias as a receiver

`const X = @import("f.zig").X;` reads `X` off an `@import(...)` receiver.
The named-import rule claims that builtin, so the inline-import branch
skipped it and never emitted the receiver-keyed namespace binding the deep
form (`@import("lib.zig").B.work`) and member-call receivers already get.
Every such read was recorded as a receiver-unresolved drop; the ones whose
alias shares the member's name only produced an edge through the lexical
read fallback, and a renaming alias (`const Gauge = ...DEFAULT_NS;`)
produced nothing.

`isZigInlineImportReceiver` now also accepts the value read of a keyword
one-member import declaration (never a type annotation), and a claimed
builtin that is a receiver gets the namespace binding instead of being
dropped. The named import itself is unchanged, so `Gauge` stays an import
binding. Clears the 9 F-A rows of the #3498 drop triage; the reads now
resolve through the written module (`import-resolved`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): type a forwarding type constructor as the type it forwards to

`fn Bridge(comptime T: type) type { return Builder(T); }` (Lightpanda's
`js.Bridge(T)`) names no container of its own. `zigReturnTypeIsNominal`
drops `type` returns and `zigTypeConstructorOf` only names a container
returned directly, so a receiver typed `Bridge(X)` reached a Function that
owns no members and every call on it was lost.

A type constructor whose body is a single `return <TitleCase call>` (and
whose callee is not one of its own parameters) now binds `Bridge ↦ Builder`
through its return-type binding. The binding is declared in Bridge's own
scope, so a chain followed from an importer still looks `Builder` up where
it is visible. The new aliases.zig `b12` case (a named import of the
forwarding constructor, bound and inline spellings) resolves into
generic.zig's Builder. The namespace-qualified spelling of the #3498 F-B
rows (`generic.Bridge(Local)`) additionally needs the `ns.f` return-type
fold, which this binding feeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): resolve a member read off an anonymous container in place

`struct { fn lessThan ... }.lessThan` (the comparator handed to
`std.sort.pdq`) and `const byteSize = struct { fn it ... }.it;` read a fn
value off an anonymous container. The receiver capture was the container's
whole body, which names nothing, so each read was a receiver-unresolved
drop and the qualified-argument value reference declined. The only edge
came from the lexical read fallback at the site's anchor, which starts at
`struct` and therefore inside the container's own Class scope: the
container "accessed" its own fn.

A reference group whose receiver is a container node now carries the
identity that container is minted under (`Sorter.sortBoth$1`, via
`zigContainerName`), and its site anchor moves to the member, which is
evaluated in the enclosing scope. The reads resolve as ACCESSES from the
fn that evaluates them (and, for the pdq argument, a value-ref USES); no
CALLS edge. Clears the 3 F-C rows of the #3498 drop triage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): type a type-constructor call used as a receiver

`Stack(u8).init()` calls `init` on the TYPE `Stack(u8)`. The free-call
branch of `resolveCompoundReceiverClass` found `fn Stack`, found no return
binding (it returns `type`), and fell into the construction path, which
declines for a language without `constructionSyntax`. The call and its
callee-position read were dropped, while the bound form
`var t = Stack(u8).init(); t.top()` resolved.

Add a language-neutral `ScopeResolver.isTypeApplicationCall(callee)` hook.
When it answers true, the construction path types the receiver as the
class the callee names (direct or namespace-qualified), exactly as a bare
construction does; the callee's return-type binding still wins. Zig answers
with `isZigTypeConstructorCallee`, the text twin of
`isZigTypeConstructorCall` (a TitleCase last identifier). Clears the
2 F-D rows of the #3498 drop triage (main.zig:42 call + read).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): re-baseline receiver resolution for the Zig receiver fixes

The four Zig fixes clear 14 receiver-unresolved sites in the corpus:
all-kind drops 192 -> 178, reads 44 -> 31, calls 128 -> 127 (Zig 11 -> 10,
no-chain 27 -> 26, in-program 55 -> 54). Nine one-member `@import(...).X`
reads, three anonymous-container reads and the `Stack(u8).init()` call and
read now resolve. Writes and every shape-arm state are unchanged; no new
drop was introduced. BASELINE.md records the per-site breakdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): keep type applications lexical

* fix(zig): bind typed inline imports

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:24:26 +00:00
Abhigyan Patwari
2b6542737a
fix(go): resolve imports within selected modules (#3654)
* fix(go): resolve imports against every go.mod in the repository

The Go resolver read only the repository-root go.mod, and its import
target ignored the importing file. With no root go.mod, every import took
the GOPATH suffix match down to a two-segment tail. Two modules'
`internal/repository` then both answered `.../internal/repository`, and a
module whose directory differs from its path (`/vN`, `services/orders`
for `github.com/acme/orders`) resolved to nothing. With a root go.mod,
imports of a nested module were not the root's and dropped, while the
root module's own packages matched any directory ending in the package
path, including nested modules' trees.

Capture every go.mod and go.work from the scan's path list, as Dart does
for pubspec.yaml; standalone and rename callers rescan with the same
walker. For each import, the module whose path is the longest
`/`-prefix owns it. The package is the exact directory under that
module's root, unless a nested go.mod removes the directory from the
module. Several go.mod files declaring one path are disambiguated by the
importer's own module, then by its go.work `use` list, else left
unresolved. Unowned imports from inside a module stay unresolved; the
GOPATH suffix match remains only for importers outside every module.

In the combined lang-resolution corpus, no CALLS or ACCESSES edge is
removed or retargeted against the parent commit, and 70 are added outside
the two new fixtures. Drops are unchanged at 179: these sites were
silent. The import-target bench's go arm now passes no module config,
and its answers and fingerprint are unchanged. The scope-capture go
fingerprint moves for the new fixtures only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(go): look up an importer's module only when a path is contested

`locateGoImport` walked to the importer's nearest `go.mod` and `go.work` for
every import, though most imports (standard library, dependencies) are owned
by no in-repo module and need neither, and every candidate package directory
repeated the same ownership walk. The walks are now lazy (only a module path
several `go.mod` files declare consults them) and memoized per directory on
the config's index, and a path with no backslash skips the normalizing
replace. The Go package index is fetched once per import instead of per
candidate directory.

Behavior-preserving: the lang-resolution corpus emits the identical 3,002
CALLS/ACCESSES edges before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(go): surface go.mod read failures instead of dropping the module

Go's workspace capture skipped any manifest it could not read and turned a
failed standalone scan into "no modules". Either one silently erases a module
boundary: its imports then resolve against the enclosing module or fall back
to GOPATH suffix matching, the misresolution this capture exists to remove,
and a rename refresh compares a different config against its preview's.
`captureResolutionConfig` requires capture failures to throw.

- A manifest that fails to read now throws; the standalone scan propagates
  its error.
- Manifests skipped on purpose stay skipped, named in one place: symlinks,
  which can point outside the repository (Dart's manifest capture excludes
  them too), non-files, files over the source-file size cap, and files gone
  since the scan listed them.
- Manifests are read 32 at a time; capture blocks the scan, and k8s-style
  trees have hundreds of go.mod files.

Tests: data-driven rows for each skipped shape, a go.mod that fails to read,
and a standalone scan of a non-directory root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): re-measure Go counts and fingerprint on top of #3651

Rebased onto main after #3651. Import resolution across every go.mod clears
the 13 Go call drops left in the combined corpus (go-chain-call,
go-grouped-type-decl, go-interface-field-dispatch,
go-pointer-receiver-field-chain): call drops 148 -> 135, all kinds 208 -> 195,
.go call drops 14 -> 1. No drop added; shape-arm states unchanged.

The scope-capture go fingerprint moves for the two new fixtures only: parking
them restores main's acee4449f691d9138e68e7b2b19da863fc2c89b0e9846acdf10e93d76b2eec78
byte-for-byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(go): read go.mod through one handle so it cannot change mid-check

CodeQL flagged the manifest read as a file-system race: `lstat` checked the
path and `readFile` then opened it again, so a different file could be read
than the one checked. The manifest is now opened once (O_NOFOLLOW refuses a
leaf symlink, O_NONBLOCK keeps a FIFO from hanging) and its type, size and
contents all come from that handle, the way Dart's manifest reader works. A
platform without O_NOFOLLOW still refuses a link after the open.

The read-permission test is POSIX-only: permission bits do not block reads
on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(go): skip the unreadable-go.mod test as root; correct a fixture comment

Review follow-up for #3654.

- The permission test asserted EACCES on a mode-000 go.mod, which root can
  still open. It now skips for root as well as on Windows.
- The orders-service ledger fixture said it shares its method names with
  billing's; only Save is shared. The comment says so, in the same two
  lines. Comment text is part of the Go captures, so the golden gains the
  new text and the scope-capture go fingerprint moves:
  3ba3b6e1551311ddb0e9330c939ac995856b487303b6f64bc89da3689a6d9d2c ->
  b1dfa8e1a8d92fd9d5779a02bc4bd99f54ec87adae4a326be08b74cb48fd5ef3, with
  fixture and group counts unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(go): enforce module visibility (#3654)

Limit local providers to the importing module and peers selected by its
nearest go.work. Treat failed package-qualified receiver lookup as final
for direct calls, field chains, and method values so bare-name fallback
cannot recreate calls into independent modules.

Add import/call boundary regressions and remeasure receiver drops. The
same-corpus comparison removes exactly two false calls and adds no edges.

Validation: 7,263 resolver tests, both typechecks, lint, and import-target,
scope-capture, and receiver baseline checks pass. Two parser-identity
checks affected by local snapshot/build drift pass after freezing inputs
and rebuilding. The full local run continues; final CI remains required.

* fix(go): reject ambiguous package providers (#3654)

Check all selected module prefixes for package presence. Preserve a shorter-only provider and refuse imports supplied by multiple modules. Cover all four presence cases and correct the receiver suppression comment.

Note: the full local suite hit a host GLIBCXX_3.4.31 failure in parse-cache-identity-package.test.ts; focused Go tests and both TypeScript checks pass.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:24:18 +00:00
Abhigyan Patwari
ab0b03846d
fix(go): type receivers through package-qualified declared types (#3651)
`normalizeGoTypeName` cuts the package qualifier from field, parameter and
local types, so `*models.User` is stored as `User` and the bare lookup either
refuses the name or binds the wrong package once two packages declare a
`User`. A new optional `ScopeResolver.qualifiedTypeLookupSpelling` hook
recovers `models.User` from the written spelling; receiver typing (compound
receivers, simple bindings and callable-value-flow method values) tries it
first, grounded on the qualifier's import with the unique-name fallback off,
and falls back to the bare lookup on any miss. The qualifier is read from the
module scope so `vault *vault.Vault` cannot shadow its import, and a chain
step keeps a binding's own written spelling so a binding whose type the step
replaced never borrows another binding's package.

Corpus A/B against main: no edge removed or retargeted outside the new
fixture; 19 Go edges added and 13 Go drops cleared. Receiver bench: call drops
153 -> 148, all kinds 221 -> 208.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:37:02 +00:00
Abhigyan Patwari
278863068f
fix(scope): follow namespace factory return types and record untyped drops (#3648)
`ns.make()` now takes `make`'s return annotation from the namespace's module,
in both the bound (`x = rp.make()`) and inline (`rp.make().m()`) forms, for
every language whose provider records return annotations; Dart binds its
prefixed-call locals so the same lookup applies. The lookup fails closed
whenever it cannot prove the fact: the return must be anchored on a
module-level Function of that name (TS/Dart hoist method returns, Go binds
package-scope methods), the namespace must bind the module it targets
(`import pkg.sub`), and a dotted return is not followed further. The
namespace-name set is built from finalized import edges, never a
`scopeTree.byId` walk, so it is safe under GITNEXUS_DISK_SCOPE_INDEX=1.

Calls the analyzer cannot type are recorded as receiver-unresolved drops
instead of exact misses: a receiver bound to an untyped namespace call, and
an outer call that shares its start position with an inner one (site
identity is now `receiver.member`).

Corpus A/B against main: identical CALLS/ACCESSES edges outside the new
fixtures. Receiver bench: call drops 128 -> 153, reads 44 -> 48.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:24:26 +00:00
Abhigyan Patwari
9df1e98350
fix(routes): scope inline Express handlers and report api_impact lower bound (#3643)
* fix(routes): scope inline Express handlers and report api_impact lower bound

Inline Express handlers are anonymous Function nodes named by position, but
the route facts only recorded a handler name for bare identifiers. Routes
with inline handlers therefore had no handlerSymbolId: every route in a file
got the union of its handlers' response keys and every flow starting there.

Record the positional name for inline handlers, read response keys from the
resolved handler when a file registers several routes, and let processes
link exactly by handlerSymbolId as they already do for named handlers.

Count, per route, the files that pass its URL to a callee not proven to be
a fetch wrapper (excluding proven consumers and the route's own file). The
count is stored as Route.unverifiedConsumers; api_impact reports
epistemic: 'lower-bound' with that count when it is non-zero. Older indexes
without the column fall back to the previous query.

Closes #3488

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(routes): match unverified consumers once per URL

Memoize route matching per distinct fetch URL, skip the proven-consumer
scan when no unproven call matches a route, and build the Route query's
optional columns from a list instead of two booleans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(group): keep inline handlers attributed to their registrar

Inline Express handlers now carry their positional Function name in the
route facts so ingestion can resolve a handlerSymbolId. Group HTTP
contracts read the same facts, but that name is a graph identity rather
than a symbol name the contract lookup can match, so the provider lost its
enclosing-registrar attribution. Ignore the name for inline handlers there.

Rebaseline the two bench guards the branch moved:
- emit-persistence: route.csv gains the header-only unverifiedConsumers
  field; stripping it restores the prior fingerprint exactly.
- receiver-resolution: the new fixture's req.query.path read on an untyped
  inline-handler parameter adds one receiver-unresolved read (191 -> 192).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-10-20 17:44:08 +01:00
Gergő Magyar
01af833875
fix(scope): preserve lexical bindings in HTTP wrapper detection (#3632)
* docs(scope): clarify lexical binding ownership and wrapper provenance

Checkpoint for #3630. Publishing the implemented source, regression tests, and benchmark harness as a draft PR; full-suite and final scaling validation remain in progress.

* fix(scope): publish #3630 docs/plans

* fix(scope): publish #3630 gitnexus/bench/lexical-binding-provenance

* fix(scope): publish #3630 gitnexus/bench/lexical-binding-provenance/fixtures

* fix(scope): publish #3630 gitnexus/bench/lexical-binding-provenance/results

* fix(scope): publish #3630 gitnexus/src/core/ingestion

* fix(scope): publish #3630 gitnexus/src/core/ingestion/languages/typescript

* fix(scope): publish #3630 gitnexus/src/core/ingestion/languages

* fix(scope): publish #3630 gitnexus/src/core/ingestion/pipeline-phases

* fix(scope): publish #3630 gitnexus/src/core/ingestion/route-extractors

* fix(scope): publish #3630 gitnexus/src/core/ingestion/workers

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/packages/http

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/packages/http/src

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web

* fix(scope): publish #3630 gitnexus/test/integration/resolvers

* fix(scope): publish #3630 gitnexus/test/unit

* fix(scope): publish #3630 gitnexus/test/unit/group

* fix(scope): publish #3630 gitnexus/test/unit/scope-resolution

* fix(scope): publish #3630 gitnexus/test/unit/scope-resolution/typescript

* fix(scope): publish #3630 gitnexus-shared/src/scope-resolution

* fix(scope): treat wrapped assignment targets as writes

`(app) = new Map()`, `(app as T) = x` and `app! = x` assign the same binding as
`app = x`, but route-fact invalidation and the shared pattern walker stopped at
the wrapper, so Express provenance survived the write and produced a false
route. Recurse through parenthesized, as/satisfies and non-null targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): resolve wrappers through overloads, declarator lists and barrels

Three wrapper shapes lost their identity:

- Bodyless overload signatures were indexed as separate definitions sharing
  the implementation's binding, so the binding looked ambiguous and both local
  and imported calls were rejected. Signatures now declare no callable.
- Facts for `const base = 0, request = (url) => fetch(url)` followed the first
  declarator. The captured name now selects its own declarator.
- An import resolved to a workspace barrel stopped at index.ts. Workers now
  record named, star and imported-then-exported re-exports, and resolution
  follows them to the declaring module: local and named exports shadow
  `export *`, star exports never carry `default`, and conflicting star exports
  block the name. No repository-wide name fallback is restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(scope): assert closure ownership against the declaration

The closure-write assertion compared one lookup with itself, which also passes
when both are undefined. Compare it with the outer declaration and the shadowing
parameter, and keep the memoization check explicit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(routes): share lexical helpers and release per-file HTTP contexts

- Share binding keys, parameter patterns and transparent expression wrappers
  from lexical-bindings instead of four local copies; drop the unused
  moduleStem export; name the inferred-source predicates in lookup.
- Key the per-file HTTP context by its tree rather than its ParsedFile.
  Workers retain ParsedFiles for the whole chunk, which kept every file's AST
  and traversal maps alive until the chunk flushed.
- Index re-exports by name, memoize barrel walks per starting export, and keep
  facts without import targets as-is instead of copying them.
- Move TS-only wrapper cases into the TS describe.each instead of branching
  inside the per-language loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): keep wrapper identity for HOCs, CommonJS, object keys and fetch packages

Four definition shapes that main linked by name lost their wrapper identity:

- `const load = useCallback((url) => fetch(url))` and curried HOCs produced no
  facts because definition lookup stopped at the call; it now follows the
  callable argument, as the structural definition capture does.
- `module.exports = { request }`, `exports.x = …` and `module.exports.x = …`
  were not exports, so destructured `require` callers missed the wrapper.
  CommonJS export assignments are recorded when `module`/`exports` are the
  unshadowed globals, including functions defined inline in the export object.
- An object-literal key or method name was resolved as a lexical binding and
  collided with a same-named module wrapper. Only binding identifiers now name
  a definition's binding.
- `import fetch from 'node-fetch'` no longer counted as fetch. Imports of known
  fetch implementations (node-fetch, cross-fetch, isomorphic-fetch, the fetch
  member of undici and whatwg-fetch) are fetch evidence again; parameters and
  other local names still are not.

Reference identifiers are also resolved only when their name is bound to a
callable or class region; other identifiers cannot add a reachability edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(routes): drive the forwarding fallback case from data and cover re-export merges

Replace the boolean-switched forwarding fallback case with table rows instead
of branching inside the test, and assert that barrel re-export facts survive
worker sub-batch merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): reject stale or dirty call-graph exports

The committed comparison named a candidate revision that is not on this branch,
and nothing stopped it. Comparison now requires both exports to come from clean
checkouts and the candidate export to come from the checkout running the
comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate the call-graph comparison on the reviewed head

Clean baseline 565e50276 and clean candidate 8a5f7b738 on the frozen 432-file
corpus (fingerprint 88be1faf...): exactly one CALLS target changes, the
annotated closure probe `declaredOwner.writeAndRead` now calls `Base.m`
instead of `Foo.m`. The other 431 CALLS and all 11 METHOD_OVERRIDES edges are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): emit export provenance and review notes from the comparison

The comparison report carried export provenance and the expected-change
explanation only as hand-added fields, which a regeneration dropped. Emit
both exports' revisions, dirty flags and fingerprints, the unchanged CALLS
and METHOD_OVERRIDES counts, and an optional `--note` explanation directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): record calibrated scaling reports and the regenerated comparison

Duration-calibrated-v2 runs (5 warmups, 15 measured batches, 500 ms pilot
target) pinned to the same CPU, baseline then candidate back to back:

- Baseline 565e50276 exceeds the 1.8 normalized ceiling on six arms at 4x
  input: position-index 7.92, position-deep-completed 4.65, nested-helpers
  4.82, nested-helper-chain 14.05, reverse-chain 4.31, name-collisions 2.61
  (medians).
- Candidate 94f1a055b passes --check: every arm stays at or below 1.32 at 4x
  for both minimum and median, with correct semantic guards on every arm.

The comparison is regenerated from clean exports with its provenance and
review note emitted by the tool: one expected CALLS target change, 431 CALLS
and 11 METHOD_OVERRIDES unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(scope): replace recursion with explicit stacks in new traversals

The binding work added five recursive traversals whose depth follows input
shape: region summaries (callable nesting), barrel resolution (re-export
chain length), pattern binding names (destructuring depth), route-fact write
invalidation (pattern depth), and the position index's max-end tree. Each
now uses an explicit stack with the same visiting order and results.

Regression cases cover the inputs the recursive versions could not handle:
a 20,000-deep destructuring pattern and a 50,000-module re-export chain both
threw RangeError before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(routes): avoid a full-tree script walk when building lexical bindings

Route-fact extraction ran 1.78x slower than main on a 512-function file
(87 ms vs 49 ms) because createLexicalBindings walked every AST node in script
to detect collapsed parameter/body environments, which only functions with
parameter defaults or computed keys can have.

- Find functions with the native type search, skip plain parameter lists
  without any subtree search, and walk only the formals and bodies of the
  rest. Detection is unchanged, except that functions inside syntax-error
  regions are now also checked, which can only add fail-closed ambiguity.
- Read each node's id and start position once per lookup instead of
  repeatedly crossing into native getters.

The same extraction now takes about 56 ms against 51 ms on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(routes): walk wrapper evidence with a tree cursor

The wrapper context visited every node through namedChildren, which
materializes each child as a SyntaxNode and records a parent edge by node
id. A tree cursor reads node types and offsets natively, so only the node
types the handlers inspect become SyntaxNodes. Declared identifiers are
keyed by start offset, and the walk records the ancestor facts later
queries need (stored binding, default export, declarator of a name)
instead of a parent map.

On the 512-wrapper scaling input, context construction drops from about
58 ms to 48 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate the call-graph comparison on the cursor walk

The candidate export at 6061ca71b has the same fingerprint as the previous
candidate: the traversal rewrite changes no edge. The one expected target
change and the 431 unchanged CALLS and 11 METHOD_OVERRIDES remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): record final scaling reports on the cursor walk

Baseline 565e50276 and candidate 61de364ff ran back to back on CPU 23
with the duration-calibrated-v2 protocol. Every candidate check stays at
or below a 1.32 normalized factor against the 1.8 ceiling; the baseline
fails six of nine arms. Workload fingerprints agree except the documented
name-collision false positive in the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): checkpoint the final review, scaling and suite results for #3630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): treat export aliases of one wrapper as one star-export target

Star-export candidates were deduplicated by the resolved export name, so
`export { request, request as send }` reached through two barrels counted
as two targets and blocked the import, dropping the consumer's FETCHES
edge. Candidates are now keyed by the declaration each export name names,
matching ECMAScript ResolveExport's binding comparison; distinct
declarations exported under the same name stay ambiguous.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate scaling and call-graph reports on the alias fix

Baseline 565e50276 and candidate 4d62bd045 ran back to back on CPU 23.
Every candidate check stays at or below 1.22 against the 1.8 ceiling;
the baseline still fails six of nine arms. The CALLS/METHOD_OVERRIDES
export is unchanged (fingerprint 4db24f57), so the comparison only
records the new candidate revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): record the export-alias fix and the regenerated reports for #3630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): keep the type a write gives an undeclared name

A constructor write to a name no scope declares (`client = new ApiClient()`
in sloppy JavaScript) declares nothing, so the lookup stays absent. The
carried constructor fact was dropped on that exit, which lost the receiver
type main had inferred for later reads in the writing scope. The absent
result now carries it, and findReceiverTypeBinding uses it before the
namespace fallbacks. Only complete-claim providers carry such a fact.

Tests pin the carried type per inferred source, the undeclared write, and
preservePosition across a module redirect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): close the wrapper identity gaps found in review

- CommonJS wrappers captured as their assignment (`exports.x = function`,
  `module.exports.x = () =>`) produce facts again: findFunction follows the
  assignment's right side.
- Name-only fallback considers only definitions that can be the target: an
  unresolvable import names a module-level export, an undeclared global a
  top-level function. Nested and unexported helpers no longer erase a
  wrapper's edges.
- Only possible wrappers and module-level functions are emitted, and the
  routes phase builds the index only when routes and candidate calls exist.
- A rebound binding (later assignment, `var` initializer over a function
  declaration, reassigned export) proves no wrapper across files.
- `(<T>x) = ...` is a write; transparent wrappers skip type arguments.
- Calls inside recovered syntax are still collected; instance-field
  initializers resolve after the module body, so a later declaration is
  visible.
- Local exports are always recorded, so a star export conflicting with a
  non-callable export is ambiguous, and fully resolved barrel frames are
  shared across walks.
- HTTP hooks run per file under a guard; a failing file or one without a
  ParsedFile still records its function names so fallback declines.
- namesFetch is iterative, large patterns no longer spread into push, the
  facts cache is keyed by the name node, and the provider context cache
  checks its ParsedFile.

Tests cover each case; the behavior rows fail on the previous source.
The TS-only call cases no longer branch inside the test file, call targets
are asserted exactly, the function-type list is drift-checked, and the
persistence test joins the cross-platform worker list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(routes): describe wrapper fallback, barrel and recall boundaries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): count the new CommonJS fixture's dropped write site

`lib/cjs-member.js` in the fetch-wrapper target-identity fixture writes
`module.exports.arrowRequest`, a member write whose receiver is not typed.
That adds one `receiver-unresolved` write (190 -> 191 all-kind drops,
19 -> 20 writes). Call drops and shape-arm states are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate scaling and call-graph reports on the review fixes

Baseline 565e50276 and candidate 528ea1478 ran back to back on CPU 23.
Every candidate check stays at or below 1.27 against the 1.8 ceiling; the
baseline still fails six of nine arms. The CALLS/METHOD_OVERRIDES export
is unchanged (fingerprint 4db24f57), so the comparison only records the
new candidate revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): checkpoint the review fixes and regenerated reports for #3630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(routes): share CommonJS checks and trim per-node work

- `isCommonJsGlobal`/`isModuleExports` in lexical-bindings replace the two
  copies in fetch-wrapper and framework-route-facts (both resolve the same
  scope from the same start position).
- LEXICAL_FUNCTION_TYPES is derived from the TypeScript FUNCTION_NODE_TYPES
  instead of a third hand-kept copy; the drift test still pins it.
- `unwrap` reads UNWRAPPED_TYPES, the set the cursor walk mirrors, and the
  class-body region is no longer shadowed under the name `deferred`.
- framework-route-facts reads each node's type once per loop iteration.
- The position index returns the latest-starting scope when it already
  contains the point, before the max-end tree search; that entry is the
  search's rightmost match.
- carried() returns a shared empty set for argument forms that cannot carry
  a value, the resolution file set is built without intermediate arrays,
  and the routes phase reads the call list once.

No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate scaling and call-graph reports on the refactor

Baseline 565e50276 and candidate 3db8657ba ran back to back on CPU 23.
Every candidate check stays at or below 1.32 against the 1.8 ceiling; the
baseline still fails six of nine arms. The CALLS/METHOD_OVERRIDES export
is unchanged (fingerprint 4db24f57). The later main merge (e2a92dab8)
touches no measured file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): checkpoint the simplification pass and its reports for #3630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address PR review feedback (#3632)

- Destructuring defaults (`{ fetch = globalThis.fetch } = {}`, renamed,
  array and local forms) are alias evidence for their leaf binding and
  no longer count a second initialization, which also invalidated
  bindings like `[send = other] = values`.
- `export default (request)` records the default export: the value is
  unwrapped before the identifier check.
- The HTTP fallback records and the collapsed-environment test row now
  say what they stand for.

Regression rows cover each form in JS, TS and TSX and fail on the
previous source; two fixture consumers cover imported wrappers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): refresh lexical binding reports after review fixes

Regenerate clean baseline and candidate measurements at c263c4f72.
All candidate scaling checks pass, the semantic delta remains one CALLS
target, and receiver-resolution counts match the existing baseline.

* docs(plan): checkpoint refreshed review-fix measurements

Record c263c4f72 review fixes, the regenerated scaling and semantic
results, and the passing receiver and CI checks.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:36:06 +01:00
Gergő Magyar
8fa8e64388
fix(rename): extend safe refactoring and flow coverage across languages (#3557) 2026-10-13 08:38:17 +03:00
Gergő Magyar
c744ce1dfd
fix(python): resolve mixin calls with CPython C3 order (#3393)
* fix(python): resolve mixin calls with CPython C3 order

Breadth-first MRO bound a diamond mixin call to the wrong base, and dropping the site left the real method out of the graph. Use C3 and take the first compatible method in that order.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(python): correct mixin receiver baseline counts

* fix(python): guard incomplete mixin inheritance

* fix(python): record unresolved MRO tail coverage (#3393)

Track a missing subtype target when the last indexed MRO owner has an unindexed parent, and cover the case with an integration test. Correct the C3 fixture description.

Note: local full npm test timed out amid parse-worker startup failures; focused tests and benchmark baseline passed.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-28 08:55:13 +00:00
EVA
f6e70016d6
fix(python): resolve mixin self calls to subtype implementations (#3390)
* fix(python): resolve missing mixin self members through subtypes

* fix: honor Python effective MRO and static mixin targets

* fix: bind Python subtype dispatch to receiver provenance

* fix(python): limit mixin fanout to instance receivers

* fix(python): capture call arity and invalidate stale parsed facts

* fix(python): count bound receivers by method context

Preserve static, free, nested and typed variadic parameters; test renamed target receivers without weakening incompatible-arity rejection. Regenerate capture goldens for receiver metadata and eight mixin fixtures. Record the deliberate missing_target coverage outcome: CI measured Python call drops 5->6 and total call drops 113->114; no shape or scaling threshold relaxed.

* test(python): require a call capture before checking unknown arity

* fix(python): bind subtype dispatch to receiver definition

* test(python): include conditional renamed-receiver target

* fix(python): prove positional mixin targets and report partial coverage

Preserve upstream notebook coordinate mapping and maintainer changes. Reject incompatible/implicit-class targets, retain proven targets across ambiguous alternatives, and report capped or unresolved coverage without confusing edge deduplication.

* fix(python): isolate subtype call proof and preserve lookup boundaries

---------

Co-authored-by: Eva <eva@100yen.org>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-28 09:42:56 +05:30
Gergő Magyar
997fc05b83
fix(resolution): resolve calls through a generic-typed field receiver in every language (#2833) (#2855)
* test(resolution): pin generic-typed field receivers across languages (#2833)

A field whose declared type carries a type argument (`repo: Repo<User>`)
emits zero CALLS edges — not a truncated chain, not an edge to the
interface declaration, nothing. This adds the cross-language matrix that
measures it, modelled on the #2807 inferred-field matrix: every language
runs the same two calls, one through a generic-typed field and one
through a non-generic control field, and each language is compared
against its OWN control row rather than an absolute edge count.

Measured state, pinned here as `known-gap` so the file is green on main
and flipping a row is a visible edit:

  affected    TypeScript, C#, C++, Python
  unaffected  Java, Kotlin, Go, Rust, Swift, Dart

The unaffected six erase type arguments at interpret time (Java's
`stripGeneric`, F41 #1928; Swift likewise). TypeScript, C# and Python
instead run a container ALLOW-LIST that returns the type ARGUMENT, so a
user-defined `Repo<User>` survives verbatim into a lookup that binds
nothing.

The `ts-local-vs-field` case is the bug in one file: `viaLocal` and
`viaParam` both resolve for the identical type, and only `viaField`
loses every edge — a bare name reaches Case 4 and its generic-aware
lookup, a dotted field receiver does not.

Negative controls pin what erasure must NOT do: an unbounded type
parameter denotes no declaration, and a C++ explicit specialization is a
different class from its primary template. The `Box2<T>` row pins a
PRE-EXISTING false edge (a workspace class named `T`) so it cannot later
be mistaken for fallout from this work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* refactor(resolution): move resolveClassBindingForName to the shared walkers (#2833)

Pure relocation, no behaviour change: the generic-aware class lookup
moves from `passes/receiver-bound-calls.ts` to `scope/walkers.ts`, beside
the bare `findClassBindingInScope` it wraps. Its two existing callers —
`classifyReceiverOrigin` and Case 4 — import it from the new home and are
otherwise untouched.

The move is required rather than cosmetic: `receiver-bound-calls.ts`
already imports from `compound-receiver.ts`, so having the compound
receiver call into the pass would close an import cycle. `walkers.ts` is
the shared floor both already depend on.

Verified behaviour-neutral: the #2833 matrix is 44/44 identical before
and after, across all fifteen fixtures.

detect_changes attributes `resolveInheritanceBaseInScope`,
`resolveQualifiedInheritanceBase` and `EMPTY_BINDINGS` to this commit;
those are line-shift artifacts of inserting a function above them, and
their bodies are byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(resolution): type generic field receivers through the generic-aware lookup (#2833)

A field receiver is spelled `this.repo` — dotted — so it types through
the receiver-chain fold and the text cascade, both of which reach
`findClassBindingInScope`. That function has no notion of type arguments,
so a field declared `Repo<User>` resolved to nothing and the call site
emitted NO edge at all: not the interface declaration, not the
implementation fan-out, nothing. A local or parameter of the identical
type is a bare name, reaches Case 4 and its generic-aware
`resolveClassBindingForName`, and resolved fine. The bug was the
asymmetry, not the generics.

Three receiver-typing lookups now call the generic-aware helper instead:
`typeOfMemberOnClass`'s primary and module-hoist branches, and the
cascade's bare-identifier type-binding read. Every other one of the 38
`findClassBindingInScope` call sites is untouched — its own docstring
records that widening it globally suppresses the `?? otherResolver(...)`
fallbacks two dozen callers rely on, which would retarget inheritance
edges, and impact rates it CRITICAL with 12 direct dependents.

Order matters and is preserved: the helper tries the exact name, then an
arity- and token-exact match against `def.templateArguments`, and only
then falls back to the base name. Erasing first would collapse a C++
explicit specialization onto its primary template — `Vec<bool>` really is
a different class. A bare type parameter carries no type arguments, so it
never enters the generic branch and cannot be erased into a class that
happens to share its name.

Measured: TypeScript and C# generic-typed fields now emit exactly what
their non-generic control rows emit, primary plus interface-dispatch
fan-out. Java, Kotlin, Go, Rust, Swift and Dart are byte-identical. Both
type-parameter negative controls are unchanged. C++ and Python are still
open and stay pinned as known-gaps — they fail for different reasons and
get their own commits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(cpp,python): bind generic-typed member fields so their calls resolve (#2833)

Completes #2833 for the two languages the shared resolution change could
not reach. Each failed for its own reason, and both were found by
measurement rather than assumed.

C++ — a CAPTURE gap, not a resolution one. All three `field_declaration`
type-binding rules required `type: (type_identifier)`, so a member
declared `Repo<User> repo;` is a `template_type` and matched none of
them: the field got no type binding at all, and every call through it
lost its edge in both the bare and `this->` spellings. A LOCAL of the
identical type resolved the whole time, because the local declaration
rules gained their `template_type` variant long ago. Three mirrored
rules close it, one per declarator shape (plain, pointer, reference).
Written as separate patterns rather than one alternation: a node-type
alternation in a field position is a tree-sitter 0.21 hazard this repo
has been bitten by before.

Python — the bracket spelling never entered the generic branch. Its
`stripGeneric` is a container allow-list over `[...]` that returns the
type ARGUMENT (`list[User]` to `User`), so a user-defined `Repo[User]`
matched nothing and survived verbatim, and the shared lookup's generic
branch is gated on `<`. It now reduces a subscripted type neither
allow-list claims to its base name — the same rule Java and Swift
already apply to `<...>`. Deliberately the LAST resort: a container must
reach its own rule first, or `list[User]` would type the receiver as the
container and retarget every call in a for-loop chain. The as-written
spelling survives on `TypeRef.declaredSpelling`, which is what the fold's
index step reads.

Both are parse-time and land in the cached ParsedFile, so SCHEMA_BUMP
goes 45 -> 46 with its pin test. Verified free against origin/main; the
ledger in that file records three prior EXACT clashes, so re-check again
immediately before merge.

The matrix now covers the spellings real code writes, all measured: a
nullable generic, a bounded wildcard, a raw type, a nested generic and a
multi-argument one. None needed work beyond the shared lookup, which is
the evidence that base-name erasure is the right primitive. The C++
specialization control now asserts what it was written for:
`Vec<bool>.save` and `Vec.save` are DIFFERENT target ids, so the
arity/token match still wins over erasure.

scope-capture is byte-identical for cpp and c, so no rebaseline — the
bench corpus contains no generic-typed member field, which is worth its
own coverage issue.

Two pre-existing gaps were measured and are deliberately NOT fixed here,
because in both cases the language's own non-generic CONTROL row fails
identically: C++ `this->field.m()` emits nothing, and JavaScript/PHP
docblock-declared field types bind nothing at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(python): do not reduce containers or typing special forms to a base name (#2833)

Review finding on this branch's own Python change, caught by probing the
interpreter directly rather than by reading it.

The base-name reduction was reached by FALLTHROUGH: "neither container rule
matched" was treated as "not a container". It is not, and two measured
shapes proved it:

  dict[str, list[User]]   ->  dict          (was: the annotation, intact)
  Dict[str, Repo[User]]   ->  Dict
  Callable[[int], User]   ->  Callable
  Literal["a"]            ->  Literal
  Union[A, B]             ->  Union
  tuple[int, ...]         ->  tuple

The dict rule's value group cannot span a nested `]`, so a nested value
declines and falls through — and the dict rule's own comment says that
shape is deliberately "left for a downstream strip pass". Collapsing it to
`dict` destroyed the value type instead. The typing SPECIAL FORMS are worse:
`Callable`, `Literal`, `Annotated` and `Union` are not classes, and reducing
them to a bare name binds any workspace class that happens to share it —
a fabricated edge, which is strictly worse than the missing edge #2833 set
out to fix, and those names are ordinary enough for a real codebase to
declare.

Reduction is now guarded by an explicit deny set covering the containers the
two allow-lists already own and the typing special forms. Everything named
there keeps its as-written text and resolves exactly as it did before #2833.

`arr[0]` also reduces to `arr` in isolation, but that is unreachable and is
now documented as such: every Python `@type-binding.type` capture is a
`(type)`, `(identifier)`, `(attribute)` or `(dotted_name)` node, so a
subscripted VALUE expression never reaches the interpreter.

Pinned by a new unit test that asserts all four groups — user generic
reduces, container reduces to its ELEMENT, declined container shape stays
intact, special form untouched. Reverting the deny set fails three of its
five cases.

Also corrects `resolveClassBindingForName`'s docstring, which this branch
had made false: it claimed only `classifyReceiverOrigin` passes the
decoration stripper, while the three receiver-typing lookups in
compound-receiver.ts now pass it too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(resolution): rank base-name candidates lexically and refuse arg-pinned defs (#2833)

Review of #2855 found that this PR turned a MISSING C++ edge into a
CONFIDENTLY WRONG one — the direction this subsystem calls unrecoverable.

`resolveClassBindingForName` ended with an unguarded base-name fallback
that returned the first same-named class the scope chain reached. A C++
primary template carries `templateArguments === undefined`, so it can
never satisfy the exact-args branch, and every non-specialized
instantiation fell through to that fallback. Measured through the real
pipeline: with the primary forward-declared and the specialization
defined first, `Vec<int> vi; vi.save()` emitted `Vec<bool>::save`.
Declaring the primary first gave the correct target — selection was
SOURCE-ORDER DEPENDENT. Two more triggers behaved the same way: a
partial specialization (`Vec<int*>` against `Vec<T*>`), and lexical
shadowing between a global `Box<bool>` and a namespaced `N::Box<bool>`.

Two changes, neither of which is any of the three remediations the
review proposed — each was rejected on measured evidence:

- Exact-argument matching is now LEXICAL-FIRST. Candidates come from the
  scope chain, and the workspace-wide qualified-name bucket is consulted
  only when the chain produced no exact match, so cross-file
  specializations still bind.
- The base-name route refuses a definition that pinned its own template
  arguments: if the fallback's answer carries `templateArguments`, the
  visible candidates are re-decided with those removed — exactly one, or
  decline.

Why not the filed options. "If specializations exist and none matches
exactly, return undefined" deletes a green committed row
(`neg-cpp-specialization/runInt` legitimately resolves to the primary).
"Resolve all defs for the base name, return only on exactly one" deletes
a working edge for C# `partial class Repo<T>` split across files — two
unspecialized defs under one name is legitimate, and
`QualifiedNameIndex`'s own docstring names that case. Preferring the
primary alone fixes nothing about shadowing, which is a ranking bug.

The guard is expressed as `carriesOwnTemplateArguments`, not as
"specialization", so shared pipeline code still names no language
(AGENTS.md R6). It can only fire where a declared name carries concrete
arguments — measured `undefined` for `class Repo<T>` in TypeScript and
C# and for a C++ primary template — so the blast radius is bounded to
C++-style specializations.

Partial-specialization SELECTION is deliberately not implemented:
choosing `Vec<T*>` for `Vec<int*>` needs template-argument deduction,
which is a semantics expansion and cannot live in language-neutral
shared code. The source-order dependence is what is fixed; the answer is
now deterministically the primary.

Also in this commit: dropped an unreachable `?? []` (QualifiedNameIndex
returns a frozen empty array on miss by contract) whose comment was
wrong on both clauses; made the docstring true about argument ERASURE
being what widens what binds, rather than only the decoration stripper;
and corrected a stale pointer that still placed
`resolveClassBindingForName` in `receiver-bound-calls`.

`findClassBindingInScope` itself is untouched — 38 call sites, CRITICAL.

Verified: matrix 56/56, cpp.test.ts 334, unit scope-resolution 1505.
Mutation proof: reverting this file fails the three trigger cases and
passes the non-regression cases; restoring it passes all five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(python): close the deny-set drift axis by case-folding, not by vigilance (#2833)

Review of #2855 found `NOT_A_USER_GENERIC` was a closed list over an
open universe: four review lanes each escaped it with a DIFFERENT set of
names. `Deque` was the sharpest — its lowercase twin `deque` was already
listed, so the omission was an internal inconsistency rather than a
judgement call, and with a workspace `class Deque` present
`self.dq: Deque[User]` fabricated a `Deque.appendleft` edge.

The structural cause is PEP 585: nearly every container has two
spellings differing only in case (`deque`/`typing.Deque`,
`frozenset`/`FrozenSet`). Exact matching forced every pair to be listed
twice, so any half-pair was a silent escape. The deny lookup is now
CASE-FOLDED, which closes that axis by construction — `Deque` becomes
impossible rather than remembered.

`SINGLE_ARG_CONTAINERS` and `MAPPING_CONTAINERS` are now the single
source of truth: they build the two container regexes (verified
byte-identical `.source` and `.flags`, so zero behaviour change) and
feed the property test. The deny set is re-scoped to a closed, auditable
universe — the documented Python stdlib type-system surface — and grew
39 -> 65 concepts: the `collections.abc` views, `contextlib` managers,
`re.Pattern`/`Match`, the `IO` family, ordinary-named stdlib generics
(`Queue`, `Task`, `Future`, `PathLike`), the remaining typing special
forms, and the generic machinery (`Generic`, `Protocol`, `TypeVar`...).

Third-party generics (`Mapped`, `QuerySet`, `Model`) are deliberately
NOT added and are pinned as a decision: that universe is open,
enumerating it only chases the last escape, and declining `Model` would
cost real edges in the many projects that declare one.

The review's suggested property test — derive the names from the
`single`/`dict` regex sources — would NOT have caught `Deque`: `deque`
appears in neither regex, only in the deny set. Both properties are
implemented, since they catch different drift.

The unit test was also TAUTOLOGICAL: it asserted members OF the deny
set, so it structurally could not detect an omission. It now asserts
case-fold closure and PEP 585 alias coverage, and the capture fixture
drops its `as unknown as` cast for the fully-typed helper pattern the
sibling `java-interpret.test.ts` already uses.

Still at interpret time, so no further SCHEMA_BUMP (already 45 -> 46).
Proving the base is a class the FILE can see — the real fix for the
remaining exposure, since `findClassBindingInScope` binds any name with
exactly one workspace def regardless of scope or imports — is a
follow-up, not reachable from this file.

Mutation proof: restoring HEAD's deny-set contents and exact-match
lookup fails four assertions including the `Deque` pair, with the
pre-existing guard rows still passing; restoring gives 125/125.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(cpp): capture qualified generic member fields, and make the bench gate see them (#2833)

Review of #2855 found that the three `field_declaration` rules this PR
added only matched a DIRECT `template_type`, so the common real-world
spelling still bound nothing: `std::vector<Item> items;`,
`ns::Repo<User> r;` and `std::unique_ptr<Repo> p;` parse as a
`qualified_identifier` WRAPPING a `template_type`. "C++ fixed" was
overstated.

Six new patterns — three declarator shapes (plain, pointer, reference)
by two qualifier depths — written as separate patterns rather than one
alternation, keeping the tree-sitter 0.21 field-position discipline the
existing rules follow.

The design choice was measured, not assumed. Codex suggested preserving
the full qualified spelling and normalizing `::`; preserving resolves
NOTHING, because `findClassBindingInScope`'s dotted-tail fallback splits
on `.` while C++ writes `::`, and `ns::Repo` is not an index key either
(C++ emits no `@declaration.qualified_name`). Measured: `ns::Repo<User>`
resolves to nothing, `ns.Repo<User>` resolves to `Repo`. Since a
tree-sitter capture is a NODE and not synthesized text, the only lever
is which node to capture — so `@type-binding.type` goes on the INNER
`template_type`, dropping the qualifier and landing on the same
single-match-or-decline path the bare spelling already takes.

Qualifier depth 3+ (`a:🅱️:c::Repo<User>`) remains uncaptured. Stated as
a limit and pinned by a test row, not claimed as fixed.

The bench blindness the review identified is also closed. The
`scope-capture` C++ corpus contained ZERO template-typed member fields —
confirmed a fourth way by applying six demonstrably behaviour-changing
patterns and getting a byte-identical fingerprint. The corpus now
carries generic and qualified-generic members, and the gate is load
bearing for the first time: three states that all hashed to 856d02f3
before now differ (pre-#2833 0e7cbda7, +this PR's 3 rules de07d8b5,
+these 6 rules bd47c82d). Rebaselined for cpp only; c is unchanged.
Histogram diff: only 5 tags move with the fields, each by exactly +40
(20 entities x 2), and every `@reference.*` count is unchanged.

Over-match is preserved: 20 shapes still produce no field capture,
including the 8 original method/pointer/reference/function-pointer/
using/typedef/friend/operator forms plus their `std::`- and
`a:🅱️:`-qualified variants.

Not fixed here, deliberately: NON-generic qualified fields
(`ns::Address addr;`, `std::string name;`) still capture nothing.
Closing that needs six more patterns and would newly bind every
`std::string`/`std::mutex` member repo-wide, changing edges far outside
#2833. Separate issue.

The template-template-parameter hazard the review filed against these
rules is NOT capture-side: a tree-sitter query has no scope knowledge,
so it cannot know `Map` is bound by the enclosing `template <...>`
header, and the PRE-EXISTING `type: (type_identifier)` rule already
captures a bare `T item;` and erases it the same way. It is handled by
the lexical ranking in `walkers.ts` in this series.

Mutation proof: reverting this file fails 9 of 32 assertions (all eight
qualified spellings return no capture) while every over-match negative
still passes; restoring gives ALL PASS. Bench `--check` passes for all
15 languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* test(resolution): pin specialization order, shadowing and the untested spellings (#2833)

Grows the generic-field matrix 56 -> 114 tests, closing every coverage
gap the #2855 review named and turning the fix-agents' scratch evidence
into permanent rows.

The rows that discriminate against the resolver fix (they fail if
`walkers.ts` is reverted):

- C++ specialization must not depend on DECLARATION ORDER: the
  forward-declared-primary/specialization-first arrangement must land on
  the primary, same as the mirror arrangement. Plus a cross-case
  property asserting the two independently built fixtures agree.
- Partial specialization is deterministic in both orders. The note says
  explicitly that selecting `Vec<T*>` would need argument deduction and
  that flipping this row later is a deliberate expansion, not a
  regression fix.
- Lexical shadowing: the namespace-local `N::Box<bool>` wins for a field
  inside `N`, and the global specialization wins at global scope.

The NON-REGRESSION rows are load-bearing — they are why two of the three
proposed remediations were rejected: cross-file C++ specialization
binding, and C# `partial class Repo<T>` split across two files with the
field in a third (two legitimate unspecialized defs under one name).

Coverage the review found missing: C++ pointer and reference generic
fields (two of this PR's three original rules had ZERO coverage); all
six qualified patterns plus the depth-3 boundary pinned as empty;
TS/C# multi-arg container collision; an anti-vacuity sibling for
`neg-bounded-type-parameter`; Swift/Dart rows restructured so the
ANNOTATION is the only possible source (the old rows gave the field an
initializer of the same generic type and could not tell which resolved);
and cross-file, inheritance/MRO, import-alias, static-member and the
TypeScript module-hoist branch.

Six things were measured and pinned AS MEASURED rather than asserted as
wishes, each flagged in its row note: a static/class-level member emits
nothing for generic AND non-generic alike (a static gap, not a generics
one); a cross-file C++ primary template does not bind while the
cross-file specialization does; `std::unique_ptr<Payload>` types to
`unique_ptr` rather than `Payload` (smart-pointer transparency is not
applied on the qualified path); two same-named C++ specializations in
one file collapse to one node id; and the container-name collision
(`Map<string, User>` binding a workspace `class Map`) is recorded as
INTENDED, since the annotation does name that class.

The `new Set(...)` dedup was kept rather than narrowed: a per-case
surplus-edge sweep measured ZERO duplicate edges anywhere in this file,
Swift included, so the quirk that justified a blanket dedup does not
reproduce. The sweep now pins zero surplus per case, so a real
double-emit fails instead of being absorbed.

The file is deliberately NOT split: four assertions compare cases
against each other, cost is linear in cases, and the 1,800,000 ms
`beforeAll` is kept because the same run measured 271-428 s depending on
host load — a tighter bound converts contention into a red suite. The
reasoning is recorded in the file header.

Also corrects the SCHEMA_BUMP pin-test title, which still said (#2766).

Mutation proof: reverting `walkers.ts` fails exactly the five order and
shadowing assertions and passes the other 109; restoring gives 114/114.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* feat(resolution): capture declared type parameters so a type variable is not a class (#2833)

Three review findings were blocked on one missing fact. `templateArguments`
records the arguments a declaration was written AGAINST (`struct Vec<bool>`);
nothing recorded the parameter list a declaration DECLARES (`template <class T>`,
`class Box<T extends Repo>`). So the resolver could not tell a type variable
from a class, and:

- `class Box2<T> { t: T }` beside a workspace `class T` emitted a FALSE edge
  `run2 -> T.foo`. `T` carries no type arguments, so it never entered the
  generic branch — the plain lookup simply bound a same-named class. The
  lexical grounding added elsewhere in this series cannot help, because
  `export class T` IS lexically bound.
- `class Box<T extends Repo> { t: T }` resolved to nothing: no recorded bound
  to resolve through.
- A full specialization `template<> struct Vec<T*>` and a partial
  `template<class T> struct Vec<T*>` were byte-identical (`['T*']`).

`SymbolDefinition.typeParameters` now records `{ name, bound? }` in declaration
order (substitution is positional). `bound` is kept verbatim and un-split, so
`Repo & Closeable` stays whole; ABSENT means UNKNOWN, never "unbounded", which
is what keeps unconverted languages behaving exactly as before.

Transport is the raw parameter-list node via `@declaration.type-parameters`,
read by a language-neutral parser that recognizes TOKENS, not languages:
`extends`/`:` introduce a bound, the name is the trailing identifier, so
`class T`, `typename T`, `in T`, `out T`, `reified T` and `class... Ts` are one
rule. Populated for TypeScript, C++, Java, Kotlin, C# and Rust. JavaScript, C,
COBOL, PHP and Ruby have no declared type parameters to capture; Go and Python
spell them with SQUARE brackets, which this parser deliberately rejects as
ambiguous against subscript and array spellings (Go already has a working
main-thread sidecar in this series); Dart and Swift are straightforward
follow-ups.

Two latent hazards found and closed on the way:

- The new capture was not in `KNOWN_SUB_TAGS`, so it could out-span its own
  declaration and become the anchor — silently DROPPING the whole class def.
- A templated C++ struct matches both the standalone and `template_declaration`
  patterns, minting two defs under one id, and only one twin could see the
  parameter list. `buildDefIndex` is first-write-wins, so MATCH ORDER decided
  whether `Vec` remembered `T`. A narrow duplicate-declaration backfill gives
  both twins the list.

Also fixed by its own test: a Rust lifetime `'a` parsed as a parameter named
`a`, which would have shadowed a real class.

Parse-time output lands in the cached ParsedFile, so SCHEMA_BUMP goes 46 -> 47.
Re-checked against origin/main at write time: main is on 45; 46 was taken by
this same branch, and a warm cache stamped 46 carries ParsedFiles with no
`typeParameters` at all.

The csharp and rust capture goldens were regenerated with the tests' own
documented `UPDATE_GOLDEN=1`; only digests moved, no captureGroups.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(resolution): ground erased base names, and stop a class name from being enough (#2833)

The review's central risk was that this PR converts MISSING edges into
CONFIDENTLY WRONG ones. Base-name erasure (`Repo<User>` -> `Repo`,
`Repo[User]` -> `Repo`) bound through a workspace-wide qualified-name
fallback that consults NO scope, NO import and NO module — it bound any name
with exactly one workspace def. That is why a Python `Mapped[User]` could bind
an unrelated `class Mapped`, and why the language deny lists were papering
over an open universe.

`resolveErasedBaseName` now admits an erased base on one of four grounds,
strongest first: the scope chain binds it; the declaration is in the SAME
FILE; the index proves the name is a template family; or the file binds no
cross-file class at all, so its silence is no evidence. The last ground fails
toward permissive on purpose — every way it can be wrong costs a wrong edge
that already existed, never a working one.

Two measurements drove that design and refuted the simpler rule. A C++
`#include` materializes NO binding whatever, and C# resolves cross-namespace
without `using` through the index — so a pure "require lexical grounding" rule
would have deleted every cross-file C++ generic member. Both are now pinned.

Python erases at CAPTURE time, so by resolution there is no `<` and the
grounded route was never entered. `erasedTypeApplication` rebuilds the
application from `TypeRef.declaredSpelling` — strictly: the raw name must be
the base and the argument list the whole balanced remainder, so `User[]`,
`vector<Item>` and `Repo<User>?` decline and behave exactly as before.

Closing it took finding FOUR emitters, not one. Three were in Case 4; the
fourth was `emitReferencesViaLookup` re-emitting the refused edge from the
pre-resolved reference index, which needed the site marked handled with a
recorded `receiver-unresolved`. A fifth lived in the text cascade: a declined
fold falls THROUGH by design, and the cascade held its own ungrounded copy of
the member-typing lookup. This file typed a receiver from a `TypeRef` in five
places and the PR had wired three; all five now go through one
`classOfDeclaredType`.

Also here, from the same review:

- Type parameters no longer bind a same-named class (uses the new
  `typeParameters`), and a BOUNDED parameter resolves through its bound.
- A cross-file C++ PRIMARY template now binds: a ranking bug, not a capture
  one — the index fallback needs exactly one candidate and `Vec` held two, so
  removing the argument-pinned declaration leaves one.
- `this->field.m()` resolved to nothing for generic AND non-generic alike. A
  language that declares `this` IS the enclosing class
  (`resolveThisViaEnclosingClass`) synthesizes no `this` typeBinding, so a
  chain whose BASE is `this` could never seed its head. Reading the provider
  flag keeps the rule language-free.
- Class-level (static) member receivers emit nothing in TypeScript and Kotlin
  — for the non-generic control too. Case 6 types them from the DEF side
  (`isStatic` + `declaredType` on the field node), which needs no capture
  change; the target lookup stays the ordinary instance walk, so a static
  field HOLDING an instance still binds an instance method and a genuine
  static call is untouched.

Partial-specialization SELECTION is deliberately not implemented: it needs
argument deduction against a parameter list, and full C++ partial ordering is
a real algorithm with no measured driving case. The discriminator now exists
if someone wants it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(cpp,js,php,go): close the remaining per-language generic-field gaps (#2833)

Four language gaps the review measured, each with a different cause.

**C++ qualified member fields.** `std::vector<Item> items;`, `ns::Repo<User> r;`
and `ns::Address addr;` captured NOTHING: every field rule required the type
node to BE a `type_identifier` or `template_type`, and a qualified member type
is neither — tree-sitter wraps both in a `qualified_identifier`. Three
depth-agnostic rules (one per declarator shape) now match the outer node, which
also REMOVES the depth boundary rather than raising it: depths 1-4 capture,
generic and non-generic alike.

Preserving the qualifier resolves nothing — measured: `ns::Repo<User>` binds
neither way, because the dotted-tail fallback splits on `.` while C++ writes
`::`, and `ns::Repo` is not an index key. Since a capture is a NODE and not
synthesized text, the qualifier is dropped in `interpret.ts` by a top-level-only
`::` split, so `std::vector<std::string>` reduces to `vector<std::string>`, not
`string`.

Measured cost of the non-generic half, which was the reason to hesitate: field
captures go 8 -> 32 across the C++ bench corpus, but the resolution-level census
over those 13 repos is 32 CALLS edges before and 32 after, BYTE-IDENTICAL. It
fabricates only where a workspace class shares a std name (`class string` beside
`std::string name;`), which is the same accepted policy the already-landed
qualified-generic rules carry, pinned in the matrix as intended.

**JavaScript `@type {Repo<User>}` and PHP `@var Repo<User>`.** Neither bound a
field type — and neither did the NON-generic control, so this was a docblock gap
rather than a generics one. PHP needed TWO captures, not one: with only the type
binding, `$this->repo->save()` resolved until a second class declared `save` and
then went unresolved, because narrowing a same-named method needs the receiver's
member owned. Generics do NOT come free in PHP — `normalizePhpType('Repo<User>')`
returns `'User'` by the container-element convention, so passing the raw spelling
through would have emitted `User::save`; type arguments are erased at capture
instead. In JavaScript they DO come free, verified byte-identical to the
TypeScript control. Both decline what they cannot prove: arrays, `list<User>`,
unions, `Promise`/`Array` wrappers (via an exported predicate rather than a
copied name list), statics, and any property that already has a native type.

**Go generic interfaces.** `UserRepo` genuinely DOES implement `Repo[User]` —
the spec says a generic type must be instantiated, that instantiation
substitutes type arguments and yields a new non-generic type, and that a type
implements an interface when it is in its type set. So the old behaviour was a
FALSE NEGATIVE and the matrix note calling it "already correct" was wrong.
Satisfaction is now checked against POSITIONALLY SUBSTITUTED method sets, so
`Repo[Order]` does not match a `Save(x User)` implementor — substitution, not
erasure. #2829's exact method-set model is untouched: pointer receivers still
follow MS(*T), unexported names stay package-scoped, the declaration's own
method set is still checked first, and the harvest is gated so a repo with no
generic interface never runs it. `go.test.ts` is unchanged at 296 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* test(resolution): pin every fix from the review, 114 -> 155 rows (#2833)

Eight rows in this matrix pinned gaps that the fixes in this series close, so
each asserted the opposite of the new truth. All eight are flipped, and the
prose describing them as open gaps is corrected. Nine new cases cover the fixes
that would otherwise have shipped unpinned.

Flipped, each measured: the type-parameter FALSE edge (`run2`) is gone; a
bounded parameter now resolves through its bound with fan-out; the cross-file
C++ primary binds; the C++ qualifier depth boundary is removed rather than
raised; Go gains its two structural implementors and JOINS the paired sweep,
which had quietly excluded it — that exclusion was the taxonomy admitting a bug;
and both static-member rows resolve.

Added: JS `@type` and PHP `@var` docblock fields with three PHP declines; a
Kotlin `companion object` receiver (given an INTERFACE control so the paired
sweep can check it, which `ts-reach-shapes` cannot — its two sides are not
count-comparable); the Python third-party grounding refusal plus the ground that
still ADMITS, so an empty row can never be read as "erased names never resolve";
the four mirrors that would break if grounding were tightened (same-file and
imported Python, a C++ `#include`, C# cross-namespace without `using`); C++
qualified non-generic fields including the fabrication policy and its absence
case; `this->field.m()` for generic and non-generic with bare controls; and a Go
negative proving substitution is positional, not erasure.

Three shapes are pinned AS MEASURED with notes saying they are deliberate limits
so nobody "fixes" them by accident: C++ partial-specialization selection is
deterministically the primary (real selection needs argument deduction);
`std::unique_ptr<T>` types to the pointer, not the pointee (`.` and `->` are
indistinguishable to the resolver, so transparency would trade a recoverable
miss for a confident wrong edge); and two same-named C++ specializations in one
file collapse to one node id, which is why the shadowing fixture uses two files.

One row pins a REMAINING wrong edge rather than hiding it: `m.inner.ping()` on
a `Mapped[User]` head still binds the unrelated workspace class, while the
one-segment-shallower `m.save(u)` correctly declines. The obvious one-line guard
was written and MEASURED not to close it, so the surviving route is elsewhere
and wants its own diagnosis — a broader refusal would change chain-head
resolution for every language without pinning the shape it is meant to fix.

`bench/scope-capture` is rebaselined for the six languages whose captures moved,
regenerated from a fresh measurement rather than pasted; `--check` passes for all
15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* perf(resolution): remove three measured hot-path regressions this series added (#2833)

A quality pass over the #2833 series found three performance defects it had
introduced, all measured, plus dead code and stale docs from six agents having
appended to the same files across four rounds. No behaviour change: the
resolver suite is identical before and after, and every scope-capture
fingerprint is byte-identical.

**An accidental quadratic in Go instantiation harvesting.** `collectGoInstantiations`
calls `record()` for every type binding and every declared, return and parameter
type in every Go file, and the `includes('[')` gate does not filter Go's most
common types — `map[string]string`, `[]map[string]*v1.Pod` and
`map[string]map[string]int` all produce a `map` candidate. Each false base then
failed a full scope-chain walk and fell through to a LINEAR SCAN OF EVERY
INTERFACE IN THE PROGRAM, with no dedupe on the spelling, so the same
`map[string]string` written 10,000 times paid 10,000 scans. Now a
qualified-name index built in `buildDetectionIndexes` (one probe, ambiguity
semantics preserved exactly) plus a per-scope base memo:

    8,000 interfaces / 80,000 spellings:  6,662 ms -> 104 ms   (64x)

`resolveEmbeddedInterface` held a byte-identical copy of that scan and now
shares the helper. `GoInstantiation` was a single-field wrapper and collapses
to the array it wrapped; its two parallel maps fold into one whose inner key IS
the dedupe. `candidateStructIdsFor` was rebuilt per instantiation although
every substituted method set has the same key set — hoisted, and materialized,
because one branch returned a live iterator that would have yielded nothing on
a second pass.

**`scanForCrossFileClass` asked a name-keyed question that needs no name key.**
It answered "does this file bind any cross-file class" by probing every
accessible namespace once PER NAME. It now iterates the channels directly,
taking whichever side is smaller so a large namespace table cannot reintroduce
the product. Predicate and early exit preserved:

    5,000 module names x 1,000 namespaces:  159.0 ms -> 1.2 ms   (132x)

**A duplicated scope walk on every generic receiver.** `resolveClassBindingForName`
computed the lexical candidate list, then `resolveErasedBaseName` recomputed
the identical `findAllBindingsInScope`. Computed once and passed:

    receiver at depth 8:  5,617 ns -> 3,091 ns   (-45%)

**A whole extra AST traversal per JavaScript and PHP file.** The docblock
synthesis passes each added a full tree walk to find one node kind — the ninth
in the JS emitter, the third in PHP. `node.namedChildren` materializes a
wrapper array across the N-API boundary for every node, so one added pass cost
1.9x what parsing the entire file costs. Folded into the existing walks as one
more node kind; capture output is byte-identical and every fingerprint is
unchanged. Total emit time per file drops 4-7%.

Hygiene, all verified stale rather than assumed:

- `receiverOriginOpts` passed `resolveThisViaEnclosingClass`, which
  `classifyReceiverOrigin` never reads — the "both hooks" comment above it is
  true again.
- The `stripDecoration` docstring's caller roll-call claimed the only
  edge-emitting caller "emits no edge and can only change a diagnostic label".
  Case 6 passes it and does emit edges. Replaced the roll-call with the rule;
  six rounds each appending a name to a list is how it went wrong.
- A Python comment described the resolution-time grounding as a follow-up that
  "this parse-time pass cannot do" — it landed in this same branch and is
  pinned by `py-erased-grounding`.
- `classOfDeclaredType` took a `scopeId` all five callers derived from the
  `TypeRef` they also passed. Dropped, so "these five are the same call" is
  enforced rather than asserted.
- Three exports with no consumer outside their own file.
- PHP had three copies of one preceding-comment sibling walk and two regexes
  for one tag, so a fix to either reader of `@var` would land on one and not
  the other — the symptom being a field typed differently from its own foreach
  element type. One walk, one regex.

Tests: the new matrix leaked a fixture repo per case; it now carries the
sibling suite's `cleanupTempDirSync` and the Windows EBUSY reasoning that goes
with it. `PAIRED` was a second hand-maintained list and 19 of 41 cases had
silently fallen out of it — it is derived from the cases now, with a new
assertion that each case is either swept as a pair or carries a written reason
it is not. That recovered one genuine omission (`php-typed-property`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* test(bench): rebaseline receiver-resolution for the #2833 this-> fix

The `Receiver-resolution drop guards` CI step failed on this branch:

  shapeArm.cpp.fieldReceiverCall:  "INVISIBLE-GAP" -> "RESOLVES"
  shapeArm.cpp.decoratedFieldType: "INVISIBLE-GAP" -> "RESOLVES"

Both are the intended improvement. The guard is exact-match by design —
the drop count cannot move without a deliberate rebaseline, and the
rebaseline path demands the movement be explained — so this records the
two shape flips and leaves the call-drop count arm untouched.

BASELINE.md still claimed `this->repo.save()` and `this->repo->save()`
were INVISIBLE-GAP. That is now false: the `resolveThisViaEnclosingClass`
head seed added in this PR resolves both. Also notes what the control
established — this was never a generics gap, since the non-generic
control failed identically before the fix.

* docs(parse-cache): narrow the SCHEMA_BUMP ledger to what the bump delivers

The ledger claimed a warm cache would make "the whole fix ... a silent
no-op on every incremental analyze". That overstates the constant. The
bump invalidates the PARSE half; whether the re-parsed captures reach the
graph is gated separately and does not move:

  - `isIncremental` (core/run-analyze.ts) tests `!options.force`, an
    existing meta, `!schemaFingerprintMismatch(...)`, feature parity,
    non-empty `fileHashes` and a git repo. SCHEMA_BUMP is in none of them.
  - the incremental branch writes back only `hashDiff.toWrite` and logs
    the rest as "unchanged file rows preserved".
  - SCHEMA_FINGERPRINT hashes node/relation DDL, untouched here, so it is
    byte-identical and moves nothing either.

So an incremental analyze re-parses an unchanged file correctly but keeps
its existing rows; the new edges land on the next full rebuild. That is
the pre-existing contract for every capture change, not a regression in
this PR — but the comment should not promise more than it delivers.

Comment only; no behavior change. SCHEMA_BUMP stays 48.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:14:13 +01:00
Gergő Magyar
911151e230
fix(resolution): resolve Go pointer-receiver calls, and report the program boundary instead of hedging (#2766) (#2782)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
2026-08-01 22:42:18 +01:00
Gergő Magyar
27ab37c432
feat(resolution): type receiver chains from AST structure across all 14 languages (#2708) + epistemic lower-bound (#2744) (#2747) 2026-07-31 07:12:57 +01:00