mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
24 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
42d276bc4a
|
chore(deps)(deps-dev): bump typescript in /gitnexus-shared (#1034) | ||
|
|
6222b5be9b
|
feat(ingestion): emit-references drains ReferenceIndex to graph edges (#925, RFC #909 Ring 2 PKG) (#973)
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
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
|
||
|
|
c6a291de67
|
feat(ingestion): ScopeExtractor driver — 5-pass CaptureMatch → ParsedFile (#919, RFC #909 Ring 2 PKG) (#965)
* feat(ingestion): ScopeExtractor driver — 5-pass CaptureMatch → ParsedFile (#919, RFC #909 Ring 2 PKG) Kicks off Ring 2 PKG. Implements RFC §5.3 + §3.2 Phase 1: the central, source-agnostic driver that turns a language provider's `CaptureMatch[]` into a `ParsedFile` — the per-file artifact the finalize orchestrator (#921) feeds into the shared `finalize()` algorithm (#915). ## Files ### New shared contracts - `gitnexus-shared/src/scope-resolution/parsed-file.ts` Per-file extraction artifact: scopes, parsedImports, localDefs, referenceSites. Structural superset of `FinalizeFile` so the finalize orchestrator threads `ParsedFile` through unchanged. - `gitnexus-shared/src/scope-resolution/reference-site.ts` Pre-resolution usage fact: name, atRange, inScope, kind, optional callForm/explicitReceiver/arity. Converted to `Reference` records by the resolution phase (populates `ReferenceIndex`). ### Ring 1 collateral tweak - `language-provider.ts: emitScopeCaptures` now returns `Promise<readonly CaptureMatch[]>` (was `readonly Capture[]`). Pre-grouping per tree-sitter match is the provider's job — the extractor expects coherent matches, not flat captures. No consumers yet (all languages still on legacy DAG), so no breakage. Docstring updated. ### New CLI module - `gitnexus/src/core/ingestion/scope-extractor.ts` Single entry point: `extract(matches, filePath, provider): ParsedFile`. Five-pass pipeline: Pass 1 — Build scope tree. `@scope.*` → `ScopeDraft[]` via range-containment parent derivation. Honors `provider.shouldCreateScope` (skip-but-reparent-children) and `provider.resolveScopeKind`. Throws `ScopeTreeInvariantError` via `buildScopeTree` on malformed input. Pass 2 — Attach declarations + local bindings. `@declaration.*` → `SymbolDefinition` + `BindingRef { origin: 'local' }`. Default attachment: innermost containing scope. Hoisting via `provider.bindingScopeFor`. Pass 3 — Collect raw imports. `@import.*` → `ParsedImport` via `provider.interpretImport`. Attached to ParsedFile (finalize resolves owning scope in Phase 2). Pass 4 — Collect type bindings. `@type-binding.*` → `TypeRef` via `provider.interpretTypeBinding` → `scope.typeBindings`. Hoistable via `bindingScopeFor`. Pass 5 — Collect reference sites. `@reference.*` → `ReferenceSite[]`. Call form from declarative sub-tag (`@reference.call.member`) or `provider.classifyCallForm`. ### Tests - `gitnexus/test/unit/scope-resolution/scope-extractor.test.ts` 23 tests organized by pass + one end-to-end fixture exercising all 5 passes together. MockProvider emits synthetic `CaptureMatch[]` with no AST — extractor is pure given those. ## Design notes - **Source-agnostic.** No `Tree` / `SyntaxNode` types leak into the driver. Works for tree-sitter providers and COBOL's regex tagger. - **One AST walk per language.** Providers do the walk inside `emitScopeCaptures`; this driver does zero traversal. - **Invariants delegated.** `ScopeTree.buildScopeTree` enforces structural rules (non-Module has parent, parent contains child, siblings don't overlap). The extractor doesn't try to repair malformed captures. - **Sub-tag whitelist.** `@reference.receiver`, `@declaration.name`, `@import.source`, etc. are known sub-tags — excluded from anchor selection so the broadest-range heuristic doesn't mis-identify them as anchors for their topic. Bug surfaced in the end-to-end fixture test (member call with a large-range receiver) and was fixed before commit. ## Verification - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`) - `gitnexus-shared` build clean - 23/23 new tests pass - Full scope-resolution / model / shadow suite: **285/285 pass** ## Closes part of #909. Unblocks - #920 parse-worker integration (emit ParsedFile from the worker) - #921 finalize orchestrator (consume ParsedFile[] workspace-wide) - #922 per-language import adapters * chore(ingestion): address #919 review findings on the extractor Addresses all 5 items from the PR #965 review in-PR. ## Structural changes - **Extract `ScopeExtractorHooks` as the narrow dependency surface.** The extractor now declares its dependency on a `Pick`-narrowed subset of `LanguageProvider` (just the 6 scope-resolution hooks it actually reads). Test mocks implement exactly that interface — no more `as unknown as LanguageProvider` cast hiding missing-field bugs. Adding a new hook read becomes a compile error, not a silent test pass. (Finding 3.2) - **Remove dead `ownerDefIdFor` stub + `isOwnerKind` helper.** The function always returned `undefined` with `void innermost; void drafts;` suppressors — an incomplete-implementation signal. The code path was also misleading: creating a clone of the def with `ownerId: undefined` is structurally identical to keeping the original. Pass 2 now keeps the def as-is. Contract is documented in a code comment: providers that need `ownerId` set it from their declaration hook; `finalize` (via #914 `MethodDispatchIndex`) fills in method/field `ownerId` in a post-extraction pass that has full def visibility. (Finding 2.1) - **Standardize `filePath` threading across passes 4 and 5.** Pass 4 was reading `drafts[0]!.filePath`; pass 5 was reading `anyFilePathFromScopeTree(scopeTree)`. Both equivalent but inconsistent. Both now take `filePath` as a parameter from the top-level `extract()` call. The `anyFilePathFromScopeTree` helper is removed. (Finding 2.2) ## Documentation - **Snapshot-semantics comment on `scopeTree` + `positionIndex`.** The hooks called during Passes 2-5 receive a `scopeTree` built BEFORE any bindings/ownedDefs/typeBindings were written. Hooks MUST NOT rely on `scope.bindings` etc. being populated — they're for parent/range/kind queries only. Added a doc block at the `scopeTree`/`positionIndex` construction site so future Ring 3 implementers don't write a `classifyCallForm` that reads bindings. (Finding 2.3) ## Tests - **Regression for the anchor-vs-receiver bug** (Finding 3.1): a member-call match where `@reference.receiver` spans columns 0-10 (wider) and the call name spans 11-15 (narrower). Without the `KNOWN_SUB_TAGS` exclusion, the broadest-range heuristic would have picked the receiver; the test pins that the call name is the one that ends up in `referenceSites[0].name`. - **Mock provider now types exactly `ScopeExtractorHooks`**, no more double-cast. Any future hook added to `extract()` that isn't in `ScopeExtractorHooks` is a compile error. ## Verification - `tsc --noEmit` clean in both `gitnexus-shared` and `gitnexus` - `gitnexus-shared` build clean - 24/24 scope-extractor tests pass (+1 regression) - Full scope-resolution / model / shadow suite: **286/286 pass** |
||
|
|
e944f90879
|
chore(shared): apply Ring 2 SHARED review follow-ups in one diff (#964)
* chore(shared): apply Ring 2 SHARED review follow-ups in one diff Aggregates all actionable follow-ups from the 9 Ring 2 SHARED PRs (#949–#963) before proceeding to Ring 2 PKG. No behavior changes; docstring edits, test refinements, and one structural cleanup. ## #913 (DefIndex / ModuleScopeIndex / QualifiedNameIndex) - Rename `freezeIndex` → `wrapIndex` across all three index builders. The old name implied `Object.freeze` on the wrapper, which we never applied; `wrapIndex` more accurately describes the lightweight readonly-interface wrap. Safety surface (frozen bucket arrays, frozen miss-empty array, readonly Maps) is unchanged. - Document in `buildModuleScopeIndex` JSDoc that callers must pre-normalize `filePath` keys (no path-separator canonicalization happens here). Prevents silent cross-platform misses. - Add an explicit hit-path freeze assertion in `qualified-name-index.test.ts` (the existing test covered only the miss-path `EMPTY` array). ## #914 (MethodDispatchIndex) - Differentiate the C3 and BFS test cases: both tests now use distinct MRO orderings so they prove the materializer stores whatever order the `computeMro` callback produces (not that C3 and BFS yield identical output). - Add `implementsOfCalls` counter in the first-write-wins test, and document the call-count contract in `MethodDispatchInput.implementsOf` JSDoc: `implementsOf` fires **per occurrence** in `input.owners` (not per unique owner); `computeMro` fires at most once per unique owner. Callers with expensive `implementsOf` implementations should pre-dedupe `owners`. ## #916 (resolveTypeRef) - Document the deliberate exclusion of `'Type'` from `TYPE_KINDS` (verified no extractor in `gitnexus/src/core/ingestion/` emits `type: 'Type'` for annotation-relevant symbols). - Rename the namespace-origin test from `'resolves ...'` to `'returns null for a namespace-origin binding whose def is not a type kind'`, matching the failure-case intent. ## #918 (shadow diff + aggregate) - Remove the partial re-export `export type { ShadowAgreement, ShadowDiff };` from `aggregate.ts` — it omitted `ShadowCallsite` and diverged from the top-level barrel. Consumers import all three from the `gitnexus-shared` entry point. - Fix the invalid `'wildcard'` evidence kind in `diff.test.ts` fixture (that kind is not a valid `ResolutionEvidence.kind`). Replaced with `'global-name'`, a real kind the test treats identically. ## #912 (ScopeTree / PositionIndex / makeScopeId) - Document the touching-boundary semantics on `PositionIndex.atPosition`: when siblings share a boundary point, the right (later-start) sibling wins per the existing innermost-wins sort contract. - Resolve the layer-inversion flagged by review: move `ScopeLookup` from `resolve-type-ref.ts` to `types.ts` (its natural home in the data-model layer). `scope-tree.ts` now imports `ScopeLookup` from `types.js` directly; the old re-export from `resolve-type-ref.ts` is removed per repo convention (`feedback_no_reexport`). Barrel export moved alongside. ## #917 (ClassRegistry / MethodRegistry / FieldRegistry) - Replace the dangling "try a name-match among class-like defs" comment in `lookupReceiverType` with explicit prose that callers must pre-resolve via `resolveTypeRef` if they want richer semantics. No behavior change — the function already returned `undefined` on ambiguous/missing qnames. - Fix `tieBreakKey.origin` default for pure Step-2 candidates. Type-binding-only hits no longer falsely inherit `'local'` from `ensureCandidate`'s neutral default; they now demote to `'import'` on their first type-binding hit, and only a later Step-1 lexical hit can upgrade them back to `'local'`. Keeps the Appendix B cascade faithful to the true origin. - Document `'global-name'` in `evidence.ts`: currently reserved for Ring 3's byName global index; `lookupCore` never emits it today. The weight stays live so `composeEvidence` remains exhaustive over the origin union. - Rename the mislabeled Step-7 test from `'confidence DESC is the primary key'` (which actually tested hard-shadow baseline) to `'inner scope shadows outer, yielding single result'`, and add a separate test that actually exercises multi-candidate confidence ordering (local vs wildcard at the same scope). ## Verification - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`) - `gitnexus-shared` build clean - Combined scope-resolution / model / shadow suite: **260/260 pass** (+1 from the new multi-candidate ordering test in #917) ## Not addressed (non-actionable) - #949 CI "failure with zero failing tests": pre-existing Swift Node 22 grammar flake unrelated to #910 scope. - #950: the two non-blocking findings were already addressed in follow-up commit `cbac32ba` (ParsedImport discriminated union + `ScopeId | null` on the two hooks). - #915: the five in-scope findings were already addressed in follow-up commit `54515a7e` (dead code, unused params, multi-hop docs, cap-hit test, stats granularity). - #915 LanguageProvider.resolveImportTarget signature divergence + `findDefById` O(F×D) perf: tracked separately as follow-up issues for the Ring 3 migration window. * chore(shared): address ce:review findings on the follow-up diff ce:review (interactive) on PR #964 surfaced two P2s and several P3s. This commit applies all `safe_auto` fixes + both manual tests in-line so the PR ships with a cleaner review trail. ## P2 fixes - **Complete `freezeIndex` → `wrapIndex` rename.** The prior commit renamed 3 of 5 sibling index files; `method-dispatch-index.ts` and `position-index.ts` still carried the old name. Now all 5 helpers use the consistent `wrapIndex` naming. (maintainability + project-standards reviewers both flagged this.) - **Add regression tests for the `recordTypeBindingHit` origin demotion.** The prior commit introduced the `tieBreakKey.origin = 'import'` demotion for Step-2-only candidates without a direct test. Added: - `registries.test.ts`: two Step-2-only siblings under the same interface, asserting deterministic DefId.localeCompare tie-break AND the stronger invariant that composeEvidence never emits a where-found signal for Step-2-only candidates (no `signals.origin`). - `position-index.test.ts`: touching-boundary test proving the right-sibling-wins rule documented in the new JSDoc. (testing + kieran-typescript + api-contract reviewers all flagged these gaps.) ## P3 fixes - Fix wrong comment in `recordTypeBindingHit` that claimed Step 1 could later upgrade a demoted origin. Step 1 runs BEFORE Step 2 — the actual upgrade path is Step 3 (`seedFromOwnerScopedContributor`). Comment now describes execution order correctly. - Fix inaccurate "re-exported there" comment in `index.ts`. `types.ts` *defines* ScopeLookup natively; it's not a re-export. Phrasing now says "defined in types.ts and exported from the type-export block above — not from this module." - Update stale `scope-tree.ts` file-header prose that still referenced `ScopeLookup` as living in #916/resolve-type-ref.ts. Now points to `./types.js` with a cross-ref to both #916 and #917 consumers. - Expand `atPosition` touching-boundary JSDoc to name the mechanism (backward scan through start-sorted array) so readers can trace the binary-search code to the claim. - Add breadcrumb to `aggregate.ts` module header pointing future readers to `./diff.ts` / the top-level barrel for `ShadowAgreement`, `ShadowCallsite`, and `ShadowDiff`. - Remove unnecessary non-null assertion in `recordTypeBindingHit`. Local `const existingMroDepth = ...` lets TS narrow to `number` in the else-branch, eliminating the `!` without behavior change. ## Verification - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`) - `gitnexus-shared` build clean - Combined scope-resolution / model / shadow suite: **262/262 pass** (+2 from the new origin-demotion + touching-boundary regression tests) |
||
|
|
1bf9fb4ef1
|
feat(shared): ClassRegistry / MethodRegistry / FieldRegistry + 7-step lookup (#917, RFC #909 Ring 2 SHARED) (#963)
Capstone of Ring 2 SHARED. Implements RFC §4 — the shared, scope-aware
resolution surface the rest of the semantic model feeds into.
## Modules (`gitnexus-shared/src/scope-resolution/registries/`)
- `context.ts` — `RegistryContext` bundling ScopeTree / DefIndex
/ QualifiedNameIndex / ModuleScopeIndex /
MethodDispatchIndex + provider hooks.
Narrows Ring 1's opaque `RegistryContributor`
to concrete `OwnerScopedContributor`.
- `tie-breaks.ts` — `compareByConfidenceWithTiebreaks`, the RFC
Appendix B cascade: confidence DESC → scope
depth ASC → MRO depth ASC → ORIGIN_PRIORITY
ASC → DefId.localeCompare.
- `evidence.ts` — `composeEvidence(signals)` / `confidenceFromEvidence`.
Translates raw walk signals into the typed
`ResolutionEvidence[]` using authoritative
`EvidenceWeights`. No magic numbers.
- `lookup-qualified.ts`— RFC §4.5. Qualified-name fast path consumed
by `resolveTypeRef` dotted fallback and by
Step 6 of lookup-core.
- `lookup-core.ts` — The 7-step canonical algorithm. Pure. Param-
eterized by `CoreLookupParams`.
- `{class,method,field}-registry.ts`
— Thin wrappers over `lookupCore` that fix
`acceptedKinds` + `useReceiverTypeBinding` per
kind. `buildClassRegistry` / `buildMethodRegistry`
/ `buildFieldRegistry` factory functions.
## RFC §4.2 algorithm contract (honored verbatim)
1. Lexical scope-chain walk. Hard shadow on any `scope.bindings.has(name)`
regardless of kind survivorship.
2. Type-binding resolution (methods/fields only, opt-in via
`useReceiverTypeBinding`). MRO walk via `MethodDispatchIndex.mroFor`.
MRO-depth-decayed weight via `typeBindingWeightAtDepth`.
3. Owner-scoped contributor — when the caller knows the receiver owner,
its direct members merge in as `origin: 'local'`.
4. Kind filter — `acceptedKinds` per registry; `kind-match` evidence
at weight 0 is always emitted for debuggability.
5. Arity filter — `provider.arityCompatibility` per candidate. When at
least one compatible candidate exists, incompatibles are dropped;
otherwise the −0.15 penalty alone disambiguates (they stay in the
result, just ranked lower).
6. Global fallback — fires only when Steps 1-3 produced NO candidates
AND the name is dotted. Delegates to `lookupQualified`.
7. Rank + tie-break — evidence list sorted by the Appendix B cascade.
## §4.7 invariants asserted in tests
- No tier vocabulary in the return type (`Resolution`, not `TierXResult`).
- Confidence is per-candidate (not per-tier).
- Shadowing is a HARD filter; globals are consulted ONLY when lexically
empty.
- Caller can read `[0]` for one-shot answers.
- `Resolution.confidence` is capped at 1.0.
- `kind-match` is always emitted (weight 0).
## Unresolved-import + dynamic-unresolved evidence shape
- `BindingRef.via.linkStatus === 'unresolved'` applies the
`unlinkedImportMultiplier` (0.5×) to the where-found signal only.
Corroborators (`arity-match`, `owner-match`, `type-binding`) remain
unaffected — the RFC §4v2 capped-signal rule applies per-signal, not
per-candidate.
- `BindingRef.via.kind === 'dynamic-unresolved'` adds a degraded
`dynamic-import-unresolved` evidence signal at weight 0.02.
## Tests (28 in registries.test.ts, 259/259 combined)
Organized per RFC §4.2 step so a regression localizes to the step it broke:
- Step 1: local + walk-to-parent + hard-shadow + origin=import
- Step 2: explicit receiver type-binding + MRO depth decay on ancestor
- Step 3: owner-scoped contributor + owner-match
- Step 5: drop-incompatible-when-compatible-exists + soft-penalty-when-all-
incompatible + unknown-when-no-provider
- Step 6: global-qualified fires only when lexically empty + never for
non-dotted names + not consulted when lexical hit exists
- Step 7: tie-break cascade (inner shadows outer; defId.localeCompare
final)
- Corroborators: unresolved-import 0.5× cap per-signal + dynamic-
unresolved 0.02 degraded signal
- §4.5: lookupQualified kind filter + empty on miss + deterministic defId
order for partial classes
- §4.7: invariants — confidence per-candidate, capped at 1.0, kind-match
always present, [0]-for-one-shot
## Known follow-up optimizations
`collectOwnedMembers` in `lookup-core.ts` iterates `defs.byId.values()`
for each MRO hop — O(D) per call. Acceptable for Ring 2 fixtures; a
by-owner index should land before Ring 3 migrates large-workspace
languages. Tracked alongside the existing `findDefById` follow-up from
#915 review.
## Module placement
All under `gitnexus-shared/src/scope-resolution/registries/` — consistent
with the Ring 2 SHARED folder layout (#912/#913/#914/#915/#916/#918).
Slight deviation from the issue's `gitnexus-shared/src/registries/`
suggestion for consistency with siblings.
## Part of
- Parent: #909
- Depends on (code): #910, #911, #912, #913, #914, #915, #916, #918.
- Closes the Ring 2 SHARED delivery band. Unblocks Ring 2 PKG (#919–#925
bridges to the gitnexus/ CLI package) and Ring 3 language migrations.
|
||
|
|
a9a5e1c388
|
feat(shared): SCC-aware finalize algorithm with bounded fixpoint (#915, RFC #909 Ring 2 SHARED) (#962)
* feat(shared): SCC-aware finalize algorithm with bounded fixpoint (#915, RFC #909 Ring 2 SHARED) Implements RFC §3.2 Phase 2 as pure logic in `gitnexus-shared`. Takes per-file parse output and returns linked `ImportEdge[]` + materialized module-scope bindings, fully language-agnostic (target resolution, wildcard expansion, and binding precedence all go through caller hooks). Three-phase algorithm: 1. Tarjan SCC over the file-level import graph (iterative, deterministic node order, O(V+E)). Returns SCCs in reverse-topological order so leaves finalize before dependents — and so disjoint SCCs are explicitly surfaced for parallel-processing callers. 2. Per-SCC bounded fixpoint. For each SCC in topo order, iterate up to `N = |intra-SCC edges|`; each pass tries to resolve every still- unlinked edge by looking up the imported name in the target file's local defs. Stops early when no progress. Edges still unlinked after the cap get `linkStatus: 'unresolved'` — keeps malformed inputs bounded and preserves the RFC §4v2 capped-signal contract for unresolved markers. 3. Wildcard expansion + module-scope binding materialization. For each `wildcard` ParsedImport that linked to a module, expand via `expandsWildcardTo` into one `wildcard-expanded` ImportEdge per exported name. Bindings per module scope are the merge of local defs (`origin: 'local'`), named / alias / reexport imports (`origin: 'import' | 'reexport'`), namespace imports (`origin: 'namespace'`), and wildcard expansions (`origin: 'wildcard'`), with precedence delegated to `provider.mergeBindings`. Dynamic imports rule: `kind: 'dynamic-unresolved'` passes through as an ImportEdge with `targetFile: null` and no BindingRef. Re-export flattening: reexport edges land with `transitiveVia: [targetFile]`. Multi-hop chains settle iteratively across the fixpoint. Types: - Adds `'wildcard'` variant to ParsedImport (parse-time signal for `import * from M`). The finalize-only `'wildcard-expanded'` ImportEdge kind is unchanged and remains finalize output only, as documented. - Exports `finalize` + `FinalizeFile` / `FinalizeInput` / `FinalizeHooks` / `FinalizeOutput` / `FinalizedScc` / `FinalizeStats`. Simple-name derivation: `deriveSimpleName` uses `def.qualifiedName` as the authoritative source (tail after the last `.`). Defs without a qualifiedName are not name-resolvable by this algorithm — an explicit design choice that trades strictness for predictability (no heuristic nodeId parsing). Tests (20, all passing): - Trivial: empty workspace · acyclic resolution · unresolvable target (file + name) · dynamic-unresolved passthrough. - Cycles: A↔B two-file cycle linked · cycles packed into SCC with isCycle=true · disjoint cycles produce disjoint SCCs · mixed linked/unresolved edges reported correctly in stats. - Wildcards: one ImportEdge per exported name · unresolved wildcards survive as single edges · expanded bindings carry origin='wildcard'. - Reexports: transitiveVia carries the intermediate file path. - Aliased + namespace: alias preserves targetExportedName under its local name · namespace links to module scope even without a module-def. - Bindings: locals land as origin='local' · imports layer on via mergeBindings · mergeBindings can drop existing (last-write-wins precedence honored). - SCC-DAG: reverse-topological ordering verified (leaf first). Combined scope-resolution / model / shadow suite: 229/229 pass. `tsc --noEmit` clean in both `gitnexus-shared` and `gitnexus`. Closes part of #909. Unblocks #917 (Registry.lookup's import-chain fast path consumes finalized ImportEdges); unblocks Ring 3 language migrations (per-language providers supply FinalizeHooks implementations). * chore(shared): address #915 review findings — dead code, docs, tests Review thread on PR #962. Code changes: - Remove dead `resolvedTargets` map + `keyFor` + `ParsedImportKey` type alias. The map was populated but never read; originally intended to cache / dedup resolutions for later phases but that path was never wired (finding 1.1). - Drop unused params (`_edgeIndex`, `_hooks`, `_workspace`) from `tryFinalize`. No planned fixpoint-state consultation; no reason to keep them reserved (finding 2.1). Documentation: - `FinalizeFile.localDefs` now documents the multi-hop re-export contract explicitly: `finalize` looks names up in the target's static `localDefs`; if B only re-exports from C and doesn't surface the name in its own localDefs, A's import of that name from B will hit the cap and be marked unresolved. Parsers that want multi-hop chains to settle end-to-end must include re-exported names in the intermediate file's localDefs (finding 1.2). - `FinalizeStats` now documents its counting granularity: all edge counters are per-`ParsedImport`, not per-materialized-`ImportEdge`. A wildcard expanding to N exports counts as one linked edge; dynamic-unresolved pass-throughs count as linked. The bindings map is the authoritative "has a BindingRef" source (finding 3.2). Tests (2 added, 22 total in finalize-algorithm.test.ts, 231/231 combined): - Explicit cap-hit → `linkStatus: 'unresolved'` assertion for a cycle where the name-level lookup never succeeds (distinct from `targetFile: null`; cap exhaustion path) (finding 3.1). - Multi-hop re-export contract test: demonstrates both variants — intermediate B WITHOUT X in localDefs → unresolved; B WITH X in localDefs → resolved to the original source DefId (finding 1.2). Not addressed (filed as follow-up issues): - LanguageProvider.resolveImportTarget vs FinalizeHooks signature divergence (finding 1.3) — pre-Ring-3 concern. - findDefById O(F×D) scan in Phase 5 (finding 4.1) — acceptable for Ring 2; optimize before large-workspace Ring 3 migrations. |
||
|
|
8cf9ae0e0d
|
feat(shared): ScopeTree + PositionIndex + makeScopeId (#912, RFC #909 Ring 2 SHARED) (#961)
Implements the scope-tree spine and position-indexed lookup as pure logic in `gitnexus-shared`. Generalizes the `enclosingFunctions` pattern from closed PR #902 to arbitrary `ScopeKind`s. Three modules under `gitnexus-shared/src/scope-resolution/`: 1. `scope-id.ts` — `makeScopeId({filePath, range, kind})` builds the canonical RFC §2.2 shape `scope:{filePath}#{startLine}:{startCol}-{endLine}:{endCol}:{kind}` and interns the result through a process-local pool so repeated calls with structurally identical inputs return the same string reference. `clearScopeIdInternPool()` exported for test isolation. 2. `scope-tree.ts` — `buildScopeTree(scopes)` validates invariants and returns an immutable `ScopeTree`: - `getScope(id)` / `getParent(id)` / `getChildren(id)` / `getAncestors(id)` - Implements the `ScopeLookup` contract from #916, so `resolveTypeRef` can consume a `ScopeTree` directly (test included). Invariants enforced (throw `ScopeTreeInvariantError` on violation): - Non-Module scopes must have a parent. - Parent must exist in the supplied set. - Parent range STRICTLY contains child range (equal ranges rejected). - Sibling ranges under the same parent do not overlap. Ranges that merely touch at the boundary (`a.end == b.start`) are accepted. - Parent and child live in the same filePath. - Duplicate scope ids are rejected. 3. `position-index.ts` — `buildPositionIndex(scopes)` produces a `PositionIndex` with `atPosition(filePath, line, col)`. Per-file sorted array; binary-search the upper bound of `start ≤ query`, scan backward through the prefix, return the first containing hit. Complexity: `O(log N_file + D)` typical (D = lexical depth ≤ ~10); degrades to `O(N_file)` only under pathological inputs (many scopes starting at the same position). "Innermost wins" falls out of the sort + backward-scan contract because `ScopeTree`'s invariants guarantee that scopes containing a point form an ancestor chain. Types: - `ScopeTree` now exported from `scope-tree.ts`. The Ring 1 opaque placeholder in `types.ts` has been removed; LanguageProvider hooks that previously took `ScopeTree = unknown` now receive the concrete interface (CLI `tsc --noEmit` passes — no existing callers rely on the opaque shape). Tests (39, all passing): - scope-id: canonical shape · all six ScopeKinds encoded · identity equality (same inputs → same reference) · distinguished by filePath / range / kind · purity under repeated calls · intern-pool clear preserves canonical shape. - scope-tree: empty tree · single module · nested Module→Class→Function · multiple siblings input-order preserved · ScopeLookup integration with resolveTypeRef · frozen children and ancestor arrays · all six invariant violations (non-Module orphan, parent-not-found, parent doesn't contain, parent == child, siblings overlap, cross-file parent, duplicate id) · boundary-touching siblings accepted. - position-index: empty · unindexed filePath · before/after-file queries · start/end inclusivity · innermost-wins for nested / co- starting / co-ending / same-line scopes · sibling dispatch · multi- file isolation · size · id-dedup. Combined scope-resolution / model / shadow suite: 190/190 pass. `tsc --noEmit` clean in both `gitnexus-shared` and `gitnexus`. Closes part of #909. Unblocks #917 (`Registry.lookup` needs the scope spine); makes `ScopeLookup` in #916 concrete without API churn. |
||
|
|
5d76dbcfa2
|
feat(shared): MethodDispatchIndex materialized view over HeritageMap (#914, RFC #909 Ring 2 SHARED) (#960)
Implements RFC §3.1 `MethodDispatchIndex`: a two-way materialized view
keyed by `DefId` for O(1) method-dispatch resolution:
- `mroByOwnerDefId` — owner class → full MRO ancestor chain
(excludes self, per-language strategy order)
- `implsByInterfaceDefId` — interface/trait → classes that implement it
**Not an MRO implementation.** `buildMethodDispatchIndex` is a pure
aggregator that calls back into caller-provided `computeMro` and
`implementsOf` functions. The five existing strategies (Python C3, Ruby
kind-aware, Java/Kotlin linear, Rust qualified-syntax, COBOL none) stay
where they are today (`model/resolve.ts`, `languages/ruby.ts`); this index
does not reimplement them.
Why callbacks rather than a shared registry: the strategies depend on the
CLI's `HeritageMap` + `SemanticModel`. Migrating both to `gitnexus-shared`
is out of scope for #914; callbacks let the shared build stay pure.
Module placement: `gitnexus-shared/src/scope-resolution/method-dispatch-index.ts`
for consistency with the other RFC §3.1 indexes (#913 DefIndex /
ModuleScopeIndex / QualifiedNameIndex; #916 resolveTypeRef).
Safety surface mirrors sibling indexes:
- First-write-wins on duplicate owners.
- Repeated (interface, owner) pairs deduplicated.
- Stored arrays are `Object.freeze`d; caller mutation of the source
array does not leak into the index.
- Miss returns a shared frozen empty array.
Tests (19, all passing): empty input, single-inheritance chain, Python
C3 diamond, Java BFS, Ruby kind-aware mixin, Rust qualified-syntax empty,
interface inversion (single, multiple, ordered), dedup within and across
callback calls, frozen miss + bucket arrays, callback-array isolation,
readonly Map iteration.
Closes part of #909.
|
||
|
|
56e32b310b
|
feat(shared): resolveTypeRef strict single-return type resolver (#916, RFC #909 Ring 2 SHARED) (#959)
Implements RFC §4.6: a strict, pure resolver for `TypeRef`s used by
`Registry.lookup` Step 2 (type-binding propagation) and by any caller that
wants the single best type-target for an annotation without paying for the
full evidence pipeline.
Algorithm (strict):
1. Walk the scope chain from `ref.declaredAtScope`:
- Return the first binding for `rawName` whose origin is in
`{'local','import','namespace','reexport'}` AND whose `def.type` is a
type-kind (class-like, interface-like, enum-like, alias-like).
- If bindings exist but none qualify (non-type shadow, wildcard-only
origin), return null immediately — do NOT fall through to the global
qualified-name index.
2. If `rawName` is dotted and the scope walk produced no match, consult
`QualifiedNameIndex.byQualifiedName`. Only accept a UNIQUE type-kind
hit; ambiguous or non-type results return null.
`'wildcard'` is deliberately excluded from strict origins — a
wildcard-expanded name is too loose to anchor type resolution.
Module placement: `gitnexus-shared/src/scope-resolution/resolve-type-ref.ts`
(alongside sibling indexes) rather than the issue's suggested
`gitnexus-shared/src/resolve-type-ref.ts`, for consistency with the rest of
the RFC §2/§3 surface.
A minimal `ScopeLookup` interface is declared inline so #916 ships
standalone; #912's `ScopeTree` will satisfy this contract without change.
Closes part of #909.
|
||
|
|
ac2012e5ed
|
feat(shared): DefIndex / ModuleScopeIndex / QualifiedNameIndex (#913, RFC #909 Ring 2 SHARED) (#958)
Three flat O(1) indexes + pure build functions over per-file artifacts. Contract-only; no runtime behavior change yet — consumers (#917 Registry lookups, #915 SCC finalize, #919 ScopeExtractor) wire in later. Each index follows the same shape: - build function: flat input list → frozen immutable index - public interface: readonly Map + get/has/size accessors - first-write-wins on id/filePath collisions (upstream bug signal) - pure, side-effect-free, safe to call repeatedly DefIndex — the global "what is this id?" lookup gitnexus-shared/src/scope-resolution/def-index.ts buildDefIndex(defs: readonly SymbolDefinition[]): DefIndex byId: ReadonlyMap<DefId, SymbolDefinition> Consumed by Registry.lookup (#917) to materialize DefId[] hits back to full SymbolDefinition records. ModuleScopeIndex — `filePath → moduleScopeId` for cross-file hops gitnexus-shared/src/scope-resolution/module-scope-index.ts buildModuleScopeIndex(entries): ModuleScopeIndex byFilePath: ReadonlyMap<string, ScopeId> Consumed by the SCC finalize link pass (#915) to resolve ImportEdge.targetFile to a concrete module scope in constant time. QualifiedNameIndex — cross-kind qualified-name fast path gitnexus-shared/src/scope-resolution/qualified-name-index.ts buildQualifiedNameIndex(defs: readonly SymbolDefinition[]): QualifiedNameIndex byQualifiedName: ReadonlyMap<string, readonly DefId[]> Returns DefId[] (not a single DefId) because partial classes, method overloads, and cross-kind collisions can legitimately share a qualifiedName. Callers filter by acceptedKinds at the lookup site. Consumed by Registry.lookup qualified fast path + resolveTypeRef dotted fallback (#916, #917). Barrel re-exports added to gitnexus-shared/src/index.ts so consumers import from 'gitnexus-shared' rather than deep paths. Tests (gitnexus/test/unit/scope-resolution/, 23 total): def-index.test.ts (6): empty, single def, multiple distinct, first-write-wins collision, missing id returns undefined, byId direct iteration module-scope-index.test.ts (6): empty, single entry, multiple files, first-write-wins on duplicate filePath, missing returns undefined, byFilePath direct iteration qualified-name-index.test.ts (11): empty, single qnamed def, partial classes accumulate, input-order preservation, qname separation, skip undefined/empty qname, pair dedup, cross-kind indexing, frozen-empty-array on miss, direct iteration Verification: - gitnexus-shared + gitnexus build clean (tsc + scripts/build.js) - test/unit/scope-resolution: 23/23 pass - model + shadow + scope-resolution combined: 129/129 pass - No runtime consumer wiring yet — indexes are standalone library functions that #915, #917, #919 will import when ready Depends on #910 (SymbolDefinition, DefId, ScopeId types — already on main). Unblocks #915 (finalize algorithm), #917 (Registry.lookup), #919 (ScopeExtractor materialization). |
||
|
|
22f0beb057
|
feat(shared): shadow-mode diff + aggregate — full implementation (#918, RFC #909 Ring 2 SHARED) (#951)
Replaces the scaffold stubs with working pure-logic implementations plus
unit-test coverage for both functions. Unblocks Ring 2 PKG #923 (shadow
harness) to consume a concrete library instead of throwing scaffolds.
gitnexus-shared/src/scope-resolution/shadow/diff.ts
`diffResolutions(callsite, legacy, newResult): ShadowDiff`
- [0] on each side is the top match
- both empty → 'both-empty', delta []
- legacy empty only → 'only-new', delta = new top evidence
- new empty only → 'only-legacy', delta = legacy top evidence
- same top nodeId → 'both-agree', delta []
- different nodeIds → 'both-disagree',
delta = symmetric difference of evidence kinds
(legacy-only first in input order, then new-only)
Evidence identity is `ResolutionEvidence.kind` — weight/note differences
for the same kind do NOT produce delta entries. Rationale: the aggregator
wants to know which *signals* explain a disagreement, not fluctuations
in calibration values.
gitnexus-shared/src/scope-resolution/shadow/aggregate.ts
`aggregateDiffs(diffs, now?): ShadowParityReport`
- buckets by `SupportedLanguages`
- tallies agreements, evidence-breakdown (divergences only — agree and
empty rows do not contribute)
- parity = bothAgree / (totalCalls - bothEmpty), yields 0 (not NaN)
when the denominator is 0
- perLanguage sorted alphabetically by enum value for stable output
- evidenceBreakdown internally sorted by kind for stable output
- overall = column-wise sum across languages
- `now` parameter makes generatedAt deterministic in tests
gitnexus-shared/src/index.ts
Re-exports the full shadow API: diffResolutions, aggregateDiffs, and all
their types (ShadowAgreement, ShadowCallsite, ShadowDiff,
LanguageParityRow, ShadowParityReport).
gitnexus/test/unit/shadow/diff.test.ts (13 tests)
- 5 agreement outcomes
- symmetric-by-kind evidence delta (disjoint, overlapping, fully-overlapping)
- weight-only differences produce no delta
- top-match only (ignores indices beyond [0])
- callsite passthrough
- delta ordering (legacy-only first, input order preserved)
gitnexus/test/unit/shadow/aggregate.test.ts (9 tests)
- empty input
- single language, all agree / mixed / all empty
- multi-language bucketing + overall sum
- alphabetical language sort
- evidence breakdown scope
- determinism via injected `now` + JSON round-trip identity
Verification:
- gitnexus-shared + gitnexus build clean (tsc + scripts/build.js)
- test/unit/shadow: 22/22 pass
- test/unit/model + test/unit/shadow combined: 106/106 pass
- No runtime behavior changes (shadow is invoked by #923, not yet wired)
Stacked on main (
|
||
|
|
af1d278a7e
|
feat(shared,ingestion): extend LanguageProvider with scope-resolution hooks (#911, RFC #909 Ring 1) (#950)
Adds the 14 optional scope-resolution hooks from RFC #909 §5.2 to
`LanguageProviderConfig` plus the supporting input/output types in
`gitnexus-shared`. Contract-only; no runtime behavior changes.
Review-driven refinements (addresses two non-blocking review comments on #950):
1. `ParsedImport` is now a 5-variant discriminated union, not a flat
record. Each variant carries only its legal fields so invalid shapes
are compile errors:
- 'named', 'alias', 'namespace', 'reexport', 'dynamic-unresolved'
'wildcard-expanded' is deliberately excluded — finalize materializes
that kind; a provider must never emit it at parse time.
'reexport' is a first-class parse-phase variant so syntactically-
detectable re-exports (TS `export { X } from './y'`, Rust
`pub use foo::bar`) keep their parse-time signal through to finalize
rather than being re-derived by the SCC pass.
`namespace` gains an `importedName` field so `import numpy as np`
can carry both `localName: 'np'` and `importedName: 'numpy'`.
`dynamic-unresolved.targetRaw` is `string | null` (was mandatory
null) so providers can emit the unresolvable expression text for
diagnostics when available.
2. `bindingScopeFor` and `importOwningScope` return type changed from
`ScopeId` to `ScopeId | null`, aligning with the X | null convention
used by the 12 sibling optional hooks (receiverBinding,
resolveScopeKind, interpretTypeBinding, …). `null` = delegate to the
central default. Enables partial overrides — a JS provider can
return a hoisted scope for `var` and `null` for `let`/`const`
without re-implementing the default lookup.
Both hooks also gain a purity JSDoc contract: same inputs yield the
same ScopeId (or null) across invocations; no closure over mutable
state. Required to keep scope-tree construction deterministic.
A richer callable-defaults pattern (typed BindingScopeDefaults /
ImportOwningDefaults helper interfaces on a `defaults` parameter)
was considered and deferred to Ring 2 PKG #919, where the concrete
ScopeExtractor will exist to inform the helper shape. Designing that
pattern before the first consumer would set cross-hook precedent
based on a single motivating example.
Supporting types added to gitnexus-shared/src/scope-resolution/types.ts:
- CaptureMatch, ParsedImport, ParsedTypeBinding
- WorkspaceIndex, ScopeTree (opaque placeholders until Ring 2)
- Callsite
14 hooks added to LanguageProviderConfig (all optional):
Parse phase: emitScopeCaptures, interpretImport, receiverBinding,
interpretTypeBinding, resolveScopeKind, shouldCreateScope,
bindingScopeFor
Finalize phase: resolveImportTarget, expandsWildcardTo,
importOwningScope, mergeBindings
Reference-extraction phase: classifyCallForm
Resolution phase: shouldShadow, arityCompatibility
Verification:
- gitnexus-shared builds clean (tsc)
- gitnexus builds clean (scripts/build.js)
- test/unit/model: 84/84 pass — no regressions
- No provider needs updating (all hooks optional)
- No BindingScopeDefaults/ImportOwningDefaults/defaults parameter
introduced (deferred to #919)
Stacked on #910 (merged as
|
||
|
|
afc0a8b6c5
|
feat(shared): add scope-resolution types + constants (#910, RFC #909 Ring 1) (#949)
Lands the authoritative data model and constants for the pure scope-based resolution RFC (#909) as Ring 1, part 1. No runtime behavior changes — types + constants only. New in gitnexus-shared/src/scope-resolution/: - types.ts — Scope, ScopeKind, ScopeId, DefId, Range, Capture, BindingRef, ImportEdge, TypeRef, Resolution, ResolutionEvidence, Reference, ReferenceIndex, LookupParams, RegistryContributor - evidence-weights.ts — EvidenceWeights constant map + typeBindingWeightAtDepth (RFC Appendix A) - origin-priority.ts — ORIGIN_PRIORITY constant map for deterministic tie-breaks (RFC Appendix B) - language-classification.ts — LanguageClassification type + LanguageClassifications map (production × 14, experimental × 2 for vue/cobol; governs Ring 4 DAG-retirement gate) - symbol-definition.ts — SymbolDefinition moved from gitnexus/src/core/ingestion/model/symbol-table.ts so scope-resolution types can reference it from the shared package Consumer updates: - symbol-table.ts: removes local SymbolDefinition declaration; imports from gitnexus-shared - model/index.ts: drops SymbolDefinition from barrel re-export per "direct imports from gitnexus-shared" convention (see gitnexus-shared feedback in project memory) - 9 source files + 5 test files: import SymbolDefinition directly from 'gitnexus-shared' Verification: - gitnexus-shared builds clean (tsc) - gitnexus builds clean (scripts/build.js) - 131/132 unit test files pass; 3767 tests green - Zero behavior changes; SymbolDefinition shape unchanged Blocks: #911 (LanguageProvider hook interface extensions) and all of Ring 2 (#912-#925). Closes part of #909. |
||
|
|
dfa449ef41
|
feat(ingestion): language-agnostic heritage extractor with config+factory pattern (#890) | ||
|
|
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> |
||
|
|
a94d6ef80b
|
Extract registries into model/ module with SemanticModel interface (#786)
* 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> |
||
|
|
0561d24efd
|
feat: METHOD_IMPLEMENTS edges, overload disambiguation, MethodExtractor unification (#574) (#642) | ||
|
|
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>
|
||
|
|
c72890d59d
|
feat(csharp): C# MethodExtractor config (#582)
* feat(csharp): add C# MethodExtractor config (#573) Add C# method extraction config mirroring the JVM pattern from PR #576. Wire csharpMethodConfig into the C# language provider and add 18 tests covering classes, interfaces, abstract classes, structs, records, constructors, params/out/ref/optional parameters, sealed methods, attributes, and visibility modifiers. * fix(csharp): add destructor, operator, conversion operator, and in-param support - Add destructor_declaration, operator_declaration, and conversion_operator_declaration to methodNodeTypes - Custom extractName for operators (e.g., "operator +", "implicit operator double") - Fix extractReturnType for operator declarations (use type field, not returns) - Add in modifier to parameter extraction (alongside out/ref) - Add 4 new tests: destructor, operator+, implicit conversion, in parameter * fix(csharp): add ref param test and document compound visibility limitation - Add test for ref parameter modifier (was only testing out) - Document that protected internal / private protected resolve to first modifier * feat(csharp): support compound visibilities (protected internal, private protected) - Add 'protected internal' and 'private protected' to FieldVisibility union - Detect compound modifiers in both C# method and field extractors via collectModifierTexts helper scanning adjacent modifier nodes - Add 2 tests for compound visibility detection * feat(csharp): primary constructors, virtual/override/async, primary fields Address all known limitations from review: - Primary constructor support (C# 12): add extractPrimaryConstructor to MethodExtractionConfig and extractPrimaryFields to FieldExtractionConfig. Record params become public readonly properties; class params become private captured fields. - Add isVirtual, isOverride, isAsync optional fields to MethodInfo, MethodExtractionConfig, NodeProperties, and parse-worker propagation. - Detect virtual/override/async modifiers in C# method config. - Move collectModifierTexts to shared helpers.ts (deduplicate). - Fix destructor name to ~ClassName (disambiguates from constructor). - Add expression-bodied method test. - 118 tests total across method + field extraction suites, all passing. * fix(csharp): review round 2 — annotations, record_struct, grammar pin - Fix primary constructor annotations: use [] instead of extracting class-level attributes (C# has no syntax for ctor-specific attributes) - Add record_struct_declaration to typeDeclarationNodes in both method and field extractors, CLASS_CONTAINER_TYPES, and isRecord visibility check - Pin tree-sitter-c-sharp version (^0.23.1) in params comment * fix(csharp): complete record_struct query + label mapping, sealed override test - Add record_struct_declaration capture patterns to tree-sitter-queries.ts (type definition + primary constructor) - Add record_struct_declaration → 'Struct' in CONTAINER_TYPE_TO_LABEL - Assert isOverride: true alongside isFinal in sealed override test * fix(csharp): record_struct label mismatch, add record struct + documented limitation tests - Fix record_struct_declaration query tag: @definition.struct (not @definition.record) to match CONTAINER_TYPE_TO_LABEL and prevent broken HAS_METHOD edges - Add 3 record struct tests: isTypeDeclaration, method extraction, primary constructor - Add documented limitation tests: partial method (isAbstract: false), generic type parameter stripping (name excludes <T>) * fix(csharp): remove record_struct_declaration — not a real tree-sitter node type tree-sitter-c-sharp 0.23.1 parses 'record struct' as record_declaration (absorbs the 'struct' keyword as an unnamed child token). The non-existent record_struct_declaration in queries caused TSQueryErrorNodeType, breaking ALL C# file processing. Remove from: tree-sitter-queries.ts, typeDeclarationNodes in both extractors, CLASS_CONTAINER_TYPES, and CONTAINER_TYPE_TO_LABEL. Record struct types are already handled via record_declaration. * feat(csharp): add isPartial support, filter targeted attributes, static ctor test - Add isPartial optional field to MethodInfo, MethodExtractionConfig, NodeProperties, and parse-worker propagation pipeline - Detect partial modifier in C# config — marks both declaration-only and implemented partial methods - Filter targeted attribute lists (e.g. [return: MarshalAs(...)]) in extractCSharpAnnotations — only untargeted attributes collected - Add static constructor test (isStatic: true, same name as class) - Add 3 partial method tests: declaration-only, with body, coexisting pair - Document record_struct/record_class as defensive dead code in export-detection.ts (grammar absorbs keywords into record_declaration) * fix(csharp): this param for extension methods, dedup visibility, test fixes - Handle this modifier on extension method parameters (type prefixed as 'this string', consistent with out/ref/in handling) - Deduplicate visibility logic in extractPrimaryConstructor — reuse csharpMethodConfig.extractVisibility instead of inline compound check - Fix record struct test title to reflect actual grammar behavior - Add conversion operator returnType assertion - Add extension method this parameter test * fix(csharp): primary constructor line points to param list, empty name guard - Use paramList.startPosition instead of ownerNode.startPosition for primary constructor line number (avoids methodInfoCache key collision) - Guard against empty param names from tree-sitter error recovery nodes |
||
|
|
313b13fade
|
feat(java,kotlin): MethodExtractor abstraction with per-language configs (#576) | ||
|
|
ddb6a704b3
|
refactor: reduce explicit any types (#566)
* refactor: replace NodeProperties index signature any with unknown Change [key: string]: any to [key: string]: unknown in NodeProperties. Remove 19 redundant (node.properties as any) casts in csv-generator.ts — all accessed properties are already declared on the type. * refactor: replace any with SyntaxNode across ingestion layer Mechanical substitution — all tree-sitter AST node parameters and variables typed as any are now properly typed as SyntaxNode. - ast-helpers.ts: 13 any → SyntaxNode - parsing-processor.ts: 8 any → SyntaxNode - parse-worker.ts: 40 any → SyntaxNode/TreeSitterLanguage/Parser.Query - php.ts: 11 any → SyntaxNode Also adds TreeSitterLanguage type alias for optional grammar loading. * refactor: eliminate remaining any in ingestion layer - call-processor, call-routing, c-cpp: SyntaxNode substitutions - parse-worker: typed WorkerIncomingMessage discriminated union - worker-pool: typed WorkerOutgoingMessage + Error handler - ast-cache, import-processor: targeted cast for Tree.delete() - community-processor: graphology AbstractGraph types, LeidenModule interface for vendored leiden code Ingestion layer: 130 → 7 any warnings remaining. |
||
|
|
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. |
||
|
|
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
|