Commit graph

127 commits

Author SHA1 Message Date
Gergo Magyar
bda168af00 fix(scope): address TypeScript import review follow-ups
Add regression coverage for non-binding import edges and circular TypeScript bindings so PR #1050 review concerns stay visible without changing runtime semantics.

Made-with: Cursor
2026-04-26 06:44:34 +01:00
Gergo Magyar
ae0bd74dd6 fix(scope): address Codex adversarial review findings on PR #1050
Four findings from the Codex adversarial review broke registry-primary
TypeScript resolution for common patterns. All four now have unit and
integration regression coverage that pass under both
`REGISTRY_PRIMARY_TYPESCRIPT=0` (legacy DAG) and the default
registry-primary path.

[high] tsconfig path aliases dropped:
Threaded `tsconfigPaths` through ScopeResolver via a new opaque
`resolutionConfig` parameter and a `loadResolutionConfig(repoPath)`
hook. The orchestrator (`scopeResolutionPhase` + `runScopeResolution`)
loads it once per workspace pass and forwards into every
`resolveImportTarget` call. TypeScript resolver now resolves
`@/services/user` style imports through the standard resolver's alias
branch.

[high] TSX parsed with the wrong grammar:
`emitTsScopeCaptures` now picks the parser/query by `filePath`
(`.tsx` -> TSX grammar) and validates cached trees against the
expected grammar via the new exported `tsCachedTreeMatchesGrammar`
helper. Stale TS-grammar trees for `.tsx` files no longer leak through
the scope query.

[medium] Literal dynamic imports never linked:
Added `kind: 'dynamic-resolved'` to `ParsedImport` and `ImportEdge`.
The decomposer emits a synthetic `@import.literal` capture for
string-literal dynamic imports; the interpreter maps that to
`dynamic-resolved`; finalize pre-finalizes it as a file-level terminal
(same shape as `side-effect`). `import('./feature')` now produces a
real IMPORTS edge under the registry-primary path. Legacy DAG keeps
its existing behavior — the new integration assertion is gated behind
the flag.

[medium] Namespace re-exports invisible from barrels:
The decomposer now emits TWO captures for `export * as ns from './m'`
— the existing `reexport-namespace` import draft AND a synthetic
`@declaration.namespace` capture (via `buildNamespaceDeclarationMatch`).
The latter creates a Namespace `SymbolDefinition` in the barrel's
`localDefs`, so downstream `import { ns } from './barrel'` resolves
through `findExportByName`.

Regression fixtures under `gitnexus/test/fixtures/lang-resolution/`:
- typescript-tsconfig-aliases (`@/` alias)
- typescript-tsx-jsx (Button.tsx + App.tsx with JSX)
- typescript-dynamic-import (`await import('./feature')`)
- typescript-reexport-namespace (`export * as Models from './base'`)

Validation:
- gitnexus-shared builds clean
- gitnexus typecheck clean
- 385/385 TS scope-resolution tests pass under both
  `REGISTRY_PRIMARY_TYPESCRIPT=0` and default

Made-with: Cursor
2026-04-25 09:24:07 +01:00
Gergo Magyar
22dbfec8a0 fix(ingestion): address PR #1050 review findings — side-effect imports, resolve-cache perf, adapter signature
Three independent fixes surfaced by the production-readiness review of
the TypeScript registry-primary scope-resolution migration (RFC #909
Ring 3). All three pass under both REGISTRY_PRIMARY_TYPESCRIPT=0 and =1.

1. Side-effect imports were silently dropped (correctness regression).
   The legacy DAG emitted IMPORTS edges for `import './polyfill'` because
   its tree-sitter query matches `(import_statement source: (string))`
   regardless of clause. The new registry-primary path returned `[]`
   from `splitImportStatement()` for clause-less imports, so no
   ParsedImport / ImportEdge was ever produced — silent file-level edge
   loss. Add a generic 'side-effect' variant to `ParsedImport` and
   `ImportEdge['kind']` in `gitnexus-shared`; finalize resolves the
   target file and pre-finalizes the edge (no `targetDefId`, no
   `BindingRef`) so the SCC fixpoint loop skips it. The TypeScript
   provider now emits + interprets the new kind end-to-end. The
   variant is intentionally generic so other languages (Rust
   `use foo as _`, Python module-init) can adopt it.

2. Per-import re-derivation in `resolveImportTarget` (perf regression).
   The TS adapter built `new Set(allFilePaths)` on every call and let
   `resolveTsImportTarget` re-derive `allFileList` /
   `normalizedFileList` and discard the `resolveCache`. For a workspace
   with N files and M imports that's O(N × M) work per pass. Wrap the
   adapter in a closure that memoizes all five derived values keyed on
   the orchestrator's `ReadonlySet` identity; reset only when the set
   reference changes (start of new pass). New cost: O(N + M).

3. Misleading fake `ParsedImport` in the adapter (architecture).
   The adapter constructed `{ kind: 'named', localName: '_',
   importedName: '_', targetRaw }` to call `resolveTsImportTarget`,
   even though only `targetRaw` and the structural-typed context are
   read. Extract `resolveTsTarget(targetRaw, ctx)` so the adapter has
   an honest signature; `resolveTsImportTarget` still works for other
   callers. Also extract `narrowTsContext` for the type narrowing.

Tests: - New 4-file fixture `typescript-side-effect-imports` with two
    side-effect imports + one named import.
  - New "TypeScript side-effect imports" describe in
    `test/integration/resolvers/typescript.test.ts` (parity-gated by
    `ci-scope-parity.yml` — runs under both flag states).
  - Updated 2 unit tests to expect 1 side-effect ParsedImport and 4
    `@import.statement` matches (was 0 / 3).
  - 785 / 785 TS scope-resolution tests pass under both
    REGISTRY_PRIMARY_TYPESCRIPT=0 and =1.
Made-with: Cursor
2026-04-25 08:41:33 +01:00
Gergo Magyar
18197d739a fix(ingestion): SCC-ordered cross-file return-type propagation + multi-hop re-export resolution
Fix CI failures on PR #1050 (TypeScript registry-primary migration) by
making `propagateImportedReturnTypes` deterministic via reverse-
topological SCC ordering and updating the multi-hop re-export contract
to match `followReexportChain` behavior.

Why: the legacy pass mirrored an intermediate ref instead of the
terminal type when an importer was processed before its source module
had its own typeBindings chain-followed (4-file alias chain regression
in `ts-simple` fixture: `models.User -> service.user -> app.user`
collapsed to `getUser` instead of `User`). Reverse-topological walk of
`indexes.sccs` (leaves first) lets every importer see the source's
already-followed terminal type in a single pass.

Changes:
- `imported-return-types.ts`: rewrite to walk SCCs leaves-first, chain-
  follow the source module's typeBindings BEFORE mirroring, and chain-
  follow the importer's typeBindings AFTER mirroring. Cyclic SCCs
  reach a partial fixpoint (no convergence guarantee, ts-circular only
  asserts no-throw).
- `finalize-algorithm.ts`: docstring update on `FinalizeFile.localDefs`
  to reflect that `followReexportChain` resolves multi-hop re-exports
  through barrels even when intermediates do not surface the name -
  surfacing is now a static optimization, not a correctness requirement.
- `contract/scope-resolver.ts` Invariant I3: explicitly document the
  SCC ordering requirement.
- `pipeline/run.ts`: split PROF timer into `finalize` and `propagate`
  so the pass's cost is observable independently.
- `ARCHITECTURE.md` Performance notes: describe SCC-ordered propagation.
- `imported-return-types.ts`: expand chain-depth comment (2x effective
  depth from pre/post follow), add multi-ref break rationale, add
  `ts-simple` motivating-fixture pointer.

Tests:
- `finalize-algorithm.test.ts`: add 4 cases (3-hop chain, cyclic
  re-export visited-set guard, wildcard re-export fall-through,
  multi-source first-match-wins); fix misleading shared nodeId in the
  thick variant; rename and update the multi-hop test for the new
  contract (transitiveVia assertion on the thin variant).
- `imported-return-types.test.ts` (NEW): unit tests for the SCC pass
  pinning topological collapse, local-annotation guard, missing-source
  skip, and cyclic-SCC no-throw.
- `cross-file-binding.test.ts` + `ts-deep-alias-chain` fixture (NEW):
  5-file integration regression guard for SCC-ordered propagation
  through 4 module boundaries.

Validation: 865 scope-resolution + cross-file tests pass on Windows;
typecheck clean across both packages; only pre-existing Swift overload
failures remain (verified on PR base commit, environmental).

Made-with: Cursor
2026-04-24 18:26:15 +01:00
Gergő Magyar
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 5a1bce7f against DoD.md. No runtime code changes.

- [medium] Strengthen bounds-only assertion in the struct-overloads
  suite: `methods.length` is now pinned to `toBe(2)` and the arity list
  to `toEqual([1, 2])`. Fixture `csharp-struct-overloads/src/Calc.cs`
  declares exactly two `Add` methods, so a regression that adds, drops,
  or merges an overload will now fail the test instead of silently
  passing a `>= 2` gate.

- [low] Pin the merged Case 2 kind-aware branch (receiver-bound-calls.ts
  lines 257-289) with a dedicated fixture and three new assertions:
  `csharp-class-static-field-access/src/Counters.cs` exercises
  `ClassName.Field = value` where the receiver resolves via
  `findClassBindingInScope` (no typeBinding on `Counters`). The test
  verifies (a) two distinct ACCESSES writes are emitted from a single
  method (per-site dedup from graph-bridge/edges.ts:80-87),
  (b) `reason === 'write'`, (c) `confidence === 1.0`, and (d) no
  spurious CALLS edges are produced for the same sites. This is the
  semantic upgrade lifted from the deleted Case 5; without a pinning
  test a future revert of the kind-aware branch would silently drop
  back to `import-resolved`/`global` at 0.85 for the same sites.
  Read-side coverage is intentionally not asserted because the C#
  tree-sitter query currently emits only `write.member` captures
  (languages/csharp/query.ts:485-501) — a read counterpart would have
  no reference site today and would give a false sense of coverage.

- [info] Left the `?? overloads[0]` fallback in place at
  receiver-bound-calls.ts:450 unchanged. With the package's current
  tsconfig (strict: false, no noUncheckedIndexedAccess) both the
  defensive fallback and a `candidates[0]!` assertion type-check
  identically, so the finding has no production-readiness impact.
  Keeping the fallback minimizes churn.

Validation (local, Windows PowerShell):
- `npx prettier --check test/integration/resolvers/csharp.test.ts` -> clean
- `npx tsc --noEmit` -> 0 errors
- `REGISTRY_PRIMARY_CSHARP=1 npx vitest run test/integration/resolvers/csharp.test.ts` -> 200/200
- `REGISTRY_PRIMARY_CSHARP=0 npx vitest run test/integration/resolvers/csharp.test.ts` -> 200/200
- `npx vitest run test/integration/resolvers/python.test.ts` -> 204/204
- `npm test` (full gitnexus suite) -> 6967 passed, 6 pre-existing failures
  (4x Swift overload/dedup, 1x Swift method-extraction unit, 1x Swift
  type-env unit, 1x LadybugDB lockfile on Windows). All six reproduce on
  5a1bce7f with these follow-up changes stashed, confirming they are
  environment/baseline failures unrelated to this work. Swift is not in
  MIGRATED_LANGUAGES so the merged Case 2 path cannot affect it.

Refs: PR #1019
Made-with: Cursor

* refactor(scope-resolution): address full-PR review findings on PR #1019

Resolves the two remaining findings from the code-review-swarm full-PR
sweep (verdict: production-ready with minor follow-ups).

[low] Complete the csharp/index.ts module-layout JSDoc.

`languages/csharp/index.ts` is the discovery surface for the C# scope-
resolution module decomposition (per AGENTS.md). The "Module layout"
list silently omitted three load-bearing modules — `accessor-unwrap.ts`
(`.Values`/`.Keys` receiver-type unwrap), `namespace-siblings.ts`
(AST-driven cross-file implicit-namespace visibility), and
`receiver-binding.ts` (`this`/`base` type-binding synthesis). Extended
the JSDoc list so the "single-concern" decomposition story is honest
and the next contributor can locate the right file without grep.
No behavior change.

[info] Replace the non-standard `'scope-resolution: super-receiver'`
      edge reason with the canonical `'global'` tier.

`passes/receiver-bound-calls.ts` emitted a non-canonical reason string
for the super/base branch, which falls outside the vocabulary declared
in ARCHITECTURE.md § Scope-Resolution Pipeline (`'import-resolved' |
'global' | 'local-call' | 'same-file' | 'interface-dispatch' | 'read'
| 'write'`). Super/base calls resolve through the MRO chain rather
than through import directives, so the correct canonical tier is
`'global'` (same classification the legacy DAG's `toResolveResult`
applies to non-same-file, non-import-scoped resolutions).

Locked the contract with `rel.reason === 'global'` assertions on the
existing `csharp-super-resolution` and `csharp-generic-parent-
resolution` suites, both of which go through the super-branch MRO
path. The `csharp-record-base` suite intentionally does not pin a
reason (records don't currently emit EXTENDS edges, so the MRO lookup
misses and the edge is produced by the reference-index fallback
instead of the super-branch). A code comment flags the pre-existing
Python-legacy asymmetry (Python legacy tier classifier marks
`super()` as `'import-resolved'` because the ancestor arrives via an
`import` statement); closing that gap requires realigning the legacy
tier classifier and is tracked separately.

Validation:
- `npx tsc --noEmit` passes.
- `npx prettier --check` clean on all four touched files.
- `test/integration/resolvers/csharp.test.ts` — 200/200 under both
  `REGISTRY_PRIMARY_CSHARP=0` (legacy DAG) and `REGISTRY_PRIMARY_CSHARP=1`
  (registry-primary), preserving same-graph parity on the super branch.
- `test/integration/resolvers/python.test.ts` — 204/204 under both
  `REGISTRY_PRIMARY_PYTHON=0` and `=1`.
- `test/unit/scope-resolution/` — 406/406 passing.

Unstaged: `gitnexus/package-lock.json` (drift from `npm install` run
to resolve the pre-existing missing `jsonc-parser` dependency — not
part of this change).

Made-with: Cursor

* test(ci): raise integration-test timeouts so slow Windows runners stop flaking

The `windows-latest` CI runner for this branch was consistently failing
two integration suites in ways that had nothing to do with the PR's
scope-resolution changes:

  * `cli-e2e.test.ts` — `analyze command runs pipeline on mini-repo`
    hit the default 30 s vitest test timeout, which raced the test's
    own 30 s subprocess timeout and prevented the existing
    `if (result.status === null) return;` slow-CI tolerance from ever
    firing. That single timeout then cascaded into the downstream
    `cypher`/`query`/`impact` tests (which exited non-zero because the
    mini-repo was never indexed) and the `EPIPE handling` test.
  * `skills-e2e.test.ts` — `beforeAll` hooks run a full
    `runSkillsCli(tmpDir)` subprocess that analyzes a fixture repo and
    generates skills. 50 s was enough on Linux/macOS but not on slow
    Windows CPUs, producing "Hook timed out in 50000ms" errors and
    cascading test failures across every language describe block.

Fix:
  * Bump the `analyze` test's vitest test-level timeout to 60 s so it
    exceeds the 30 s subprocess timeout and the slow-CI tolerance can
    actually activate.
  * Bump all 12 `runSkillsCli`-driven `beforeAll` hooks from 50 s to
    120 s.

No production-code behavior changes. No change to what the tests
assert — only the per-test/hook wall-clock budget.

