gitnexus 1.4.7 → 1.4.9

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 (242) hide show
  1. package/README.md +29 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index-repo.d.ts +15 -0
  7. package/dist/cli/index-repo.js +115 -0
  8. package/dist/cli/index.js +13 -3
  9. package/dist/cli/setup.js +90 -10
  10. package/dist/cli/wiki.d.ts +4 -0
  11. package/dist/cli/wiki.js +174 -53
  12. package/dist/config/supported-languages.d.ts +33 -1
  13. package/dist/config/supported-languages.js +32 -0
  14. package/dist/core/embeddings/embedder.d.ts +6 -1
  15. package/dist/core/embeddings/embedder.js +65 -5
  16. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  17. package/dist/core/embeddings/http-client.d.ts +31 -0
  18. package/dist/core/embeddings/http-client.js +179 -0
  19. package/dist/core/embeddings/index.d.ts +1 -0
  20. package/dist/core/embeddings/index.js +1 -0
  21. package/dist/core/embeddings/types.d.ts +1 -1
  22. package/dist/core/graph/graph.js +9 -1
  23. package/dist/core/graph/types.d.ts +11 -2
  24. package/dist/core/ingestion/call-processor.d.ts +66 -2
  25. package/dist/core/ingestion/call-processor.js +650 -30
  26. package/dist/core/ingestion/call-routing.d.ts +9 -18
  27. package/dist/core/ingestion/call-routing.js +0 -19
  28. package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
  29. package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
  30. package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
  31. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
  32. package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
  33. package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
  34. package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
  35. package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
  36. package/dist/core/ingestion/cobol-processor.d.ts +54 -0
  37. package/dist/core/ingestion/cobol-processor.js +1186 -0
  38. package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
  39. package/dist/core/ingestion/entry-point-scoring.js +52 -28
  40. package/dist/core/ingestion/export-detection.d.ts +47 -8
  41. package/dist/core/ingestion/export-detection.js +29 -50
  42. package/dist/core/ingestion/field-extractor.d.ts +29 -0
  43. package/dist/core/ingestion/field-extractor.js +25 -0
  44. package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
  45. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
  46. package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
  47. package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
  48. package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
  49. package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
  50. package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
  51. package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
  52. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
  53. package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
  54. package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
  55. package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
  56. package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
  57. package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
  58. package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
  59. package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
  60. package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
  61. package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
  62. package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
  63. package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
  64. package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
  65. package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
  66. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
  67. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
  68. package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
  69. package/dist/core/ingestion/field-extractors/generic.js +111 -0
  70. package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
  71. package/dist/core/ingestion/field-extractors/typescript.js +291 -0
  72. package/dist/core/ingestion/field-types.d.ts +59 -0
  73. package/dist/core/ingestion/field-types.js +2 -0
  74. package/dist/core/ingestion/framework-detection.d.ts +97 -2
  75. package/dist/core/ingestion/framework-detection.js +114 -14
  76. package/dist/core/ingestion/heritage-processor.js +62 -66
  77. package/dist/core/ingestion/import-processor.d.ts +9 -10
  78. package/dist/core/ingestion/import-processor.js +150 -196
  79. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
  80. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
  81. package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
  82. package/dist/core/ingestion/import-resolvers/dart.js +44 -0
  83. package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
  84. package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
  85. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +10 -1
  86. package/dist/core/ingestion/import-resolvers/jvm.js +159 -0
  87. package/dist/core/ingestion/import-resolvers/php.d.ts +25 -0
  88. package/dist/core/ingestion/import-resolvers/php.js +80 -0
  89. package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
  90. package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
  91. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
  92. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
  93. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
  94. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
  95. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
  96. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
  97. package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
  98. package/dist/core/ingestion/import-resolvers/swift.js +23 -0
  99. package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
  100. package/dist/core/ingestion/import-resolvers/types.js +6 -0
  101. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +2 -0
  102. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +7 -0
  103. package/dist/core/ingestion/language-config.d.ts +6 -0
  104. package/dist/core/ingestion/language-config.js +13 -0
  105. package/dist/core/ingestion/language-provider.d.ts +121 -0
  106. package/dist/core/ingestion/language-provider.js +24 -0
  107. package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
  108. package/dist/core/ingestion/languages/c-cpp.js +71 -0
  109. package/dist/core/ingestion/languages/cobol.d.ts +1 -0
  110. package/dist/core/ingestion/languages/cobol.js +26 -0
  111. package/dist/core/ingestion/languages/csharp.d.ts +8 -0
  112. package/dist/core/ingestion/languages/csharp.js +49 -0
  113. package/dist/core/ingestion/languages/dart.d.ts +12 -0
  114. package/dist/core/ingestion/languages/dart.js +58 -0
  115. package/dist/core/ingestion/languages/go.d.ts +11 -0
  116. package/dist/core/ingestion/languages/go.js +28 -0
  117. package/dist/core/ingestion/languages/index.d.ts +38 -0
  118. package/dist/core/ingestion/languages/index.js +63 -0
  119. package/dist/core/ingestion/languages/java.d.ts +9 -0
  120. package/dist/core/ingestion/languages/java.js +29 -0
  121. package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
  122. package/dist/core/ingestion/languages/kotlin.js +53 -0
  123. package/dist/core/ingestion/languages/php.d.ts +8 -0
  124. package/dist/core/ingestion/languages/php.js +145 -0
  125. package/dist/core/ingestion/languages/python.d.ts +12 -0
  126. package/dist/core/ingestion/languages/python.js +39 -0
  127. package/dist/core/ingestion/languages/ruby.d.ts +9 -0
  128. package/dist/core/ingestion/languages/ruby.js +44 -0
  129. package/dist/core/ingestion/languages/rust.d.ts +12 -0
  130. package/dist/core/ingestion/languages/rust.js +44 -0
  131. package/dist/core/ingestion/languages/swift.d.ts +12 -0
  132. package/dist/core/ingestion/languages/swift.js +133 -0
  133. package/dist/core/ingestion/languages/typescript.d.ts +10 -0
  134. package/dist/core/ingestion/languages/typescript.js +60 -0
  135. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  136. package/dist/core/ingestion/markdown-processor.js +124 -0
  137. package/dist/core/ingestion/mro-processor.js +22 -18
  138. package/dist/core/ingestion/named-binding-processor.d.ts +18 -0
  139. package/dist/core/ingestion/named-binding-processor.js +42 -0
  140. package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
  141. package/dist/core/ingestion/named-bindings/csharp.js +37 -0
  142. package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
  143. package/dist/core/ingestion/named-bindings/java.js +29 -0
  144. package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
  145. package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
  146. package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
  147. package/dist/core/ingestion/named-bindings/php.js +61 -0
  148. package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
  149. package/dist/core/ingestion/named-bindings/python.js +49 -0
  150. package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
  151. package/dist/core/ingestion/named-bindings/rust.js +64 -0
  152. package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
  153. package/dist/core/ingestion/named-bindings/types.js +6 -0
  154. package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
  155. package/dist/core/ingestion/named-bindings/typescript.js +58 -0
  156. package/dist/core/ingestion/parsing-processor.d.ts +6 -2
  157. package/dist/core/ingestion/parsing-processor.js +125 -85
  158. package/dist/core/ingestion/pipeline.d.ts +10 -0
  159. package/dist/core/ingestion/pipeline.js +1235 -317
  160. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  161. package/dist/core/ingestion/resolution-context.js +8 -5
  162. package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
  163. package/dist/core/ingestion/route-extractors/expo.js +36 -0
  164. package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
  165. package/dist/core/ingestion/route-extractors/middleware.js +143 -0
  166. package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
  167. package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
  168. package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
  169. package/dist/core/ingestion/route-extractors/php.js +21 -0
  170. package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
  171. package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
  172. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  173. package/dist/core/ingestion/symbol-table.js +20 -6
  174. package/dist/core/ingestion/tree-sitter-queries.d.ts +10 -9
  175. package/dist/core/ingestion/tree-sitter-queries.js +274 -11
  176. package/dist/core/ingestion/type-env.d.ts +42 -18
  177. package/dist/core/ingestion/type-env.js +481 -106
  178. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  179. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  180. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  181. package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
  182. package/dist/core/ingestion/type-extractors/dart.js +371 -0
  183. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  184. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  185. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -15
  186. package/dist/core/ingestion/type-extractors/shared.js +14 -112
  187. package/dist/core/ingestion/type-extractors/swift.js +338 -7
  188. package/dist/core/ingestion/type-extractors/types.d.ts +40 -8
  189. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  190. package/dist/core/ingestion/utils/ast-helpers.d.ts +83 -0
  191. package/dist/core/ingestion/utils/ast-helpers.js +817 -0
  192. package/dist/core/ingestion/utils/call-analysis.d.ts +73 -0
  193. package/dist/core/ingestion/utils/call-analysis.js +527 -0
  194. package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
  195. package/dist/core/ingestion/utils/event-loop.js +5 -0
  196. package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
  197. package/dist/core/ingestion/utils/language-detection.js +70 -0
  198. package/dist/core/ingestion/utils/verbose.d.ts +1 -0
  199. package/dist/core/ingestion/utils/verbose.js +7 -0
  200. package/dist/core/ingestion/workers/parse-worker.d.ts +55 -5
  201. package/dist/core/ingestion/workers/parse-worker.js +415 -225
  202. package/dist/core/lbug/csv-generator.js +51 -1
  203. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  204. package/dist/core/lbug/lbug-adapter.js +75 -4
  205. package/dist/core/lbug/schema.d.ts +8 -4
  206. package/dist/core/lbug/schema.js +65 -4
  207. package/dist/core/tree-sitter/parser-loader.js +7 -1
  208. package/dist/core/wiki/cursor-client.d.ts +31 -0
  209. package/dist/core/wiki/cursor-client.js +127 -0
  210. package/dist/core/wiki/generator.d.ts +28 -9
  211. package/dist/core/wiki/generator.js +115 -18
  212. package/dist/core/wiki/graph-queries.d.ts +4 -0
  213. package/dist/core/wiki/graph-queries.js +7 -1
  214. package/dist/core/wiki/llm-client.d.ts +2 -0
  215. package/dist/core/wiki/llm-client.js +8 -4
  216. package/dist/core/wiki/prompts.d.ts +3 -3
  217. package/dist/core/wiki/prompts.js +6 -0
  218. package/dist/mcp/core/embedder.js +11 -3
  219. package/dist/mcp/core/lbug-adapter.d.ts +5 -0
  220. package/dist/mcp/core/lbug-adapter.js +23 -2
  221. package/dist/mcp/local/local-backend.d.ts +38 -5
  222. package/dist/mcp/local/local-backend.js +804 -63
  223. package/dist/mcp/resources.js +2 -0
  224. package/dist/mcp/tools.js +73 -4
  225. package/dist/server/api.d.ts +19 -1
  226. package/dist/server/api.js +66 -6
  227. package/dist/storage/git.d.ts +12 -0
  228. package/dist/storage/git.js +21 -0
  229. package/dist/storage/repo-manager.d.ts +3 -0
  230. package/package.json +25 -16
  231. package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
  232. package/dist/core/ingestion/named-binding-extraction.js +0 -363
  233. package/dist/core/ingestion/resolvers/index.d.ts +0 -18
  234. package/dist/core/ingestion/resolvers/index.js +0 -13
  235. package/dist/core/ingestion/resolvers/jvm.js +0 -87
  236. package/dist/core/ingestion/resolvers/php.d.ts +0 -15
  237. package/dist/core/ingestion/resolvers/php.js +0 -35
  238. package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
  239. package/dist/core/ingestion/type-extractors/index.js +0 -31
  240. package/dist/core/ingestion/utils.d.ts +0 -138
  241. package/dist/core/ingestion/utils.js +0 -1290
  242. package/scripts/patch-tree-sitter-swift.cjs +0 -74
