gitnexus 1.6.6-rc.31 → 1.6.6-rc.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/cli/optional-grammars.d.ts +11 -8
  2. package/dist/cli/optional-grammars.js +12 -8
  3. package/dist/core/group/extractors/http-patterns/python.js +223 -14
  4. package/dist/core/ingestion/parsing-processor.js +12 -6
  5. package/dist/core/ingestion/scope-extractor.js +7 -1
  6. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +21 -0
  7. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +37 -0
  8. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +7 -1
  9. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +5 -0
  10. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +50 -2
  11. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +28 -0
  12. package/dist/core/ingestion/scope-resolution/scope/walkers.js +75 -23
  13. package/dist/core/ingestion/utils/ast-helpers.d.ts +30 -0
  14. package/dist/core/ingestion/utils/ast-helpers.js +99 -0
  15. package/dist/core/ingestion/workers/parse-worker.js +12 -6
  16. package/package.json +3 -6
  17. package/scripts/build-tree-sitter-dart.cjs +4 -0
  18. package/scripts/build-tree-sitter-proto.cjs +3 -4
  19. package/scripts/build-tree-sitter-swift.cjs +39 -0
  20. package/scripts/materialize-vendor-grammars.cjs +72 -0
  21. package/vendor/tree-sitter-dart/package.json +1 -1
  22. package/vendor/tree-sitter-proto/package.json +1 -1
  23. package/vendor/tree-sitter-swift/package.json +1 -8
  24. package/vendor/node_modules/node-addon-api/node_addon_api.Makefile +0 -6
  25. package/vendor/node_modules/node-addon-api/node_addon_api.target.mk +0 -106
  26. package/vendor/node_modules/node-addon-api/node_addon_api_except.target.mk +0 -110
  27. package/vendor/node_modules/node-addon-api/node_addon_api_except_all.target.mk +0 -106
  28. package/vendor/node_modules/node-addon-api/node_addon_api_maybe.target.mk +0 -106
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Optional grammar availability check.
3
3
  *
4
- * tree-sitter-dart and tree-sitter-proto are optionalDependencies that
5
- * require a `node-gyp rebuild` at install time. The build can be skipped
6
- * via GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or it can
7
- * silently soft-fail when the C++ toolchain is missing.
4
+ * tree-sitter-dart, tree-sitter-proto, and tree-sitter-swift are vendored
5
+ * under vendor/ and materialized into node_modules/ at postinstall. Dart
6
+ * and Proto are built from source with node-gyp; Swift ships platform
7
+ * prebuilds activated via node-gyp-build. All three can be skipped via
8
+ * GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
9
+ * soft-fail when the toolchain is missing (Dart/Proto) or no prebuild
10
+ * matches the host platform (Swift).
8
11
  *
9
12
  * Either path produces the same observable: the .node binding is absent
10
13
  * at runtime. This helper detects that condition and surfaces a single
11
- * stderr line per missing grammar so users learn why .dart/.proto support
12
- * is unavailable instead of silently getting a degraded index.
14
+ * stderr line per missing grammar so users learn why .dart/.proto/.swift
15
+ * support is unavailable instead of silently getting a degraded index.
13
16
  */
