gitnexus 1.4.10 → 1.5.1

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 (211) hide show
  1. package/README.md +6 -5
  2. package/dist/_shared/graph/types.d.ts +65 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -0
  4. package/dist/_shared/graph/types.js +8 -0
  5. package/dist/_shared/graph/types.js.map +1 -0
  6. package/dist/_shared/index.d.ts +7 -0
  7. package/dist/_shared/index.d.ts.map +1 -0
  8. package/dist/_shared/index.js +6 -0
  9. package/dist/_shared/index.js.map +1 -0
  10. package/dist/_shared/language-detection.d.ts +23 -0
  11. package/dist/_shared/language-detection.d.ts.map +1 -0
  12. package/dist/_shared/language-detection.js +137 -0
  13. package/dist/_shared/language-detection.js.map +1 -0
  14. package/dist/_shared/languages.d.ts +25 -0
  15. package/dist/_shared/languages.d.ts.map +1 -0
  16. package/dist/_shared/languages.js +26 -0
  17. package/dist/_shared/languages.js.map +1 -0
  18. package/dist/_shared/lbug/schema-constants.d.ts +16 -0
  19. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -0
  20. package/dist/_shared/lbug/schema-constants.js +64 -0
  21. package/dist/_shared/lbug/schema-constants.js.map +1 -0
  22. package/dist/_shared/pipeline.d.ts +16 -0
  23. package/dist/_shared/pipeline.d.ts.map +1 -0
  24. package/dist/_shared/pipeline.js +5 -0
  25. package/dist/_shared/pipeline.js.map +1 -0
  26. package/dist/cli/ai-context.d.ts +4 -1
  27. package/dist/cli/ai-context.js +19 -11
  28. package/dist/cli/analyze.d.ts +6 -0
  29. package/dist/cli/analyze.js +105 -251
  30. package/dist/cli/eval-server.js +20 -11
  31. package/dist/cli/index-repo.js +20 -22
  32. package/dist/cli/index.js +8 -7
  33. package/dist/cli/mcp.js +1 -1
  34. package/dist/cli/serve.js +29 -1
  35. package/dist/cli/setup.js +9 -9
  36. package/dist/cli/skill-gen.js +15 -9
  37. package/dist/cli/wiki.d.ts +2 -0
  38. package/dist/cli/wiki.js +141 -26
  39. package/dist/config/ignore-service.js +102 -22
  40. package/dist/config/supported-languages.d.ts +8 -42
  41. package/dist/config/supported-languages.js +8 -43
  42. package/dist/core/augmentation/engine.js +19 -7
  43. package/dist/core/embeddings/embedder.js +19 -15
  44. package/dist/core/embeddings/embedding-pipeline.js +6 -6
  45. package/dist/core/embeddings/http-client.js +3 -3
  46. package/dist/core/embeddings/text-generator.js +9 -24
  47. package/dist/core/embeddings/types.d.ts +1 -1
  48. package/dist/core/embeddings/types.js +1 -7
  49. package/dist/core/graph/graph.js +6 -2
  50. package/dist/core/graph/types.d.ts +9 -59
  51. package/dist/core/ingestion/ast-cache.js +3 -3
  52. package/dist/core/ingestion/call-processor.d.ts +20 -2
  53. package/dist/core/ingestion/call-processor.js +347 -144
  54. package/dist/core/ingestion/call-routing.js +10 -4
  55. package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
  56. package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
  57. package/dist/core/ingestion/call-sites/java.d.ts +9 -0
  58. package/dist/core/ingestion/call-sites/java.js +30 -0
  59. package/dist/core/ingestion/cluster-enricher.js +6 -8
  60. package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
  61. package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
  62. package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
  63. package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
  64. package/dist/core/ingestion/cobol-processor.js +102 -56
  65. package/dist/core/ingestion/community-processor.js +21 -15
  66. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
  67. package/dist/core/ingestion/entry-point-scoring.js +5 -6
  68. package/dist/core/ingestion/export-detection.js +32 -9
  69. package/dist/core/ingestion/field-extractor.d.ts +1 -1
  70. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
  71. package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
  72. package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
  73. package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
  74. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
  75. package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
  76. package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
  77. package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
  78. package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
  79. package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
  80. package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
  81. package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
  82. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
  83. package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
  84. package/dist/core/ingestion/field-extractors/generic.js +6 -0
  85. package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
  86. package/dist/core/ingestion/field-extractors/typescript.js +1 -1
  87. package/dist/core/ingestion/field-types.d.ts +4 -2
  88. package/dist/core/ingestion/filesystem-walker.js +3 -3
  89. package/dist/core/ingestion/framework-detection.d.ts +1 -1
  90. package/dist/core/ingestion/framework-detection.js +355 -85
  91. package/dist/core/ingestion/heritage-processor.d.ts +24 -0
  92. package/dist/core/ingestion/heritage-processor.js +99 -8
  93. package/dist/core/ingestion/import-processor.js +44 -15
  94. package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
  95. package/dist/core/ingestion/import-resolvers/dart.js +1 -1
  96. package/dist/core/ingestion/import-resolvers/go.js +4 -2
  97. package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
  98. package/dist/core/ingestion/import-resolvers/php.js +4 -4
  99. package/dist/core/ingestion/import-resolvers/python.js +1 -1
  100. package/dist/core/ingestion/import-resolvers/rust.js +9 -3
  101. package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
  102. package/dist/core/ingestion/import-resolvers/standard.js +6 -5
  103. package/dist/core/ingestion/import-resolvers/swift.js +2 -1
  104. package/dist/core/ingestion/import-resolvers/utils.js +26 -7
  105. package/dist/core/ingestion/language-config.js +5 -4
  106. package/dist/core/ingestion/language-provider.d.ts +7 -2
  107. package/dist/core/ingestion/languages/c-cpp.js +106 -21
  108. package/dist/core/ingestion/languages/cobol.js +1 -1
  109. package/dist/core/ingestion/languages/csharp.js +96 -19
  110. package/dist/core/ingestion/languages/dart.js +23 -7
  111. package/dist/core/ingestion/languages/go.js +1 -1
  112. package/dist/core/ingestion/languages/index.d.ts +1 -1
  113. package/dist/core/ingestion/languages/index.js +2 -3
  114. package/dist/core/ingestion/languages/java.js +4 -1
  115. package/dist/core/ingestion/languages/kotlin.js +60 -13
  116. package/dist/core/ingestion/languages/php.js +102 -25
  117. package/dist/core/ingestion/languages/python.js +28 -5
  118. package/dist/core/ingestion/languages/ruby.js +56 -14
  119. package/dist/core/ingestion/languages/rust.js +55 -11
  120. package/dist/core/ingestion/languages/swift.js +112 -27
  121. package/dist/core/ingestion/languages/typescript.js +95 -19
  122. package/dist/core/ingestion/markdown-processor.js +5 -5
  123. package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
  124. package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
  125. package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
  126. package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
  127. package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
  128. package/dist/core/ingestion/method-extractors/generic.js +137 -0
  129. package/dist/core/ingestion/method-types.d.ts +61 -0
  130. package/dist/core/ingestion/method-types.js +2 -0
  131. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  132. package/dist/core/ingestion/mro-processor.js +12 -8
  133. package/dist/core/ingestion/named-binding-processor.js +2 -2
  134. package/dist/core/ingestion/named-bindings/rust.js +3 -1
  135. package/dist/core/ingestion/parsing-processor.js +74 -24
  136. package/dist/core/ingestion/pipeline.d.ts +2 -1
  137. package/dist/core/ingestion/pipeline.js +208 -102
  138. package/dist/core/ingestion/process-processor.js +12 -10
  139. package/dist/core/ingestion/resolution-context.js +3 -3
  140. package/dist/core/ingestion/route-extractors/middleware.js +31 -7
  141. package/dist/core/ingestion/route-extractors/php.js +2 -1
  142. package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
  143. package/dist/core/ingestion/structure-processor.d.ts +1 -1
  144. package/dist/core/ingestion/structure-processor.js +4 -4
  145. package/dist/core/ingestion/symbol-table.d.ts +1 -1
  146. package/dist/core/ingestion/symbol-table.js +22 -6
  147. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  148. package/dist/core/ingestion/tree-sitter-queries.js +1 -1
  149. package/dist/core/ingestion/type-env.d.ts +2 -2
  150. package/dist/core/ingestion/type-env.js +75 -50
  151. package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
  152. package/dist/core/ingestion/type-extractors/csharp.js +24 -14
  153. package/dist/core/ingestion/type-extractors/dart.js +6 -8
  154. package/dist/core/ingestion/type-extractors/go.js +7 -6
  155. package/dist/core/ingestion/type-extractors/jvm.js +10 -21
  156. package/dist/core/ingestion/type-extractors/php.js +26 -13
  157. package/dist/core/ingestion/type-extractors/python.js +11 -15
  158. package/dist/core/ingestion/type-extractors/ruby.js +8 -3
  159. package/dist/core/ingestion/type-extractors/rust.js +6 -8
  160. package/dist/core/ingestion/type-extractors/shared.js +134 -50
  161. package/dist/core/ingestion/type-extractors/swift.js +16 -13
  162. package/dist/core/ingestion/type-extractors/typescript.js +23 -15
  163. package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
  164. package/dist/core/ingestion/utils/ast-helpers.js +72 -35
  165. package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
  166. package/dist/core/ingestion/utils/call-analysis.js +96 -49
  167. package/dist/core/ingestion/utils/event-loop.js +1 -1
  168. package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
  169. package/dist/core/ingestion/workers/parse-worker.js +364 -84
  170. package/dist/core/ingestion/workers/worker-pool.js +5 -10
  171. package/dist/core/lbug/csv-generator.js +54 -15
  172. package/dist/core/lbug/lbug-adapter.d.ts +5 -0
  173. package/dist/core/lbug/lbug-adapter.js +86 -23
  174. package/dist/core/lbug/schema.d.ts +3 -6
  175. package/dist/core/lbug/schema.js +6 -30
  176. package/dist/core/run-analyze.d.ts +49 -0
  177. package/dist/core/run-analyze.js +257 -0
  178. package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
  179. package/dist/core/tree-sitter/parser-loader.js +1 -1
  180. package/dist/core/wiki/cursor-client.js +2 -7
  181. package/dist/core/wiki/generator.js +38 -23
  182. package/dist/core/wiki/graph-queries.js +10 -10
  183. package/dist/core/wiki/html-viewer.js +7 -3
  184. package/dist/core/wiki/llm-client.d.ts +23 -2
  185. package/dist/core/wiki/llm-client.js +96 -26
  186. package/dist/core/wiki/prompts.js +7 -6
  187. package/dist/mcp/core/embedder.js +1 -1
  188. package/dist/mcp/core/lbug-adapter.d.ts +4 -1
  189. package/dist/mcp/core/lbug-adapter.js +17 -7
  190. package/dist/mcp/local/local-backend.js +247 -95
  191. package/dist/mcp/resources.js +14 -6
  192. package/dist/mcp/server.js +13 -5
  193. package/dist/mcp/staleness.js +5 -1
  194. package/dist/mcp/tools.js +100 -23
  195. package/dist/server/analyze-job.d.ts +53 -0
  196. package/dist/server/analyze-job.js +146 -0
  197. package/dist/server/analyze-worker.d.ts +13 -0
  198. package/dist/server/analyze-worker.js +59 -0
  199. package/dist/server/api.js +795 -44
  200. package/dist/server/git-clone.d.ts +25 -0
  201. package/dist/server/git-clone.js +91 -0
  202. package/dist/storage/git.js +1 -3
  203. package/dist/storage/repo-manager.d.ts +5 -2
  204. package/dist/storage/repo-manager.js +4 -4
  205. package/dist/types/pipeline.d.ts +1 -21
  206. package/dist/types/pipeline.js +1 -18
  207. package/hooks/claude/gitnexus-hook.cjs +52 -22
  208. package/package.json +5 -4
  209. package/scripts/build.js +69 -0
  210. package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
  211. package/dist/core/ingestion/utils/language-detection.js +0 -70