@@ -1,13 +1,142 @@
1
1
  import Parser from 'tree-sitter';
2
2
  import { TIER_CONFIDENCE } from './resolution-context.js';
3
3
  import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
4
- import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
4
+ import { getProvider } from './languages/index.js';
5
5
  import { generateId } from '../../lib/utils.js';
6
- import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, extractMixedChain, } from './utils.js';
7
- import { buildTypeEnv } from './type-env.js';
6
+ import { getLanguageFromFilename } from './utils/language-detection.js';
7
+ import { isVerboseIngestionEnabled } from './utils/verbose.js';
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';
11
+ import { buildTypeEnv, isSubclassOf } from './type-env.js';
8
12
  import { getTreeSitterBufferSize } from './constants.js';
9
- import { callRouters } from './call-routing.js';
13
+ import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
10
14
  import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
15
+ const MAX_EXPORTS_PER_FILE = 500;
16
+ const MAX_TYPE_NAME_LENGTH = 256;
17
+ /** Build a map of imported callee names → return types for cross-file call-result binding.
18
+ * Consulted ONLY when SymbolTable has no unambiguous local match (local-first principle). */
19
+ export function buildImportedReturnTypes(filePath, namedImportMap, symbolTable) {
20
+ const result = new Map();
21
+ const fileImports = namedImportMap.get(filePath);
22
+ if (!fileImports)
23
+ return result;
24
+ for (const [localName, binding] of fileImports) {
25
+ const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName);
26
+ if (!def?.returnType)
27
+ continue;
28
+ const simpleReturn = extractReturnTypeName(def.returnType);
29
+ if (simpleReturn)
30
+ result.set(localName, simpleReturn);
31
+ }
32
+ return result;
33
+ }
34
+ /** Build cross-file RAW return types for imported callables.
35
+ * Unlike buildImportedReturnTypes (which stores extractReturnTypeName output),
36
+ * this stores the raw declared return type string (e.g., 'User[]', 'List<User>').
37
+ * Used by lookupRawReturnType for for-loop element extraction via extractElementTypeFromString. */
38
+ export function buildImportedRawReturnTypes(filePath, namedImportMap, symbolTable) {
39
+ const result = new Map();
40
+ const fileImports = namedImportMap.get(filePath);
41
+ if (!fileImports)
42
+ return result;
43
+ for (const [localName, binding] of fileImports) {
44
+ const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName);
45
+ if (!def?.returnType)
46
+ continue;
47
+ result.set(localName, def.returnType);
48
+ }
49
+ return result;
50
+ }
51
+ /** Collect resolved type bindings for exported file-scope symbols.
52
+ * Uses graph node isExported flag — does NOT require isExported on SymbolDefinition. */
53
+ function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
54
+ const fileScope = typeEnv.fileScope();
55
+ if (!fileScope || fileScope.size === 0)
56
+ return null;
57
+ const exported = new Map();
58
+ for (const [varName, typeName] of fileScope) {
59
+ if (exported.size >= MAX_EXPORTS_PER_FILE)
60
+ break;
61
+ if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH)
62
+ continue;
63
+ const nodeId = symbolTable.lookupExact(filePath, varName);
64
+ if (!nodeId)
65
+ continue;
66
+ const node = graph.getNode(nodeId);
67
+ if (node?.properties?.isExported) {
68
+ exported.set(varName, typeName);
69
+ }
70
+ }
71
+ return exported.size > 0 ? exported : null;
72
+ }
73
+ /** Build ExportedTypeMap from graph nodes — used for worker path where TypeEnv
74
+ * is not available in the main thread. Collects returnType/declaredType from
75
+ * exported symbols that have callables with known return types. */
76
+ export function buildExportedTypeMapFromGraph(graph, symbolTable) {
77
+ const result = new Map();
78
+ graph.forEachNode(node => {
79
+ if (!node.properties?.isExported)
80
+ return;
81
+ if (!node.properties?.filePath || !node.properties?.name)
82
+ return;
83
+ const filePath = node.properties.filePath;
84
+ const name = node.properties.name;
85
+ if (!name || name.length > MAX_TYPE_NAME_LENGTH)
86
+ return;
87
+ // For callable symbols, use returnType; for properties/variables, use declaredType
88
+ const def = symbolTable.lookupExactFull(filePath, name);
89
+ if (!def)
90
+ return;
91
+ const typeName = def.returnType ?? def.declaredType;
92
+ if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH)
93
+ return;
94
+ // Extract simple type name (strip Promise<>, etc.) — reuse shared utility
95
+ const simpleType = extractReturnTypeName(typeName) ?? typeName;
96
+ if (!simpleType)
97
+ return;
98
+ let fileExports = result.get(filePath);
99
+ if (!fileExports) {
100
+ fileExports = new Map();
101
+ result.set(filePath, fileExports);
102
+ }
103
+ if (fileExports.size < MAX_EXPORTS_PER_FILE) {
104
+ fileExports.set(name, simpleType);
105
+ }
106
+ });
107
+ return result;
108
+ }
109
+ /** Seed cross-file receiver types into pre-extracted call records.
110
+ * Fills missing receiverTypeName for single-hop imported variables
111
+ * using ExportedTypeMap + namedImportMap — zero disk I/O, zero AST re-parsing.
112
+ * Mutates calls in-place. Runs BEFORE processCallsFromExtracted. */
113
+ export function seedCrossFileReceiverTypes(calls, namedImportMap, exportedTypeMap) {
114
+ if (namedImportMap.size === 0 || exportedTypeMap.size === 0) {
115
+ return { enrichedCount: 0 };
116
+ }
117
+ let enrichedCount = 0;
118
+ for (const call of calls) {
119
+ if (call.receiverTypeName || !call.receiverName)
120
+ continue;
121
+ if (call.callForm !== 'member')
122
+ continue;
123
+ const fileImports = namedImportMap.get(call.filePath);
124
+ if (!fileImports)
125
+ continue;
126
+ const binding = fileImports.get(call.receiverName);
127
+ if (!binding)
128
+ continue;
129
+ const upstream = exportedTypeMap.get(binding.sourcePath);
130
+ if (!upstream)
131
+ continue;
132
+ const type = upstream.get(binding.exportedName);
133
+ if (type) {
134
+ call.receiverTypeName = type;
135
+ enrichedCount++;
136
+ }
137
+ }
138
+ return { enrichedCount };
139
+ }
11
140
  // Stdlib methods that preserve the receiver's type identity. When TypeEnv already