14
17
  export interface MissingGrammar {
15
18
  name: string;
@@ -19,8 +22,8 @@ export interface MissingGrammar {
19
22
  * Returns the list of optional grammars whose native binding cannot be
20
23
  * loaded. Actually `require()`s the package — `require.resolve` would
21
24
  * locate the entry path even when the `.node` binding is absent (the
22
- * `file:` package directory is installed regardless of postinstall
23
- * outcome), giving false negatives for the exact users we want to warn:
25
+ * package directory exists without a working `.node` binding), giving false
26
+ * negatives for the exact users we want to warn:
24
27
  * those who installed with `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` or whose
25
28
  * native rebuild soft-failed for missing toolchain.
26
29
  *
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Optional grammar availability check.
3
3
  *
4
- * tree-sitter-dart and tree-sitter-proto are optionalDependencies that
5
- * require a `node-gyp rebuild` at install time. The build can be skipped
6
- * via GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or it can
7
- * silently soft-fail when the C++ toolchain is missing.
4
+ * tree-sitter-dart, tree-sitter-proto, and tree-sitter-swift are vendored
5
+ * under vendor/ and materialized into node_modules/ at postinstall. Dart
6
+ * and Proto are built from source with node-gyp; Swift ships platform
7
+ * prebuilds activated via node-gyp-build. All three can be skipped via
8
+ * GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
9
+ * soft-fail when the toolchain is missing (Dart/Proto) or no prebuild
10
+ * matches the host platform (Swift).
8
11
  *
9
12
  * Either path produces the same observable: the .node binding is absent
10
13
  * at runtime. This helper detects that condition and surfaces a single
11
- * stderr line per missing grammar so users learn why .dart/.proto support
12
- * is unavailable instead of silently getting a degraded index.
14
+ * stderr line per missing grammar so users learn why .dart/.proto/.swift
15
+ * support is unavailable instead of silently getting a degraded index.
13
16
  */
14
17
  import { createRequire } from 'module';
15
18
  import { cliWarn } from './cli-message.js';
@@ -17,13 +20,14 @@ const _require = createRequire(import.meta.url);
17
20
  const OPTIONAL_GRAMMARS = [
18
21
  { name: 'tree-sitter-dart', pkg: 'tree-sitter-dart', extensions: ['.dart'] },
19
22
  { name: 'tree-sitter-proto', pkg: 'tree-sitter-proto', extensions: ['.proto'] },
23
+ { name: 'tree-sitter-swift', pkg: 'tree-sitter-swift', extensions: ['.swift'] },
20
24
  ];
21
25
  /**
22
26
  * Returns the list of optional grammars whose native binding cannot be
23
27
  * loaded. Actually `require()`s the package — `require.resolve` would
24
28
  * locate the entry path even when the `.node` binding is absent (the
25
- * `file:` package directory is installed regardless of postinstall
26
- * outcome), giving false negatives for the exact users we want to warn:
29
+ * package directory exists without a working `.node` binding), giving false
30
+ * negatives for the exact users we want to warn:
27
31
  * those who installed with `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` or whose
28
32
  * native rebuild soft-failed for missing toolchain.
29
33
  *
@@ -5,7 +5,12 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
5
5
  * - FastAPI `@app.get("/path")` provider decorators
6
6
  * - `requests.get/post/...("url")` consumer calls
7
7
  * - Generic `requests.request("METHOD", "url")` consumer calls
8
- * - `httpx.AsyncClient` instances calling `.get/.post/...("url")`
8
+ * - `httpx.AsyncClient` instances calling `.get/.post/...("url")`, including
9
+ * aliased imports such as `import httpx as hx`,
10
+ * `from httpx import AsyncClient`, and
11
+ * `from httpx import AsyncClient as HttpxAsyncClient`.
12
+ * Locally rebound names (e.g. `AsyncClient = mock_factory()` inside a
13
+ * function) are excluded to avoid false-positive consumer contracts.
9
14
  */
10
15
  const FASTAPI_VERBS = {
11
16
  get: 'GET',
@@ -67,12 +72,48 @@ const REQUESTS_GENERIC_PATTERNS = compilePatterns({
67
72
  ],
68
73
  });
69
74
  // ─── Consumer: httpx.AsyncClient assignments ────────────────────────
70
- // NOTE: This targeted detector only tracks explicit `httpx.AsyncClient(...)`
71
- // construction. Direct imports (`from httpx import AsyncClient`) and module
72
- // aliases (`import httpx as hx`) and annotated assignments (`client: httpx.AsyncClient = ...`)
73
- // are intentionally left for a follow-up. Module-scope clients are only matched
75
+ // Module-scope clients are only matched
74
76
  // at module scope; calls inside functions require a function/class-local tracked
75
77
  // client to avoid false positives from same-name local variables.
78
+ const HTTPX_MODULE_IMPORT_PATTERNS = compilePatterns({
79
+ name: 'python-httpx-module-imports',
80
+ language: Python,
81
+ patterns: [
82
+ {
83
+ meta: {},
84
+ query: `
85
+ (import_statement
86
+ name: (aliased_import
87
+ name: (dotted_name (identifier) @module)
88
+ alias: (identifier) @alias))
89
+ `,
90
+ },
91
+ ],
92
+ });
93
+ const HTTPX_ASYNC_CLIENT_IMPORT_PATTERNS = compilePatterns({
94
+ name: 'python-httpx-async-client-imports',
95
+ language: Python,
96
+ patterns: [
97
+ {
98
+ meta: {},
99
+ query: `
100
+ (import_from_statement
101
+ module_name: (dotted_name (identifier) @module)
102
+ name: (dotted_name (identifier) @client_class))
103
+ `,
104
+ },
105
+ {
106
+ meta: {},
107
+ query: `
108
+ (import_from_statement
109
+ module_name: (dotted_name (identifier) @module)
110
+ name: (aliased_import
111
+ name: (dotted_name (identifier) @client_class)
112
+ alias: (identifier) @alias))
113
+ `,
114
+ },
115
+ ],
116
+ });
76
117
  const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
77
118
  name: 'python-httpx-async-client-assign',
78
119
  language: Python,
@@ -84,8 +125,23 @@ const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
84
125
  left: (_) @client
85
126
  right: (call
86
127
  function: (attribute
87
- object: (identifier) @module (#eq? @module "httpx")
88
- attribute: (identifier) @client_class (#eq? @client_class "AsyncClient"))))
128
+ object: (identifier) @module
129
+ attribute: (identifier) @client_class)))
130
+ `,
131
+ },
132
+ ],
133
+ });
134
+ const HTTPX_ASYNC_CLIENT_DIRECT_ASSIGN_PATTERNS = compilePatterns({
135
+ name: 'python-httpx-async-client-direct-assign',
136
+ language: Python,
137
+ patterns: [
138
+ {
139
+ meta: {},
140
+ query: `
141
+ (assignment
142
+ left: (_) @client
143
+ right: (call
144
+ function: (identifier) @client_class))
89
145
  `,
90
146
  },
91
147
  ],
@@ -101,8 +157,23 @@ const HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS = compilePatterns({
101
157
  (as_pattern
102
158
  (call
103
159
  function: (attribute
104
- object: (identifier) @module (#eq? @module "httpx")
105
- attribute: (identifier) @client_class (#eq? @client_class "AsyncClient")))
160
+ object: (identifier) @module
161
+ attribute: (identifier) @client_class))
162
+ (as_pattern_target (identifier) @client))
163
+ `,
164
+ },
165
+ ],
166
+ });
167
+ const HTTPX_ASYNC_CLIENT_DIRECT_WITH_ALIAS_PATTERNS = compilePatterns({
168
+ name: 'python-httpx-async-client-direct-with-alias',
169
+ language: Python,
170
+ patterns: [
171
+ {
172
+ meta: {},
173
+ query: `
174
+ (as_pattern
175
+ (call
176
+ function: (identifier) @client_class)
106
177
  (as_pattern_target (identifier) @client))
107
178
  `,
108
179
  },
@@ -131,14 +202,120 @@ function trackedClientScopeKey(clientNode) {
131
202
  return getScopeKey(clientNode.parent, clientNode.text.includes('.'));
132
203
  }
133
204
  function callScopeKeys(clientNode) {
134
- const keys = new Set();
135
- const preferClass = clientNode.text.includes('.');
136
- const nearestScope = getScopeKey(clientNode.parent, preferClass);
137
- keys.add(nearestScope);
138
- return [...keys];
205
+ return [getScopeKey(clientNode.parent, clientNode.text.includes('.'))];
206
+ }
207
+ // Returns the scope key that a rebind of an imported alias would shadow under
208
+ // Python LEGB rules, or `null` when the rebind does not shadow anything that
209
+ // could produce a false-positive consumer detection.
210
+ // - Rebind inside a function/method → that function's scope.
211
+ // - Rebind at module top level → 'module' (shadows the whole file).
212
+ // - Rebind in a class body without an enclosing function → null. Python
213
+ // class attributes do not shadow bare-name lookups inside methods (methods
214
+ // see the module binding, not the class attribute), so we must not poison
215
+ // them.
216
+ function shadowScopeKey(node) {
217
+ let current = node;
218
+ let passedThroughClass = false;
219
+ while (current) {
220
+ if (current.type === 'function_definition') {
221
+ // Reuse getScopeKey's key format so the two helpers cannot drift apart.
222
+ return getScopeKey(current);
223
+ }
224
+ if (current.type === 'class_definition') {
225
+ passedThroughClass = true;
226
+ }
227
+ current = current.parent;
228
+ }
229
+ return passedThroughClass ? null : 'module';
230
+ }
231
+ function collectHttpxImportAliases(tree) {
232
+ const moduleAliases = new Set(['httpx']);
233
+ const asyncClientAliases = new Set();
234
+ // The @module capture is a single identifier inside a `dotted_name`, so for
235
+ // `import package.httpx as hx` the pattern would match the inner `httpx`
236
+ // segment. Check the full `dotted_name` text via `parent` to anchor the match.
237
+ for (const match of runCompiledPatterns(HTTPX_MODULE_IMPORT_PATTERNS, tree)) {
238
+ const moduleNode = match.captures.module;
239
+ const aliasNode = match.captures.alias;
240
+ if (moduleNode?.parent?.text === 'httpx' && aliasNode)
241
+ moduleAliases.add(aliasNode.text);
242
+ }
243
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_IMPORT_PATTERNS, tree)) {
244
+ const moduleNode = match.captures.module;
245
+ const classNode = match.captures.client_class;
246
+ if (moduleNode?.parent?.text !== 'httpx' || classNode?.text !== 'AsyncClient')
247
+ continue;
248
+ asyncClientAliases.add(match.captures.alias?.text ?? classNode.text);
249
+ }
250
+ return { moduleAliases, asyncClientAliases };
251
+ }
252
+ // Tracks local rebindings (`AsyncClient = ...`, `hx = ...`) that shadow an
253
+ // imported alias. We treat the whole enclosing scope (module, class, or
254
+ // function) as shadowed for that alias name, so subsequent constructions in
255
+ // that scope are not falsely detected as httpx consumers. Covers bare-identifier
256
+ // targets and the common tuple / list destructuring shapes.
257
+ const ALIAS_SHADOW_PATTERNS = compilePatterns({
258
+ name: 'python-httpx-alias-shadow',
259
+ language: Python,
260
+ patterns: [
261
+ {
262
+ meta: {},
263
+ query: `(assignment left: (identifier) @name)`,
264
+ },
265
+ {
266
+ meta: {},
267
+ query: `(assignment left: (pattern_list (identifier) @name))`,
268
+ },
269
+ {
270
+ meta: {},
271
+ query: `(assignment left: (tuple_pattern (identifier) @name))`,
272
+ },
273
+ {
274
+ meta: {},
275
+ query: `(assignment left: (list_pattern (identifier) @name))`,
276
+ },
277
+ ],
278
+ });
279
+ function collectAliasShadowScopes(tree, aliases) {
280
+ const shadowed = new Map();
281
+ if (aliases.size === 0)
282
+ return shadowed;
283
+ for (const match of runCompiledPatterns(ALIAS_SHADOW_PATTERNS, tree)) {
284
+ const nameNode = match.captures.name;
285
+ if (!nameNode || !aliases.has(nameNode.text))
286
+ continue;
287
+ const scopeKey = shadowScopeKey(nameNode.parent);
288
+ if (scopeKey === null)
289
+ continue;
290
+ const set = shadowed.get(nameNode.text) ?? new Set();
291
+ set.add(scopeKey);
292
+ shadowed.set(nameNode.text, set);
293
+ }
294
+ return shadowed;
295
+ }
296
+ function isAliasShadowed(shadowed, aliasName, node) {
297
+ const scopes = shadowed.get(aliasName);
298
+ if (!scopes || scopes.size === 0)
299
+ return false;
300
+ let current = node.parent;
301
+ while (current) {
302
+ if (current.type === 'function_definition') {
303
+ // Reuse getScopeKey's key format so the two helpers cannot drift apart.
304
+ if (scopes.has(getScopeKey(current)))
305
+ return true;
306
+ }
307
+ current = current.parent;
308
+ }
309
+ // A module-level rebind shadows the alias for the entire file.
310
+ return scopes.has('module');
139
311
  }
140
312
  function collectHttpxAsyncClients(tree) {
141
313
  const clients = new Map();
314
+ const { moduleAliases, asyncClientAliases } = collectHttpxImportAliases(tree);
315
+ // Module aliases (`hx`) and AsyncClient aliases (`AsyncClient`,
316
+ // `HttpxAsyncClient`) share disjoint name spaces, so one shadow map keyed by
317
+ // alias name serves both lookups and we only walk the tree for rebinds once.
318
+ const shadowed = collectAliasShadowScopes(tree, new Set([...moduleAliases, ...asyncClientAliases]));
142
319
  const addClient = (clientNode) => {
143
320
  if (!clientNode)
144
321
  return;
@@ -149,9 +326,41 @@ function collectHttpxAsyncClients(tree) {
149
326
  clients.set(clientText, scopes);
150
327
  };
151
328
  for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS, tree)) {
329
+ const moduleNode = match.captures.module;
330
+ const classNode = match.captures.client_class;
331
+ if (!moduleNode || !classNode)
332
+ continue;
333
+ if (!moduleAliases.has(moduleNode.text) || classNode.text !== 'AsyncClient')
334
+ continue;
335
+ if (isAliasShadowed(shadowed, moduleNode.text, moduleNode))
336
+ continue;
337
+ addClient(match.captures.client);
338
+ }
339
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_DIRECT_ASSIGN_PATTERNS, tree)) {
340
+ const classNode = match.captures.client_class;
341
+ if (!classNode || !asyncClientAliases.has(classNode.text))
342
+ continue;
343
+ if (isAliasShadowed(shadowed, classNode.text, classNode))
344
+ continue;
152
345
  addClient(match.captures.client);
153
346
  }
