@zuvia-software-solutions/code-mapper 1.4.0

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 (213) hide show
  1. package/README.md +215 -0
  2. package/dist/cli/ai-context.d.ts +19 -0
  3. package/dist/cli/ai-context.js +168 -0
  4. package/dist/cli/analyze.d.ts +7 -0
  5. package/dist/cli/analyze.js +325 -0
  6. package/dist/cli/augment.d.ts +7 -0
  7. package/dist/cli/augment.js +27 -0
  8. package/dist/cli/clean.d.ts +5 -0
  9. package/dist/cli/clean.js +56 -0
  10. package/dist/cli/eval-server.d.ts +25 -0
  11. package/dist/cli/eval-server.js +365 -0
  12. package/dist/cli/index.d.ts +6 -0
  13. package/dist/cli/index.js +102 -0
  14. package/dist/cli/lazy-action.d.ts +6 -0
  15. package/dist/cli/lazy-action.js +19 -0
  16. package/dist/cli/list.d.ts +2 -0
  17. package/dist/cli/list.js +27 -0
  18. package/dist/cli/mcp.d.ts +8 -0
  19. package/dist/cli/mcp.js +35 -0
  20. package/dist/cli/refresh.d.ts +12 -0
  21. package/dist/cli/refresh.js +165 -0
  22. package/dist/cli/serve.d.ts +5 -0
  23. package/dist/cli/serve.js +8 -0
  24. package/dist/cli/setup.d.ts +6 -0
  25. package/dist/cli/setup.js +218 -0
  26. package/dist/cli/status.d.ts +2 -0
  27. package/dist/cli/status.js +33 -0
  28. package/dist/cli/tool.d.ts +28 -0
  29. package/dist/cli/tool.js +87 -0
  30. package/dist/config/ignore-service.d.ts +32 -0
  31. package/dist/config/ignore-service.js +282 -0
  32. package/dist/config/supported-languages.d.ts +23 -0
  33. package/dist/config/supported-languages.js +52 -0
  34. package/dist/core/augmentation/engine.d.ts +22 -0
  35. package/dist/core/augmentation/engine.js +232 -0
  36. package/dist/core/embeddings/embedder.d.ts +35 -0
  37. package/dist/core/embeddings/embedder.js +171 -0
  38. package/dist/core/embeddings/embedding-pipeline.d.ts +41 -0
  39. package/dist/core/embeddings/embedding-pipeline.js +402 -0
  40. package/dist/core/embeddings/index.d.ts +5 -0
  41. package/dist/core/embeddings/index.js +6 -0
  42. package/dist/core/embeddings/text-generator.d.ts +20 -0
  43. package/dist/core/embeddings/text-generator.js +159 -0
  44. package/dist/core/embeddings/types.d.ts +60 -0
  45. package/dist/core/embeddings/types.js +23 -0
  46. package/dist/core/graph/graph.d.ts +4 -0
  47. package/dist/core/graph/graph.js +65 -0
  48. package/dist/core/graph/types.d.ts +69 -0
  49. package/dist/core/graph/types.js +3 -0
  50. package/dist/core/incremental/child-process.d.ts +8 -0
  51. package/dist/core/incremental/child-process.js +649 -0
  52. package/dist/core/incremental/refresh-coordinator.d.ts +32 -0
  53. package/dist/core/incremental/refresh-coordinator.js +147 -0
  54. package/dist/core/incremental/types.d.ts +78 -0
  55. package/dist/core/incremental/types.js +153 -0
  56. package/dist/core/incremental/watcher.d.ts +63 -0
  57. package/dist/core/incremental/watcher.js +338 -0
  58. package/dist/core/ingestion/ast-cache.d.ts +12 -0
  59. package/dist/core/ingestion/ast-cache.js +34 -0
  60. package/dist/core/ingestion/call-processor.d.ts +34 -0
  61. package/dist/core/ingestion/call-processor.js +937 -0
  62. package/dist/core/ingestion/call-routing.d.ts +40 -0
  63. package/dist/core/ingestion/call-routing.js +97 -0
  64. package/dist/core/ingestion/cluster-enricher.d.ts +30 -0
  65. package/dist/core/ingestion/cluster-enricher.js +151 -0
  66. package/dist/core/ingestion/community-processor.d.ts +26 -0
  67. package/dist/core/ingestion/community-processor.js +272 -0
  68. package/dist/core/ingestion/constants.d.ts +5 -0
  69. package/dist/core/ingestion/constants.js +8 -0
  70. package/dist/core/ingestion/entry-point-scoring.d.ts +23 -0
  71. package/dist/core/ingestion/entry-point-scoring.js +317 -0
  72. package/dist/core/ingestion/export-detection.d.ts +11 -0
  73. package/dist/core/ingestion/export-detection.js +203 -0
  74. package/dist/core/ingestion/filesystem-walker.d.ts +18 -0
  75. package/dist/core/ingestion/filesystem-walker.js +64 -0
  76. package/dist/core/ingestion/framework-detection.d.ts +42 -0
  77. package/dist/core/ingestion/framework-detection.js +405 -0
  78. package/dist/core/ingestion/heritage-processor.d.ts +15 -0
  79. package/dist/core/ingestion/heritage-processor.js +237 -0
  80. package/dist/core/ingestion/import-processor.d.ts +31 -0
  81. package/dist/core/ingestion/import-processor.js +416 -0
  82. package/dist/core/ingestion/language-config.d.ts +32 -0
  83. package/dist/core/ingestion/language-config.js +161 -0
  84. package/dist/core/ingestion/mro-processor.d.ts +32 -0
  85. package/dist/core/ingestion/mro-processor.js +343 -0
  86. package/dist/core/ingestion/named-binding-extraction.d.ts +51 -0
  87. package/dist/core/ingestion/named-binding-extraction.js +343 -0
  88. package/dist/core/ingestion/parsing-processor.d.ts +20 -0
  89. package/dist/core/ingestion/parsing-processor.js +282 -0
  90. package/dist/core/ingestion/pipeline.d.ts +3 -0
  91. package/dist/core/ingestion/pipeline.js +416 -0
  92. package/dist/core/ingestion/process-processor.d.ts +42 -0
  93. package/dist/core/ingestion/process-processor.js +357 -0
  94. package/dist/core/ingestion/resolution-context.d.ts +40 -0
  95. package/dist/core/ingestion/resolution-context.js +171 -0
  96. package/dist/core/ingestion/resolvers/csharp.d.ts +10 -0
  97. package/dist/core/ingestion/resolvers/csharp.js +101 -0
  98. package/dist/core/ingestion/resolvers/go.d.ts +8 -0
  99. package/dist/core/ingestion/resolvers/go.js +33 -0
  100. package/dist/core/ingestion/resolvers/index.d.ts +14 -0
  101. package/dist/core/ingestion/resolvers/index.js +10 -0
  102. package/dist/core/ingestion/resolvers/jvm.d.ts +9 -0
  103. package/dist/core/ingestion/resolvers/jvm.js +74 -0
  104. package/dist/core/ingestion/resolvers/php.d.ts +7 -0
  105. package/dist/core/ingestion/resolvers/php.js +30 -0
  106. package/dist/core/ingestion/resolvers/ruby.d.ts +9 -0
  107. package/dist/core/ingestion/resolvers/ruby.js +13 -0
  108. package/dist/core/ingestion/resolvers/rust.d.ts +5 -0
  109. package/dist/core/ingestion/resolvers/rust.js +62 -0
  110. package/dist/core/ingestion/resolvers/standard.d.ts +16 -0
  111. package/dist/core/ingestion/resolvers/standard.js +144 -0
  112. package/dist/core/ingestion/resolvers/utils.d.ts +18 -0
  113. package/dist/core/ingestion/resolvers/utils.js +113 -0
  114. package/dist/core/ingestion/structure-processor.d.ts +4 -0
  115. package/dist/core/ingestion/structure-processor.js +39 -0
  116. package/dist/core/ingestion/symbol-table.d.ts +34 -0
  117. package/dist/core/ingestion/symbol-table.js +48 -0
  118. package/dist/core/ingestion/tree-sitter-queries.d.ts +20 -0
  119. package/dist/core/ingestion/tree-sitter-queries.js +691 -0
  120. package/dist/core/ingestion/type-env.d.ts +52 -0
  121. package/dist/core/ingestion/type-env.js +349 -0
  122. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +4 -0
  123. package/dist/core/ingestion/type-extractors/c-cpp.js +214 -0
  124. package/dist/core/ingestion/type-extractors/csharp.d.ts +4 -0
  125. package/dist/core/ingestion/type-extractors/csharp.js +224 -0
  126. package/dist/core/ingestion/type-extractors/go.d.ts +4 -0
  127. package/dist/core/ingestion/type-extractors/go.js +261 -0
  128. package/dist/core/ingestion/type-extractors/index.d.ts +20 -0
  129. package/dist/core/ingestion/type-extractors/index.js +30 -0
  130. package/dist/core/ingestion/type-extractors/jvm.d.ts +5 -0
  131. package/dist/core/ingestion/type-extractors/jvm.js +386 -0
  132. package/dist/core/ingestion/type-extractors/php.d.ts +4 -0
  133. package/dist/core/ingestion/type-extractors/php.js +280 -0
  134. package/dist/core/ingestion/type-extractors/python.d.ts +4 -0
  135. package/dist/core/ingestion/type-extractors/python.js +175 -0
  136. package/dist/core/ingestion/type-extractors/ruby.d.ts +12 -0
  137. package/dist/core/ingestion/type-extractors/ruby.js +218 -0
  138. package/dist/core/ingestion/type-extractors/rust.d.ts +4 -0
  139. package/dist/core/ingestion/type-extractors/rust.js +290 -0
  140. package/dist/core/ingestion/type-extractors/shared.d.ts +81 -0
  141. package/dist/core/ingestion/type-extractors/shared.js +322 -0
  142. package/dist/core/ingestion/type-extractors/swift.d.ts +4 -0
  143. package/dist/core/ingestion/type-extractors/swift.js +140 -0
  144. package/dist/core/ingestion/type-extractors/types.d.ts +111 -0
  145. package/dist/core/ingestion/type-extractors/types.js +4 -0
  146. package/dist/core/ingestion/type-extractors/typescript.d.ts +4 -0
  147. package/dist/core/ingestion/type-extractors/typescript.js +227 -0
  148. package/dist/core/ingestion/utils.d.ts +73 -0
  149. package/dist/core/ingestion/utils.js +992 -0
  150. package/dist/core/ingestion/workers/parse-worker.d.ts +99 -0
  151. package/dist/core/ingestion/workers/parse-worker.js +1055 -0
  152. package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
  153. package/dist/core/ingestion/workers/worker-pool.js +123 -0
  154. package/dist/core/lbug/csv-generator.d.ts +28 -0
  155. package/dist/core/lbug/csv-generator.js +355 -0
  156. package/dist/core/lbug/lbug-adapter.d.ts +96 -0
  157. package/dist/core/lbug/lbug-adapter.js +753 -0
  158. package/dist/core/lbug/schema.d.ts +46 -0
  159. package/dist/core/lbug/schema.js +402 -0
  160. package/dist/core/search/bm25-index.d.ts +20 -0
  161. package/dist/core/search/bm25-index.js +123 -0
  162. package/dist/core/search/hybrid-search.d.ts +32 -0
  163. package/dist/core/search/hybrid-search.js +131 -0
  164. package/dist/core/search/query-cache.d.ts +18 -0
  165. package/dist/core/search/query-cache.js +47 -0
  166. package/dist/core/search/query-expansion.d.ts +19 -0
  167. package/dist/core/search/query-expansion.js +75 -0
  168. package/dist/core/search/reranker.d.ts +29 -0
  169. package/dist/core/search/reranker.js +122 -0
  170. package/dist/core/search/types.d.ts +154 -0
  171. package/dist/core/search/types.js +51 -0
  172. package/dist/core/semantic/tsgo-service.d.ts +67 -0
  173. package/dist/core/semantic/tsgo-service.js +355 -0
  174. package/dist/core/tree-sitter/parser-loader.d.ts +12 -0
  175. package/dist/core/tree-sitter/parser-loader.js +71 -0
  176. package/dist/lib/memory-guard.d.ts +35 -0
  177. package/dist/lib/memory-guard.js +70 -0
  178. package/dist/lib/utils.d.ts +3 -0
  179. package/dist/lib/utils.js +6 -0
  180. package/dist/mcp/compatible-stdio-transport.d.ts +32 -0
  181. package/dist/mcp/compatible-stdio-transport.js +209 -0
  182. package/dist/mcp/core/embedder.d.ts +24 -0
  183. package/dist/mcp/core/embedder.js +168 -0
  184. package/dist/mcp/core/lbug-adapter.d.ts +29 -0
  185. package/dist/mcp/core/lbug-adapter.js +330 -0
  186. package/dist/mcp/local/local-backend.d.ts +188 -0
  187. package/dist/mcp/local/local-backend.js +2759 -0
  188. package/dist/mcp/resources.d.ts +22 -0
  189. package/dist/mcp/resources.js +379 -0
  190. package/dist/mcp/server.d.ts +10 -0
  191. package/dist/mcp/server.js +217 -0
  192. package/dist/mcp/staleness.d.ts +10 -0
  193. package/dist/mcp/staleness.js +25 -0
  194. package/dist/mcp/tools.d.ts +21 -0
  195. package/dist/mcp/tools.js +202 -0
  196. package/dist/server/api.d.ts +5 -0
  197. package/dist/server/api.js +340 -0
  198. package/dist/server/mcp-http.d.ts +7 -0
  199. package/dist/server/mcp-http.js +95 -0
  200. package/dist/storage/git.d.ts +6 -0
  201. package/dist/storage/git.js +35 -0
  202. package/dist/storage/repo-manager.d.ts +87 -0
  203. package/dist/storage/repo-manager.js +249 -0
  204. package/dist/types/pipeline.d.ts +35 -0
  205. package/dist/types/pipeline.js +20 -0
  206. package/hooks/claude/code-mapper-hook.cjs +238 -0
  207. package/hooks/claude/pre-tool-use.sh +79 -0
  208. package/hooks/claude/session-start.sh +42 -0
  209. package/models/mlx-embedder.py +185 -0
  210. package/package.json +100 -0
  211. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  212. package/vendor/leiden/index.cjs +355 -0
  213. package/vendor/leiden/utils.cjs +392 -0