12
141
  // strips nullable wrappers (Option<User> → User), these chain steps are no-ops
13
142
  // for type resolution — the current type passes through unchanged.
@@ -21,7 +150,7 @@ const TYPE_PRESERVING_METHODS = new Set([
21
150
  * Walk up the AST from a node to find the enclosing function/method.
22
151
  * Returns null if the call is at module/file level (top-level code).
23
152
  */
24
- const findEnclosingFunction = (node, filePath, ctx) => {
153
+ const findEnclosingFunction = (node, filePath, ctx, provider) => {
25
154
  let current = node.parent;
26
155
  while (current) {
27
156
  if (FUNCTION_NODE_TYPES.has(current.type)) {
@@ -31,7 +160,33 @@ const findEnclosingFunction = (node, filePath, ctx) => {
31
160
  if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
32
161
  return resolved.candidates[0].nodeId;
33
162
  }
34
- return generateId(label, `${filePath}:${funcName}`);
163
+ // Apply labelOverride so label matches the definition phase (single source of truth).
164
+ let finalLabel = label;
165
+ if (provider.labelOverride) {
166
+ const override = provider.labelOverride(current, label);
167
+ if (override !== null)
168
+ finalLabel = override;
169
+ }
170
+ return generateId(finalLabel, `${filePath}:${funcName}`);
171
+ }
172
+ }
173
+ // Language-specific enclosing function resolution (e.g., Dart where
174
+ // function_body is a sibling of function_signature, not a child).
175
+ if (provider.enclosingFunctionFinder) {
176
+ const customResult = provider.enclosingFunctionFinder(current);
177
+ if (customResult) {
178
+ // Try SymbolTable first (same pattern as the FUNCTION_NODE_TYPES branch above).
179
+ const resolved = ctx.resolve(customResult.funcName, filePath);
180
+ if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
181
+ return resolved.candidates[0].nodeId;
182
+ }
183
+ let finalLabel = customResult.label;
184
+ if (provider.labelOverride) {
185
+ const override = provider.labelOverride(current.previousSibling, finalLabel);
186
+ if (override !== null)
187
+ finalLabel = override;
188
+ }
189
+ return generateId(finalLabel, `${filePath}:${customResult.funcName}`);
35
190
  }
36
191
  }
37
192
  current = current.parent;
@@ -87,10 +242,23 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
87
242
  }
88
243
  return verified;
89
244
  };
90
- export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
245
+ export const processCalls = async (graph, files, astCache, ctx, onProgress, exportedTypeMap,
246
+ /** Phase 14: pre-resolved cross-file bindings to seed into buildTypeEnv. Keyed by filePath → Map<localName, typeName>. */
247
+ importedBindingsMap,
248
+ /** Phase 14 E3: cross-file return types for imported callables. Keyed by filePath → Map<calleeName, returnType>.
249
+ * Consulted ONLY when SymbolTable has no unambiguous match (local-first principle). */
250
+ importedReturnTypesMap,
251
+ /** Phase 14 E3: cross-file RAW return types for for-loop element extraction. Keyed by filePath → Map<calleeName, rawReturnType>. */
252
+ importedRawReturnTypesMap) => {
91
253
  const parser = await loadParser();
92
254
  const collectedHeritage = [];
93
255
  const pendingWrites = [];
256
+ // Phase P cross-file: accumulate heritage across files for cross-file isSubclassOf.
257
+ // Used as a secondary check when per-file parentMap lacks the relationship — helps
258
+ // when the heritage-declaring file is processed before the call site file.
259
+ // For remaining cases (reverse file order), the SymbolTable class-type fallback applies.
260
+ const globalParentMap = new Map();
261
+ const globalParentSeen = new Map();
94
262
  const logSkipped = isVerboseIngestionEnabled();
95
263
  const skippedByLang = logSkipped ? new Map() : null;
96
264
  for (let i = 0; i < files.length; i++) {
@@ -107,7 +275,8 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
107
275
  }
108
276
  continue;
109
277
  }
110
- const queryStr = LANGUAGE_QUERIES[language];
278
+ const provider = getProvider(language);
279
+ const queryStr = provider.treeSitterQueries;
111
280
  if (!queryStr)
112
281
  continue;
113
282
  await loadLanguage(language, file.path);
@@ -132,14 +301,65 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
132
301
  console.warn(`Query error for ${file.path}:`, queryError);
133
302
  continue;
134
303
  }