154
347
  for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS, tree)) {
348
+ const moduleNode = match.captures.module;
349
+ const classNode = match.captures.client_class;
350
+ if (!moduleNode || !classNode)
351
+ continue;
352
+ if (!moduleAliases.has(moduleNode.text) || classNode.text !== 'AsyncClient')
353
+ continue;
354
+ if (isAliasShadowed(shadowed, moduleNode.text, moduleNode))
355
+ continue;
356
+ addClient(match.captures.client);
357
+ }
358
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_DIRECT_WITH_ALIAS_PATTERNS, tree)) {
359
+ const classNode = match.captures.client_class;
360
+ if (!classNode || !asyncClientAliases.has(classNode.text))
361
+ continue;
362
+ if (isAliasShadowed(shadowed, classNode.text, classNode))
363
+ continue;
155
364
  addClient(match.captures.client);
156
365
  }
157
366
  return clients;
@@ -7,7 +7,7 @@ import { extractVueScript, isVueSetupTopLevel } from './vue-sfc-extractor.js';
7
7
  import { yieldToEventLoop } from './utils/event-loop.js';
8
8
  import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
9
9
  import { isVerboseIngestionEnabled } from './utils/verbose.js';
10
- import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, getLabelFromCaptures, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js';
10
+ import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, getLabelFromCaptures, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js';
11
11
  import { detectFrameworkFromAST } from './framework-detection.js';