@@ -3,12 +3,13 @@ import { TIER_CONFIDENCE } from './resolution-context.js';
3
3
  import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
4
4
  import { getProvider } from './languages/index.js';
5
5
  import { generateId } from '../../lib/utils.js';
6
- import { getLanguageFromFilename } from './utils/language-detection.js';
6
+ import { getLanguageFromFilename } from 'gitnexus-shared';
7
7
  import { isVerboseIngestionEnabled } from './utils/verbose.js';
8
8
  import { yieldToEventLoop } from './utils/event-loop.js';
9
- import { FUNCTION_NODE_TYPES, extractFunctionName, findEnclosingClassId } from './utils/ast-helpers.js';
10
- import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, } from './utils/call-analysis.js';
9
+ import { FUNCTION_NODE_TYPES, extractFunctionName, findEnclosingClassId, } from './utils/ast-helpers.js';
10
+ import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, extractCallArgTypes, } from './utils/call-analysis.js';
11
11
  import { buildTypeEnv, isSubclassOf } from './type-env.js';
12
+ import { resolveExtendsType } from './heritage-processor.js';
12
13
  import { getTreeSitterBufferSize } from './constants.js';
13
14
  import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
14
15
  import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
@@ -75,7 +76,7 @@ function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
75
76
  * exported symbols that have callables with known return types. */