Made-with: Cursor
2026-04-23 12:38:13 +01:00
Sonu Verma
253f9cae37
feat(ingestion): make large-file skip threshold configurable (#1044)
* feat(ingestion): make large-file skip threshold configurable

The walker previously hardcoded a 512KB skip threshold, which silently dropped legitimate large source files (e.g. ~900KB hand-written Java service classes) during analysis with no way to override short of editing source.

Allow overrides via the GITNEXUS_MAX_FILE_SIZE env var (KB) — consistent with the existing GITNEXUS_NO_GITIGNORE / GITNEXUS_VERBOSE patterns — and a matching --max-file-size <kb> flag on gitnexus analyze.

- New utility getMaxFileSizeBytes() in core/ingestion/utils/max-file-size.ts parses the env var, falls back to the 512KB default for missing/invalid values, and clamps against TREE_SITTER_MAX_BUFFER (32MB) to keep the downstream parser safe.
- filesystem-walker.ts now resolves the threshold per call and drops the 'likely generated/vendored' editorial when the user has explicitly raised the limit.
- analyze CLI wires --max-file-size to the env var and echoes a one-line notice when the threshold is overridden, mirroring how --no-gitignore is handled.
- index.ts documents the new flag and env var under the analyze help text.
- Warnings for invalid or out-of-range values are emitted exactly once per distinct value to avoid log spam.

Tests:
- New test/unit/max-file-size.test.ts covers defaults, KB parsing, clamp-at-ceiling, invalid-input fallback + warn-once, and distinct-value warnings.
- test/integration/filesystem-walker.test.ts gains a 'large file skip threshold (#991)' block: 600KB fixture skipped by default, included under GITNEXUS_MAX_FILE_SIZE=1024, invalid values fall back and warn once, and the 'generated/vendored' suffix is only emitted under the default threshold.

Closes #991

* fix(cli): show effective clamped max-file-size in banner

Addresses the PR #1044 review finding: the startup banner printed the raw GITNEXUS_MAX_FILE_SIZE value rather than the clamped effective threshold, producing misleading telemetry when the value exceeded the 32 MB tree-sitter ceiling.

The banner is also suppressed when the effective threshold equals the default, removing log noise when operators explicitly set the value to the current default.

Extracted the logic into a new getMaxFileSizeBannerMessage() helper and pinned the behavior with unit tests covering default, raised override, invalid fallback, and above-ceiling clamp cases.
2026-04-23 11:07:37 +01:00
evolution
38db0244e8
fix(go): align worker CALLS source IDs for receiver methods (#1043) 2026-04-23 08:08:56 +01:00
Copilot
962f22482b
feat(cli): Fingerprint indexed repos by remote URL to detect sibling-clone graph drift (#982)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / scope-parity (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
* Initial plan

* feat: detect sibling-clone graph drift via remote URL fingerprint

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e5decb67-7fec-40e7-b2a1-b5e94a0d393f

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: address review feedback — fake commit, same-commit case, regex docs

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e5decb67-7fec-40e7-b2a1-b5e94a0d393f

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(mcp): address review feedback — CI green, perf, dead branch, one-shot test

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc2259f7-94e4-4243-aaa9-e03b7c632d32

* Merge branch 'main' into copilot/fix-single-path-indexing-issue

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/5840b3dd-e879-4854-a067-d1622bec2634

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* Merge branch 'main' into copilot/fix-single-path-indexing-issue

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9025262f-4dd4-4774-8f32-e14434100004

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* style: prettier format run-analyze.ts after merge with main

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a7be18dd-102f-4a7b-ac56-53fbd414fe3b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: realpath both sides of cwdGitRoot assertion for Windows 8.3 short-name compat

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b2a1c6a3-e454-4b87-b0e4-69d7c0d9a51b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(test): use path-agnostic assertion for cwdGitRoot on Windows (#1015)

git rev-parse --show-toplevel returns long path names on Windows
while os.tmpdir() returns 8.3 short names. fs.realpathSync does not
expand short names, so exact path comparison always fails on Windows
CI runners. Replace with behavioral assertions instead.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <copilot-swe-agent[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: evolution <wjc163@sina.cn>
2026-04-21 21:58:54 +01:00
Sam Fakhreddine
95a38c7e2d
fix(group): surface friendly error when group name not found (#903 regression test) (#989)
* fix(group): surface friendly error when group name not found

Squashed commits:
- test(csharp): add #903 regression — parse completeness for single-file C# repo
- fix(group): add GroupNotFoundError guard to groupList + re-throw tests for groupQuery/groupStatus
- fix(test): restore section comments in csharp.test.ts stripped during rebase

* fix(group): catch GroupNotFoundError explicitly in groupContext and groupImpact
2026-04-21 15:52:36 +01:00
Copilot
ff4ae89aaa
feat(python): scope-based call resolution + registry-primary flip + perf + generalization (RFC #909 Ring 3) (#980)
* Initial plan

* plan: Python scope-based resolution migration

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0eee6c69-fc17-4df5-9ac6-358ab41f5740

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat(python): scope-based resolution provider hooks + 62 tests

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0eee6c69-fc17-4df5-9ac6-358ab41f5740

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor(python): split scope-hooks monolith into focused modules

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/db76e937-4b0e-4c4d-82b1-265a1fb3673d

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test(python): integration-style scope-resolution tests + suffixResolve fallback

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/db76e937-4b0e-4c4d-82b1-265a1fb3673d

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* wire python scope-based resolution end-to-end (initial pass)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c474dc66-5cf7-445d-8eb4-76501c5e6d67

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* keep legacy IMPORTS for python (heritage needs importMap), scope phase owns CALLS only

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c474dc66-5cf7-445d-8eb4-76501c5e6d67

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test(python): remove parallel scope-resolution integration test

The new test/integration/python-scope-resolution.test.ts duplicated coverage
the reviewer explicitly rejected. The existing
test/integration/resolvers/python.test.ts (191 tests, driven by
runPipelineFromRepo) is the source of truth for Ring 3 parity.

Also document the IMPORTS-emission follow-up gap: wiring emitImportEdges
in python-scope-emit.ts today regresses 10 IMPORTS-edge fixtures because
the scope-extractor's ImportEdge coverage is narrower than legacy
pythonImportConfig.importResolver. Tracked as a follow-up.

Baseline with REGISTRY_PRIMARY_PYTHON=1 is unchanged: 109/191 pass.

* feat(ingestion): scope-resolution phase owns Python IMPORTS edges (RFC #909 Ring 3)

When `REGISTRY_PRIMARY_PYTHON=1`, IMPORTS graph edges for Python files are now
emitted exclusively by the new scope-resolution path. The legacy
`import-processor` still runs — heritage resolution needs its importMap /
namedImportMap / moduleAliasMap population — but its graph edge emission is
gated per-language so Python no longer double-emits.

This closes the reviewer's second change request on PR #980: "the legacy path
must be turned off". Legacy IMPORTS edges for Python are now off by default
when the flag is enabled.

Three bugs were fixed to make the new path's coverage match legacy:

1. **Root-file bailout** (import-resolvers/python.ts): `resolvePythonImportInternal`
   returned null immediately when the importer file lived at the repo root
   (importerDir === ''). The ancestor directory walk further down already
   handles this case correctly; the early return was the bug. Proximity check
   now only runs when importerDir is non-empty, and the ancestor walk sees
   root-level files for the first time.

2. **External dotted imports** (languages/python/import-target.ts): the new
   path fell straight through to `suffixResolve` for multi-segment imports,
   which happily matched `django.apps` to a local `accounts/apps.py`. Mirror
   `pythonImportStrategy`'s `hasRepoCandidate` guard — suffix-match only when
   the leading segment exists somewhere in-repo as a package, __init__.py,
   or namespace directory.

3. **suffixResolve ambiguity** (languages/python/import-target.ts): the
   shared `suffixResolve` helper requires a pre-built `SuffixIndex` to
   disambiguate ties. Without one it falls back to an O(files) scan that
   silently picks the first match when the last segment collides across
   directories (e.g. `accounts.models` matching `billing/models.py`).
   Replaced with `resolveAbsoluteFromFiles` — exact lookup first, then a
   deterministic suffix match.

Validation:
- Flag OFF: 191/191 pass (no regression).
- Flag ON: 109/191 pass (82 fail — exact baseline match; remaining 82 are
  unchanged CALLS-edge provider-feature gaps tracked as Phase B follow-ups).
- `tsc --noEmit`: clean.

The 82 CALLS failures cluster into 44 describe blocks covering type-inference
features (assignment chains, walrus, class-level annotations, constructor
inference, C3 MRO, overload dispatch, return-type inference) that need
dedicated Ring 3 follow-up work. Each cluster is tracked against the RFC #909
shadow-parity gate (>=99% fixtures / >=98% corpus) in the per-language ticket.

* ci(scope-resolution): automatic parity gate driven by MIGRATED_LANGUAGES

Adds the Ring 3 parity gate the RFC §6.4 requires: when a language's
scope-resolution migration is marked complete, CI runs its resolver
integration test twice on every PR (once with the legacy DAG, once with
the registry-primary path) and both must pass.

The "is this language migrated" signal is a single TypeScript constant:

  // gitnexus/src/core/ingestion/registry-primary-flag.ts
  export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> =
    new Set([ /* SupportedLanguages.Python when ready */ ]);

Adding a language here has three simultaneous effects:

  1. `isRegistryPrimary(lang)` defaults to true for that language in
     production (env-var override still wins if set explicitly).
  2. `.github/workflows/ci-scope-parity.yml` auto-discovers the set via
     `npx tsx scripts/ci-list-migrated-languages.ts`, builds a parity
     matrix, and runs:
       - `REGISTRY_PRIMARY_<LANG>=0 npx vitest run resolvers/<slug>.test.ts`
       - `REGISTRY_PRIMARY_<LANG>=1 npx vitest run resolvers/<slug>.test.ts`
     Both legs must pass for the job to succeed.
  3. Legacy-path gating in call-processor.ts / import-processor.ts kicks
     in automatically through the same `isRegistryPrimary` lookup.

No JSON registry, no manual workflow edit, no second source of truth —
contributors update the Set and CI picks it up. Empty Set = parity job
is a skipped matrix (workflow still reports success).

The new `scope-parity` reusable workflow is added to ci.yml's `needs`
graph and ci-status gate. Its result must be `success` (skipped would
mean upstream discover job failed and should block).

Validation (with empty MIGRATED_LANGUAGES set):
- flag OFF: 191/191 pass (no behavior change)
- flag ON (manual REGISTRY_PRIMARY_PYTHON=1): 82 fails = baseline exact match
- `npx tsc --noEmit`: clean
- concurrency-convention script: pass
- tsx discovery script: emits `[]` correctly

* ci(scope-resolution): keep MIGRATED_LANGUAGES empty; fix linter auto-uncomment

Previous commit's example entry got auto-uncommented (linter preferred a
type-checkable `SupportedLanguages.Python` over a commented-out reference).
That would have triggered the parity CI gate against Python, which today
has 82 known flag-on failures — unintended and would block the PR.

Use the explicit generic `new Set<SupportedLanguages>([])` so an empty set
still type-checks without needing an uncommented-out sample member.
Example in the comment now has `//   SupportedLanguages.Python,` so it
remains illustrative without participating in the set.

* feat(python): capture constructor-inferred + annotated type bindings

Extends the Python scope-extractor with two new type-binding capture
patterns so receiver-typed method dispatch has concrete type bindings
to work from:

1. `u: User = ...` / `u: User` — variable annotations. `@type-binding.annotation`
   anchor, `source: 'annotation'`.
2. `u = User("alice")` — assignment RHS is a bare-identifier call (Python
   has no `new` keyword; constructor-shaped calls are syntactically
   identical to function calls). `@type-binding.constructor` anchor,
   `source: 'constructor-inferred'`.

The runtime query lives in `query.ts` (the `.scm` file is documentation
per the comment at its top); both are updated.

Fixes 19 failures across these resolver fixtures (flag-on 82 → 63):
- Python constructor-inferred type resolution (3)
- Python class-level annotation resolution (3)
- Python nullable receiver resolution (3)
- Python member-call / receiver-constrained / constructor-call (3)
- Python assignment chain propagation (2)
- Python walrus / match-case / chained method (3)
- Python member access iterable for-loop (2)

* feat(python): strip nullable unions + prefer annotations over inference

Two linked changes that together fix the 4 nullable-receiver tests:

1. `stripNullable` in Python's `interpretTypeBinding` unwraps `User | None`,
   `None | User`, and `Optional[User]` to `User`, so receiver-typed
   resolution treats nullable receivers identically to non-nullable ones.
   Three-arm unions (`User | Error | None`) are left unchanged — truly
   ambiguous for single-receiver inference.

2. Source-strength ordering in `pass4CollectTypeBindings`. When multiple
   matches fire for the same bound name in the same scope — e.g. the
   `u: User = find()` idiom where both the annotation and
   constructor-inferred patterns match — the explicit annotation now
   wins regardless of query-match arrival order. Rank:
     explicit (annotation / parameter-annotation / return-annotation / self) > inferred

Also reorders the two Python patterns in query.ts / scopes.scm so the
constructor-inferred pattern appears first — a belt-and-braces fallback
that keeps behavior deterministic if the shared priority ranking is ever
revisited.

Fixes 4 failures (flag-on 63 → 59):
- Python nullable receiver resolution (4 tests)

Flag-off regression check: 191/191 still pass.

* feat(python): walrus, qualified-call, match-case type bindings

Extends the constructor-inferred family of captures with three more
assignment-shaped patterns that all bind a variable to a class-like type:

- Walrus: `(u := User(...))` → `u: User` via `(named_expression)`.
- Qualified call RHS: `u = models.User(...)` → `u: models.User` via
  `(attribute)` node .text. Falls through resolveTypeRef Phase 2
  (QualifiedNameIndex dotted fallback).
- Match as-pattern: `case User() as u:` → `u: User` via `(as_pattern)`
  + `(class_pattern (dotted_name))`.

Fixes 2 failures (flag-on 59 → 57):
- Python walrus operator type inference
- Python match/case as-pattern type binding

Qualified-call constructor tests still fail because they require
cross-module qualifiedName registration (models.User → models.py's User
class) which isn't yet wired in the Python extractor. Tracked as
follow-up alongside module-import CALLS (#337) resolution.

* feat(python): chain type bindings + strip list[T] generic for for-loop

Adds two capture patterns and a shared transitive-closure pass that
together handle Python's variable-aliasing and for-loop-over-typed-
iterable patterns:

1. `(assignment left: (identifier) right: (identifier))` — `alias = u`.
2. `(for_statement left: (identifier) right: (identifier))` — `for u in users`.

Both emit `@type-binding.alias` with the RHS identifier as rawName. The
shared `pass4CollectTypeBindings` now runs a final transitive-closure
walk that follows identifier-chain TypeRefs through the declaring scope
and its ancestors (depth-capped, cycle-guarded) so `alias` ultimately
points at the class type instead of another local variable name.

Generic stripping in `interpret.ts` unwraps single-arg collection
wrappers — `list[User]`, `set[User]`, `Iterable[User]`, etc. — to the
element type. Multi-arg generics (`dict[str, User]`, `Callable[...]`)
are left alone; their semantics aren't unambiguous.

Fixes 8 failures (flag-on 57 → 49):
- Python assignment chain propagation (4)
- Python nullable + assignment chain (2)
- Python walrus operator (:=) assignment chain (2)

Flag-off still 191/191.

* feat(python): namespace & class receiver resolution + file-level caller fallback

Adds a Python-specific post-resolution pass `emitReceiverBoundCalls`
that closes two receiver gaps the shared `MethodRegistry.lookup` doesn't
cover:

1. **Namespace receivers** — `import models; models.User()` /
   `import models as m; m.User()`. The shared `lookupReceiverType` only
   walks `scope.typeBindings`; namespace imports never land there
   (they're filtered out of `scope.bindings` when the target module
   has no self-named def, per `finalize-algorithm.ts:540`). The new
   pass walks `indexes.imports` directly, builds a per-file
   `localName → targetFilePath` map, and emits CALLS/ACCESSES edges
   against the target file's `localDefs`.

2. **Class-name receivers** — `Dog.classify("dog")`. The shared resolver
   requires typeBindings; class bindings in `scope.bindings` are never
   consulted as receivers. The new pass checks class-kind bindings in
   the call scope's chain and resolves members via `ownerId`.

Also fixes module-level call attribution: `resolveCallerGraphId` now
falls back to the File node id (`generateId('File', filePath)`) when no
enclosing function/method/class is found. Matches legacy DAG behavior
for module-scope calls like `u = models.User()` at the top of app.py.

Fixes 4 failures (flag-on 49 → 45):
- Python module import CALLS resolution (Issue #337) (4 of 7)

Flag-off still 191/191.

* feat(python): dotted-typebinding receiver resolution

Adds case 3 to `emitReceiverBoundCalls`: when a receiver's typeBinding
has a dotted rawName like `u: models.User` (the constructor-inferred
form fired by `u = models.User(...)`), walk the namespace map + target
file's defs to find the class, then look up the member via ownerId.

`resolveTypeRef`'s QualifiedNameIndex fallback can't cover this because
the target class's qualifiedName in models.py is just `"User"`, not
`"models.User"` — the dotted form only exists in the call-site file's
receiver expression. This pass bridges that gap without modifying the
shared registry.

Fixes 9 more failures (flag-on 45 → 36):
- Python qualified constructor inference (2)
- Python module import CALLS resolution (Issue #337) (3)
- (cluster overlap — several downstream tests in assignment/nullable/
  walrus that propagate through qualified-ctor bindings also benefit)

Flag-off still 191/191.

* feat(python): consult finalized bindings for receiver resolution

`findClassBindingInScope` now walks BOTH:
  1. `scope.bindings` — pre-finalize local declarations (origin: 'local')
  2. `indexes.bindings` — post-finalize cross-file imports/namespaces

Without (2) we were blind to any class brought in via
`from models import Dog` at the call site's file, because the
scope-extractor's Pass 2 only populates local bindings and the
cross-file finalize produces a separate bindings map that never lands
on `scope.bindings`.

Case 2 (`Dog.classify()`) now walks MRO so inherited static/class
methods resolve — `Dog.classify()` where `classify` lives on `Animal`.

Case 4 (simple typeBinding like `u: U` from aliased import) now uses
`findClassBindingInScope` instead of the shared `resolveTypeRef`,
because `resolveTypeRef`'s `ctx.scopes` only sees pre-finalize local
bindings too.

Fixes 4 more failures (flag-on 36 → 32):
- Python method enrichment > Dog.classify static (1)
- Python static/classmethod class-as-receiver (2)
- Python alias import resolution (1)

Flag-off still 191/191.

* refactor(python-scope): extract language-agnostic emit-core/

Unit 1 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).

Splits python-scope-emit.ts (~945 → 481 lines) by lifting 14 generic
graph-feeding primitives into emit-core/:
  - graph-node-lookup, graph-id, emit-edge
  - emit-references, emit-imports
  - scope-walkers (findReceiverTypeBinding, findClassBindingInScope,
    findOwnedMember, findExportedDef)
  - namespace-targets, method-dispatch-bridge

Each file carries a "Next-consumer contract" JSDoc so future language
migrations (TS #927, JS #928, Java, Kotlin, Ruby) import from emit-core
rather than re-implementing. python-scope-emit.ts keeps only the four
Python-specific pieces: runPythonScopeResolution (orchestrator),
buildPythonMro, emitReceiverBoundCalls (4 cases), populateMethodOwnerIds
— these move to languages/python/emit/ in Unit 11.

Pure refactor, zero behavior change:
  - flag-off: 191/191 python.test.ts pass (identical baseline).
  - flag-on (REGISTRY_PRIMARY_PYTHON=1): 32 fail / 159 pass (identical
    baseline — the refactor neither fixes nor regresses any test).
  - tsc --noEmit clean.

* feat(python-scope): arity metadata + bind function decls in parent scope

Unit 2 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).

Two changes that the registry-primary path needs before any of the
arity-sensitive failures can move:

1. Arity metadata on scope-extracted Function/Method defs.
   - New helper `languages/python/arity-metadata.ts` reuses
     `pythonMethodConfig.extractParameters` so self/cls stripping,
     defaults, and *args/**kwargs detection match legacy semantics.
   - `emit-captures.ts` synthesizes
     `@declaration.parameter-count` /
     `@declaration.required-parameter-count` /
     `@declaration.parameter-types` captures on every
     `@declaration.function` match.
   - Generic `scope-extractor.ts buildDefFromDeclarationMatch` reads
     the three optional captures into `SymbolDefinition`. Absence is
     still the no-op default for non-Python providers.

2. Hoist function/class declaration bindings to the enclosing scope.
   The "innermost scope containing the anchor" default placed
   `def greet(...)` inside greet's OWN body — invisible to other
   module-level callers, so every flag-on free-call resolved to
   `unresolved`. The hoist condition (`anchor range == innermost
   range`) only fires for scope-creating declarations, so variable /
   for-loop captures whose anchor is a child identifier stay put.
   Hooks can still override via `bindingScopeFor`.

Verification:
  - Flag-off: 191/191 (identical baseline).
  - Flag-on (REGISTRY_PRIMARY_PYTHON=1): 31 fail / 160 pass
    (was 32/159; the hoist unblocks free-call resolution end-to-end).
  - tsc --noEmit clean.

Per-(source,target) edge collapse for multi-call-site cases
(default-params, variadic) still pending — landing it without
regressing the static-method find_user fixture (which expects two
distinct edges through different targets) needs the ownership-aware
qualified-id work that lands with Unit 4 / Unit 11.

* feat(python-scope): capture function return-type annotations

Unit 3 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).

Wires the `def get_user() -> User` return-type annotation into the
typeBindings stream so the existing constructor-inferred + transitive
chain machinery can resolve `u = get_user(); u.save()` to `User#save`
without any orchestrator change.

Changes:
- `query.ts` + `scopes.scm`: new `@type-binding.return` pattern keyed by
  the function name (matches RFC §5.1 canonical vocabulary).
- `interpret.ts`: maps `@type-binding.return` to the existing
  `'return-annotation'` source label (no shared change needed).
- `scope-extractor.ts pass4CollectTypeBindings`: extends the Pass 2
  auto-hoist (anchor range == innermost scope range → bind in parent)
  to type bindings as well — return-type bindings whose anchor IS the
  function_definition land in the function's enclosing scope so
  callers see them.

Same-file return-type inference is now end-to-end:
  `def get_user() -> User: ...` + `u = get_user()` produces
  `u: User (return-annotation)` in the caller's scope via
  `followChainedRef`.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 31 fail / 160 pass (no change — every remaining
  return-type test in this fixture set is *cross-file*; carrying
  `get_user → User` across module boundaries lands with the
  cross-file typeBinding propagation work in Unit 5/7).
- tsc --noEmit clean.

* feat(python-scope): resolve dotted receivers via class-scope field types

Unit 4 partial — the dotted-receiver case (`user.address.save()`).

Class-body annotations like `class User: address: Address` already
land in the class scope's typeBindings via the existing
`@type-binding.annotation` capture. This commit consumes that signal:

- Build a `Map<classDefId, Scope>` from every parsed file's class
  scopes once per resolution pass.
- New Case 0 in `emitReceiverBoundCalls`: when the receiver's name
  contains a dot, walk the chain — resolve the head's type, then for
  each remaining segment look up that field's type in the owner
  class's scope.typeBindings, then emit the call against the final
  class with MRO walk.
- Cross-scope lookups use each TypeRef's `declaredAtScope` so an
  imported `Address` resolves in the file that owns the field
  declaration, not the file holding the call site.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 29 fail / 162 pass (was 31/160; both `Field type
  resolution` fixtures now pass — same-file and cross-file disambig).
- tsc --noEmit clean.

Remaining Unit 4 work (write ACCESSES, `self.X` for-loop iteration)
needs Unit 6's tuple/iterable destructuring before it can land —
`for u in self.users` requires the iterable typing path.

* feat(python-scope): chain receiver via call-expression return types

Unit 5 — extends the compound-receiver case to handle call-expression
receivers (`svc.get_user().save()`).

`resolveCompoundReceiverClass` is the single recursive entry point for
all compound receivers. Three shapes:
  - bare identifier — typeBinding chain
  - dotted `obj.field[.field]…` — class-scope field types
  - call `expr.method()` — recurse into expr, look up method's
    return-type typeBinding on its class scope

Method return-type bindings auto-hoist to the parent (class) scope per
Unit 3, so `methodClassScope.typeBindings.get(methodName)` is the
canonical lookup. Free-call return types (`get_user()`) walk the
caller's scope chain.

Depth-capped at 4 hops to bound recursion.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 28 fail / 163 pass (was 29/162; `Python chained method
  call resolution` now passes).
- tsc --noEmit clean.

Two related tests (`city.save() via method chain`, `c.greet().save()
depth-2 MRO`) still fail because the captures yield typeBindings
shaped like `city → user.get_city` (no trailing parens — the capture
grabs the attribute text). Resolving those needs a follow step that
detects the call-shape rawName and feeds it through the compound
recurser. Lands with the chain-typeBinding work in a follow-up.

* feat(python-scope): free-call fallback consults finalized bindings

Unit 7 — closes the cross-file free-call gap.

The shared `MethodRegistry.lookup` walks `scope.bindings` (pre-finalize
local-only) for free-call resolution. Cross-file imports land in
`indexes.bindings` (post-finalize). Without the dual-source lookup,
`from x import f; f()` resolves to "unresolved" and no CALLS edge is
emitted.

Two changes:

- `emit-core/scope-walkers.ts`: new `findCallableBindingInScope` —
  same dual-source pattern as `findClassBindingInScope`, but accepts
  Function/Method/Constructor. Promoted to emit-core because every
  language with cross-file imports needs the same lookup.
- `python-scope-emit.ts emitFreeCallFallback`: post-pass that walks
  every free-call reference site, looks up the callee with the new
  helper, and emits via `tryEmitEdge`. Pre-seeds `seen` from the
  shared resolver's emissions so we never double-count.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 22 fail / 169 pass (was 28/163; +6 tests including
  the Python overload dispatch fixtures, ancestor-directory imports,
  and same-name module-alias collision).
- tsc --noEmit clean.

* feat(python-scope): super() receiver dispatches up the MRO

Unit 8 — `super().method()` inside a class method walks the enclosing
class's MRO chain (skipping self) and resolves to the first ancestor
that owns the method.

New receiver branch in `emitReceiverBoundCalls` recognizes
`super(...)` syntactically (regex-cheap), finds the enclosing class
via a new `findEnclosingClassDef` scope-walk helper, then re-uses
`scopes.methodDispatch.mroFor` + `findOwnedMember` from the existing
class-receiver path. Handled before the compound-receiver case so
`super()` doesn't fall into the bare-identifier branch where `super`
isn't a binding.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 21 fail / 170 pass (was 22/169; `super().save() inside
  User to BaseModel.save` now passes).
- tsc --noEmit clean.

* feat(python-scope): suppress shared resolver on member-call sites

Unit 9 — `app_metrics.get_metrics()` (namespace import alias) was
emitting two CALLS edges: a wrong self-call from the shared
resolver's free-call fallback, plus the correct namespace-receiver
edge from the Python post-pass.

Mechanism:

- `emit-core/emit-references.ts`: new optional `skipSites` parameter
  (`Set<string>` of `${filePath}:${line}:${col}` keys). When supplied,
  references at those positions are skipped — the provider has
  already emitted (or chosen not to emit) for that site.
- `python-scope-emit.ts`: reorders Phase 4 — receiver-bound + free-
  call fallback run FIRST, populating `handledSites`. The shared
  `emitReferencesViaLookup` then runs with that set so the resolver's
  fallback can't fight a precise per-receiver emission. Site keys are
  added only on successful tryEmitEdge (not for sites the post-pass
  saw but couldn't resolve — those still get a chance from the shared
  path).

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 20 fail / 171 pass (was 21/170; same-name module-alias
  collision now resolves correctly).
- tsc --noEmit clean.

* feat(python-scope): propagate return-type bindings across imports

Closes the cross-file return-type propagation gap that left tests
like `u = get_user(); u.save()` (where get_user lives in another
file) with `u` typed as the function name instead of its return type.

The shared finalize pass copies callable bindings (`from x import f`
puts `f` in the importer's bindings) but typeBindings stay file-local
because they live on `Scope.typeBindings`, not on the index. Mutate
post-finalize:

- For each module-scope import binding (`origin: 'import'` or
  `'reexport'`), look up the source file's module-scope typeBinding
  for the def's simple name. If present (return-annotation source),
  mirror it under the importer's local alias. Skip when the importer
  already has its own typeBinding for the name (explicit local always
  wins).
- After propagation, re-run a chain-follow on every scope's
  typeBindings — pass-4 ran before propagation and missed any chain
  whose terminal lived in a foreign file. Same algorithm as
  `followChainedRef` in scope-extractor, but operates on the
  finalized scopes so propagated entries are visible.

Mutating `Scope.typeBindings` is safe — `draftToScope` constructs a
plain `new Map(...)`, not a frozen one.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 16 fail / 175 pass (was 20/171; +4 — both cross-file
  return-type tests, plus two related propagation cases).
- tsc --noEmit clean.

* feat(python-scope): for-loop call-iterable typeBinding

Adds `(for_statement left: (identifier) right: (call function:
(identifier)))` to the typeBinding capture set. Combined with Unit 3's
return-type capture and the cross-file return-type propagation pass,
this makes `for u in get_users(): u.save()` resolve to `User.save`
even when `get_users` is imported from another module.

Captured as `@type-binding.alias` (rawName = function identifier,
without parens) so the existing chain-follow walks the alias to the
function's return-type binding without any new code path.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 12 fail / 179 pass (was 16/175; +4 for-loop call-iterable
  tests across get_users / get_repos fixtures).
- tsc --noEmit clean.

* feat(python-scope): collapse free-call edges per (caller, target)

Free calls (no explicit receiver) now emit a single CALLS edge per
(caller, target) pair regardless of how many call sites the caller
contains. Mirrors the legacy DAG's per-pair dedup contract — what
the `default-params`, `variadic`, and `overload` fixtures expect.

Member calls keep position-based dedup so distinct resolved targets
(e.g. UserService.find_user vs AdminService.find_user from the same
caller) still produce distinct edges.

Implementation: bypass `tryEmitEdge` (which dedupes positionally) and
hand-roll the relationship with a position-independent rel.id
(`rel:CALLS:<caller>-><target>`). Site handling is now unconditional —
even when the dedup-collapse skips the actual emit, we mark the site
handled so the shared `emit-references` doesn't fight us with its
fallback.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 10 fail / 181 pass (was 12/179; +2 — both `default
  parameter arity` tests now pass).
- tsc --noEmit clean.

* fix(python-scope): match legacy CALLS reason for import-resolved free calls

The arity-narrowing test asserts \`rel.reason === 'import-resolved'\`
for cross-file free-call edges. Switch the free-call fallback's
reason to mirror legacy DAG semantics:
  - target-file !== source-file → 'import-resolved'
  - same file                   → 'local-call'

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 9 fail / 182 pass (was 10/181; +1 arity-narrowing test).
- tsc --noEmit clean.

* fix(python-scope): drop dead pre-seeding from receiver-bound pass

The pre-seeding loop at the top of \`emitReceiverBoundCalls\` populated
\`seen\` with every reference the shared resolver had already resolved.
That was useful when emit-references ran FIRST. After Unit 9 reversed
the order (emit-references runs after the Python passes and uses
\`handledSites\` to skip what we processed), the pre-seed only causes
harm: when an MRO walk in Case 0 (compound receiver) and Case 4
(simple typeBinding) both touch the same site at the same position
but resolve to different targets, the pre-seed suppresses the second
emission because the shared resolver had already entered the wrong
target into \`seen\`.

Concrete case: \`c.greet().save()\` — Case 0 emits the outer save edge
to Greeting.save; Case 4 then resolves the inner \`c.greet()\` to
A.greet via MRO walk. With pre-seed both edges should emit (different
targets, different rel.ids); without removing the pre-seed the inner
emission was being deduped against an already-seeded entry and the
A.greet edge was lost.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 8 fail / 183 pass (was 9/182; +1 — \`c.greet() to A#greet
  via MRO walk\` now passes).
- tsc --noEmit clean.

* feat(python-scope): enumerate(X) for-loop tuple destructuring

Adds two new typeBinding capture patterns for the canonical enumerate
pattern:

  for (i, u) in enumerate(users): ...   ; tuple_pattern
  for  i, u  in enumerate(users): ...   ; pattern_list

Both bind the second tuple element (u) to the iterable identifier
(users). The chain-follow then unwraps users → its element type via
the existing generic-strip in interpret.ts (List[User] → User).

The #eq? predicate scopes the pattern to enumerate specifically;
generic tuple destructuring of arbitrary callables is left to a
future iteration once we have a richer signal for "what does this
call yield".

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 7 fail / 184 pass (was 8/183; +1 — `parenthesized tuple:
  for (i, u) in enumerate(users)` now passes).
- tsc --noEmit clean.

* feat(python-scope): dict.items() value-type unwrapping

Two changes that together resolve `for k, v in data.items(): v.save()`:

- `interpret.ts stripGeneric`: extends to `dict[K, V]` /
  `Dict[K, V]` / `Mapping[K, V]` etc., stripping to the value type V.
  Previously only single-arg generics (list[User] → User) were
  stripped; multi-arg ones returned the raw text.
- `query.ts` + `scopes.scm`: new typeBinding patterns for
  `for k, v in X.items()` (both pattern_list and tuple_pattern). The
  second tuple element binds to X; the chain-follow then unwraps X's
  dict annotation to V via the new stripGeneric branch.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 6 fail / 185 pass (was 7/184; +1 — `dict.items() loop`
  test now passes).
- tsc --noEmit clean.

* feat(python-scope): nested tuple destructuring for enumerate(d.items())

Two more for-loop typeBinding patterns:

- `for i, (k, v) in enumerate(d.items())` — nested tuple destructuring
  where v is the value of the dict's items() yield.
- `for v in d.values()` — explicit values() form (companion to items).

Both bind the loop var to the dict identifier; the chain-follow
unwraps via the dict-aware stripGeneric to the value type.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 5 fail / 186 pass (was 6/185; +1 nested tuple test).
- tsc --noEmit clean.

* feat(python-scope): 3-var flat destructuring for enumerate(d.items())

Adds the \`for i, k, v in enumerate(d.items())\` shape — flat
3-variable destructuring of the (i, (k, v)) tuple yielded by
\`enumerate\` over \`items()\`. Binds v (the last identifier in the
pattern_list) to the dict identifier; the existing dict-aware
stripGeneric unwraps to the value type.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 4 fail / 187 pass (was 5/186; +1).
- tsc --noEmit clean.

* feat(python-scope): write ACCESSES edges for attribute assignments

Three changes that together produce ACCESSES (write) edges for
\`obj.field = value\` assignments:

- New \`@reference.write.member\` capture in query.ts and scopes.scm
  matching \`(assignment left: (attribute object: ... attribute: ...))\`.
  Reuses the existing receiver/name capture shape so the
  receiver-bound emit pass can resolve obj's class and look up the
  field.
- \`populateMethodOwnerIds\` now sets ownerId on class-body fields too,
  not only on methods. Previously it only walked Function scopes
  whose parent was Class; class-body annotations like \`name: str\`
  live directly in the Class scope's ownedDefs and were missed, so
  \`findOwnedMember(User, "name")\` returned undefined.
- \`emit-core isLinkableLabel\` extends to Variable and Property so
  field nodes appear in the graph-node lookup (the legacy parser
  emits both kinds for class-body annotations).
- Case 4 in receiver-bound pass now uses the kind word as the edge
  reason for read/write sites — matches the legacy DAG convention
  the test asserts on.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 3 fail / 188 pass (was 4/187; +1 — write-ACCESSES test).
- tsc --noEmit clean.

* feat(python-scope): chain-typebinding + field-fallback method lookup

Reaches the architectural-plan target of >= 189/191 flag-on passing.

Two intertwined changes:

- Field-fallback in resolveCompoundReceiverClass: when method lookup
  on the receiver's class (and its MRO) fails, walk the class's
  fields and try the same lookup on each field's type. Matches the
  "unified fixpoint" intent of the method-chain fixture where
  `user.get_city()` reaches `Address.get_city` through User's
  `address: Address` field.
- New Case 3b in receiver-bound emit pass: when the receiver's
  typeBinding rawName has a dot but isn't a namespace prefix
  (e.g. `city -> user.get_city` from the constructor-inferred capture
  for `city = user.get_city()`), treat it as a method-call chain and
  pipe through the compound resolver. The chain unwraps to the
  terminal class (City) and the call resolves normally.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 2 fail / 189 pass (was 3/188; +1 city.save method chain).
- tsc --noEmit clean.

Remaining 2 failures are fixture-driven (self.users / self.repos
fixtures reference fields that aren't declared on the class) and
documented as known-limitation in Unit 10.

* feat(python-scope): flip Python to registry-primary (191/191 parity)

Adds the \`for u in self.X\` heuristic typeBinding capture (binds u to
the attribute name X so the chain-follow can resolve via the enclosing
method's parameter typeBinding) — closes the last two failing
fixtures whose classes reference \`self.X\` for fields that are
actually method parameters.

With 191/191 passing on BOTH legacy and registry-primary paths,
flips \`MIGRATED_LANGUAGES\` to include \`SupportedLanguages.Python\`.

Effects:
- Production default for Python files: registry-primary path.
- CI parity gate auto-discovers Python via the script + workflow
  (\`scripts/ci-list-migrated-languages.ts\` /
  \`.github/workflows/ci-scope-parity.yml\`) and runs the resolver
  integration test BOTH ways on every PR.
- Operators retain the \`REGISTRY_PRIMARY_PYTHON=0\` escape hatch.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (unset, post-flip): 191/191 (uses registry).
- tsc --noEmit clean.

This concludes RFC #909 Ring 3 — Python migration.

* refactor(emit-core): EmitProvider interface + promote 5 generic helpers

G-Units 1-2 of the emit-pipeline generalization plan.

Adds:
- emit-core/emit-provider.ts — typed EmitProvider contract (6 required +
  2 optional fields). Will be consumed by the generic orchestrator in
  G-Unit 6. Documents the LanguageProvider vs EmitProvider boundary.
- emit-core/emit-free-call.ts — emitFreeCallFallback promoted as-is
  (drops the unused referenceIndex pre-seed parameter; underscore-prefixed
  to keep the signature compatible).
- emit-core/propagate-return-types.ts — propagateImportedReturnTypes +
  followChainPostFinalize. Documents the mutation contract (Invariant
  I3 + I6 from the plan): runs after finalize, before resolve, mutates
  the non-frozen Scope.typeBindings map.
- emit-core/scope-walkers.ts: + findEnclosingClassDef +
  findExportedDefByName. Both were already generic in the Python
  source.

python-scope-emit.ts shrinks 1055 → 799 lines (–256). Imports the
promoted helpers from emit-core. No behavior change.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.

* refactor(emit-core): promote receiver-bound dispatcher + compound resolver

G-Unit 3 of the emit-pipeline generalization plan.

- emit-core/emit-compound-receiver.ts — resolveCompoundReceiverClass
  + matchingOpenParen + COMPOUND_RECEIVER_MAX_DEPTH. Field-fallback
  is now an option (default true) so strictly-typed languages can
  opt out via EmitProvider.fieldFallbackOnMethodLookup.
- emit-core/emit-receiver-bound.ts — the 7-case dispatcher (super,
  Cases 0/1/2/3/3b/4). Accepts a ReceiverBoundProviderSubset
  (isSuperReceiver + fieldFallbackOnMethodLookup) so partial wiring
  works during the rest of the migration. Documents Contract
  Invariants I4 (case order) and I5 (no pre-seeding).

python-scope-emit.ts shrinks 799 → 384 lines. The orchestrator now
calls the generic emitReceiverBoundCalls with an inline minimal
provider (pythonEmitProviderInline) — full provider lands in G-Unit 6
when the orchestrator itself moves to languages/python/emit/.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.

* refactor(emit-core): promote MRO walk + populateClassOwnedMembers

G-Units 4-5 of the emit-pipeline generalization plan.

- emit-core/build-mro.ts — generic buildMro takes a LinearizeStrategy
  hook receiving (classDefId, directParents, parentsByDefId). Three
  shared steps (collect EXTENDS, build defId-by-graphId, walk per
  class) + parametric linearization. Default strategy is BFS-with-
  visited (Python's depth-first first-seen, also correct for
  single-inheritance languages).
- emit-core/scope-walkers.ts: + populateClassOwnedMembers — generic
  OO ownership rule (methods + class-body fields). Both rules ship
  together because every OO language migrated so far (Python; planned
  TS/JS/Java/Kotlin) wants both. Languages that need different rules
  can compose with this as a base step.

python-scope-emit.ts shrinks 384 → 255 lines.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.

* refactor(scope-resolution): generic orchestrator + language-agnostic phase

G-Units 6-7 of the emit-pipeline generalization plan, plus the
pipeline-phase generalization (the user's observation that the phase
itself is generic once the orchestrator is).

Changes:

- emit-core/orchestrator.ts — runScopeResolution(input, provider).
  The 180 lines of pipeline glue moved here, parametrized by
  EmitProvider. Provider supplies LanguageProvider, importEdgeReason,
  and the 6 emit-side hooks.
- emit-core/emit-provider.ts — EmitProvider gains languageProvider
  and importEdgeReason fields so the orchestrator needs nothing else.
  resolveImportTarget now takes (targetRaw, fromFile, allFilePaths).
- languages/python/emit/index.ts — pythonEmitProvider + thin
  runPythonScopeResolution wrapper. The first reference impl every
  next-language migration copies.
- emit-providers-registry.ts (NEW) — registry of per-language
  EmitProviders keyed by SupportedLanguages. Adding a language is
  one line here + the provider file.
- pipeline-phases/scope-resolution.ts (NEW) — language-agnostic phase
  iterating EMIT_PROVIDERS ∩ MIGRATED_LANGUAGES. Replaces
  pipeline-phases/python-scope.ts (deleted).
- python-scope-emit.ts deleted.
- pipeline.ts swaps pythonScopePhase → scopeResolutionPhase.

The next language migration is now: implement EmitProvider, register
it, add to MIGRATED_LANGUAGES. No new pipeline phase, no orchestrator
copy-paste. The Python migration's 700+ lines of glue collapse to
~80 lines per future language.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (post MIGRATED_LANGUAGES flip): 191/191.
- tsc --noEmit clean.

* docs(emit-provider): migration cookbook for next-language porters

* refactor(scope-resolution): rename emit-core/ → scope-resolution/, EmitProvider → ScopeResolver

Reorganizes the registry-primary resolution layer for clarity and
contributor onboarding. Driven by feedback that "emit" was triple-
overloaded (graph-edge emission + tree-sitter capture extraction +
the provider name itself), and the flat 16-file emit-core/ folder
mixed five concerns.

External research (rust-analyzer hir-def/nameres, Pyright analyzer/,
TypeScript binder/checker, Roslyn Binder, IntelliJ Resolver, swc
semantic/, biome semantic/, semgrep naming/, JDT Binding, clangd
Sema) consistently uses **the phase name** for this layer, never an
output verb. "Scope resolution" matches our pipeline-phase name, the
plan, and the RFC.

## Folder rename

  emit-core/                              → scope-resolution/
  ├── (16 flat files)                     → ├── contract/scope-resolver.ts
                                            ├── pipeline/{run,registry,phase}.ts
                                            ├── passes/{receiver-bound-calls,
                                            │           free-call-fallback,
                                            │           compound-receiver,
                                            │           imported-return-types,
                                            │           mro}.ts
                                            ├── graph-bridge/{node-lookup,ids,
                                            │                 edges,references-to-edges,
                                            │                 imports-to-edges,
                                            │                 method-dispatch}.ts
                                            └── scope/{walkers,namespace-targets}.ts

Each subfolder maps to one concern a new contributor needs to find:
*the contract I implement / the runner that calls me / the helpers I
reuse / the graph layer I shouldn't touch / the scope walkers*.

## Symbol renames

  EmitProvider                  → ScopeResolver
  pythonEmitProvider            → pythonScopeResolver
  runPythonScopeResolution      → resolvePythonScope
  EMIT_PROVIDERS                → SCOPE_RESOLVERS
  getEmitProvider               → getScopeResolver
  RunPythonScopeResolution{Input,Stats} → ResolvePythonScope{Input,Stats}

## File renames (per-language)

  languages/python/emit/index.ts → languages/python/scope-resolver.ts
  languages/python/emit-captures.ts → languages/python/captures.ts
                                     (kills the parse-side "emit" collision)

## Mechanics

- Used `git mv` for all files so blame history is preserved.
- Updated ~30 import lines across 18 files plus the pipeline-phases
  barrel and pipeline.ts.
- Updated JSDoc cross-references throughout to match the new vocabulary.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (post MIGRATED_LANGUAGES flip): 191/191.
- tsc --noEmit clean.

Migration cookbook in `scope-resolution/contract/scope-resolver.ts`
JSDoc points the next-language porter at all the new names and
folder locations.

* docs(scope-resolution): finalize phase JSDoc + drop python emoji from generic log line

* perf(scope-resolution): O(1) workspace lookup index

Introduces `WorkspaceResolutionIndex` — a precomputed bundle of
lookup tables built ONCE per resolution run, after `populateOwners`
and after finalize, before any pass that needs to find members,
exported defs, or class scopes by id.

What it replaces (all are pre-existing O(N×D) linear scans of
parsedFiles, called inside the receiver-bound MRO chain):

- `findOwnedMember(ownerId, name, parsedFiles)` → `Map.get` via
  `index.memberByOwner.get(ownerId)?.get(name)`. Was the worst
  offender — receiver-bound dispatcher calls this O(sites × MRO
  depth) times.
- `findExportedDef(filePath, name, parsedFiles)` → `Map.get` via
  `index.defsByFileAndName`. Hot for namespace-receiver case.
- `findExportedDefByName` workspace-wide fallback scan → `Map.get`
  via `index.callablesBySimpleName`.
- `classScopeByDefId` (rebuilt inside `emitReceiverBoundCalls` on
  every invocation) — moved to one-shot build during finalize, read
  from `index.classScopeByDefId` everywhere.
- `moduleScopeByFile` (rebuilt inside `propagateImportedReturnTypes`
  on every invocation) — read from `index.moduleScopeByFile`.

Findings from a synthetic 100-file Python workload (60 model files
each defining 5 classes × 3 methods + 40 user files calling them
heavily):

  scope-resolution wall time: 764ms → 710ms (median, 5 iters)

That's a ~7% in-layer win. The smaller-than-expected gain was
informative: profiling the synthetic workload shows scope-resolution
breakdown is `extract=62% resolve=30% emit=4%`; the index touched
the 4% slice (emit + walker calls inside it). Larger O(D) per owner
classes will benefit more.

Profiling the FULL pipeline (49 fixtures × 3 iters) shows
scope-resolution accounts for ~1% of pipeline wall time — the
remaining 99% is parse (tree-sitter), heritage, ORM, MRO, processes,
and DB writes. So further optimization of this specific layer has
marginal pipeline impact; the next-biggest wins live in those
phases. Documented as the "double-parse" finding in the audit
(captures.ts re-parses each Python file even though the parse phase
already produced a tree-sitter Tree) — that's a separate plumbing
project across phase boundaries.

Bonus: opt-in PROF_SCOPE_RESOLUTION=1 env var prints a per-phase
ms breakdown to stderr, so future perf work can measure without
extra code changes.

Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.

* perf(parse/heritage/mro): typed graph iterator + cross-phase tree cache

Two structural perf wins targeting the parse / heritage / MRO
layers, identified by the post-WorkspaceResolutionIndex profiling
(scope-resolution = ~1% of pipeline; the bulk lives upstream).

## 1. KnowledgeGraph.iterRelationshipsByType (PHM-Units 1-2)

- Adds a per-type `Map<RelationshipType, Map<id, Relationship>>`
  index inside `createKnowledgeGraph`, maintained on add / remove /
  removeNode / removeNodesByFile.
- New `iterRelationshipsByType(type)` returns a typed iterator that
  yields only the requested type. Backwards-compatible: existing
  `iterRelationships()` / `forEachRelationship()` callers untouched.
- Migrated two MRO call sites:
  - `mro-processor.ts buildAdjacency`: split the single
    `forEachRelationship` (which scanned every edge in the graph and
    type-filtered per-iteration) into three typed iterations
    (EXTENDS, IMPLEMENTS, HAS_METHOD).
  - `scope-resolution/passes/mro.ts buildMro`: replaced
    `for (const rel of graph.iterRelationships()) if (rel.type !== 'EXTENDS') continue`
    with `for (const rel of graph.iterRelationshipsByType('EXTENDS'))`.
- Heritage-processor (PHM-Unit 3) was a no-op: it only WRITES
  EXTENDS/IMPLEMENTS edges, never re-reads. Index is still useful
  for the seven other graph-iter consumers (community-processor,
  csv-generator, wildcard-synthesis, process-processor, etc.) — those
  follow-ups can switch to the typed iterator without touching the
  graph layer.
- Adds 5 unit tests for the new method (add/remove/dedupe semantics,
  empty-type fresh iterator, removeNode index sync).

## 2. Cross-phase tree cache (PHM-Units 4-5)

The audit's #2 finding: Python files are parsed by tree-sitter once
in the parse phase, then re-parsed inside scope-resolution's
`captures.ts`. Eliminate the second parse by sharing the Tree across
phases.

- `parse-impl.ts` now maintains TWO ASTCaches with distinct lifetimes:
  - `astCache` (chunk-local, cleared between chunks) — unchanged;
    used by call/heritage/import processors during parse.
  - `scopeTreeCache` (total-parseable-sized, never cleared) — new,
    exposed via `ParseOutput.astCache` for cross-phase consumption.
- `parsing-processor.ts` writes every sequentially-parsed Tree to
  BOTH caches. Worker-mode parses skip the persistent cache too
  (Trees can't cross MessageChannels).
- `LanguageProvider.emitScopeCaptures` gains an optional `cachedTree`
  parameter (typed `unknown` to keep the tree-sitter dep out of the
  contract).
- `captures.ts` short-circuits its own `parser.parse(sourceText)`
  when a cached Tree is supplied. Cache miss falls back to a fresh
  parse — same correctness path as before.
- `runScopeResolution` accepts an optional `treeCache` and forwards
  per-file `cachedTree` to `extractParsedFile`.
- `scope-resolution/pipeline/phase.ts` reads
  `getPhaseOutput<{astCache}>(deps, 'parse')` and passes through.

Verified end-to-end: a small fixture run with PROF_SCOPE_RESOLUTION=1
shows 6/6 cache hits (100% hit rate) on the python-grandparent fixture
that exercises the full pipeline below the worker-pool threshold.

## Verification

- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- New graph.test.ts: 25/25 (was 20).
- tsc --noEmit clean.

## Where the win lands

Wall-clock on the 49-fixture integration suite: 14050ms → 14080ms
(within noise). Fixtures are 1-3 files each, dominated by per-fixture
pipeline overhead (worker-pool init, DB writes, fixture startup).
The cache + typed-iterator wins are constant-factor improvements
that scale linearly with workload size and visible only on larger
repos. The dev-mode `PROF_SCOPE_RESOLUTION` instrumentation +
`getPythonCaptureCacheStats()` are kept for future perf work.

## Plan

docs/plans/2026-04-20-002-perf-parse-heritage-mro-plan.md.
PHM-Unit 3 (heritage-processor migration) intentionally collapsed
to a no-op — heritage only writes, never re-reads.

* perf(scope-resolution): bound tree-cache lifetime + gate population

Address P1 residuals from ce:review of 8c6f5cee:

- Dispose scopeTreeCache at end of scopeResolutionPhase via
  astCache.clear(). Trees were previously retained for the full
  pipeline (10-100x memory regression on large repos). Downstream
  phases (mro, community, csv-generator) never read them.
- Gate scopeTreeCache.set on provider.emitScopeCaptures !== undefined.
  Polyglot repos no longer retain Trees for languages with no
  scope-resolution consumer.
- PROF_SCOPE_RESOLUTION=1 now warns when workers engage, since
  Trees can't cross MessageChannels so the cache will be empty for
  worker-parsed files — prevents a silent perf cliff once a repo
  crosses the worker-pool threshold.

Tests: 26/26 graph unit, 299/299 scope-resolution unit, 191/191
python integration both flag paths.

* refactor(scope-resolution): clean up P2/P3 review residuals

P2:
- WASM dual-ownership invariant documented on ASTCache dispose:
  a Tree must live in AT MOST ONE disposing ASTCache. Native
  tree-sitter today is unaffected; WASM adoption would require
  tree.copy() or a non-disposing secondary cache.
- mro-processor C3 ordering test: pins EXTENDS-before-IMPLEMENTS
  parent grouping for classes with interleaved edge additions.
  Asserts exact MRO ['Base', 'Iface'] — a revert to single-loop
  insertion-order iteration would produce ['Iface', 'Base'] and
  fail loudly.
- cached-tree parity test: emitPythonScopeCaptures(src, path, T)
  returns identical CaptureMatch[] to emitPythonScopeCaptures(src,
  path). Pins the cache-hit path's correctness so a regression
  that silently returns stale captures would break the test.

P3:
- Dev-mode cache counters moved from captures.ts to cache-stats.ts.
  Production hot-path module no longer carries the module-global
  export surface; PROF gating behavior preserved.
- ParseOutput field rename astCache → scopeTreeCache. Clarifies
  that the surfaced cache is the persistent cross-phase one, not
  the chunk-local astCache parse-impl clears between chunks.
  Single consumer (scopeResolutionPhase) updated; no other readers.
- ASTCacheReader interface extracted. scopeResolutionPhase now
  reads the phase dep via a shared type instead of a hand-rolled
  inline structural shape that could drift from ASTCache's contract.
- graph.ts dual-index invariant enforced through writeRel/deleteRel
  private helpers instead of duplicated add/delete at 3 mutation
  sites. Adding a new mutation method only needs to call the
  helpers — forgetting to update one index becomes structurally
  impossible.

Tests: 382/382 unit (incl. 2 new), 191/191 python integration both
flag paths. tsc clean.

* fix(ci): prettier formatting + Python-migration test adjustments

CI run 24666612657 failed on three jobs. Fixes:

quality/format:
- Prettier --check flagged 3 files after the accumulated branch work.
  Ran prettier --write from repo root (CI's invocation cwd) to apply:
  simple-hooks.ts, resolve-references.ts, python-hooks.test.ts.

tests/{ubuntu,macos,windows} — 9 assertion failures, all traceable to
Python landing in MIGRATED_LANGUAGES (default-on registry-primary):

  - registry-primary-flag.test.ts (3 tests): the 'returns false by
    default' / 'primaryLanguages empty' / 'Python mid-process
    mutation' assertions were written in Ring 2 when MIGRATED_LANGUAGES
    was empty. Rewrote to assert MIGRATED_LANGUAGES membership is the
    default, use Java (unmigrated) for the no-stale-cache test, and
    verify env overrides work in both directions (migrated-off,
    unmigrated-on).
  - call-processor.test.ts (6 tests in SM-10 + D2-widen blocks):
    these exercise the LEGACY call-resolution DAG on .py fixtures.
    processCalls now gates Python out (isRegistryPrimary === true by
    default), returning 0 edges. Added REGISTRY_PRIMARY_PYTHON=false
    override in the relevant beforeEach + restore in afterEach, so
    the legacy DAG runs for these test-local fixtures without
    affecting the production-default behavior.

Local verification: 4126/4126 unit tests pass, prettier clean.

* docs(python): known-limitation block on scope-resolution public API

Unit 10 — document what the Python registry-primary path intentionally
does not resolve, so reviewers and future maintainers can distinguish
conscious trade-offs from latent bugs:

- Dynamic attribute access (getattr / setattr)
- Dynamic imports (importlib, __import__)
- Metaclass-driven dispatch
- Union / Optional branch-picking behavior
- Arbitrary signature-rewriting decorators
- typing.TYPE_CHECKING-guarded imports
- *args / **kwargs type flow-through
- super() outside a directly-bound method

Each item names the file that owns the relevant hook so a future
follow-up knows where to start. Shadow-harness corpus parity + the
CI parity gate remain the authoritative signal for which of these
matter at fleet scale.

* docs: record scope-resolution pipeline alongside legacy call DAG

Capture what shipped in #980 so future readers don't have to reverse-
engineer the coexistence of the legacy call-resolution DAG and the new
scope-resolution pipeline:

- ARCHITECTURE.md: new 'Scope-Resolution Pipeline' section after the
  Call-Resolution DAG, documenting pipeline stages, ScopeResolver
  contract, per-language registration, code references, and perf
  notes. Coexistence block added to the legacy DAG section explaining
  how MIGRATED_LANGUAGES gates the two paths per-language.
- AGENTS.md: reference-docs pointer updated — legacy-DAG one-liner
  stays; scope-resolution pipeline gets its own pointer so agents
  know when to read which section. Changelog bumped.
- type-resolution-system.md: callout at the 'call-processor.ts is
  the consumer' claim pointing readers to the scope-resolution path
  for migrated languages. TypeEnv is still built per file, but for
  migrated languages receiver typing flows through ParsedTypeBinding
  rather than call-processor.ts.

CHANGELOG.md intentionally not touched — owned by the release process.

* chore: remove obsolete scheduled_tasks.lock file

* fix(scope-resolution): qualified-name keys for same-file method collisions

Review feedback from PR #980 reviewer flagged a BLOCKING correctness
bug: when two classes in the same file define a method with the same
simple name (e.g. class User: def save + class Document: def save),
every d.save() CALLS edge silently resolved to User.save because the
graph node lookup keyed only by (filePath, simpleName) and first-wins
took User's method.

Three-layer fix:

1. populateClassOwnedMembers now promotes a nested def's
   qualifiedName from `save` to `ClassName.save` when the def sits
   inside a class scope. Python's scopes.scm doesn't emit
   @declaration.qualified_name for methods, so without this the
   finalized SymbolDefinition carried only the simple name.
2. buildGraphNodeLookup adds a second key per node:
   (filePath, qualifiedName). For Method/Function nodes the qualifier
   is parsed deterministically out of the node id
   (`Method:file.py:User.save#N` → `User.save`), which is robust to
   Windows-style filePath colons. Simple-name key retained as a
   fallback for callers that don't know the qualifier.
3. resolveDefGraphId now tries the qualified key first, then falls
   back to the simple-name lookup.

Also addresses the non-blocking review items:

- scopeResolutionPhase.deps now includes `crossFile` so the Kahn's
  runner can't schedule scope-resolution before crossFile finishes
  writing heritage edges that buildMro consumes.
- run.ts no longer mutates the finalized ScopeResolutionIndexes via
  `as` cast — spreads into a fresh object with the populated
  methodDispatch field instead.
- Doc nits: scope-resolver.ts registry path + phase.ts Ring number.

Test coverage:
- New fixture test/fixtures/lang-resolution/python-same-file-method-collision
  with User.save + Document.save in one file and app.py calling both
  through typed receivers.
- Three new integration assertions pin that u.save() and d.save()
  target the correct qualified node id. Fail before the fix, pass
  after. Confirmed by running once without populateClassOwnedMembers
  qualifier promotion — reproduces the original User.save-for-both bug.

Verification: 194/194 test/integration/resolvers/python.test.ts pass
both REGISTRY_PRIMARY_PYTHON=0 and =1. 523/523 related unit tests.
tsc --noEmit clean.

* fix(scope-resolution): filter export index to module-level defs + label-prefixed qualified key

Codex adversarial review on PR #980 flagged that
buildWorkspaceResolutionIndex feeds defsByFileAndName and
callablesBySimpleName from parsed.localDefs — the flat set of every
def in the file including methods, fields, and nested functions.
findExportedDef / findExportedDefByName treat those maps as
file-level exports, so `mod.save()` could silently bind to User.save
whenever a method's simple name appeared first in parse order.

Plan: docs/plans/2026-04-21-001-fix-workspace-index-module-scope-only-plan.md

Fix layers:

1. workspace-index.ts: split the single parsed.localDefs loop into
   two passes:
   - Module-export pass: iterate moduleScope.ownedDefs PLUS ownedDefs
     of every child scope whose parent is the module scope. Top-level
     class and function declarations each live in their own scope
     with parent=module, not in moduleScope.ownedDefs directly, so
     the "parent === moduleScope.id" walk is required to reach them.
     Methods (scope.parent === Class scope) and nested functions
     (scope.parent === another Function scope) are excluded.
   - Member-by-owner pass: keeps iterating parsed.localDefs since
     that map is keyed on ownerId and correctly saw class-owned defs
     before this change.

2. graph-bridge/node-lookup.ts: qualified keys now live in a separate
   keyspace (`<q>:filePath::<label>::<qualifiedName>`) and include
   the node label. Without the label prefix, a top-level `def save`
   (Function, qualifier `save`) would collide with a class method
   `User.save` (Method, simple name `save`) in the same simple-key
   slot because the Function's qualifier happens to equal the
   Method's simple name. The label differentiates them.

3. graph-bridge/ids.ts: resolveDefGraphId uses the new
   type-prefixed qualified key when def.type is set. Simple-name
   fallback retained for languages that don't yet synthesize
   qualifiers on their defs.

Test fixture: python-module-export-vs-method-collision places
`class User: def save` BEFORE top-level `def save` — parse order
that exposes the bug (class method enters the index first). Three
new integration assertions:
  - `mod.save(x)` resolves to the module-level Function, not User.save
  - `u.save()` resolves to User.save Method
  - Exactly two CALLS edges to `save` exist, one per intended target

Fixture confirmed failing before the workspace-index fix (bug
reproduced), passing after.

Verification: 197/197 test/integration/resolvers/python.test.ts pass
both REGISTRY_PRIMARY_PYTHON=0 and =1. 523/523 related unit tests.
tsc --noEmit clean.

* fix(scope-resolution): drive module export index from moduleScope.bindings

Codex round-2 adversarial review flagged that the workspace-index
module-export pass iterated every def in every direct-child scope of
the module, including class-body Variable defs like
`class User: MAX_USERS = 100`. `defsByFileAndName[file][MAX_USERS]`
silently aliased to the class attribute. Latent today because Python
doesn't emit ACCESSES edges for `mod.NAME` member access, but the
index-layer leak would surface the moment reference capture widens.

Plan: docs/plans/2026-04-21-002-fix-codex-round2-scope-resolution-plan.md

Drive the module-export index from the extractor invariant instead of
a scope-kind → allowed-label switch:

moduleScope.bindings already contains exactly the names visible at
module level — top-level class/function declarations, module-level
variable assignments, imports. Class methods, class-body attributes,
and nested-function defs bind to their containing (Class or Function)
scope, not the module, so they're naturally excluded.

Filter to `BindingRef.origin === 'local'` so imports and wildcard
re-exports stay out of the index (matches the pre-fix invariant when
the source was `parsed.localDefs`).

No per-kind predicates, no scope-kind / def-kind enumeration, no
two-pass merge between moduleScope.ownedDefs and direct-child scope
walks — one loop, language-agnostic.

Codex also flagged `propagateImportedReturnTypes` as potentially
broken for function-local imports, but scope-dump probing showed the
finalize algorithm puts `from svc import get_user` into the MODULE
scope's finalized bindings even when declared inside a function, so
the existing module-scope propagation already handles the case. The
new python-function-local-import-chain integration test pins that
working behavior as a regression guard; no code change required.

Coverage:
- test/unit/scope-resolution/workspace-index.test.ts (new, 5 tests) —
  directly asserts the index shape. The "excludes class-body Variable
  defs" test fails without this fix and passes after (confirmed via
  stash-pop probe).
- test/integration/resolvers/python.test.ts — 4 new integration
  assertions across two describe blocks (python-class-attr-export-leak,
  python-function-local-import-chain) pin end-to-end invariants.
- Two new fixtures under test/fixtures/lang-resolution/.

Verification: 201/201 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. 528/528 related unit tests (was
523). tsc clean.

* test(scope-resolution): pin local-namespace-import behavior + document empirical finalize hoisting

Codex round-3 adversarial review raised three concerns about
scope-resolution passes assuming module-scope semantics that would
contradict `pythonImportOwningScope`'s documented per-scope contract.
Empirical verification via scope-dump probes resolved each:

Plan: docs/plans/2026-04-21-003-fix-codex-round3-scope-aware-resolution-plan.md

1. Function- and class-local namespace imports: VERIFIED WORKING.
   `def outer(): import svc as s; s.call()` and `class A: import mod;
   def use(self): mod.helper()` both emit CALLS edges with reason
   "scope-resolution: namespace-receiver". finalize-algorithm hoists
   the ImportEdges onto `indexes.imports[moduleScope]` regardless of
   where the `import` statement appears, so collectNamespaceTargets'
   module-scope read finds them.

2. Imported return-type propagation module-scope-only: VERIFIED
   WORKING (already pinned in round 2). `from svc import get_user`
   inside a function body lands in indexes.bindings[moduleScope], so
   propagateImportedReturnTypes' module-scope read still finds it.

3. Nested method-local defs stamped as class members: VERIFIED FALSE.
   The scope extractor creates nested Function scopes for inner
   `def`s; `def helper` inside `def save` inside `class User` lives
   in helper's own Function scope whose parent is save's Function
   scope (NOT the Class scope). populateClassOwnedMembers'
   `parentScope.kind === 'Class'` branch correctly skips it;
   helper.ownerId stays undefined.

Instead of implementing speculative scope-aware refactors that the
tests would pass regardless, this commit:

- Adds regression fixtures and integration assertions that pin each
  working behavior. If finalize routing ever changes to honor the
  hook's per-scope contract, these assertions flip red and signal the
  need for the scope-chain-aware refactor.
- Adds defensive JSDoc to the three flagged call sites
  (collectNamespaceTargets, propagateImportedReturnTypes,
  populateClassOwnedMembers) documenting the empirical invariant so
  future reviewers don't re-derive Codex's theoretical concern
  without the benefit of the probe.

Files:
- Two new fixtures under test/fixtures/lang-resolution/ covering the
  function-local and class-body namespace-import patterns.
- Two new describe blocks in test/integration/resolvers/python.test.ts
  (3 assertions, positive-pin intent).
- Defensive comments in namespace-targets.ts, imported-return-types.ts,
  and scope-resolution/scope/walkers.ts.

Verification: 204/204 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. tsc clean.

* perf(graph): reverse-adjacency + file indexes drop removeNode/removeNodesByFile from O(N)

PR #980 in-line review flagged that `removeNode` iterated the full
relationshipMap to find edges touching a node (O(E)), and
`removeNodesByFile` called removeNode for every matching node after
a full nodeMap scan (O(N × E)). Pre-existing, but worth fixing
properly since the writeRel/deleteRel helpers we just added make the
index-maintenance story coherent.

Two new indexes maintained on every mutation path:

- `edgeIdsByNode: Map<nodeId, Set<relId>>` — reverse adjacency. Every
  edge records both endpoints, so removeNode iterates
  edgeIdsByNode.get(id) instead of every relationship. Self-edges
  skip the duplicate-endpoint write to keep the Set dedup explicit.
- `nodeIdsByFile: Map<filePath, Set<nodeId>>` — file index.
  removeNodesByFile reaches its file's nodes directly.

Complexity:
- removeNode: O(edges-touching-node), was O(total-edges).
- removeNodesByFile: O(file-nodes × avg-edges-per-node + scan of the
  file bucket), was O(total-nodes + file-nodes × total-edges).

Index maintenance is centralized in writeRel/deleteRel + new
addToBucket/removeFromBucket helpers. Empty buckets are pruned to
keep the indexes compact. Existing dual-invariant (relationshipMap ↔
relationshipsByType) preserved.

Nodes without a `filePath` property (e.g. Community/Cluster nodes)
are intentionally NOT indexed in nodeIdsByFile — they can't belong
to any file, so removeNodesByFile correctly leaves them alone.

Coverage: 7 new unit tests (33/33 total, was 26). Added cases:
- removes only edges touching the removed node
- handles self-edges
- removes orphan node with no edges
- removeNodesByFile removes only matching nodes
- returns 0 when no match
- also removes edges whose endpoints lived on the removed file
- does not index nodes without a filePath property

Verification: 204/204 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. 4235/4235 unit tests. tsc clean.

* refactor(ingestion): merge python/ast-utils into utils/ast-helpers; iterative findNodeAtRange

python/ast-utils.ts held three language-agnostic helpers
(nodeToCapture, syntheticCapture, findNodeAtRange) plus two
duplicates of the shared utils version (findChildOfType ==
findChild; findIdentifierChild was unused). Consolidating into
utils/ast-helpers.ts so the next language migrating to the
scope-resolution pipeline imports from one place.

findNodeAtRange rewritten iteratively using an explicit stack.
Previous implementation was recursive — fine for shallow Python
trees today, but a landmine for languages with deeper nesting
(Kotlin sealed-hierarchy decomposition, Rust macro expansion,
etc.) and the task hooks explicitly call out "no recursion".
Children are pushed reverse-index so LIFO pop visits them
left-to-right; row-bound pruning preserves the prior early-skip
optimization (the `break` shortcut is replaced with `continue`
since a stack can't leverage ordered sibling termination).

findChildOfType consumers migrated to the existing findChild
helper. findIdentifierChild deleted — no callers remained.

Coverage: 204/204 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. 339/339 scope-resolution +
graph unit tests. tsc clean.

* refactor(scope-resolution): remove unused shouldShadow / shouldCreateScope hooks

Both LanguageProvider hooks were dead weight:

- `shouldShadow` had zero call sites — the interface declared it,
  Python implemented a trivial always-true no-op, but no consumer
  ever read it. The shadowing decision lives in pythonMergeBindings
  and the central merge algorithm, not in a per-scope predicate.
- `shouldCreateScope` had one call site in pass1BuildScopes but the
  only language implementing it (Python) always returned true. No
  producer ever emits a `@scope.block` for Python, so the hook's
  "declines to create" branch was unreachable. Other languages
  didn't implement it at all.

Removing both:

- Drops the interface declarations in language-provider.ts.
- Drops `shouldCreateScope` from ScopeExtractorHooks Pick and from
  the pass1BuildScopes conditional — the stack-based parent-resolve
  loop becomes unconditional.
- Drops pythonShouldShadow / pythonShouldCreateScope from simple-hooks,
  the Python index barrel, and the python.ts provider wiring.
- Drops the tests that exercised the removed hooks: one block-
  suppression scenario in scope-extractor.test.ts, one shouldCreateScope
  test in parse-worker-scope-integration.test.ts, and the
  pythonShouldShadow / pythonShouldCreateScope always-true assertions
  in python-hooks.test.ts. pythonBindingScopeFor's delegate-to-default
  test is preserved in its own describe block.

Shadowing itself is unchanged: pythonMergeBindings still runs, LEGB
ordering still applies, wildcard transparency is still handled via
the merge precedence rules. The hook API just no longer has a
vestigial per-scope toggle we decided not to use.

Verification: 204/204 test/integration/resolvers/python.test.ts both
REGISTRY_PRIMARY_PYTHON=0 and =1. 335/335 scope-resolution + graph
unit tests (was 339, net -4 after removing the hook-specific
assertions). tsc clean.

* refactor(scope-resolution): drop dead exports surfaced by knip

Knip flagged 44+ dead exports in the PR surface. Cleanup:

Barrel deletion:
- Remove src/core/ingestion/scope-resolution/index.ts entirely.
  It re-exported 30+ symbols but only one file
  (languages/python/scope-resolver.ts) imported from it, and only
  7 symbols. Matches the project's "no barrel re-exports" preference
  and removes a drift surface. scope-resolver.ts now imports from
  concrete files (passes/mro.ts, scope/walkers.ts, contract/...).

Dead functions/interfaces removed:
- resolvePythonScope + ResolvePythonScopeInput + ResolvePythonScopeStats
  in languages/python/scope-resolver.ts — never called. pipelinePhase
  reaches pythonScopeResolver via SCOPE_RESOLVERS, not via a
  per-language entry point.
- getScopeResolver in scope-resolution/pipeline/registry.ts — had zero
  callers. Consumers read SCOPE_RESOLVERS directly.

Exports demoted to module-internal (used only within their own file):
- PYTHON_SCOPE_QUERY (query.ts) + its re-export from python/index.ts
- PROF (cache-stats.ts)
- PythonArityMetadata (arity-metadata.ts)
- ReferenceSiteSkipSet (graph-bridge/references-to-edges.ts)
- ReceiverBoundProviderSubset (passes/receiver-bound-calls.ts)
- ResolveCompoundReceiverOptions interface (passes/compound-receiver.ts)
- matchingOpenParen function (passes/compound-receiver.ts)
- followChainPostFinalize function (passes/imported-return-types.ts)
- RunScopeResolutionInput + RunScopeResolutionStats (pipeline/run.ts)

Also removed:
- Redundant `export type { Scope }` re-export from contract/scope-resolver.ts
  (consumers import Scope directly from gitnexus-shared).

Verification: knip reports zero dead exports in PR-touched files.
204/204 test/integration/resolvers/python.test.ts both flag paths.
335/335 scope-resolution + graph unit tests. tsc clean.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-21 15:50:00 +01:00
azizur100389
bd271da7b7
feat(cli): gitnexus remove <target> to unindex a registered repo by name or path (#664) (#1003)
* feat(cli): gitnexus remove <target> to unindex a registered repo by name or path (#664)

Add a `remove` CLI command that deletes the `.gitnexus/` index AND
unregisters a repo from the global registry (~/.gitnexus/registry.json),
addressing the lifecycle gap flagged in #664: previously users had to
cd into the repo to run `clean`, and there was no path-based or
alias-based remove for an already-deleted working tree.

- New command `gitnexus remove <target> [-f|--force]`. `<target>` is
  alias / basename-derived name / remote-inferred name / absolute path.
- New helper `resolveRegistryEntry(entries, target)` in repo-manager.ts
  with path > name precedence; throws RegistryNotFoundError or
  RegistryAmbiguousTargetError (typed, `kind`-discriminated).
- Atomicity mirrors `clean`: fs.rm first, then unregisterRepo; partial
  failures self-heal on next `listRegisteredRepos({ validate: true })`.
- Idempotent on unknown targets (exit 0 with warning) per the #664
  spec: "behave atomically and idempotently so retries are safe".
- `--force` uses `clean`-style confirmation-skip semantics — distinct
  from `analyze --force` (pipeline re-index); here there is no pipeline
  so no conflation.
- 7 new unit tests cover resolver precedence, case sensitivity,
  ambiguity, and not-found hints; 2 integration tests cover the real
  CLI -> registry -> filesystem chain including the --allow-duplicate-name
  (#829) ambiguity case.

* fix(cli): canonicalize repo paths so remove/register match across platforms (#1003 review)

Address review feedback from @evander-wang and @magyargergo on PR #1003
plus the Windows + macOS CI failure (same root cause).

Problem:
- macOS: /var is a symlink to /private/var. `path.resolve` does NOT
  follow symlinks, so a child running analyze in /var/folders/X stores
  /private/var/folders/X (realpath from OS cwd) but an outer caller
  passing the symlink form misses.
- Windows: GitHub runners surface tmpdirs in 8.3 short-name form
  (RUNNERA~1) while process.cwd() returns the long form (runneradmin).
  Same divergence.

Fix: new `canonicalizePath(p)` helper wraps `path.resolve` plus
`fs.realpathSync.native`, falling back to `path.resolve` when the path
doesn't exist (preserves idempotent-on-missing semantics needed by
`remove <unknown>`). Applied at 3 call-sites — registerRepo,
unregisterRepo, resolveRegistryEntry — canonicalising BOTH the input
and each stored `entry.path` at compare time. That last bit is the
backward-compat story: registries written by older versions
(pre-canonicalisation) still match correctly, so we don't need a
migration script.

Test side: the ambiguous-target integration test now reads the path
from the registry snapshot rather than passing the outer `repoA`
variable directly, so it exercises the registry contract regardless of
which path form the platform stores. 4 new unit tests cover the helper
(idempotent, fallback-on-missing, absolute-for-relative) plus the
backward-compat resolver path.

* fix(cli): store resolved (non-canonical) path, compare via canonicalizePath (#1003 CI)

Follow-up to c5eceba0. The previous commit canonicalised the repo path
at BOTH write-time AND compare-time in registerRepo — that expanded
Windows 8.3 short names (RUNNER~1) to long names (runneradmin) when
storing `entry.path`. Pre-existing #829 unit tests that assert
`path.resolve(err.existingPath) === path.resolve(tmpPath)` then broke
because `tmpPath` is still short-form (path.resolve doesn't expand
8.3) while `entry.path` was long-form (canonicalizePath does).

Fix: split storage from comparison.
- entry.path stores `path.resolve(repoPath)` — whatever form the
  caller passed. `list` output and error messages show the path the
  user typed.
- All compare points (existing-entry lookup in registerRepo, the
  collision guard, unregisterRepo, resolveRegistryEntry path tier)
  canonicalise BOTH sides via `canonicalizePath`. That is where the
  /var ↔ /private/var and RUNNER~1 ↔ runneradmin divergence actually
  matters.

Net effect: storage is tolerant (preserves user input), matching is
strict (canonical-vs-canonical). Pre-existing #829 tests stay green
because `err.existingPath` is unchanged from what `path.resolve` gives
back; the cross-platform CI failure from #1003 stays fixed because
every comparison path goes through `canonicalizePath`.

* fix(cli): refuse destructive fs.rm when registry storagePath isn't <repo>/.gitnexus (#1003 review)

Address @magyargergo's inline review finding on remove.ts:89 and the
sibling vulnerability in clean.ts --all (caught during a pre-commit
safety audit). ~/.gitnexus/registry.json is a user-writable plain-text
file, so a corrupted or hand-edited entry could point storagePath at
the repo root (catastrophic: rm the working tree), an empty string
(→ cwd), a parent dir, or anywhere else. fs.rm(recursive: true,
force: true) on any of those is a runtime disaster.

- New UnsafeStoragePathError + exported assertSafeStoragePath() in
  repo-manager.ts. Pure lexical string check (Windows-case-
  insensitive) asserting entry.storagePath === path.join(entry.path,
  '.gitnexus').
- Guard wired into BOTH destructive registry-trusting sites:
  - remove.ts: exit 1 with actionable hint
  - clean.ts --all: skip the poisoned entry with a warning and
    continue (preserves existing per-repo error tolerance — one bad
    entry doesn't halt the batch)
- clean.ts default path and server/api.ts are safe-by-construction
  (they recompute storagePath from findRepo / getStoragePath rather
  than trusting the registry field).
- 8 unit tests cover the guard (valid, repo-root, parent, empty,
  unrelated, sibling, error payload, Windows case).
- 2 integration tests prove the full CLI path: remove-poisoned exits
  1 without touching the working tree; clean --all with a poisoned
  sibling entry cleans the good entry, skips the bad one, and leaves
  the poisoned repo intact.

* test(cli): assert full remove dry-run + success output shape (#1003 NIT)

Address the one NIT from the senior-reviewer pass on PR #1003: the
integration test was only checking for the "Run with --force" hint in
dry-run output, not verifying that the three actual console.log lines
(alias, repo path, storage path) appear. Same weak check on the
success-branch "Removed" output.

Tighten both assertions to toContain(alias), toContain(entry.path),
toContain(storagePath). Catches silent format regressions — e.g. a
future refactor that drops a console.log line or swaps
entry.name/entry.path in the output.

No code change; +20 test lines. All assertions in the happy-path
integration test now fire for a meaningful reason.
2026-04-21 11:52:59 +01:00
ivkond
00966630c4
feat: cross-repo impact analysis (#794) — @repo MCP routing + group resources (#984) 2026-04-20 11:55:07 +01:00
azizur100389
dae7bd3b3f
feat(cli): analyze --name <alias> + duplicate-name guard for the repo registry (#955) 2026-04-19 07:23:48 +01:00
azizur100389
ac148612ab
feat(search): per-phase timing instrumentation for the query pipeline (#953)
* feat(search): per-phase timing instrumentation for the query pipeline

The eval harness already measures search-pipeline latency per phase,
but the *product* query() tool has no timing visibility. That leaves
production latency opaque:

 - Is BM25 the tail, or vector search?
 - How much Promise.all overlap do concurrent searches actually save?
 - Does symbol_lookup dominate when per-symbol Cypher round-trips pile up?

None of this is answerable from the outside, which blocks the
latency-quality Pareto work tracked in #546 / #553.

Changes:

* New PhaseTimer class at src/core/search/phase-timer.ts.
  Supports three APIs:
    - start(phase) / stop() for sequential phases (per issue spec)
    - mark(phase, durationMs) for pre-measured durations
    - time(phase, promise) to wrap a promise inside Promise.all

  The issue's original spec was sequential-only, which doesn't work
  for BM25 + vector inside Promise.all — the second start() would
  auto-stop the first and only one phase would get timed. The mark()
  and time() variants resolve that without changing the sequential
  API for the other phases.

* local-backend.ts query() instrumented across seven phase markers:
    bm25, vector   (concurrent via timer.time inside Promise.all)
    merge          (RRF reciprocal-rank-fusion)
    symbol_lookup  (per-symbol process + cohesion + content Cypher)
    ranking        (in-memory priority sort)
    formatting     (response object construction + dedup)
    wall           (end-to-end; separate mark so callers can compare
                   sum(phases) vs wall and see Promise.all savings)

* logQueryTiming() helper next to logQueryError(), same console-based
  pattern (repo has no structured logger). Emits
    GitNexus [query:timing] query="..." totalMs=N phases={...}
  to stdout — greppable prefix, JSON-parseable payload, no new deps.

* timing: Record<string, number> added as a top-level field on the
  query() response. Strict superset of the previous shape — existing
  tests only assert field presence, so no regression. Other MCP tools
  use the same top-level-metadata convention (status, row_count,
  warning) rather than a nested _meta wrapper.

Tests:

 - 6 new unit tests for PhaseTimer covering start/stop, implicit
   stop-on-start, additive mark(), Promise.all-safe time(),
   negative/NaN rejection, and totalMs auto-stop.
 - 3 new assertions on the existing query integration test verifying
   timing.wall is a non-negative number and at least one of
   bm25/vector fired.

Verification:
  npx vitest run test/unit/phase-timer.test.ts       -> 6 pass
  npx vitest run test/unit/calltool-dispatch.test.ts -> 65 pass
  npx vitest run test/integration/local-backend-calltool.test.ts -> 18 pass
  npm run test:unit                                   -> 3777 pass
    (4 pre-existing env failures unchanged: skip-git-cli needs
     built dist/, git-utils tmpdir on Windows worktree)
  npx tsc --noEmit                                    -> clean

Scope declined for v1:

 - In-process histogram aggregation — the log line is enough for
   external tooling
 - Pareto curve generation — issue asks to enable it, not generate it
 - Sub-phases of symbol_lookup (process vs cohesion vs content) —
   issue lists them under one bucket; can split later if demand surfaces

Closes #553

* fix(search): route query:timing log to stderr to preserve stdio MCP contract

CI (#953) failed the `query: JSON appears on stdout, not stderr`
e2e test in test/integration/cli-e2e.test.ts with:

  SyntaxError: Unexpected token 'G', "GitNexus [..." is not valid JSON

Root cause: my initial logQueryTiming() in 63fbdc4 used console.log,
which writes to stdout. The MCP stdio transport uses stdout
exclusively for JSON-RPC responses (#324), and the CLI e2e test
guards that contract by asserting stdout parses as JSON on every
tool invocation. The "GitNexus [query:timing] ..." line was
interleaving with the response JSON and breaking the parse.

Fix: route logQueryTiming through console.error instead. stderr is
the correct channel for human-readable diagnostics and it is what
the sibling logQueryError already uses for the same reason. The log
line format is otherwise unchanged -- still greppable, still
JSON-parseable payload.

Verification (local, with dist built):
  npx vitest run test/integration/cli-e2e.test.ts -t "query: JSON"
    -> now passes (was failing across ubuntu/windows/macos in CI)
  npx tsc --noEmit                                  -> clean
  Two unrelated pre-existing failures on non-git
  directory handling remain (same on upstream/main).

Closes the CI regression introduced in 63fbdc4.
2026-04-18 16:30:07 +01:00
Gergő Magyar
d9da7d6692
fix(test): isolate cli-e2e from shared mini-repo fixture (#954)
Deterministic fix for the Windows-flaky pipeline-graph-golden test.

Root cause
  cli-e2e.test.ts wrote into the SHARED fixture directory
  (test/fixtures/mini-repo/) — git init, analyze run that creates
  AGENTS.md, CLAUDE.md, .claude/, .gitnexus/. When pipeline-graph-golden
  ran in parallel, its `cpSync` of the source directory could capture
  the mid-flight pollution before cli-e2e's afterAll cleanup fired.
  macOS/Ubuntu won the race often enough that the flake presented as
  Windows-only.

Fix
  cli-e2e now copies mini-repo into a fresh `mkdtemp`'d parent whose
  basename is `mini-repo` (preserving `--repo mini-repo` CLI lookup by
  basename), runs git-init there, and rm's the whole tmpdir in afterAll.
  The shared fixture source is never touched.

  Fallout from the cwd change: bare `--import tsx` specifiers (2
  spawnSync + 1 spawn) can't resolve `tsx` from an os.tmpdir cwd where
  there is no node_modules. Switched them to the already-existing
  `tsxImportUrl` (absolute file:// URL to the tsx loader), matching
  the `runCliOutsideProject` pattern that was already set up for this
  exact case.

  Updated the "MINI_REPO is inside the project tree" comment in the
  `status on non-indexed repo` test — MINI_REPO is now in os.tmpdir,
  so the rationale for using a separate throwaway tmp git repo is
  different (but still valid: previous tests in the suite create
  MINI_REPO/.gitnexus, which findRepo() would pick up).

  Also updated pipeline-graph-golden's comment explaining WHY it
  copies to tmp — it's now defense-in-depth rather than a necessity,
  so a future test that adds files to the source can't silently
  regress the golden.

Verification
  - 5x consecutive `cli-e2e + pipeline-graph-golden` runs: 20/20 pass
    (deterministic)
  - 3x full suite including pipeline.test: 27/27 pass
  - test/fixtures/mini-repo/ post-run contents: only `src/` —
    zero pollution from any test
  - macOS/Ubuntu behavior unchanged (they were passing; tmpdir
    isolation is purely additive)
2026-04-18 12:54:59 +01:00
azizur100389
131d411ae4
feat(mcp): rank context/impact disambiguation candidates and expose kind/file_path hints (#888)
* feat(mcp): rank context/impact disambiguation candidates and expose kind/file_path hints

The `context` MCP tool already returned `{ status: 'ambiguous', candidates }`
when a name hit multiple symbols, but the candidates were returned in
arbitrary DB order and the only hint it accepted was file_path. The
`impact` tool was worse: when its name resolver found multiple viable
matches it silently picked the first one from a priority UNION, with no
signal back to the caller that a different symbol might have been
intended.

Both failure modes were flagged in issue #470 and reconfirmed in the
comments by a second user who described impact as returning "incorrect
parsing results and meaningless tool calls" in the multi-match case.

Changes:

* Add `resolveSymbolCandidates(repo, query, hints)` private helper on
  LocalBackend. Single place that:
   - Short-circuits on direct uid (zero-ambiguity)
   - Runs the same name-or-qualified-id match as before, with LIMIT 20
     (was 10) so the ranker has headroom instead of arbitrary truncation
   - Preserves the #480 Class/Constructor preference -- when the only
     ambiguity is a Class and its own Constructor, the Class wins
     silently
   - Scores each candidate (pure TS, no extra DB round-trip): base 0.50,
     +0.40 for file_path match, +0.20 for kind match, plus a small
     kind-priority tiebreaker (Class > Interface > Function > Method >
     Constructor) when no explicit kind hint is given
   - Sorts desc by score with stable tiebreakers (shorter filePath,
     then lex uid)
   - Promotes to a single confident resolve when the top score is
     >= 0.95 AND beats the runner-up by >= 0.10 -- lets a strong hint
     cut through without forcing the caller through a disambiguation
     round-trip

* Rewire `context()` to use the shared helper. Response shape is a
  strict superset of today's: candidates gain a `score` field, the
  existing `{ uid, name, kind, filePath, line }` keys are preserved so
  every downstream consumer (rename, eval-server formatter, etc.) keeps
  working. New `kind` input hint accepted.

* Rewire `impact()` to use the shared helper. Now emits the same
  `{ status: 'ambiguous', candidates, impactedCount: 0, risk: 'UNKNOWN' }`
  shape instead of silent first-pick. New inputs accepted:
  `target_uid`, `file_path`, `kind`.

* Update tool schemas in mcp/tools.ts to advertise the new inputs and
  describe ranked disambiguation.

Backward compatibility:

The #480 Class/Constructor collapse is preserved and covered by the
existing java-class-impact integration test (still green). The
ambiguous response shape is a strict superset -- `eval-formatters`
unit test that parses the old shape is unchanged and still passes.
`impact` going from silent-first-pick to structured ambiguous is a
semantic improvement that is the entire point of the issue; callers
relying on silent first-pick now get an actionable response.

Scope declined for v1:

module/community hint -- the issue lists it as one of several hints,
but kind + file_path cover the vast majority of disambiguation needs
in practice, and a community-label filter requires an extra graph
query per candidate. Natural v2 follow-up.

Tests: calltool-dispatch.test.ts gains 5 new cases covering file_path
boost, kind hint boost, impact ambiguous shape, impact target_uid
short-circuit, and score field presence on the existing ambiguous
test. Plus the extended assertions on the existing
`context tool returns disambiguation for multiple matches`.

Verification:
  npx vitest run test/unit/calltool-dispatch.test.ts       -> 64 pass
  npx vitest run test/integration/java-class-impact.test.ts -> pass
  npm run test:unit                                         -> 3642 pass
    (4 pre-existing env failures unchanged: skip-git-cli needs built
    dist/, git-utils tmpdir on Windows worktree -- same on main)
  npx tsc --noEmit                                          -> clean

Closes #470

* fix(mcp): enrich labels from UNION when labels(n)[0] is empty; address review findings

CI on PR #888 caught 13 integration-test failures I did not cover locally:
my resolver refactor collected candidates via `labels(n)[0] AS type`, but
LadybugDB returns an empty string for that projection on certain node
types (most importantly Class). With an empty `type`, impact's downstream
`_runImpactBFS` no longer recognised `symType === 'Class' | 'Interface'`
and stopped seeding Constructor + File nodes into the frontier, so the
"impact(upstream) surfaces the file importer" assertion broke across 11
language fixtures plus 2 OVERRIDES filter tests.

The original impact resolver worked around this by running a prioritised
UNION across Class/Interface/Function/Method/Constructor and picking the
first hit. My refactor dropped that. Fix: keep the simple candidate MATCH
but enrich types afterward via a single scoped UNION query, so every
candidate carries an accurate label for both scoring and downstream
BFS seeding. The UID direct-lookup path is patched the same way.

Also addresses the findings from the senior reviewer on PR #888:

* MIGRATION.md: document the `impact` behavioural change (silent first-
  pick → structured `{ status: 'ambiguous', candidates }`) so downstream
  callers know to branch on `result.status` before reading byDepth/
  summary. `context` is unchanged shape-wise (strict superset).

* New test: `context tool promotes top candidate via scoring when
  multiple rows survive DB pre-filter`. The review flagged that the
  existing file_path test works only because the mock ignores WHERE
  parameters -- the scored-promotion path (top ≥ 0.95 AND gap > 0.09)
  wasn't directly exercised. The new test uses two candidates both in
  App.tsx-containing paths plus a kind hint so promotion is decided by
  scoring, not DB pre-filtering. Also tightened the comment on the
  earlier file_path test to describe the mock vs production divergence
  honestly.

* NIT: added a paragraph explaining why `scored.length >= 2` is kept as
  a defensive guard even though the `normalized.length === 1` early
  return already covers the single-candidate path.

* Integration: two tests in `local-backend-calltool.test.ts` targeted
  `'authenticate'`, which now correctly resolves as ambiguous (two
  Method nodes: AuthService.authenticate and BaseService.authenticate).
  Updated both to pass `file_path: 'src/auth.ts'` so they exercise the
  new disambiguation API and still assert the METHOD_OVERRIDES filtering
  they were originally about.

Edge case fix in the promotion gap check: IEEE754 makes 0.50 + 0.40 +
0.20 - 0.90 = 0.09999999999999998 instead of exactly 0.10, which would
otherwise break the "winner clearly dominates" intent for legitimate
1.00 vs 0.90 cases. Changed `>= 0.10` to `> 0.09`; same user-facing
intent, no floating-point sensitivity.

Verification (all from gitnexus/):
  npx vitest run test/integration/class-impact-all-languages.test.ts
    -> 52 pass (was 11 FAIL on CI before this fix)
  npx vitest run test/integration/local-backend-calltool.test.ts
    -> 18 pass (was 2 FAIL on CI before this fix)
  npx vitest run test/integration/java-class-impact.test.ts
    -> 10 pass (regression guard for #480 preserved)
  npx vitest run test/unit/calltool-dispatch.test.ts
    -> 65 pass (1 new test + 4 from original #470 PR)
  npm run test:unit
    -> 3626 pass, 4 pre-existing env failures unchanged
  npx tsc --noEmit
    -> clean
2026-04-18 12:52:42 +01:00
Copilot
dfa449ef41
feat(ingestion): language-agnostic heritage extractor with config+factory pattern (#890) 2026-04-17 17:51:17 +01:00
Yacine Hmito
daca8360bf
fix(python): avoid local matches for external dotted imports (#899) 2026-04-17 11:35:59 +01:00
Ryanba
77a13113ea
fix: keep worker warnings non-terminal (#261) 2026-04-17 06:46:31 +01:00
Copilot
ed5a4220dd
feat(ingestion): language-agnostic variable extractor with config+factory pattern (#878)
* Initial plan

* feat(ingestion): add variable extraction types, factory, configs, and wire into language providers

- Create variable-types.ts with VariableInfo, VariableExtractionConfig, VariableExtractor interfaces
- Create variable-extractors/generic.ts with createVariableExtractor() factory
- Add variableExtractor field to LanguageProvider interface
- Create per-language variable extraction configs for all 16 languages
- Wire variableExtractor into all language providers
- Add variable metadata enrichment to parse-worker for Const/Static/Variable labels

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3cb85c68-1792-473e-9a46-ea2588da0e5e

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat(ingestion): add variable extraction tests and fix Python/TS config issues

- Create test/unit/variable-extraction.test.ts with 29 tests covering
  TypeScript, JavaScript, Python, Go, Rust, C, C++, Ruby, and factory behavior
- Fix isConst in generic factory to use config.isConst over node-type membership
  (TS let/const both use lexical_declaration)
- Fix Python type extraction for annotated assignments at module scope
- Fix Python dunder name visibility (e.g., __name__ is public, not protected)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3cb85c68-1792-473e-9a46-ea2588da0e5e

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address code review feedback — move imports, clarify scope comment, use shared test context

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3cb85c68-1792-473e-9a46-ea2588da0e5e

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address review comments, fix prettier formatting and lint errors

- Fix prettier formatting in 5 files (c-cpp, jvm, swift configs, test file)
- Remove unused SyntaxNode imports in php.ts and ruby.ts (lint errors)
- Remove unused constNodeSet/variableNodeSet variables in generic.ts (warnings)
- Remove semantically wrong `methodProps.isReadonly = varInfo.isConst` (review)
- Remove dead `nodeLabel === 'Variable'` guard in parse-worker (review)
- Fix test guard: replace `if (declNode)` with `expect(declNode).toBeDefined()` (review)
- Add comment about Python expression_statement broadness (review)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/040edbbf-65b5-40e1-80c8-e98f7c4bb54a

* feat(ingestion): add block-scoped variable extraction via tree-sitter queries

Add @definition.const and @definition.variable tree-sitter query patterns
for TypeScript, JavaScript, Python, Go, Java, C, C++, C#, PHP, Ruby, and
Dart. Add parse-worker dedup logic to avoid duplicate nodes when variable
captures overlap with existing function/property captures. Add 'Variable'
label support in getLabelFromCaptures and DEFINITION_CAPTURE_KEYS.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9fa828c1-87b7-4482-8f26-d2079fb4c58a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: add block-scoped variable extraction tests and query capture tests

Add 6 tests for block-scoped variable extraction (TypeScript, Go, Rust, C,
Python). Add 14 tests verifying @definition.const/@definition.variable
query patterns exist in all language query strings. Import RUBY_QUERIES
in test file.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9fa828c1-87b7-4482-8f26-d2079fb4c58a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: add Python non-assignment expression statement rejection test

Addresses code review feedback: verify that the Python variable extractor
returns null for expression_statement nodes that contain function calls
rather than assignments (e.g. `print("hello")`).

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9fa828c1-87b7-4482-8f26-d2079fb4c58a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: Dart query node type, add Variable schema, update schema counts

- Change `top_level_variable_declaration` → `declaration` in DART_QUERIES
  (the former doesn't exist in tree-sitter-dart grammar, causing all
  Dart integration tests to fail with TSQueryErrorNodeType)
- Add VARIABLE_SCHEMA to schema.ts and register in initLbug() so that
  Variable-labeled nodes are persisted to LadybugDB (not silently dropped)
- Add 'Variable' to MULTI_LANG_TYPES in csv-generator.ts
- Update Dart variable config to remove invalid node type
- Update schema test counts (30→31 node schemas, 32→33 total)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/f79931d1-207f-4fbb-91da-259d44f7fd88

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address code review comment improvements

- Clarify processedDefinitionNodes tracks start indices, not nodes
- Improve Python variableNodeTypes comment wording

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/f79931d1-207f-4fbb-91da-259d44f7fd88

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: add Variable to NODE_TABLES, RELATION_SCHEMA, update golden snapshot

- Add 'Variable' to NODE_TABLES in gitnexus-shared so validTables.has('Variable')
  returns true and Variable graph edges are not silently dropped
- Add FROM File TO Variable, FROM Variable TO Community, FROM Variable TO Process
  to RELATION_SCHEMA so KuzuDB can represent edges connecting Variable nodes
- Update schema.test.ts: add Variable to multiLang list, fix count 30→31
- Regenerate pipeline-graph-golden snapshot for mini-repo fixture

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e3aad558-e7bb-40d1-b53f-0a2c0132ca96

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: isolate golden test from cli-e2e fixture pollution

The pipeline-graph-golden test was non-deterministic because cli-e2e.test.ts
creates AGENTS.md, CLAUDE.md, .claude/skills/, and .gitignore in the shared
mini-repo fixture during analyze. These leftover files caused the golden test
to find 9 files instead of 7 when tests ran in parallel.

Fixes:
- Golden test now copies the fixture to a temp dir before running, making it
  immune to concurrent test pollution
- cli-e2e afterAll cleanup now removes ALL generated files (AGENTS.md,
  CLAUDE.md, .claude/, .gitignore) not just .git/ and .gitnexus/
- Golden snapshot regenerated from clean 7-file fixture

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bd378e73-6f37-49c6-aed6-7fabf4dc6183

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-16 13:57:25 +01:00
Filipe Oliveira (Redis)
9ad1984b17
fix: resolve C/C++ cross-file calls through transitive #include chains (#816)
* fix: resolve C/C++ cross-file calls through transitive #include chains

In C/C++, #include is transitive: if a.c includes b.h and b.h includes
c.h, then a.c can call any function declared in c.h. The wildcard import
synthesis only walked direct imports (1 hop), missing symbols reachable
through transitive header chains.

This is the dominant pattern in large C codebases — Redis's db.c includes
server.h which includes dict.h, so db.c should resolve calls to dictFind()
declared in dict.h and defined in dict.c. Before this fix, those cross-file
call edges were missing entirely.

The fix expands the import closure transitively for C/C++ files before
synthesizing wildcard bindings. A BFS walks ctx.importMap and graphImports
to collect all transitively reachable headers, then passes the full closure
to synthesizeForFile.

Tested on Redis (github.com/redis/redis):
- Before: dictFetchValue had 0 cross-file callers, processCommand had 0
- After: dictFetchValue has 9 callers, processCommand has 1, +1946 edges total

Fixes #813

* refactor(ingestion): dispatch wildcard synthesis by import-semantics strategy

Generalize PR #816's C/C++ transitive #include fix into a language-agnostic
strategy pattern. The `wildcard-synthesis.ts` pipeline phase no longer
references `SupportedLanguages.C` / `SupportedLanguages.CPlusPlus` — it
dispatches on `provider.importSemantics` via an exhaustive `switch`.

Also fixes a correctness bug the original BFS introduced: `queue.pop()`
(LIFO/DFS) reversed the iteration order of `#include` directives, which —
combined with first-seen-wins dedup in `synthesizeForFile` — silently
bound overloaded symbols to the wrong header. For the `cpp-calls`
fixture, `write_audit("hello")` was being resolved to `zero.h`'s arity-0
overload instead of `one.h`'s arity-1 overload, breaking arity
narrowing. Switched to FIFO (`queue.shift()`) with direct imports seeded
in declaration order.

Taxonomy (researched across 20+ languages + stack-graphs / SCIP prior art):

  | Tag                 | Traversal       | Languages                          |
  |---------------------|-----------------|------------------------------------|
  | named               | none            | TS, JS, Java, C#, Rust, PHP, Kotlin|
  | wildcard-transitive | BFS closure     | C, C++                             |
  | wildcard-leaf       | single hop      | Go, Ruby, Swift, Dart              |
  | namespace           | none at import  | Python                             |
  | explicit-reexport   | topological DAG | (scaffold; TS `export *` future)   |

Changes:
- Widen `ImportSemantics` union from 3 to 5 tags with full taxonomy JSDoc
- Retag 5 providers: c-cpp (x2) → wildcard-transitive; dart, go, ruby,
  swift → wildcard-leaf
- Move BFS closure into `wildcard-synthesis.ts` as `expandTransitiveIncludeClosure`
  (pipeline-owned; providers stay pure declarations)
- Replace `if (lang === C || CPP)` with `dispatchSynthesis` helper called
  by both Loop 1 (ctx.importMap) and Loop 2 (graphImports) so a future
  transitive language whose edges arrive via graphImports gets closure
  expansion consistently
- `never`-assertion default arm forces compile-time exhaustiveness
- `explicit-reexport` arm falls through to leaf behavior (scaffold;
  TODO: implement re-export DAG walk for TS `export *` / Rust `pub use`)
- New unit tests covering circular includes, deep chains, diamond dedup,
  graphImports-only paths, and order-preservation (the regression fix)

Verification:
- All existing C/C++ transitive tests pass unchanged
- Previously failing `cpp.test.ts > resolves run → write_audit to one.h
  via arity narrowing` now passes
- `tsc --noEmit` clean
- 225/225 tests pass across wildcard-synthesis, cross-file-binding,
  cpp resolver, and new closure unit tests

* fix(ingestion): bound closure size, O(1) dequeue, track Strategy 4 (#816 review)

Address @xkonjin's review feedback on the import-resolution strategy refactor:

1. **DoS guard**: cap transitive closures at 5,000 files via
   `MAX_TRANSITIVE_CLOSURE_SIZE`. Pathological codebases (boost-style headers,
   monoheader kernels) could previously produce closures with tens of thousands
   of entries per translation unit. BFS now stops early and returns a partial
   closure rather than risking OOM. The closest-headers-first BFS ordering
   means the partial closure still contains the files overload resolution
   cares about.

2. **Perf**: replace `Array.prototype.shift()` (O(n)) with a head-index queue
   (O(1) dequeue). Deep chains previously had quadratic BFS behavior; now
   linear in closure size.

3. **Strategy 4 tracking**: change TODO in `dispatchSynthesis` to
   `TODO(#821)` referencing the filed issue for TS `export *` / Rust
   `pub use` DAG-walk implementation, and clarify that today's leaf
   fallthrough preserves correctness for direct imports — only the extra
   re-export traversal is missing.

4. **Test**: new unit test exercising the 5,000-file cap on a 10k-file
   synthetic chain, verifying partial-closure invariants (starts from
   importer side, bounded, deep nodes excluded).

Not addressed in this commit (followups):
- Review point 3 (graphImports-only deep-chain *integration* fixture):
  unit tests already exercise the `graphImports` traversal path directly
  in isolation and combined with `importMap`. A fixture that stresses
  graphImports-only transitive resolution is valuable but requires
  understanding when the pipeline populates graphImports distinctly from
  ctx.importMap — tracking as a followup rather than blocking this PR.

---------

Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-14 09:39:17 +01:00
Copilot
26ff700e37
refactor(pipeline): DAG-based phase architecture + container-logic extraction to LanguageProvider (#809)
* Initial plan

* refactor: move language-specific container node logic into LanguageProvider

- Add resolveEnclosingOwner hook to LanguageProviderConfig
- Add staticOwnerTypes to MethodExtractionConfig
- Implement Ruby resolveEnclosingOwner (singleton_class → class/module)
- Replace hardcoded STATIC_OWNER_TYPES with config.staticOwnerTypes
- Move Ruby static types to rubyMethodConfig
- Move Kotlin static types to kotlinMethodConfig
- Remove Ruby singleton_class branch from findEnclosingClassInfo
- Collapse seqFindEnclosingClassNode/seqFindRawEnclosingContainerNode
  into single provider-aware seqFindEnclosingOwnerNode
- Update worker path to pass provider.resolveEnclosingOwner

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bc9f9d4d-f749-4872-9ff2-17fc86e08787

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: add regression tests for config-driven staticOwnerTypes and resolveEnclosingOwner hook

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bc9f9d4d-f749-4872-9ff2-17fc86e08787

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor: implement DAG-based pipeline architecture with phase extraction

Restructure the ingestion pipeline from a ~1800-line monolithic orchestrator
into a DAG (Directed Acyclic Graph) of named phases with explicit dependencies.

New files under pipeline-phases/:
- types.ts: PipelinePhase, PipelineContext, PhaseResult contracts
- runner.ts: DAG runner with topological sort validation
- scan.ts, structure.ts, markdown.ts, cobol.ts: early phases
- parse.ts + parse-impl.ts: chunked parse + resolve (the core)
- routes.ts, tools.ts, orm.ts: post-parse enrichment phases
- cross-file.ts + cross-file-impl.ts: cross-file binding propagation
- mro.ts, communities.ts, processes.ts: graph analysis phases
- index.ts: barrel export

pipeline.ts reduced from ~1960 lines to ~184 lines:
- DAG phase array declaration
- runPipelineFromRepo as thin orchestrator
- topologicalLevelSort retained for backward compat

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136bf9c3-2f4f-449b-9fff-001332c8371c

* test: add DAG runner unit tests, update ARCHITECTURE.md with phase DAG docs

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136bf9c3-2f4f-449b-9fff-001332c8371c

* fix: address code review - pass resolutionContext through parse output, fix worker URL path

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136bf9c3-2f4f-449b-9fff-001332c8371c

* fix: declare transitive parse dependency explicitly in mro/communities/processes phases

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136bf9c3-2f4f-449b-9fff-001332c8371c

* refactor: improve pipeline-phases clean code and folder structure

- Extract synthesizeWildcardImportBindings to wildcard-synthesis.ts
- Extract extractORMQueriesInline to orm-extraction.ts
- Create shared constants.ts for AST_CACHE_CAP
- Fix inline type import in orm.ts (use proper top-level import)
- Add comprehensive JSDoc to getPhaseOutput explaining type safety
- Move isDev to module level in cross-file.ts (consistency)
- Improve module-level documentation across files
- Organize barrel exports in index.ts with section comments

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2bd6d4aa-6271-4009-8dd2-332ea8ec73ab

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* address review feedback: fix circular dep, allFetchCalls mutation, progress bugs, remove DAG naming, extract isDev, fix _item naming, fix O(n²) line calc

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6cf53c9b-d55d-4c6f-bf3d-7bfb82d512b6

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* improve JSDoc on lineNumberAtOffset binary search

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6cf53c9b-d55d-4c6f-bf3d-7bfb82d512b6

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* address review: filter deps in runner, move totalFiles to ctx, fix cycle JSDoc, centralize isDev, remove DAG naming

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b388424f-b939-4a94-97de-3855f9465564

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix doc consistency in graph-sort.ts module-level and function-level JSDoc

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b388424f-b939-4a94-97de-3855f9465564

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(pipeline): wrap phase errors with phase name and emit terminal error progress event

Restores phase diagnostics at CLI/MCP boundary. runPipeline now wraps
phase.execute() in try/catch and rethrows with 'Phase <name> failed: ...'
preserving the original via { cause }. Also emits a terminal
{ phase: 'error' } progress event so subscribers see the failure before
the rejection propagates. Handler errors during error reporting are
swallowed to keep the original cause authoritative.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U1)

* fix(pipeline): move bindingAccumulator dispose into crossFile try/finally; make single-use

crossFile.execute() now wraps its body in try/finally so the accumulator
is released on both the happy path and when runCrossFileBindingPropagation
throws. Dev-mode telemetry stays inside the try block before dispose (all
three counters return 0 after dispose clears internal maps).

BindingAccumulator becomes single-use: appendFile after dispose now throws
'BindingAccumulator: use after dispose' instead of silently re-animating
via the old _disposed auto-clear. Docs updated; the only production
construction site (parse-impl) always creates a fresh instance per run,
so no caller relied on the re-use contract.

Residual risk documented in crossFile module JSDoc: a future phase
inserted between parse and crossFile that throws would still leak the
accumulator. Any such phase must manage accumulator lifetime explicitly.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U2)

* docs(pipeline): explain why importCtx teardown is safe before crossFile

Investigation (plan U3) confirms: `importCtx` (ImportResolutionContext)
is a scratch workspace with no downstream consumer after parse.
`resolutionContext` (returned to crossFile) is a distinct object that
owns importMap / namedImportMap / packageMap / moduleAliasMap / model,
and never closes over importCtx. cross-file-impl consumes only that
ctx via processCalls. The two confusingly-similar "context" names
were the root of the adversarial reviewer's concern — comment locks
in the invariant so the next reader sees it.

No behavioral change.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U3)

* refactor(pipeline): remove ctx.totalFiles side-channel; promote to ParseOutput

totalFiles was a hidden mutable field on PipelineContext written by
parse and read by mro/communities/processes — five reviewers flagged
this as a violation of the immutable-context invariant. Removed from
PipelineContext, which is now fully readonly, and made the implicit
temporal dep explicit: mro/communities/processes now declare 'parse'
as a dep and read totalFiles via getPhaseOutput<ParseOutput>(...).

No behavior change. Topo-sort unchanged because parse was already a
transitive dep through crossFile.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U4)

* feat(method-extractor): runtime staticOwnerTypes guard at factory chokepoint

createMethodExtractor now rejects MethodExtractionConfigs that list
companion_object / singleton_class / object_declaration in
typeDeclarationNodes but omit the matching entry from staticOwnerTypes.
Fails loudly at provider construction time instead of producing
silent isStatic=false on the 50000th file analyzed.

Opt-out convention preserved: an explicit `new Set()` (empty Set)
signals intentional exclusion and passes the guard (memory obs #30588).

All 13 existing language configs pass the guard; the new negative test
fails without it. Test-first.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U5)

* fix(pipeline): wrap sequential-fallback in try/finally so cleanup survives throws

The sequential-fallback block in runChunkedParseAndResolve now runs
inside a try/finally that guarantees astCache.clear(), accumulator
finalize, and enrichExportedTypeMap execute even if readFileContents
or processCalls throws mid-fallback. Cleanup failures are caught
inside the finally so they can't mask the original error.

Accumulator disposal ownership remains with crossFile (U2) — U6 only
adds astCache cleanup and preserves finalize ordering on the error
path.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U6)

* test(pipeline): direct unit coverage for wildcard-synthesis and cross-file-impl

Both modules previously had zero direct unit coverage — branches were
exercised only through integration tests' happy paths.

wildcard-synthesis.test.ts covers: Go graph-IMPORTS fallback, Python
moduleAliasMap build, MAX_SYNTHETIC_BINDINGS_PER_FILE cap, dedup
against existing namedImportMap entries, and empty-exportedSymbols
early return.

cross-file-impl.test.ts covers: gapRatio below threshold no-op,
MAX_CROSS_FILE_REPROCESS cap, graph-only exportedTypeMap fallback,
and empty namedImportMap short-circuit.

Tests assert current behavior — any future regression flips them.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U7)

* test(pipeline): golden-file graph-parity regression guard on mini-repo fixture

Pins the current post-P1/P2 graph output (57 symbols, 92 relationships,
4 processes, deterministic edge digest) so future silent refactors
cannot drift behavior unnoticed. If any count changes or any edge
rewires, the test fails with a readable diff listing what changed
and a copy-pasteable UPDATE_GOLDEN=1 regen command.

Edge digest keyed by symbolic (label, name, filePath) triples rather
than raw generateId output — stays meaningful across id-encoding
refactors while still catching real semantic rewiring.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U8)

* fix(pipeline): minimal cycle reporting + resolveEnclosingOwner loop safeguards

U9: runner cycle detection now reports only the SCC members via DFS
back-edge trace ('Cycle detected: A -> B -> C -> A') rather than
everything with inDegree > 0 (which mixed cycle members with blocked
dependents). Also emits the 'error' progress event for graph-
validation failures, symmetric with U1's runtime-error path.

U16: findEnclosingClassInfo now defends against language-provider
hooks that return non-container nodes — visitedContainers Set breaks
repeat-visit loops, MAX_ENCLOSING_WALK_ITERATIONS is belt-and-braces.
Documented the hook contract invariant so future provider authors
know the walk-continues-upward expectation.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U9, U16)

* refactor(pipeline): type hygiene, dead code cleanup, shared allPathSet, graph-sort naming

Bundles plan units U10, U11, U12, U14, U15:

U10 — Type hygiene: readonly ParseOutput arrays (allExtractedRoutes,
allDecoratorRoutes, allToolDefs, allORMQueries, allPaths); removed
redundant 'as string[] | undefined' cast in routes.ts and 'as URL' in
parse-impl.ts; WorkerPool is now 'import type'. Readonly contract
propagated into processORMQueries (only iterates).

U11 — Dead code & shims: deleted constants.ts shim (AST_CACHE_CAP
inlined into its sole real consumer cross-file-impl.ts; isDev
consumers now import directly from ../utils/env.js). Removed internal
utility re-exports from pipeline-phases/index.ts (no external
consumers). Removed topologicalLevelSort re-export from pipeline.ts;
updated topological-sort.test.ts to import from the canonical
utils/graph-sort.js. Stripped 'Phase 3+4:' stale JSDoc from
parse-impl.ts.

U12 — Perf: StructureOutput now carries allPathSet (ReadonlySet<string>)
built once; cobol, markdown, and cross-file-impl consume the shared
set instead of allocating their own. Parse forwards it via
ParseOutput.allPathSet; processCobol/processMarkdown widened to
ReadonlySet<string>.

U14 — graph-sort.ts: renamed local 'inDegree' to
'pendingImportsPerFile' with expanded JSDoc explaining the reverse-
graph Kahn's formulation and warning future maintainers not to
'correct' it to standard in-degree semantics. Added self-edge test.

U15 — Unconditional worker-fallback logging: removed isDev guard on
the worker-pool-creation-failure console.warn so operators can
diagnose perf degradations in production.

No behavior change. U8 golden-file test confirms pipeline output is
byte-identical.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U10, U11, U12, U14, U15)

* docs: fix ARCHITECTURE.md table integrity; bump AGENTS.md/CLAUDE.md to 1.3.0

U13 — documentation fixes:

ARCHITECTURE.md: the prior insertion of the 'Pipeline Phase DAG'
section orphaned 7 rows from the 'Where to change what' header.
Moved those 7 rows back up under their header so the table reads
contiguously; DAG section now follows the completed table.

AGENTS.md + CLAUDE.md: bumped version 1.2.0 -> 1.3.0, updated Last
reviewed to 2026-04-13, added matching Changelog row documenting
the GitNexus index stats refresh after the DAG refactor. Stat
bumps (symbols/relationships/execution flows) that were sitting
uncommitted in the working tree are now landed under a proper
changelog entry per each file's own documented schema.

Plan: docs/plans/2026-04-13-001-fix-pipeline-dag-refactor-review-findings-plan.md (U13)

* refactor(pipeline): drop spurious parse deps, true-readonly ParseOutput.exportedTypeMap, skip redundant wildcard synth

- mro/communities/processes: switch redundant `parse` dep to `structure` —
  totalFiles originates in structure, so depending on parse for it was a
  spurious data dep that obscured the real DAG.
- ParseOutput.exportedTypeMap: typed as truly ReadonlyMap<...,ReadonlyMap>>;
  graph→exports enrichment moved into parse-impl so the snapshot is
  fully populated at parse return. crossFile builds its own local mutable
  working copy for per-file re-resolution writes — no cast at the boundary.
- parse-impl: hasSynthesized flag guards the unconditional final
  synthesizeWildcardImportBindings call when per-chunk/fallback synthesis
  already ran (graph-global + idempotent across chunks).
- cross-file-impl: documented the intentional `phase: 'parsing'` progress
  label so telemetry bucketing stays consistent with the parse phase.
- cross-file-impl test: replaced the now-moved fallback-enrichment
  assertion with a stronger one — crossFile must not mutate the
  parse-supplied map.

Addresses PR #809 review pass 5 carry-overs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-13 20:31:05 +01:00
Deepak Chauhan
d786e692af
[cli] Preserve Ruby singleton_class context in sequential parsing (#774)
* fix(parsing): preserve ruby singleton class context

* refactor(parsing): clarify singleton class helpers
2026-04-13 13:12:52 +01:00
Arkh74278
a6421b3b1b
[dart] Add call patterns for await, cascade, lambda, and widget-tree contexts (#801)
* feat(dart): add call patterns for await, cascade, lambda, and widget-tree contexts

* fix(dart): address review feedback — await member-chain, cascade comment, static_final comment, add to query-compilation smoke test

* test(dart): add integration tests for await and widget-tree call patterns

* style: apply prettier formatting to dart integration tests

---------

Co-authored-by: arkh <local@localhost>
2026-04-13 11:21:11 +01:00
Copilot
79e1d933fa
fix: resolve generic TypeScript awaited function calls missing from call graph (#804)
* Initial plan

* fix: resolve generic TypeScript function callers missed by impact analysis

When a generic function call is combined with `await` (e.g. `await fn<T>(args)`),
tree-sitter-typescript parses it as a `call_expression` whose `function` field is
an `await_expression` rather than a bare `identifier`. The existing queries only
matched `call_expression { function: identifier }`, so these calls produced no
`@call.name` capture and were silently dropped from the call graph.

Fix: add two new tree-sitter query patterns to `TYPESCRIPT_QUERIES` that handle:
1. `await fn<T>(args)` — awaited generic free call
2. `await obj.fn<T>(args)` — awaited generic member call

Both patterns require the `(type_arguments)` child to be present (which is what
causes tree-sitter to parse the `function` field as an `await_expression`).
Non-generic awaited calls (`await fn(args)`) are unaffected: tree-sitter parses
them as `await_expression { call_expression { identifier } }`, which is still
captured by the existing first pattern.

Also adds a new test fixture `typescript-generic-calls` with two callers of a
generic `verifyToken<T>` function using `await` and three new integration tests.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/4cf75290-900b-4cea-8a65-2a245ff86970

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: clean up test fixture interface ordering and imports

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/4cf75290-900b-4cea-8a65-2a245ff86970

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: add coverage for awaited generic member-call form (await obj.fn<T>())

Address review feedback: the member-call query pattern was untested.

Adds service.ts (TokenService with generic verify<T> method) and guest.ts
(calls await svc.verify<GuestPayload>()) to the typescript-generic-calls
fixture, plus a new integration test asserting the CALLS edge resolves.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/fcbf8d99-8dbc-40ce-b2a3-60b8d63c095a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* revert: undo accidental ladybugdb version bump in package files

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/fcbf8d99-8dbc-40ce-b2a3-60b8d63c095a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* style: run prettier on changed files

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/8c7d8291-74bb-4a86-ae47-7c79e2cbb57e

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-13 10:16:42 +01:00
Copilot
a94d6ef80b
Extract registries into model/ module with SemanticModel interface (#786)
Some checks are pending
CI / Save PR Metadata (push) Blocked by required conditions
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / CI Gate (push) Blocked by required conditions
* Initial plan

* feat(SM-20): extract registries into model/ module with SemanticModel interface

- Create model/type-registry.ts — TypeRegistry interface + factory
- Create model/method-registry.ts — MethodRegistry interface + factory
- Create model/field-registry.ts — FieldRegistry interface + factory
- Create model/semantic-model.ts — SemanticModel interface + factory
- Create model/heritage-map.ts — re-export HeritageMap types
- Create model/binding-accumulator.ts — re-export BindingAccumulator types
- Create model/resolve.ts — move lookupMethodByOwnerWithMRO from call-processor
- Update symbol-table.ts — delegate to SemanticModel for registry ops
- Update call-processor.ts — re-export lookupMethodByOwnerWithMRO from model/resolve

No circular dependencies: model/resolve.ts does NOT import resolution-context.ts.
All 775 related unit tests pass with no regressions.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/27ad2975-1a31-4f50-815b-178ee8a95277

* fix: clarify re-export comment per code review feedback

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/27ad2975-1a31-4f50-815b-178ee8a95277

* refactor(SM-20): wire up SemanticModel as first-class resolution input

PR #786 extracted TypeRegistry/MethodRegistry/FieldRegistry into model/
behind SemanticModel, but consumers still routed through SymbolTable
delegates. This change completes Phase 6 of the fuzzy-lookup elimination
roadmap by making call-processor, resolution-context, type-env, and
heritage-map query the model directly via `table.model.{types,methods,fields}`.

Also absorbs the open PR #786 review findings so the branch lands clean:
- Removed duplicate JSDoc block on lookupMethodByOwner (symbol-table.ts)
- Added model/index.ts barrel for the public model/ surface
- Fixed O(n) buildParentMapFromHeritage BFS via head-pointer queue
- Clarified re-export facade framing on binding-accumulator.ts and
  heritage-map.ts inside model/
- Refined @internal JSDoc on lookupMethodByOwnerWithMRO

Changes:
- symbol-table.ts: expose `readonly model: SemanticModel` on the
  SymbolTable interface. SymbolTable delegate wrappers (lookupClassByName
  etc.) stay as thin pass-throughs for backward compat; deletion is a
  follow-up once all internal callers are migrated.
- model/resolve.ts: lookupMethodByOwnerWithMRO now takes SemanticModel
  instead of SymbolTable, removing the last SymbolTable import from the
  model/ module. Preserves circular-dependency firewall.
- call-processor.ts: 6 call sites in D0 member resolution, field
  resolution, ctor override, and ctor disambiguation migrated to
  model.types/methods/fields.
- resolution-context.ts: tier 3 class+impl lookup migrated.
- type-env.ts: 5 sites across lookupClassDefsByName, resolveFieldType,
  and resolveMethodReturnType migrated.
- heritage-map.ts: parent/child class-name resolution migrated.

Tests:
- symbol-table.test.ts: +10 parity and feeding-audit tests covering
  every model.{types,methods,fields} path (Class, Method, Property,
  Impl, Function-with-ownerId, Property-without-ownerId skip, arity
  filtering, clear cascade).
- call-processor.test.ts: classLookupSpy now targets
  ctx.symbols.model.types since the wrapper is bypassed.
- type-env.test.ts: createMockSymbolTable and the destructured-call
  makeSymbolTable helpers gained a model shim that forwards to the
  (possibly overridden) top-level lookup stubs.

Validation: full suite 5603 passed / 159 skipped, resolver integration
suite (19 files, 1766 tests) clean, tsc --noEmit clean.

* refactor(SM-21): invert ownership — SemanticModel contains SymbolTable

Follow-up to SM-20. Previously SymbolTable owned a `model` subfield;
this commit turns the ownership direction around so the SemanticModel
is the top-level container and SymbolTable is nested as `.symbols`:

    SemanticModel (top-level, passed everywhere)
      ├── types   (TypeRegistry)
      ├── methods (MethodRegistry)
      ├── fields  (FieldRegistry)
      └── symbols (SymbolTable — file-indexed + callable-name index)

The owner-scoped registries live directly on the model; file and
callable-name lookups go through `.symbols`. Consumers receive a
`SemanticModel` and reach into the appropriate field — no more
`table.model.types.X` double-hop.

Core changes:
- symbol-table.ts: createSymbolTable now takes injected
  TypeRegistry/MethodRegistry/FieldRegistry via a SymbolTableDeps
  argument. When omitted (test fallback), it creates standalone
  registries locally and clears them in clear() — production callers
  always inject. The five registry convenience delegates
  (lookupClassByName, lookupMethodByOwner, lookupFieldByOwner,
  lookupClassByQualifiedName, lookupImplByName) remain as thin
  forwards to the injected registries so standalone SymbolTable use
  (chiefly tests) stays ergonomic.
- model/semantic-model.ts: createSemanticModel() now creates the
  three registries AND a SymbolTable wired to them, exposing the
  SymbolTable as `.symbols`. clear() cascades through all four.
- resolution-context.ts: `readonly symbols: SymbolTable` field is
  replaced with `readonly model: SemanticModel`. Internal factory
  builds a SemanticModel and keeps a local `symbols` alias for
  backward-compatible inner body.

Consumer migrations (src/):
- call-processor.ts: ctx.symbols.add/.lookupExactAll/
  .lookupCallableByName → ctx.model.symbols.*; ctx.symbols.model.X →
  ctx.model.X. buildTypeEnv option key renamed symbolTable → model.
- type-env.ts: symbolTable parameter renamed model (type
  SemanticModel), all internal call sites rewritten to use
  model.types.*, model.methods.*, model.fields.*,
  model.symbols.lookupExactAll / .lookupCallableByName.
- heritage-map.ts: 2 class-lookup sites migrated.
- pipeline.ts: ctx.symbols → ctx.model.symbols throughout.

Test migrations:
- symbol-table.test.ts: parity tests (which validated the old
  table.model.X hop) replaced with direct SemanticModel coverage via
  createSemanticModel(). New tests exercise types/methods/fields/
  symbols feeding end-to-end.
- type-env.test.ts: createMockSymbolTable rebuilt as a
  SemanticModel-shaped mock that still accepts the legacy flat
  override bag for backward compat; inline `makeSymbolTable` helpers
  for destructured-call and importedReturnTypes suites rewritten to
  match the new shape; buildTypeEnv options `symbolTable: X` and
  `{ symbolTable }` shorthand renamed to `model:`; one real
  createSymbolTable-based test rewritten to use createSemanticModel.
- call-processor.test.ts, heritage-map.test.ts,
  heritage-processor.test.ts, symbol-resolver.test.ts: bulk sed
  `ctx.symbols.` → `ctx.model.symbols.`. call-processor.test.ts spy
  updated to target `ctx.model.types.lookupClassByName`.

Validation: full test suite 5589 passed / 169 skipped / 0 failed;
tsc --noEmit clean; pre-commit eslint + prettier + typecheck all
green. CLAUDE.md / AGENTS.md stats bumped from an earlier `npx
gitnexus analyze` refresh (3965 symbols / 10012 edges / 243 flows).

* refactor(SM-22/SM-23): dispatch table + DAG rearchitecture

SM-22: Extract registration dispatch table into model/registration-table.ts.
Replaces the if/else ladder inside SymbolTable.add() with an O(1)
Map<NodeLabel, RoutingDecision> fan-out. SemanticModel wires the table
per-instance so hooks close over the correct registries.

SM-23: DAG rearchitecture. symbol-table.ts is now a pure 2-index leaf
(fileIndex + callableByName) with zero imports from model/. All
type/method/field routing lives in the model/ layer. Tests migrated to
createSemanticModel() + model.symbols access pattern.

Tests: 5632 passed, 0 failures.

* refactor: delete dead code (skipCallableIndex + model/ facades)

Removes the unused skipCallableIndex flag from the registration dispatch
table and deletes two facade files that had zero consumers.

skipCallableIndex was declared on RoutingDecision and populated for all
10 entries but never read at runtime — semantic-model.ts explicitly
documented that the flag was NOT consulted. The callable-index gate
lives inside SymbolTable.add() via CALLABLE_TYPES.has(type), which is
the single source of truth. Deleting the flag keeps SymbolTable as the
sole decision point and removes documentation-as-data.

model/binding-accumulator.ts and model/heritage-map.ts were facade
pass-throughs of their parent-directory counterparts. Grep confirms no
consumer imports either from the model/ path — all usage goes through
../binding-accumulator.js and ../heritage-map.js directly. model/index.ts
was the only "user" and re-exported them with a note about unifying the
import boundary, but that boundary has no actual consumers today.

Resolves review findings M-01 and M-03 from
.context/compound-engineering/ce-review/20260411-144641-59605d93/maintainability.json

Tests: 5631 passed, 0 failures (1 less than pre-Unit-1: the
skipCallableIndex-specific assertion was removed).

* refactor: remove lookupMethodByOwnerWithMRO backward-compat shim

call-processor.ts re-exported lookupMethodByOwnerWithMRO from
./model/resolve.js as a backward-compat shim for symbol-table.test.ts.
The function already lives in model/resolve.ts and is re-exported
properly from model/index.ts (the barrel) — the call-processor shim
was a duplicate export path with no durable reason to exist.

Migrated the test import from call-processor.js to model/index.js
(the canonical barrel). Deleted the re-export statement and the stale
"re-exported for backward compatibility" comment block. Hoisted the
remaining import to the top of the file with the other imports; the
bottom-of-file position was a relic of the shim pattern.

Resolves review finding M-02 from
.context/compound-engineering/ce-review/20260411-144641-59605d93/maintainability.json

Tests: 5631 passed, 0 failures.

* refactor: harden registration dispatch runtime safety

Two hardening changes in semantic-model.ts, both closing silent-failure
paths in the SM-series dispatcher-bypass failure mode.

1. model.symbols.clear() now cascades to the owner-scoped registries.
   Previously, the SymbolTable facade exposed rawSymbols.clear directly,
   which only emptied fileIndex + callableByName — the types/methods/
   fields registries stayed populated. Any caller holding a SymbolTable
   reference that invoked .clear() left the model in a split state where
   subsequent .add() calls double-registered in the registries. No
   current caller exercises this path, but it was a latent phantom-
   resolution risk that didn't belong in a public API. Extracted the
   cascade into a single cascadeClear closure wired into both
   model.clear() and the facade's clear field.

2. runExhaustivenessGuard now throws instead of console.warn on drift.
   The production short-circuit via NODE_ENV === 'production' is
   preserved, so real users never see the throw — but CI and dev runs
   now fail loudly if a NodeLabel is added to gitnexus-shared without
   being placed in one of the three registration-table allowlists. The
   previous warn-only behavior was silent in test output volume; SM-19
   already documented dispatcher-bypass as the dominant silent-failure
   mode in this codebase.

Test-first: added test/unit/model/semantic-model.test.ts covering
model.symbols.clear() cascade (4 registries × clear = 4 tests), the
existing model.clear() cascade (regression guard), and a happy-path
construction test that verifies the current allowlists have zero drift.

Resolves correctness P2 finding (symbols.clear() partial clear),
correctness P3 (exhaustiveness warn-only), and kieran-typescript KT-03
(same exhaustiveness finding, agreement boost).

Tests: 5638 passed (+7 new), 0 failures.

* docs: fix stale JSDoc references in resolveStaticCall

call-processor.ts:2215-2216 referenced SymbolTable.lookupClassByName
and SymbolTable.lookupMethodByOwner via {@link}. Both methods were
removed from SymbolTable during SM-20 — they now live on TypeRegistry
and MethodRegistry respectively, accessible via model.types and
model.methods.

Other SymbolTable.* references in the codebase (lookupExactFull, add,
lookupCallableByName in call-processor.ts:593, symbol-table.ts:86,
type-extractors/types.ts:57) target methods that are still on
SymbolTable and remain valid.

Resolves correctness P3 and kieran-typescript KT-02 (same finding,
agreement boost).

* refactor: deduplicate ALL_NODE_LABELS constant

ALL_NODE_LABELS was private in semantic-model.ts and duplicated
verbatim in registration-table.test.ts. Two hardcoded lists meant a
new NodeLabel added to gitnexus-shared could land in one copy but not
the other, silently drifting the exhaustiveness invariant.

Exported ALL_NODE_LABELS from semantic-model.ts, re-exported through
model/index.ts for barrel consistency, and switched the test to import
it instead of redeclaring. The explanatory comment now describes the
single-source-of-truth contract.

Resolves maintainability M-04.

Tests: 5638 passed, 0 failures.

* refactor: add compile-time NodeLabel exhaustiveness check

The runtime exhaustiveness guard in semantic-model.ts caught drift at
test time. Added a type-level check in registration-table.ts that
catches drift at BUILD time — if a new NodeLabel is added to
gitnexus-shared without being classified into one of the three
allowlists, TypeScript fails the _exhaustiveCheck assignment and
names the missing label.

The runtime guard stays as belt-and-suspenders: if a future contributor
bypasses the type check with @ts-ignore, the runtime guard still fires
in dev/test.

Implementation: converted the three allowlist Set<NodeLabel> initializers
to use `as const` tuples, then derived a union type from the tuples and
asserted `Exclude<NodeLabel, union> extends never`. Zero runtime impact
— the exported Sets are unchanged, Map.get hot-path performance is
unchanged, the test API is unchanged.

Resolves kieran-typescript KT-04.

Tests: 21/21 registration-table tests pass with zero modifications.

* refactor(test): restore type safety to createMockSymbolTable

createMockSymbolTable was widened to (overrides: any = {}): any with an
eslint-disable-next-line, and every buildTypeEnv call site passed the
mock as `model: mockSymbolTable as any`. The widening masked silent
false-green tests: buildTypeEnv accesses model.types/methods/fields,
and a flat any-typed override could silently return undefined from a
path that TypeScript should have caught at compile time.

Defined LegacyMockOverrides interface with typed stubs for each method
the mock can override (SymbolTable reads + TypeRegistry/MethodRegistry/
FieldRegistry lookups). Return type is now SemanticModel, so the mock
object is compile-checked against the real interface — a missing
registry method is a type error, not a silent runtime undefined.

Removed the eslint-disable and all 9 `as any` casts at call sites
(lines 1287, 1300, 1307, 2124, 2138, 5823, 5835, 5850, 5870). The
mock's return value now flows through buildTypeEnv's typed `model`
option without coercion.

Resolves kieran-typescript KT-01 and testing gap TG-02. This was the
highest-value cleanup in the plan — the only finding representing real
hidden test weakness.

Tests: 360 passed | 7 skipped (type-env.test.ts), typecheck clean.

* test: close coverage gaps in model/ registries

Added direct unit tests for the three owner-scoped registries that
previously had only transitive coverage via symbol-table.test.ts and
registration-table.test.ts. These new tests pin behaviors that were
flagged by the testing reviewer as untested or undertested.

method-registry.test.ts (14 tests):
- T-01: arity-fallback branch — when argCount matches no overload,
  fall back to the full pool so fuzzy resolution still has candidates.
  Previously untested and would have returned undefined instead of
  a valid candidate if the branch regressed.
- T-02: requiredParameterCount range filtering — methods with default
  parameters accept any argCount in [requiredParameterCount,
  parameterCount]. Previously untested at the registry level.
- Variadic fallback (parameterCount=undefined is retained during arity
  narrowing, bypassing range check).
- Return-type dedup paths: shared returnType → first wins, differing
  returnTypes → undefined, firstReturnType=undefined → undefined,
  single-overload skips dedup entirely.

type-registry.test.ts (9 tests):
- classByName homonym accumulation (two User classes in different
  packages both returned).
- classByQualifiedName disambiguation — same simple name, different
  FQNs resolve independently.
- Partial classes with identical simple + qualified name accumulate
  in both indexes.
- registerImpl stores Rust impl blocks separately from classes.
- Multiple impl blocks per type accumulate.

field-registry.test.ts (6 tests):
- register/lookup round-trip, owner-scope isolation, last-wins on
  duplicate key (flat map, not overload list).
- clear + re-register round-trip.

Extended symbol-table.test.ts cascade test (renamed from "both
registries" to "all three registries and the nested symbol table") to
also assert model.methods and model.fields are cleared — the test
name previously implied full coverage but only asserted types + symbols.

Resolves testing findings T-01, T-02, T-03, T-05.

Tests: 5667 passed (+29 new), 0 failures.

* refactor(test): replace brittle reference-equality tests + add intent comments

Two cleanups flagged as low-severity P3 by the testing reviewer:

1. registration-table.test.ts: Replaced three reference-equality tests
   (hook identity via toBe) with behavioral tests that survive a future
   refactor to per-label closures. The new "class-like behavior group"
   describe iterates Class/Struct/Interface/Enum/Record/Trait and
   verifies each one writes to types.registerClass. Same pattern for
   Method/Constructor. A separate "behavior group isolation" describe
   verifies class-like hooks don't leak into methods/fields and Impl
   never pollutes registerClass. Strictly more coverage than the
   reference-equality tests provided and implementation-independent.

2. symbol-resolver.test.ts: Added a comment above the lookupExactFull
   and SM-16: getFiles() describes explaining why they intentionally
   use createSymbolTable() directly instead of createSemanticModel().
   The DAG leaf-only behaviors they test do not involve registries, so
   testing the bare SymbolTable keeps the unit isolated. Prevents a
   future reader from "fixing" the inconsistency.

3. qualified-class-lookups.test.ts: Added a comment above
   `const symbolTable = model.symbols` explaining that processParsing
   writes still reach the owner-scoped registries via SemanticModel's
   fan-out — the alias is convenience, not a leaf in isolation.

Resolves testing T-04, kieran-typescript KT-05, kieran-typescript KT-06.

Tests: affected files all green (112 passed in registration-table +
symbol-resolver + qualified-class-lookups).

* refactor(model): collapse RoutingDecision wrapper and trim barrel surface

Two cleanups against the advanced-review findings on post-Unit-9 state:

S2 (cross-reviewer agreement — architecture-strategist + code-simplicity):
Delete the RoutingDecision single-field wrapper interface. Post-Unit-1
it held exactly one field (hook: RegistrationHook) and added pure
ceremony at every call site — `dispatchTable.get(key)!.hook(name, def)`
vs the now-direct `dispatchTable.get(key)!(name, def)`. Change the Map
type from Map<NodeLabel, RoutingDecision> to Map<NodeLabel,
RegistrationHook>, drop the interface, and update 17 test call sites.

A3 (architecture-strategist): Trim model/index.ts barrel surface.
createRegistrationTable, RegistrationHook, and RegistrationTableDeps
were re-exported from the barrel despite having zero legitimate
consumers outside model/ itself. The only callers (semantic-model.ts
and registration-table.test.ts) import directly from
./registration-table.js. Barrel exposure invited external callers to
construct orphan dispatch tables with independent registries,
weakening the SM-21 ownership inversion where SemanticModel is the
composition root. Kept CALLABLE_ONLY_LABELS, INERT_LABELS,
DISPATCH_LABELS exported since those remain useful for downstream
resolution logic and have no construction risk.

Resolves review findings:
- S2 (code-simplicity P3, 0.85) + architecture-strategist residual
- A3 (architecture-strategist P3, 0.82)

Tests: 5674 passed, 0 failures. Typecheck clean.

* refactor(model): replace runtime exhaustiveness guard with compile-time bijection

Replace the three-layer drift protection (hardcoded ALL_NODE_LABELS
array + 3 tuple consts + _ExhaustiveLabelCheck type + runExhaustivenessGuard
runtime + CI taxonomy test) with a single Record<NodeLabel, LabelBehavior>
map that structurally proves every invariant at compile time.

## Before

- ALL_NODE_LABELS hardcoded in semantic-model.ts (36 entries, could drift)
- DISPATCH_LABELS_TUPLE / CALLABLE_ONLY_LABELS_TUPLE / INERT_LABELS_TUPLE
  private tuples (36 more entries total, could overlap or miss)
- _ClassifiedLabel / _UncoveredLabel type-level check (caught missing
  labels but NOT duplicates across tuples)
- runExhaustivenessGuard runtime throw (only defense against duplicates)
- NodeLabel taxonomy coverage test in CI (same check as runtime guard)

Four defenses for invariants that the type system can express directly.

## After

```ts
type LabelBehavior = 'dispatch' | 'callable-only' | 'inert';

const LABEL_BEHAVIOR = {
  Class: 'dispatch',
  // ...36 entries...
  Tool: 'inert',
} as const satisfies Record<NodeLabel, LabelBehavior>;
```

The `as const satisfies Record<NodeLabel, LabelBehavior>` combo enforces:

1. **Every NodeLabel must be a key** — Record requires all K keys.
   Adding a NodeLabel to gitnexus-shared without classifying it here
   fails with "Property 'X' is missing in type ..." naming the drifted label.
2. **No non-NodeLabel keys allowed** — `satisfies` with object literals
   triggers excess-property checking. A typo'd key fails to compile.
3. **No duplicate classification** — impossible by construction; object
   keys are unique at the source level.
4. **Valid category** — LabelBehavior is a narrow union, typos caught.

`ALL_NODE_LABELS`, `DISPATCH_LABELS`, `CALLABLE_ONLY_LABELS`, and
`INERT_LABELS` are now derived via `Object.keys(LABEL_BEHAVIOR)` and
`filter(l => LABEL_BEHAVIOR[l] === ...)` — single source of truth,
structurally impossible to drift.

## Deleted

- runExhaustivenessGuard() function in semantic-model.ts (~18 lines)
- ALL_NODE_LABELS hardcoded array in semantic-model.ts (~38 lines)
- DISPATCH_LABELS_TUPLE / CALLABLE_ONLY_LABELS_TUPLE / INERT_LABELS_TUPLE
  private consts in registration-table.ts (~30 lines)
- _ClassifiedLabel / _UncoveredLabel / _exhaustiveCheck type machinery
  (~20 lines)

## Kept named proofs: none

The `as const satisfies` on the object literal already catches all four
drift modes. Named type-level proofs (_MissingFromMap / _ExtraKeysInMap)
are pure duplication and were removed per review.

## Also in this commit

- S6: trim wrappedAdd narration comments in semantic-model.ts
  (Step 1/2/3 block comments removed; kept the Function+ownerId WHY note)
- A3: tighten model/index.ts barrel — createRegistrationTable,
  RegistrationHook, RegistrationTableDeps remain direct-imports only;
  ALL_NODE_LABELS and LabelBehavior re-exported from the new home in
  registration-table.ts

## Resolves

- Advanced-review S4 (runtime guard per-call cost) — guard no longer exists
- Advanced-review S1 (tuple three-defenses indirection) — single Record replaces all tuples
- Correctness P3 (exhaustiveness warns-only) — structurally impossible to drift
- Unit 6 type-level check — subsumed by the Record type
- Unit 3 runtime throw — no longer needed

Tests: 5674 passed, 0 failures. Typecheck clean.

* test(model): delete duplicate closure-isolation spy tests

S5 (code-simplicity P3): The 'closure isolation — each hook can only
write to its registry' describe block duplicated the 'behavior group
isolation' block's coverage via a different mechanism.

Behavioral tests (lines 151-174, kept):
  table.get('Class')!('User', def);
  expect(deps.methods.lookupMethodByOwner('unrelated', 'User')).toBeUndefined();
  expect(deps.fields.lookupFieldByOwner('unrelated', 'User')).toBeUndefined();

Spy tests (deleted, ~55 lines):
  vi.spyOn(deps.methods, 'register')
  table.get('Class')!('User', def);
  expect(methodsSpy).not.toHaveBeenCalled();

Both assert the same invariant — classHook does not touch the methods or
fields registries. The behavioral form observes the END STATE of the
registry (lookup returns undefined), which is the actual contract.
The spy form asserts the IMPLEMENTATION (a specific method was not
called), which couples to internal wiring — a refactor to a different
register function name would break the spy test while the behavioral
test would still pass.

Also dropped the now-unused `vi` import from vitest.

Tests: 24/24 registration-table.test.ts pass (-4 from spy deletion).

* refactor(model): compile-time cross-invariant between CLASS_TYPES and dispatch classHook

A1 (architecture-strategist P2, 0.90): CLASS_TYPES in symbol-table.ts
and the class-like entries of the dispatch table were two independent
hardcoded sets. Adding a new class-like label (e.g. Swift 'Extension')
to one but not the other would silently degrade qualifiedName
population — the symptom is subtle (partial qualified-name lookups)
and no test asserted the co-extensive invariant.

Fixed with a single source of truth and a two-layer compile-time
enforcement:

## symbol-table.ts

- Add `CLASS_TYPES_TUPLE` as `readonly [...] as const satisfies
  readonly NodeLabel[]`. The `satisfies` forces every tuple entry to
  be a valid NodeLabel at compile time.
- Export derived type `ClassLikeLabel = typeof CLASS_TYPES_TUPLE[number]`.
- Derive `CLASS_TYPES` Set from the tuple — same runtime shape as
  before, now typed `ReadonlySet<NodeLabel>`.

## registration-table.ts

- Import `CLASS_TYPES_TUPLE` and `ClassLikeLabel` from symbol-table.ts.
- Narrow the `satisfies` on `LABEL_BEHAVIOR` via intersection:
      Record<NodeLabel, LabelBehavior> & Record<ClassLikeLabel, 'dispatch'>
  This forces every class-like label to have value 'dispatch' at
  compile time. Adding a label to CLASS_TYPES_TUPLE without
  classifying it as dispatch in LABEL_BEHAVIOR fails to compile with
  a type error naming the drifted label.
- Build the class-like entries of the dispatch Map by iterating
  `CLASS_TYPES_TUPLE` at factory time. Adding a label to the tuple
  automatically wires it to classHook — no second place to update.

## What the design prevents

1. Drift scenario A (A1 original): 'Extension' added to CLASS_TYPES_TUPLE
   but not to LABEL_BEHAVIOR → compile error on LABEL_BEHAVIOR's
   satisfies.
2. Drift scenario B: 'Extension' added to CLASS_TYPES_TUPLE but not
   wired to classHook → impossible because the Map is derived from the
   tuple.
3. Drift scenario C: class-like label classified as something other
   than 'dispatch' in LABEL_BEHAVIOR → compile error on the narrowed
   intersection.

Runtime behavior unchanged: same 6 labels in CLASS_TYPES, same 6
class-like entries in the dispatch Map. Tests pin the behavior via
the existing behavior-group tests in registration-table.test.ts.

DAG unchanged: registration-table.ts already imported from symbol-table.ts
(the allowed upward direction). symbol-table.ts still imports nothing
from model/.

Tests: 5670 passed, 0 failures. Typecheck clean.

* test(field-extraction): use SemanticModel facade instead of raw SymbolTable

A6 (architecture-strategist P3, 0.85): field-extraction.test.ts created
its FieldExtractorContext fixture with `symbolTable: createSymbolTable()` —
a raw SymbolTable leaf, not the facade. In production, the context's
symbolTable field is always `model.symbols` (the SemanticModel-wrapped
facade where .add() dispatches through the owner-scoped registries).

The current field extractors don't call symbolTable.add() at all, so
this change is behavior-neutral today. The value is architectural
consistency — matching the test fixture to the production shape
prevents silent drift if a future field extractor starts registering
dynamically-discovered properties via the context. Without the fix,
such writes would hit the raw leaf and skip the fan-out, and tests
would pass even though the symptom (empty FieldRegistry) would
manifest in production.

Tests: 50/50 field-extraction.test.ts pass. Production tsc --noEmit
clean. Test-tsconfig error count unchanged (634 pre-existing errors
in unrelated test files, out of scope).

* refactor(A5): decouple model/resolve.ts from language registry

Move the MroStrategy type into gitnexus-shared and replace the
language: SupportedLanguages parameter on lookupMethodByOwnerWithMRO
with a direct mroStrategy: MroStrategy literal. Callers derive the
strategy from their language provider before invoking the resolver.

model/resolve.ts no longer imports from ../languages/index.js, so the
model/ layer is free of cross-layer coupling with the language
registry — this closes finding A5 from the SM-20/21/22/23 advanced
review (plan 006).

* feat(A4): add MethodRegistry.lookupMethodByName flat-by-name index

Add a secondary `methodsByName: Map<string, SymbolDefinition[]>` index
on MethodRegistry that returns every method with a given unqualified
name, accumulated across owners and overloads. The new index shares
SymbolDefinition references with methodByOwner — no duplication.

This is step 1 of the A4 double-index removal (plan 006). Tier 3
global resolution will switch to this index in Unit 3 so Method and
Constructor can be removed from CALLABLE_TYPES in Unit 4.

* refactor(A4): extend Tier 3 + memberCallByFile to consult method registry

Add model.methods.lookupMethodByName to Tier 3 global resolution in
resolution-context.ts and to the callable-pool build in
call-processor.ts (resolveMemberCallByFile + D2 widen path).

Intentionally behavior-preserving: Method and Constructor are still
in CALLABLE_TYPES so the new lookup returns identical candidates that
already reach Tier 3 through callableByName. Both paths dedup by
nodeId during this intermediate state — Unit 4 shrinks CALLABLE_TYPES
and the dedup is removed.

Part of plan 006 A4 step 2.

* refactor(A4): shrink CALLABLE_TYPES to free callables only

CALLABLE_TYPES = {Function, Macro, Delegate}. Method and Constructor
are no longer double-indexed in callableByName — they reach resolvers
through model.methods.lookupMethodByName instead.

Companion changes:
- Introduce CALL_TARGET_TYPES = CALLABLE_TYPES ∪ {Method, Constructor}
  for the resolver's kind filter (filterCallableCandidates,
  countCallableCandidates). Separates registration semantics (narrow)
  from the resolver's acceptable-target set (wide).
- type-env.ts for-loop return-type inference consults both indexes,
  treating the union as the authoritative call pool.
- resolveMemberCallByFile + D2 widen path keep the nodeId dedup in
  place: Python/Rust/Kotlin class methods emitted as Function+ownerId
  still land in both indexes until Unit 5 unblocks the normalization.
- Tier 3 global resolution (resolution-context.ts) keeps the same
  dedup for the same reason.

Test updates reflect the new contract: Method/Constructor live in
methodsByName, not callableByName. Orphan Method-without-ownerId now
lives only in the file index (no registry coverage).

Part of plan 006 — closes A4 for strictly-labeled methods. Python/
Rust/Kotlin Function+ownerId normalization is tracked as Unit 5
(blocked).

* refactor: rename CALLABLE_TYPES → FREE_CALLABLE_TYPES

Pure rename. The constant's meaning changed in Unit 4 (free callables
only — no methods, no constructors) so the name now reflects that
scope: "callables that have no owner scope". Updates the constant
declaration and every consumer in src/ and test/.

Closes plan 006 Unit 6.

* refactor(A2): strict SymbolTableReader (pure reads) + SymbolTableWriter (+add)

Split the SymbolTable interface into three strictly layered surfaces:

- SymbolTableReader: lookups + iteration. NO add, NO clear. Holders
  cannot mutate the table in any way.
- SymbolTableWriter extends Reader: + add. NO clear. Holders can
  register new symbols but cannot trigger a leaf-index reset.
- InternalSymbolTable (private, not exported): + clear. The cascading
  reset capability is reachable only through createSymbolTable's
  return type, held exclusively by SemanticModel.rawSymbols.

SemanticModel.symbols is now typed as SymbolTableWriter — external
consumers (workers, processors, pipelines) can register symbols and
query them, but cannot reach .clear(). The A2 LSP fix holds: callers
holding any public reference cannot desync the leaf indexes from the
owner-scoped registries.

Delete the transitional `type SymbolTable = SymbolTableReader` alias
and migrate every consumer (src + test) to the explicit names:
- Field and parameter annotations use SymbolTableReader by default;
  only code that calls .add() uses SymbolTableWriter.
- parsing-processor (workers + sequential paths) takes
  SymbolTableWriter so it can register extracted symbols.
- field-types, call-processor, named-binding-processor,
  workers/parse-worker: use SymbolTableReader (query-only).
- Tests: drop the stale `clear` fields from mock factories and
  migrate the semantic-model cascade tests from the removed
  model.symbols.clear() path to model.clear().

Closes plan 006 Unit 7. Industry sources: TypeScript compiler API
builder pattern, Salsa ParallelDatabase, .NET IReadOnlyList. See the
a2-lsp-clear-contract-research artifact for full citations.

* feat(A2): add SemanticModel.resetFileIndex() partial-reset entry point

Add a named method that clears only the leaf file and callable
indexes without cascading to the three owner-scoped registries
(types, methods, fields). Replaces the rare partial-reset use case
that was previously reachable via the now-removed symbols.clear()
path from A2 (plan 006 Unit 7).

JSDoc makes the semantic difference with model.clear() explicit so
future readers don't have to guess which method to call for a given
reingestion scenario.

Test-first: three scenarios cover the partial-vs-full semantics,
re-add after reset, and idempotency.

Closes plan 006 Unit 8.

* docs(S7): trim registration-table module JSDoc

Remove the ~24 lines of design-provenance citations from the module
JSDoc. The rust-analyzer, TypeScript-compiler, and Fowler references
are preserved in git history via the original SM-22 commits and in
plan 006 Unit 9.

Keep the ownership diagram, behavior-group table, and the
'How to add a new NodeLabel' checklist — those are load-bearing for
future contributors.

Closes plan 006 Unit 9 (S7 advanced-review finding).

* test(S3): migrate type-env.test.ts off LegacyMockOverrides

Replace the createMockSymbolTable bridge and LegacyMockOverrides
interface with real createSemanticModel() + add() calls across all
14 call sites. Where a test needs a specific registry lookup that
can't be pre-populated cleanly, use vi.spyOn on the real registry
instead.

Pattern breakdown:
- Pattern A (pre-populate via model.symbols.add): 13 sites
- Pattern B (vi.spyOn on registry lookup): 1 site

Deletes LegacyMockOverrides + createMockSymbolTable entirely. The
real MethodRegistry arity/returnType semantics match the hand-rolled
mock behavior in every migrated case, and no 'as any' casts remain
in the file.

Closes plan 006 Unit 10 (S3 advanced-review finding).

* refactor: remove unused MroStrategy type exports from language-provider and resolve modules

* refactor: relocate symbol-table, heritage-map, resolution-context into model/

Use git mv so blame and history follow each file:
- gitnexus/src/core/ingestion/symbol-table.ts → model/symbol-table.ts
- gitnexus/src/core/ingestion/heritage-map.ts → model/heritage-map.ts
- gitnexus/src/core/ingestion/resolution-context.ts → model/resolution-context.ts

These three files are part of the SemanticModel layer (file/callable
indexes, heritage parent map, tiered resolver) and now sit alongside
the registries they collaborate with. Updates every consumer import
path across src/ and test/ to the new locations.

* refactor(model): enforce pure-leaf DAG + delete legacy re-exports

model/ is now a pure leaf: zero upward imports and zero compat
shims in its parent processors. Completes the DAG cleanup started
in the previous commit.

1. walkBindingChain — moved into model/resolution-context.ts;
   named-binding-processor.ts deleted.

2. NamedImportMap + NamedImportBinding + isFileInPackageDir —
   moved into model/resolution-context.ts. Every consumer now
   imports from the canonical location directly. Legacy re-exports
   in import-processor.ts deleted.

3. c3Linearize + gatherAncestors — moved into model/resolve.ts.
   mro-processor.ts imports them back for computeMRO. Legacy
   c3Linearize re-export from mro-processor.ts deleted.

4. ExtractedHeritage type — moved into model/heritage-map.ts.
   call-processor.ts, parsing-processor.ts, pipeline.ts,
   heritage-processor.ts, and the test files now import it from
   the canonical location. Legacy re-exports in parse-worker.ts
   and heritage-processor.ts deleted.

5. resolveExtendsType — rewritten in model/heritage-map.ts to
   take an explicit HeritageResolutionStrategy (A5-style DI).
   buildHeritageMap accepts an optional getHeritageStrategy
   callback; production uses getHeritageStrategyForLanguage from
   heritage-processor.ts. Legacy resolveExtendsType re-export
   from heritage-processor.ts deleted.

Verified:
- grep 'from "..' gitnexus/src/core/ingestion/model → empty
- grep 'Re-export for legacy' gitnexus/src/core/ingestion → empty
- npx tsc --noEmit → clean
- npx vitest run → 5686 passing

* docs(model): strip phase/plan references from module comments

Remove SM-20/21/22/23, A2/A4/A5, plan 006, Unit N labels and historical
phrasing ("previously", "legacy", "model-leaf DAG cleanup") from all 10
files in src/core/ingestion/model/. Preserve domain vocabulary (Tier
1/2/3), invariants, and caveats — only the plan archaeology is gone.

* refactor(model): tighten interface segregation + compile-time invariants

Apply four gated findings from branch-wide code review:

- SemanticModel.symbols now typed as SymbolTableReader; MutableSemanticModel
  widens it back to SymbolTableWriter. ResolutionContext.model is typed as
  MutableSemanticModel since it owns the lifecycle. Resolvers that only
  query symbols can annotate their own fields as SemanticModel to drop
  write access at the type level.

- Lookup methods (lookupExactAll, lookupCallableByName, lookupClassByName,
  lookupClassByQualifiedName, lookupImplByName) now return
  readonly SymbolDefinition[]. The returned arrays are live views into
  the internal indexes; the readonly marker prevents accidental caller
  mutation. walkBindingChain return type narrowed to match.

- FREE_CALLABLE_TUPLE + FreeCallableLabel exported from symbol-table.ts
  as the single source of truth for free-callable labels. LABEL_BEHAVIOR
  now satisfies Record<FreeCallableLabel, 'callable-only'> as a second
  cross-invariant alongside Record<ClassLikeLabel, 'dispatch'>. Adding a
  label to the tuple without classifying it as 'callable-only' fails at
  build time. CALLABLE_ONLY_LABELS is now a re-export alias of
  FREE_CALLABLE_TYPES so the two sets cannot drift.

- walkBindingChain fast-exits before allocating its cycle-detection Set
  when the caller's file has no named bindings. Skips ~200k transient
  Set allocations per large-repo resolution pass.

Also fixes five stale comments flagged by the review: duplicate JSDoc
block on RegistrationHook merged; resolve.ts "delegates to mro-processor"
direction corrected; RegistrationTableDeps JSDoc names
createRegistrationTable (not createSymbolTable); mro-processor.ts
"re-exported at top" stale comment removed; gatherAncestors export
comment matches reality.

tsc --noEmit clean, full test suite green (5786 tests).

* refactor(model): resolve four deferred P2 review findings

Address the four gated items from the branch-wide review that needed
design decisions before applying:

F#3 — Method/Constructor without ownerId fallback to callable index.
The dispatch hook silently skips owner-scoped labels that lack an owner
(an extractor contract violation — AST-degraded parse, or a buggy
language extractor). Pre-dispatch-table code let such defs fall through
to callableByName and stay reachable at Tier 3 global resolution. This
restores that fallback in SymbolTable.add so orphaned Methods and
Constructors don't silently vanish. Property deliberately does NOT
participate in the fallback to avoid polluting common names like
id / name / type.

F#4 — Delete MutableSemanticModel.resetFileIndex. The method had zero
production callers (only three tests), documented a "rare partial-
reingestion flow" that was never implemented, and contained the
adversarial-reviewer's double-populate trap: calling resetFileIndex
followed by re-adding the same class symbol would push a duplicate
SymbolDefinition into TypeRegistry.classByName without ever clearing
the first one. If incremental reingestion is ever needed, it can be
designed properly with per-file TypeRegistry invalidation. For now,
deleting the footgun is safer than documenting it.

F#5 — Compile-time dispatch-table completeness check. `LABEL_BEHAVIOR`
already enforces "every NodeLabel is classified" via
`Record<NodeLabel, LabelBehavior>`, but the dispatch-table factory
populated its Map with manual `table.set(...)` calls that TypeScript
could not correlate back to the `'dispatch'` classification. Add a
type-level `DispatchLabel` extracted from `LABEL_BEHAVIOR` via a
conditional mapped type, and build the table from an object literal
that satisfies `Record<DispatchLabel, RegistrationHook>`. Adding a new
dispatch-classified label without wiring it to a hook now fails the
build with a named-key error — no more silent no-op hooks.

F#7 — Tier 3 dedup fast-path via MethodRegistry.hasFunctionMethods.
The Set-based dedup between callableDefs and methodDefs is only needed
when a Python/Rust/Kotlin class method (emitted as Function+ownerId by
the worker) lands in both indexes. For TS/Java/C#/C++/Ruby-only repos
— where the two indexes are disjoint by construction — the dedup was
pure overhead on every global-tier hit. MethodRegistry now tracks
whether any Function-typed def was ever registered, and resolution-
context branches Tier 3 into a concat-only fast path when that flag
is false. Slow path with dedup survives unchanged for mixed-language
repos.

New tests pin the invariants: hasFunctionMethods flag transitions,
Method/Constructor orphan fallback, Property non-fallback, and the
MethodRegistry clear() reset. Full test suite green (5756 tests).

* refactor(model): close remaining P3 review findings + coverage gaps

Address the remaining review items in one batch.

Production refactors:

- Rename classHook → classLikeHook (M05). The hook handles Class /
  Struct / Interface / Enum / Record / Trait; the vocabulary used in
  surrounding docs and the behavior-group table is "class-like". The
  rename makes the code match the taxonomy without forcing readers
  through a mental glossary.

- Extract MAX_BINDING_CHAIN_DEPTH constant in resolution-context.ts
  and document it as a known silent false-negative source (ADV-003).
  Five hops cover the common TypeScript monorepo pattern; raising the
  cap is a one-line change if a real repo exceeds it. walkBindingChain
  consumes the constant so the 5 magic number no longer floats free.

- Replace defs.filter() allocation in MethodRegistry.lookupMethodByOwner
  with a two-pass streaming count + conditional materialization
  (PERF-04). Pure-match and pure-reject arity paths now skip the
  filtered-array allocation entirely; only the discriminating case
  (at least one match AND at least one rejection) pays it.

- Rewrite NOOP_SYMBOL_TABLE in parse-worker.ts and NOOP_SYMBOL_TABLE_SEQ
  in parsing-processor.ts to implement all six SymbolTableReader
  methods (ADV-005). The `as unknown as SymbolTableReader` cast is
  removed in favor of a direct SymbolTableReader annotation, so future
  additions to the interface surface as compile errors on the stubs
  instead of silently falling through.

- type-env.ts getCallableUnionCount and getFirstCallable now take
  `model: SemanticModel` as an explicit argument instead of reaching
  into the enclosing `model!` non-null assertion (KT-003). Callers
  enter via an `if (model)` guard and pass the narrowed reference, so
  the non-null precondition is visible at the type level and the
  closures cannot be accidentally extracted into a context without
  the guard.

- Tier 3 dedup in resolution-context.ts now covers all four index reads
  (classDefs, implDefs, callableDefs, methodDefs) via a pushUnique
  helper (C-03). Previously classDefs and implDefs were spread directly
  without dedup; any theoretical nodeId collision would have produced
  duplicates in globalDefs.

Test infrastructure:

- Extract makeDef / makeMethod factory helpers into
  test/unit/model/helpers.ts (T-07). The four registry/table test
  files now import the shared helper and specialize with overrides,
  removing ~25 lines of duplicated boilerplate and creating a single
  point of maintenance.

New test coverage:

- T-01: c3 BFS fallback — cyclic Python hierarchy that fails c3
  linearization and must fall back to heritageMap.getAncestors() BFS
  order. Added to the lookupMethodByOwnerWithMRO describe block.

- T-02: Tier 2a-named precedence — verifies the binding chain walker
  fires before Tier 2a import-scoped when an aliased import
  `import { User as U } from B` competes with a raw same-name Tier 2a
  hit. Also pins Tier 1 same-file precedence over Tier 2a-named.

- T-03: Tier 3 Function+ownerId dedup — end-to-end test that a Python
  class method emitted as `Function + ownerId` yields exactly ONE Tier
  3 candidate (not two). Companion test pins the fast-path branch for
  hasFunctionMethods === false repos.

- T-06: walkBindingChain guards — circular re-export detection,
  depth-cap exceeded drop, and boundary case at exactly
  MAX_BINDING_CHAIN_DEPTH hops resolving successfully.

All tests added to a new test/unit/model/resolution-context.test.ts
dedicated to ResolutionContext.resolve() tier-precedence invariants.

Full suite: 5708 passing (minus the known Windows LBUG lock flake
that passes in isolation).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-12 01:06:55 +01:00
smTheApex
75635638b1
feat(csharp): capture interface-to-interface heritage (#789)
The C# tree-sitter query set only matched `base_list` on
`class_declaration`, so interfaces extending other interfaces
(`interface IFoo : IBar`) were never captured as heritage edges.

This broke transitive interface implementation chains. For example,
given:

    interface IBase { }
    interface IFoo : IBase { }
    class MyClass : IFoo { }

only `MyClass -> IFoo` was emitted, and the `IFoo -> IBase` edge was
silently dropped. Any analysis that relies on walking the full
interface inheritance chain (e.g. "which classes implement IBase?")
therefore returned incomplete results.

This patch adds two new query patterns mirroring the existing
class_declaration heritage patterns, but targeting
`interface_declaration`:

    (interface_declaration name: (identifier) @heritage.class
      (base_list (identifier) @heritage.extends)) @heritage
    (interface_declaration name: (identifier) @heritage.class
      (base_list (generic_name (identifier) @heritage.extends))) @heritage

The existing heritage-processor pipeline already handles these
captures correctly once the query emits them, so no changes are
needed outside of tree-sitter-queries.ts.

Testing:
- New fixture `csharp-interface-heritage/` covering:
    * interface : interface  (single base)
    * interface : interface, interface  (multiple bases)
    * class : interface (where that interface derives from others)
- 6 new test cases in test/integration/resolvers/csharp.test.ts
  asserting exactly 4 IMPLEMENTS edges and 0 EXTENDS edges for the
  fixture.
- Full C# resolver suite: 175/175 passing, no regressions.

Co-authored-by: Prota100 <Prota100@users.noreply.github.com>
2026-04-11 17:41:51 +01:00
Mr. WorldwideBrown
6d9ec1009e
fix: load VECTOR extension during DB init for semantic search (#782)
* fix: load VECTOR extension during DB init for semantic search

The VECTOR extension was only loaded inside the embedding generation

pipeline (createVectorIndex). On a fresh gitnexus serve session,

semantic and hybrid search failed because QUERY_VECTOR_INDEX was

unknown.

Now loads the VECTOR extension alongside FTS during database

initialization in both the single-connection and pool-based paths.

Fixes #766

* fix: reset vectorExtensionLoaded on DB close and retry paths

The vectorExtensionLoaded flag was not being reset in closeLbug() or
the busy-retry cleanup path in withLbugDb(). This caused the VECTOR
extension to not be re-loaded after a close+re-init cycle, breaking
semantic search on reconnection.

Also resets shared.ftsLoaded and shared.vectorLoaded in the pool
adapter closeOne() for external DB entries, preventing stale extension
state when the pool is re-opened.

Adds integration tests covering vector extension loading, idempotency,
and state reset on both close and busy-retry paths.

* fix: set ftsLoaded flag in initLbugWithDb to avoid redundant extension reloads

* fix: set shared.vectorLoaded flag in initLbugWithDb to avoid redundant reloads
2026-04-11 12:17:40 +01:00
Copilot
ab956f113c
feat(SM-15): Wire BindingAccumulator into processCallsFromExtracted for cross-file return type propagation (#763)
* Initial plan

* Initial setup - Phase 9 BindingAccumulator cross-file return type wiring

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7cee6490-090d-4714-8cb5-a704168ff47a

* feat(SM-15): wire BindingAccumulator into processCallsFromExtracted for Phase 9 cross-file return type propagation

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7cee6490-090d-4714-8cb5-a704168ff47a

* fix(SM-15): address all PR #763 review findings

Performance (R1)
- Changed _fileScopeByFile from Map<string, [string,string][]> to
  Map<string, Map<string,string>>. fileScopeGet(filePath, name) is
  now O(1) — replaces the O(n) linear scan + defensive-copy alloc
  that ran once per ConstructorBinding entry. fileScopeEntries()
  reconstructs tuples from Map.entries() for backward compat.
- Updated finalize() dev-mode invariant to compare deduplicated Map
  size rather than raw array length (Map.set deduplicates same-name).

Lifecycle (R2)
- Documented that Phase 9 intentionally reads pre-finalize because
  finalize() cannot move before both the worker consumer (line 984)
  AND the sequential-path writer (line 1061). Pre-finalize reads are
  safe because finalize() is write-lock-only with no side effects.
  Replaced the ambiguous "populated but not yet finalized" comment
  with the full lifecycle ordering explanation.

Sequential-path parity (R3)
- Wired bindingAccumulator into processCalls at line 797 (sequential
  path) so verifyConstructorBindings gets the Phase 9 fallback.
- Added bindingAccumulator parameter to processAssignmentsFromExtracted
  signature and wired it at the pipeline.ts call site (line 1026).
- Both paths now produce identical Phase 9 behavior for the same code.

Tracking comments (R4)
- Added "Overlapping mechanism (N of 3)" cross-references at:
  1. buildImportedReturnTypes (~line 109)
  2. collectExportedBindings (~line 168)
  3. Phase 9 fallback in verifyConstructorBindings (~line 563)
  Each links to the other two and notes future unification.

Language coverage (R5)
- Added 5 new Phase 9 integration test suites in cross-file-binding.test.ts:
  JavaScript, C++, C#, PHP, Ruby. Each uses the existing fixture
  directories and asserts getUser() → User → user.save() resolves.
  Total cross-file binding tests: 52 (was 37).

Quality asymmetry (R6)
- Added inline comment at the Phase 9 fallback noting worker-path
  entries are Tier 0/1 only and that binding accuracy is structurally
  lower for large repos where the worker path dominates.

Tests (+21 new)
- 6 fileScopeGet unit tests (happy path, unknown file/name, mixed
  scopes, post-dispose, duplicate varName last-write-wins)
- 15 integration tests across 5 new language suites

Verification
- tsc --noEmit clean
- 3147 unit tests pass (+6 new)
- 52 cross-file binding integration tests pass (+15 new)
- 1766 resolver integration tests pass
- Zero regressions

Plan: docs/plans/2026-04-10-001-fix-sm15-review-findings-plan.md
Review: https://github.com/abhigyanpatwari/GitNexus/pull/763#issuecomment-4220354242

* fix(SM-15): gate accumulator fallback on resolution tier and fix sequential file-order dependency

Two Codex adversarial reviews identified medium-severity bugs in the Phase 9
BindingAccumulator fallback:

1. Local-first violation: the fallback fired regardless of whether ctx.resolve()
   found same-file candidates, letting an imported callee shadow a local one
   and produce false CALLS edges. Fixed by gating on tiered.tier !== 'same-file'
   and callableDefs.length <= 1.

2. Sequential file-order dependency: processCalls flushed and verified per-file,
   so consumer files processed before their providers missed accumulator bindings.
   Fixed by splitting into a flush pre-pass (all files) then a resolution loop,
   mirroring the worker path's "all appends before any reads" pattern.

Also adds 11 consumer-before-provider integration test fixtures (one per
supported language) and 4 unit tests for tier gating edge cases.

* refactor(SM-15): eliminate duplicated prepare logic in processCalls two-pass split

Replace the duplicated pre-pass + legacy-path code (parse → query → heritage
→ TypeEnv → exports) with a single preparation loop followed by a resolution
loop. Both paths now share the same preparation code — the only conditional
is the accumulator flush.

Side benefit: globalParentMap is now fully populated before any resolution
runs, improving cross-file isSubclassOf accuracy regardless of file order.

Net -118 lines (226 removed, 108 added).

* fix(SM-15): address PR #763 third-pass review findings

1. Update stale dispose() JSDoc — remove forward-reference to Phase 9
   wiring that is now complete; document actual consumers.

2. Add processAssignmentsFromExtracted Phase 9 unit test — verifies the
   accumulator fallback produces ACCESSES write edges when the SymbolTable
   has no returnType for the callee.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-10 10:29:31 +01:00
Copilot
bb68cc1eb0
Extract resolveMemberCall from resolveCallTarget (SM-11) (#744)
* Initial plan

* feat(SM-11): extract resolveMemberCall from resolveCallTarget

- Create resolveMemberCall(ownerType, methodName, currentFile, ctx, heritageMap?)
  that uses owner-scoped + MRO resolution only (no fuzzy lookup)
- resolveCallTarget delegates member calls (D0 path) to resolveMemberCall
- walkMixedChain uses resolveMemberCall for owner-scoped member-call resolution
- Add 7 unit tests for resolveMemberCall covering direct, inherited, MRO,
  null cases, and confidence tier assertions
- Export resolveMemberCall for external use

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3b7889a9-5f2f-4572-8904-45084210f10d

* fix(SM-11): address PR #744 review

Blocking fixes:

- B1: Revert unrelated package-lock.json gitnexus-shared addition

- B2: Document confidence-tier semantic change on resolveMemberCall

Performance / coupling fixes:

- S1: walkMixedChain now calls resolveMethodByOwner directly (hot path) to avoid throwaway ResolveResult allocation per chain step

- S2: Thread tier from resolveMethodByOwner via { def, tier } tuple; eliminates double ctx.resolve

Alignment with semantic-model plan (Phase 3 target):

- resolveMethodByOwner now iterates ALL class-like candidates from ctx.resolve, deduplicating matches by nodeId. Absorbs D4's ownerId-filtering into the owner-scoped path.

- Handles homonym classes (two Users in different files) without falling through to D1-D4 fuzzy widening

- Shared-ancestor MRO walks automatically dedup (both homonyms walk to same base method)

- Unified direct-vs-MRO lookup under a single canWalkMRO check

Tests added:

- T1: Three D0 skip-condition tests via new _resolveCallTargetForTesting internal export (overloadHints, preComputedArgTypes, hasActiveModuleAlias)

- T2: Rust qualified-syntax null test (trait-inherited method) + direct impl control

- T3: C++ leftmost-base diamond inheritance test

- B2 lock-in: cross-file class tier assertion

- Homonym disambiguation: only-one-owns-method, both-own-method ambiguity, shared-ancestor MRO convergence

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3014 passed

- vitest run test/integration/resolvers/: 1746 passed

* test(SM-11): address second PR #744 review round + per-language integration tests

Review fixes (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4211877593):

P1 (Performance): Replace Map allocation in resolveMethodByOwner with a firstDef+ambiguous flag pattern. Zero allocation for the common single-candidate case on the hot path — the previous Map approach allocated on every member call regardless of whether deduplication was needed.

P2 (Test gap): Strengthen the module-alias D0 skip test with a homonym fixture (two Users in different files). Previously the test passed whether or not D0 was actually bypassed; the new version proves D0 must be skipped by showing that resolveMemberCall directly returns null (ambiguous) but D1-D4 with alias narrowing picks the right one. Also fixes the underlying D2-vs-alias widening interaction: when filteredCandidates was narrowed by module-alias disambiguation, D2 no longer widens back to the full fuzzy pool (introduces aliasNarrowed boolean flag).

L1 (Language coverage): Add C# and Kotlin implements-split tests at the resolveMemberCall layer.

L2 (Maintainability): Export OverloadHints as @internal so the test can use a direct cast instead of fragile Parameters<...> type inference.

Per-language integration tests:

- rust-child-extends-parent: Direct impl method resolution via D0 (with honest documentation of the trait-method-as-Function gap that is Phase 5 / SM-16 scope)

- java-interface-default-method: User implements Validator with default method resolved via implements-split MRO

- csharp-interface-default-method: Same pattern for C# 8.0+ default interface methods

- kotlin-interface-default-method: Same pattern for Kotlin interfaces with default implementations

- python-multi-level-mro: 3-level C3 linearization (Grandparent ← Parent ← Child)

- cpp-diamond-inheritance: Classic diamond (Base ← A, B ← Derived) via leftmost-base MRO

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3015 passed

- vitest run test/integration/resolvers/: 1763 passed (+17 new per-language tests)

* fix(SM-11): Codex adversarial review corrections + deeper D0 fixes

Addresses the three high-severity findings from the Codex adversarial review of PR #744 (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4212075120), plus four deeper fixes discovered during regression triage. All discovered issues are now addressed end-to-end rather than papered over with tail-return fallbacks.

Codex review findings:

R1 (C++ diamond): The cpp-diamond-inheritance fixture used non-virtual inheritance, which is genuinely ambiguous in real C++ (two Base subobjects). Changed A and B to use 'virtual public Base' so there's a single shared Base subobject and d.method() is an unambiguous call that the leftmost-base MRO walk correctly resolves.

R2 (C# default-interface): The csharp-interface-default-method fixture called user.Validate() via a User-typed variable, but C# does not inherit default interface methods as callable class members — the call is only valid through an interface-typed variable. Changed App.cs to 'IValidator user = new User(...)' which is the idiomatic dispatch pattern.

R3 (resolveCallTarget tail-return): When D1-D4 receiver filtering produced zero file-matched and zero owner-matched candidates for a member call, the function fell through to the permissive single-candidate tail return — silently emitting CALLS edges for methods that don't belong to the receiver. Added an explicit null-route inside the D1-D4 block that fires only when both filters yielded 0.

R4 (Rust negative assertion): Added the c.trait_only() negative integration test in rust.test.ts demonstrating that direct member calls on Rust structs do not walk trait ancestry. The test now passes because of R3 (previously fell through to the tail return).

Regression triage discoveries:

1. D0 was dead code on the sequential pipeline. The sequential path sets overloadHints for every call regardless of whether the method is overloaded, and the original D0 skip condition '!overloadHints && !preComputedArgTypes' was therefore always false. The Java/C#/C++ SM-9/SM-10 inheritance tests were passing ONLY via the tail-return fallback. Fix: narrow the skip to 'overloadHints && filteredCandidates.length > 1' — skip D0 only when there are actually multiple candidates that need overload disambiguation.

2. lookupMethodByOwner couldn't disambiguate arity-differing overloads (e.g. C++ greet() vs greet(string)). With D0 now firing on the sequential path, same-name/different-arity overloads would collapse to an arbitrary first pick. Fix: added an optional argCount parameter to lookupMethodByOwner + lookupMethodByOwnerWithMRO that filters the overload set by parameterCount/requiredParameterCount before the returnType dedup.

3. Python and Rust class methods are captured as Function nodes (not Method) with ownerId set to the class. The methodByOwner index only accepted 'Method' and 'Constructor' types, so Python class methods and Rust trait methods were invisible to D0. Fix: extended the methodByOwner indexing condition to include 'Function' when ownerId is set. This also unlocks the Rust trait-method negative assertion by ensuring the qualified-syntax MRO strategy has something to return null for.

4. D0 was being skipped when a local variable shadowed an imported module name (Python 'from models.c import C; c = C()' creates both a module alias 'c → models/c.py' AND a typed local 'c'). Fix: the D0 skip now gates on 'aliasNarrowed' (a new boolean tracking whether the alias block actually narrowed filteredCandidates) instead of 'hasActiveModuleAlias'. If the method isn't in the aliased module, the receiver is a typed local variable and D0 should run.

5. PHP trait walk missed the HasTimestamps trait because lookupClassByName did not include 'Trait' type. buildHeritageMap uses lookupClassByName to resolve parent names, so 'BaseModel use HasTimestamps' was failing to register an ancestor edge for BaseModel → HasTimestamps. Fix: added 'Trait' to CLASS_TYPES. The trait is now a valid class-like type for heritage resolution (PHP use, Rust impl Trait for Struct, Scala traits).

Test updates:

- Updated the 'no heritageMap' unit test in call-processor.test.ts to assert the correct null-route behavior instead of the old tail-return fallback.

- Added a new unit test asserting Trait inclusion in the class set.

- Updated the 'does NOT include other type-like labels' test to remove Trait from its rejection set.

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3016 passed (+1 new Trait inclusion test)

- vitest run test/integration/resolvers/: 1764 passed (+1 new Rust negative assertion)

- Zero regressions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-09 09:52:12 +01:00
Roshan Warrier
d6debf3324
fix(symbol-table): index constructors in methodByOwner (#753)
Co-authored-by: txhno <198242577+txhno@users.noreply.github.com>
2026-04-09 08:26:15 +01:00
Copilot
d9ba9aa998
SM-10: Add MRO fast path before D2 fuzzy widening in resolveCallTarget (#741)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
* Initial plan

* Add MRO fast path before D2 fuzzy widening in resolveCallTarget

When receiverTypeName is known, try resolveMethodByOwner (owner-scoped
+ MRO lookup) before falling back to the expensive lookupFuzzy in D2.
This short-circuits cross-file member call resolution for the common
non-overloaded case.

The fast path is skipped when overload disambiguation hints are
available (overloadHints or preComputedArgTypes) to avoid picking the
wrong overload for same-return-type overloaded methods.

Passes heritageMap to resolveCallTarget from all 4 call sites:
- Language seed path (processCalls)
- Sequential path (processCalls)
- walkMixedChain fallback
- Worker path (processCallsFromExtracted)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9e49521f-2472-47bc-96e9-be4a46b073f0

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(SM-10): address PR #741 review

Correctness:
- Module-alias guard for D0. When call.receiverName matches an active
  entry in ctx.moduleAliasMap for the current file, D0 is now skipped
  and resolution falls through to D1-D4 which respects the
  alias-narrowed candidate pool. Prevents a homonymous class in a
  different file from being picked by ctx.resolve(receiverTypeName)
  inside resolveMethodByOwner. New unit test pins the contract.

Unit tests (call-processor.test.ts — 3 new):
- D0 hit: child.parentMethod() resolves via MRO walk when
  heritageMap is provided.
- D0 skipped: same scenario still resolves via D1-D4 when heritageMap
  is undefined (backward-compat guard).
- Module-alias guard: two files both define class User with a save()
  method; 'import auth_mod as auth' in app.py must resolve
  auth.user.save() to auth_mod.py, not user_mod.py.

Integration language coverage (+3 fixtures/tests):
- swift-child-extends-parent — first-wins, gated on swiftAvailable.
- ruby-child-extends-parent   — first-wins.
- php-child-extends-parent    — first-wins (uses ParentClass since
  'Parent' is a PHP reserved word).

* test(SM-10): address second PR #741 review round

Unit tests (call-processor.test.ts, +2 new):
- overloadHints guard: Java source with two same-return-type overloads
  method(int) and method(String), int added first so lookupMethodByOwner
  would return it. processCalls auto-generates overloadHints for Java,
  forcing D0 to be skipped. o.method("hello") must resolve to
  method(String) via literal-inferred disambiguation.
- preComputedArgTypes guard: worker-path equivalent via
  processCallsFromExtracted with ExtractedCall.argTypes=['String'].
  Same two overloads, same correctness guarantee.

Integration tests (+2 fixtures + test blocks):
- go-child-extends-parent    — struct embedding, first-wins
  (Go structs are labeled 'Struct' not 'Class' in GitNexus).
- dart-child-extends-parent  — extends, first-wins, gated on
  dartAvailable like other Dart tests.

Documentation:
- Expanded the fallthrough comment in resolveMethodByOwner to clarify
  that unknown-extension paths land on plain lookupMethodByOwner
  without an ancestor walk, and that D1-D4 still runs on D0 miss.

* test(SM-10): D0 miss with heritageMap present falls through to D1-D4

Closes the last remaining gap from PR #741 review round 3. The existing
'D0 skipped' test only covered the heritageMap=undefined case, leaving
the miss-with-heritageMap path implicitly covered by integration tests
only. This adds a focused unit test where:

- Class Obj has a method doWork findable via tiered resolution
  (import-scoped) but intentionally NOT registered in methodByOwner
  (no ownerId), so lookupMethodByOwner misses.
- heritageMap is provided but built from an empty heritage array, so
  getAncestors(class:Obj) returns []. The MRO walk yields no parents.
- lookupMethodByOwnerWithMRO therefore returns undefined → D0 miss.
- D1 resolves the receiver type; D2 widens via lookupFuzzy;
  D3 file-filter picks the single matching candidate.
- A CALLS edge must still be emitted — D0 miss must not swallow
  the call.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-08 23:24:07 +01:00
Copilot
c19e76a4a3
feat(SM-9): Add lookupMethodByOwnerWithMRO using HeritageMap (#740)
* Initial plan

* feat(SM-9): add lookupMethodByOwnerWithMRO with HeritageMap parent chain walking

- Export c3Linearize from mro-processor.ts for reuse
- Add lookupMethodByOwnerWithMRO in call-processor.ts with MRO strategy support
- Update resolveMethodByOwner to fall back to MRO walk when HeritageMap available
- Thread heritageMap through walkMixedChain for chain resolution
- Add 10 unit tests covering all acceptance criteria

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc58249b-42f1-45a9-89fb-e3917e4d0171

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat(SM-9): add Java integration test with class Child extends Parent fixture

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc58249b-42f1-45a9-89fb-e3917e4d0171

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docs: address code review comments on MRO strategy documentation

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc58249b-42f1-45a9-89fb-e3917e4d0171

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* perf(SM-9): address PR #740 review comments

- Eliminate double direct lookup in resolveMethodByOwner: delegate
  straight to lookupMethodByOwnerWithMRO when a HeritageMap is
  available (the MRO helper already does the direct lookup before
  walking ancestors). Fallback path handles the no-HeritageMap case.
- Memoize C3 linearization per HeritageMap via a WeakMap keyed cache.
  HeritageMap is immutable after build, so C3 results are stable for
  its lifetime; WeakMap lets the cache auto-drain when the HeritageMap
  is GC'd. Null sentinel caches linearization failures so cyclic
  hierarchies are not reprocessed. Eliminates per-call buildParentMap +
  c3Linearize on Python codebases.
- ancestors variable typed as readonly to accept the cached result
  without copying.
- Add four missing MRO unit tests: Kotlin implements-split, C#
  implements-split, JavaScript first-wins (separate provider from TS),
  and C++ leftmost-base diamond (first diamond test for C++).

* fix(SM-9): CI prettier + address PR #740 follow-up review

- Fix CI prettier failure in test/integration/resolvers/java.test.ts
  (auto-formatted — was introduced in 37563a31 before my first fix
  commit but had not been caught locally).
- Pin caller on the SM-9 Java integration test (parentMethodCall.source
  === 'run') so a regression that misattributes the CALLS edge fails.
- Add two implements-split unit tests:
  * Ambiguous default from two interfaces → BFS first-wins. Pins the
    contract that lookupMethodByOwnerWithMRO returns a defined result
    (full ambiguity detection is deferred to computeMRO graph pass).
  * Class method precedence over interface default: Child extends Base
    implements IFoo where both define handle() — documents that BFS
    visits the extends edge first, matching Java's class-wins rule.
- Add @internal JSDoc on lookupMethodByOwnerWithMRO clarifying it is
  exported only for testing; resolveMethodByOwner is the proper entry
  point for callers.

* test(SM-9): per-language integration fixtures and tests for inherited method resolution

Extends the SM-9 integration coverage beyond Java with six new
child-extends-parent fixtures, one per MRO strategy:

- python-child-extends-parent       → C3 strategy
- typescript-child-extends-parent   → first-wins
- javascript-child-extends-parent   → first-wins (separate provider)
- kotlin-child-extends-parent       → implements-split
- csharp-child-extends-parent       → implements-split
- cpp-child-extends-parent          → leftmost-base

Each fixture follows the java-child-extends-parent pattern:
- Parent class with a single method
- Child class extending Parent, no override
- App class/function that instantiates Child and calls
  the parent method — exercises the full ingestion pipeline,
  HeritageMap construction, and lookupMethodByOwnerWithMRO walk.

For every fixture the matching integration test asserts:
- Parent and Child classes are detected
- Child → Parent EXTENDS edge is emitted
- The parent-method call resolves to the correct target file
- The caller is pinned (source === 'run' / 'Run') to catch
  edge misattribution regressions

Rust is intentionally omitted — its qualified-syntax strategy
returns undefined from lookupMethodByOwnerWithMRO by design, so
there is no inherited-method resolution to assert against.

All 1739 integration resolver tests pass (+18 new SM-9 tests
across 6 languages).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-08 20:46:45 +01:00
Deepak Chauhan
be2401061e
[cli] Add qualified class lookups to SymbolTable (#716)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
2026-04-07 22:57:18 +01:00
Gergő Magyar
cb772b9e29
feat: lookupMethodByOwner index for O(1) cross-class chain resolution (#665)
Add eagerly-populated methodByOwner index to SymbolTable, keyed by
ownerNodeId\0methodName. Used by walkMixedChain as a fast path for
resolving intermediate method calls in cross-class chains like
user.getAddress().getCity().getZipCode(), avoiding expensive fuzzy
lookups when the owner type is already known.

Handles overloaded methods: returns the first match when all overloads
share the same returnType, undefined when return types differ (ambiguous).

- Add lookupMethodByOwner to SymbolTable interface + implementation
- Add resolveMethodByOwner helper in call-processor.ts
- Add fast path in walkMixedChain before resolveCallTarget fallback
- Add Java cross-class chain fixture + 6 integration tests
- Add 148 unit tests for methodByOwner index behavior
2026-04-06 10:20:04 +01:00
Gergő Magyar
5a7c0fdbb1
feat: same-arity overload disambiguation via type-hash suffix (#651) (#658)
* feat: same-arity overload disambiguation via type-hash suffix (#651)

Add ~type1,type2 suffix to Method/Constructor node IDs when same-arity
overloads with different parameter types exist in the same class. Also add
$const suffix for C++ const-qualified method overloads via new isConst field.

Key changes:
- typeTagForId() detects same-arity collisions and appends ~typeTag
- constTagForId() detects const/non-const collisions and appends $const
- TS/JS excluded from type-hashing (overload signatures collapse to impl body)
- Sequential findEnclosingFunction fixed: falls through on ambiguous same-class
  candidates instead of picking first; fallback path includes typeTag + constTag
- Per-call-site integration tests across Java, C#, Kotlin, C++, TypeScript
- Cross-file + chain resolution tests for all 5 languages
- C++ isConst extraction via tree-sitter type_qualifier in function_declarator

1710 integration + 18 unit tests pass.

* fix: preserve generic/template args in type-hash, perf + type safety fixes

- Add rawType field to ParameterInfo preserving full type text (vector<int>)
  while type stays simplified (vector). typeTagForId uses rawType for tags.
- Populate rawType in all 11 language method extractors
- Add buildCollisionGroups() to pre-group methods by name#arity (O(N) once
  per class instead of O(N) per method call)
- Cache method extraction in call-processor findEnclosingFunction fallback
- Fix null guards on getLanguageFromFilename in all findEnclosing paths
- Tighten SKIP_TYPE_HASH_LANGUAGES to ReadonlySet<SupportedLanguages>
- Document ID stability invariant on first overload introduction
- C++ integration tests: template overloads (vector<int> vs vector<string>),
  cross-file template + chain resolution, out-of-class method definitions

1718 integration + 20 unit tests pass.

* fix: add rawType to method-extraction unit test assertions

All 26 parameter .toEqual() assertions in method-extraction.test.ts
needed the new rawType field added to match ParameterInfo schema change.

* perf: cache tempMap/groups per class, consolidate extractFromNode

- Cache derived method map + collision groups per classNode.id in
  parsing-processor (avoids rebuild per method in same class)
- Replace per-call extractFromNode with cached class extraction +
  funcName:line lookup in call-processor fallback (avoids AST walk
  per call site)
- Remove dead clearEnclosingFunctionCache export, fix JSDoc

* test: add sequential-path integration test for same-arity overloads

Add skipWorkers option to PipelineOptions to force sequential parsing.
New test suite verifies type-hash disambiguation produces identical
results through the sequential path (parsing-processor + call-processor
findEnclosingFunction) as the worker path.
2026-04-05 21:51:55 +01:00
Gergő Magyar
0561d24efd
feat: METHOD_IMPLEMENTS edges, overload disambiguation, MethodExtractor unification (#574) (#642) 2026-04-04 18:41:47 +01:00
Gergő Magyar
63fc4c795f
feat: MethodExtractor configs for Python, PHP, Swift, Dart, Rust, Ruby (#624)
* feat: MethodExtractor configs for Python, PHP, Swift, Dart, Rust, Ruby with exhaustive integration tests

Add per-language MethodExtractionConfig for all remaining tree-sitter languages
(RFC #568 PR 2). Each config follows the established createMethodExtractor()
factory pattern — no new types, no parse-worker changes.

Configs:
- Python: @abstractmethod, @staticmethod/@classmethod, *args/**kwargs, type hints, _/__ visibility
- PHP: abstract/final/static keywords, PHP 8 #[] attributes, __construct/__destruct
- Swift: 5-level visibility, protocol-as-abstract, static/class methods, @ attributes
- Dart: _ convention visibility, abstract (no body), method_signature unwrapping
- Rust: pub visibility, &self receiver, trait_item + impl_item, #[] attributes
- Ruby: positional visibility via sibling-walk, singleton_method as static

Integration fixtures (18 directories) covering 3 resolution patterns:
- Method enrichment: parameterTypes, isAbstract, isFinal, annotations on graph nodes
- Overload dispatch: arity-based CALLS resolution via parameterTypes
- Abstract dispatch: abstract/concrete method distinction (Python, PHP, Rust, Swift)

Go deferred — requires factory changes for receiver-based method extraction.

Closes #571

* fix: address code review findings across 6 MethodExtractor configs

Fix all actionable items from the PR #624 deep-dive review:

Dart (critical — fixes 6 CI failures):
- isDartStatic: check children first, siblings as fallback
- isDartAbstract: handle declaration nodes for abstract methods
- extractSingleParam: detect required keyword as sibling token
- Add declaration to methodNodeTypes, mixin_declaration to typeDeclarationNodes
- Add member call query for variable assignments in tree-sitter-queries

Python:
- hasDecorator now matches dotted paths (e.g. @abc.abstractmethod)
- Fix version comment from ^0.23.6 to 0.23.4

PHP:
- Add enum_declaration to typeDeclarationNodes (PHP 8.1+)
- Add version comment for 0.23.12

Swift:
- Add isOverride using hasKeyword/hasModifier pattern

Rust:
- Fix version comment from ^0.23.2 to 0.23.1

Also: identifier fallback in generic.ts for mixin owner names,
Dart integration test label fix (Method vs Function), version
comment for tree-sitter-dart 1.0.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Dart extension_declaration and Ruby module_function support

Dart:
- Add extension_declaration to typeDeclarationNodes and extension_body
  to bodyNodeTypes — extension methods are now extracted into the graph
- Add extension_declaration and mixin_declaration to CLASS_CONTAINER_TYPES
  for HAS_METHOD edge resolution

Ruby:
- module_function now maps to visibility 'private' in extractRubyVisibility
- module_function methods marked isStatic via backward-walk in isStatic
- Override semantics: private/public after module_function resets isStatic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(go): Go MethodExtractor config with receiver-based extraction

Add Go as the 13th language with a per-language MethodExtractor config.
Go methods are top-level (not nested in struct bodies), so this adds
extractFromNode() to the MethodExtractor interface for direct method
node extraction without an enclosing class.

Config extracts:
- Name from field_identifier (methods) / identifier (functions)
- Return type including multi-return (first type from parameter_list)
- Parameters with variadic support
- Visibility via uppercase/lowercase convention
- Receiver type with pointer unwrapping (*User → User)
- isStatic for functions (no receiver)

Infrastructure:
- extractOwnerName optional hook on MethodExtractionConfig
- extractFromNode on MethodExtractor (factory auto-implements)
- Parse-worker uses extractFromNode when no enclosing class found
- method_declaration added to CLASS_CONTAINER_TYPES

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: method enrichment integration tests for 7 languages + TS abstract class fix

Add method-enrichment integration test fixtures and test blocks for
Go, C++, Java, Kotlin, TypeScript, JavaScript, and C#. Each fixture
tests: class detection, HAS_METHOD edges, EXTENDS edges, isAbstract,
isStatic, annotations, parameterTypes, and CALLS edge resolution.

Fixes found during testing:
- Remove method_declaration from CLASS_CONTAINER_TYPES (added for Go
  but broke Java/C# HAS_METHOD edge resolution — method_declaration
  is also Java's method node type)
- Add abstract_class_declaration query to TypeScript tree-sitter
  queries (was missing, so abstract classes were invisible to pipeline)

1699 integration tests pass across 20 test files, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: format typeDeclarationNodes array for better readability in PHP config

* fix: Go interface methods + Rust impl-for-Struct owner resolution

Go:
- Add method_elem to methodNodeTypes so interface method signatures
  are extractable as abstract methods
- Integration test: Animal interface detected, Speak isAbstract,
  CALLS edges from app.go

Rust:
- Add extractOwnerName to resolve impl Trait for Struct to the
  concrete Struct (not the Trait) — fixes method misattribution
- Fix findEnclosingClassId to generate Struct: label (not Impl:)
  for impl blocks so HAS_METHOD edges resolve to struct nodes
- Tighten abstract-dispatch test: assert SqlRepo owns find/save

generic.ts:
- Fix extractOwnerName fallback: when hook returns a value, skip
  both name-field and type_identifier scan (was overwriting result)

1703 integration tests pass, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: code review response — Rust impl label, Swift params, Dart async, sequential methodExtractor

Address code review findings from PR #624:

- ast-helpers: Rust `impl Trait for Struct` uses Struct label (matches existing
  graph node), plain `impl Struct` uses Impl label (matches definition.impl)
- swift: fix parameter type extraction (user_type not type_annotation), detect
  default values as function_declaration siblings, add version comment
- dart: isDartAsync now detects async*/sync* generators, add clarifying comment
  for declaration nodes in extension bodies
- python: correct isFinal comment (PEP 591 @typing.final exists, just not modeled)
- parsing-processor: port methodExtractor enrichment to sequential path so
  isAbstract/isStatic/visibility/annotations/isFinal populate on <15-file repos
- tests: remove silent `if (prop !== undefined)` guards, assert properties
  directly, fix label queries (Dart Method vs Function, Swift Method for protocol
  methods), add Rust HAS_METHOD sourceLabel tests, Swift parameterTypes tests,
  and Dart async/sync* integration tests with fixture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Rust grammar gap + qualified method IDs to resolve same-file collisions

Phase 1 — Rust grammar:
- Add function_signature_item query to RUST_QUERIES so abstract trait methods
  (fn speak(&self) -> String;) become graph nodes with isAbstract=true

Phase 2 — Qualified method IDs:
- findEnclosingClassInfo returns {classId, className} for AST-based class lookup
- Both parsing paths (sequential + worker) qualify method/property IDs with
  enclosing class: Method:file:ClassName.method instead of Method:file:method
- extractFuncNameFromSourceId handles ClassName.method format
- Fixes silent data loss when same-name methods in different classes shared a
  file (e.g., Animal.speak and Dog.speak both now exist as distinct graph nodes)

Test updates:
- Rust: abstract+concrete trait methods both verified, function count adjusted
- Python: static method disambiguation now emits 2 CALLS edges (correct — no
  more ID collision masking the second call)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: owner-aware resolution for qualified method IDs

Address Codex adversarial review findings after qualified ID change:

- findEnclosingFunction: disambiguate candidates by ownerId when multiple
  same-name methods exist in file; qualify fallback-generated IDs
- findEnclosingFunctionId (worker): qualify sourceIds with enclosing class
  name so CALLS source attribution matches definition-phase node IDs
- buildExportedTypeMapFromGraph: use lookupExactAll + nodeId match instead
  of lookupExactFull which returns first definition for bare name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: methodExtractor variadic arity, return type preservation, PHP abstract dispatch

Three bugs in the methodExtractor enrichment path broke 17 integration tests:

1. Variadic parameterCount: buildMethodProps and parse-worker set
   parameterCount = info.parameters.length even for variadic functions,
   causing arity filtering to reject valid calls. Now checks isVariadic
   and sets parameterCount = undefined (matching extractMethodSignature).

2. C++ bare `...` token: extractCppParameters only iterated named
   children, missing the unnamed `...` token in C-style variadics like
   log_entry(const char* fmt, ...). Added fallback scan of all children.

3. Return type stripping: All 11 language extractReturnType functions
   used extractSimpleTypeName() which strips generic parameters
   (List<User> → "List", Task<User> → "Task"). Changed to .text?.trim()
   to preserve full generic types needed for for-loop iterable resolution,
   async-await binding, and return-type inference.

Also fixes PHP abstract dispatch test that matched SqlRepository instead
of the interface due to ambiguous filePath.includes('Repository') filter,
and adds parent-walk fallback in PHP isAbstract for extractFromNode path.

* chore: remove plan and review artifacts from PR

* fix: address Round 4 review findings + infrastructure improvements

- Ruby: add singleton_class support for class << self methods (4 new tests)
- PHP: add enum_declaration to CLASS_CONTAINER_TYPES
- Dart: add mixin/extension labels to CONTAINER_TYPE_TO_LABEL
- Swift: add TODO for unverifiable struct/enum node types on Node 22
- C#: add grammar version comment (0.23.1)
- Ruby: fix version comment range to pin (0.23.1)
- Rust/ast-helpers: add cross-reference comments for impl_item duplication
- ast-helpers: document CLASS_CONTAINER_TYPES ↔ typeDeclarationNodes invariant
- generic.ts: replace Array.includes with Set for O(1) dedup in addNestedBodies
- Go/Python/Ruby: align isAbstract signature with 2-param interface contract
- CLAUDE.md: fix malformed backtick around gitnexus:start HTML comment
- parsing-processor: add per-class method extraction cache (eliminates O(N*M))
- ast-helpers: add scoped_type_identifier to impl_item resolution
- call-processor: add dev-mode warnings at silent candidates[0] fallbacks
- MCP context(): surface methodMetadata for Method/Function/Constructor nodes
- resources.ts: update schema to list all stored Method properties

* fix: singleton_class HAS_METHOD edge regression in findEnclosingClassInfo

singleton_class (class << self) was added to CLASS_CONTAINER_TYPES but
has no name field — its receiver `self` has node type 'self', not
'identifier'. findEnclosingClassInfo now walks up to the enclosing
class/module to inherit its name, matching ruby.ts:extractOwnerName.

Also fixes findEnclosingClassNode in parse-worker.ts to skip
singleton_class and return the actual class/module node.

Adds integration test assertions for from_habitat (class << self method):
HAS_METHOD edge from Animal, isStatic=true, parameterCount=1.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:11:31 +01:00
Abhigyan Patwari
5c4fca21c3
Merge pull request #626 from ivkond/feat/intra-repo-service-tracking-clean
[group] Intra-repo service communication tracking
2026-04-03 17:04:24 +05:30
Nguyen Hai Son
dd0f5eed7d
feat(vue): Vue SFC support + destructured call result tracking (#604)
* feat(vue): add Vue SFC (.vue) support for indexing

Vue Single File Components are now fully supported in the indexing pipeline.
The implementation extracts <script> / <script setup> blocks from .vue files
and parses them using the existing TypeScript tree-sitter grammar — no new
npm dependencies required.

Key changes:
- SFC script extractor: regex-based extraction of <script setup lang="ts">
  blocks with correct line offset mapping back to the .vue file
- Vue language provider: reuses TypeScript queries, type config, field
  extractors, and named binding extraction
- Import resolution: .vue added to EXTENSIONS so `import Foo from './Foo'`
  resolves to Foo.vue; Vue import resolver delegates to TS resolver for
  tsconfig path alias support
- Export detection: <script setup> top-level bindings are implicitly exported
- Template component detection: PascalCase tags in <template> emit CALLS edges
- Line offsets applied to all emitted positions (startLine, endLine, route
  lineNumbers, decorator positions) in both worker and sequential paths

Validated on a 3,553-file Vue project:
  Before: 24,693 nodes | 73,614 edges | 0 symbols from .vue
  After:  30,495 nodes | 112,324 edges | 5,213 symbols from .vue
          18,682 imports from .vue | 5,826 vue-to-vue imports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(typescript): track destructured call results in TypeEnv

Extend `extractPendingAssignment` to handle object destructuring from
function calls and await expressions:

  const { isMaker } = useUserRole()
  const { data } = await fetchData()
  const { name } = repo.getProfile()

Previously, only `const { x } = someVariable` (identifier RHS) produced
TypeEnv bindings. Call-expression RHS was silently skipped, leaving
destructured properties untracked.

The fix emits a synthetic `callResult` item plus N `fieldAccess` items
per destructured property, which the existing fixpoint resolver processes
in 2 iterations. No changes needed to type-env.ts, PendingAssignment
types, or call-processor — the existing infrastructure handles it.

Also extracts a `collectDestructuredFields` helper to share the
object_pattern property iteration logic between the identifier and
call-expression branches.

Note: Full property-type resolution requires the callee to have a
declared returnType in the SymbolTable. Arrow-function composables
without type annotations (common in Vue/React) won't resolve property
types until return-type inference is added in a future change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(vue): address PR review issues for Vue SFC support

- Extract duplicated isVueSetupTopLevel to vue-sfc-extractor.ts shared
  utility, removing identical copies from parse-worker.ts and
  parsing-processor.ts
- Fix VUE_BUILT_INS to be a superset of TS BUILT_INS by importing and
  spreading the TypeScript set, preventing spurious unresolved calls for
  standard built-ins (Symbol, BigInt, WeakMap, array methods, etc.)
- Add Vue template component CALLS edge resolution in both sequential
  and worker paths (call-processor.ts), matching PascalCase template
  tags against imported .vue file basenames via the import map
- Add integration test for template PascalCase CALLS edges
  (App.vue → Button.vue)
- Add integration test for isExported: false on non-setup <script>
  blocks (OldStyle.vue options API)
- Add comment explaining TEMPLATE_RE greedy regex behavior for nested
  template tags
- Fix stale language count comment (14 → 15) and remove dead code
  branch in test

Made-with: Cursor

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:18:55 +05:30
Chirag Nighut
e3d73a7aed
Java method reference (#622) 2026-04-02 15:16:35 +01:00
ivkond
255e3e79eb fix(group): address 4 HIGH-priority issues from PR #626 review
1. Path traversal via group name — add validateGroupName() with regex
   [a-zA-Z0-9][a-zA-Z0-9_-]*, called in getGroupDir (defense in depth)

2. gRPC proto regex can't handle nested braces — replace serviceRe with
   extractServiceBlocks() brace-depth counter (init depth=1, skip
   malformed protos)

3. Service boundary detector directory exclusions — add EXCLUDED_DIRS
   set (vendor, target, build, dist, __pycache__, .venv, venv, .tox,
   .mypy_cache, .gradle, .mvn, out, bin) replacing inline node_modules

4. Double-close of LadybugDB pools — remove blanket closeLbug() from
   cli/group.ts; sync.ts per-id cleanup is sufficient

Tests: 22 new tests across 5 files. Full suite: 4706 passed, 0 failed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:55:33 +03:00
ivkond
4fed097abb feat(group): add sync pipeline, CLI, MCP tools, and monorepo fixture
Wire extractors into the sync pipeline with service boundary detection.
GroupService provides high-level API for all group operations.

- Sync pipeline: orchestrates extraction (HTTP, gRPC, topics) with
  service boundary assignment and exact matching
- GroupService: groupList, groupSync, groupContracts, groupQuery,
  groupStatus (groupImpact deferred to cross-repo follow-up PR)
- CLI: group create/add/remove/list/sync/contracts/query/status
- MCP tools: group_list, group_sync, group_contracts, group_query,
  group_status
- Monorepo fixture: 3 services (auth/orders/gateway) connected via
  gRPC + Kafka + HTTP — all intra-repo cross-links discovered
- Documentation: CLI commands and MCP tools added to both READMEs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 00:40:31 +03:00
Gergő Magyar
ba5de0bde4
feat(cpp): C/C++ MethodExtractor config with pure virtual detection (#617)
* feat(cpp): C/C++ MethodExtractor config with pure virtual detection (#572)

- Pure virtual (= 0) detected as isAbstract via token scanning
- virtual/final/override via hasKeyword and virtual_specifier children
- Access specifier visibility via backward sibling walk (public:/private:/protected:)
- Pointer/reference parameter types extracted correctly
- Constructor and destructor support via declaration node type
- Static detection via storage_class_specifier
- 16 new tests covering all acceptance criteria

* fix(cpp): isVirtual infers from override/final + out-of-class resolution

- isVirtual returns true for override/final methods (C++ mandates these
  are virtual)
- Add findClassNodeByQualifiedName to parse-worker: resolves Foo::bar()
  back to the Foo class declaration for method extractor enrichment
- Handles pointer/ref return types, constructors, destructors
- Integration test for virtual/static/constructor inline methods
- 233 unit+integration tests pass, 97 C++ resolver tests pass

* fix(cpp): address review — deep pointers, templates, unions, trailing returns

- Fix extractParamName: recursive unwrap for int** ptr → "ptr" (not "**ptr")
- Fix findFunctionDeclarator: recursive unwrap for multi-level pointer chains
- Template methods: generic extractor unwraps template_declaration to inner node
- union_specifier: added to typeDeclarationNodes, visibility defaults to public
- Trailing return type: auto foo() -> T now extracts T instead of "auto"
- Fix version comment: ^0.22.4 → ^0.23.4 to match package.json
- 4 new tests: double pointer params, template methods, union methods, trailing returns

* fix(cpp): template method visibility + union isTypeDeclaration test

extractCppVisibility now walks from the template_declaration parent
when the node is wrapped by a template, restoring correct access-
specifier resolution for templated class methods.

Also adds missing isTypeDeclaration assertion for union_specifier and
expands the template method test with explicit visibility checks.

* fix(cpp): address deep gap analysis review findings

- findClassNodeByQualifiedName: recursive pointer/reference
  declarator unwrap, fixing out-of-class linking for deep pointer
  return types (e.g. int** Foo::bar())
- findClassNodeByQualifiedName: recurse into namespace_definition
  blocks so namespace-wrapped classes resolve correctly
- Suppress = delete / = default special members from extraction
  via delete_method_clause / default_method_clause node detection
- Update known-gaps: namespace-wrapped classes, const-overload collapse
- Add tree-sitter-c version comment for consistency
- toBeFalsy() → toBe(undefined) for precise isVirtual assertion
- Tests: = delete, = default, = 0 non-regression, operator overloads,
  deep pointer return types, default visibility (class vs struct),
  multiple access specifier sections
2026-04-01 18:07:11 +01:00
Gergő Magyar
12be2025f1
feat(ts,js): TypeScript/JavaScript MethodExtractor config (#588)
* feat(ts,js): MethodExtractor config for TypeScript and JavaScript (#570)

Add per-language method extraction config following the established
JVM and C# patterns. Shared config base mirrors the field extractor's
typescript-javascript.ts pattern — TS-only node types are harmless
no-ops for JS.

Key features:
- isAbstract for abstract class methods and interface methods
- Parameter extraction with isOptional (?:, defaults) and isVariadic (...)
- Decorator extraction from preceding body-level siblings
- isAsync and isOverride detection
- Visibility via accessibility_modifier two-pass pattern
- Return type extraction unwrapping type_annotation

* test(ts,js): add override, getter/setter, destructured param tests

Address code review findings:
- Add override method detection test
- Add getter/setter extraction test
- Add destructured parameter with type annotation test
- Tighten constructor and private method assertions

* refactor(ts,js): address code review findings

- Replace O(M*N) decorator index scan with previousNamedSibling walk
- Remove dead findVisibility 'modifiers' fallback (TS uses
  accessibility_modifier, not a modifiers wrapper)
- Document call_signature/construct_signature as known gaps
- Document that TS constructors are method_definition nodes
- Remove unused findVisibility import

* fix(ts,js): type guard before cast, add generator/computed/overload tests

- Use type guard pattern (Set.has check before as-cast) in visibility
  extraction to ensure string is validated before narrowing
- Add generator method test (*items()) — confirms extraction works
- Add computed property name test ([Symbol.iterator]) — documents
  bracket-in-name behavior as intentional
- Add class-level method overload test — verifies overload signatures
  + implementation are all extracted

* fix(ts,js): detect #private methods as visibility 'private'

ES2022 private class methods (#name) use private_property_identifier
as their name node type. Detect this and return 'private' visibility
instead of the default 'public'.

* fix(ts,js): address review findings + close ingestion gaps

- hasKeyword/findVisibility: skip name field child to prevent false
  positives on soft-keyword method names (e.g. `abstract()`, `static()`)
- extractTsJsParameters: filter TS `this` parameter (compile-time only)
- extractMethodSignature: mirror `this`-param skip in fallback path
- tree-sitter queries: capture abstract_method_signature,
  method_signature, and private_property_identifier for TS; add
  private_property_identifier for JS
- Remove dead childForFieldName('name') fallbacks and typeFromAnnotation
  fallback
- Add 10+ unit tests, 4 integration tests through query pipeline

* test(ts): update HAS_METHOD count for interface method_signature capture

The new method_signature query now captures ILogger.log() as a Method
node with a HAS_METHOD edge, increasing the expected count from 4 to 5.

* fix(ts,js): address second review — async generator test, declare module gap

- Add async generator method test (async *values() → isAsync: true)
- Document declare module/global augmentation as known gap
2026-04-01 14:09:59 +01:00
Chirag Nighut
e2de9271fc
feat(java): method references, worker overload disambiguation, interface dispatch (#540)
* feat(java): method references + worker overload disambiguation (TypeEnv + argTypes)

Fix two Java gaps: (1) method references (obj::method) via
  tree-sitter @call + parseJavaMethodReference wired through extractLanguageCallSiteSeed for parse-worker and call-processor; (2) overloaded calls with typed
  non-literal args by extending OverloadHints with TypeEnv for identifiers and adding ExtractedCall.argTypes from extractCallArgTypes on the worker path with
  matchCandidatesByArgTypes (inferJvmLiteralType remains for literals).

* test(csharp): expect interface-dispatch edge for IRepository.Save in heritage fixture

* refactor(ingestion): move parseJavaMethodReference to call-sites/java.ts

* refactor(ingestion): defer worker call resolution until implementor map is complete

* style: prettier + remove unused import for CI quality checks

Made-with: Cursor

* fix(ingestion): implementor map for C# base_list + sequential pipeline path

- buildImplementorMap: treat extends rows as implements when resolveExtendsType
  says IMPLEMENTS (worker heritage mirrors parse-worker, all base_list as extends)
- Worker path: pass ctx into buildImplementorMap(deferredWorkerHeritage, ctx)
- Sequential path: extract heritage before processCalls and pass implementor map
  so small repos get interface-dispatch CALLS (fixes csharp-proj integration test)

Made-with: Cursor

* perf(pipeline): accumulate sequential implementor map without O(E) per chunk

- Merge buildImplementorMap(chunk heritage) into one map each sequential chunk so
  work is O(heritage) per chunk and interface dispatch sees prior chunks (worker parity)
- Drop unused globalImplementorMap + redundant merge after worker pass

Made-with: Cursor
2026-03-28 16:59:28 +00:00
Gergő Magyar
acf6fbdd39
feat: configure eslint with unused import removal (#564)
* feat: configure eslint with unused import removal

Add ESLint v9 (flat config) for code quality:
- eslint-plugin-unused-imports for auto-removing dead imports
- @typescript-eslint for TypeScript-aware linting
- eslint-plugin-react-hooks for React hooks rules
- eslint-config-prettier to avoid formatting conflicts
- lint-staged runs eslint --fix before prettier on .ts/.tsx
- CI lint job added to ci-quality.yml

* refactor: remove unused imports via eslint --fix

Auto-fixed by eslint-plugin-unused-imports. No logic changes.

* chore: add eslint fix commit to .git-blame-ignore-revs
2026-03-28 15:28:09 +00:00
Gergő Magyar
bf09eab95b
feat: configure prettier with pre-commit hook (#563)
* feat: configure prettier with pre-commit hook integration

Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo
root with husky pre-commit hook integration. Moves husky from
gitnexus/ to root package.json for reliable hook installation.

- Root package.json with prepare/format/format:check scripts
- .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4
- .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md
- .gitattributes enforcing LF line endings for Windows consistency
- Pre-commit hook uses direct node_modules/.bin/ paths (no npx)

* style: apply prettier formatting to entire codebase

One-time bulk format. No logic changes.
Use .git-blame-ignore-revs to skip this commit in git blame.

* chore: add .git-blame-ignore-revs for prettier format commit

* perf: pre-commit hook runs only tests related to staged files

Use vitest --related to scope test execution to tests that import
the changed files, instead of running the full suite on every commit.

* perf: remove vitest from pre-commit hook, keep in CI only

Pre-commit now runs lint-staged + tsc only. Tests run in CI
(ci-tests.yml) where they belong — keeps commits fast.

* ci: add prettier format check to quality workflow

PRs will now fail if code isn't formatted with prettier.
2026-03-28 14:58:04 +00:00
Gergő Magyar
fd7fb5bf1f
feat: unify web and cli ingestion pipeline (#536)
* feat: add server-side ingestion API (POST /api/analyze, SSE progress)

Extract core analysis orchestration from CLI into shared run-analyze.ts
module. Add server-side analyze endpoints so the web app can trigger
ingestion via HTTP instead of running the full pipeline in-browser.

New files:
- src/core/run-analyze.ts — shared runFullAnalysis() orchestrator
- src/server/analyze-job.ts — job manager (single-slot, dedup, SSE events)
- src/server/analyze-worker.ts — forked child process (8GB heap, IPC)
- src/server/git-clone.ts — shallow clone/pull with SSRF protection

API endpoints:
- POST /api/analyze — start analysis (returns 202 + jobId)
- GET /api/analyze/:jobId — poll job status
- GET /api/analyze/:jobId/progress — SSE progress stream

Security: URL validation blocks private IPs and non-HTTP schemes.
Path validation requires absolute paths. Git stderr not leaked to API.

* feat(web): add server-side analyze UI (Phase 2)

Add "Analyze on Server" flow to the web app's Server tab so users
can trigger server-side ingestion from the browser. On completion,
the graph is automatically loaded via the existing connectToServer flow.

New files:
- AnalyzeProgress.tsx — progress bar with phase label, elapsed time, cancel

Modified files:
- backend.ts — startAnalyze(), streamAnalyzeProgress() SSE client
- DropZone.tsx — analyze URL input + button below Connect section
- App.tsx — onServerAnalyze handler wires analyze -> connect flow

* feat: add job cancellation, timeout, and child process tracking (Phase 3)

- DELETE /api/analyze/:jobId — cancel running analysis (SIGTERM to worker)
- 30-minute timeout kills long-running workers automatically
- Child process refs tracked in JobManager for cleanup on shutdown
- dispose() kills all active children on SIGINT/SIGTERM
- Web cancel button now calls server DELETE endpoint
- cancelAnalyze() added to web backend client

* refactor(web): remove browser ingestion pipeline (Phase 4)

Delete 16 duplicated ingestion files, 2 unused service files
(git-clone, zip), and tree-sitter parser-loader from gitnexus-web.
All ingestion now runs server-side via POST /api/analyze.

Deleted (18 files, ~5,000 lines):
- core/ingestion/*.ts (16 pipeline processors)
- core/tree-sitter/parser-loader.ts (WASM tree-sitter loader)
- services/git-clone.ts (isomorphic-git client-side clone)
- services/zip.ts (JSZip extraction)

Simplified:
- DropZone.tsx — server-only (removed ZIP/GitHub tabs)
- ingestion.worker.ts — removed runPipeline/runPipelineFromFiles
- useAppState.tsx — removed pipeline callbacks
- App.tsx — removed handleFileSelect/handleGitClone
- main.tsx — removed Buffer polyfill for isomorphic-git
- types/pipeline.ts — removed PipelineResult/serialize helpers

Kept: cluster-enricher.ts (LLM enrichment, still used by worker)

Dependencies now removable: web-tree-sitter, isomorphic-git,
@isomorphic-git/lightning-fs, jszip (estimated 3-4MB bundle savings)

* refactor(web): sync graph schema from CLI + delete WASM grammars

Sync graph/types.ts and lbug/schema.ts from the CLI (source of truth)
to the web module so the browser LadybugDB can handle all node and
relationship types the server pipeline produces.

Synced types: Route, Tool, Section node labels; HANDLES_ROUTE, FETCHES,
HANDLES_TOOL, ENTRY_POINT_OF, WRAPS, QUERIES relationship types;
description fields on Function/Class/Interface/Method/CodeElement.

Deleted: public/wasm/ directory (14 tree-sitter WASM grammars + core).
Removed deps: web-tree-sitter, isomorphic-git, @isomorphic-git/lightning-fs,
jszip, buffer, @types/jszip (~3-4MB bundle savings).

* feat: create gitnexus-shared package for unified type definitions

Create a new gitnexus-shared package that is the single source of truth
for types shared between the CLI and web modules:

- SupportedLanguages enum (15 languages)
- Graph types: NodeLabel, NodeProperties, RelationshipType, GraphNode, GraphRelationship
- Schema constants: NODE_TABLES, REL_TYPES, REL_TABLE_NAME, EMBEDDING_TABLE_NAME
- Pipeline types: PipelinePhase, PipelineProgress

Both gitnexus (CLI) and gitnexus-web import from gitnexus-shared via
file: dependency. Each package re-exports and extends with platform-specific
additions (CLI: KnowledgeGraph with mutation methods; Web: simpler KnowledgeGraph).

This ensures types can never drift between packages — adding a new
language, node type, or relationship type in gitnexus-shared automatically
propagates to both consumers.

* refactor: import shared types directly from gitnexus-shared at call sites

Replace all re-export patterns with direct imports from gitnexus-shared.
72 files updated across CLI and web:

- SupportedLanguages: 49 CLI files now import from 'gitnexus-shared'
  instead of '../config/supported-languages.js'
- GraphNode, GraphRelationship, NodeLabel: 22 CLI + 10 web files now
  import from 'gitnexus-shared' instead of local re-export wrappers
- NODE_TABLES: api.ts imports from 'gitnexus-shared'
- PipelineProgress: useAppState.tsx imports from 'gitnexus-shared'

Local types.ts files now only define platform-specific KnowledgeGraph
(CLI has mutation methods, web has add-only). No more re-exports.

* fix: update lock files for gitnexus-shared, remove stale vite polyfills

Add gitnexus-shared@1.0.0 to lock files so npm ci succeeds in CI.
Remove buffer polyfill and global define from vite.config.ts (isomorphic-git was removed).

* fix(security): add write guard to HTTP /api/query, fix CORS proxy bypass

- Add isWriteQuery() check to POST /api/query handler — blocks CREATE,
  DELETE, SET, MERGE, DROP, etc. via HTTP API (guard was only in MCP
  pool adapter and browser-side, not the HTTP server path)
- Extend CYPHER_WRITE_RE with CALL, INSTALL, LOAD keywords
- Fix CORS proxy subdomain bypass: endsWith('github.com') allowed
  'evil-github.com'. Now requires exact match or '.github.com' suffix

* feat(server): enhance /api/search with enrichment, add /api/grep, strip graph content

- POST /api/search: add mode param (hybrid|semantic|bm25), server-side
  enrichment returns connections/cluster/processes per result in one call
  (collapses 31 sequential HTTP calls to 1 for the agent search tool)
- GET /api/grep: regex search across indexed file contents, eliminates
  need to transfer all file contents to browser
- GET /api/graph: strip content field by default (80-95% payload
  reduction). Use ?includeContent=true for backward compat
- Add LRU cache invalidation hook point for future caching

* feat(server): add /api/embed endpoint for server-side embedding generation

- POST /api/embed: triggers embedding pipeline via onnxruntime-node
  with JobManager for single-slot concurrency, timeout, and dedup
- GET /api/embed/:jobId: poll job status
- GET /api/embed/:jobId/progress: SSE stream with heartbeat, event IDs,
  and X-Accel-Buffering:no header for proxy compatibility
- DELETE /api/embed/:jobId: cancel running embedding job
- Maps embedding pipeline phases (ready→complete, error→failed) to
  JobManager status conventions

* feat(web): create consolidated BackendClient module

Single HTTP client replacing backend.ts, server-connection.ts, and
worker HTTP helpers. Includes:
- Typed methods: runQuery, search (enriched), grep, readFile, connect
- Generic streamSSE<T> utility extracted from analyze progress pattern
- BackendError with discriminated code field (network/server/client/timeout)
- Embed API: startEmbeddings, streamEmbeddingProgress, cancelEmbeddings
- Search with mode param (hybrid|semantic|bm25) and enrichment

* refactor(web): rewrite Graph RAG tools for backend-only HTTP queries

- Search tool: uses enriched /api/search (1 call replaces 31 sequential queries)
- Cypher tool: removes browser-side embedding; {{QUERY_VECTOR}} routes to
  /api/search with mode:'semantic' instead of local transformers.js
- Grep tool: uses /api/grep instead of in-memory fileContents map
- Read tool: uses /api/file instead of fileContents map lookup
- Impact tool: getCallSiteSnippet now async via /api/file
- createGraphRAGTools now accepts GraphRAGBackend interface instead of
  7 separate function params + fileContents map
- createGraphRAGAgent simplified to (config, backend, context?)
- Removed imports: embedder, lbug/schema (replaced with gitnexus-shared)
- Net: -205 lines

* refactor(web): delete WASM infrastructure, remove 7 packages (-5242 lines)

Delete browser-side LadybugDB, embeddings, search, and worker:
- gitnexus-web/src/core/lbug/ (adapter, csv-generator, schema, query-result)
- gitnexus-web/src/core/embeddings/ (embedder, pipeline, text-gen, types)
- gitnexus-web/src/core/search/ (bm25-index, hybrid-search)
- gitnexus-web/src/workers/ingestion.worker.ts (828 lines)
- gitnexus-web/src/services/server-connection.ts (merged into backend-client)
- gitnexus-web/src/types/lbug-wasm.d.ts

Remove packages: @ladybugdb/wasm-core, @huggingface/transformers,
comlink, minisearch, vite-plugin-wasm, vite-plugin-top-level-await,
vite-plugin-static-copy

Update vite.config.ts: remove WASM plugins, COOP/COEP headers,
worker config, optimizeDeps exclude

Update imports: App.tsx, DropZone, Header, AnalyzeProgress,
BackendRepoSelector, useBackend → backend-client

* refactor(web): replace Worker/Comlink with direct BackendClient calls

- useAppState: remove Worker instantiation, Comlink.wrap, apiRef.
  All queries now go through BackendClient HTTP functions directly.
- Agent runs on main thread (I/O-bound LLM streaming, not CPU-bound)
- initializeAgent: creates GraphRAGAgent with GraphRAGBackend interface
  bound to BackendClient methods (runQuery, search, grep, readFile)
- startEmbeddings: calls POST /api/embed + SSE progress instead of
  running browser-side transformers.js pipeline
- switchRepo: no longer loads graph into WASM DB or extracts fileContents
- App.tsx: handleServerConnect simplified (no fileContents, no loadServerGraph)
- Delete old backend.ts (replaced by backend-client.ts)
- Net: -396 lines

* fix(web): fix await-in-map build error in agent streaming

Move dynamic import of AIMessage outside .map() callback to avoid
"await can only be used inside an async function" build error.

* fix(web): remove stale apiRef references that broke chat functionality

sendChatMessage referenced apiRef.current (deleted Worker ref) which
would throw TypeError. Replaced with agentRef.current guard since agent
now runs on main thread.

* fix(server): dispose embedJobManager on shutdown, fix job mutation

- Add embedJobManager.dispose() to shutdown handler (was missing,
  causing cleanup timer to keep Node process alive)
- Replace direct job.repoName/status mutation with updateJob() to
  ensure SSE event emission for initial status change

* fix(server): parameterize Cypher, harden grep, unify SSE endpoints

- Search enrichment: replace string interpolation with executePrepared()
  using $nid parameter binding to prevent Cypher injection
- Add executePrepared() to core lbug-adapter (prepare/execute pattern)
- /api/grep: add 200-char pattern length limit (ReDoS protection),
  search files on disk instead of loading entire corpus into memory
  (constant memory usage regardless of repo size)
- Extract mountSSEProgress() shared helper for SSE streaming — both
  analyze and embed endpoints now have consistent heartbeat (30s),
  event IDs (reconnection support), and X-Accel-Buffering header

* refactor(web): remove dead code from Worker-era architecture

- Remove loadServerGraph no-op function, interface member, and all consumers
- Remove testArrayParams stub and interface member
- Remove fileContents state from GraphStateProvider (never populated in
  server-side architecture)
- Remove forceDevice parameter from startEmbeddings (server-side, no device choice)
- Replace phantom EmbeddingProgress type with inline { phase, percent }
- Replace resolvePathFromContents (needed fileContents Map) with graph-based
  file path resolution using filePathIndex built from graph nodes
- Fix: AI citation grounding ([[file.ts:10]]) now works via graph node lookup
  instead of broken fileContents-based resolution

* fix(web): use streamAgentResponse for full tool_call/reasoning streaming

Replace naive agent.stream() loop that only handled content chunks with
streamAgentResponse() generator from agent.ts. This properly routes:
- reasoning tokens (before/between tool calls)
- tool_call events (name, args, status)
- tool_result events (completed tool output)
- content tokens (final answer after all tools done)

Previously the onChunk handler for tool_call/tool_result/reasoning was
dead code since the streaming loop only emitted content events.

* fix(web): resolve CI type errors from dead code removal

- Import GraphNode/GraphRelationship from gitnexus-shared in graph.ts
  (not re-exported from local types.ts)
- Add Route, Tool entries to NODE_COLORS and NODE_SIZES constants
- Add PipelineResult type to web types/pipeline.ts
- Remove fileContents from CodeReferencesPanel and RightPanel
- Remove testArrayParams and forceDevice from EmbeddingStatus
- Remove forceDevice args from startEmbeddings() calls in App.tsx
- Fix embeddingProgress property accesses for simplified type

* fix(ci): add setup-gitnexus-web action, build shared once per job

- Remove prepare script from gitnexus-shared (tsc not available during
  npm ci of consuming packages)
- Create .github/actions/setup-gitnexus-web composite action: builds
  gitnexus-shared then runs npm ci for gitnexus-web
- setup-gitnexus action: already builds gitnexus-shared for CLI jobs
- ci-quality typecheck-web: uses setup-gitnexus-web (DRY)
- ci-e2e: uses setup-gitnexus-web (DRY)
- ci-tests: gitnexus-shared already built by setup-gitnexus, just
  install web deps without rebuilding

* fix(ci): use prepare script so gitnexus-shared builds during npm ci

Move typescript from devDependencies to dependencies in gitnexus-shared
so the prepare script (tsc) works when npm resolves file: deps during
npm ci. No GHA modifications needed — npm handles the build lifecycle
automatically.

Remove manual gitnexus-shared build steps from setup-gitnexus and
setup-gitnexus-web actions.

* fix(ci): build gitnexus-shared explicitly in setup actions

The file: dependency protocol doesn't reliably run prepare scripts
because devDependencies aren't installed first. Instead of fragile
lifecycle hacks, build gitnexus-shared explicitly in both setup actions:
- setup-gitnexus: npm install && npm run build in gitnexus-shared/
- setup-gitnexus-web: same, before npm ci in gitnexus-web/
- ci-tests: shared already built by setup-gitnexus, web just npm ci

No prepare script, no dist in git, no typescript as a prod dependency.

* fix: remove CALL from CYPHER_WRITE_RE — breaks FTS and vector search

CALL is used by read-only procedures: CALL QUERY_FTS_INDEX(...) and
CALL QUERY_VECTOR_INDEX(...). Adding it to the write guard blocked all
FTS search, causing 3 test failures. The database is opened in read-only
mode as defense-in-depth against write procedures via CALL.

Keep INSTALL and LOAD in the blocklist (genuinely dangerous).

* fix(web): update vercel.json for gitnexus-shared, remove COOP/COEP

- Add installCommand that builds gitnexus-shared before installing
  web deps (Vercel doesn't know about the monorepo file: dependency)
- Remove Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy
  headers (no longer needed — WASM LadybugDB removed)

* fix(web): update tests for deleted modules

- Delete csv-generator.test.ts (tests deleted WASM-only csv-generator)
- Update security-guards.test.ts: import NODE_TABLES/REL_TYPES from
  gitnexus-shared instead of deleted src/core/lbug/schema
- Update server-connection.test.ts: import normalizeServerUrl from
  backend-client, remove extractFileContents tests (function deleted)

* fix(e2e): remove Server tab click — UI is now server-only

The DropZone no longer has ZIP/GitHub/Server tabs (browser ingestion
was removed). The server URL input is directly visible on the landing
page. Update e2e test to skip the tab click and go straight to input.

All 5 e2e tests pass locally.

* refactor: use gitnexus-shared for PipelinePhase/PipelineProgress types

CLI was duplicating PipelinePhase and PipelineProgress locally instead
of importing from gitnexus-shared. Updated all consumers to import
directly. Also removed dead code: SerializablePipelineResult,
serializePipelineResult(), deserializePipelineResult().

* fix(server): address PR #536 review — security, race conditions, dead code

- Fix path traversal in POST /api/analyze: split into isAbsolute + normalize check
- Add shared repo lock (activeRepoPaths) preventing concurrent analyze+embed on same repo
- Fix 202 response returning actual job.status instead of hardcoded 'queued'
- Add 30-minute timeout for embedding jobs (was missing unlike analyze jobs)
- Fix DropZone calling startAnalyze without setting backend URL first
- Add SSE reconnect with exponential backoff (3 retries) and Last-Event-ID
- Fix normalizeServerUrl to return base URL (no /api suffix) — clear contract
- Delete dead code: proxy.ts, server-graph-hydration.ts, pipeline.ts re-export barrel
- Update LoadingOverlay to import PipelineProgress directly from gitnexus-shared

* fix(server): fix repo lock key mismatch and embed cancel race

- Use getStoragePath(targetPath) as lock key in analyze handler to match
  embed handler's entry.storagePath — keys now always align
- Guard embed completion: don't overwrite 'failed' with 'complete' when
  job was cancelled while pipeline was still running
- Remove unused jobType parameter from acquireRepoLock
- Log backend.init() errors instead of silently swallowing

* fix: add gitnexus-shared as a local dependency in package-lock.json

* refactor: move language detection to gitnexus-shared, add syntax highlighting for all 15 languages

Move getLanguageFromFilename() from CLI to gitnexus-shared with COBOL
support added. Add getSyntaxLanguageFromFilename() for Prism-compatible
syntax highlighting covering all 15 code languages plus auxiliary
formats (json, yaml, markdown, html, css, bash, sql, xml).

Refactor CodeReferencesPanel to use shared function instead of a local
30-line switch. Delete dead gitnexus-web/src/config/supported-languages.ts
(web already imports SupportedLanguages from gitnexus-shared).

* feat(web): add first-time user onboarding with auto server detection

Replace the manual "Connect to Server" panel with an automatic onboarding
flow that guides first-time users through starting the GitNexus server.

Server detection:
- useBackend hook polls via setTimeout chain (3s, no overlap)
- Page Visibility API pauses polling when tab is hidden
- SSE heartbeat (/api/heartbeat) for instant disconnect detection

Onboarding UI (OnboardingGuide.tsx):
- Step-by-step flow: copy command → run → auto-connect
- Smart command: shows `gitnexus serve` in dev, `npx gitnexus@latest serve` in prod
- Node.js version auto-detected from package.json via Vite define
- Faux terminal windows with copy-to-clipboard, platform tabs, polling indicator

Transitions (DropZone.tsx):
- Crossfade wrapper with snapshot pattern for smooth phase transitions
- Three phases: onboarding → success (1.2s hold) → loading → graph
- Auto-recovery: falls back to onboarding if server dies or connect fails

Server changes:
- GET /api/heartbeat: SSE endpoint for liveness detection
- GET /api/info: version, launch context, Node.js version
- npm run serve script for local development
- app.disable('x-powered-by') hardening

* feat(web): add repo analysis UI, SSE heartbeat, and review fixes

Repo analysis:
- AnalyzeOnboarding: empty-state card when server has zero repos
- RepoAnalyzer: GitHub URL + Local Folder tabs with browse button
- Header repo dropdown: click project badge to switch repos or analyze new
- DropZone 'analyze' phase integrated into Crossfade transitions

Reliability fixes from 5-agent review:
- Polling: stop scheduling timers when tab hidden, restart on visibility return
- Heartbeat: exponential backoff (1s/2s/4s, 3 retries) prevents graph loss on blip
- RepoAnalyzer: completion timer tracked in ref, cleaned up on unmount
- DropZone: standardized card padding (p-7), heading sizes (text-lg)

Accessibility:
- prefers-reduced-motion global CSS rule (WCAG 2.3.3)
- focus-visible rings on CopyButton
- cursor-pointer on all Header buttons
- Consistent rounded-xl on all dropdowns

Cleanup:
- Deleted dead AnalyzeSheet.tsx (219 LOC) and BackendRepoSelector.tsx (89 LOC)
- Fixed AnalyzeProgress lucide import (lucide-react → @/lib/lucide-icons)

* fix(server): resolve analyze worker fork crash in dev mode

The forked analyze worker was crashing immediately with exit code 1
when running via `npm run serve` (tsx). Two issues:

1. Worker path resolved to `analyze-worker.js` but only `.ts` exists
   in the source directory — the `.js` file is only in `dist/`.

2. On Windows, bare `--import tsx` in execArgv fails because Node's
   ESM resolver for --import uses the child's CWD, not the parent's
   node_modules. Windows also rejects raw paths as `d:` is not a
   valid URL scheme.

Fix: detect dev vs prod via `import.meta.url` extension. In dev mode,
resolve `tsx/esm` to an absolute `file://` URL via `pathToFileURL()`
anchored to the parent's `createRequire` context. This works on all
platforms and doesn't depend on the child's CWD or PATH.

Also captures child stderr for better crash diagnostics.

Verified: `POST /api/analyze` with GitHub URL completes successfully
in dev mode (tsx) — status goes from cloning → analyzing → complete.

* fix(server): add worker auto-retry, error handling, and crash diagnostics

Worker resilience:
- Auto-retry up to 2 times with exponential backoff (1s, 2s) on crash
- SSE progress shows "Retrying after crash (1/2)..." during retry
- Captures child stderr for crash diagnostics in failure message
- AnalyzeJob tracks retryCount per job

Server error handling:
- app.listen wrapped in Promise so EADDRINUSE/EACCES propagate cleanly
- serve.ts catches startup errors with friendly messages and exit code 1
- EADDRINUSE gets actionable guidance (stop other process or --port flag)
- Global uncaughtException/unhandledRejection handlers prevent silent exits
- DEBUG=1 env var shows full stack traces

* feat: add e2e tests for onboarding flows, worker retry, and error handling

E2E tests (onboarding.spec.ts — 11 tests):
- Flow 1: OnboardingGuide shown when server unreachable (6 tests)
- Flow 2: Auto-connect with success card, analyze phase for zero repos
- Flow 3: Analyze form — GitHub URL validation, Local Folder tab, tab switching
- Flow 4: Repo dropdown in exploring view (skipped without live server)

Updated server-connect.spec.ts:
- Replaced manual Connect button flow with auto-connect waitForGraphLoaded

Server resilience:
- Worker auto-retry (2 attempts with exponential backoff) on crash
- Friendly error messages for serve startup failures (EADDRINUSE etc.)
- Global uncaughtException/unhandledRejection handlers prevent silent exits
- app.listen wrapped in Promise for proper error propagation

* refactor(shared): enforce exhaustive language coverage via Record types

Replace the if/else chain in getLanguageFromFilename with two exhaustive
Record<SupportedLanguages, ...> maps:

- EXTENSION_MAP: every language → its file extensions
- SYNTAX_MAP: every language → its Prism syntax identifier

Adding a new member to the SupportedLanguages enum without adding it to
both maps now produces a TypeScript compile error:

  Property '[SupportedLanguages.NewLang]' is missing in type...

This matches the existing pattern in languages/index.ts (providers table)
which already uses `satisfies Record<SupportedLanguages, LanguageProvider>`.

Three compile-time enforcement points now exist:
1. EXTENSION_MAP in language-detection.ts (file extensions)
2. SYNTAX_MAP in language-detection.ts (Prism syntax identifiers)
3. providers in languages/index.ts (LanguageProvider instances)

* feat(web): load source code from server and scroll to selected line

CodeReferencesPanel now fetches file content via GET /api/file when a
node is selected, instead of showing "Code not available in memory".

- Fetches via readFile() from backend-client when selectedFilePath changes
- Shows loading spinner while fetching
- After content loads, auto-scrolls to the selected node's startLine
- Highlights the selected line range with a cyan left border
- Cancels in-flight fetch if selection changes before it completes

Also: refactored language-detection.ts to use exhaustive Record types
(EXTENSION_MAP and SYNTAX_MAP) so adding a new SupportedLanguages enum
member without implementing extensions/syntax is a compile error.

* feat: buffered file reading for Code Inspector

Server: GET /api/file now supports ?startLine=N&endLine=M for reading
a line range instead of the entire file. Returns { content, startLine,
endLine, totalLines }.

Client: readFile() returns ReadFileResult with metadata. When selecting
a symbol (function, class, method), fetches only ±50 lines around the
symbol's startLine/endLine instead of the full file. File nodes still
fetch the entire file.

SyntaxHighlighter startingLineNumber set from the buffer offset so line
numbers are correct even for partial reads.

* fix: adapt readFile callers to new ReadFileResult return type

tools.ts: readFile comes from GraphRAGBackend interface which returns
Promise<string> (the adapter in useAppState extracts .content), so
revert the { content } destructuring back to plain string assignment.

useAppState.tsx: wrap backendReadFile with { repo } options object
and extract .content to satisfy the GraphRAGBackend interface.

* fix(web): ensure new repos appear in list immediately after analysis

Two fixes:

1. DropZone: handleAnalyzeComplete now passes the repoName through to
   connectToServer so the specific newly-analyzed repo loads — not the
   server's default first repo.

2. App.tsx: fetchRepos() is now awaited BEFORE handleServerConnect in
   both the DropZone and Header flows. This ensures the repo list is
   populated before the exploring view renders, so the new repo appears
   in the header dropdown immediately without a page reload.

* feat: delete repos, re-analyze with force, select after analysis

Server — DELETE /api/repo:
- Acquires repo lock first (409 if analyze/embed in flight)
- Closes LadybugDB, deletes index + clone dir, unregisters, re-inits
- Lock released in finally block

Server — analyze complete:
- backend.init() must succeed before SSE complete fires
- If backend.init() fails, job is marked failed (not complete)

Web — Header repo dropdown:
- Re-analyze: calls POST /api/analyze with force=true, shows spinning
  icon + inline progress bar via SSE
- Delete: acquires lock, aborts any running re-analysis SSE for same
  repo, refreshes list, switches to next repo
- After analysis completes: refreshes repo list, connects to the
  specific repo by name, loads graph, shows in explorer
- Retry with 1.5s backoff on 404 (server may still be reinitializing)

Type safety:
- err: any → err: unknown + instanceof BackendError in retry loop
- Added missing BackendRepo + BackendError imports in App.tsx
2026-03-28 14:07:11 +00:00
Oleg Deezus
cde3858e03
refactor: Phase 8 & 9 — Field Types and Return-Type Binding (#494)
* feat(phase8): add field type data structures and extractor interface

* feat(phase8): implement TypeScript field extractor

* feat-phase9-add-call-result-binding

* test-phase8-add-field-extraction-unit-tests

* docs: update documentation for Phase 8 and Phase 9

* feat(swift): Phase 8/9 integration tests for field-type and call-result binding

Add Swift field-type resolution and call-result binding integration tests
with fixtures, plus merge-conflict fixes for the FieldExtractor code.

**Swift integration tests:**
- `swift-field-types/` fixture (Models.swift + App.swift) — tests
  HAS_PROPERTY edges, field-chain CALLS resolution (user.address.save()
  → Address#save), and ACCESSES edges for field reads.
- `swift-call-result-binding/` fixture — tests call-result binding
  (let user = getUser(); user.save() → User#save).
- 2 new describe blocks in swift.test.ts with skipIf(!swiftAvailable).

**Swift arity fix:**
- extractMethodSignature fallback counts direct `parameter` children
  when no wrapper list node exists (Swift's tree-sitter grammar places
  parameters as direct children of function_declaration).  Without this,
  all Swift functions had parameterCount: 0 and the arity filter rejected
  valid call targets.

**FieldExtractor merge-conflict fixes:**
- field-extractor.ts: update import from removed ./utils.js to
  ./utils/ast-helpers.js; use typeEnv.fileScope() instead of .get('').
- field-extractors/typescript.ts: same import fix.
- field-types.ts: alias TypeEnvironment as TypeEnv (renamed on main).
- field-extraction.test.ts: mock TypeEnvironment interface properly.

* feat(field-extractors): generic table-driven field extractors for all 14 languages, wired into pipeline

Implements field extractors for all supported languages and integrates
them into the ingestion pipeline as the single source of truth for
Property node metadata.

**Generic field extractor factory** — `field-extractors/generic.ts`
defines a `createFieldExtractor(config)` factory that generates
FieldExtractor instances from a per-language `FieldExtractionConfig`.
Each config specifies AST node types, name/type/visibility extraction
functions, and static/readonly detection — typically 20-40 lines per
language vs 300+ for a hand-written extractor.

**Per-language configs** — `field-extractors/configs/` has 11 config
files covering 13 languages (TS/JS share, Java/Kotlin share).
TypeScript keeps its hand-written extractor for richer handling.

**LanguageProvider integration** — New optional `fieldExtractor` property
on LanguageProviderConfig, set via defineLanguage() in each language
file. Follows the same strategy pattern as typeConfig, exportChecker,
and labelOverride.  Removed the separate FieldExtractorRegistry class
and field-extractors/index.ts — extractors are accessed via
getProvider(lang).fieldExtractor.

**Pipeline wiring** — Both parse-worker.ts (worker pool) and
parsing-processor.ts (sequential fallback) now call the FieldExtractor
during Property node creation. Results are cached per class node.
Property nodes are enriched with: declaredType, visibility, isStatic,
isReadonly.

**extractPropertyDeclaredType removed** — The 100-line multi-strategy
function in type-extractors/shared.ts is replaced by the FieldExtractor.
All 14 languages register an extractor, eliminating the need for a
generic fallback.  The Python config's extractType was fixed to handle
annotation-without-value patterns (address: Address).

**Integration tests** — Each language's resolver test file gains
pipeline-based assertions verifying visibility/isStatic/isReadonly on
Property nodes via getNodesByLabelFull.  Tests run through
runPipelineFromRepo with real fixtures — no direct extractor calls.

* fix(type-env): thread enclosingFunctionFinder through scope resolution, unskip Dart ACCESSES test

The type-env's findEnclosingScopeKey had the same Dart sibling problem
as findEnclosingFunction — it walked parents but never found
function_signature because the call lives inside function_body (a
sibling).  Instead of hardcoding a function_body check, thread the
provider's enclosingFunctionFinder hook through BuildTypeEnvOptions →
lookupInEnv → findEnclosingScopeKey.  All three buildTypeEnv call sites
(call-processor, parsing-processor, parse-worker) now pass the hook.

This enables the type-env to resolve scoped parameter bindings for Dart
(e.g., `user: User` in processUser), which lets the chain-resolution
tier (Step 1c) walk `user.address` and emit ACCESSES edges.

Dart integration test unskipped — 10/10 passing including ACCESSES.
Reverted CHANGELOG.md to origin/main.

* fix: resolve all PR #494 review findings (10 items)

CRITICAL:
- parse-worker.ts: classNode: any → SyntaxNode on getFieldInfo
  and findEnclosingClassNode; removed redundant as number casts
- parsing-processor.ts: classNode: any → SyntaxNode on seqGetFieldInfo

HIGH:
- ruby.ts: attr_accessor now extracts ALL symbol arguments via
  extractNames hook in generic factory (was firstNamedChild only)
- typescript.ts: added JSDoc explaining why hand-written extractor
  coexists with config-based typescript-javascript.ts

MEDIUM:
- field-types.ts: FieldVisibility union type replaces string
  ('public'|'private'|'protected'|'internal'|'package'|'fileprivate'|'open')
  Propagated through field-extractor.ts, generic.ts, all 7 config files
- typescript.ts: extractFullType collapsed from 12 branches to 3 lines
- generic.ts: added extractNames? optional hook + buildField refactor

LOW:
- ruby.ts: extractVisibility(node) → extractVisibility(_node)
- python.ts: fixed misleading isStatic comment

TypeScript compiles cleanly.

* test: add 24 field extraction tests for generic factory + 5 languages

Generic factory (4 tests):
- createFieldExtractor with TypeScript config validates factory itself
- Body discovery for interfaces, static/readonly modifiers
- Non-type node rejection

Python (4 tests):
- Annotated class field extraction
- Underscore-based visibility: _name=protected, __name=private

Go (5 tests):
- isTypeDeclaration on type_declaration nodes
- Config functions: uppercase=public, lowercase=package visibility
- extractType, isStatic, isReadonly

C++ (5 tests):
- public/private/protected access specifier backward-sibling walk
- Default visibility: class=private, struct=public
- static/const modifier detection

Ruby (6 tests):
- attr_accessor multi-symbol: :name, :email, :age → 3 fields
- attr_reader=readonly, attr_writer=non-readonly
- Multiple attr_* calls in one class

Total: 46 tests passing

* chore: remove plan doc from PR

---------

Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-03-26 16:37:34 +00:00