12
12
  import { buildTypeEnv } from './type-env.js';
13
13
  import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from './utils/method-props.js';
@@ -400,6 +400,9 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
400
400
  ? cachedFindEnclosingClassInfo(nameNode || definitionNodeForRange, file.path, provider.resolveEnclosingOwner)
401
401
  : null;
402
402
  const enclosingClassId = enclosingClassInfo?.classId ?? null;
403
+ const objectLiteralOwnerInfo = !enclosingClassId && nodeLabel === 'Method' && definitionNode
404
+ ? findObjectLiteralBindingInfo(definitionNode, file.path)
405
+ : null;
403
406
  // Qualify method/property IDs with enclosing class name to avoid collisions
404
407
  // e.g. "Method:animal.dart:Animal.speak" vs "Method:animal.dart:Dog.speak"
405
408
  const qualifiedName = enclosingClassInfo
@@ -615,7 +618,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
615
618
  returnType: methodProps.returnType,
616
619
  declaredType,
617
620
  templateArguments: classTemplateArguments,
618
- ownerId: enclosingClassId ?? undefined,
621
+ ownerId: enclosingClassId ?? objectLiteralOwnerInfo?.ownerId ?? undefined,
619
622
  qualifiedName: qualifiedTypeName,
620
623
  });
621
624
  const fileId = generateId('File', file.path);
