mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
10 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
77360e1043
|
fix(scope-resolution): make interface dispatch generic-instantiation aware (#2912) (#2939)
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
* fix(scope-resolution): make interface dispatch generic-instantiation aware (#2912) Interface-dispatch fan-out walked the subtype closure with generic arguments erased, so `IValidator<string>` and `IValidator<int>` — one declaration, one subtype list — were indistinguishable and a call through the first reached `IntValidator.Check(int)`, a target no runtime dispatch can produce. The arguments were already in the capture, unread: every language anchors `@reference.inherits` on the whole base node while `@reference.name` keeps the erased base. `ReferenceSite.typeArguments` is therefore derived generically in `scope-extractor.ts` from the anchor's own spelling — no per-language query changed — covering C#, Java, TypeScript, Kotlin, Go (`Base[int]` embedding), Python (`Base[User]`) and Swift; Rust and Dart anchor on the bare name and get nothing, which reads as "unknown". `preEmitInheritanceEdges` is the only code that pairs a heritage site with a resolved (subtype, supertype), so it records the instantiation there and hands it to the dispatch pass. The closure is then walked carrying a substitution, as a type checker would: `Wrapper<T> : IValidator<T>` binds T to the receiver's argument and stays reachable from every instantiation, while its own subtypes are matched against that binding. An incompatible hop is skipped without being marked seen, so a type reachable by a second, compatible path still gets its edge, and without descending, since its subtypes inherit the mismatch. Pruning happens only on positive evidence that two instantiations differ. Unknown arguments on either side, an arity that does not line up, an unresolved qualified spelling of the same simple name, or an argument that might be a type variable the language never captured all keep the target. Telling an uncaptured type VARIABLE from a concrete type is the crux: `typeParameters` is absent both for a non-generic declaration and for every declaration in a language whose query omits `@declaration.type-parameters`, so the pass reads the evidence in front of it — one run resolves one language, so a single generic declaration anywhere in it proves the captures record parameters. A language recording neither arguments nor parameters keeps exactly its pre-#2912 fan-out. Type arguments are compared as resolved declarations rather than spellings, so `Models.User` and an imported `User` are one type; the new optional `ScopeResolver.normalizeTypeArgument` hook canonicalizes a language's predefined aliases, implemented for C# (`string` ≡ `String`) where mixing the spellings would otherwise delete a real implementor. Fan-out cap, skipped-target reporting, overload selection and non-generic closure behaviour are unchanged. SCHEMA_BUMP 60 -> 64: the heritage arguments are a parse-time capture, so a warm cache would replay pre-fix sites and leave the filter silently inert on unchanged files (61/62/63 are claimed by open PRs). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef * fix(scope-resolution): close the two generic-dispatch gaps (#2912) The first commit left two shapes on the pre-#2912 fan-out. Both are now covered, and the second one turned out to need a route the pipeline did not have at all. **Folded receivers (Cases 0 and 3b).** `this._validator.Check(x)` is typed by the compound fold, and the fold answers with a CLASS — which is exactly what loses the instantiation, since `IValidator<string>` and `IValidator<int>` fold to one declaration. The fold now reports the SPELLING it typed each receiver position from, through a pure side channel (`recordReceiverType`) added to the one helper every declared-type route already shares plus the two return-type routes; resolution is unchanged whether or not a caller passes it. The reader keeps the last report and uses it only when it names the class the fold returned, so an intermediate position cannot lend its arguments to another class. This covers the dependency-injection shape the issue is really about — a field-held generic interface — and multi-hop chains, where it is the last hop's spelling that types the receiver. **Rust and Dart heritage.** Neither recorded arguments, for two different reasons, so both routes exist now: - Rust's `@reference.inherits` anchor is the trait identifier INSIDE a `generic_type`. Widening the anchor would move the site's range, and that range is part of every inheritance edge's id, so the arguments arrive through a new `@reference.type-arguments` sub-tag instead. - Dart's `implements` / `with` never become reference sites at all: they travel as heritage MARKERS and their edges are emitted by the language hook. The arguments ride the marker payload as an optional fourth field (dropped, not encoded, when the spelling contains the marker delimiter), and `ScopeResolver.emitHeritageEdges` now receives the same sink `preEmitInheritanceEdges` writes to, so whichever pass emits an edge records that edge's instantiation. Dart also gained the `@declaration.type-parameters` capture, without which its own type VARIABLES are indistinguishable from concrete arguments and `class Box<T> implements Validator<T>` would be pruned from every instantiation. Note this makes Rust and Dart record their instantiations; it does not make them fan out. Interface dispatch still fires only for a receiver whose folded type is an `Interface` symbol, so a Rust `Trait` or a Dart abstract `Class` receiver has no secondary targets to filter. Widening that gate emits new edges for several languages and belongs to its own issue. **Two matcher rules the wider coverage exposed.** A WILDCARD names a set of types rather than one — `Repo<? extends User>` holds a `Repo<User>`, and Kotlin's `Repo<*>` / `Repo<out User>` say the same — so a position with one on either side is unknown; nullable spellings trip the same test, which costs a little precision in the safe direction. And insignificant whitespace inside a nested spelling (`Map<string, User>` vs `Map<string,User>`) is no longer a difference. One expectation changed in the #2833 field-receiver matrix: a `Repo<Repo<User>>` receiver no longer reaches `UserRepo implements Repo<User>`. That edge is precisely the false positive this issue is about, and the primary edge to the interface's own declaration — which is what the matrix row exists to prove — is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef * refactor(scope-resolution): apply the quality pass to the #2912 change Three cleanups, no behaviour change. **One balanced-list scanner, not two.** `erasedTypeApplication` and `typeApplicationArguments` each carried a copy of the same fiddly scan — one bracket list, balanced, closing on the last character, non-empty — differing only in what they did with the result. Both now call `balancedTailList`; the rule that rejects `User[][]` and `Repo<User>?` lives in one place instead of being free to drift between two. **The receiver's arguments are parsed after the gates, not before them.** `emitInterfaceDispatchFor` takes the receiver's declared SPELLING and parses it itself, once the owner is known to be an Interface with subtypes. Every one of the five cases calls it unconditionally and the overwhelming majority of receivers are concrete classes that return at the first line, so the parse was running per resolved receiver site to be discarded immediately. Case 4 and Case 6 now hand over the string they already hold, and the folded-receiver helper returns the recorded spelling rather than parsing it. **One question gates the whole instantiation apparatus.** Inside the closure walk, the graph-id lookups now hang off "is the supertype's instantiation known?" — false for every non-generic receiver and for every language that captures no heritage arguments, which is what makes those walks cost exactly what they cost before #2912. Also lifted the argument-route choice in `pass5CollectReferences` out of a nested ternary into a named `heritageTypeArguments`, where the reason the explicit sub-tag wins over the anchor text can be stated once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef * test(scope-resolution): cover generic interface dispatch in Kotlin and Go (#2912) Extends the #2912 dispatch coverage past C#/Java/TypeScript. No production code changes — the derivation is language-agnostic by construction (`heritageTypeArguments` reads the heritage anchor's own spelling), so the question was only which languages actually reach the filter. Kotlin rides the shared heritage pre-pass; Go reaches the same filter from the other side, matching implementors structurally while the receiver's `Validator[string]` spelling carries the instantiation. Both are confirmed to prune the mismatched implementor. Each language gets a NON-GENERIC control asserting the fan-out still reaches every implementor. Without it the `not.toContain` assertion passes just as well when a language emits no dispatch edge at all — which is what Dart, Python and Rust were measured doing for this receiver shape, generic or not. They are deliberately not asserted on here: a "filtered correctly" test over a path that never fans out measures nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * test(bench): re-baseline the Rust and Dart capture fingerprints for #2912 The Rust trait-impl and Dart heritage capture changes this branch makes are additive TEXT on existing matches — each carries the instantiation the clause was written with — so they drift the scope-capture digest without adding or removing a match. The baselines were never re-measured when those captures landed, which left `measure.mjs --check` red on this branch independently of the merge. Re-measured rather than hand-edited. Rust's capture_groups_fp (3556) and fixture_count (202) are unchanged across the move, which is the evidence that this is digest drift and not a capture-set regression. The other 13 languages are byte-identical; 15/15 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * refactor(scope-resolution): quality pass over the #2912 change Cleanup only — no behavior change. Findings from a four-angle review (reuse, simplification, efficiency, altitude), applied where they were verified. Reuse / duplication: * `stripTrailingCallSuffix` was a second copy of `matchingOpenParen`'s backward balanced-paren scan. Both now live in `template-arguments.ts` beside `balancedTailList`, for the reason that helper was shared in the first place: two copies of a scan this fiddly are free to disagree. * The two call-return arms of the compound fold repeated the same four-part expression character for character; they share `classOfReturnType` now, the return-type twin of `classOfDeclaredType`, which keeps the "look up by rawName, report the erased application" pairing in one place. * `pipeline/run.ts` implemented first-writer-wins twice — once in the pre-pass and once in the provider sink. One store, one sink, one rule; the pass keeps its `Set<string>` return and the callable-flow-only arm stops building an empty map to satisfy a widened return shape. Simplification: * `subtypeParametersComplete` dropped a disjunct that could never decide: every `subDef` reaching it comes out of the same loop that sets `languageCapturesTypeParameters`, from exactly those defs. * The heritage-argument lookup asked "is the supertype's instantiation known?" three times; `superGraphId` now gates the block once. * `TypeArgumentResolver` and `HeritageInstantiationResult` un-exported — no consumer outside their module. Efficiency (all on the per-site dispatch walk): * `resolveSupertypeArgument` captures only the site, so it is built once per site instead of once per subtype visited; the subtype's scope id is looked up once per subtype instead of once per argument position. * `erasedTypeApplication` no longer runs on every fold hop through a call — the spelling is built only once the lookup has found a class, since it is discarded otherwise. * `normalize`+`compact` computed once per side rather than twice. * Regex literals and the identity `normalize` fallback hoisted to module scope. * C# `System.` prefix stripped with `startsWith`/`slice` instead of a regex. Verified: tsc clean, build clean, 1994 scope-resolution unit tests, 171 generic-dispatch + generic-field-receiver integration tests, 15/15 capture bench fingerprints unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * style: apply Prettier to the two files the quality pass reformatted Whitespace only — `quality / format` (npx prettier --check .) was red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * fix(scope-resolution): close the generic-dispatch review findings (#2912) Addresses the gitnexus-check review on #2939. A repeated type variable was rebound rather than unified: `class C<T> : Pair<T, T>` accepted a `Pair<string, int>` receiver, with `T = int` silently replacing `T = string` and the bogus substitution carried to the next hop. It now unifies, and prunes only on the same positive evidence the concrete path demands — an undecidable repeat keeps the target with no binding. A type PARAMETER of the declaration enclosing either side is now recognised and never compared. `subtypeParametersComplete` is evidence about the SUBTYPE's parameter list and says nothing about a `T` written at the call site, so `void Run<T>(IValidator<T> v) { v.Check(x); }` pruned every implementor: unbounded, `T` grounds to nothing; bounded, it grounds to its BOUND. Both read as a difference of type. That is the missing-edge failure this filter is built to avoid, and it is the common dependency-injection shape in C#, Java and Kotlin. Making that recognition reliable is why generic METHODS now capture `@declaration.type-parameters` in C#, Java and Kotlin — TypeScript already did, which is why its generic functions never had the defect. The capture feeds the existing `bindsTypeParameter` guard, so a method-level `T` also stops resolving to a same-named class in every other lookup. C# alias normalization additionally strips the `global::` qualifier, which `import-decomposer` already unwraps elsewhere: `global::System.String` read as unequal to `string` and pruned a live implementor. The C# captures golden fixture is regenerated for the new capture; the extractor reads `@declaration.type-parameters` generically, so no reader changed. SCHEMA_BUMP 64 already covers these capture changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * fix(scope-resolution): close the two remaining gitnexus-check findings (#2912) `balancedTailList` counted ONE bracket family, so a crossed pair slipped through: scanning `Foo<Bar]>` it never sees the `]`, reaches the final `>` at depth zero, and reports `Bar]` as a balanced argument list — which `typeApplicationArguments` then splits and `erasedTypeApplication` rebuilds a spelling from. It now tracks a stack of expected closers, so every closer must match the opener it actually closes and a crossed pair declines to `undefined`, the "unknown" both callers already fail open on. Well-formed mixed nesting (`List<Dict[a, b]>`) is unaffected. C# `normalizeTypeArgument` stripped `System.` from every qualified spelling, so `System.Custom` answered `Custom` and compared equal to an unrelated `Custom` elsewhere in the workspace. The strip is now earned: a keyword answers from the alias table first, and the qualifier is dropped only when what remains IS a predefined type. `System.Custom` is returned as written and goes to the identity comparison instead — the step that can actually tell two declarations apart. `global::System.String` still meets `string`. Both are pinned by unit tests, including the well-formed mixed nesting and the `global::`-qualified ordinary type that must keep its qualifier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * docs(csharp): record why a shadowed `String` keeps its implementor (#2912) Answers a review finding rather than changing behavior. A workspace may declare its own type named `String`, shadowing the BCL simple name, and the alias table then reads `IValidator<String>` as the `string` instantiation and keeps that implementor. That is the SAFE direction, not an oversight: pruning instead would rest on the belief that two spellings differ, which is the missing-edge failure `generic-instantiation.ts` exists to avoid. Resolving rather than normalizing cannot settle it either — the identity comparison needs a `definitionId` from both sides, and a built-in name carries none, so "built-in versus workspace-declared implies different" would be a new prune with no positive evidence behind it. The cost is one surplus edge for that pair, which is exactly the pre-#2912 fan-out and no worse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * refactor(scope-resolution): pair the receiver spelling with the class structurally (#2912) The fan-out needs the spelling a receiver position was typed from, because the class the fold returns has lost the generic arguments. That was carried by a PASS-LEVEL mutable holder, written by every declared-type lookup anywhere in the fold and read back through a def-id coincidence check, with the holder cleared by hand before each call site. Three things were load-bearing and none were enforced: * the reset had to be remembered at every call site. It was not: the Case 3b retry (`rawName` then `rawName + '()'`) reset once, BEFORE the first attempt, so a spelling reported by the attempt that failed could be attributed to the one that succeeded. * the holder outlived every resolution, so a site that resolved through a route reporting nothing could read the previous site's spelling if the def ids happened to line up. * the pairing itself was inferred from "whichever lookup reported last", not from the fold's own bookkeeping — losing branches (an MRO walk that moved on, a step later folded past) report too. `foldReceiverChain` already had the answer and threw it away: its final `FoldState` holds `def` and `declaredType` produced by the SAME step. It now reports that pairing last, so the structural route is the one that stands. `resolveCompoundReceiverTyped` returns `{def, declaredSpelling}` and owns a sink created and read within the single call, which is what removes the reset discipline — a local cannot be forgotten, and each of the two retry attempts carries its own. The def-id guard stays as the check that a report names the class actually returned. Behavior is unchanged: 1975 scope-resolution unit tests, 177 generic-dispatch and generic-field-receiver integration tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cabd5b82f9
|
fix(go): model Go method sets exactly so interface satisfaction is decidable (#2813) (#2829)
* test(go): pin calls through an interface-typed struct field (#2813) A call through an interface-typed struct field never reaches the implementation: the CALLS edge stops at the interface DECLARATION, so `impact()` on the implementing method reports 0 callers. This commit adds the executable statement of that defect; the fixes follow. Two stacked defects produce it, and either alone is enough to reproduce — which is why no existing fixture could observe it: D1 `buildDetectionIndexes` skips every POINTER-receiver method, so a struct whose methods are all `func (r *T)` has an empty method set, structurally satisfies nothing, and gets no IMPLEMENTS edge. Go's rule is that the method set of *T includes pointer-receiver methods, and idiomatic Go stores *T in an interface-typed field. D2 Case 0 (compound receiver) emits its primary edge and short-circuits without the interface-dispatch fan-out Case 4 performs. A struct field receiver `s.orderRepo` contains a dot and so always takes Case 0; a local or parameter receiver is a bare name and reaches Case 4. Every implementor in both pre-existing structural-dispatch fixtures uses a VALUE receiver, and the one pointer-receiver type is pinned as a negative (`not.toContain('PointerOnlyThing -> PointerOnly')`), so the corpus could not see D1 by construction. The new fixture is pointer-receiver throughout, cross-package, and carries concrete-field controls in the same structs. Failing-first, verified against this tree: 7 of the 11 new assertions fail and 4 pass. The 4 that pass are exactly the controls that must not regress — the primary edge to the interface declaration, the concrete-field call, the absence of fan-out on a concrete field, and the partial-signature negative — so the suite discriminates rather than merely failing. Two recorded artifacts move here because the FIXTURE was added, not because capture output changed: - test/fixtures/go-captures-golden/expected-captures.json — regenerated additively (32 insertions, 0 deletions). - bench/scope-capture/baselines.json — go fingerprint, fixture_count 102 -> 110. Both are regenerated in this commit rather than deferred to the end of the series: the fixture is their only cause, no later commit touches capture emission, so they cannot re-drift and every commit stays green. The check that this is corpus growth and not a capture regression is that go was the only one of 15 language fingerprints to move on the same run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(go): count pointer-receiver methods toward structural interface satisfaction (#2813) D1 of two stacked defects. `buildDetectionIndexes` skipped every method whose receiver is a pointer, so a struct declaring `func (r *OrderRepo) DeleteItem(...)` had an EMPTY method set, structurally satisfied nothing, and produced no IMPLEMENTS edge at all. Go's method-set rule is per-type, and there are two types involved: the method set of `T` holds only value-receiver methods, while the method set of `*T` holds both. #1966 implemented the `T` reading, which is exactly right for `T` — and leaves `*T` permanently empty. GitNexus models one Struct node per type with no separate `*T` node, so only one of the two can be represented, and the `T` reading is the one idiomatic Go almost never uses: methods take pointer receivers so they can mutate, and `*T` is what gets stored in an interface-typed field. The cost was silence rather than caution. With no IMPLEMENTS edge, a call through an interface-typed field resolved to the interface DECLARATION and `impact()` on the implementing method returned 0 callers — byte-identical to a symbol that genuinely has none, which is what made the reporter's blast-radius check unusable rather than merely incomplete. This picks the `*T` reading: the graph now answers "which types provide this interface's behaviour", and no longer proves `var x I = T{}` invalid. The trade is deliberate and was checked against every consumer of IMPLEMENTS before being made — MRO/METHOD_IMPLEMENTS derivation, community clustering, the receiver-dispatch fan-out index, and the epistemic heritage probe. None performs value-assignability checking. Two negative pins encoded the #1966 decision and are REVERSED here rather than deleted, each keeping a comment that explains why the polarity moved: - go.test.ts: `PointerOnlyThing -> PointerOnly` now expected to be emitted. - go-hooks.test.ts: the pointer-receiver-only unit case now expects the implementor instead of `undefined`. `goReceiverKind` is still stamped in method-owners.ts — it is the hook a future value/pointer-aware model would read — but is deliberately no longer a filter. Its now-dead local predicate and type alias are removed so the file no longer carries a helper asserting the reverted rule. Measured on the #2813 fixture, this commit alone: the two IMPLEMENTS assertions flip to passing (6 pass, up from 4) while the five interface-dispatch fan-out assertions still fail — those are D2, fixed in the next commit. Keeping the two commits separate is what makes that attribution visible. Go unit suite: 91 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(resolution): fan out interface dispatch from a compound receiver (#2813) D2 of two stacked defects, and the one that closes the issue. Case 0 (compound receiver) emitted its primary edge and short-circuited without the interface-dispatch fan-out that Case 4 performs, so a call whose receiver is a struct FIELD stopped at the interface's method DECLARATION and never reached any implementation. The gap was a property of receiver SYNTAX rather than of types. Case 0 is selected by `receiverName.includes('.')`, so a field receiver (`s.orderRepo`) always lands there, while the very same interface reached through a local or a parameter is a bare name and falls through to Case 4 — which fans out correctly. Field-held interfaces, i.e. dependency injection, were the half that silently lost every implementation edge; the pre-existing fixtures exercise the local and parameter forms only, which is why the suite was green. The fix is the call Case 4 already makes, placed after Case 0's primary `tryEmitEdge` and before its `handledSites.add`. It stays language-agnostic (AGENTS.md section 42): `emitInterfaceDispatchFor` self-gates on `ownerDef.type !== 'Interface'`, so a receiver that folds to a Struct emits nothing extra and no language check is needed. Confidence is Case 0's own 0.85 literal, not Case 4's site.kind-dependent value — Case 0 has no read/write arm to mirror. The case ladder itself is untouched: invariant I4 in contract/scope-resolver.ts makes the ordering a contract, so the fan-out is added INSIDE Case 0 rather than by reordering or merging cases. Also flips a second, previously unnoticed encoding of the #1966 value-only reading that the full sweep surfaced: the exact-set assertion at go.test.ts:361 enumerates every structural IMPLEMENTS edge, and D1 correctly adds `PointerOnlyThing -> PointerOnly` to it. It is D1 fallout rather than D2's, but D1 had already landed; recording it here with its reason beats amending a commit whose separate measurability is the point. Measured: - #2813 suite: 11 of 11 pass (was 7 failing after D1 alone, which fixed only the two IMPLEMENTS rows). - go.test.ts: 160 passed. - Full cross-language sweep, test/integration/resolvers: 3027 passed, 1 skipped, across 52 files. The single failure in that run was the exact-set assertion above, fixed here; no other language regressed. `detect_changes` rates this HIGH (6 affected flows, all EmitReceiverBoundCalls at step 1) — inherent to editing a hub symbol in the resolution pipeline. The sweep above is the empirical answer to that label. An existing index must be re-analyzed to show the new edges; this changes what the resolver produces, not how it is stored, so no SCHEMA_BUMP applies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(go): pin the heritage edges that make impact() hedge an interface-bound count (#2813) The epistemic half of the issue, resolved by MEASUREMENT rather than by new code, and pinned at its mechanism. The reporter's disqualifying complaint was that `impact()` reported `impactedCount 0, epistemic "exact", risk LOW` for a method reachable only through an interface-typed field — byte-identical to what it reports for a symbol that genuinely has no callers. A zero therefore could not be used defensively, which was the entire use case. That verdict comes from `computeEpistemicBoundary`, which has two producers and neither fired: the call sites were not DROPPED (they resolved, just to the interface declaration, so the #2744 receiver-typing producer saw nothing), and its heritage probe walks IMPLEMENTS/METHOD_IMPLEMENTS edges out of the queried symbol — of which there were none, because the pointer-receiver exclusion (D1) meant no such edge was ever emitted. Restoring those edges fixes the epistemics as a side effect, so the planned conditional change to local-backend.ts is NOT needed. Measured on this fixture against the fixed tree: impact(OrderRepo.DeleteItem, upstream) before: impactedCount 0, epistemic "exact" after: impactedCount 3, epistemic "lower-bound", with an interface boundary note; the three callers are OrderHandlers.Delete, PickService.StartSession and WaveService.Release — all correct. impact(CartRepo.Get, upstream) [concrete receiver, no interface] after: impactedCount 1, epistemic "exact" The second row is the one that matters for trust: the hedge discriminates instead of firing on everything, so "exact" still means exact. This test asserts the METHOD_IMPLEMENTS edges the probe walks. Pinning the mechanism keeps the resolver suite from reaching into the MCP layer while still failing loudly if the edges regress; the impact() numbers above are recorded in the commit message and PR body rather than re-asserted here. #2813 suite: 12 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(go): model Go method sets exactly so interface satisfaction is decidable (#2813) Replaces the approximate structural-interface model with the rules the Go spec actually defines, so the graph answers what the compiler answers instead of a useful-but-wrong summary of it. Three answers were provably wrong before; all three are now exact and covered. Method sets (go.dev/ref/spec#Method_sets): MS(T) = methods declared with receiver T MS(*T) = methods declared with receiver *T OR T Promotion (#Struct_types): S embeds T -> MS(S) and MS(*S) get promoted methods with receiver T; MS(*S) ALSO gets those with receiver *T S embeds *T -> MS(S) AND MS(*S) both get receiver T or *T Identifier identity (#Uniqueness_of_identifiers): "Two identifiers are different if they are spelled differently, OR IF THEY APPEAR IN DIFFERENT PACKAGES AND ARE NOT EXPORTED." func (b *Base) Ping() // pointer receiver type ByValue struct{ Base } type ByPointer struct{ *Base } type before exact answer Base IMPLEMENTS only *Base implements ByValue IMPLEMENTS only *ByValue implements ByPointer IMPLEMENTS the VALUE type implements All three were the same edge. Two of the three were wrong, and nothing in the graph could tell them apart. Worse, in a different direction: package sealed; type Sealed interface { seal() } package foreign; func (t *T) seal() {} `foreign.T` cannot implement `sealed.Sealed` in Go — `seal` is unexported, so the two identifiers are DIFFERENT. Matching on the bare name emitted a FALSE IMPLEMENTS edge, and the interface-dispatch fan-out then turned it into an impossible CALLS edge. That is the entire basis of the sealed-interface idiom. - `methodSetKey` qualifies UNEXPORTED method names with their declaring package, leaving exported names unqualified (which is what makes cross-package satisfaction work at all). Exactness, not a heuristic: the sealed case now emits no edge, while the legitimate same-package implementor is retained. - `collectStructMethodEntries` builds MS(T) and MS(*T) together and applies the promotion table above. The embed FORM is load-bearing, so it is now captured: `@reference.embedded-pointer` records `*T` versus `T`, which the parser previously discarded (the `*` is an unnamed token). - Detection returns `{ structDefId, receiverForm }`. `receiverForm: 'pointer'` means the value type does NOT implement and only `*T` does — the fact `var x I = T{}` turns on. - The form rides in the edge `reason` (`-structural-implements-pointer`). Relationships carry no arbitrary properties, so a new field would change the relation DDL, move SCHEMA_FINGERPRINT and force a rebuild for a fact a string already expresses. Value-form implementors keep the ORIGINAL unsuffixed reason, so a consumer matching the old string now sees exactly the assignable set — which is what that string always claimed to mean. - `emitInterfaceDispatchFor` walks the SUBTYPE CLOSURE (IMPLEMENTS + EXTENDS) and skips bodiless declarations, instead of stopping at depth 1. Two reproduced Java shapes emitted an edge to a second abstract declaration while the only class with a body got nothing: a sub-interface that re-declares the method, and an abstract base between interface and implementation. Both now reach the implementation and neither emits the declaration edge. - The fan-out is bounded by `MAX_INTERFACE_DISPATCH_FANOUT` (32, `GITNEXUS_MAX_INTERFACE_DISPATCH_FANOUT`) and reports what it dropped, mirroring `MAX_PROPERTY_DISPATCH_FANOUT`. A bare cap would silently discard valid dispatch targets, which is the same false-safe silence this issue is about. - Corrects a rationale comment that was factually wrong about the code 70 lines above it (Case 0 DOES branch on `site.kind`, at :713-716; what it lacks is a read/write branch in its reason/confidence computation). - Updates both copies of the case-ladder contract, which still described the fan-out as Case-4-exclusive. The embed-pointer marker is PARSE-TIME capture emission, so a warm cache would replay the pre-marker capture set and the distinction would never appear — silently, the v27/v30 failure mode. 43 and not 40 because origin/main allocated 40, 41 and 42 while this branch was in review, which is exactly the window this file's history records both prior EXACT clashes landing in. Pin moved with it. RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGE. - Go unit: 93 passed, including new rows pinning that `populateGoOwners` stamps `goReceiverKind` (previously the field had no reader and could rot silently) and that a pointer-receiver-only type implements in POINTER form only. - Cross-language sweep, test/integration/resolvers: 3034 passed, 1 skipped, 52 files, zero regressions. - scope-capture bench: PASS (15 languages). Go is the ONLY fingerprint that moved, which is the check that this is a Go capture change and not a cross-language regression; rebaselined with rationale. - Also closes review gaps in this PR's own tests: the concrete-field control was vacuous with respect to the type gate (repointed at a struct that IS an implementor), the two-service-file row could not distinguish the two files it is named for (both ends now file-qualified), plus new rows for signature mismatch, emitted confidence, and an exact N-by-M fan-out bound. An existing index must be re-analyzed; the schema bump forces it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
27ab37c432
|
feat(resolution): type receiver chains from AST structure across all 14 languages (#2708) + epistemic lower-bound (#2744) (#2747) | ||
|
|
ed8ab1c246
|
fix(scope-resolution): resolve callable reference flows (#2437) (#2522)
Some checks are pending
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (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
* docs(plans): add provider-hook value-refs plan (#2437) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(plans): deepen #2437 plan to USES + property-dispatch design Design revised after prior-art research (Kythe ref vs ref/call, Joern METHOD_REF, Feldthaus field-based call graphs, CodeQL impliedReceiverStep): registration sites emit reference-class USES, invocation is recovered by a field-based property-dispatch pass synthesizing CALLS at member-call sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scope-resolution): model provider-hook value references (#2437) Functions referenced as object-literal property values (provider hooks like emitScopeCaptures: emitCppScopeCaptures) previously produced no edge at all, so impact/context reported a false-safe 0 upstream dependents. Two coordinated halves, per prior art (Kythe ref vs ref/call, Joern METHOD_REF, Feldthaus ICSE'13 field-based call graphs, CodeQL impliedReceiverStep): - Registration -> USES: new ReferenceKind 'value-ref'; TS/JS queries capture pair values and shorthand properties (with @reference.property-key); emitted as a reference-class USES edge, reason 'scope-resolution: value-ref'. Resolution is callable-gated so plain values emit nothing. - Dispatch -> CALLS: new shared pass emitPropertyDispatchCalls synthesizes CALLS (reason 'property-dispatch', confidence 0.7, per-key fan-out cap 32 calibrated on this repo's 16-provider hook tables) from member-call sites to every function registered under the same property key. Deviation from plan: the pass owns value-ref resolution entirely via the post-finalize findCallableBindingInScope walker — the shared registries only see pre-finalize local bindings, so imported hooks (the c-cpp.ts case) were unresolvable through lookupForSite; Reference.propertyKey passthrough dropped as unnecessary. SCHEMA_BUMP 13 -> 14: ParsedFile gains value-ref sites + propertyKey. Verified end-to-end: impact(emitCppScopeCaptures, upstream) now reports 8 impacted / HIGH with extractParsedFile (true dispatch caller) at d=1 via property-dispatch and the c-cpp.ts registration via USES. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(scope-resolution): cover value-ref registration and property dispatch (#2437) Integration: same-file/cross-file/aliased/shorthand registrations emit USES; non-callable and destructuring values emit nothing; dispatch sites gain property-dispatch CALLS (incl. JS twins and per-language partitioning); fan-out-capped keys are dropped entirely; factory-call values unchanged. Unit: capture-shape pins for @reference.value-ref + @reference.property-key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): surface dropped property-dispatch keys in stats (#2437) Review finding: skippedKeys was returned but discarded — a hook table larger than the fan-out cap silently reopened the #2437 gap for those keys. Log dropped keys and fold value-ref USES + dispatch CALLS into referenceEdgesEmitted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(plans): add callable reference-flow implementation plan * fix(scope-resolution): close property-dispatch review gaps * feat(scope-resolution): add callable flow facts * feat(scope-resolution): resolve callable value flow * feat(scope-resolution): resolve callable references across providers * fix: harden callable reference flow resolution * fix(scope-resolution): preserve callable binding semantics * docs(plans): add pr-2522-review-fixes plan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): bump INCREMENTAL_SCHEMA_VERSION for callable-value-flow edges Callable-value-flow CALLS/USES edges (#2437) can connect two files whose content did not change, but the incremental write set only covers changed files — a top-up against a pre-v7 index would silently omit the new edges for every unchanged file pair, indefinitely. Force the one-time full re-analyze (review finding 1, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): sanitize callable-flow sites per-site at load, log drops The load-time validator rejected the WHOLE ParsedFile when one site was malformed or over-bound, with no logging — and C++ legitimately emits empty-string parameterTypes entries ('' = unknown, the ReferenceSite.argumentTypes convention) for cv-only/ERROR-recovered types, so real repos fell into a permanent, silent warm-cache-miss reparse loop through the #1983-sensitive main-thread path (review finding 7, #2522). Now: '' entries are valid in type arrays; a malformed/over-bound site drops only itself (counted, warned once per load); only non-array garbage — evidence the serialization itself is untrustworthy — rejects the file. Deviation from plan §6 wording: validator-side tolerance replaces emit-side clamps — smaller diff, same asymmetry closed at the single chokepoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): keep declarations in the union for reassigned callable cells The binding-lookup suppression for fact-constrained cells was wholesale: reassigning a declared function through its own name (greet = other; greet()) deferred the call to the solver, which then refused the lexical lookup that resolves the declaration — an unresolvable RHS yielded zero CALLS for a call that resolved pre-flow (review finding 8, #2522). Suppression now applies only to cells bound by FORMAL facts — its actual purpose (a parameter whose grammar emits no declaration binding must not adopt a same-named outer function). Copy/alias/store/load destinations keep their declaration as an inclusion seed (Andersen-style union). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scope-resolution): count forfeited deferred sites in the budget-bailout warning On work-budget exhaustion the deferred invoke sites end the run with zero CALLS — free-call fallback and reference emission already skipped them — but the warning said 'ordinary graph emission remains untouched', which is false for exactly those sites. The warning context now carries the unresolved deferred-site count and the comment states the real cost (review finding: budget-bailout honesty, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scope-resolution): surface dropped property-dispatch keys in stats and warn payload The over-cap warning carried only a count; the dropped key NAMES were discarded and RunScopeResolutionStats had no field, so the PR-body claim 'includes them in resolver statistics' was unimplemented (review finding, #2522; reviewer ask on the fan-out cap). The warn payload now names up to 20 dropped keys and the stats carry propertyDispatchSkippedKeys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(scope-resolution): drop producer-less ownerQualifiedName from formal sites No capture emitter anywhere produces @callable-flow.owner-qualified-name — the solver branch consuming it was unreachable in production, yet the field was typed, parsed, validated, and unit-tested with hand-built input (review finding 16, #2522; YAGNI). Re-add with a real producer if C++ qualified member declarators ever need it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(scope-resolution): drop dead callable-flow knobs CallableFlowPassingMode 'callable-object' had no producer and no consumer distinguishing it, and CallableFlowCaptureOptions.extractCallArguments had no language providing it (unlike its live sibling extractCallCallee) — review finding 17, #2522 (YAGNI). The invocation-kind 'callable-object' is a different, live concept and stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): bind subscripted callable cells to the container, not the index terminalIdentifier iterates children in reverse, so tbl[i] = handler seeded the INDEX variable's cell (polluting a same-named formal) and tbl[i](7) looked up the callee under i in a different scope — no join, no CALLS edge for the classic function-pointer-array dispatch (review finding 12, #2522). Subscript nodes now recurse into their container field only, in both bindingIdentifier and terminalIdentifier, across the fielded grammars (C/C++/JS/TS/Python/Go/Java). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): make cross-function file-scope callable bindings resolvable Two stacked gaps killed the canonical C callback-registration pattern (fp assigned in init(), called in run()) — the exact #2437 false-safe this PR exists to fix (review finding H1, #2522): 1. isVisibleValueBinding only consulted assignment regions and formals, so a call in a function OTHER than the assigning one emitted no invoke fact. A declared callable-typed binding is now a value binding wherever its declaration is visible (visibleCallableSignature). 2. The C scope query had no @declaration.variable pattern for function- pointer declarators — void (*fp)(int); created no scope-tree binding, so the seed (init) and invoke (run) cells canonicalized to different keys and never joined. Both bare and initialized forms now bind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(c): detect variadic parameters via the named variadic_parameter node tree-sitter-c materializes '...' as a named variadic_parameter node; the anonymous-token checks never matched, so variadic function-pointer signatures were emitted with a wrong fixed arity and no '...' sentinel (review finding, #2522). C++ is unaffected ('...' stays an anonymous token there); the token checks remain for such grammars. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): emit invoke facts for field-stored callable member calls The C ops-vtable pattern (o->run = handler; o->run(1)) captured the store but never the call — the member path in emitCallFacts bailed for languages without protocol methods, and the value-binding index recorded the member store under the OBJECT's name ('o'), not the member's ('run') (review finding 11/M3, #2522). Member destinations now also record their terminal member name, and a member call whose name-cell has a visible store emits an indirect invoke — gated on the store so plain accessor calls (map.get) stay inert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): disambiguate (obj->*ptr)() ERROR recovery by token order tree-sitter-cpp groups the recovered '->*' two ways depending on error-recovery cost (identifier lengths): [identifier, ERROR '->*m'] or [ERROR 'obj->*', identifier]. The recovery assumed the first shape, so the second silently swapped receiver/member and dropped the call site — the committed test passed only by name luck (review finding H2, #2522). The identifier's position relative to '->*' inside the ERROR now decides roles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): class members are never file-local in hasFileLocalCallableLinkage The name-keyed file-local set is populated from every static declaration, so an in-class 'static void make();' (external linkage — in-class static means no-instance) and any member sharing a name with a static free function were over-marked, refusing legitimate cross-file declaration/definition joins (review finding 13/M2, #2522). Method and Constructor defs now bypass the name-set, per the hook's own linkage-only contract. Deviation from plan step 13: the regression is a unit-level contract pin rather than an end-to-end join test — C++ merges out-of-line member definitions onto the member node by qualified identity, so the graph shape cannot discriminate the join refusal for members. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): classify parameter passing mode from the declarator chain only A whole-subtree scan for reference_declarator inverted copy vs alias: void reg(void (*cb)(int& out)) marked the by-value pointer cb as 'reference' because of the NESTED parameter's int&, making the solver back-propagate formal targets into every caller's argument cell — alias semantics for a copy (review finding 14/M5, #2522). The chain walk never descends into nested parameter lists; a reference anywhere ON the chain (int& x, void (*&cb)(int)) still aliases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ruby): bare identifiers are calls, not callable references Ruby parses a receiver-less zero-arg method call identically to a variable read, so 'action = process' — which CALLS process and stores its return — seeded action with the callable and minted a wrong CALLS edge from any dispatch through it, confirmed end-to-end (review finding 15/HIGH, #2522). New provider knob bareNamesAreCalls: a bare name that is not a provably local value binding and not an explicit reference form (method(:x), lambda/proc) emits no flow fact, on both the assignment and argument paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(go): pair multi-value := positionally instead of cross-wiring The shared field fallback took the FIRST LHS identifier and the LAST RHS identifier of Go's expression_list pair, cross-wiring 'a, b := f, g' and synthesizing a garbage comma-joined qualified name — the real relationships were silently dropped (review finding 16, #2522). extractAssignment may now return multiple pairs; Go pairs list entries positionally and emits nothing for a length mismatch (multi-return call RHS). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(java): drop get/test from callableProtocolMethods 'get' and 'test' collide with ubiquitous non-functional-interface APIs (Map/List/Optional/Future.get), so every ordinary container access emitted a spurious callable-object invoke fact — high-volume misleading graph facts with a cross-wiring risk on receiver-name reuse (review finding 17, #2522). Supplier.get/Predicate.test dispatch is deliberately traded away until the check can gate on the receiver's declared type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rust): pin the qualified-name no-degrade guard as a hard invariant Rust's scoped_identifier callable-reference capture over-includes unit enum variants and associated constants (Shape::Square seeds as if callable); they stay edge-free only because resolveSeedCandidates refuses to degrade an unresolved qualified name to a simple-name lookup (review finding 18, #2522). Capture-side type filtering would false-negative on tuple-variant constructors, so the guard IS the contract: documented as a hard invariant (Go's mis-shaped multi-value forms also rely on it) and pinned end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(php): remove nonexistent optional_parameter node type tree-sitter-php has no 'optional_parameter' — defaults ride on simple_parameter — so the entry was dead weight the #1920 literal gate does not cover for capture-option Sets (review finding 19, #2522). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cobol): detect procedure pointers on fixed-format sources Two stacked defects made the feature a no-op on classic sequence-numbered fixed format (review finding 20/H3, #2522): 1. parseDataItemClauses' USAGE alternation knew POINTER but not PROCEDURE-POINTER/FUNCTION-POINTER, so the dataItems filter was dead. 2. The raw-line fallback scanned UNCLEANED text, where the sequence number satisfied the leading digits and the LEVEL NUMBER got captured as the pointer name. It now scans preprocessed lines and requires a letter- initial name (COBOL data names must contain a letter). 161 COBOL preprocessor/copy-expander tests stay green; free-format matrix case unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cobol): skip comment lines in SET seed/copy scans A commented-out SET (indicator-column '*'/'/' or free-format '*>') produced a live seed and a false CALLS edge from dead code (review finding 21/M1, #2522). The scan now skips indicator-column comment lines and strips inline '*>' tails before matching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(architecture): document callable-flow-only mode and skipped-key reporting The Callable-value flow section omitted scopeResolutionEdgeMode: 'callable-flow-only' — a real emit-pipeline branch that suppresses all ordinary emission for standalone providers (review finding 22, #2522) — and predated the skipped-key names/stats surfacing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(scope-resolution): correct value-ref resolution attribution and stale pdg-gating comments The value-ref contract comment claimed MethodRegistry resolution — the mechanism is the post-finalize findCallableBindingInScope walker owned by emitPropertyDispatchCalls (resolveReferenceSites skips these sites). Three 'only under --pdg' calleeIdSink comments were falsified by the #2437 gating change (callee-id-sink.ts's header was updated; these copies were missed). Review finding 23, #2522. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(ingestion): direct unit coverage for synthesizeCallableFlowCaptures The 1,100-line shared synthesizer had no test naming it — only downstream consumers were covered (review finding 24, #2522). Pins seed/invoke/ formal/argument emission, subscript container binding, store-gated member invokes, produced-value guards, and the bareNamesAreCalls knob over a minimal options object so assertions target the synthesizer's own semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(resolvers): deepen shallow-language coverage; fix Kotlin/Swift reassignment gaps it exposed Adds the COBOL SET x TO y copy-branch scenario and conditional-assignment scenarios for Kotlin, C#, Swift, and Dart (10 languages previously had one generic case each — review finding 25, #2522). The new scenarios exposed two real capture gaps, fixed here: - tree-sitter-kotlin's 'assignment' node is fieldless, so nested reassignments (chosen = ::target inside a block) produced no flow facts; Kotlin's extractAssignment now decomposes it positionally. - tree-sitter-swift fields its assignment as target:/result:, neither in the shared fallback's field lists; both added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(infra): literal-validation gate for callable-capture option Sets The #1920 gate validates query literals and exported configs but not the module-private *_CALLABLE_CAPTURE_OPTIONS Sets consumed by the shared synthesizer — a typo'd node type silently captures nothing (PHP shipped a dead 'optional_parameter'; review finding 26, #2522). Every <key>NodeTypes Set literal is now validated against its language's grammar; name-carrying sets (callableProtocolMethods, memberPointerOperators) are deliberately outside the contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(storage): centralize corrupt-fixture casts into makeStoreEntry The callable-flow store tests scattered 'as unknown as' double-casts per fixture (review finding 27, #2522; standing no-as-any rule). One typed helper now owns the single controlled escape hatch for building malformed serialization-boundary payloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(bench): refresh capture fingerprints after review fixes python-scope: the committed baseline (8d5c3699) never matched this branch's code — CI's benchmarks arm was red on the PR head (review finding 2/HIGH, #2522); regenerated (a99e69ab), scaling 1.04 in budget. scope-capture: ruby/cpp/swift/java/kotlin drifted from the review-fix commits (bare-name suppression, passing modes + ->* recovery, assignment fields, protocol narrowing, positional assignment); all 14 languages re-verified PASS with ratios <= 1.18 against the 1.5 budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(docs): untrack docs/plans working documents docs/ is gitignored (local working docs); the plan files were force-added past the ignore. Untracked from the index only — they stay on disk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(golden): regenerate captures goldens after callable-flow review fixes The per-language digest guards (csharp/go/php/python/ruby/rust/swift) locked the pre-fix capture output; the review-fix series intentionally changed it — store-gated member invokes, subscript container binding, Ruby bare-name suppression, Swift assignment fields, positional pairing. Regenerated with UPDATE_GOLDEN=1; clean verification run 59/59; all other parity/golden guards (pipeline-graph, spring-route, python parity) pass untouched at 33/33. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): prototypes are callees, not callable value cells The cross-function visibility fix indexed EVERY signature-bearing declaration as a value binding — including plain function/method prototypes (void f(int);). Every call to a declared function then became an indirect invoke, and with emitCanonicalInvokeReference (C/C++) minted a free-call reference that resolved through the registry, bypassing the precise passes' two-phase/ambiguity/subobject suppression — eight phantom CALLS edges in the cpp resolver suite on CI. Only declarations whose binding identifier sits under a pointer/ parenthesized declarator (callable-typed variables like void (*fp)(int);) create value cells now. cpp resolver suite 331/331; callable-value-flow + C/C++ suites 181/181 (the cross-function fp regression still passes); cpp fingerprint rebaselined, both bench gates PASS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c60ad9f7ab
|
fix(ingestion): fully-qualified nested-type identity for C++/Ruby — structure (#1978) + resolution (#1982) (#1981)
* fix(ingestion): qualify nested-type node identity for C++/Ruby (#1978) Nested types sharing a tail name in one file — C++ `Outer::Inner` vs `Other::Inner`, Ruby `Outer::Inner` vs `Other::Inner` modules — silently merged into a single graph node keyed by the simple tail (`Struct:file:Inner`), cross-wiring their methods/properties onto one owner. Key class-like type nodes (Class/Struct/Interface/Enum/Record) by their normalized fully-qualified path (`Struct:file:Outer.Inner`) instead of the simple name. Gated per-language by a new `qualifiedNodeId` config flag (default false → byte-identical for every other language); enabled here for C++ and Ruby. - class-types.ts / generic.ts: `qualifiedNodeId` flag on ClassExtractor + config - ast-helpers.ts: findEnclosingClassInfo gains an optional getQualifiedOwnerName hook + EnclosingClassInfo.qualifiedClassId, so member-owner edges resolve to the qualified class node id (owner id == node id by construction) - parsing-processor.ts + parse-worker.ts: flag-gated qualified node-id + owner edges on both the sequential and worker parse paths (incl. routed properties) - call-processor.ts: same qualifier in the routed-property pre-pass (lockstep with the worker `kind === 'properties'` block) - configs/c-cpp.ts, configs/ruby.ts: qualifiedNodeId: true Method/Property node ids stay simple-qualified; only type nodes get the qualified id. Deferred to a resolution-side follow-up: Ruby SAME-TAIL routed-property/mixin owner identity under registry-primary (`emitRubyMixinEdges` keys owners by the simple tail name, last-wins); and Rust inherent-impl methods (impl_item is not a typeDeclaration — its #1978 test is describe.skip). Tests: same-tail collision fixtures + #1978 resolver tests for C++/Ruby (positive owner identity, R7), a worker-path parity block, and an unambiguous nested attr_accessor case; the C++ #1975 out-of-line test updated to assert qualified-id distinctness (forward-decl + out-of-line now unify). Verified green on both parity legs, the worker path, and tsc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): scope #1978 resolver tests to registry-primary leg; fix lint - helpers.ts: exclude the new #1978 C++/Ruby resolver tests from the legacy parity leg (LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES). They PASS on legacy too — the fix lives in the SHARED structure phase, not the legacy resolution path — so this is a deliberate registry-primary-only scoping (not a legacy gap), keeping the legacy path untouched and uncoupled from the new node-identity behavior. - rust.test.ts: drop the `eslint-disable vitest/no-disabled-tests` directive. That rule isn't configured in this repo, so eslint errored "Definition for rule 'vitest/no-disabled-tests' was not found" and failed `quality / lint`. The describe.skip needs no disable directive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): satisfy CI for the new #1978 fixtures (format + golden + fingerprint) Adding the {cpp,ruby,rust}-nested-tail-collision fixtures changed the lang-resolution corpus, which the scope-capture golden snapshots and the fingerprint baselines gate on. These are pure fixture-corpus additions — #1978 does not touch the scope-capture phase (captures.ts / emit*ScopeCaptures are unchanged). Verified: the regenerated ruby/rust golden diffs are additive-only (no existing fixture's capture digest changed), so the cpp/ruby/ rust fingerprint drift is solely the new fixtures. - prettier --write test/integration/resolvers/{ruby,rust}.test.ts - regenerate ruby/rust captures-golden snapshots (UPDATE_GOLDEN=1; +1 fixture each) - rebaseline cpp/ruby/rust scope-capture fingerprints (bench/scope-capture/baselines.json) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): extract shared qualified-name normalizer (#1982) Move normalizeQualifiedName/splitQualifiedName out of class-extractors/ generic.ts into utils/qualified-name.ts so the structure-phase buildQualifiedName, the scope-resolution inheritance resolver, and the per-language capture emitters can all key against ONE normalizer. A raw '::' qualifier must normalize to the exact '.'-joined key the QualifiedNameIndex already holds, or the qualified lookup silently misses (the #1982 resolution-side foundation). Pure relocation — byte-identical function bodies; tsc clean; existing C++ nested-collision tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve same-tail C++ nested-type heritage to the correct qualified node (#1982) Registry-primary C++ inheritance (preEmitInheritanceEdges -> resolveInheritanceBaseInScope) resolved a same-tail nested base by its SIMPLE TAIL with first-wins, so `struct DerivedB : Other::Inner` mis-resolved EXTENDS to Outer.Inner (the wrong sibling; 0 dangling, so undetected). The namespace qualifier was discarded at the C++ inheritance capture. Fix (additive, qualified-first): - ReferenceSite gains an optional `rawQualifiedName`; the C++ inheritance capture emits `@reference.qualified-name` (qualifier-preserving, template-stripped: Other::Inner, ns::Base<T> -> ns::Base) only when the base is qualified, registered as a sub-tag so it can't shadow the `@reference.inherits` anchor. - resolveInheritanceBaseInScope resolves the qualifier against the full-path QualifiedNameIndex FIRST (which already carries Outer.Inner / Other.Inner keys from the structure phase), with progressive-prefix lookup for relative bases and refuse-on-tie, falling through to the existing simple-tail walk on miss — so unqualified bases and the single-candidate cross-file case are unchanged. Registry-primary cpp.test.ts 278/278 (incl. worker-path: rawQualifiedName survives worker serialization). Legacy leg unaffected (207 pass / 71 skip) — the new resolution-side assertions are registry-primary-only via helpers.ts. tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve same-tail Ruby mixin/attr_accessor owners to the correct qualified node (#1982) emitRubyMixinEdges keyed its owner map by the SIMPLE tail (def.qualifiedName split-popped) with last-wins, and the __heritage__/__property__ markers carried only the immediate owner name — so `module Outer; class Inner` and `module Other; class Inner` collapsed onto one `Inner` key and cross-wired their include/attr_accessor edges onto whichever Inner was processed last. Fix (lockstep, full-qualified): - ruby/captures.ts: build the marker owner from the FULL enclosing class/module chain (buildEnclosingQualifiedName walks all ancestors, normalizing the compact `class Outer::Inner` scope_resolution form via the shared splitQualifiedName) so the marker owner byte-matches the resolution def's qualifiedName. - ruby/scope-resolver.ts: key graphIdByName by the full def.qualifiedName instead of the simple tail. Top-level owners/mixins are unchanged (full == simple). Registry-primary ruby.test.ts 142/142 incl. a new worker-path block (the deferred note's duplicate-edge concern: markers survive worker serialization, exactly one HAS_PROPERTY per attr). Legacy leg unaffected (136 pass / 6 skip) — new assertions registry-primary-only via helpers.ts. tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): rebaseline #1982 golden/fingerprint + lint/format sweep Cross-cutting verification artifacts for the #1982 same-tail resolution fix: - ruby capture golden regenerated: ONLY the ruby-nested-tail-collision fixture drifts (+10 capture groups from its new include/attr_accessor + the now full-qualified __heritage__/__property__ marker owner). All other ruby fixtures byte-identical (proves the owner-qualification is localized to nested owners). - bench/scope-capture/baselines.json: rebaseline cpp + ruby fingerprints (the only two that drift; 12 other languages byte-identical). cpp = additive @reference.qualified-name capture; ruby = the localized owner change. Provenance notes record both. scaling linear (~1.0), 14/14 PASS. - generic.ts: drop the now-unused normalizeQualifiedName import (lint error). - walkers.ts / ruby.test.ts: prettier formatting. Verified: cpp 278/278 + ruby 142/142 (registry-primary), both legacy legs clean (skips registry-primary-only assertions), go/java/csharp 542 (cross-language regression — the qualified-first branch is gated on rawQualifiedName, set only by C++, so non-C++ inheritance resolution is unchanged). tsc + eslint(0 errors) + prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve nested Ruby mixin included by short name (#1982) emitRubyMixinEdges keyed graphIdByName by the full def.qualifiedName on the owner side, but the __heritage__ marker carries the mixin target as the bare written name (arg.text). A nested mixin module included by its short name (include Loggable where it is App::Loggable) missed the full-qn map and its IMPLEMENTS edge was silently dropped (0 dangling, undetectable). The shipped same-tail fixture used only top-level mixin modules, so CI stayed green. Add a secondary simple-tail fallback map consulted only when the full-qn mixin lookup misses; owner lookups stay full-qn so same-tail owner disambiguation is preserved. Characterization test + fixture (registry-primary only); golden regenerated additively. Addresses PR #1981 review (4417182679) P1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): normalize qualified Ruby mixin arg in heritage marker (#1982) `include Outer::Mixin` embedded the raw `Outer::Mixin` into the ':'-delimited __heritage__ marker, so the `::` collided with the field separator and emitRubyMixinEdges mis-split it (className became empty), dropping the IMPLEMENTS edge. Normalize the mixin arg via splitQualifiedName(...).join('.') before emit so the marker carries the dotted form, which both parses correctly and matches the mixin def's qualifiedName. Simple names are unchanged (no golden drift). Addresses PR #1981 review (4417182679) secondary R2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): resolve C++ same-tail nested heritage inside a namespace (#1982) A namespace-nested C++ type's scope-model qualifiedName carried its enclosing CLASS chain (A.Inner) but dropped the enclosing NAMESPACE, while the structure-phase graph node is keyed by the full path (NS.A.Inner). resolveDefGraphId's qualifiedKey therefore missed and fell back to simpleKey('Inner'), collapsing same-tail nested bases across sibling namespace members — DB : B::Inner pointed at NS.A.Inner. The shipped fixture was top-level only, so it could not catch this. Fix without disturbing the qualifiedName-keyed resolution index (an earlier attempt that rewrote qualifiedName regressed brace-init / UDC / two-phase namespace resolution): tagNamespacePrefixes records each namespace-nested def's enclosing-namespace prefix on a sidecar field, and resolveDefGraphId retries the node lookup with the namespace-prefixed key before the simpleKey fallback. The helper is language-agnostic (acts only on Namespace scopes) and opt-in — only the C++ provider calls it. Namespaced fixture + sequential & worker tests (registry-primary only). All 280 cpp resolver tests pass; tsc clean. Addresses PR #1981 review (4417182679) P2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): worker-path parity for Ruby mixin IMPLEMENTS + C++ DerivedA (#1982) The Ruby worker-path parity block asserted only attr_accessor (HAS_PROPERTY); add an IMPLEMENTS assertion so a dropped/cross-wired mixin owner on the worker path is caught (the __heritage__ marker owner must survive serialization). The C++ worker heritage block asserted only DerivedB; add a DerivedA assertion with a toHaveLength(1) duplicate guard. Registry-primary only. Addresses PR #1981 review (4417182679) test-coverage gap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): distinct Rust same-tail nested-mod inherent-impl ownership (#1982) Rust methods live in `impl Inner` blocks, and findEnclosingClassInfo keyed the inherent-impl owner by the target's RAW tail (`Impl:lib.rs:Inner`), so two same-tail `impl Inner` blocks under different mods (mod outer / mod other) collapsed onto ONE Impl node and their methods cross-wired. The shipped fixture test for this was skipped/deferred. Qualify an UNSCOPED inherent-impl target by its enclosing `mod_item` scope (`outer.Inner`) in BOTH the owner walk (ast-helpers.qualifyRustImplTargetByModScope) and the Impl-node materialization (parsing-processor + parse-worker, lockstep) so the owner edge and node id agree byte-for-byte. Gated on the Impl label + impl_item + an unscoped type_identifier target — Rust-impl-exclusive, so C++/Ruby and the rust captures golden are untouched; a SCOPED `impl a::Inner` keeps its full raw text (#1975, unchanged). The previously-skipped distinct-ownership test is now active and passing; rust 170/170, cpp+ruby+golden 437/437, tsc clean. Done in-PR at maintainer request (was deferred as a follow-up). Addresses PR #1981 review (4417182679) test-coverage gap R7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ingestion): single qualified-name normalizer + module-scoped Ruby PROPERTY_PREFIX (#1982) Replace cpp/captures.ts's parallel normalizeCppNamespaceQName with the shared normalizeQualifiedName (behaviorally equivalent for C++ qualified-identifier inputs: '::'->'.' with leading/trailing-:: handling; no interior whitespace reaches it). Promote Ruby's PROPERTY_PREFIX to module scope alongside HERITAGE_PREFIX (was function-local — asymmetric with no behavioral effect). Maintainability only; cpp+ruby resolver suites 428/428, tsc clean. Addresses PR #1981 review (4417182679) maintainability item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf+fix(ingestion): single enclosing-class walk + root-anchored base guard (#1982) U7 (perf): preEmitInheritanceEdges resolved the deriving class AND resolveQualifiedInheritanceBase re-walked findEnclosingClassDef for the same site. Resolve callerClass once and thread it into resolveInheritanceBaseInScope -> resolveQualifiedInheritanceBase -> enclosingScopeSegments, so the enclosing class is walked once per qualified site. Add a 'program' early-exit to buildEnclosingQualifiedName (ruby/captures.ts). Behavior-preserving. U8 (P3): a root-anchored C++ base ": ::A::Inner" names the GLOBAL type, but resolveQualifiedInheritanceBase prepended the deriving class's enclosing segments and could mis-bind to an enclosing-relative same-path type. Detect the leading "::" on the raw qualifier and try only the root-anchored key. Discriminating fixture + test (registry-primary only). cpp+ruby+rust resolver suites 599/599; tsc clean. Addresses PR #1981 review (4417182679) perf + P3 items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): rebaseline ruby+cpp scope-capture fingerprints for new #1982 fixtures The four new fixtures (ruby-nested-mixin-shortname, ruby-qualified-mixin, cpp-namespaced-collision, cpp-global-base-anchor) grow the lang-resolution corpus, drifting the ruby and cpp order-independent capture fingerprints. Verified purely additive: the ruby captures golden shows only the two new fixtures added (existing byte-identical), and removing the two cpp fixtures reverts the cpp fingerprint to the prior baseline (so the U3/U6/U8 code changes are scope-resolution / behavior-preserving, not capture-emission). measure.mjs --check PASS (14 languages). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(ingestion): prettier-wrap ruby resolver test call (#1982) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
04ade15451
|
fix(rust): scope-resolution coverage gaps — F66,F68,F71,F72 (#1934) (#1974)
* fix(rust): scope-resolution coverage gaps — F66,F68,F71,F72,F73 (#1934) * fix(rust): reviewer fixes — macro namespace, revert pattern:(_), drop variadic * fix(rust): wire macro resolution end-to-end + materialize unions (#1974 review) Addresses the outstanding #1974 review (second batch). Per maintainer decision, F72 is FULLY WIRED rather than documented capture-only. F72 macro — was a capture-only no-op (@reference.macro dropped downstream): - gitnexus-shared: add 'macro' ReferenceKind + Reference.kind; add MACRO_KINDS (['Macro']) and a MacroRegistry that resolves a macro invocation ONLY to a macro_rules! definition — never a same-named free function (the disjoint-namespace guarantee the review required). - scope-extractor: referenceKindFromAnchor @reference.macro -> 'macro'; normalizeNodeLabel 'macro' -> Macro. - resolve-references: route 'macro' sites through MacroRegistry. - emit-references / graph-bridge edges: 'macro' -> USES (kept out of the CALLS keyspace, which denotes function/method dispatch). - node-lookup isLinkableLabel: Macro is linkable, bridging the registry def to the legacy @definition.macro graph node. - rust query: capture macro_rules! as @declaration.macro; fix the scoped macro arm to capture the tail identifier, not the full path (P3). F71 union — the @declaration.struct scope capture had no graph node to resolve to (legacy RUST_QUERIES never captured union_item): - legacy query: capture union_item as @definition.struct so the union is materialized as a Struct node and is genuinely resolvable. - query.ts: document the deliberate union->Struct downgrade rationale. Tests: - rust.test.ts (parity-gated): pipeline-level union resolution + macro resolution (USES to the Macro, exactly one CALLS to fn, none to Macro). Macro resolution is registry-primary-only -> listed in LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES['rust']. - rust-coverage.test.ts: scoped-macro tail + macro-def capture assertions; reframed as capture-layer only, pointing at the pipeline tests. - new fixtures rust-macro, rust-union. F73: dropped from baselines.json _note (variadic was never implemented). Rebaselined the rust capture golden + scope-capture fingerprint (a5fdff2c..., scaling ~0.99, fixture_count 126). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(rust): prettier-format the Reference.kind union (#1974) CI quality/format gate — collapse the multi-line 'macro' addition back to one line (fits the 100-col print width). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <abhigyan1.patwari@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5f0c0eba0e
|
feat(cpp): Expand type_traits constraint registry (#1648) | ||
|
|
a7b3fa1b81
|
feat(csharp): migrate C# to registry-primary scope-resolution (Closes #934) (#1019)
* feat(csharp-scope): unit 1 — scope query + captures orchestrator First slice of the C# scope-resolution migration (issue #934, RFC #909 Ring 3). Closes `Unit 1` of docs/plans/2026-04-21-004-feat-csharp-scope-resolution-plan.md. Adds: - src/core/ingestion/languages/csharp/query.ts — tree-sitter scope query covering compilation_unit, namespace (block + file-scoped), class-like (class/interface/struct/record/enum), method-like (method/constructor/destructor/local_function/operator), property and field declarations, using directives, type bindings (parameter annotations, local variable annotations, constructor inference, invocation alias), and references (free call, member call including null-conditional, constructor call, member write). - src/core/ingestion/languages/csharp/captures.ts — pass-through orchestrator mirroring python/captures.ts. Import decomposition (Unit 2), receiver-type-binding synthesis (Unit 3), and arity metadata synthesis (Unit 5) stub out for future units. - src/core/ingestion/languages/csharp/cache-stats.ts — PROF instrumentation mirror of python/cache-stats.ts. Design notes: - Return-type / field-type / property-type captures deferred. tree-sitter-c-sharp does not expose these under a clean named field that pattern-matches. When Unit 7 parity gate surfaces a gap, add positional patterns or a post-hoc extractor lookup. - object_creation_expression with qualified_name type — the qualified name itself is the reference text; captured as a whole via a dedicated tag so interpretation in later units can split namespace + name. - Null-conditional calls use positional descendant patterns because tree-sitter-c-sharp's member_binding_expression and conditional_access_expression don't expose named fields. Coverage: - 23/23 new unit tests in test/unit/scope-resolution/csharp/csharp-captures.test.ts cover every capture tag. Confirmed against tree-sitter-c-sharp via the probe-script loop during development; grammar drift would surface as a capture-shape assertion failure. - tsc --noEmit clean. No changes to shared infrastructure. Resolver wiring + registration land in Unit 6. * fix(csharp-scope): capture null-conditional receiver + operator decls Adversarial review surfaced two Unit 1 bugs that would silently corrupt the graph once C# is flipped on the scope-resolution path: - `obj?.Save()` only emitted @reference.name, so receiver-bound resolution downgraded to the free-call fallback and could mis-link to an imported `Save`. Capture the conditional_access_expression receiver under @reference.receiver. - `operator_declaration` had @scope.function but no @declaration.method owner, so calls inside operator bodies were attributed to the enclosing class and the operator itself disappeared from method lookup. Capture the operator token as @declaration.name (downstream csharpMethodConfig normalizes to op_Addition etc.). - `conversion_operator_declaration` was missing from both scope and declaration sets. Added with the target type as the name anchor. Arity metadata for overload resolution remains deferred to Unit 5 and gated behind Unit 7's parity flip, as documented in captures.ts. * chore(scope-resolution): drop unused python/scopes.scm sibling The file was documentation-only — the authoritative scope query is the embedded `PYTHON_SCOPE_QUERY` constant in `python/query.ts`. Nothing loaded the `.scm` at runtime, so it drifted from the code. Remove it and update the four doc comments that pointed at it: - language-provider.ts: "scopes.scm query" → "scope query (embedded in each language's query.ts)". - languages/python.ts: capture-vocabulary pointer → query.ts. - python/query.ts header: drop the "edit both together" note. - python/receiver-binding.ts: "keeps the .scm declarative" → "keeps the embedded scope query declarative". - scope/walkers.ts: "Python's scopes.scm" → "Python's scope query". Historical plan docs under docs/plans/ still reference scopes.scm but are frozen artifacts, not living documentation. C# never had a .scm sibling, so no action needed there. * feat(csharp-scope): Unit 2 — import interpret + target resolver Adds the three files Unit 2 of the C# scope-resolution plan calls for: - `import-decomposer.ts` — inspects each `using_directive` node and synthesizes `@import.kind/source/name/alias` markers. Kinds: `namespace` — `using X;` / `using X.Y.Z;` `alias` — `using Alias = X.Y.Z;` (generics stripped) `static` — `using static X.Y;` `global using` maps to namespace (plan's deferred decision); the `global::` qualifier is stripped before emitting. - `interpret.ts` — reads the markers and builds `ParsedImport`. Static using maps to `kind: 'wildcard'` since it brings members into unqualified scope; Unit 4's merge-bindings tiers wildcards lowest. Also provides `interpretCsharpTypeBinding` with nullable/single-arg generic/qualifier stripping so receiver-typed resolution sees the concrete class name. - `import-target.ts` — suffix-match adapter returning a single primary file. Cross-file partial-class aggregation runs later at graph-bridge time (Unit 6). The csproj-based `resolveCSharpImportInternal` stays on the legacy path until Unit 7's parity gate surfaces a gap. - `captures.ts` routes `@import.statement` matches through the decomposer so the interpreter sees the markers it needs. Tests cover every using flavor + resolution edge cases. 38/38 scope- resolution C# unit tests pass; tsc clean. * feat(csharp-scope): Unit 3 — simple hooks (binding/import/receiver) Adds simple-hooks.ts mirroring Python's pattern: - `csharpBindingScopeFor` — delegates to innermost (block scope is already captured by @scope.block in the query). - `csharpImportOwningScope` — binds `using` inside a namespace to that namespace's scope so imports don't leak into sibling namespaces. File-level using delegates to module. Function-body using (not legal C# but possible from malformed input) attaches to the function. - `csharpReceiverBinding` — looks up `this` / `base` in the function scope's type bindings; returns null for statics, free functions, and non-Function scopes. `this` / `base` synthesis itself is deferred to a follow-up (matches Python's receiver-binding.ts pattern). 9 new tests pin delegation semantics. 47/47 C# scope-resolution unit tests pass; tsc clean. * feat(csharp-scope): Unit 4 — mergeBindings (using precedence) Three-tier shadowing, same shape as Python's LEGB merge: 0: local — class members, locals, parameters 1: using — namespace / named / reexport (equal tier; compiler requires explicit qualifier if two using collide) 2: wildcard — `using static X.Y;` static-member imports Within the surviving tier, de-dup by DefId (last-write-wins) so a re-declared `using` cleanly replaces its earlier binding. Explicit interface implementations bind under their qualified name in the extractor layer, so they don't collide with plain simple names here. 7 new tests pin precedence + dedup semantics. 54/54 C# scope-resolution unit tests pass. * feat(csharp-scope): Unit 5 — arity metadata synthesis + compatibility Adversarial review flagged overload narrowing as a blocker for the Unit 7 flip. This lands the declaration-side metadata; callsite-side arity synthesis is a separate gap we'll address if the parity gate surfaces overload misresolution. - `arity-metadata.ts` — reads `csharpMethodConfig.extractParameters` and produces `{ parameterCount, requiredParameterCount, parameterTypes }`. `params` variadic collapses parameterCount to undefined (matches Python's `*args` treatment) and appends a literal `'params'` marker to parameterTypes so the compatibility hook can detect it without re-reading the AST. Default-valued parameters contribute to optionalCount → requiredParameterCount = total − optional. - `arity.ts` — `csharpArityCompatibility(def, callsite)` returns compatible / incompatible / unknown. Mirrors Python's three-verdict shape so the central registry's arity filter works without adapter logic per-verdict. - `captures.ts` — on every @declaration.method / @declaration.constructor / @declaration.function match, synthesize @declaration.parameter-count, @declaration.required-parameter-count, and @declaration.parameter-types captures. Covers method_declaration, constructor_declaration, destructor_declaration, operator_declaration, conversion_operator_declaration, and local_function_statement. 12 new tests: 5 on captures-side synthesis (method + params + types + variadic + constructor + local function), 7 on the compatibility hook. 66/66 C# scope-resolution unit tests pass; tsc clean. * feat(csharp-scope): Unit 6 — wire csharpScopeResolver + register Creates the public barrel (index.ts) and ScopeResolver (scope-resolver.ts) and plumbs them into the provider + registry: - `languages/csharp/index.ts` — re-exports the hook entry points and documents the 8 known limitations of the registry-primary path (csproj-driven namespace resolution, multi-file namespace expansion, type-based overload resolution, nested generics, dynamic, preprocessor branches, cross-file global using, expression-bodied members). - `languages/csharp/scope-resolver.ts` — ScopeResolver shape mirroring Python's. `isSuperReceiver` matches the literal `base` keyword. `fieldFallbackOnMethodLookup: false` since C# is statically typed — the type-binding layer already produces precise owner types; `propagatesReturnTypesAcrossImports: true` since signatures are authoritative. - `languages/csharp.ts` — adds the 9 hook entry points to the provider (emitScopeCaptures, interpretImport, interpretTypeBinding, four simple hooks, mergeBindings, arityCompatibility, resolveImportTarget). - `scope-resolution/pipeline/registry.ts` — registers csharpScopeResolver alongside the Python entry. MIGRATED_LANGUAGES stays at {Python} — the resolver sits idle until Unit 7's parity gate confirms ≥99% fixture parity. 368/368 scope-resolution unit tests pass; tsc clean. * feat(csharp-scope): parity Unit 1 — this/base receiver-binding synthesis Closes 3 parity failures (51 → 48). Target bucket: Category C from the parity plan. Changes: - `languages/csharp/receiver-binding.ts` (new): walks up from a function node to the enclosing class/struct/record/interface, synthesizes `@type-binding.self` captures with boundName `'this'` (and `'base'` when the enclosing type is a class/record with an explicit base_list entry). Skips static methods and interface / struct `base` cases. Anchors to the method's `body` block so the scope-extractor's positionIndex places the binding inside the function scope (not the enclosing class scope). - `languages/csharp/captures.ts`: route `@scope.function` matches through the synth, emitting the receiver captures as separate matches. - `languages/csharp/interpret.ts`: map `@type-binding.self` to `source: 'self'` (parity with Python). - `languages/csharp/query.ts`: explicit patterns for `this.X()`, `base.X()`, and `this.X = ...` / `base.X = ...` assignment writes. `this` and `base` are anonymous tokens in tree-sitter-c-sharp so the existing `expression: (_)` pattern (named-only) didn't match. Tests: - 8 new unit tests for receiver-binding synthesis edge cases (class/struct/record/interface, static, nested, constructor, local function inside method). - Parity: 48 failed | 127 passed (175) under REGISTRY_PRIMARY_CSHARP=1; legacy path 175/175 green. * feat(csharp-scope): parity Unit 2a — foreach + pattern + field captures Closes 11 parity failures (48 → 37). Partial Unit 2 progress. Adds type-binding captures for every shape the parity suite exercises whose resolution path is in-file: - Typed foreach `foreach (User u in xs)` — @type-binding.annotation with bindingName `u` and type `User`. - Var foreach `foreach (var u in xs)` — @type-binding.alias so the generic-stripper unwraps `List<User>` / `Dictionary<K,V>.Values` to the element type at chain-follow time. Matches Python's for-loop alias pattern. - `is` pattern `if (obj is User u)` — @type-binding.annotation with scope narrowing simplified to function scope (matches Python's match-case treatment since we don't emit @scope.block). - `switch_section > declaration_pattern` (`case User u:`) — no case_pattern_switch_label wrapper in tree-sitter-c-sharp. - `recursive_pattern` (`is User { Age: 1 } u` / `case User { ... } u:`) — named binding via type+name fields on the pattern node. - Field declaration `private City _city;` — @type-binding.annotation attached to the class scope for `this._city.X` resolution. - Property declaration `public User Owner { get; set; }` — same. - Assignment rebind `alias = Factory()` / `alias = new User()` — @type-binding.alias / @type-binding.constructor so reassignment propagates type info to later receiver-typed resolution. Closed tests: foreach (3), var foreach Tier 1c (2), is-pattern (1), switch pattern (2), recursive_pattern (3). Remaining 37 include tests that need cross-file same-namespace visibility (field chains, assignment chain, cross-file return-type propagation) — deferred to Unit 5 where the IMPORTS/cross-file work lives. 74/74 scope-resolution unit tests pass; legacy path 175/175 green. * feat(csharp-scope): parity Unit 2b — same-namespace cross-file visibility Closes 3 parity failures (37 → 34). Adds the C#-specific implicit import that has no syntactic counterpart: every type declared in `namespace X` is visible to every other file also declaring `namespace X`, without any `using` directive. Changes: - `scope-resolution/contract/scope-resolver.ts` — new optional hook `populateNamespaceSiblings(parsedFiles, indexes, { fileContents })`. Most languages leave it undefined; Python / TypeScript / Java need explicit imports so there's no analogous pass. - `scope-resolution/pipeline/run.ts` — invoke the hook after `buildWorkspaceResolutionIndex` and before `propagateImportedReturnTypes` so the return-type pass sees cross-file sibling class bindings. - `languages/csharp/namespace-siblings.ts` (new) — groups top-level class-like defs by namespace name (extracted from source via regex since `file_scoped_namespace_declaration` scope range covers only the declaration line, not the rest of the file). Injects sibling classes into each file's Module AND Namespace scope bindings with origin='namespace'. Local declarations shadow cross-file siblings via mergeBindings tier precedence. - `languages/csharp/scope-resolver.ts` — wire the hook. 74/74 scope-resolution unit tests pass; legacy path 175/175 green; 34 parity failures remain (was 37) under REGISTRY_PRIMARY_CSHARP=1. * feat(csharp-scope): parity Unit 2c — alias/await/return-type captures Closes 7 parity failures (34 → 27). Adds the remaining type-binding shapes the parity suite exercises: - `var alias = u;` / `alias = u;` — identifier-to-identifier alias. The resolver's chain-follow walks alias → u → u's declared type. - `var u = svc.GetUser();` — chained method call alias. Anchors on the method_access_expression's `name` field; chain-follow picks up GetUser's return type. - `var u = await Factory();` / `await svc.Get();` — await propagation. Strips the `await_expression` wrapper; interpret layer's `stripGeneric` handles `Task<T>` / `ValueTask<T>` unwrapping. - `public User GetUser() { ... }` — method return-type annotation via `@type-binding.return`. Required for `propagateImportedReturnTypes` to see the return type in later cross-file passes. Covers identifier, generic_name, qualified_name, and nullable_type return shapes. 74/74 scope-resolution unit tests pass; legacy path 175/175 green; 27 parity failures remain under REGISTRY_PRIMARY_CSHARP=1. * feat(csharp-scope): parity Unit 3a — cross-namespace `using` binding Closes 2 parity failures (27 → 25). Extends the namespace-siblings pass to resolve `using X;` directives against known namespace buckets: for each `using` that targets a namespace declared somewhere in the workspace, inject that namespace's classes into the importer's module scope with origin='namespace'. This is the scope-resolution analog of legacy's csproj-driven directory↔namespace mapping. Without it, `new User()` in `Services/UserService.cs` (namespace MyApp.Services) can't see the User class in `Models/User.cs` (namespace MyApp.Models) even with `using MyApp.Models;` — the scope-resolver layer doesn't have csproj metadata to translate the dotted namespace path into a directory lookup. Legacy 175/175 green; 25 parity failures remain. * feat(csharp-scope): parity Unit 3b — constructor CALLS emission Closes 3 parity failures (25 → 22). Adds constructor-form CALLS edge emission + C# 12 primary constructor synthesis. Changes: - `scope-resolution/passes/free-call-fallback.ts`: when a site's callForm === 'constructor', look up the class def (not a callable) and pick its explicit Constructor def via workspaceIndex's memberByOwner — or fall back to the Class def itself for implicit constructors. Matches legacy behavior (targetLabel === 'Constructor' when explicit, 'Class' when implicit). - `scope-resolution/pipeline/run.ts`: pass workspaceIndex to the free-call fallback. - `languages/csharp/captures.ts`: synthesize @declaration.constructor for C# 12 primary constructors — `class User(string name, int age)` / `record Person(string First, string Last)`. The parameter_list is a named child of the class_declaration / record_declaration (not a separate constructor_declaration node). Skip the synthesis when the type already has an explicit constructor to avoid duplicates. Emits @declaration.parameter-count + required-parameter-count alongside. Legacy 175/175 green; 376/376 scope-resolution unit tests pass; 22 parity failures remain. * feat(csharp-scope): parity Unit 3c — static call + default-namespace Closes 2 parity failures (22 → 21). - `receiver-bound-calls.ts`: add Case 5 for class-as-receiver. When `Animal.Classify()` has an identifier receiver that resolves to a Class binding (rather than a variable with a typeBinding), look up the member on the class's MRO chain. Covers C#-style static calls and any type-qualified member access. Python doesn't hit this because `ClassName.method()` is syntactically identical to a free call there. - `namespace-siblings.ts`: treat files with no `namespace X;` declaration as living in the default (empty-name) bucket, so types declared in no-namespace files share cross-file visibility. Required for fixtures without explicit namespaces (e.g. the method-enrichment fixture's Animal/App/Dog classes). Legacy 175/175 green; 21 parity failures remain. * feat(csharp-scope): parity Unit 4 — callsite arity synthesis (infra) Synthesize @reference.arity on every invocation_expression and object_creation_expression by counting `argument` named children of the backing `argument_list`. Wires the capture-to-Callsite pipeline shared extractor already consumes (`scope-extractor.ts:878`). No parity-count movement: the remaining arity-adjacent failures (overload disambiguation, optional-parameter dedup, variadic resolution) need type-based argument inference or member-call dedup, both explicitly deferred in the plan's Known Limitations section. This commit is infrastructure — future work lands on top of it. Legacy 175/175 green; 21 parity failures remain. * feat(csharp-scope): parity Unit 5a — IMPORTS edge + static-using mapping Closes 1 parity failure (21 → 20). Fixes cross-file IMPORTS edge emission for C#: - `languages/csharp/interpret.ts`: map `using static X.Y;` to `kind: 'namespace'` rather than `'wildcard'`. The File→File IMPORTS edge needs a non-wildcard kind to survive finalize's Phase 4 (wildcard-expanded edges drop to empty when the provider doesn't implement `expandsWildcardTo`). Unqualified static-member access is a deferred limitation — covered by the namespace-siblings cross-namespace pass for type lookups, and documented under the module's Known Limitations. - `languages/csharp/import-target.ts`: progressive prefix stripping. `using CrossFile.Models;` in a repo laid out `Models/User.cs` (no `CrossFile/` directory) works because the legacy resolver consults csproj; the scope-resolver tries each suffix of the dotted path against `.cs` files. Also handles `using static NS.Type;` by stripping leading segments until a direct match lands. - `test/unit/scope-resolution/csharp/csharp-imports.test.ts`: update the `using static` test to the new namespace-kind shape. 376/376 scope-resolution unit tests pass; legacy 175/175 green; 20 parity failures remain. * feat(csharp-scope): parity Unit 5b — return-type module hoist + chain fallback Closes 1 parity failure (20 → 19) and lays groundwork for Unit 6. Based on investigation-agent findings, addresses cluster of 7 cross-file + chain tests whose return-type bindings were stuck at Class scope and invisible to the chain-follow and propagation passes. Changes: - `languages/csharp/simple-hooks.ts::csharpBindingScopeFor`: when the declaration is a `@type-binding.return`, hoist the binding all the way to the Module scope. The central extractor's auto-hoist only promotes one level (Function → Class); for C# methods the parent is always a Class, so without this override the return binding never reaches Module where chain-follow and cross-file `propagateImportedReturnTypes` read from. - `scope-resolution/passes/compound-receiver.ts`: when the class-scope typeBindings lookup at `objClass.typeBindings.get( methodName)` misses, walk up from the class scope through the parent chain (→ Module) for a return-type binding. Preserves the existing class-scope fast-path while restoring owner-chain lookup for languages that hoist to Module. Python parity suite stays 204/204 green on both flag paths; legacy C# 175/175 green; 19 C# parity failures remain. * feat(csharp-scope): parity Unit 5c — switch-expr + reasons + ACCESSES 1.0 Closes 4 parity failures (19 → 15). - `languages/csharp/query.ts`: add captures for `switch_expression_arm` with `declaration_pattern` and `recursive_pattern`. C# expression- switch (`obj switch { User u => ..., Repo { Name: "x" } r => ... }`) uses a different AST node from classic `switch_statement`'s `switch_section` — needed separate query patterns. - `scope-resolution/passes/receiver-bound-calls.ts`: replace the self-describing `'scope-resolution: *-receiver'` reason strings (which fail legacy-parity consumer filters) with the legacy convention: `'import-resolved'` when the resolved member lives in a different file, `'global'` otherwise. Mirrors `free-call-fallback.ts`'s existing reason logic. - `scope-resolution/passes/receiver-bound-calls.ts`: pass `confidence: 1.0` to `tryEmitEdge` for write/read ACCESSES edges, matching legacy DAG behavior (default 0.85 was legacy-CALLS). Python parity 204/204 on both flag paths; legacy C# 175/175; 15 C# parity failures remain. * feat(csharp-scope): parity Unit 5d — cross-file typeBinding mirror Closes 3 parity failures (15 → 12). `languages/csharp/namespace-siblings.ts`: extend the pass to mirror method return-type bindings from accessible sibling files' Module scopes into the importer's Module scope. "Accessible" = same-namespace siblings + `using namespace X;` targets. Without this mirror, `var u = svc.GetUser()` in App.cs couldn't chain-follow to User even after Unit 5b's module-scope hoist: `GetUser → User` lived on User.cs's Module scope, which isn't on the ancestor chain of App.cs's function scope, and `propagateImportedReturnTypes` only mirrors across explicit ImportEdge targets (not same-namespace implicit visibility). Closes: var-invocation return type, async/await u.Save (ambient namespace), cross-file return-type propagation (via u.Save / u.GetName in Program.cs). Python parity 204/204 on both flag paths; legacy C# 175/175; 12 C# parity failures remain. * feat(csharp-scope): parity Unit 5e — namespace-prefix bucket matching Closes 2 parity failures (12 → 10). `languages/csharp/namespace-siblings.ts`: when matching accessible namespaces against class buckets, also probe every dotted prefix. `using static CrossFile.Models.UserFactory;` parses into the importer's accessible-namespace set as the full type path, but the matching bucket is keyed on the containing namespace (`CrossFile.Models`). Walking back through the dotted segments ensures the static-using importer sees the containing namespace's sibling files' return-type bindings. Legacy 175/175 green; 10 C# parity failures remain. * feat(csharp-scope): parity Unit 6a — class-like owner extension Closes 1 parity failure (10 → 9). Extends `populateClassOwnedMembers` to recognize Interface / Struct / Record / Enum / Trait as class-like owners, not just Class. The C# scope query collapses interface_declaration / struct_declaration / record_declaration / enum_declaration to @scope.class (they share body-scope semantics), but the declaration-side tags produce defs of type Interface / Struct / Record / Enum. `populateClassOwnedMembers` previously only looked for Class-typed defs in class scopes, so interface members (including C# 8+ default methods) never got ownerIds — making them invisible to `findOwnedMember` via `memberByOwner`. With this fix, `user.Validate()` on a variable typed as `IValidator` resolves correctly: receiver-bound-calls Case 4 finds IValidator via findClassBindingInScope (which already accepted Interface), walks the chain, and findOwnedMember locates Validate now that the interface default has a proper ownerId. Legacy C# 175/175 green; Python parity 204/204 on both flag paths; 9 C# parity failures remain. * feat(csharp-scope): parity Unit 6b — member-call dedup + handled-site fix Closes 1 parity failure (9 → 8). Adds the missing legacy-parity behavior: collapse multiple member-call sites from the same caller to the same target into one CALLS edge. Changes: - `scope-resolution/contract/scope-resolver.ts`: new optional `collapseMemberCallsByCallerTarget` flag. Default false (preserves the per-site invariant); C# sets it true. - `scope-resolution/graph-bridge/edges.ts`: dedup key drops `line:col` when `collapseByCallerTarget` is on AND edgeType is `CALLS` (ACCESSES writes keep per-site granularity). - `scope-resolution/passes/receiver-bound-calls.ts`: plumbs `collapse` through every `tryEmitEdge` call, and crucially marks `handledSites.add(siteKey)` whenever a resolved def was found — not only when the edge was freshly emitted. Otherwise the site leaked through to `emitReferencesViaLookup` which re-emitted a per-site edge, defeating the collapse. - `languages/csharp/scope-resolver.ts`: opt in to the collapse. Python parity 204/204 on both flag paths; legacy C# 175/175 green; 8 C# parity failures remain. * feat(csharp-scope): parity Unit 6c — Dictionary.Values / .Keys unwrap Closes 2 parity failures (8 → 6). Dictionary<K,V>.Values in a foreach binds the element to V; .Keys binds to K. Without this, `foreach (var user in data.Values)` where `data: Dictionary<string, User>` couldn't propagate user's type to User, and `user.Save()` stayed unresolved. Changes: - `languages/csharp/interpret.ts`: don't strip the qualifier when the final dotted segment is a known collection accessor (`Values` / `Keys`). Preserves the dotted form so downstream resolvers can unwrap the receiver's generic type based on the suffix. - `scope-resolution/passes/compound-receiver.ts`: new `extractDictionaryArgs` helper splits `Dictionary<K, V>` at the top-level comma. In the dotted-access walk, detect trailing `.Values` / `.Keys` and return V/K via findClassBindingInScope instead of the normal class-walk (Dictionary itself isn't a local class def). - Handles nested cases: `this.data.Values` walks `this.data` recursively (resolving `data` as a field on `this`'s class) before applying the unwrap. - `scope-resolution/passes/receiver-bound-calls.ts` Case 3b: when the typeRef's trailing segment is an accessor, pass the raw dotted path to `resolveCompoundReceiverClass` without appending `()` — the extra parens would misroute to the call-expression branch. Python parity 204/204 on both flag paths; legacy C# 175/175 green; 6 C# parity failures remain. * feat(csharp-scope): parity Unit 6d — using-static member injection Closes 2 parity failures (6 → 4). `using static X.Y.Z;` now injects every public static method of class Z into the importer's module scope, so `Record("hi")` (without `Logger.` qualifier) resolves to `Logger.Record` as a free call. `languages/csharp/namespace-siblings.ts`: regex-scan each file's source for `using static X.Y.Z;` directives. For each, look up the class Z in the `X.Y` namespace bucket, walk its owning file's localDefs for method/function members with `ownerId === Z.nodeId`, and inject them as `origin: 'import'` bindings in the importer's module-scope finalized bindings map. `findCallableBindingInScope` then picks them up via its imported-bindings check. Closes: variadic `Record(params string[])` + heritage arity narrowing `WriteAudit`. Python parity 204/204 on both flag paths; legacy C# 175/175 green; 4 C# parity failures remain (interface-dispatch pass + type-based overload disambiguation). * feat(csharp-scope): parity Unit 6e — overload disambig + interface dispatch + FLAG FLIP Closes the final 4 parity failures (4 → 0). C# now runs the registry-primary scope-resolution path by default — added to MIGRATED_LANGUAGES. Changes: - `scope-resolution/scope/walkers.ts`: was already extended in Unit 6a to recognize Interface/Struct/Record/Enum as class-like owners (interface default methods get ownerIds). - `scope-resolution/passes/receiver-bound-calls.ts`: build IMPLEMENTS edge index → emit secondary `interface-dispatch` CALLS edges to every implementor's same-named member when the primary receiver-typed edge targets an Interface method (closes heritage CreateUser CALLS-count test). - `scope-resolution/passes/receiver-bound-calls.ts`: new `pickOverload` helper narrows multi-valued `membersByOwner.get(owner).get(name)` candidates by arity then argument types. Replaces the first-seen `findOwnedMember` lookup in Case 4 so receiver-typed overloaded calls pick the right def. - `scope-resolution/passes/free-call-fallback.ts`: new `pickImplicitThisOverload` walks up to the enclosing class scope and applies the same arity + argument-type narrowing for free calls inside a class body (`Lookup("alice")` → `Lookup(string)`). - `scope-resolution/workspace-index.ts`: new `membersByOwner` multi-valued index (`Map<owner, Map<name, Def[]>>`) preserves every overload alongside the existing first-seen `memberByOwner`. - `scope-resolution/graph-bridge/node-lookup.ts` + `scope-resolution/graph-bridge/ids.ts`: include parameter-types suffix in the qualified lookup key for Method nodes. Legacy parse-phase encodes the type tag into the node id (`Method:f.cs: UserService.Lookup#1~int`); without this two same-arity overloads collapsed to one lookup entry and routed to the wrong graph node. - `scope-resolution/contract/scope-resolver.ts`: new `collapseMemberCallsByCallerTarget` opt-in flag (was added in Unit 6b for member-call dedup; documented here). - `gitnexus-shared/src/scope-resolution/reference-site.ts`: new `argumentTypes` field carrying inferred per-arg types. - `scope-extractor.ts`: read @reference.parameter-types capture into `site.argumentTypes` and add it + the declaration-arity tags to KNOWN_SUB_TAGS so the anchor-detection picks the right anchor. - `languages/csharp/captures.ts`: synthesize @reference.parameter-types by inferring arg types from literal AST nodes (integer_literal → 'int', string_literal → 'string', constructor_expression → type-name, etc). - `languages/csharp/scope-resolver.ts`: opt in to `collapseMemberCallsByCallerTarget`. - `registry-primary-flag.ts`: **add CSharp to MIGRATED_LANGUAGES**. Final state: - C# parity: 175/175 green on flag-on AND flag-off. - Python parity: 204/204 green on both flag paths (no regression). - TypeScript clean. 51 → 0 failures across 18 commits on `feat/csharp-scope-resolution`. * refactor(scope-resolution): extract language-specific accessor unwrap to provider hook Optimizer pass: move C# Dictionary-family `.Values`/`.Keys` handling out of the shared `compound-receiver.ts` (where it had hardcoded regex + accessor names) into a provider-level `unwrapCollectionAccessor` hook. The shared pass now takes an arbitrary language-specific unwrap function; C# supplies its Dictionary implementation in `languages/csharp/accessor-unwrap.ts`. Related cleanup in `receiver-bound-calls.ts` Case 3b: replace the hardcoded `tail === 'Values' || tail === 'Keys'` accessor check with a try-dotted-walk-first / fall-back-to-call-form strategy. This removes the last C#-specific branch in the shared pass and makes the logic generalize cleanly to other languages that use property-style accessors for collection views (Kotlin `.size`, future languages). Changes: - `scope-resolution/contract/scope-resolver.ts`: new optional `unwrapCollectionAccessor(receiverType, accessor) => string | undefined` hook. Documented as language-specific with examples. - `scope-resolution/passes/compound-receiver.ts`: delete `extractDictionaryArgs`, accept `unwrapCollectionAccessor` via options, call it for trailing accessor segments. - `scope-resolution/passes/receiver-bound-calls.ts`: plumb the hook through to `resolveCompoundReceiverClass`, remove the C#-hardcoded Case 3b accessor check. - `languages/csharp/accessor-unwrap.ts` (new): C# Dictionary-family regex + element-type extraction. - `languages/csharp/scope-resolver.ts`: opt in. Audit outcome: everything else added across the 19 C# migration commits is either correctly scoped to `languages/csharp/` (query, captures, namespace-siblings, receiver-binding, interpret, imports) or correctly generic in shared paths (argumentTypes field, collapseMemberCallsByCallerTarget flag, overload narrowing via parameterTypes, interface-dispatch via IMPLEMENTS edges, class-like owner extension for Interface/Struct/Record/Enum, type-tagged node IDs, module-scope return-type lookup fallback). 175/175 C# green on both flag paths; 204/204 Python green on both flag paths; TypeScript clean. * refactor(scope-resolution): gate module-scope typeBinding walk-up on hook Add optional `hoistTypeBindingsToModule` to the ScopeResolver contract and gate the Module-scope walk-up in `resolveCompoundReceiverClass` on it. Only providers that hoist method return-type bindings to Module scope (C#) opt in; Python and other providers no longer traverse that fallback path. Closes the architectural leak flagged in the production-readiness review: the walk-up was unconditional and therefore widened Python's code path despite existing only for C#. No behavior change for C# (hook=true restores the prior lookup). No behavior change for Python (hook undefined = walk-up skipped, matching pre-PR behavior). Verified: - npx tsc --noEmit clean - C# unit suite 74/74 passing - C# + Python integration 388/388 passing * refactor(csharp-scope): remove as-unknown-as double casts in scope-resolver Tighten three type boundaries that were previously papered over with `as unknown as` casts: * `CsharpResolveContext.allFilePaths`: `Set<string>` → `ReadonlySet<string>`. The orchestrator only hands out a read-only view; drop the widening cast at the resolver-adapter site. * `resolveCsharpImportTarget`: call passes the narrow context directly. `WorkspaceIndex` is `unknown` in the shared contract, so the `as unknown as WorkspaceIndex` cast was gratuitous — structural assignability covers it. * `csharpMergeBindings`: drop unused `_scope: Scope` parameter. The implementation never read it; the cast chain in `scope-resolver.ts` existed only to satisfy an unused slot. LanguageProvider.mergeBindings now wraps with a tiny arrow adapter; ScopeResolver.mergeBindings passes through directly. No runtime behavior change. `grep 'as unknown as' csharp/scope-resolver.ts` returns zero matches. Verified: - npx tsc --noEmit clean - C# unit + integration 462/462 passing (incl. Python integration) * test(csharp-scope): integration fixtures for Units 6c/6d/6e runtime behavior Close the integration-coverage gap flagged in the production-readiness review. Units 6c (collection-accessor unwrap), 6d (using-static member injection), and 6e (overload disambig + interface dispatch) previously had only hook-level unit tests; the end-to-end wiring was exercised only by the parity harness. Three minimal fixtures + four new it() blocks: * csharp-collection-accessor — RenderAll iterates Dictionary<string, Widget>.Values and calls .Render(); asserts the CALLS edge lands on Widget.Render. * csharp-using-static — `using static Helpers.MathUtils;` makes Square(int) a free-callable in the consumer; asserts the CALLS edge lands on MathUtils.Square. * csharp-overload-interface — three assertions: 1. Run → Log binds to the 2-arg overload only (arity narrowing); verified via target Method node's parameterTypes.length === 2. 2. Run → Greet emits one primary edge to IGreeter.Greet plus two reason='interface-dispatch' siblings to En/FrGreeter.Greet. 3. Interface-dispatch fan-out excludes the primary target. Verified: - csharp integration 189/189 passing * docs(scope-resolution): de-c#-ify optional-hook doc-comments on contract Rewrite the doc-comments on four optional hooks so they describe the behavior and when a provider would enable it, rather than naming C# as the sole consumer. Hook names were already generic — only the comments had baked in one-language framing, which risked discouraging future reuse. Affected hooks: * unwrapCollectionAccessor * collapseMemberCallsByCallerTarget * populateNamespaceSiblings * hoistTypeBindingsToModule Language-specific rationale stays where it belongs — next to the hook assignment in `languages/csharp/scope-resolver.ts`. Zero-match grep for `C#|csharp|CSharp` in the contract file confirms the separation. No code change. * docs(csharp-scope): justify regex-based namespace-sibling detection Record why `namespace-siblings.ts` uses regex over AST walks and enumerate the known misses so the next reader has ground to stand on: * `global using static X.Y;` — no plain `using static` token. * Aliased `using static X = Y.Z;` — `=` breaks the pattern. * Attributed namespace declarations between `]` and `{`. * Multi-namespace files — first-wins attribution. * Preprocessor-gated namespace declarations — textual branch only. Rationale: the pass is file-path-driven and the tree-sitter tree isn't available at its call site (the orchestrator feeds raw fileContents); re-parsing to count namespaces would cost more than the regex walk. Refactor to AST-driven detection is deferred to a separate PR. Mirrored the known-miss list into `csharp/index.ts`'s limitations ledger so the operator-visible surface and the in-code justification stay in sync. No code change. * refactor(csharp-scope): AST-driven namespace detection with treeCache reuse Replace regex-over-source-content with tree-sitter AST walks in namespace-siblings.ts; thread the orchestrator's treeCache through the populateNamespaceSiblings hook so the pass reuses the same parse trees `extractParsedFile` already consumed (single-source-of-truth for the AST — no double-parse). Behavior gains (no longer "known misses"): * `global using static X.Y;` is now detected. * Aliased `using static X = Y.Z;` is now detected. * Attributed namespace declarations (`[attr] namespace X`) parse correctly because tree-sitter sees them as one node. * Preprocessor-gated namespace declarations parse via the grammar. Contract change (additive, optional): * `populateNamespaceSiblings` ctx now carries an optional `treeCache?: { get(filePath): unknown }`. Existing providers that don't set it on `RunScopeResolutionInput` see undefined, and the hook falls back to a fresh parse (current behavior preserved on cache miss). Limitation ledger updated in csharp/index.ts: the AST-based detection removes 4 of the 5 prior known misses; only "first-wins multi-namespace file attribution" remains. Verified: - npx tsc --noEmit clean - C# + Python integration 393/393 passing * refactor(python-scope): remove as-unknown-as casts in scope-resolver (mirrors Unit 2) Replay the C# scope-resolver cleanup on the Python side so both providers share a single clean pattern: * Drop `ws as unknown as WorkspaceIndex` — `WorkspaceIndex` is `unknown` in the shared contract, so the narrow context assigns structurally without a cast. * Drop `{ id: scopeId } as unknown as Scope` — `pythonMergeBindings` never read the scope (the parameter was `_scope`), so the stub was a type-only ghost. Signature is now `(bindings)` and the LanguageProvider slot wraps with an arrow adapter. * Drop `allFilePaths as Set<string>` — the orchestrator hands a `ReadonlySet<string>`; we copy it into a `Set` at the resolver adapter so the legacy downstream `resolvePythonImportInternal` chain (typed for mutable `Set<string>`) keeps working. The copy is O(N) once per import, trivial cost. Left intact on purpose: the `(callsite, def) → (def, callsite)` arrow wrapper on `arityCompatibility`. That's a documented shape difference between `LanguageProvider.arityCompatibility(def, callsite)` and `ScopeResolver.arityCompatibility(callsite, def)`; both providers (Python + C#) carry the same wrapper. Reconciling is a separate refactor across both contracts. No runtime behavior change. Verified: - npx tsc --noEmit clean - Python + C# unit + integration suites 529/529 passing * docs(scope-resolution): document I1-I8 invariants, source-of-truth, and same-graph guarantee Promote contract knowledge that was implicit in code into the canonical docs so future migrations and the next reviewer don't have to reverse-engineer it. contract/scope-resolver.ts: * Migration cookbook lists every optional hook (was: only the two booleans), with one-line guidance per hook including when to enable `hoistTypeBindingsToModule`. * Contract Invariants I1-I7 are now spelled out in full (was: only I1/I3/I5 summarized with a pointer to a plan file). Added new I8 "post-finalize hooks may mutate Scope.typeBindings and indexes.bindings; consumers must not freeze or snapshot before all post-finalize hooks have run". * New "Semantic-model source of truth" section: ParsedFile is the single semantic model; passes that need AST-level facts must reuse the orchestrator's treeCache rather than re-parse. * New "Same-graph guarantee" section: legacy DAG and scope-resolution emit indistinguishable edges (node identity, edge vocabulary, confidence). CI parity workflow enforces this. gitnexus-shared/src/scope-resolution/parsed-file.ts: * Added "Source-of-truth invariant" pointer paragraph. ARCHITECTURE.md (Coexistence section): * Updated migrated-language list (Python + C#). * Added "Same-graph guarantee" subsection. * Added "Semantic-model source of truth" subsection. * Filled in the ScopeResolver hook table with the five optional hooks that landed in this branch (unwrapCollectionAccessor, collapseMemberCallsByCallerTarget, populateNamespaceSiblings, hoistTypeBindingsToModule, fieldFallbackOnMethodLookup). * Added C# rows to the code-references table. Verified: - npx tsc --noEmit clean - C# + Python integration 393/393 passing * refactor(scope-resolution): consume SemanticModel as single authoritative store Unify scope-resolution and legacy parse into one symbol index per the industry pattern (Roslyn / tsc / rust-analyzer). Scope-resolution passes now consume `SemanticModel.methods` / `SemanticModel.fields` / `SemanticModel.symbols` for all symbol-keyed lookups. The legacy DAG already read from these; the drift — two parallel owner-keyed indexes populated by two writers with divergent ownerId semantics — is closed. Changes: * `MethodRegistry.lookupAllByOwner(owner, name)`: new API returning every overload without arity narrowing. Powers `findOwnedMember` / `pickOverload`. * `pipeline/run.ts` reconciliation pass: after `provider.populateOwners(parsed)`, iterate `parsed.localDefs[i]` and register methods/fields into the SemanticModel under the corrected ownerId. Idempotent — skips defs already present under `(ownerId, simple)` by nodeId, so unmigrated languages whose legacy extractor already set ownerId (C#) don't double-register. Closes the Python gap where class-body methods were invisible to `MethodRegistry` because the legacy Python method extractor couldn't resolve `enclosingClassId` at parse time. * `WorkspaceResolutionIndex` slimmed to Scope-valued maps only (`classScopeByDefId`, `moduleScopeByFile`). Dropped `memberByOwner`, `membersByOwner`, `defsByFileAndName`, `callablesBySimpleName` — all symbol-keyed duplicates of SemanticModel indexes. * Walker helpers now consume SemanticModel: - `findOwnedMember(owner, name, model)` → methods then fields fallback (ACCESSES writes target Property/Variable defs too). - `findExportedDefByName` fallback walks every Module scope's `origin === 'local'` bindings via `index.moduleScopeByFile` (preserves the module-export-visibility filter that SymbolTable.fileIndex can't cheaply encode). - `findExportedDef` reads `moduleScope.bindings` directly. * `pickOverload` in receiver-bound-calls.ts falls back to `model.fields.lookupFieldByOwner` when method lookup returns empty, fixing ACCESSES write edges that receive a Property target. * `phase.ts` threads `resolutionContext.model` into `RunScopeResolutionInput`. Boundary rule, enforced by file placement: - symbol-indexed lookups (key = nodeId / name / filePath) → `SemanticModel` - Scope-valued lookups (value = `Scope`) → `WorkspaceResolutionIndex` Research synthesized from web-researcher + Explore + best-practices + system-architect agents; canonical references: Roslyn Overview, rust-analyzer architecture, stack-graphs paper. Verified: - npx tsc --noEmit clean - C# + Python integration 393/393 passing * docs(scope-resolution): refresh comments after dropping duplicated indexes Replace references to the now-deleted `memberByOwner` / `callablesBySimpleName` index fields with comments that describe the actual lookup path (`SemanticModel` registries + scope-tied module bindings). Pure doc cleanup; no behavior change. * feat(scope-resolution): extract reconciliation pass + add parity validator Extract the SemanticModel reconciliation pass (previously inline in `pipeline/run.ts`) into a dedicated module with: * `reconcileOwnership(parsedFiles, model)` — pure function returning stats (methodsRegistered / fieldsRegistered / skippedAlreadyPresent). Idempotent; safe to re-run. * `validateOwnershipParity(parsedFiles, model, onWarn)` — dev-mode runtime validator for Contract Invariant I9. Walks every def with an `ownerId` and asserts it is reachable via `model.methods.lookupAllByOwner` or `model.fields.lookupFieldByOwner`. Soft-fails via `onWarn`; never throws. Validator is gated on both `NODE_ENV !== 'production'` and `VALIDATE_SEMANTIC_MODEL !== '0'` so production incurs zero cost but development surfaces any drift between `parsed.localDefs` ownership and the registries. 12 new unit tests cover: * happy path: method, property, Variable registration * edge case: defs without ownerId are skipped * idempotency: second call is a no-op * coexistence: defs the legacy extractor already registered (via `model.symbols.add`) are skipped on reconcile * overloads: multiple methods under the same (owner, name) * validator: no warnings after reconciliation * validator: warns on drift * validator: no-op under NODE_ENV=production * validator: no-op when VALIDATE_SEMANTIC_MODEL=0 * validator: warns on missing Property same as missing Method Verified: - npx tsc --noEmit clean - reconcile-ownership unit tests 12/12 passing - C# + Python integration 393/393 passing * refactor(scope-resolution): narrow handles + tighten required params Two small hygiene fixes that fell out of the unified-model work: * Introduce `readonlyModel: SemanticModel` in `runScopeResolution` immediately after reconciliation so the write/read phase boundary is explicit at the code level. Downstream passes (receiver-bound, free-call) receive the narrowed `SemanticModel` rather than the `MutableSemanticModel` that only the reconciliation pass needs. The type system now rejects accidental writes in the read phase. * Make `emitFreeCallFallback`'s `workspaceIndex` parameter required. It's now always passed (every caller threads it through), and the `workspaceIndex?` guard was dead code. Also drops the `| undefined` branch from `pickConstructorOrClass` which no caller can hit. No behavior change. * docs(semantic-model): document unified single-source-of-truth invariant (I9) Add Contract Invariant I9 to the ScopeResolver contract and write the single-source-of-truth + write/read phase contract into both the SemanticModel file-head and ARCHITECTURE.md. Three landing points so the rule is reachable from every entry: * contract/scope-resolver.ts — new I9 entry in the Contract Invariants list: scope-resolution passes consult SemanticModel exclusively for symbol-keyed lookups; WorkspaceResolutionIndex is reserved for Scope-valued maps. Documents the two-phase write (legacy parse + reconcileOwnership) and the narrowed-handle read posture. Calls out the reconciliation shim as transitional. * model/semantic-model.ts — new "Single-source-of-truth invariant" and "Write / read phase contract" sections in the file-head. Three ordered write phases (parse → reconcile → attachScopeIndexes), then frozen for readers. * ARCHITECTURE.md § "Semantic-model source of truth" — expanded subsection covering both invariants (ParsedFile = AST truth, SemanticModel = symbol truth), the write/read phase diagram, and the reconciliation-shim rationale. No code change. * test(scope-resolution): rewrite workspace-index test for slimmed index The test file previously asserted on \`defsByFileAndName\`, \`callablesBySimpleName\`, and \`memberByOwner\` — fields removed when symbol-keyed lookups moved to \`SemanticModel\`. Rewrite so the same invariants are asserted via the authoritative consumers: * New WorkspaceResolutionIndex shape test (scope-only maps). * \`findExportedDef\` module-export visibility tests: - keeps top-level class and function defs. - excludes class-body Variable defs (MAX_USERS = 100). - excludes class methods from module-export lookup. * \`findExportedDefByName\` fallback excludes class methods when a same-named module function exists. * \`findOwnedMember\` via the reconciled SemanticModel finds Python class methods after populateOwners + reconcileOwnership. Total assertions preserved: every invariant from the old test file is still pinned; the assertion surface shifted from the index shape to the walker helpers. Verified: - workspace-index.test.ts 8/8 passing * fix(tests): update registry-primary-flag test for C# migration The "returns exactly the flipped languages" case expected `enabled.size === 1` after toggling Python off and Go on. After the C# migration lands C# in MIGRATED_LANGUAGES, C# is default-on too — so the size is now 2 (Go + C#) unless C# is also opted out. Turn off C# alongside Python in the test setup. Added a comment noting that future migrations must add their REGISTRY_PRIMARY_<LANG>='false' line here. * refactor(scope-resolution): address PR #1019 review findings Resolves all 5 findings from the automated review on feat/csharp-scope-resolution. Shared ingestion code stays language-agnostic; C# (and every class-like language) benefits. F1 [high] Broaden class-like predicate Hoist `isClassLike` in `scope/walkers.ts` to an exported top-level helper covering Class | Interface | Struct | Record | Enum | Trait. Use it in `findClassBindingInScope`, `findEnclosingClassDef`, and `buildWorkspaceResolutionIndex` so C# records, structs, interfaces, and enums participate in scope chains and receiver binding the same way Python classes do. F2 [medium] Remove stale comment in csharp simple-hooks `csharpReceiverBinding`'s doc claimed this/base synthesis was "planned for a follow-up"; synthesis has been implemented in receiver-binding.ts since the migration landed. Rewrite the doc to describe the actual behavior (non-null TypeRef on instance-method bodies, null on static/free functions). F3 [medium] O(1) reverse lookup for classScopeId -> classDefId Add `classScopeIdToDefId: ReadonlyMap<ScopeId, string>` to `WorkspaceResolutionIndex`, populated as the inverse of `classScopeByDefId`. Replace the O(C) linear scan in `pickImplicitThisOverload` (free-call-fallback.ts) with an O(1) `Map.get` — turns per-site reverse resolution from linear in class count to constant time for every free call. F4 [low] Extract narrowOverloadCandidates shared utility New `passes/overload-narrowing.ts` centralizes the arity + argument- type narrowing previously duplicated across `pickOverload` (receiver-bound-calls.ts) and `pickImplicitThisOverload` (free-call-fallback.ts). Both callsites now share identical narrowing semantics; variadic `params T` handling is preserved. Return type is `readonly SymbolDefinition[]` with no defensive spreads (allocations saved on the hot path). F5 [low] Merge unreachable Case 5 into Case 2 `Case 5` in `receiver-bound-calls.ts` was dead code — `Case 2` pre-empted it for every static/class-name receiver. Delete Case 5 and lift its kind-aware read/write ACCESSES reason/confidence logic into Case 2 so static-style member access (e.g. `Interface.Member`, `TypeName.StaticMember`) gets the correct edge metadata. Tests - New unit tests for `narrowOverloadCandidates` covering empty input, arity filtering, variadic params, type narrowing, and fallback semantics. - New unit tests for `classScopeIdToDefId` verifying inverse invariant and empty index behavior. - New C# integration fixtures and tests: * csharp-record-base — record inheritance + `base.Save()` * csharp-struct-overloads — struct with implicit-this overload narrowing (pinned exact edge count under registry-primary) * csharp-interface-receiver-static — interface-qualified static- style call exercises the merged Case 2. - Full runs green: * scope-resolution unit: 406/406 * csharp integration (registry-primary): 197/197 * csharp integration (legacy DAG): 197/197 * python integration (regression guard): 204/204 Chore - Add `.context/` to root `.gitignore` to prevent agent scratch files from being committed. Made-with: Cursor * test(csharp-scope-resolution): address adversarial review follow-ups on PR #1019 Applies the three actionable follow-ups from the post-commit adversarial review of |
||
|
|
c6a291de67
|
feat(ingestion): ScopeExtractor driver — 5-pass CaptureMatch → ParsedFile (#919, RFC #909 Ring 2 PKG) (#965)
* feat(ingestion): ScopeExtractor driver — 5-pass CaptureMatch → ParsedFile (#919, RFC #909 Ring 2 PKG) Kicks off Ring 2 PKG. Implements RFC §5.3 + §3.2 Phase 1: the central, source-agnostic driver that turns a language provider's `CaptureMatch[]` into a `ParsedFile` — the per-file artifact the finalize orchestrator (#921) feeds into the shared `finalize()` algorithm (#915). ## Files ### New shared contracts - `gitnexus-shared/src/scope-resolution/parsed-file.ts` Per-file extraction artifact: scopes, parsedImports, localDefs, referenceSites. Structural superset of `FinalizeFile` so the finalize orchestrator threads `ParsedFile` through unchanged. - `gitnexus-shared/src/scope-resolution/reference-site.ts` Pre-resolution usage fact: name, atRange, inScope, kind, optional callForm/explicitReceiver/arity. Converted to `Reference` records by the resolution phase (populates `ReferenceIndex`). ### Ring 1 collateral tweak - `language-provider.ts: emitScopeCaptures` now returns `Promise<readonly CaptureMatch[]>` (was `readonly Capture[]`). Pre-grouping per tree-sitter match is the provider's job — the extractor expects coherent matches, not flat captures. No consumers yet (all languages still on legacy DAG), so no breakage. Docstring updated. ### New CLI module - `gitnexus/src/core/ingestion/scope-extractor.ts` Single entry point: `extract(matches, filePath, provider): ParsedFile`. Five-pass pipeline: Pass 1 — Build scope tree. `@scope.*` → `ScopeDraft[]` via range-containment parent derivation. Honors `provider.shouldCreateScope` (skip-but-reparent-children) and `provider.resolveScopeKind`. Throws `ScopeTreeInvariantError` via `buildScopeTree` on malformed input. Pass 2 — Attach declarations + local bindings. `@declaration.*` → `SymbolDefinition` + `BindingRef { origin: 'local' }`. Default attachment: innermost containing scope. Hoisting via `provider.bindingScopeFor`. Pass 3 — Collect raw imports. `@import.*` → `ParsedImport` via `provider.interpretImport`. Attached to ParsedFile (finalize resolves owning scope in Phase 2). Pass 4 — Collect type bindings. `@type-binding.*` → `TypeRef` via `provider.interpretTypeBinding` → `scope.typeBindings`. Hoistable via `bindingScopeFor`. Pass 5 — Collect reference sites. `@reference.*` → `ReferenceSite[]`. Call form from declarative sub-tag (`@reference.call.member`) or `provider.classifyCallForm`. ### Tests - `gitnexus/test/unit/scope-resolution/scope-extractor.test.ts` 23 tests organized by pass + one end-to-end fixture exercising all 5 passes together. MockProvider emits synthetic `CaptureMatch[]` with no AST — extractor is pure given those. ## Design notes - **Source-agnostic.** No `Tree` / `SyntaxNode` types leak into the driver. Works for tree-sitter providers and COBOL's regex tagger. - **One AST walk per language.** Providers do the walk inside `emitScopeCaptures`; this driver does zero traversal. - **Invariants delegated.** `ScopeTree.buildScopeTree` enforces structural rules (non-Module has parent, parent contains child, siblings don't overlap). The extractor doesn't try to repair malformed captures. - **Sub-tag whitelist.** `@reference.receiver`, `@declaration.name`, `@import.source`, etc. are known sub-tags — excluded from anchor selection so the broadest-range heuristic doesn't mis-identify them as anchors for their topic. Bug surfaced in the end-to-end fixture test (member call with a large-range receiver) and was fixed before commit. ## Verification - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`) - `gitnexus-shared` build clean - 23/23 new tests pass - Full scope-resolution / model / shadow suite: **285/285 pass** ## Closes part of #909. Unblocks - #920 parse-worker integration (emit ParsedFile from the worker) - #921 finalize orchestrator (consume ParsedFile[] workspace-wide) - #922 per-language import adapters * chore(ingestion): address #919 review findings on the extractor Addresses all 5 items from the PR #965 review in-PR. ## Structural changes - **Extract `ScopeExtractorHooks` as the narrow dependency surface.** The extractor now declares its dependency on a `Pick`-narrowed subset of `LanguageProvider` (just the 6 scope-resolution hooks it actually reads). Test mocks implement exactly that interface — no more `as unknown as LanguageProvider` cast hiding missing-field bugs. Adding a new hook read becomes a compile error, not a silent test pass. (Finding 3.2) - **Remove dead `ownerDefIdFor` stub + `isOwnerKind` helper.** The function always returned `undefined` with `void innermost; void drafts;` suppressors — an incomplete-implementation signal. The code path was also misleading: creating a clone of the def with `ownerId: undefined` is structurally identical to keeping the original. Pass 2 now keeps the def as-is. Contract is documented in a code comment: providers that need `ownerId` set it from their declaration hook; `finalize` (via #914 `MethodDispatchIndex`) fills in method/field `ownerId` in a post-extraction pass that has full def visibility. (Finding 2.1) - **Standardize `filePath` threading across passes 4 and 5.** Pass 4 was reading `drafts[0]!.filePath`; pass 5 was reading `anyFilePathFromScopeTree(scopeTree)`. Both equivalent but inconsistent. Both now take `filePath` as a parameter from the top-level `extract()` call. The `anyFilePathFromScopeTree` helper is removed. (Finding 2.2) ## Documentation - **Snapshot-semantics comment on `scopeTree` + `positionIndex`.** The hooks called during Passes 2-5 receive a `scopeTree` built BEFORE any bindings/ownedDefs/typeBindings were written. Hooks MUST NOT rely on `scope.bindings` etc. being populated — they're for parent/range/kind queries only. Added a doc block at the `scopeTree`/`positionIndex` construction site so future Ring 3 implementers don't write a `classifyCallForm` that reads bindings. (Finding 2.3) ## Tests - **Regression for the anchor-vs-receiver bug** (Finding 3.1): a member-call match where `@reference.receiver` spans columns 0-10 (wider) and the call name spans 11-15 (narrower). Without the `KNOWN_SUB_TAGS` exclusion, the broadest-range heuristic would have picked the receiver; the test pins that the call name is the one that ends up in `referenceSites[0].name`. - **Mock provider now types exactly `ScopeExtractorHooks`**, no more double-cast. Any future hook added to `extract()` that isn't in `ScopeExtractorHooks` is a compile error. ## Verification - `tsc --noEmit` clean in both `gitnexus-shared` and `gitnexus` - `gitnexus-shared` build clean - 24/24 scope-extractor tests pass (+1 regression) - Full scope-resolution / model / shadow suite: **286/286 pass** |