@@ -0,0 +1,937 @@
1
+ // code-mapper/src/core/ingestion/call-processor.ts
2
+ /** @file call-processor.ts @description Resolves function/method call sites to target symbol nodes, supporting sequential AST-based and pre-extracted worker-based paths */
3
+ import Parser from 'tree-sitter';
4
+ import { TIER_CONFIDENCE } from './resolution-context.js';
5
+ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
6
+ import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
7
+ import { generateId } from '../../lib/utils.js';
8
+ import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
9
+ import { buildTypeEnv } from './type-env.js';
10
+ import { getTreeSitterBufferSize } from './constants.js';
11
+ import { callRouters } from './call-routing.js';
12
+ /** Walk up the AST to find the enclosing function/method, or null for top-level code */
13
+ const findEnclosingFunction = (node, filePath, ctx) => {
14
+ let current = node.parent;
15
+ while (current) {
16
+ if (FUNCTION_NODE_TYPES.has(current.type)) {
17
+ const { funcName, label } = extractFunctionName(current);
18
+ if (funcName) {
19
+ const resolved = ctx.resolve(funcName, filePath);
20
+ if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
21
+ return resolved.candidates[0].nodeId;
22
+ }
23
+ return generateId(label, `${filePath}:${funcName}`);
24
+ }
25
+ }
26
+ current = current.parent;
27
+ }
28
+ return null;
29
+ };
30
+ /** Verify constructor bindings against SymbolTable and infer receiver types */
31
+ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
32
+ const verified = new Map();
33
+ for (const { scope, varName, calleeName, receiverClassName } of bindings) {
34
+ const tiered = ctx.resolve(calleeName, filePath);
35
+ const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
36
+ if (isClass) {
37
+ verified.set(receiverKey(scope, varName), calleeName);
38
+ }
39
+ else {
40
+ let callableDefs = tiered?.candidates.filter(d => d.type === 'Function' || d.type === 'Method');
41
+ // When receiver class is known (e.g. $this->method() in PHP), narrow
42
+ // candidates to methods owned by that class
43
+ if (callableDefs && callableDefs.length > 1 && receiverClassName) {
44
+ if (graph) {
45
+ // Worker path: use graph.getNode
46
+ const narrowed = callableDefs.filter(d => {
47
+ if (!d.ownerId)
48
+ return false;
49
+ const owner = graph.getNode(d.ownerId);
50
+ return owner?.properties.name === receiverClassName;
51
+ });
52
+ if (narrowed.length > 0)
53
+ callableDefs = narrowed;
54
+ }
55
+ else {
56
+ // Sequential path: use ctx.resolve
57
+ const classResolved = ctx.resolve(receiverClassName, filePath);
58
+ if (classResolved && classResolved.candidates.length > 0) {
59
+ const classNodeIds = new Set(classResolved.candidates.map(c => c.nodeId));
60
+ const narrowed = callableDefs.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
61
+ if (narrowed.length > 0)
62
+ callableDefs = narrowed;
63
+ }
64
+ }
65
+ }
66
+ if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) {
67
+ const typeName = extractReturnTypeName(callableDefs[0].returnType);
68
+ if (typeName) {
69
+ verified.set(receiverKey(scope, varName), typeName);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return verified;
75
+ };
76
+ /** Check if a node is inside a catch clause */
77
+ const isInsideCatch = (node) => {
78
+ let current = node.parent;
79
+ while (current) {
80
+ if (current.type === 'catch_clause')
81
+ return true;
82
+ // Stop at function boundary
83
+ if (FUNCTION_NODE_TYPES.has(current.type))
84
+ return false;
85
+ current = current.parent;
86
+ }
87
+ return false;
88
+ };
89
+ /** Check if a node is inside an if-statement body (conditional call) */
90
+ const isInsideConditional = (node) => {
91
+ let current = node.parent;
92
+ while (current) {
93
+ if (current.type === 'if_statement' || current.type === 'conditional_expression' || current.type === 'ternary_expression')
94
+ return true;
95
+ if (FUNCTION_NODE_TYPES.has(current.type))
96
+ return false;
97
+ current = current.parent;
98
+ }
99
+ return false;
100
+ };
101
+ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
102
+ const parser = await loadParser();
103
+ const collectedHeritage = [];
104
+ const logSkipped = isVerboseIngestionEnabled();
105
+ const skippedByLang = logSkipped ? new Map() : null;
106
+ for (let i = 0; i < files.length; i++) {
107
+ const file = files[i];
108
+ onProgress?.(i + 1, files.length);
109
+ if (i % 20 === 0)
110
+ await yieldToEventLoop();
111
+ const language = getLanguageFromFilename(file.path);
112
+ if (!language)
113
+ continue;
114
+ if (!isLanguageAvailable(language)) {
115
+ if (skippedByLang) {
116
+ skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
117
+ }
118
+ continue;
119
+ }
120
+ const queryStr = LANGUAGE_QUERIES[language];
121
+ if (!queryStr)
122
+ continue;
123
+ await loadLanguage(language, file.path);
124
+ let tree = astCache.get(file.path);
125
+ if (!tree) {
126
+ try {
127
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
128
+ }
129
+ catch (parseError) {
130
+ continue;
131
+ }
132
+ astCache.set(file.path, tree);
133
+ }
134
+ let query;
135
+ let matches;
136
+ try {
137
+ const language = parser.getLanguage();
138
+ query = new Parser.Query(language, queryStr);
139
+ matches = query.matches(tree.rootNode);
140
+ }
141
+ catch (queryError) {
142
+ console.warn(`Query error for ${file.path}:`, queryError);
143
+ continue;
144
+ }
145
+ const lang = getLanguageFromFilename(file.path);
146
+ const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
147
+ const callRouter = callRouters[language];
148
+ const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
149
+ ? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
150
+ : new Map();
151
+ ctx.enableCache(file.path);
152
+ matches.forEach(match => {
153
+ const captureMap = {};
154
+ match.captures.forEach(c => captureMap[c.name] = c.node);
155
+ if (!captureMap['call'])
156
+ return;
157
+ const nameNode = captureMap['call.name'];
158
+ if (!nameNode)
159
+ return;
160
+ const calledName = nameNode.text;
161
+ const routed = callRouter(calledName, captureMap['call']);
162
+ if (routed) {
163
+ switch (routed.kind) {
164
+ case 'skip':
165
+ case 'import':
166
+ return;
167
+ case 'heritage':
168
+ for (const item of routed.items) {
169
+ collectedHeritage.push({
170
+ filePath: file.path,
171
+ className: item.enclosingClass,
172
+ parentName: item.mixinName,
173
+ kind: item.heritageKind,
174
+ });
175
+ }
176
+ return;
177
+ case 'properties': {
178
+ const fileId = generateId('File', file.path);
179
+ const propEnclosingClassId = findEnclosingClassId(captureMap['call'], file.path);
180
+ for (const item of routed.items) {
181
+ const nodeId = generateId('Property', `${file.path}:${item.propName}`);
182
+ graph.addNode({
183
+ id: nodeId,
184
+ label: 'Property', // TODO: add 'Property' to graph node label union
185
+ properties: {
186
+ name: item.propName, filePath: file.path,
187
+ startLine: item.startLine, endLine: item.endLine,
188
+ language, isExported: true,
189
+ description: item.accessorType,
190
+ },
191
+ });
192
+ ctx.symbols.add(file.path, item.propName, nodeId, 'Property', propEnclosingClassId ? { ownerId: propEnclosingClassId } : undefined);
193
+ const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
194
+ graph.addRelationship({
195
+ id: relId, sourceId: fileId, targetId: nodeId,
196
+ type: 'DEFINES', confidence: 1.0, reason: '',
197
+ });
198
+ if (propEnclosingClassId) {
199
+ graph.addRelationship({
200
+ id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
201
+ sourceId: propEnclosingClassId, targetId: nodeId,
202
+ type: 'HAS_METHOD', confidence: 1.0, reason: '',
203
+ });
204
+ }
205
+ }
206
+ return;
207
+ }
208
+ case 'call':
209
+ break;
210
+ }
211
+ }
212
+ // B3: Handle dynamic imports — `const { foo } = await import('./module.js')`
213
+ // Extract destructured names and create CALLS edges to the resolved module's exports
214
+ if (calledName === 'import') {
215
+ const callNode = captureMap['call'];
216
+ // Get the import path from the argument
217
+ let importPath = null;
218
+ for (const arg of (callNode?.children || [])) {
219
+ if (arg.type === 'arguments') {
220
+ const strArg = arg.children?.find((c) => c.type === 'string' || c.type === 'template_string');
221
+ if (strArg) {
222
+ importPath = strArg.text?.replace(/['"``]/g, '') || null;
223
+ }
224
+ }
225
+ }
226
+ if (importPath) {
227
+ // Find the variable_declarator parent to get destructured names
228
+ let parent = callNode.parent;
229
+ while (parent && parent.type !== 'variable_declarator' && parent.type !== 'lexical_declaration') {
230
+ parent = parent.parent;
231
+ }
232
+ if (parent?.type === 'variable_declarator' || parent?.type === 'lexical_declaration') {
233
+ const declarator = parent.type === 'variable_declarator' ? parent : parent.children?.find((c) => c.type === 'variable_declarator');
234
+ const pattern = declarator?.childForFieldName?.('name') || declarator?.children?.find((c) => c.type === 'object_pattern');
235
+ if (pattern?.type === 'object_pattern') {
236
+ // Extract destructured names: { rerank, mergeWithRRF }
237
+ for (const child of (pattern.children || [])) {
238
+ if (child.type === 'shorthand_property_identifier_pattern' || child.type === 'shorthand_property_identifier') {
239
+ const importedName = child.text;
240
+ if (importedName && !isBuiltInOrNoise(importedName)) {
241
+ const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
242
+ const sourceId = enclosingFuncId || generateId('File', file.path);
243
+ // Try module-scoped resolution first: extract the target filename
244
+ // from the import path and look up the name in that file
245
+ let targetId = null;
246
+ let confidence = 0.5;
247
+ let reason = 'global';
248
+ // Extract filename hint from import path (e.g. '../../core/search/hybrid-search.js' → 'hybrid-search')
249
+ const pathBase = importPath.split('/').pop()?.replace(/\.\w+$/, '');
250
+ if (pathBase) {
251
+ // Try exact lookup in files whose path contains the base name
252
+ const candidates = ctx.symbols.lookupFuzzy(importedName);
253
+ const moduleMatch = candidates.find(c => c.filePath.includes(pathBase) && (c.type === 'Function' || c.type === 'Class' || c.type === 'Interface' || c.type === 'Method'));
254
+ if (moduleMatch) {
255
+ targetId = moduleMatch.nodeId;
256
+ confidence = 0.9;
257
+ reason = 'dynamic-import-resolved';
258
+ }
259
+ }
260
+ // Fall back to general resolution
261
+ if (!targetId) {
262
+ const resolved = ctx.resolve(importedName, file.path);
263
+ if (resolved && resolved.candidates.length > 0) {
264
+ targetId = resolved.candidates[0].nodeId;
265
+ confidence = Math.max(TIER_CONFIDENCE[resolved.tier] ?? 0.5, 0.8);
266
+ reason = `dynamic-import:${resolved.tier}`;
267
+ }
268
+ }
269
+ if (targetId) {
270
+ graph.addRelationship({
271
+ id: generateId('CALLS', `${sourceId}:dyn_import:${importedName}->${targetId}`),
272
+ sourceId,
273
+ targetId,
274
+ type: 'CALLS',
275
+ confidence,
276
+ reason,
277
+ });
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+ return; // don't process 'import' as a regular call
286
+ }
287
+ if (isBuiltInOrNoise(calledName))
288
+ return;
289
+ const callNode = captureMap['call'];
290
+ const callForm = inferCallForm(callNode, nameNode);
291
+ const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
292
+ let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
293
+ // Fall back to verified constructor bindings
294
+ if (!receiverTypeName && receiverName && verifiedReceivers.size > 0) {
295
+ const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
296
+ const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
297
+ receiverTypeName = lookupReceiverType(verifiedReceivers, funcName, receiverName);
298
+ }
299
+ // Fall back to class-as-receiver for static method calls (e.g. UserService.find_user())
300
+ // when receiver name resolves to a Class/Struct/Interface via tiered resolution
301
+ if (!receiverTypeName && receiverName && callForm === 'member') {
302
+ const typeResolved = ctx.resolve(receiverName, file.path);
303
+ if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
304
+ receiverTypeName = receiverName;
305
+ }
306
+ }
307
+ // Fall back to chained call resolution when receiver is a call expression
308
+ // (e.g. svc.getUser().save() -- receiver of save() is getUser())
309
+ if (callForm === 'member' && !receiverTypeName && !receiverName) {
310
+ const receiverNode = extractReceiverNode(nameNode);
311
+ if (receiverNode && CALL_EXPRESSION_TYPES.has(receiverNode.type)) {
312
+ const extracted = extractCallChain(receiverNode);
313
+ if (extracted) {
314
+ // Resolve the base receiver type
315
+ let baseType = extracted.baseReceiverName && typeEnv
316
+ ? typeEnv.lookup(extracted.baseReceiverName, callNode)
317
+ : undefined;
318
+ if (!baseType && extracted.baseReceiverName && verifiedReceivers.size > 0) {
319
+ const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
320
+ const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
321
+ baseType = lookupReceiverType(verifiedReceivers, funcName, extracted.baseReceiverName);
322
+ }
323
+ // Class-as-receiver for chain base
324
+ if (!baseType && extracted.baseReceiverName) {
325
+ const cr = ctx.resolve(extracted.baseReceiverName, file.path);
326
+ if (cr?.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
327
+ baseType = extracted.baseReceiverName;
328
+ }
329
+ }
330
+ receiverTypeName = resolveChainedReceiver(extracted.chain, baseType, file.path, ctx);
331
+ }
332
+ }
333
+ }
334
+ // Fix 2: Skip generic method names (has, get, set, etc.) when called on unknown receiver
335
+ // These almost always resolve to wrong targets (Map.has -> type-env.has) with low confidence
336
+ const GENERIC_MEMBER_METHODS = new Set(['has', 'get', 'set', 'add', 'remove', 'delete', 'close', 'stop', 'clear', 'reset', 'toString', 'valueOf', 'push', 'pop', 'entries', 'keys', 'values']);
337
+ if (callForm === 'member' && !receiverTypeName && GENERIC_MEMBER_METHODS.has(calledName))
338
+ return;
339
+ const resolved = resolveCallTarget({
340
+ calledName,
341
+ argCount: countCallArguments(callNode),
342
+ callForm,
343
+ receiverTypeName,
344
+ }, file.path, ctx);
345
+ if (!resolved)
346
+ return;
347
+ const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
348
+ const sourceId = enclosingFuncId || generateId('File', file.path);
349
+ const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
350
+ const callContext = isInsideCatch(callNode) ? 'error-handler' : isInsideConditional(callNode) ? 'conditional' : '';
351
+ graph.addRelationship({
352
+ id: relId,
353
+ sourceId,
354
+ targetId: resolved.nodeId,
355
+ type: 'CALLS',
356
+ confidence: resolved.confidence,
357
+ reason: callContext || resolved.reason,
358
+ callLine: callNode.startPosition?.row != null ? callNode.startPosition.row + 1 : undefined,
359
+ });
360
+ });
361
+ // Fix 9: Scan for dynamic imports that tree-sitter queries can't capture
362
+ // Pattern: const { foo, bar } = await import('./module.js')
363
+ // The tree-sitter query `(call_expression function: (import) @call.name) @call`
364
+ // doesn't reliably capture import() because (import) has no text as an identifier.
365
+ // This post-processing walks the AST directly to find these patterns.
366
+ try {
367
+ const visitNode = (node) => {
368
+ if (node.type === 'call_expression') {
369
+ const funcChild = node.childForFieldName?.('function');
370
+ if (funcChild?.type === 'import') {
371
+ // Found a dynamic import
372
+ const args = node.childForFieldName?.('arguments');
373
+ const strArg = args?.children?.find((c) => c.type === 'string' || c.type === 'template_string');
374
+ const importPath = strArg?.text?.replace(/['"``]/g, '') || null;
375
+ if (importPath) {
376
+ // Walk up to find variable_declarator with object_pattern
377
+ let parent = node.parent;
378
+ // Skip await_expression wrapper
379
+ if (parent?.type === 'await_expression')
380
+ parent = parent.parent;
381
+ // Should be in a variable_declarator
382
+ if (parent?.type === 'variable_declarator') {
383
+ const pattern = parent.childForFieldName?.('name');
384
+ if (pattern?.type === 'object_pattern') {
385
+ for (const child of (pattern.children || [])) {
386
+ if (child.type === 'shorthand_property_identifier_pattern' || child.type === 'shorthand_property_identifier') {
387
+ const importedName = child.text;
388
+ if (importedName && !isBuiltInOrNoise(importedName)) {
389
+ const enclosingFuncId = findEnclosingFunction(node, file.path, ctx);
390
+ const sourceId = enclosingFuncId || generateId('File', file.path);
391
+ const resolved = ctx.resolve(importedName, file.path);
392
+ if (resolved && resolved.candidates.length > 0) {
393
+ graph.addRelationship({
394
+ id: generateId('CALLS', `${sourceId}:dyn:${importedName}->${resolved.candidates[0].nodeId}`),
395
+ sourceId,
396
+ targetId: resolved.candidates[0].nodeId,
397
+ type: 'CALLS',
398
+ confidence: 0.85,
399
+ reason: 'dynamic-import',
400
+ });
401
+ }
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+ // Recurse into children
411
+ for (let i = 0; i < node.childCount; i++) {
412
+ visitNode(node.child(i));
413
+ }
414
+ };
415
+ visitNode(tree.rootNode);
416
+ }
417
+ catch (e) {
418
+ // Non-fatal — dynamic import scanning is best-effort
419
+ }
420
+ ctx.clearCache();
421
+ }
422
+ if (skippedByLang && skippedByLang.size > 0) {
423
+ for (const [lang, count] of skippedByLang.entries()) {
424
+ console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in call processing — ${lang} parser not available.`);
425
+ }
426
+ }
427
+ return collectedHeritage;
428
+ };
429
+ const CALLABLE_SYMBOL_TYPES = new Set([
430
+ 'Function',
431
+ 'Method',
432
+ 'Constructor',
433
+ 'Macro',
434
+ 'Delegate',
435
+ ]);
436
+ const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
437
+ const filterCallableCandidates = (candidates, argCount, callForm) => {
438
+ let kindFiltered;
439
+ if (callForm === 'constructor') {
440
+ const constructors = candidates.filter(c => c.type === 'Constructor');
441
+ if (constructors.length > 0) {
442
+ kindFiltered = constructors;
443
+ }
444
+ else {
445
+ const types = candidates.filter(c => CONSTRUCTOR_TARGET_TYPES.has(c.type));
446
+ kindFiltered = types.length > 0 ? types : candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
447
+ }
448
+ }
449
+ else {
450
+ kindFiltered = candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
451
+ }
452
+ if (kindFiltered.length === 0)
453
+ return [];
454
+ if (argCount === undefined)
455
+ return kindFiltered;
456
+ const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
457
+ if (!hasParameterMetadata)
458
+ return kindFiltered;
459
+ return kindFiltered.filter(candidate => candidate.parameterCount === undefined || candidate.parameterCount === argCount);
460
+ };
461
+ const toResolveResult = (definition, tier) => ({
462
+ nodeId: definition.nodeId,
463
+ confidence: TIER_CONFIDENCE[tier],
464
+ reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global',
465
+ });
466
+ /**
467
+ * Resolve a chain of intermediate method calls to find the receiver type
468
+ * for a final member call (e.g. svc.getUser().save())
469
+ *
470
+ * @param chainNames - Ordered method names from outermost to innermost
471
+ * @param baseReceiverTypeName - Already-resolved type of the base receiver, or undefined
472
+ * @param currentFile - File path for resolution context
473
+ * @param ctx - Resolution context for symbol lookup
474
+ * @returns Final intermediate call's return type name, or undefined if resolution fails
475
+ */
476
+ function resolveChainedReceiver(chainNames, baseReceiverTypeName, currentFile, ctx) {
477
+ let currentType = baseReceiverTypeName;
478
+ for (const name of chainNames) {
479
+ const resolved = resolveCallTarget({ calledName: name, callForm: 'member', receiverTypeName: currentType }, currentFile, ctx);
480
+ if (!resolved)
481
+ return undefined;
482
+ const candidates = ctx.symbols.lookupFuzzy(name);
483
+ const symDef = candidates.find(c => c.nodeId === resolved.nodeId);
484
+ if (!symDef?.returnType)
485
+ return undefined;
486
+ const returnTypeName = extractReturnTypeName(symDef.returnType);
487
+ if (!returnTypeName)
488
+ return undefined;
489
+ currentType = returnTypeName;
490
+ }
491
+ return currentType;
492
+ }
493
+ /**
494
+ * Resolve a function call to its target node ID
495
+ *
496
+ * Strategy: A) narrow by scope tier, B) filter callable kinds, C) arity filter,
497
+ * D) receiver-type filter. Refuses to emit CALLS edge if multiple candidates remain
498
+ */
499
+ const resolveCallTarget = (call, currentFile, ctx) => {
500
+ const tiered = ctx.resolve(call.calledName, currentFile);
501
+ if (!tiered)
502
+ return null;
503
+ const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
504
+ // D. Receiver-type filtering: resolve type via tiered imports, filter method
505
+ // candidates to the type's defining file. Applied regardless of candidate count
506
+ // since the sole same-file candidate may belong to the wrong class
507
+ if (call.callForm === 'member' && call.receiverTypeName) {
508
+ // D1. Resolve receiver type
509
+ const typeResolved = ctx.resolve(call.receiverTypeName, currentFile);
510
+ if (typeResolved && typeResolved.candidates.length > 0) {
511
+ const typeNodeIds = new Set(typeResolved.candidates.map(d => d.nodeId));
512
+ const typeFiles = new Set(typeResolved.candidates.map(d => d.filePath));
513
+ // D2. Widen candidates: same-file tier may miss parent's method in another file
514
+ const methodPool = filteredCandidates.length <= 1
515
+ ? filterCallableCandidates(ctx.symbols.lookupFuzzy(call.calledName), call.argCount, call.callForm)
516
+ : filteredCandidates;
517
+ // D3. File-based: prefer candidates in the resolved type's file
518
+ const fileFiltered = methodPool.filter(c => typeFiles.has(c.filePath));
519
+ if (fileFiltered.length === 1) {
520
+ return toResolveResult(fileFiltered[0], tiered.tier);
521
+ }
522
+ // D4. ownerId fallback: narrow by ownerId matching type's nodeId
523
+ const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
524
+ const ownerFiltered = pool.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
525
+ if (ownerFiltered.length === 1) {
526
+ return toResolveResult(ownerFiltered[0], tiered.tier);
527
+ }
528
+ if (fileFiltered.length > 1 || ownerFiltered.length > 1)
529
+ return null;
530
+ }
531
+ }
532
+ if (filteredCandidates.length === 0)
533
+ return null;
534
+ // Deduplicate by nodeId — the same symbol can appear multiple times from different resolution paths
535
+ if (filteredCandidates.length > 1) {
536
+ const uniqueById = new Map(filteredCandidates.map(c => [c.nodeId, c]));
537
+ if (uniqueById.size === 1) {
538
+ return toResolveResult(filteredCandidates[0], tiered.tier);
539
+ }
540
+ // Multiple distinct candidates — ambiguous, skip
541
+ return null;
542
+ }
543
+ return toResolveResult(filteredCandidates[0], tiered.tier);
544
+ };
545
+ // Return type text helpers
546
+ // Operates on raw return-type text in SymbolDefinition (e.g. "User", "Promise<User>",
547
+ // "User | null", "*User") to extract the base user-defined type name
548
+ // Primitive/built-in types that should NOT produce a receiver binding
549
+ const PRIMITIVE_TYPES = new Set([
550
+ 'string', 'number', 'boolean', 'void', 'int', 'float', 'double', 'long',
551
+ 'short', 'byte', 'char', 'bool', 'str', 'i8', 'i16', 'i32', 'i64',
552
+ 'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'usize', 'isize',
553
+ 'undefined', 'null', 'None', 'nil',
554
+ ]);
555
+ /**
556
+ * Extract a simple type name from raw return-type text
557
+ *
558
+ * Handles: "User", "Promise<User>", "Option<User>", "Result<User, Error>",
559
+ * "User | null", "User?", "*User" (Go pointer), "&User" (Rust reference)
560
+ *
561
+ * @returns The base type name, or undefined for complex types or primitives
562
+ */
563
+ // Wrapper generics that get unwrapped to their inner type argument
564
+ // Containers (List, Vec, Set, etc.) are excluded -- methods are called on the container
565
+ const WRAPPER_GENERICS = new Set([
566
+ 'Promise', 'Observable', 'Future', 'CompletableFuture', 'Task', 'ValueTask',
567
+ 'Option', 'Some', 'Optional', 'Maybe',
568
+ 'Result', 'Either',
569
+ 'Rc', 'Arc', 'Weak',
570
+ 'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard',
571
+ 'Ref', 'RefMut',
572
+ 'Cow',
573
+ ]);
574
+ /** Extract the first type argument from a generic string, respecting nested angle brackets */
575
+ function extractFirstGenericArg(args) {
576
+ let depth = 0;
577
+ for (let i = 0; i < args.length; i++) {
578
+ if (args[i] === '<')
579
+ depth++;
580
+ else if (args[i] === '>')
581
+ depth--;
582
+ else if (args[i] === ',' && depth === 0)
583
+ return args.slice(0, i).trim();
584
+ }
585
+ return args.trim();
586
+ }
587
+ /** Extract the first non-lifetime type argument, skipping Rust lifetime params ('a, '_) */
588
+ function extractFirstTypeArg(args) {
589
+ let remaining = args;
590
+ while (remaining) {
591
+ const first = extractFirstGenericArg(remaining);
592
+ if (!first.startsWith("'"))
593
+ return first;
594
+ // Skip past this lifetime arg + comma separator
595
+ const commaIdx = remaining.indexOf(',', first.length);
596
+ if (commaIdx < 0)
597
+ return first; // only lifetimes, fall through
598
+ remaining = remaining.slice(commaIdx + 1).trim();
599
+ }
600
+ return args.trim();
601
+ }
602
+ export const extractReturnTypeName = (raw, depth = 0) => {
603
+ if (depth > 10)
604
+ return undefined;
605
+ let text = raw.trim();
606
+ if (!text)
607
+ return undefined;
608
+ // Strip pointer/reference prefixes (*User, &User, &mut User)
609
+ text = text.replace(/^[&*]+\s*(mut\s+)?/, '');
610
+ // Strip nullable suffix (User?)
611
+ text = text.replace(/\?$/, '');
612
+ // Handle union types ("User | null" -> "User")
613
+ if (text.includes('|')) {
614
+ const parts = text.split('|').map(p => p.trim()).filter(p => p !== 'null' && p !== 'undefined' && p !== 'void' && p !== 'None' && p !== 'nil');
615
+ if (parts.length === 1)
616
+ text = parts[0];
617
+ else
618
+ return undefined; // genuine union, too complex
619
+ }
620
+ // Handle generics (Promise<User> -> unwrap if wrapper, else take base)
621
+ const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
622
+ if (genericMatch) {
623
+ const [, base, args] = genericMatch;
624
+ if (WRAPPER_GENERICS.has(base)) {
625
+ // Take the first non-lifetime type argument using bracket-balanced splitting
626
+ const firstArg = extractFirstTypeArg(args);
627
+ return extractReturnTypeName(firstArg, depth + 1);
628
+ }
629
+ // Non-wrapper generic: return the base type (e.g. Map<K,V> -> Map)
630
+ return PRIMITIVE_TYPES.has(base.toLowerCase()) ? undefined : base;
631
+ }
632
+ // Bare wrapper type without generic argument is meaningless
633
+ if (WRAPPER_GENERICS.has(text))
634
+ return undefined;
635
+ // Handle qualified names (models.User -> User, Models::User -> User)
636
+ if (text.includes('::') || text.includes('.') || text.includes('\\')) {
637
+ text = text.split(/::|[.\\]/).pop();
638
+ }
639
+ // Skip primitives
640
+ if (PRIMITIVE_TYPES.has(text) || PRIMITIVE_TYPES.has(text.toLowerCase()))
641
+ return undefined;
642
+ // Must start with uppercase (class/type convention)
643
+ if (!/^[A-Z_]\w*$/.test(text))
644
+ return undefined;
645
+ return text;
646
+ };
647
+ // Scope key helpers
648
+ // Scope keys: "funcName@startIndex" (type-env.ts)
649
+ // Source IDs: "Label:filepath:funcName" (parse-worker.ts)
650
+ // NUL (\0) separates composite keys to prevent ambiguous concatenation
651
+ // receiverKey uses full scope to distinguish overloaded methods (e.g. User.save@100 vs Repo.save@200)
652
+ /** Extract function name from a scope key ("funcName@startIndex" -> "funcName") */
653
+ const extractFuncNameFromScope = (scope) => scope.slice(0, scope.indexOf('@'));
654
+ /** Extract trailing function name from a sourceId ("Function:filepath:funcName" -> "funcName") */
655
+ const extractFuncNameFromSourceId = (sourceId) => {
656
+ const lastColon = sourceId.lastIndexOf(':');
657
+ return lastColon >= 0 ? sourceId.slice(lastColon + 1) : '';
658
+ };
659
+ /** Build a composite key for receiver type storage using full scope string */
660
+ const receiverKey = (scope, varName) => `${scope}\0${varName}`;
661
+ /**
662
+ * Look up receiver type from a verified receiver map keyed by scope\0varName
663
+ *
664
+ * Scans entries matching funcName@ prefix with matching varName. Returns the
665
+ * type if exactly one unique match exists, undefined if ambiguous. Falls back
666
+ * to file-level scope (\0varName)
667
+ */
668
+ const lookupReceiverType = (map, funcName, varName) => {
669
+ // Fast path: file-level scope (empty funcName, used as fallback)
670
+ const fileLevelKey = receiverKey('', varName);
671
+ const prefix = `${funcName}@`;
672
+ const suffix = `\0${varName}`;
673
+ let found;
674
+ let ambiguous = false;
675
+ for (const [key, value] of map) {
676
+ if (key === fileLevelKey)
677
+ continue; // handled separately below
678
+ if (key.startsWith(prefix) && key.endsWith(suffix)) {
679
+ // Verify key is "funcName@<startIndex>\0varName" with non-empty middle segment
680
+ const middle = key.slice(prefix.length, key.length - suffix.length);
681
+ if (middle.length === 0)
682
+ continue; // malformed key, skip
683
+ if (found === undefined) {
684
+ found = value;
685
+ }
686
+ else if (found !== value) {
687
+ ambiguous = true;
688
+ break;
689
+ }
690
+ }
691
+ }
692
+ if (!ambiguous && found !== undefined)
693
+ return found;
694
+ // Fallback: file-level scope
695
+ return map.get(fileLevelKey);
696
+ };
697
+ /** Resolve pre-extracted call sites from workers (no AST parsing needed) */
698
+ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
699
+ // Scope-aware receiver types keyed by filePath -> scope\0varName -> typeName
700
+ const fileReceiverTypes = new Map();
701
+ if (constructorBindings) {
702
+ for (const { filePath, bindings } of constructorBindings) {
703
+ const verified = verifyConstructorBindings(bindings, filePath, ctx, graph);
704
+ if (verified.size > 0) {
705
+ fileReceiverTypes.set(filePath, verified);
706
+ }
707
+ }
708
+ }
709
+ const byFile = new Map();
710
+ for (const call of extractedCalls) {
711
+ let list = byFile.get(call.filePath);
712
+ if (!list) {
713
+ list = [];
714
+ byFile.set(call.filePath, list);
715
+ }
716
+ list.push(call);
717
+ }
718
+ const totalFiles = byFile.size;
719
+ let filesProcessed = 0;
720
+ for (const [filePath, calls] of byFile) {
721
+ filesProcessed++;
722
+ if (filesProcessed % 100 === 0) {
723
+ onProgress?.(filesProcessed, totalFiles);
724
+ await yieldToEventLoop();
725
+ }
726
+ ctx.enableCache(filePath);
727
+ const receiverMap = fileReceiverTypes.get(filePath);
728
+ for (const call of calls) {
729
+ let effectiveCall = call;
730
+ // Step 1: resolve receiver type from constructor bindings
731
+ if (!call.receiverTypeName && call.receiverName && receiverMap) {
732
+ const callFuncName = extractFuncNameFromSourceId(call.sourceId);
733
+ const resolvedType = lookupReceiverType(receiverMap, callFuncName, call.receiverName);
734
+ if (resolvedType) {
735
+ effectiveCall = { ...call, receiverTypeName: resolvedType };
736
+ }
737
+ }
738
+ // Step 1b: class-as-receiver for static method calls
739
+ if (!effectiveCall.receiverTypeName && effectiveCall.receiverName && effectiveCall.callForm === 'member') {
740
+ const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath);
741
+ if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
742
+ effectiveCall = { ...effectiveCall, receiverTypeName: effectiveCall.receiverName };
743
+ }
744
+ }
745
+ // Step 2: resolve receiver call chain (e.g. svc.getUser().save())
746
+ // Runs even when Step 1 set a receiverTypeName -- that's the BASE receiver,
747
+ // the chain resolves the FINAL receiver type
748
+ if (effectiveCall.receiverCallChain?.length) {
749
+ // Use Step 1's base receiver type as starting point for chain resolution
750
+ let baseType = effectiveCall.receiverTypeName;
751
+ // If Step 1 didn't resolve it, try the receiver map directly
752
+ if (!baseType && effectiveCall.receiverName && receiverMap) {
753
+ const callFuncName = extractFuncNameFromSourceId(effectiveCall.sourceId);
754
+ baseType = lookupReceiverType(receiverMap, callFuncName, effectiveCall.receiverName);
755
+ }
756
+ const chainedType = resolveChainedReceiver(effectiveCall.receiverCallChain, baseType, effectiveCall.filePath, ctx);
757
+ if (chainedType) {
758
+ effectiveCall = { ...effectiveCall, receiverTypeName: chainedType };
759
+ }
760
+ }
761
+ const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
762
+ if (!resolved)
763
+ continue;
764
+ const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
765
+ graph.addRelationship({
766
+ id: relId,
767
+ sourceId: effectiveCall.sourceId,
768
+ targetId: resolved.nodeId,
769
+ type: 'CALLS',
770
+ confidence: resolved.confidence,
771
+ reason: resolved.reason,
772
+ callLine: effectiveCall.callLine,
773
+ });
774
+ }
775
+ ctx.clearCache();
776
+ }
777
+ onProgress?.(totalFiles, totalFiles);
778
+ };
779
+ /** Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods */
780
+ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, onProgress) => {
781
+ for (let i = 0; i < extractedRoutes.length; i++) {
782
+ const route = extractedRoutes[i];
783
+ if (i % 50 === 0) {
784
+ onProgress?.(i, extractedRoutes.length);
785
+ await yieldToEventLoop();
786
+ }
787
+ if (!route.controllerName || !route.methodName)
788
+ continue;
789
+ const controllerResolved = ctx.resolve(route.controllerName, route.filePath);
790
+ if (!controllerResolved || controllerResolved.candidates.length === 0)
791
+ continue;
792
+ if (controllerResolved.tier === 'global' && controllerResolved.candidates.length > 1)
793
+ continue;
794
+ const controllerDef = controllerResolved.candidates[0];
795
+ const confidence = TIER_CONFIDENCE[controllerResolved.tier];
796
+ const methodResolved = ctx.resolve(route.methodName, controllerDef.filePath);
797
+ const methodId = methodResolved?.tier === 'same-file' ? methodResolved.candidates[0]?.nodeId : undefined;
798
+ const sourceId = generateId('File', route.filePath);
799
+ if (!methodId) {
800
+ const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
801
+ const relId = generateId('CALLS', `${sourceId}:route->${guessedId}`);
802
+ graph.addRelationship({
803
+ id: relId,
804
+ sourceId,
805
+ targetId: guessedId,
806
+ type: 'CALLS',
807
+ confidence: confidence * 0.8,
808
+ reason: 'laravel-route',
809
+ });
810
+ continue;
811
+ }
812
+ const relId = generateId('CALLS', `${sourceId}:route->${methodId}`);
813
+ graph.addRelationship({
814
+ id: relId,
815
+ sourceId,
816
+ targetId: methodId,
817
+ type: 'CALLS',
818
+ confidence,
819
+ reason: 'laravel-route',
820
+ });
821
+ }
822
+ onProgress?.(extractedRoutes.length, extractedRoutes.length);
823
+ };
824
+ // ── DI Awareness: DEPENDS_ON + PROVIDES edge creation ───────────────────
825
+ /** Symbol types that represent injectable dependencies */
826
+ const INJECTABLE_TYPES = new Set(['Class', 'Interface', 'Trait', 'Struct']);
827
+ /** Method names that act as constructors across languages */
828
+ const CONSTRUCTOR_METHOD_NAMES = new Set(['__init__', 'initialize', 'constructor']);
829
+ /**
830
+ * Create DEPENDS_ON edges from constructor parameter types.
831
+ *
832
+ * For each Constructor (or constructor-like method like Python __init__),
833
+ * resolves its parameterTypes and creates DEPENDS_ON edges from the owning
834
+ * class to each resolved Class/Interface/Trait/Struct parameter type.
835
+ *
836
+ * This bridges DI: BookingService(IPaymentGateway) → DEPENDS_ON → IPaymentGateway
837
+ */
838
+ export const createDependsOnEdges = async (graph, ctx) => {
839
+ let edgesCreated = 0;
840
+ const seen = new Set(); // deduplicate: "ownerClassId->targetId"
841
+ // Collect Constructor nodes + constructor-like methods (__init__, initialize)
842
+ const candidates = [];
843
+ graph.forEachNode(n => {
844
+ if (n.label === 'Constructor') {
845
+ candidates.push({ nodeId: n.id, filePath: n.properties.filePath, name: n.properties.name });
846
+ }
847
+ else if ((n.label === 'Method' || n.label === 'Function') && CONSTRUCTOR_METHOD_NAMES.has(n.properties.name)) {
848
+ candidates.push({ nodeId: n.id, filePath: n.properties.filePath, name: n.properties.name });
849
+ }
850
+ });
851
+ for (const ctor of candidates) {
852
+ const symDef = ctx.symbols.lookupExactFull(ctor.filePath, ctor.name);
853
+ if (!symDef?.parameterTypes?.length || !symDef.ownerId)
854
+ continue;
855
+ for (const paramType of symDef.parameterTypes) {
856
+ const resolved = ctx.resolve(paramType, ctor.filePath);
857
+ if (!resolved || resolved.candidates.length === 0)
858
+ continue;
859
+ // Find first injectable candidate
860
+ const target = resolved.candidates.find(c => INJECTABLE_TYPES.has(c.type));
861
+ if (!target)
862
+ continue;
863
+ // Skip self-referential edges
864
+ if (target.nodeId === symDef.ownerId)
865
+ continue;
866
+ const edgeKey = `${symDef.ownerId}->${target.nodeId}`;
867
+ if (seen.has(edgeKey))
868
+ continue;
869
+ seen.add(edgeKey);
870
+ graph.addRelationship({
871
+ id: generateId('DEPENDS_ON', edgeKey),
872
+ sourceId: symDef.ownerId,
873
+ targetId: target.nodeId,
874
+ type: 'DEPENDS_ON',
875
+ confidence: TIER_CONFIDENCE[resolved.tier],
876
+ reason: 'constructor-injection',
877
+ });
878
+ edgesCreated++;
879
+ }
880
+ }
881
+ return edgesCreated;
882
+ };
883
+ /**
884
+ * Create PROVIDES edges from factory method return types.
885
+ *
886
+ * For methods/functions whose return type resolves to an Interface or Trait,
887
+ * creates a PROVIDES edge from the method to the interface. Heuristic filter:
888
+ * only short methods (< 20 lines) OR methods with DI-related framework annotations.
889
+ *
890
+ * This captures @Bean factories, provider methods, and factory functions.
891
+ */
892
+ export const createProvidesEdges = async (graph, ctx) => {
893
+ const PROVIDER_TYPES = new Set(['Interface', 'Trait']);
894
+ let edgesCreated = 0;
895
+ const candidates = [];
896
+ graph.forEachNode(n => {
897
+ if ((n.label === 'Method' || n.label === 'Function') && n.properties.returnType) {
898
+ candidates.push({
899
+ id: n.id,
900
+ filePath: n.properties.filePath,
901
+ returnType: n.properties.returnType,
902
+ startLine: n.properties.startLine ?? 0,
903
+ endLine: n.properties.endLine ?? 0,
904
+ frameworkReason: n.properties.astFrameworkReason,
905
+ });
906
+ }
907
+ });
908
+ for (const node of candidates) {
909
+ const returnTypeName = extractReturnTypeName(node.returnType);
910
+ if (!returnTypeName)
911
+ continue;
912
+ const resolved = ctx.resolve(returnTypeName, node.filePath);
913
+ if (!resolved || resolved.candidates.length === 0)
914
+ continue;
915
+ const interfaceTarget = resolved.candidates.find(c => PROVIDER_TYPES.has(c.type));
916
+ if (!interfaceTarget)
917
+ continue;
918
+ // Heuristic: only short methods (likely factories) or DI-annotated methods
919
+ const bodyLength = node.endLine - node.startLine;
920
+ const isDIAnnotated = node.frameworkReason && (node.frameworkReason.includes('bean') ||
921
+ node.frameworkReason.includes('provider') ||
922
+ node.frameworkReason.includes('provides') ||
923
+ node.frameworkReason.includes('di'));
924
+ if (bodyLength > 20 && !isDIAnnotated)
925
+ continue;
926
+ graph.addRelationship({
927
+ id: generateId('PROVIDES', `${node.id}->${interfaceTarget.nodeId}`),
928
+ sourceId: node.id,
929
+ targetId: interfaceTarget.nodeId,
930
+ type: 'PROVIDES',
931
+ confidence: TIER_CONFIDENCE[resolved.tier] * 0.9,
932
+ reason: 'factory-return-type',
933
+ });
934
+ edgesCreated++;
935
+ }
936
+ return edgesCreated;
937
+ };