@@ -630,15 +633,18 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
630
633
  };
631
634
  graph.addRelationship(relationship);
632
635
  // ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
633
- if (enclosingClassId) {
636
+ const ownerIdForMemberEdge = enclosingClassId ?? objectLiteralOwnerInfo?.ownerId ?? null;
637
+ if (ownerIdForMemberEdge) {
634
638
  const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
635
639
  graph.addRelationship({
636
- id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
637
- sourceId: enclosingClassId,
640
+ id: generateId(memberEdgeType, `${ownerIdForMemberEdge}->${nodeId}`),
641
+ sourceId: ownerIdForMemberEdge,
638
642
  targetId: nodeId,
639
643
  type: memberEdgeType,
640
644
  confidence: 1.0,
641
- reason: '',
645
+ reason: objectLiteralOwnerInfo
646
+ ? 'object literal method belongs to exported object binding'
647
+ : '',
642
648
  });
643
649
  }
644
650
  });
@@ -511,8 +511,14 @@ function normalizeNodeLabel(kindStr) {
511
511
  case 'property':
512
512
  return 'Property';
513
513
  case 'variable':
514
- case 'const':
515
514
  return 'Variable';
515
+ // `const` / `let` declarations align with the legacy DAG parse phase,
516
+ // which emits `Const` graph nodes via `@definition.const` capture for
517
+ // `lexical_declaration`. Returning `'Const'` here lets resolveDefGraphId's
518
+ // qualified-key path succeed for value receivers without relying on the
519
+ // simple-key fallback (PR #1718 review Finding 1 / 2026-05-21-002 U4).
520
+ case 'const':
521
+ return 'Const';
516
522
  case 'typealias':
517
523
  case 'type_alias':
518
524
  return 'TypeAlias';
@@ -41,3 +41,24 @@ export declare function tryEmitEdge(graph: KnowledgeGraph, scopes: ScopeResoluti
41
41
  };
42
42
  readonly kind: string;
43
43
  }, targetDef: SymbolDefinition, reason: string, seen: Set<string>, confidence?: number, collapseByCallerTarget?: boolean): boolean;