135
- const lang = getLanguageFromFilename(file.path);
136
- const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
137
- const callRouter = callRouters[language];
138
- const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
304
+ // Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
305
+ // Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
306
+ const fileParentMap = new Map();
307
+ for (const match of matches) {
308
+ const captureMap = {};
309
+ match.captures.forEach(c => captureMap[c.name] = c.node);
310
+ if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
311
+ const className = captureMap['heritage.class'].text;
312
+ const parentName = captureMap['heritage.extends'].text;
313
+ const extendsNode = captureMap['heritage.extends'];
314
+ const fieldDecl = extendsNode.parent;
315
+ if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
316
+ continue;
317
+ let parents = fileParentMap.get(className);
318
+ if (!parents) {
319
+ parents = [];
320
+ fileParentMap.set(className, parents);
321
+ }
322
+ if (!parents.includes(parentName))
323
+ parents.push(parentName);
324
+ }
325
+ }
326
+ const parentMap = fileParentMap;
327
+ // Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups.
328
+ // Uses a parallel Set (globalParentSeen) for O(1) deduplication instead of O(n) includes().
329
+ for (const [cls, parents] of fileParentMap) {
330
+ let global = globalParentMap.get(cls);
331
+ let seen = globalParentSeen.get(cls);
332
+ if (!global) {
333
+ global = [];
334
+ globalParentMap.set(cls, global);
335
+ }
336
+ if (!seen) {
337
+ seen = new Set();
338
+ globalParentSeen.set(cls, seen);
339
+ }
340
+ for (const p of parents) {
341
+ if (!seen.has(p)) {
342
+ seen.add(p);
343
+ global.push(p);
344
+ }
345
+ }
346
+ }
347
+ const importedBindings = importedBindingsMap?.get(file.path);
348
+ const importedReturnTypes = importedReturnTypesMap?.get(file.path);
349
+ const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
350
+ const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
351
+ if (typeEnv && exportedTypeMap) {
352
+ const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
353
+ if (fileExports)
354
+ exportedTypeMap.set(file.path, fileExports);
355
+ }
356
+ const callRouter = provider.callRouter;
357
+ const verifiedReceivers = typeEnv.constructorBindings.length > 0
139
358
  ? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