76
77
  export function buildExportedTypeMapFromGraph(graph, symbolTable) {
77
78
  const result = new Map();
78
- graph.forEachNode(node => {
79
+ graph.forEachNode((node) => {
79
80
  if (!node.properties?.isExported)
80
81
  return;
81
82
  if (!node.properties?.filePath || !node.properties?.name)
@@ -141,8 +142,17 @@ export function seedCrossFileReceiverTypes(calls, namedImportMap, exportedTypeMa
141
142
  // strips nullable wrappers (Option<User> → User), these chain steps are no-ops
142
143
  // for type resolution — the current type passes through unchanged.
143
144
  const TYPE_PRESERVING_METHODS = new Set([
144
- 'unwrap', 'expect', 'unwrap_or', 'unwrap_or_default', 'unwrap_or_else', // Rust Option/Result
145
- 'clone', 'to_owned', 'as_ref', 'as_mut', 'borrow', 'borrow_mut', // Rust clone/borrow
145
+ 'unwrap',
146
+ 'expect',
147
+ 'unwrap_or',
148
+ 'unwrap_or_default',
149
+ 'unwrap_or_else', // Rust Option/Result
150
+ 'clone',
151
+ 'to_owned',
152
+ 'as_ref',
153
+ 'as_mut',
154
+ 'borrow',
155
+ 'borrow_mut', // Rust clone/borrow
146
156
  'get', // Kotlin/Java Optional.get()
147
157
  'orElseThrow', // Java Optional
148
158
  ]);
@@ -201,18 +211,18 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
201
211
  const verified = new Map();
202
212
  for (const { scope, varName, calleeName, receiverClassName } of bindings) {
203
213
  const tiered = ctx.resolve(calleeName, filePath);
204
- const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
214
+ const isClass = tiered?.candidates.some((def) => def.type === 'Class') ?? false;
205
215
  if (isClass) {
206
216
  verified.set(receiverKey(scope, varName), calleeName);
207
217
  }
208
218
  else {
209
- let callableDefs = tiered?.candidates.filter(d => d.type === 'Function' || d.type === 'Method');
219
+ let callableDefs = tiered?.candidates.filter((d) => d.type === 'Function' || d.type === 'Method');
210
220
  // When receiver class is known (e.g. $this->method() in PHP), narrow
211
221
  // candidates to methods owned by that class to avoid false disambiguation failures.
212
222
  if (callableDefs && callableDefs.length > 1 && receiverClassName) {
213
223
  if (graph) {
214
224
  // Worker path: use graph.getNode (fast, already in-memory)
215
- const narrowed = callableDefs.filter(d => {
225
+ const narrowed = callableDefs.filter((d) => {
216
226
  if (!d.ownerId)
217
227
  return false;
218
228
  const owner = graph.getNode(d.ownerId);
@@ -225,8 +235,8 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
225
235
  // Sequential path: use ctx.resolve (no graph available)
226
236
  const classResolved = ctx.resolve(receiverClassName, filePath);
227
237
  if (classResolved && classResolved.candidates.length > 0) {
228
- const classNodeIds = new Set(classResolved.candidates.map(c => c.nodeId));
229
- const narrowed = callableDefs.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
238
+ const classNodeIds = new Set(classResolved.candidates.map((c) => c.nodeId));
239
+ const narrowed = callableDefs.filter((d) => d.ownerId && classNodeIds.has(d.ownerId));
230
240
  if (narrowed.length > 0)
231
241
  callableDefs = narrowed;
232
242
  }
@@ -242,6 +252,85 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
242
252
  }
243
253
  return verified;
244
254
  };
255
+ /**
256
+ * Build an ImplementorMap from extracted heritage data.
257
+ * Only direct `implements` relationships are tracked (transitive not needed for
258
+ * the common Java/Kotlin/C# interface dispatch pattern).
259
+ * `extends` is ignored — dispatch keyed on abstract class bases is not modeled here.
260
+ */
261
+ /**
262
+ * Maps interface name → file paths of classes that implement it (direct only).
263
+ * When `ctx` is set, `kind: 'extends'` rows are classified like heritage-processor
264
+ * (C#/Java base_list: class vs interface parents share one capture name).
265
+ */
266
+ export const buildImplementorMap = (heritage, ctx) => {
267
+ const map = new Map();
268
+ for (const h of heritage) {
269
+ let record = false;
270
+ if (h.kind === 'implements') {
271
+ record = true;
272
+ }
273
+ else if (h.kind === 'extends' && ctx) {
274
+ const lang = getLanguageFromFilename(h.filePath);
275
+ if (lang) {
276
+ const { type } = resolveExtendsType(h.parentName, h.filePath, ctx, lang);
277
+ record = type === 'IMPLEMENTS';
278
+ }
279
+ }
280
+ if (record) {
281
+ let files = map.get(h.parentName);
282
+ if (!files) {
283
+ files = new Set();
284
+ map.set(h.parentName, files);
285
+ }
286
+ files.add(h.filePath);
287
+ }
288
+ }
289
+ return map;
290
+ };
291
+ /**
292
+ * Merge a chunk's implementor map into the global accumulator.
293
+ */
294
+ export const mergeImplementorMaps = (target, source) => {
295
+ for (const [name, files] of source) {
296
+ let existing = target.get(name);
297
+ if (!existing) {
298
+ existing = new Set();
299
+ target.set(name, existing);
300
+ }
301
+ for (const f of files)
302
+ existing.add(f);
303
+ }
304
+ };
305
+ /**
306
+ * After resolving a call to an interface method, find additional targets
307
+ * in classes implementing that interface. Returns implementation method
308
+ * results with lower confidence ('interface-dispatch').
309
+ */
310
+ function findInterfaceDispatchTargets(calledName, receiverTypeName, currentFile, ctx, implementorMap, primaryNodeId) {
311
+ const implFiles = implementorMap.get(receiverTypeName);
312
+ if (!implFiles || implFiles.size === 0)
313
+ return [];
314
+ const typeResolved = ctx.resolve(receiverTypeName, currentFile);
315
+ if (!typeResolved)
316
+ return [];
317
+ if (!typeResolved.candidates.some((c) => c.type === 'Interface'))
318
+ return [];
319
+ const results = [];
320
+ for (const implFile of implFiles) {
321
+ const methods = ctx.symbols.lookupExactAll(implFile, calledName);
322
+ for (const method of methods) {
323
+ if (method.nodeId !== primaryNodeId) {
324
+ results.push({
325
+ nodeId: method.nodeId,
326
+ confidence: 0.7,
327
+ reason: 'interface-dispatch',
328
+ });
329
+ }
330
+ }
331
+ }
332
+ return results;
333
+ }
245
334
  export const processCalls = async (graph, files, astCache, ctx, onProgress, exportedTypeMap,
246
335
  /** Phase 14: pre-resolved cross-file bindings to seed into buildTypeEnv. Keyed by filePath → Map<localName, typeName>. */
247
336
  importedBindingsMap,
@@ -249,7 +338,7 @@ importedBindingsMap,
249
338
  * Consulted ONLY when SymbolTable has no unambiguous match (local-first principle). */
250
339
  importedReturnTypesMap,
251
340
  /** Phase 14 E3: cross-file RAW return types for for-loop element extraction. Keyed by filePath → Map<calleeName, rawReturnType>. */
252
- importedRawReturnTypesMap) => {
341
+ importedRawReturnTypesMap, implementorMap) => {
253
342
  const parser = await loadParser();
254
343
  const collectedHeritage = [];
255
344
  const pendingWrites = [];
@@ -283,7 +372,9 @@ importedRawReturnTypesMap) => {
283
372
  let tree = astCache.get(file.path);
284
373
  if (!tree) {
285
374
  try {
286
- tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
375
+ tree = parser.parse(file.content, undefined, {
376
+ bufferSize: getTreeSitterBufferSize(file.content.length),
377
+ });
287
378
  }
288
379
  catch (parseError) {
289
380
  continue;
@@ -306,7 +397,7 @@ importedRawReturnTypesMap) => {
306
397
  const fileParentMap = new Map();
307
398
  for (const match of matches) {
308
399
  const captureMap = {};
309
- match.captures.forEach(c => captureMap[c.name] = c.node);
400
+ match.captures.forEach((c) => (captureMap[c.name] = c.node));
310
401
  if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
311
402
  const className = captureMap['heritage.class'].text;
312
403
  const parentName = captureMap['heritage.extends'].text;
@@ -347,7 +438,14 @@ importedRawReturnTypesMap) => {
347
438
  const importedBindings = importedBindingsMap?.get(file.path);
348
439
  const importedReturnTypes = importedReturnTypesMap?.get(file.path);
349
440
  const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
350
- const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
441
+ const typeEnv = buildTypeEnv(tree, language, {
442
+ symbolTable: ctx.symbols,
443
+ parentMap,
444
+ importedBindings,
445
+ importedReturnTypes,
446
+ importedRawReturnTypes,
447
+ enclosingFunctionFinder: provider?.enclosingFunctionFinder,
448
+ });
351
449
  if (typeEnv && exportedTypeMap) {
352
450
  const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
353
451
  if (fileExports)
@@ -360,11 +458,13 @@ importedRawReturnTypesMap) => {
360
458
  const receiverIndex = buildReceiverTypeIndex(verifiedReceivers);
361
459
  ctx.enableCache(file.path);
362
460
  const widenCache = new Map();
363
- matches.forEach(match => {
461
+ matches.forEach((match) => {
364
462
  const captureMap = {};
365
- match.captures.forEach(c => captureMap[c.name] = c.node);
463
+ match.captures.forEach((c) => (captureMap[c.name] = c.node));
366
464
  // ── Write access: emit ACCESSES {reason: 'write'} for assignments to member fields ──
367
- if (captureMap['assignment'] && captureMap['assignment.receiver'] && captureMap['assignment.property']) {
465
+ if (captureMap['assignment'] &&
466
+ captureMap['assignment.receiver'] &&
467
+ captureMap['assignment.property']) {
368
468
  const receiverNode = captureMap['assignment.receiver'];
369
469
  const propertyName = captureMap['assignment.property'].text;
370
470
  // Resolve receiver type: simple identifier → TypeEnv lookup or class resolution
@@ -381,8 +481,12 @@ importedRawReturnTypesMap) => {
381
481
  }
382
482
  if (!receiverTypeName && receiverText) {
383
483
  const resolved = ctx.resolve(receiverText, file.path);
384
- if (resolved?.candidates.some(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'
385
- || d.type === 'Enum' || d.type === 'Record' || d.type === 'Impl')) {
484
+ if (resolved?.candidates.some((d) => d.type === 'Class' ||
485
+ d.type === 'Struct' ||
486
+ d.type === 'Interface' ||
487
+ d.type === 'Enum' ||
488
+ d.type === 'Record' ||
489
+ d.type === 'Impl')) {
386
490
  receiverTypeName = receiverText;
387
491
  }
388
492
  }
@@ -430,9 +534,12 @@ importedRawReturnTypesMap) => {
430
534
  id: nodeId,
431
535
  label: 'Property',
432
536
  properties: {
433
- name: item.propName, filePath: file.path,
434
- startLine: item.startLine, endLine: item.endLine,
435
- language, isExported: true,
537
+ name: item.propName,
538
+ filePath: file.path,
539
+ startLine: item.startLine,
540
+ endLine: item.endLine,
541
+ language,
542
+ isExported: true,
436
543
  description: item.accessorType,
437
544
  },
438
545
  });
@@ -442,14 +549,21 @@ importedRawReturnTypesMap) => {
442
549
  });
443
550
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
444
551
  graph.addRelationship({
445
- id: relId, sourceId: fileId, targetId: nodeId,
446
- type: 'DEFINES', confidence: 1.0, reason: '',
552
+ id: relId,
553
+ sourceId: fileId,
554
+ targetId: nodeId,
555
+ type: 'DEFINES',
556
+ confidence: 1.0,
557
+ reason: '',
447
558
  });
448
559
  if (propEnclosingClassId) {
449
560
  graph.addRelationship({
450
561
  id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
451
- sourceId: propEnclosingClassId, targetId: nodeId,
452
- type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
562
+ sourceId: propEnclosingClassId,
563
+ targetId: nodeId,
564
+ type: 'HAS_PROPERTY',
565
+ confidence: 1.0,
566
+ reason: '',
453
567
  });
454
568
  }
455
569
  }
@@ -495,10 +609,14 @@ importedRawReturnTypesMap) => {
495
609
  // when a type annotation AND constructor are both present (val x: Base = Sub()),
496
610
  // confirming both are class-like types is sufficient — the original code would
497
611
  // not compile if Sub didn't extend Base.
498
- if (isSubclassOf(ctorType, receiverTypeName, parentMap)
499
- || isSubclassOf(ctorType, receiverTypeName, globalParentMap)
500
- || (ctx.symbols.lookupFuzzy(ctorType).some(d => d.type === 'Class' || d.type === 'Struct')
501
- && ctx.symbols.lookupFuzzy(receiverTypeName).some(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'))) {
612
+ if (isSubclassOf(ctorType, receiverTypeName, parentMap) ||
613
+ isSubclassOf(ctorType, receiverTypeName, globalParentMap) ||
614
+ (ctx.symbols
615
+ .lookupFuzzy(ctorType)
616
+ .some((d) => d.type === 'Class' || d.type === 'Struct') &&
617
+ ctx.symbols
618
+ .lookupFuzzy(receiverTypeName)
619
+ .some((d) => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'))) {
502
620
  receiverTypeName = ctorType;
503
621
  }
504
622
  }
@@ -514,7 +632,11 @@ importedRawReturnTypesMap) => {
514
632
  // through the standard tiered resolution, use it directly as the receiver type.
515
633
  if (!receiverTypeName && receiverName && callForm === 'member') {
516
634
  const typeResolved = ctx.resolve(receiverName, file.path);
517
- if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
635
+ if (typeResolved &&
636
+ typeResolved.candidates.some((d) => d.type === 'Class' ||
637
+ d.type === 'Interface' ||
638
+ d.type === 'Struct' ||
639
+ d.type === 'Enum')) {
518
640
  receiverTypeName = receiverName;
519
641
  }
520
642
  }
@@ -538,7 +660,10 @@ importedRawReturnTypesMap) => {
538
660
  }
539
661
  if (!currentType && extracted.baseReceiverName) {
540
662
  const cr = ctx.resolve(extracted.baseReceiverName, file.path);
541
- if (cr?.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
663
+ if (cr?.candidates.some((d) => d.type === 'Class' ||
664
+ d.type === 'Interface' ||
665
+ d.type === 'Struct' ||
666
+ d.type === 'Enum')) {
542
667
  currentType = extracted.baseReceiverName;
543
668
  }
544
669
  }
@@ -552,7 +677,7 @@ importedRawReturnTypesMap) => {
552
677
  // Only used when multiple candidates survive arity filtering — ~1-3% of calls.
553
678
  const langConfig = provider.typeConfig;
554
679
  const hints = langConfig?.inferLiteralType
555
- ? { callNode, inferLiteralType: langConfig.inferLiteralType }
680
+ ? { callNode, inferLiteralType: langConfig.inferLiteralType, typeEnv }
556
681
  : undefined;
557
682
  const resolved = resolveCallTarget({
558
683
  calledName,
@@ -572,6 +697,19 @@ importedRawReturnTypesMap) => {
572
697
  confidence: resolved.confidence,
573
698
  reason: resolved.reason,
574
699
  });
700
+ if (implementorMap && callForm === 'member' && receiverTypeName) {
701
+ const implTargets = findInterfaceDispatchTargets(calledName, receiverTypeName, file.path, ctx, implementorMap, resolved.nodeId);
702
+ for (const impl of implTargets) {
703
+ graph.addRelationship({
704
+ id: generateId('CALLS', `${sourceId}:${calledName}->${impl.nodeId}`),
705
+ sourceId,
706
+ targetId: impl.nodeId,
707
+ type: 'CALLS',
708
+ confidence: impl.confidence,
709
+ reason: impl.reason,
710
+ });
711
+ }
712
+ }
575
713
  });
576
714
  ctx.clearCache();
577
715
  }
@@ -597,39 +735,34 @@ importedRawReturnTypesMap) => {
597
735
  }
598
736
  return collectedHeritage;
599
737
  };
600
- const CALLABLE_SYMBOL_TYPES = new Set([
601
- 'Function',
602
- 'Method',
603
- 'Constructor',
604
- 'Macro',
605
- 'Delegate',
606
- ]);
738
+ const CALLABLE_SYMBOL_TYPES = new Set(['Function', 'Method', 'Constructor', 'Macro', 'Delegate']);
607
739
  const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
608
740
  const filterCallableCandidates = (candidates, argCount, callForm) => {
609
741
  let kindFiltered;
610
742
  if (callForm === 'constructor') {
611
- const constructors = candidates.filter(c => c.type === 'Constructor');
743
+ const constructors = candidates.filter((c) => c.type === 'Constructor');
612
744
  if (constructors.length > 0) {
613
745
  kindFiltered = constructors;
614
746
  }
615
747
  else {
616
- const types = candidates.filter(c => CONSTRUCTOR_TARGET_TYPES.has(c.type));
617
- kindFiltered = types.length > 0 ? types : candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
748
+ const types = candidates.filter((c) => CONSTRUCTOR_TARGET_TYPES.has(c.type));
749
+ kindFiltered =
750
+ types.length > 0 ? types : candidates.filter((c) => CALLABLE_SYMBOL_TYPES.has(c.type));
618
751
  }
619
752
  }
620
753
  else {
621
- kindFiltered = candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
754
+ kindFiltered = candidates.filter((c) => CALLABLE_SYMBOL_TYPES.has(c.type));
622
755
  }
623
756
  if (kindFiltered.length === 0)
624
757
  return [];
625
758
  if (argCount === undefined)
626
759
  return kindFiltered;
627
- const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
760
+ const hasParameterMetadata = kindFiltered.some((candidate) => candidate.parameterCount !== undefined);
628
761
  if (!hasParameterMetadata)
629
762
  return kindFiltered;
630
- return kindFiltered.filter(candidate => candidate.parameterCount === undefined
631
- || (argCount >= (candidate.requiredParameterCount ?? candidate.parameterCount)
632
- && argCount <= candidate.parameterCount));
763
+ return kindFiltered.filter((candidate) => candidate.parameterCount === undefined ||
764
+ (argCount >= (candidate.requiredParameterCount ?? candidate.parameterCount) &&
765
+ argCount <= candidate.parameterCount));
633
766
  };
634
767
  const toResolveResult = (definition, tier) => ({
635
768
  nodeId: definition.nodeId,
@@ -638,13 +771,10 @@ const toResolveResult = (definition, tier) => ({
638
771
  returnType: definition.returnType,
639
772
  });
640
773
  /**
641
- * Kotlin (and JVM in general) uses boxed type names in parameter declarations
642
- * (e.g. `Int`, `Long`, `Boolean`) while inferJvmLiteralType returns unboxed
643
- * primitives (`int`, `long`, `boolean`). Normalise both sides to lowercase so
644
- * that the comparison `'Int' === 'int'` does not fail.
645
- *
646
- * Only applied to single-word identifiers that look like a JVM primitive alias;
647
- * multi-word or qualified names are left untouched.
774
+ * Kotlin often declares parameters with boxed names (`Int`, `Boolean`, …) while
775
+ * literal inference yields JVM primitives (`int`, `boolean`). This map aligns
776
+ * those for overload matching. Java parameter text is usually already primitive
777
+ * spellings, so lookups here are typically unchanged.
648
778
  */
649
779
  const KOTLIN_BOXED_TO_PRIMITIVE = {
650
780
  Int: 'int',
@@ -657,48 +787,10 @@ const KOTLIN_BOXED_TO_PRIMITIVE = {
657
787
  Char: 'char',
658
788
  };
659
789
  const normalizeJvmTypeName = (name) => KOTLIN_BOXED_TO_PRIMITIVE[name] ?? name;
660
- /**
661
- * Try to disambiguate overloaded candidates using argument literal types.
662
- * Only invoked when filteredCandidates.length > 1 and at least one has parameterTypes.
663
- * Returns the single matching candidate, or null if ambiguous/inconclusive.
664
- */
665
- const tryOverloadDisambiguation = (candidates, hints) => {
666
- if (!candidates.some(c => c.parameterTypes))
790
+ const matchCandidatesByArgTypes = (candidates, argTypes) => {
791
+ if (!candidates.some((c) => c.parameterTypes))
667
792
  return null;
668
- // Find the argument list node in the call expression.
669
- // Kotlin wraps value_arguments inside a call_suffix child, so we must also
670
- // search one level deeper when a direct match is not found.
671
- let argList = hints.callNode.childForFieldName?.('arguments')
672
- ?? hints.callNode.children.find((c) => c.type === 'arguments' || c.type === 'argument_list' || c.type === 'value_arguments');
673
- if (!argList) {
674
- // Kotlin: call_expression → call_suffix → value_arguments
675
- const callSuffix = hints.callNode.children.find((c) => c.type === 'call_suffix');
676
- if (callSuffix) {
677
- argList = callSuffix.children.find((c) => c.type === 'value_arguments');
678
- }
679
- }
680
- if (!argList)
681
- return null;
682
- const argTypes = [];
683
- for (const arg of argList.namedChildren) {
684
- if (arg.type === 'comment')
685
- continue;
686
- // Unwrap argument wrapper nodes before passing to inferLiteralType:
687
- // - Kotlin value_argument: has 'value' field containing the literal
688
- // - C# argument: has 'expression' field (handles named args like `name: "alice"`
689
- // where firstNamedChild would return name_colon instead of the value)
690
- // - Java/others: arg IS the literal directly (no unwrapping needed)
691
- const valueNode = arg.childForFieldName?.('value')
692
- ?? arg.childForFieldName?.('expression')
693
- ?? (arg.type === 'argument' || arg.type === 'value_argument'
694
- ? arg.firstNamedChild ?? arg
695
- : arg);
696
- argTypes.push(hints.inferLiteralType(valueNode));
697
- }
698
- // If no literal types could be inferred, can't disambiguate
699
- if (argTypes.every(t => t === undefined))
700
- return null;
701
- const matched = candidates.filter(c => {
793
+ const matched = candidates.filter((c) => {
702
794
  // Keep candidates without type info — conservative: partially-annotated codebases
703
795
  // (e.g. C++ with some missing declarations) may have mixed typed/untyped overloads.
704
796
  // If one typed and one untyped both survive, matched.length > 1 → returns null (no edge).
@@ -718,13 +810,24 @@ const tryOverloadDisambiguation = (candidates, hints) => {
718
810
  // implementation body all collide via generateId). Deduplicate by nodeId — if all
719
811
  // matched candidates resolve to the same graph node, disambiguation succeeded.
720
812
  if (matched.length > 1) {
721
- const uniqueIds = new Set(matched.map(c => c.nodeId));
813
+ const uniqueIds = new Set(matched.map((c) => c.nodeId));
722
814
  if (uniqueIds.size === 1)
723
815
  return matched[0];
724
816
  }
725
817
  return null;
726
818
  };
727
- const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) => {
819
+ /**
820
+ * Try to disambiguate overloaded candidates using argument literal types.
821
+ * Only invoked when filteredCandidates.length > 1 and at least one has parameterTypes.
822
+ * Returns the single matching candidate, or null if ambiguous/inconclusive.
823
+ */
824
+ const tryOverloadDisambiguation = (candidates, hints) => {
825
+ const argTypes = extractCallArgTypes(hints.callNode, hints.inferLiteralType, hints.typeEnv ? (varName, cn) => hints.typeEnv.lookup(varName, cn) : undefined);
826
+ if (!argTypes)
827
+ return null;
828
+ return matchCandidatesByArgTypes(candidates, argTypes);
829
+ };
830
+ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, preComputedArgTypes) => {
728
831
  const tiered = ctx.resolve(call.calledName, currentFile);
729
832
  if (!tiered)
730
833
  return null;
@@ -733,7 +836,7 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
733
836
  // If free-form filtering found no callable candidates but the symbol resolves to a
734
837
  // Class/Struct, retry with constructor form so CONSTRUCTOR_TARGET_TYPES applies.
735
838
  if (filteredCandidates.length === 0 && call.callForm === 'free') {
736
- const hasTypeTarget = tiered.candidates.some(c => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
839
+ const hasTypeTarget = tiered.candidates.some((c) => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
737
840
  if (hasTypeTarget) {
738
841
  filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
739
842
  }
@@ -754,7 +857,7 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
754
857
  if (aliasMap) {
755
858
  const moduleFile = aliasMap.get(call.receiverName);
756
859
  if (moduleFile) {
757
- const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
860
+ const aliasFiltered = filteredCandidates.filter((c) => c.filePath === moduleFile);
758
861
  if (aliasFiltered.length > 0) {
759
862
  filteredCandidates = aliasFiltered;
760
863
  }
@@ -769,8 +872,7 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
769
872
  fuzzyDefs = ctx.symbols.lookupFuzzy(call.calledName);
770
873
  widenCache?.set(cacheKey, fuzzyDefs);
771
874
  }
772
- const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm)
773
- .filter(c => c.filePath === moduleFile);
875
+ const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm).filter((c) => c.filePath === moduleFile);
774
876
  if (widened.length > 0)
775
877
  filteredCandidates = widened;
776
878
  }
@@ -789,8 +891,8 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
789
891
  // D1. Resolve the receiver type
790
892
  const typeResolved = ctx.resolve(call.receiverTypeName, currentFile);
791
893
  if (typeResolved && typeResolved.candidates.length > 0) {
792
- const typeNodeIds = new Set(typeResolved.candidates.map(d => d.nodeId));
793
- const typeFiles = new Set(typeResolved.candidates.map(d => d.filePath));
894
+ const typeNodeIds = new Set(typeResolved.candidates.map((d) => d.nodeId));
895
+ const typeFiles = new Set(typeResolved.candidates.map((d) => d.filePath));
794
896
  // D2. Widen candidates: same-file tier may miss the parent's method when
795
897
  // it lives in another file. Query the symbol table directly for all
796
898
  // global methods with this name, then apply arity/kind filtering.
@@ -798,32 +900,39 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
798
900
  ? filterCallableCandidates(ctx.symbols.lookupFuzzy(call.calledName), call.argCount, call.callForm)
799
901
  : filteredCandidates;
800
902
  // D3. File-based: prefer candidates whose filePath matches the resolved type's file
801
- const fileFiltered = methodPool.filter(c => typeFiles.has(c.filePath));
903
+ const fileFiltered = methodPool.filter((c) => typeFiles.has(c.filePath));
802
904
  if (fileFiltered.length === 1) {
803
905
  return toResolveResult(fileFiltered[0], tiered.tier);
804
906
  }
805
907
  // D4. ownerId fallback: narrow by ownerId matching the type's nodeId
806
908
  const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
807
- const ownerFiltered = pool.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
909
+ const ownerFiltered = pool.filter((c) => c.ownerId && typeNodeIds.has(c.ownerId));
808
910
  if (ownerFiltered.length === 1) {
809
911
  return toResolveResult(ownerFiltered[0], tiered.tier);
810
912
  }
811
913
  // E. Try overload disambiguation on the narrowed pool
812
- if ((fileFiltered.length > 1 || ownerFiltered.length > 1) && overloadHints) {
914
+ if (fileFiltered.length > 1 || ownerFiltered.length > 1) {
813
915
  const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
814
- const disambiguated = tryOverloadDisambiguation(overloadPool, overloadHints);
916
+ const disambiguated = overloadHints
917
+ ? tryOverloadDisambiguation(overloadPool, overloadHints)
918
+ : preComputedArgTypes
919
+ ? matchCandidatesByArgTypes(overloadPool, preComputedArgTypes)
920
+ : null;
815
921
  if (disambiguated)
816
922
  return toResolveResult(disambiguated, tiered.tier);
817
- }
818
- if (fileFiltered.length > 1 || ownerFiltered.length > 1)
819
923
  return null;
924
+ }
820
925
  }
821
926
  }
822
927
  // E. Overload disambiguation: when multiple candidates survive arity + receiver filtering,
823
- // try matching argument literal types against parameter types (Phase P).
824
- // Only available on sequential path (has AST); worker path falls through gracefully.
825
- if (filteredCandidates.length > 1 && overloadHints) {
826
- const disambiguated = tryOverloadDisambiguation(filteredCandidates, overloadHints);
928
+ // try matching argument types against parameter types (Phase P).
929
+ // Sequential path uses AST-based hints; worker path uses pre-computed argTypes.
930
+ if (filteredCandidates.length > 1) {
931
+ const disambiguated = overloadHints
932
+ ? tryOverloadDisambiguation(filteredCandidates, overloadHints)
933
+ : preComputedArgTypes
934
+ ? matchCandidatesByArgTypes(filteredCandidates, preComputedArgTypes)
935
+ : null;
827
936
  if (disambiguated)
828
937
  return toResolveResult(disambiguated, tiered.tier);
829
938
  }
@@ -833,8 +942,9 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
833
942
  // primary definition), they represent the same symbol. Prefer the primary
834
943
  // definition (shortest file path: Product.swift over ProductExtension.swift).
835
944
  if (filteredCandidates.length > 1) {
836
- const allSameType = filteredCandidates.every(c => c.type === filteredCandidates[0].type);
837
- if (allSameType && (filteredCandidates[0].type === 'Class' || filteredCandidates[0].type === 'Struct')) {
945
+ const allSameType = filteredCandidates.every((c) => c.type === filteredCandidates[0].type);
946
+ if (allSameType &&
947
+ (filteredCandidates[0].type === 'Class' || filteredCandidates[0].type === 'Struct')) {
838
948
  const sorted = [...filteredCandidates].sort((a, b) => a.filePath.length - b.filePath.length);
839
949
  return toResolveResult(sorted[0], tiered.tier);
840
950
  }
@@ -948,8 +1058,12 @@ const resolveFieldOwnership = (receiverName, fieldName, filePath, ctx) => {
948
1058
  const typeResolved = ctx.resolve(receiverName, filePath);
949
1059
  if (!typeResolved)
950
1060
  return undefined;
951
- const classDef = typeResolved.candidates.find(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'
952
- || d.type === 'Enum' || d.type === 'Record' || d.type === 'Impl');
1061
+ const classDef = typeResolved.candidates.find((d) => d.type === 'Class' ||
1062
+ d.type === 'Struct' ||
1063
+ d.type === 'Interface' ||
1064
+ d.type === 'Enum' ||
1065
+ d.type === 'Record' ||
1066
+ d.type === 'Impl');
953
1067
  if (!classDef)
954
1068
  return undefined;
955
1069
  return ctx.symbols.lookupFieldByOwner(classDef.nodeId, fieldName) ?? undefined;
@@ -1025,7 +1139,7 @@ const walkMixedChain = (chain, startType, filePath, ctx, onFieldResolved) => {
1025
1139
  * Fast path: resolve pre-extracted call sites from workers.
1026
1140
  * No AST parsing — workers already extracted calledName + sourceId.
1027
1141
  */
1028
- export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
1142
+ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings, implementorMap) => {
1029
1143
  // Scope-aware receiver types: keyed by filePath → "funcName\0varName" → typeName.
1030
1144
  // The scope dimension prevents collisions when two functions in the same file
1031
1145
  // have same-named locals pointing to different constructor types.
@@ -1069,9 +1183,15 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
1069
1183
  }
1070
1184
  }
1071
1185
  // Step 1b: class-as-receiver for static method calls (e.g. UserService.find_user())
1072
- if (!effectiveCall.receiverTypeName && effectiveCall.receiverName && effectiveCall.callForm === 'member') {
1186
+ if (!effectiveCall.receiverTypeName &&
1187
+ effectiveCall.receiverName &&
1188
+ effectiveCall.callForm === 'member') {
1073
1189
  const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath);
1074
- if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
1190
+ if (typeResolved &&
1191
+ typeResolved.candidates.some((d) => d.type === 'Class' ||
1192
+ d.type === 'Interface' ||
1193
+ d.type === 'Struct' ||
1194
+ d.type === 'Enum')) {
1075
1195
  effectiveCall = { ...effectiveCall, receiverTypeName: effectiveCall.receiverName };
1076
1196
  }
1077
1197
  }
@@ -1087,7 +1207,10 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
1087
1207
  }
1088
1208
  if (!currentType && effectiveCall.receiverName) {
1089
1209
  const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath);
1090
- if (typeResolved?.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
1210
+ if (typeResolved?.candidates.some((d) => d.type === 'Class' ||
1211
+ d.type === 'Interface' ||
1212
+ d.type === 'Struct' ||
1213
+ d.type === 'Enum')) {
1091
1214
  currentType = effectiveCall.receiverName;
1092
1215
  }
1093
1216
  }
@@ -1098,7 +1221,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
1098
1221
  }
1099
1222
  }
1100
1223
  }
1101
- const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache);
1224
+ const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache, effectiveCall.argTypes);
1102
1225
  if (!resolved)
1103
1226
  continue;
1104
1227
  const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
@@ -1110,6 +1233,19 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
1110
1233
  confidence: resolved.confidence,
1111
1234
  reason: resolved.reason,
1112
1235
  });
1236
+ if (implementorMap && effectiveCall.callForm === 'member' && effectiveCall.receiverTypeName) {
1237
+ const implTargets = findInterfaceDispatchTargets(effectiveCall.calledName, effectiveCall.receiverTypeName, effectiveCall.filePath, ctx, implementorMap, resolved.nodeId);
1238
+ for (const impl of implTargets) {
1239
+ graph.addRelationship({
1240
+ id: generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${impl.nodeId}`),
1241
+ sourceId: effectiveCall.sourceId,
1242
+ targetId: impl.nodeId,
1243
+ type: 'CALLS',
1244
+ confidence: impl.confidence,
1245
+ reason: impl.reason,
1246
+ });
1247
+ }
1248
+ }
1113
1249
  }
1114
1250
  ctx.clearCache();
1115
1251
  }
@@ -1145,8 +1281,12 @@ export const processAssignmentsFromExtracted = (graph, assignments, ctx, constru
1145
1281
  // Tier 3: static class-as-receiver fallback
1146
1282
  if (!receiverTypeName) {
1147
1283
  const resolved = ctx.resolve(asn.receiverText, asn.filePath);
1148
- if (resolved?.candidates.some(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'
1149
- || d.type === 'Enum' || d.type === 'Record' || d.type === 'Impl')) {
1284
+ if (resolved?.candidates.some((d) => d.type === 'Class' ||
1285
+ d.type === 'Struct' ||
1286
+ d.type === 'Interface' ||
1287
+ d.type === 'Enum' ||
1288
+ d.type === 'Record' ||
1289
+ d.type === 'Impl')) {
1150
1290
  receiverTypeName = asn.receiverText;
1151
1291
  }
1152
1292
  }
@@ -1232,25 +1372,82 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, on
1232
1372
  // that happen to share names with response variables (data, result, response, etc.).
1233
1373
  const RESPONSE_ACCESS_BLOCKLIST = new Set([
1234
1374
  // Fetch/Response API
1235
- 'json', 'text', 'blob', 'arrayBuffer', 'formData', 'ok', 'status', 'headers', 'clone',
1375
+ 'json',
1376
+ 'text',
1377
+ 'blob',
1378
+ 'arrayBuffer',
1379
+ 'formData',
1380
+ 'ok',
1381
+ 'status',
1382
+ 'headers',
1383
+ 'clone',
1236
1384
  // Promise
1237
- 'then', 'catch', 'finally',
1385
+ 'then',
1386
+ 'catch',
1387
+ 'finally',
1238
1388
  // Array
1239
- 'map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every',
1240
- 'push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'concat', 'join',
1241
- 'sort', 'reverse', 'includes', 'indexOf',
1389
+ 'map',
1390
+ 'filter',
1391
+ 'forEach',
1392
+ 'reduce',
1393
+ 'find',
1394
+ 'some',
1395
+ 'every',
1396
+ 'push',
1397
+ 'pop',
1398
+ 'shift',
1399
+ 'unshift',
1400
+ 'splice',
1401
+ 'slice',
1402
+ 'concat',
1403
+ 'join',
1404
+ 'sort',
1405
+ 'reverse',
1406
+ 'includes',
1407
+ 'indexOf',
1242
1408
  // Object
1243
- 'length', 'toString', 'valueOf', 'keys', 'values', 'entries',
1409
+ 'length',
1410
+ 'toString',
1411
+ 'valueOf',
1412
+ 'keys',
1413
+ 'values',
1414
+ 'entries',
1244
1415
  // DOM methods — file-download patterns often reuse `data`/`response` variable names
1245
- 'appendChild', 'removeChild', 'insertBefore', 'replaceChild', 'replaceChildren',
1246
- 'createElement', 'getElementById', 'querySelector', 'querySelectorAll',
1247
- 'setAttribute', 'getAttribute', 'removeAttribute', 'hasAttribute',
1248
- 'addEventListener', 'removeEventListener', 'dispatchEvent',
1249
- 'classList', 'className',
1250
- 'parentNode', 'parentElement', 'childNodes', 'children',
1251
- 'nextSibling', 'previousSibling', 'firstChild', 'lastChild',
1252
- 'click', 'focus', 'blur', 'submit', 'reset',
1253
- 'innerHTML', 'outerHTML', 'textContent', 'innerText',
1416
+ 'appendChild',
1417
+ 'removeChild',
1418
+ 'insertBefore',
1419
+ 'replaceChild',
1420
+ 'replaceChildren',
1421
+ 'createElement',
1422
+ 'getElementById',
1423
+ 'querySelector',
1424
+ 'querySelectorAll',
1425
+ 'setAttribute',
1426
+ 'getAttribute',
1427
+ 'removeAttribute',
1428
+ 'hasAttribute',
1429
+ 'addEventListener',
1430
+ 'removeEventListener',
1431
+ 'dispatchEvent',
1432
+ 'classList',
1433
+ 'className',
1434
+ 'parentNode',
1435
+ 'parentElement',
1436
+ 'childNodes',
1437
+ 'children',
1438
+ 'nextSibling',
1439
+ 'previousSibling',
1440
+ 'firstChild',
1441
+ 'lastChild',
1442
+ 'click',
1443
+ 'focus',
1444
+ 'blur',
1445
+ 'submit',
1446
+ 'reset',
1447
+ 'innerHTML',
1448
+ 'outerHTML',
1449
+ 'textContent',
1450
+ 'innerText',
1254
1451
  ]);
1255
1452
  export const extractConsumerAccessedKeys = (content) => {
1256
1453
  const keys = new Set();
@@ -1370,7 +1567,9 @@ export const extractFetchCallsFromFiles = async (files, astCache) => {
1370
1567
  let tree = astCache.get(file.path);
1371
1568
  if (!tree) {
1372
1569
  try {
1373
- tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
1570
+ tree = parser.parse(file.content, undefined, {
1571
+ bufferSize: getTreeSitterBufferSize(file.content.length),
1572
+ });
1374
1573
  }
1375
1574
  catch {
1376
1575
  continue;
@@ -1388,7 +1587,7 @@ export const extractFetchCallsFromFiles = async (files, astCache) => {
1388
1587
  }
1389
1588
  for (const match of matches) {
1390
1589
  const captureMap = {};
1391
- match.captures.forEach(c => captureMap[c.name] = c.node);
1590
+ match.captures.forEach((c) => (captureMap[c.name] = c.node));
1392
1591
  if (captureMap['route.fetch']) {
1393
1592
  const urlNode = captureMap['route.url'] ?? captureMap['route.template_url'];
1394
1593
  if (urlNode) {
@@ -1404,7 +1603,11 @@ export const extractFetchCallsFromFiles = async (files, astCache) => {
1404
1603
  const url = captureMap['http_client.url'].text;
1405
1604
  const HTTP_CLIENT_ONLY = new Set(['head', 'options', 'request', 'ajax']);
1406
1605
  if (method && HTTP_CLIENT_ONLY.has(method) && url.startsWith('/')) {
1407
- result.push({ filePath: file.path, fetchURL: url, lineNumber: captureMap['http_client'].startPosition.row });
1606
+ result.push({
1607
+ filePath: file.path,
1608
+ fetchURL: url,
1609
+ lineNumber: captureMap['http_client'].startPosition.row,
1610
+ });
1408
1611
  }
1409
1612
  }
1410
1613
  }