44
+ /**
45
+ * Variant of `tryEmitEdge` that takes a pre-resolved target graph id
46
+ * instead of resolving it from a `SymbolDefinition`. Used by the
47
+ * value-receiver-owner bridge (`receiver-bound-calls.ts` Case 5) where
48
+ * the picked owner-indexed method def carries no `qualifiedName` (object
49
+ * literals have no class owner to seed it) and therefore cannot
50
+ * round-trip through `resolveDefGraphId`. The def's `nodeId` IS the
51
+ * canonical graph node id (written by the parse phase), so the caller
52
+ * passes it directly.
53
+ *
54
+ * All other invariants of `tryEmitEdge` apply: dedup key shape, collapse
55
+ * flag honoring, edge-type mapping, caller-id resolution.
56
+ */
57
+ export declare function tryEmitEdgeWithExplicitTargetId(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, nodeLookup: GraphNodeLookup, site: {
58
+ readonly inScope: ScopeId;
59
+ readonly atRange: {
60
+ startLine: number;
61
+ startCol: number;
62
+ };
63
+ readonly kind: string;
64
+ }, targetGraphId: string, reason: string, seen: Set<string>, confidence?: number, collapseByCallerTarget?: boolean): boolean;
@@ -77,3 +77,40 @@ export function tryEmitEdge(graph, scopes, nodeLookup, site, targetDef, reason,
77
77
  });
78
78
  return true;
79
79
  }