140
359
  : new Map();
141
360
  const receiverIndex = buildReceiverTypeIndex(verifiedReceivers);
142
361
  ctx.enableCache(file.path);
362
+ const widenCache = new Map();
143
363
  matches.forEach(match => {
144
364
  const captureMap = {};
145
365
  match.captures.forEach(c => captureMap[c.name] = c.node);
@@ -155,7 +375,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
155
375
  }
156
376
  // Fall back to verified constructor bindings (mirrors CALLS resolution tier 2)
157
377
  if (!receiverTypeName && receiverText && receiverIndex.size > 0) {
158
- const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx);
378
+ const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
159
379
  const funcName = enclosing ? extractFuncNameFromSourceId(enclosing) : '';
160
380
  receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverText);
161
381
  }
@@ -167,7 +387,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
167
387
  }
168
388
  }
169
389
  if (receiverTypeName) {
170
- const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx);
390
+ const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
171
391
  const srcId = enclosing || generateId('File', file.path);
172
392
  // Defer resolution: Ruby attr_accessor properties are registered during
173
393
  // this same loop, so cross-file lookups fail if the declaring file hasn't
@@ -185,7 +405,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
185
405
  if (!nameNode)
186
406
  return;
187
407
  const calledName = nameNode.text;
188
- const routed = callRouter(calledName, captureMap['call']);
408
+ const routed = callRouter?.(calledName, captureMap['call']);
189
409
  if (routed) {
190
410
  switch (routed.kind) {
191
411
  case 'skip':
@@ -239,15 +459,53 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
239
459
  break;
240
460
  }
241
461
  }
242
- if (isBuiltInOrNoise(calledName))
462
+ if (provider.isBuiltInName(calledName))
243
463
  return;
244
464
  const callNode = captureMap['call'];
245
465
  const callForm = inferCallForm(callNode, nameNode);
246
466
  const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
247
467
  let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
468
+ // Phase P: virtual dispatch override — when the declared type is a base class but
469
+ // the constructor created a known subclass, prefer the more specific type.
470
+ // Checks per-file parentMap first, then falls back to globalParentMap for
471
+ // cross-file heritage (e.g. Dog extends Animal declared in a different file).
472
+ // Reconstructs the exact scope key (funcName@startIndex\0varName) from the
473
+ // enclosing function AST node for a correct, O(1) map lookup.
474
+ if (receiverTypeName && receiverName && typeEnv && typeEnv.constructorTypeMap.size > 0) {
475
+ // Reconstruct scope key to match constructorTypeMap's scope\0varName format
476
+ let scope = '';
477
+ let p = callNode.parent;
478
+ while (p) {
479
+ if (FUNCTION_NODE_TYPES.has(p.type)) {
480
+ const { funcName } = extractFunctionName(p);
481
+ if (funcName) {
482
+ scope = `${funcName}@${p.startIndex}`;
483
+ break;
484
+ }
485
+ }
486
+ p = p.parent;
487
+ }
488
+ const ctorType = typeEnv.constructorTypeMap.get(`${scope}\0${receiverName}`);
489
+ if (ctorType && ctorType !== receiverTypeName) {
490
+ // Verify subclass relationship: per-file parentMap first, then cross-file
491
+ // globalParentMap, then fall back to SymbolTable class verification.
492
+ // The SymbolTable fallback handles cross-file cases where heritage is declared
493
+ // in a file not yet processed (e.g. Dog extends Animal in models/Dog.kt when
494
+ // processing services/App.kt). Since constructorTypeMap only records entries
495
+ // when a type annotation AND constructor are both present (val x: Base = Sub()),
496
+ // confirming both are class-like types is sufficient — the original code would
497
+ // 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'))) {
502
+ receiverTypeName = ctorType;
503
+ }
504
+ }
505
+ }
248
506
  // Fall back to verified constructor bindings for return type inference
249
507
  if (!receiverTypeName && receiverName && receiverIndex.size > 0) {
250
- const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
508
+ const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx, provider);
251
509
  const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
252
510
  receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverName);
253
511
  }
@@ -261,7 +519,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
261
519
  }
262
520
  }
263
521
  // Hoist sourceId so it's available for ACCESSES edge emission during chain walk.
264
- const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
522
+ const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx, provider);
265
523
  const sourceId = enclosingFuncId || generateId('File', file.path);
266
524
  // Fall back to mixed chain resolution when the receiver is a complex expression
267
525
  // (field chain, call chain, or interleaved — e.g. user.address.city.save() or
@@ -290,12 +548,19 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
290
548
  }
291
549
  }
292
550
  }
551
+ // Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++).
552
+ // Only used when multiple candidates survive arity filtering — ~1-3% of calls.
553
+ const langConfig = provider.typeConfig;
554
+ const hints = langConfig?.inferLiteralType
555
+ ? { callNode, inferLiteralType: langConfig.inferLiteralType }
556
+ : undefined;
293
557
  const resolved = resolveCallTarget({
294
558
  calledName,
295
559
  argCount: countCallArguments(callNode),
296
560
  callForm,
297
561
  receiverTypeName,
298
- }, file.path, ctx);
562
+ receiverName,
563
+ }, file.path, ctx, hints, widenCache);
299
564
  if (!resolved)
300
565
  return;
301
566
  const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
@@ -362,7 +627,9 @@ const filterCallableCandidates = (candidates, argCount, callForm) => {
362
627
  const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
363
628
  if (!hasParameterMetadata)
364
629
  return kindFiltered;
365
- return kindFiltered.filter(candidate => candidate.parameterCount === undefined || candidate.parameterCount === argCount);
630
+ return kindFiltered.filter(candidate => candidate.parameterCount === undefined
631
+ || (argCount >= (candidate.requiredParameterCount ?? candidate.parameterCount)
632
+ && argCount <= candidate.parameterCount));
366
633
  };
367
634
  const toResolveResult = (definition, tier) => ({
368
635
  nodeId: definition.nodeId,
@@ -371,19 +638,145 @@ const toResolveResult = (definition, tier) => ({
371
638
  returnType: definition.returnType,
372
639
  });
373
640
  /**
374
- * Resolve a function call to its target node ID using priority strategy:
375
- * A. Narrow candidates by scope tier via ctx.resolve()
376
- * B. Filter to callable symbol kinds (constructor-aware when callForm is set)
377
- * C. Apply arity filtering when parameter metadata is available
378
- * D. Apply receiver-type filtering for member calls with typed receivers
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.
379
645
  *
380
- * If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
646
+ * Only applied to single-word identifiers that look like a JVM primitive alias;
647
+ * multi-word or qualified names are left untouched.
381
648
  */
382
- const resolveCallTarget = (call, currentFile, ctx) => {
649
+ const KOTLIN_BOXED_TO_PRIMITIVE = {
650
+ Int: 'int',
651
+ Long: 'long',
652
+ Short: 'short',
653
+ Byte: 'byte',
654
+ Float: 'float',
655
+ Double: 'double',
656
+ Boolean: 'boolean',
657
+ Char: 'char',
658
+ };
659
+ 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))
667
+ 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 => {
702
+ // Keep candidates without type info — conservative: partially-annotated codebases
703
+ // (e.g. C++ with some missing declarations) may have mixed typed/untyped overloads.
704
+ // If one typed and one untyped both survive, matched.length > 1 → returns null (no edge).
705
+ if (!c.parameterTypes)
706
+ return true;
707
+ return c.parameterTypes.every((pType, i) => {
708
+ if (i >= argTypes.length || !argTypes[i])
709
+ return true;
710
+ // Normalise Kotlin boxed type names (Int→int, Boolean→boolean, etc.) so
711
+ // that the stored declaration type matches the inferred literal type.
712
+ return normalizeJvmTypeName(pType) === argTypes[i];
713
+ });
714
+ });
715
+ if (matched.length === 1)
716
+ return matched[0];
717
+ // Multiple survivors may share the same nodeId (e.g. TypeScript overload signatures +
718
+ // implementation body all collide via generateId). Deduplicate by nodeId — if all
719
+ // matched candidates resolve to the same graph node, disambiguation succeeded.
720
+ if (matched.length > 1) {
721
+ const uniqueIds = new Set(matched.map(c => c.nodeId));
722
+ if (uniqueIds.size === 1)
723
+ return matched[0];
724
+ }
725
+ return null;
726
+ };
727
+ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) => {
383
728
  const tiered = ctx.resolve(call.calledName, currentFile);
384
729
  if (!tiered)
385
730
  return null;
386
- const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
731
+ let filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
732
+ // Swift/Kotlin: constructor calls look like free function calls (no `new` keyword).
733
+ // If free-form filtering found no callable candidates but the symbol resolves to a
734
+ // Class/Struct, retry with constructor form so CONSTRUCTOR_TARGET_TYPES applies.
735
+ if (filteredCandidates.length === 0 && call.callForm === 'free') {
736
+ const hasTypeTarget = tiered.candidates.some(c => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
737
+ if (hasTypeTarget) {
738
+ filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
739
+ }
740
+ }
741
+ // Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
742
+ // The attribute access gives callForm='member', but the callee may be a Class — a valid
743
+ // constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
744
+ // emits a CALLS edge to the class node.
745
+ if (filteredCandidates.length === 0 && call.callForm === 'member') {
746
+ filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
747
+ }
748
+ // Module-alias disambiguation: Python `import auth; auth.User()` — receiverName='auth'
749
+ // selects auth.py via moduleAliasMap. Runs for ALL member calls with a known module alias,
750
+ // not just ambiguous ones — same-file tier may shadow the correct cross-module target when
751
+ // the caller defines a function with the same name as the callee (Issue #417).
752
+ if (call.callForm === 'member' && call.receiverName) {
753
+ const aliasMap = ctx.moduleAliasMap?.get(currentFile);
754
+ if (aliasMap) {
755
+ const moduleFile = aliasMap.get(call.receiverName);
756
+ if (moduleFile) {
757
+ const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
758
+ if (aliasFiltered.length > 0) {
759
+ filteredCandidates = aliasFiltered;
760
+ }
761
+ else {
762
+ // Same-file tier returned a local match, but the alias points elsewhere.
763
+ // Widen to global candidates and filter to the aliased module's file.
764
+ // Use per-file widenCache to avoid repeated lookupFuzzy for the same
765
+ // calledName+moduleFile from multiple call sites in the same file.
766
+ const cacheKey = `${call.calledName}\0${moduleFile}`;
767
+ let fuzzyDefs = widenCache?.get(cacheKey);
768
+ if (!fuzzyDefs) {
769
+ fuzzyDefs = ctx.symbols.lookupFuzzy(call.calledName);
770
+ widenCache?.set(cacheKey, fuzzyDefs);
771
+ }
772
+ const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm)
773
+ .filter(c => c.filePath === moduleFile);
774
+ if (widened.length > 0)
775
+ filteredCandidates = widened;
776
+ }
777
+ }
778
+ }
779
+ }
387
780
  // D. Receiver-type filtering: for member calls with a known receiver type,
388
781
  // resolve the type through the same tiered import infrastructure, then
389
782
  // filter method candidates to the type's defining file. Fall back to
@@ -415,12 +808,39 @@ const resolveCallTarget = (call, currentFile, ctx) => {
415
808
  if (ownerFiltered.length === 1) {
416
809
  return toResolveResult(ownerFiltered[0], tiered.tier);
417
810
  }
811
+ // E. Try overload disambiguation on the narrowed pool
812
+ if ((fileFiltered.length > 1 || ownerFiltered.length > 1) && overloadHints) {
813
+ const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
814
+ const disambiguated = tryOverloadDisambiguation(overloadPool, overloadHints);
815
+ if (disambiguated)
816
+ return toResolveResult(disambiguated, tiered.tier);
817
+ }
418
818
  if (fileFiltered.length > 1 || ownerFiltered.length > 1)
419
819
  return null;
420
820
  }
421
821
  }