80
+ /**
81
+ * Variant of `tryEmitEdge` that takes a pre-resolved target graph id
82
+ * instead of resolving it from a `SymbolDefinition`. Used by the
83
+ * value-receiver-owner bridge (`receiver-bound-calls.ts` Case 5) where
84
+ * the picked owner-indexed method def carries no `qualifiedName` (object
85
+ * literals have no class owner to seed it) and therefore cannot
86
+ * round-trip through `resolveDefGraphId`. The def's `nodeId` IS the
87
+ * canonical graph node id (written by the parse phase), so the caller
88
+ * passes it directly.
89
+ *
90
+ * All other invariants of `tryEmitEdge` apply: dedup key shape, collapse
91
+ * flag honoring, edge-type mapping, caller-id resolution.
92
+ */
93
+ export function tryEmitEdgeWithExplicitTargetId(graph, scopes, nodeLookup, site, targetGraphId, reason, seen, confidence = 0.85, collapseByCallerTarget = false) {
94
+ const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
95
+ const edgeType = mapReferenceKindToEdgeType(site.kind);
96
+ if (callerGraphId === undefined)
97
+ return false;
98
+ if (edgeType === undefined)
99
+ return false;
100
+ const useCollapsed = collapseByCallerTarget && edgeType === 'CALLS';
101
+ const dedupKey = useCollapsed
102
+ ? `${edgeType}:${callerGraphId}->${targetGraphId}`
103
+ : `${edgeType}:${callerGraphId}->${targetGraphId}:${site.atRange.startLine}:${site.atRange.startCol}`;
104
+ if (seen.has(dedupKey))
105
+ return false;
106
+ seen.add(dedupKey);
107
+ graph.addRelationship({
108
+ id: `rel:${dedupKey}`,
109
+ sourceId: callerGraphId,
110
+ targetId: targetGraphId,
111
+ type: edgeType,
112
+ confidence,
113
+ reason,
114
+ });
115
+ return true;
116
+ }
@@ -137,5 +137,11 @@ export function isLinkableLabel(label) {
137
137
  // ACCESSES edges target field nodes (e.g. `user.name = "x"` →
138
138
  // ACCESSES edge to User's `name` Variable/Property node).
139
139
  label === 'Variable' ||
140
- label === 'Property');
140
+ label === 'Property' ||
141
+ // Const is linkable so the value-receiver-owner bridge in
142
+ // `receiver-bound-calls.ts` Case 5 can translate the scope-resolution
143
+ // `Variable` def for `export const fooService = {...}` to the canonical
144
+ // `Const:filePath:name` graph node id, against which object-literal
145
+ // method symbols register their `ownerId` (PR #1718 / issue #1358).
146
+ label === 'Const');
141
147
  }
@@ -21,6 +21,11 @@
21
21
  * but not a namespace prefix → compound resolver
22
22
  * 7. **Case 4 (simple typeBinding)** — `typeRef.rawName` has no dot →
23
23
  * MRO walk + `findOwnedMember`
24
+ * 8. **Case 5 (value-receiver bridge)** — receiver is a `Const`/`Variable`
25
+ * whose `nodeId` is referenced as an `ownerId` in `model.methods`
26
+ * (object-literal services). Last-resort fallback for lowercase
27
+ * receivers with no class-like or type-binding match. Mirrors
28
+ * the legacy DAG bridge in `call-processor.ts`.
24
29
  *
25
30
  * Reordering or merging cases changes resolution semantics.
26
31
  *
@@ -21,6 +21,11 @@
21
21
  * but not a namespace prefix → compound resolver
22
22
  * 7. **Case 4 (simple typeBinding)** — `typeRef.rawName` has no dot →
23
23
  * MRO walk + `findOwnedMember`
24
+ * 8. **Case 5 (value-receiver bridge)** — receiver is a `Const`/`Variable`
25
+ * whose `nodeId` is referenced as an `ownerId` in `model.methods`
26
+ * (object-literal services). Last-resort fallback for lowercase
27
+ * receivers with no class-like or type-binding match. Mirrors
28
+ * the legacy DAG bridge in `call-processor.ts`.
24
29
  *
25
30
  * Reordering or merging cases changes resolution semantics.
26
31
  *
@@ -32,8 +37,8 @@
32
37
  * resolved to a wrong target.
33
38
  */
34
39
  import { collectNamespaceTargets } from '../scope/namespace-targets.js';
35
- import { findClassBindingInScope, findEnclosingClassDef, findExportedDef, findOwnedMember, findReceiverTypeBinding, isClassLike, } from '../scope/walkers.js';
36
- import { tryEmitEdge } from '../graph-bridge/edges.js';
40
+ import { findClassBindingInScope, findEnclosingClassDef, findExportedDef, findOwnedMember, findReceiverTypeBinding, findValueBindingInScope, isClassLike, } from '../scope/walkers.js';
41
+ import { tryEmitEdge, tryEmitEdgeWithExplicitTargetId } from '../graph-bridge/edges.js';
37
42
  import { resolveCompoundReceiverClass } from '../passes/compound-receiver.js';
38
43
  import { resolveDefGraphId } from '../graph-bridge/ids.js';
39
44
  import { narrowOverloadCandidates, isOverloadAmbiguousAfterNormalization, } from './overload-narrowing.js';
@@ -514,6 +519,49 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
514
519
  }
515
520
  }
516
521
  }