422
- if (filteredCandidates.length !== 1)
822
+ // 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);
827
+ if (disambiguated)
828
+ return toResolveResult(disambiguated, tiered.tier);
829
+ }
830
+ if (filteredCandidates.length !== 1) {
831
+ // Deduplicate: Swift extensions create multiple Class nodes with the same name.
832
+ // When all candidates share the same type and differ only by file (extension vs
833
+ // primary definition), they represent the same symbol. Prefer the primary
834
+ // definition (shortest file path: Product.swift over ProductExtension.swift).
835
+ 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')) {
838
+ const sorted = [...filteredCandidates].sort((a, b) => a.filePath.length - b.filePath.length);
839
+ return toResolveResult(sorted[0], tiered.tier);
840
+ }
841
+ }
423
842
  return null;
843
+ }
424
844
  return toResolveResult(filteredCandidates[0], tiered.tier);
425
845
  };
426
846
  // ── Scope key helpers ────────────────────────────────────────────────────
@@ -636,6 +1056,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
636
1056
  await yieldToEventLoop();
637
1057
  }
638
1058
  ctx.enableCache(filePath);
1059
+ const widenCache = new Map();
639
1060
  const receiverMap = fileReceiverTypes.get(filePath);
640
1061
  for (const call of calls) {
641
1062
  let effectiveCall = call;
@@ -677,7 +1098,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
677
1098
  }
678
1099
  }
679
1100
  }
680
- const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
1101
+ const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache);
681
1102
  if (!resolved)
682
1103
  continue;
683
1104
  const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
@@ -791,3 +1212,202 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, on
791
1212
  }
792
1213
  onProgress?.(extractedRoutes.length, extractedRoutes.length);
793
1214
  };