522
+ // ── Case 5: value-receiver bridge (object-literal services) ──
523
+ // When prior cases couldn't resolve the receiver as a class or
524
+ // type binding, fall back to value-binding resolution. Covers:
525
+ //
526
+ // export const fooService = { getUser(id) {...} };
527
+ // import { fooService } from './service';
528
+ // fooService.getUser(id); // ← resolve here
529
+ //
530
+ // `fooService` is a `Const`/`Variable` (not class-like, no typeBinding
531
+ // for unannotated literals), so Cases 2-4 skip it. Scope-resolution
532
+ // defs for non-class values carry a synthetic id, so we translate to
533
+ // the canonical graph node ID via `resolveDefGraphId` before owner-
534
+ // indexed lookup — the parser writes the graph node ID as `ownerId`
535
+ // on the method symbol-table entry to match.
536
+ //
537
+ // Object-literal methods do not carry a `qualifiedName` (no class
538
+ // owner to seed it), so the picked def cannot round-trip through
539
+ // `tryEmitEdge` → `resolveDefGraphId`. We use
540
+ // `tryEmitEdgeWithExplicitTargetId` instead, passing `picked.nodeId`
541
+ // directly — same dedup-key shape, collapse-flag honoring, and
542
+ // caller resolution as `tryEmitEdge`.
543
+ const valueDef = findValueBindingInScope(site.inScope, receiverName, scopes);
544
+ if (valueDef !== undefined) {
545
+ const ownerGraphId = resolveDefGraphId(valueDef.filePath, valueDef, nodeLookup) ?? valueDef.nodeId;
546
+ const picked = pickOverload(ownerGraphId, memberName, site, model, provider);
547
+ if (picked === OVERLOAD_AMBIGUOUS) {
548
+ handledSites.add(siteKey);
549
+ continue;
550
+ }
551
+ if (picked !== undefined) {
552
+ const reason = site.kind === 'write' || site.kind === 'read'
553
+ ? site.kind
554
+ : picked.filePath !== parsed.filePath
555
+ ? 'import-resolved'
556
+ : 'global';
557
+ const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
558
+ const ok = tryEmitEdgeWithExplicitTargetId(graph, scopes, nodeLookup, site, picked.nodeId, reason, seen, confidence, collapse);
559
+ if (ok)
560
+ emitted++;
561
+ handledSites.add(siteKey);
562
+ continue;
563
+ }
564
+ }
517
565
  }
518
566
  }
519
567
  return emitted;
@@ -91,6 +91,34 @@ export declare function findReceiverTypeBinding(startScope: ScopeId, receiverNam
91
91
  * Without (2) we'd miss every cross-file class-receiver call.
92
92
  */
93
93
  export declare function findClassBindingInScope(startScope: ScopeId, receiverName: string, scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
94
+ /**
95
+ * Predicate for value-receiver bridge: the labels for which
96
+ * `reconcileOwnership` registers methods/fields under the def's
97
+ * `nodeId` as the `ownerId`. Explicit allowlist so future NodeLabel
98
+ * additions (Module, Namespace, TypeAlias, EnumMember, etc.) do NOT
99
+ * silently widen the bridge — adding a new ownerable label requires
100
+ * touching both this predicate and `reconcileOwnership`.
101
+ *
102
+ * See: `scope-resolution/pipeline/reconcile-ownership.ts` Property /
103
+ * Variable / Const / Static registration block.
104
+ */
105
+ export declare function isOwnableValueLabel(t: string): boolean;
106
+ /**
107
+ * Look up a value-binding (Const/Variable/Property/Static) by name in
108
+ * the given scope's chain. Used by the value-receiver-owner bridge
109
+ * for object-literal services such as:
110
+ *
111
+ * export const fooService = { getUser(id) {...} };
112
+ *
113
+ * where `fooService` is a `Const`/`Variable` whose `nodeId` is the
114
+ * `ownerId` of the member method. Neither `findClassBindingInScope`
115
+ * (rejects non-class-like) nor `findReceiverTypeBinding` (no typeBinding
116
+ * for an unannotated literal) finds it.
117
+ *
118
+ * Mirrors `findClassBindingInScope` exactly; only the accepted def-type
119
+ * predicate differs.
120
+ */
121
+ export declare function findValueBindingInScope(startScope: ScopeId, receiverName: string, scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
94
122
  /**
95
123
  * Look up a callable (Function/Method/Constructor) by name in the
96
124
  * given scope's chain. Uses the dual-source pattern (scope.bindings +