1215
+ /**
1216
+ * Extract property access keys from a consumer file's source code near fetch calls.
1217
+ *
1218
+ * Looks for three patterns after a fetch/response variable assignment:
1219
+ * 1. Destructuring: `const { data, pagination } = await res.json()`
1220
+ * 2. Property access: `response.data`, `result.items`
1221
+ * 3. Optional chaining: `data?.key1?.key2`
1222
+ *
1223
+ * Returns deduplicated top-level property names accessed on the response.
1224
+ *
1225
+ * NOTE: This scans the entire file content, not just code near a specific fetch call.
1226
+ * If a file has multiple fetch calls to different routes, all accessed keys are
1227
+ * attributed to each fetch. This is an acceptable tradeoff for regex-based extraction.
1228
+ */
1229
+ /** Common method names on response/data objects that are NOT property accesses */
1230
+ // Properties/methods to ignore when extracting consumer accessed keys from `data.X` patterns.
1231
+ // Avoids false positives from Fetch API, Array, Object, Promise, and DOM access on variables
1232
+ // that happen to share names with response variables (data, result, response, etc.).
1233
+ const RESPONSE_ACCESS_BLOCKLIST = new Set([
1234
+ // Fetch/Response API
1235
+ 'json', 'text', 'blob', 'arrayBuffer', 'formData', 'ok', 'status', 'headers', 'clone',
1236
+ // Promise
1237
+ 'then', 'catch', 'finally',
1238
+ // Array
1239
+ 'map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every',
1240
+ 'push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'concat', 'join',
1241
+ 'sort', 'reverse', 'includes', 'indexOf',
1242
+ // Object
1243
+ 'length', 'toString', 'valueOf', 'keys', 'values', 'entries',
1244
+ // 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',
1254
+ ]);
1255
+ export const extractConsumerAccessedKeys = (content) => {
1256
+ const keys = new Set();
1257
+ // Pattern 1: Destructuring from .json() — const { key1, key2 } = await res.json()
1258
+ // Also matches: const { key1, key2 } = await (await fetch(...)).json()
1259
+ const destructurePattern = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(?:await\s+)?(?:\w+\.json\s*\(\)|(?:await\s+)?(?:fetch|axios|got)\s*\([^)]*\)(?:\.then\s*\([^)]*\))?(?:\.json\s*\(\))?)/g;
1260
+ let match;
1261
+ while ((match = destructurePattern.exec(content)) !== null) {
1262
+ const destructuredBody = match[1];
1263
+ // Extract identifiers from destructuring, handling renamed bindings (key: alias)
1264
+ const keyPattern = /(\w+)\s*(?::\s*\w+)?/g;
1265
+ let keyMatch;
1266
+ while ((keyMatch = keyPattern.exec(destructuredBody)) !== null) {
1267
+ keys.add(keyMatch[1]);
1268
+ }
1269
+ }
1270
+ // Pattern 2: Destructuring from a data/result/response/json variable
1271
+ // e.g., const { items, total } = data; or const { error } = result;
1272
+ const dataVarDestructure = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(?:data|result|response|json|body|res)\b/g;
1273
+ while ((match = dataVarDestructure.exec(content)) !== null) {
1274
+ const destructuredBody = match[1];
1275
+ const keyPattern = /(\w+)\s*(?::\s*\w+)?/g;
1276
+ let keyMatch;
1277
+ while ((keyMatch = keyPattern.exec(destructuredBody)) !== null) {
1278
+ keys.add(keyMatch[1]);
1279
+ }
1280
+ }
1281
+ // Pattern 3: Property access on common response variable names
1282
+ // Matches: data.key, response.key, result.key, json.key, body.key
1283
+ // Also matches optional chaining: data?.key
1284
+ const propAccessPattern = /\b(?:data|response|result|json|body|res)\s*(?:\?\.|\.)(\w+)/g;
1285
+ while ((match = propAccessPattern.exec(content)) !== null) {
1286
+ const key = match[1];
1287
+ // Skip common method calls that aren't property accesses
1288
+ if (!RESPONSE_ACCESS_BLOCKLIST.has(key)) {
1289
+ keys.add(key);
1290
+ }
1291
+ }
1292
+ return [...keys];
1293
+ };
1294
+ /**
1295
+ * Create FETCHES edges from extracted fetch() calls to matching Route nodes.
1296
+ * When consumerContents is provided, extracts property access patterns from
1297
+ * consumer files and encodes them in the edge reason field.
1298
+ */
1299
+ export const processNextjsFetchRoutes = (graph, fetchCalls, routeRegistry, // routeURL → handlerFilePath
1300
+ consumerContents) => {
1301
+ // Pre-count how many routes each consumer file matches (for confidence attribution)
1302
+ const routeCountByFile = new Map();
1303
+ for (const call of fetchCalls) {
1304
+ const normalized = normalizeFetchURL(call.fetchURL);
1305
+ if (!normalized)
1306
+ continue;
1307
+ for (const [routeURL] of routeRegistry) {
1308
+ if (routeMatches(normalized, routeURL)) {
1309
+ routeCountByFile.set(call.filePath, (routeCountByFile.get(call.filePath) ?? 0) + 1);
1310
+ break;
1311
+ }
1312
+ }
1313
+ }
1314
+ for (const call of fetchCalls) {
1315
+ const normalized = normalizeFetchURL(call.fetchURL);
1316
+ if (!normalized)
1317
+ continue;
1318
+ for (const [routeURL] of routeRegistry) {
1319
+ if (routeMatches(normalized, routeURL)) {
1320
+ const sourceId = generateId('File', call.filePath);
1321
+ const routeNodeId = generateId('Route', routeURL);
1322
+ // Extract consumer accessed keys if file content is available
1323
+ let reason = 'fetch-url-match';
1324
+ if (consumerContents) {
1325
+ const content = consumerContents.get(call.filePath);
1326
+ if (content) {
1327
+ const accessedKeys = extractConsumerAccessedKeys(content);
1328
+ if (accessedKeys.length > 0) {
1329
+ reason = `fetch-url-match|keys:${accessedKeys.join(',')}`;
1330
+ }
1331
+ }
1332
+ }
1333
+ // Encode multi-fetch count so downstream can set confidence
1334
+ const fetchCount = routeCountByFile.get(call.filePath) ?? 1;
1335
+ if (fetchCount > 1) {
1336
+ reason = `${reason}|fetches:${fetchCount}`;
1337
+ }
1338
+ graph.addRelationship({
1339
+ id: generateId('FETCHES', `${sourceId}->${routeNodeId}`),
1340
+ sourceId,
1341
+ targetId: routeNodeId,
1342
+ type: 'FETCHES',
1343
+ confidence: 0.9,
1344
+ reason,
1345
+ });
1346
+ break;
1347
+ }
1348
+ }
1349
+ }
1350
+ };
1351
+ /**
1352
+ * Extract fetch() calls from source files (sequential path).
1353
+ * Workers handle this via tree-sitter captures in parse-worker; this function
1354
+ * provides the same extraction for the sequential fallback path.
1355
+ */
1356
+ export const extractFetchCallsFromFiles = async (files, astCache) => {
1357
+ const parser = await loadParser();
1358
+ const result = [];
1359
+ for (const file of files) {
1360
+ const language = getLanguageFromFilename(file.path);
1361
+ if (!language)
1362
+ continue;
1363
+ if (!isLanguageAvailable(language))
1364
+ continue;
1365
+ const provider = getProvider(language);
1366
+ const queryStr = provider.treeSitterQueries;
1367
+ if (!queryStr)
1368
+ continue;
1369
+ await loadLanguage(language, file.path);
1370
+ let tree = astCache.get(file.path);
1371
+ if (!tree) {
1372
+ try {
1373
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
1374
+ }
1375
+ catch {
1376
+ continue;
1377
+ }
1378
+ astCache.set(file.path, tree);
1379
+ }
1380
+ let matches;
1381
+ try {
1382
+ const lang = parser.getLanguage();
1383
+ const query = new Parser.Query(lang, queryStr);
1384
+ matches = query.matches(tree.rootNode);
1385
+ }
1386
+ catch {
1387
+ continue;
1388
+ }
1389
+ for (const match of matches) {
1390
+ const captureMap = {};
1391
+ match.captures.forEach(c => captureMap[c.name] = c.node);
1392
+ if (captureMap['route.fetch']) {
1393
+ const urlNode = captureMap['route.url'] ?? captureMap['route.template_url'];
1394
+ if (urlNode) {
1395
+ result.push({
1396
+ filePath: file.path,
1397
+ fetchURL: urlNode.text,
1398
+ lineNumber: captureMap['route.fetch'].startPosition.row,
1399
+ });
1400
+ }
1401
+ }
1402
+ else if (captureMap['http_client'] && captureMap['http_client.url']) {
1403
+ const method = captureMap['http_client.method']?.text;
1404
+ const url = captureMap['http_client.url'].text;
1405
+ const HTTP_CLIENT_ONLY = new Set(['head', 'options', 'request', 'ajax']);
1406
+ if (method && HTTP_CLIENT_ONLY.has(method) && url.startsWith('/')) {
1407
+ result.push({ filePath: file.path, fetchURL: url, lineNumber: captureMap['http_client'].startPosition.row });
1408
+ }
1409
+ }
1410
+ }
1411
+ }
1412
+ return result;
1413
+ };