@veewo/gitnexus 1.3.11 → 1.4.6-rc

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 (181) hide show
  1. package/README.md +37 -80
  2. package/dist/benchmark/agent-context/tool-runner.js +2 -2
  3. package/dist/benchmark/neonspark-candidates.js +3 -3
  4. package/dist/benchmark/tool-runner.js +2 -2
  5. package/dist/cli/ai-context.d.ts +2 -1
  6. package/dist/cli/ai-context.js +16 -12
  7. package/dist/cli/analyze.d.ts +2 -0
  8. package/dist/cli/analyze.js +68 -48
  9. package/dist/cli/augment.js +1 -1
  10. package/dist/cli/eval-server.d.ts +8 -1
  11. package/dist/cli/eval-server.js +30 -13
  12. package/dist/cli/index.js +28 -82
  13. package/dist/cli/lazy-action.d.ts +6 -0
  14. package/dist/cli/lazy-action.js +18 -0
  15. package/dist/cli/mcp.js +3 -1
  16. package/dist/cli/setup.js +87 -48
  17. package/dist/cli/setup.test.js +18 -13
  18. package/dist/cli/skill-gen.d.ts +26 -0
  19. package/dist/cli/skill-gen.js +549 -0
  20. package/dist/cli/status.js +13 -4
  21. package/dist/cli/tool.d.ts +3 -2
  22. package/dist/cli/tool.js +50 -16
  23. package/dist/cli/wiki.js +8 -4
  24. package/dist/config/ignore-service.d.ts +25 -0
  25. package/dist/config/ignore-service.js +76 -0
  26. package/dist/config/supported-languages.d.ts +4 -1
  27. package/dist/config/supported-languages.js +3 -2
  28. package/dist/core/augmentation/engine.js +94 -67
  29. package/dist/core/embeddings/embedder.d.ts +1 -1
  30. package/dist/core/embeddings/embedder.js +1 -1
  31. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  32. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  33. package/dist/core/embeddings/types.d.ts +1 -1
  34. package/dist/core/graph/types.d.ts +7 -2
  35. package/dist/core/ingestion/ast-cache.js +3 -2
  36. package/dist/core/ingestion/call-processor.d.ts +8 -6
  37. package/dist/core/ingestion/call-processor.js +468 -206
  38. package/dist/core/ingestion/call-routing.d.ts +53 -0
  39. package/dist/core/ingestion/call-routing.js +108 -0
  40. package/dist/core/ingestion/constants.d.ts +16 -0
  41. package/dist/core/ingestion/constants.js +16 -0
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +116 -23
  44. package/dist/core/ingestion/export-detection.d.ts +18 -0
  45. package/dist/core/ingestion/export-detection.js +231 -0
  46. package/dist/core/ingestion/filesystem-walker.js +4 -3
  47. package/dist/core/ingestion/framework-detection.d.ts +19 -4
  48. package/dist/core/ingestion/framework-detection.js +182 -6
  49. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  50. package/dist/core/ingestion/heritage-processor.js +109 -55
  51. package/dist/core/ingestion/import-processor.d.ts +16 -20
  52. package/dist/core/ingestion/import-processor.js +199 -579
  53. package/dist/core/ingestion/language-config.d.ts +46 -0
  54. package/dist/core/ingestion/language-config.js +167 -0
  55. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  56. package/dist/core/ingestion/mro-processor.js +369 -0
  57. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  58. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  59. package/dist/core/ingestion/parsing-processor.d.ts +4 -1
  60. package/dist/core/ingestion/parsing-processor.js +107 -109
  61. package/dist/core/ingestion/pipeline.d.ts +6 -3
  62. package/dist/core/ingestion/pipeline.js +208 -114
  63. package/dist/core/ingestion/process-processor.js +8 -2
  64. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  65. package/dist/core/ingestion/resolution-context.js +132 -0
  66. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  67. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  68. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  69. package/dist/core/ingestion/resolvers/go.js +42 -0
  70. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  71. package/dist/core/ingestion/resolvers/index.js +13 -0
  72. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  73. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  74. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  75. package/dist/core/ingestion/resolvers/php.js +35 -0
  76. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  77. package/dist/core/ingestion/resolvers/python.js +52 -0
  78. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  79. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  80. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  81. package/dist/core/ingestion/resolvers/rust.js +73 -0
  82. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  83. package/dist/core/ingestion/resolvers/standard.js +123 -0
  84. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  85. package/dist/core/ingestion/resolvers/utils.js +122 -0
  86. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  87. package/dist/core/ingestion/symbol-table.js +40 -12
  88. package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
  89. package/dist/core/ingestion/tree-sitter-queries.js +297 -7
  90. package/dist/core/ingestion/type-env.d.ts +49 -0
  91. package/dist/core/ingestion/type-env.js +611 -0
  92. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  93. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  94. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  95. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  96. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  97. package/dist/core/ingestion/type-extractors/go.js +467 -0
  98. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  99. package/dist/core/ingestion/type-extractors/index.js +31 -0
  100. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  101. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  102. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  103. package/dist/core/ingestion/type-extractors/php.js +549 -0
  104. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  105. package/dist/core/ingestion/type-extractors/python.js +406 -0
  106. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  107. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  108. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  109. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  110. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  111. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  112. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  113. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  114. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  115. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  116. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  117. package/dist/core/ingestion/utils.d.ts +103 -0
  118. package/dist/core/ingestion/utils.js +1085 -4
  119. package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
  120. package/dist/core/ingestion/workers/parse-worker.js +634 -222
  121. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  122. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
  123. package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
  124. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
  125. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
  126. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  127. package/dist/core/{kuzu → lbug}/schema.js +23 -22
  128. package/dist/core/lbug/schema.test.d.ts +1 -0
  129. package/dist/core/search/bm25-index.d.ts +4 -4
  130. package/dist/core/search/bm25-index.js +12 -11
  131. package/dist/core/search/hybrid-search.d.ts +2 -2
  132. package/dist/core/search/hybrid-search.js +6 -6
  133. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  134. package/dist/core/tree-sitter/parser-loader.js +19 -0
  135. package/dist/core/wiki/generator.d.ts +2 -2
  136. package/dist/core/wiki/generator.js +6 -6
  137. package/dist/core/wiki/graph-queries.d.ts +4 -4
  138. package/dist/core/wiki/graph-queries.js +7 -7
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
  142. package/dist/mcp/core/lbug-adapter.js +327 -0
  143. package/dist/mcp/local/local-backend.d.ts +21 -16
  144. package/dist/mcp/local/local-backend.js +306 -706
  145. package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
  146. package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
  147. package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
  148. package/dist/mcp/resources.js +2 -2
  149. package/dist/mcp/server.js +28 -13
  150. package/dist/mcp/staleness.js +2 -2
  151. package/dist/mcp/tools.js +12 -3
  152. package/dist/server/api.js +12 -12
  153. package/dist/server/mcp-http.d.ts +1 -1
  154. package/dist/server/mcp-http.js +1 -1
  155. package/dist/storage/git.js +4 -1
  156. package/dist/storage/repo-manager.d.ts +20 -2
  157. package/dist/storage/repo-manager.js +74 -4
  158. package/dist/types/pipeline.d.ts +1 -1
  159. package/hooks/claude/gitnexus-hook.cjs +149 -46
  160. package/hooks/claude/pre-tool-use.sh +2 -1
  161. package/hooks/claude/session-start.sh +0 -0
  162. package/package.json +20 -4
  163. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  164. package/skills/gitnexus-cli.md +8 -8
  165. package/skills/gitnexus-debugging.md +1 -1
  166. package/skills/gitnexus-exploring.md +1 -1
  167. package/skills/gitnexus-guide.md +1 -1
  168. package/skills/gitnexus-impact-analysis.md +1 -1
  169. package/skills/gitnexus-pr-review.md +163 -0
  170. package/skills/gitnexus-refactoring.md +1 -1
  171. package/dist/cli/claude-hooks.d.ts +0 -22
  172. package/dist/cli/claude-hooks.js +0 -97
  173. package/dist/mcp/core/kuzu-adapter.js +0 -231
  174. /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
  175. /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
  176. /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
  177. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
  178. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
  179. /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
  180. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
  181. /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
@@ -1,151 +1,114 @@
1
1
  import Parser from 'tree-sitter';
2
- import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
2
+ import { TIER_CONFIDENCE } from './resolution-context.js';
3
+ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
3
4
  import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
4
5
  import { generateId } from '../../lib/utils.js';
5
- import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
6
- /**
7
- * Node types that represent function/method definitions across languages.
8
- * Used to find the enclosing function for a call site.
9
- */
10
- const FUNCTION_NODE_TYPES = new Set([
11
- // TypeScript/JavaScript
12
- 'function_declaration',
13
- 'arrow_function',
14
- 'function_expression',
15
- 'method_definition',
16
- 'generator_function_declaration',
17
- // Python
18
- 'function_definition',
19
- // Common async variants
20
- 'async_function_declaration',
21
- 'async_arrow_function',
22
- // Java
23
- 'method_declaration',
24
- 'constructor_declaration',
25
- // C/C++
26
- // 'function_definition' already included above
27
- // Go
28
- // 'method_declaration' already included from Java
29
- // C#
30
- 'local_function_statement',
31
- // Rust
32
- 'function_item',
33
- 'impl_item', // Methods inside impl blocks
34
- ]);
6
+ import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
7
+ import { buildTypeEnv } from './type-env.js';
8
+ import { getTreeSitterBufferSize } from './constants.js';
9
+ import { callRouters } from './call-routing.js';
10
+ import { extractReturnTypeName } from './type-extractors/shared.js';
35
11
  /**
36
12
  * Walk up the AST from a node to find the enclosing function/method.
37
13
  * Returns null if the call is at module/file level (top-level code).
38
14
  */
39
- const findEnclosingFunction = (node, filePath, symbolTable) => {
15
+ const findEnclosingFunction = (node, filePath, ctx) => {
40
16
  let current = node.parent;
41
17
  while (current) {
42
18
  if (FUNCTION_NODE_TYPES.has(current.type)) {
43
- // Found enclosing function - try to get its name
44
- let funcName = null;
45
- let label = 'Function';
46
- // Different node types have different name locations
47
- if (current.type === 'function_declaration' ||
48
- current.type === 'function_definition' ||
49
- current.type === 'async_function_declaration' ||
50
- current.type === 'generator_function_declaration' ||
51
- current.type === 'function_item') { // Rust function
52
- // Named function: function foo() {}
53
- const nameNode = current.childForFieldName?.('name') ||
54
- current.children?.find((c) => c.type === 'identifier' || c.type === 'property_identifier');
55
- funcName = nameNode?.text;
56
- }
57
- else if (current.type === 'impl_item') {
58
- // Rust method inside impl block: wrapper around function_item or const_item
59
- // We need to look inside for the function_item
60
- const funcItem = current.children?.find((c) => c.type === 'function_item');
61
- if (funcItem) {
62
- const nameNode = funcItem.childForFieldName?.('name') ||
63
- funcItem.children?.find((c) => c.type === 'identifier');
64
- funcName = nameNode?.text;
65
- label = 'Method';
19
+ const { funcName, label } = extractFunctionName(current);
20
+ if (funcName) {
21
+ const resolved = ctx.resolve(funcName, filePath);
22
+ if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
23
+ return resolved.candidates[0].nodeId;
66
24
  }
25
+ return generateId(label, `${filePath}:${funcName}`);
67
26
  }
68
- else if (current.type === 'method_definition') {
69
- // Method: foo() {} inside class (JS/TS)
70
- const nameNode = current.childForFieldName?.('name') ||
71
- current.children?.find((c) => c.type === 'property_identifier');
72
- funcName = nameNode?.text;
73
- label = 'Method';
74
- }
75
- else if (current.type === 'method_declaration') {
76
- // Java method: public void foo() {}
77
- const nameNode = current.childForFieldName?.('name') ||
78
- current.children?.find((c) => c.type === 'identifier');
79
- funcName = nameNode?.text;
80
- label = 'Method';
81
- }
82
- else if (current.type === 'constructor_declaration') {
83
- // Java constructor: public ClassName() {}
84
- const nameNode = current.childForFieldName?.('name') ||
85
- current.children?.find((c) => c.type === 'identifier');
86
- funcName = nameNode?.text;
87
- label = 'Method'; // Treat constructors as methods for process detection
88
- }
89
- else if (current.type === 'arrow_function' || current.type === 'function_expression') {
90
- // Arrow/expression: const foo = () => {} - check parent variable declarator
91
- const parent = current.parent;
92
- if (parent?.type === 'variable_declarator') {
93
- const nameNode = parent.childForFieldName?.('name') ||
94
- parent.children?.find((c) => c.type === 'identifier');
95
- funcName = nameNode?.text;
27
+ }
28
+ current = current.parent;
29
+ }
30
+ return null;
31
+ };
32
+ /**
33
+ * Verify constructor bindings against SymbolTable and infer receiver types.
34
+ * Shared between sequential (processCalls) and worker (processCallsFromExtracted) paths.
35
+ */
36
+ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
37
+ const verified = new Map();
38
+ for (const { scope, varName, calleeName, receiverClassName } of bindings) {
39
+ const tiered = ctx.resolve(calleeName, filePath);
40
+ const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
41
+ if (isClass) {
42
+ verified.set(receiverKey(scope, varName), calleeName);
43
+ }
44
+ else {
45
+ let callableDefs = tiered?.candidates.filter(d => d.type === 'Function' || d.type === 'Method');
46
+ // When receiver class is known (e.g. $this->method() in PHP), narrow
47
+ // candidates to methods owned by that class to avoid false disambiguation failures.
48
+ if (callableDefs && callableDefs.length > 1 && receiverClassName) {
49
+ if (graph) {
50
+ // Worker path: use graph.getNode (fast, already in-memory)
51
+ const narrowed = callableDefs.filter(d => {
52
+ if (!d.ownerId)
53
+ return false;
54
+ const owner = graph.getNode(d.ownerId);
55
+ return owner?.properties.name === receiverClassName;
56
+ });
57
+ if (narrowed.length > 0)
58
+ callableDefs = narrowed;
59
+ }
60
+ else {
61
+ // Sequential path: use ctx.resolve (no graph available)
62
+ const classResolved = ctx.resolve(receiverClassName, filePath);
63
+ if (classResolved && classResolved.candidates.length > 0) {
64
+ const classNodeIds = new Set(classResolved.candidates.map(c => c.nodeId));
65
+ const narrowed = callableDefs.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
66
+ if (narrowed.length > 0)
67
+ callableDefs = narrowed;
68
+ }
96
69
  }
97
70
  }
98
- if (funcName) {
99
- // Look up the function in symbol table to get its node ID
100
- // Try exact match first
101
- const nodeId = symbolTable.lookupExact(filePath, funcName);
102
- if (nodeId)
103
- return nodeId;
104
- // Try construct ID manually if lookup fails (common for non-exported internal functions)
105
- // Format should match what parsing-processor generates: "Function:path/to/file:funcName"
106
- // Check if we already have a node with this ID in the symbol table to be safe
107
- const generatedId = generateId(label, `${filePath}:${funcName}`);
108
- // Ideally we should verify this ID exists, but strictly speaking if we are inside it,
109
- // it SHOULD exist. Returning it is better than falling back to File.
110
- return generatedId;
71
+ if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) {
72
+ const typeName = extractReturnTypeName(callableDefs[0].returnType);
73
+ if (typeName) {
74
+ verified.set(receiverKey(scope, varName), typeName);
75
+ }
111
76
  }
112
- // Couldn't determine function name - try parent (might be nested)
113
77
  }
114
- current = current.parent;
115
78
  }
116
- return null; // Top-level call (not inside any function)
79
+ return verified;
117
80
  };
118
- export const processCalls = async (graph, files, astCache, symbolTable, importMap, onProgress) => {
81
+ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
119
82
  const parser = await loadParser();
83
+ const collectedHeritage = [];
84
+ const logSkipped = isVerboseIngestionEnabled();
85
+ const skippedByLang = logSkipped ? new Map() : null;
120
86
  for (let i = 0; i < files.length; i++) {
121
87
  const file = files[i];
122
88
  onProgress?.(i + 1, files.length);
123
89
  if (i % 20 === 0)
124
90
  await yieldToEventLoop();
125
- // 1. Check language support first
126
91
  const language = getLanguageFromFilename(file.path);
127
92
  if (!language)
128
93
  continue;
94
+ if (!isLanguageAvailable(language)) {
95
+ if (skippedByLang) {
96
+ skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
97
+ }
98
+ continue;
99
+ }
129
100
  const queryStr = LANGUAGE_QUERIES[language];
130
101
  if (!queryStr)
131
102
  continue;
132
- // 2. ALWAYS load the language before querying (parser is stateful)
133
103
  await loadLanguage(language, file.path);
134
- // 3. Get AST (Try Cache First)
135
104
  let tree = astCache.get(file.path);
136
- let wasReparsed = false;
137
105
  if (!tree) {
138
- // Cache Miss: Re-parse
139
- // Use larger bufferSize for files > 32KB
140
106
  try {
141
- tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
107
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
142
108
  }
143
109
  catch (parseError) {
144
- // Skip files that can't be parsed
145
110
  continue;
146
111
  }
147
- wasReparsed = true;
148
- // Cache re-parsed tree so heritage phase gets hits
149
112
  astCache.set(file.path, tree);
150
113
  }
151
114
  let query;
@@ -159,28 +122,130 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
159
122
  console.warn(`Query error for ${file.path}:`, queryError);
160
123
  continue;
161
124
  }
162
- // 3. Process each call match
125
+ const lang = getLanguageFromFilename(file.path);
126
+ const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
127
+ const callRouter = callRouters[language];
128
+ const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
129
+ ? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
130
+ : new Map();
131
+ ctx.enableCache(file.path);
163
132
  matches.forEach(match => {
164
133
  const captureMap = {};
165
134
  match.captures.forEach(c => captureMap[c.name] = c.node);
166
- // Only process @call captures
167
135
  if (!captureMap['call'])
168
136
  return;
169
137
  const nameNode = captureMap['call.name'];
170
138
  if (!nameNode)
171
139
  return;
172
140
  const calledName = nameNode.text;
173
- // Skip common built-ins and noise
141
+ const routed = callRouter(calledName, captureMap['call']);
142
+ if (routed) {
143
+ switch (routed.kind) {
144
+ case 'skip':
145
+ case 'import':
146
+ return;
147
+ case 'heritage':
148
+ for (const item of routed.items) {
149
+ collectedHeritage.push({
150
+ filePath: file.path,
151
+ className: item.enclosingClass,
152
+ parentName: item.mixinName,
153
+ kind: item.heritageKind,
154
+ });
155
+ }
156
+ return;
157
+ case 'properties': {
158
+ const fileId = generateId('File', file.path);
159
+ const propEnclosingClassId = findEnclosingClassId(captureMap['call'], file.path);
160
+ for (const item of routed.items) {
161
+ const nodeId = generateId('Property', `${file.path}:${item.propName}`);
162
+ graph.addNode({
163
+ id: nodeId,
164
+ label: 'Property',
165
+ properties: {
166
+ name: item.propName, filePath: file.path,
167
+ startLine: item.startLine, endLine: item.endLine,
168
+ language, isExported: true,
169
+ description: item.accessorType,
170
+ },
171
+ });
172
+ ctx.symbols.add(file.path, item.propName, nodeId, 'Property', propEnclosingClassId ? { ownerId: propEnclosingClassId } : undefined);
173
+ const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
174
+ graph.addRelationship({
175
+ id: relId, sourceId: fileId, targetId: nodeId,
176
+ type: 'DEFINES', confidence: 1.0, reason: '',
177
+ });
178
+ if (propEnclosingClassId) {
179
+ graph.addRelationship({
180
+ id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
181
+ sourceId: propEnclosingClassId, targetId: nodeId,
182
+ type: 'HAS_METHOD', confidence: 1.0, reason: '',
183
+ });
184
+ }
185
+ }
186
+ return;
187
+ }
188
+ case 'call':
189
+ break;
190
+ }
191
+ }
174
192
  if (isBuiltInOrNoise(calledName))
175
193
  return;
176
- // 4. Resolve the target using priority strategy (returns confidence)
177
- const resolved = resolveCallTarget(calledName, file.path, symbolTable, importMap);
194
+ const callNode = captureMap['call'];
195
+ const callForm = inferCallForm(callNode, nameNode);
196
+ const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
197
+ let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
198
+ // Fall back to verified constructor bindings for return type inference
199
+ if (!receiverTypeName && receiverName && verifiedReceivers.size > 0) {
200
+ const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
201
+ const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
202
+ receiverTypeName = lookupReceiverType(verifiedReceivers, funcName, receiverName);
203
+ }
204
+ // Fall back to class-as-receiver for static method calls (e.g. UserService.find_user()).
205
+ // When the receiver name is not a variable in TypeEnv but resolves to a Class/Struct/Interface
206
+ // through the standard tiered resolution, use it directly as the receiver type.
207
+ if (!receiverTypeName && receiverName && callForm === 'member') {
208
+ const typeResolved = ctx.resolve(receiverName, file.path);
209
+ if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
210
+ receiverTypeName = receiverName;
211
+ }
212
+ }
213
+ // Fall back to chained call resolution when the receiver is a call expression
214
+ // (e.g. svc.getUser().save() — receiver of save() is getUser(), not a simple identifier).
215
+ if (callForm === 'member' && !receiverTypeName && !receiverName) {
216
+ const receiverNode = extractReceiverNode(nameNode);
217
+ if (receiverNode && CALL_EXPRESSION_TYPES.has(receiverNode.type)) {
218
+ const extracted = extractCallChain(receiverNode);
219
+ if (extracted) {
220
+ // Resolve the base receiver type if possible
221
+ let baseType = extracted.baseReceiverName && typeEnv
222
+ ? typeEnv.lookup(extracted.baseReceiverName, callNode)
223
+ : undefined;
224
+ if (!baseType && extracted.baseReceiverName && verifiedReceivers.size > 0) {
225
+ const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
226
+ const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
227
+ baseType = lookupReceiverType(verifiedReceivers, funcName, extracted.baseReceiverName);
228
+ }
229
+ // Class-as-receiver for chain base (e.g. UserService.find_user().save())
230
+ if (!baseType && extracted.baseReceiverName) {
231
+ const cr = ctx.resolve(extracted.baseReceiverName, file.path);
232
+ if (cr?.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
233
+ baseType = extracted.baseReceiverName;
234
+ }
235
+ }
236
+ receiverTypeName = resolveChainedReceiver(extracted.chain, baseType, file.path, ctx);
237
+ }
238
+ }
239
+ }
240
+ const resolved = resolveCallTarget({
241
+ calledName,
242
+ argCount: countCallArguments(callNode),
243
+ callForm,
244
+ receiverTypeName,
245
+ }, file.path, ctx);
178
246
  if (!resolved)
179
247
  return;
180
- // 5. Find the enclosing function (caller)
181
- const callNode = captureMap['call'];
182
- const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
183
- // Use enclosing function as source, fallback to file for top-level calls
248
+ const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
184
249
  const sourceId = enclosingFuncId || generateId('File', file.path);
185
250
  const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
186
251
  graph.addRelationship({
@@ -192,105 +257,216 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
192
257
  reason: resolved.reason,
193
258
  });
194
259
  });
195
- // Tree is now owned by the LRU cache — no manual delete needed
260
+ ctx.clearCache();
196
261
  }
262
+ if (skippedByLang && skippedByLang.size > 0) {
263
+ for (const [lang, count] of skippedByLang.entries()) {
264
+ console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in call processing — ${lang} parser not available.`);
265
+ }
266
+ }
267
+ return collectedHeritage;
268
+ };
269
+ const CALLABLE_SYMBOL_TYPES = new Set([
270
+ 'Function',
271
+ 'Method',
272
+ 'Constructor',
273
+ 'Macro',
274
+ 'Delegate',
275
+ ]);
276
+ const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
277
+ const filterCallableCandidates = (candidates, argCount, callForm) => {
278
+ let kindFiltered;
279
+ if (callForm === 'constructor') {
280
+ const constructors = candidates.filter(c => c.type === 'Constructor');
281
+ if (constructors.length > 0) {
282
+ kindFiltered = constructors;
283
+ }
284
+ else {
285
+ const types = candidates.filter(c => CONSTRUCTOR_TARGET_TYPES.has(c.type));
286
+ kindFiltered = types.length > 0 ? types : candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
287
+ }
288
+ }
289
+ else {
290
+ kindFiltered = candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
291
+ }
292
+ if (kindFiltered.length === 0)
293
+ return [];
294
+ if (argCount === undefined)
295
+ return kindFiltered;
296
+ const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
297
+ if (!hasParameterMetadata)
298
+ return kindFiltered;
299
+ return kindFiltered.filter(candidate => candidate.parameterCount === undefined || candidate.parameterCount === argCount);
197
300
  };
301
+ const toResolveResult = (definition, tier) => ({
302
+ nodeId: definition.nodeId,
303
+ confidence: TIER_CONFIDENCE[tier],
304
+ reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global',
305
+ });
198
306
  /**
199
- * Resolve a function call to its target node ID using priority strategy:
200
- * A. Check imported files first (highest confidence)
201
- * B. Check local file definitions
202
- * C. Fuzzy global search (lowest confidence)
307
+ * Resolve a chain of intermediate method calls to find the receiver type for a
308
+ * final member call. Called when the receiver of a call is itself a call
309
+ * expression (e.g. `svc.getUser().save()`).
203
310
  *
204
- * Returns confidence score so agents know what to trust.
311
+ * @param chainNames Ordered list of method names from outermost to innermost
312
+ * intermediate call (e.g. ['getUser'] for `svc.getUser().save()`).
313
+ * @param baseReceiverTypeName The already-resolved type of the base receiver
314
+ * (e.g. 'UserService' for `svc`), or undefined.
315
+ * @param currentFile The file path for resolution context.
316
+ * @param ctx The resolution context for symbol lookup.
317
+ * @returns The type name of the final intermediate call's return type, or undefined
318
+ * if resolution fails at any step.
205
319
  */
206
- const resolveCallTarget = (calledName, currentFile, symbolTable, importMap) => {
207
- // Strategy B first (cheapest — single map lookup): Check local file
208
- const localNodeId = symbolTable.lookupExact(currentFile, calledName);
209
- if (localNodeId) {
210
- return { nodeId: localNodeId, confidence: 0.85, reason: 'same-file' };
320
+ function resolveChainedReceiver(chainNames, baseReceiverTypeName, currentFile, ctx) {
321
+ let currentType = baseReceiverTypeName;
322
+ for (const name of chainNames) {
323
+ const resolved = resolveCallTarget({ calledName: name, callForm: 'member', receiverTypeName: currentType }, currentFile, ctx);
324
+ if (!resolved)
325
+ return undefined;
326
+ const candidates = ctx.symbols.lookupFuzzy(name);
327
+ const symDef = candidates.find(c => c.nodeId === resolved.nodeId);
328
+ if (!symDef?.returnType)
329
+ return undefined;
330
+ const returnTypeName = extractReturnTypeName(symDef.returnType);
331
+ if (!returnTypeName)
332
+ return undefined;
333
+ currentType = returnTypeName;
211
334
  }
212
- // Strategy A: Check if any definition of calledName is in an imported file
213
- // Reversed: instead of iterating all imports and checking each, get all definitions
214
- // and check if any is imported. O(definitions) instead of O(imports).
215
- const allDefs = symbolTable.lookupFuzzy(calledName);
216
- if (allDefs.length > 0) {
217
- const importedFiles = importMap.get(currentFile);
218
- if (importedFiles) {
219
- for (const def of allDefs) {
220
- if (importedFiles.has(def.filePath)) {
221
- return { nodeId: def.nodeId, confidence: 0.9, reason: 'import-resolved' };
222
- }
335
+ return currentType;
336
+ }
337
+ /**
338
+ * Resolve a function call to its target node ID using priority strategy:
339
+ * A. Narrow candidates by scope tier via ctx.resolve()
340
+ * B. Filter to callable symbol kinds (constructor-aware when callForm is set)
341
+ * C. Apply arity filtering when parameter metadata is available
342
+ * D. Apply receiver-type filtering for member calls with typed receivers
343
+ *
344
+ * If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
345
+ */
346
+ const resolveCallTarget = (call, currentFile, ctx) => {
347
+ const tiered = ctx.resolve(call.calledName, currentFile);
348
+ if (!tiered)
349
+ return null;
350
+ const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
351
+ // D. Receiver-type filtering: for member calls with a known receiver type,
352
+ // resolve the type through the same tiered import infrastructure, then
353
+ // filter method candidates to the type's defining file. Fall back to
354
+ // fuzzy ownerId matching only when file-based narrowing is inconclusive.
355
+ //
356
+ // Applied regardless of candidate count — the sole same-file candidate may
357
+ // belong to the wrong class (e.g. super.save() should hit the parent's save,
358
+ // not the child's own save method in the same file).
359
+ if (call.callForm === 'member' && call.receiverTypeName) {
360
+ // D1. Resolve the receiver type
361
+ const typeResolved = ctx.resolve(call.receiverTypeName, currentFile);
362
+ if (typeResolved && typeResolved.candidates.length > 0) {
363
+ const typeNodeIds = new Set(typeResolved.candidates.map(d => d.nodeId));
364
+ const typeFiles = new Set(typeResolved.candidates.map(d => d.filePath));
365
+ // D2. Widen candidates: same-file tier may miss the parent's method when
366
+ // it lives in another file. Query the symbol table directly for all
367
+ // global methods with this name, then apply arity/kind filtering.
368
+ const methodPool = filteredCandidates.length <= 1
369
+ ? filterCallableCandidates(ctx.symbols.lookupFuzzy(call.calledName), call.argCount, call.callForm)
370
+ : filteredCandidates;
371
+ // D3. File-based: prefer candidates whose filePath matches the resolved type's file
372
+ const fileFiltered = methodPool.filter(c => typeFiles.has(c.filePath));
373
+ if (fileFiltered.length === 1) {
374
+ return toResolveResult(fileFiltered[0], tiered.tier);
375
+ }
376
+ // D4. ownerId fallback: narrow by ownerId matching the type's nodeId
377
+ const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
378
+ const ownerFiltered = pool.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
379
+ if (ownerFiltered.length === 1) {
380
+ return toResolveResult(ownerFiltered[0], tiered.tier);
223
381
  }
382
+ if (fileFiltered.length > 1 || ownerFiltered.length > 1)
383
+ return null;
224
384
  }
225
- // Strategy C: Fuzzy global (no import match found)
226
- const confidence = allDefs.length === 1 ? 0.5 : 0.3;
227
- return { nodeId: allDefs[0].nodeId, confidence, reason: 'fuzzy-global' };
228
385
  }
229
- return null;
386
+ if (filteredCandidates.length !== 1)
387
+ return null;
388
+ return toResolveResult(filteredCandidates[0], tiered.tier);
389
+ };
390
+ // ── Scope key helpers ────────────────────────────────────────────────────
391
+ // Scope keys use the format "funcName@startIndex" (produced by type-env.ts).
392
+ // Source IDs use "Label:filepath:funcName" (produced by parse-worker.ts).
393
+ // NUL (\0) is used as a composite-key separator because it cannot appear
394
+ // in source-code identifiers, preventing ambiguous concatenation.
395
+ //
396
+ // receiverKey stores the FULL scope (funcName@startIndex) to prevent
397
+ // collisions between overloaded methods with the same name in different
398
+ // classes (e.g. User.save@100 and Repo.save@200 are distinct keys).
399
+ // Lookup uses a secondary funcName-only index built in lookupReceiverType.
400
+ /** Extract the function name from a scope key ("funcName@startIndex" → "funcName"). */
401
+ const extractFuncNameFromScope = (scope) => scope.slice(0, scope.indexOf('@'));
402
+ /** Extract the trailing function name from a sourceId ("Function:filepath:funcName" → "funcName"). */
403
+ const extractFuncNameFromSourceId = (sourceId) => {
404
+ const lastColon = sourceId.lastIndexOf(':');
405
+ return lastColon >= 0 ? sourceId.slice(lastColon + 1) : '';
230
406
  };
231
407
  /**
232
- * Filter out common built-in functions and noise
233
- * that shouldn't be tracked as calls
408
+ * Build a composite key for receiver type storage.
409
+ * Uses the full scope string (e.g. "save@100") to distinguish overloaded
410
+ * methods with the same name in different classes.
234
411
  */
235
- /** Pre-built set (module-level singleton) to avoid re-creating per call */
236
- const BUILT_IN_NAMES = new Set([
237
- // JavaScript/TypeScript built-ins
238
- 'console', 'log', 'warn', 'error', 'info', 'debug',
239
- 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
240
- 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
241
- 'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
242
- 'JSON', 'parse', 'stringify',
243
- 'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt',
244
- 'Map', 'Set', 'WeakMap', 'WeakSet',
245
- 'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
246
- 'Math', 'Date', 'RegExp', 'Error',
247
- 'require', 'import', 'export',
248
- 'fetch', 'Response', 'Request',
249
- // React hooks and common functions
250
- 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext',
251
- 'useReducer', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue',
252
- 'createElement', 'createContext', 'createRef', 'forwardRef', 'memo', 'lazy',
253
- // Common array/object methods
254
- 'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', 'some', 'every',
255
- 'includes', 'indexOf', 'slice', 'splice', 'concat', 'join', 'split',
256
- 'push', 'pop', 'shift', 'unshift', 'sort', 'reverse',
257
- 'keys', 'values', 'entries', 'assign', 'freeze', 'seal',
258
- 'hasOwnProperty', 'toString', 'valueOf',
259
- // Python built-ins
260
- 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
261
- 'open', 'read', 'write', 'close', 'append', 'extend', 'update',
262
- 'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
263
- 'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
264
- // C/C++ standard library and common kernel helpers
265
- 'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
266
- 'scanf', 'fscanf', 'sscanf',
267
- 'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp',
268
- 'strlen', 'strcpy', 'strncpy', 'strcat', 'strncat', 'strcmp', 'strncmp', 'strstr', 'strchr', 'strrchr',
269
- 'atoi', 'atol', 'atof', 'strtol', 'strtoul', 'strtoll', 'strtoull', 'strtod',
270
- 'sizeof', 'offsetof', 'typeof',
271
- 'assert', 'abort', 'exit', '_exit',
272
- 'fopen', 'fclose', 'fread', 'fwrite', 'fseek', 'ftell', 'rewind', 'fflush', 'fgets', 'fputs',
273
- // Linux kernel common macros/helpers (not real call targets)
274
- 'likely', 'unlikely', 'BUG', 'BUG_ON', 'WARN', 'WARN_ON', 'WARN_ONCE',
275
- 'IS_ERR', 'PTR_ERR', 'ERR_PTR', 'IS_ERR_OR_NULL',
276
- 'ARRAY_SIZE', 'container_of', 'list_for_each_entry', 'list_for_each_entry_safe',
277
- 'min', 'max', 'clamp', 'abs', 'swap',
278
- 'pr_info', 'pr_warn', 'pr_err', 'pr_debug', 'pr_notice', 'pr_crit', 'pr_emerg',
279
- 'printk', 'dev_info', 'dev_warn', 'dev_err', 'dev_dbg',
280
- 'GFP_KERNEL', 'GFP_ATOMIC',
281
- 'spin_lock', 'spin_unlock', 'spin_lock_irqsave', 'spin_unlock_irqrestore',
282
- 'mutex_lock', 'mutex_unlock', 'mutex_init',
283
- 'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree',
284
- 'get', 'put',
285
- ]);
286
- const isBuiltInOrNoise = (name) => BUILT_IN_NAMES.has(name);
412
+ const receiverKey = (scope, varName) => `${scope}\0${varName}`;
413
+ /**
414
+ * Look up a receiver type from a verified receiver map.
415
+ * The map is keyed by `scope\0varName` (full scope with @startIndex).
416
+ * Since the lookup side only has `funcName` (no startIndex), we scan for
417
+ * all entries whose key starts with `funcName@` and has the matching varName.
418
+ * If exactly one unique type is found, return it. If multiple distinct types
419
+ * exist (true overload collision), return undefined (refuse to guess).
420
+ * Falls back to the file-level scope key `\0varName` (empty funcName).
421
+ */
422
+ const lookupReceiverType = (map, funcName, varName) => {
423
+ // Fast path: file-level scope (empty funcName — used as fallback)
424
+ const fileLevelKey = receiverKey('', varName);
425
+ const prefix = `${funcName}@`;
426
+ const suffix = `\0${varName}`;
427
+ let found;
428
+ let ambiguous = false;
429
+ for (const [key, value] of map) {
430
+ if (key === fileLevelKey)
431
+ continue; // handled separately below
432
+ if (key.startsWith(prefix) && key.endsWith(suffix)) {
433
+ // Verify the key is exactly "funcName@<digits>\0varName" with no extra chars.
434
+ // The part between prefix and suffix should be the startIndex (digits only),
435
+ // but we accept any non-empty segment to be forward-compatible.
436
+ const middle = key.slice(prefix.length, key.length - suffix.length);
437
+ if (middle.length === 0)
438
+ continue; // malformed key skip
439
+ if (found === undefined) {
440
+ found = value;
441
+ }
442
+ else if (found !== value) {
443
+ ambiguous = true;
444
+ break;
445
+ }
446
+ }
447
+ }
448
+ if (!ambiguous && found !== undefined)
449
+ return found;
450
+ // Fallback: file-level scope (bindings outside any function)
451
+ return map.get(fileLevelKey);
452
+ };
287
453
  /**
288
454
  * Fast path: resolve pre-extracted call sites from workers.
289
455
  * No AST parsing — workers already extracted calledName + sourceId.
290
- * This function only does symbol table lookups + graph mutations.
291
456
  */
292
- export const processCallsFromExtracted = async (graph, extractedCalls, symbolTable, importMap, onProgress) => {
293
- // Group by file for progress reporting
457
+ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
458
+ // Scope-aware receiver types: keyed by filePath "funcName\0varName" → typeName.
459
+ // The scope dimension prevents collisions when two functions in the same file
460
+ // have same-named locals pointing to different constructor types.
461
+ const fileReceiverTypes = new Map();
462
+ if (constructorBindings) {
463
+ for (const { filePath, bindings } of constructorBindings) {
464
+ const verified = verifyConstructorBindings(bindings, filePath, ctx, graph);
465
+ if (verified.size > 0) {
466
+ fileReceiverTypes.set(filePath, verified);
467
+ }
468
+ }
469
+ }
294
470
  const byFile = new Map();
295
471
  for (const call of extractedCalls) {
296
472
  let list = byFile.get(call.filePath);
@@ -302,26 +478,112 @@ export const processCallsFromExtracted = async (graph, extractedCalls, symbolTab
302
478
  }
303
479
  const totalFiles = byFile.size;
304
480
  let filesProcessed = 0;
305
- for (const [_filePath, calls] of byFile) {
481
+ for (const [filePath, calls] of byFile) {
306
482
  filesProcessed++;
307
483
  if (filesProcessed % 100 === 0) {
308
484
  onProgress?.(filesProcessed, totalFiles);
309
485
  await yieldToEventLoop();
310
486
  }
487
+ ctx.enableCache(filePath);
488
+ const receiverMap = fileReceiverTypes.get(filePath);
311
489
  for (const call of calls) {
312
- const resolved = resolveCallTarget(call.calledName, call.filePath, symbolTable, importMap);
490
+ let effectiveCall = call;
491
+ // Step 1: resolve receiver type from constructor bindings
492
+ if (!call.receiverTypeName && call.receiverName && receiverMap) {
493
+ const callFuncName = extractFuncNameFromSourceId(call.sourceId);
494
+ const resolvedType = lookupReceiverType(receiverMap, callFuncName, call.receiverName);
495
+ if (resolvedType) {
496
+ effectiveCall = { ...call, receiverTypeName: resolvedType };
497
+ }
498
+ }
499
+ // Step 1b: class-as-receiver for static method calls (e.g. UserService.find_user())
500
+ if (!effectiveCall.receiverTypeName && effectiveCall.receiverName && effectiveCall.callForm === 'member') {
501
+ const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath);
502
+ if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
503
+ effectiveCall = { ...effectiveCall, receiverTypeName: effectiveCall.receiverName };
504
+ }
505
+ }
506
+ // Step 2: if the call has a receiver call chain (e.g. svc.getUser().save()),
507
+ // resolve the chain to determine the final receiver type.
508
+ // This runs whenever receiverCallChain is present — even when Step 1 set a
509
+ // receiverTypeName, that type is the BASE receiver (e.g. UserService for svc),
510
+ // and the chain must be walked to produce the FINAL receiver (e.g. User from
511
+ // getUser() : User).
512
+ if (effectiveCall.receiverCallChain?.length) {
513
+ // Step 1 may have resolved the base receiver type (e.g. svc → UserService).
514
+ // Use it as the starting point for chain resolution.
515
+ let baseType = effectiveCall.receiverTypeName;
516
+ // If Step 1 didn't resolve it, try the receiver map directly.
517
+ if (!baseType && effectiveCall.receiverName && receiverMap) {
518
+ const callFuncName = extractFuncNameFromSourceId(effectiveCall.sourceId);
519
+ baseType = lookupReceiverType(receiverMap, callFuncName, effectiveCall.receiverName);
520
+ }
521
+ const chainedType = resolveChainedReceiver(effectiveCall.receiverCallChain, baseType, effectiveCall.filePath, ctx);
522
+ if (chainedType) {
523
+ effectiveCall = { ...effectiveCall, receiverTypeName: chainedType };
524
+ }
525
+ }
526
+ const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
313
527
  if (!resolved)
314
528
  continue;
315
- const relId = generateId('CALLS', `${call.sourceId}:${call.calledName}->${resolved.nodeId}`);
529
+ const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
316
530
  graph.addRelationship({
317
531
  id: relId,
318
- sourceId: call.sourceId,
532
+ sourceId: effectiveCall.sourceId,
319
533
  targetId: resolved.nodeId,
320
534
  type: 'CALLS',
321
535
  confidence: resolved.confidence,
322
536
  reason: resolved.reason,
323
537
  });
324
538
  }
539
+ ctx.clearCache();
325
540
  }
326
541
  onProgress?.(totalFiles, totalFiles);
327
542
  };
543
+ /**
544
+ * Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
545
+ */
546
+ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, onProgress) => {
547
+ for (let i = 0; i < extractedRoutes.length; i++) {
548
+ const route = extractedRoutes[i];
549
+ if (i % 50 === 0) {
550
+ onProgress?.(i, extractedRoutes.length);
551
+ await yieldToEventLoop();
552
+ }
553
+ if (!route.controllerName || !route.methodName)
554
+ continue;
555
+ const controllerResolved = ctx.resolve(route.controllerName, route.filePath);
556
+ if (!controllerResolved || controllerResolved.candidates.length === 0)
557
+ continue;
558
+ if (controllerResolved.tier === 'global' && controllerResolved.candidates.length > 1)
559
+ continue;
560
+ const controllerDef = controllerResolved.candidates[0];
561
+ const confidence = TIER_CONFIDENCE[controllerResolved.tier];
562
+ const methodResolved = ctx.resolve(route.methodName, controllerDef.filePath);
563
+ const methodId = methodResolved?.tier === 'same-file' ? methodResolved.candidates[0]?.nodeId : undefined;
564
+ const sourceId = generateId('File', route.filePath);
565
+ if (!methodId) {
566
+ const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
567
+ const relId = generateId('CALLS', `${sourceId}:route->${guessedId}`);
568
+ graph.addRelationship({
569
+ id: relId,
570
+ sourceId,
571
+ targetId: guessedId,
572
+ type: 'CALLS',
573
+ confidence: confidence * 0.8,
574
+ reason: 'laravel-route',
575
+ });
576
+ continue;
577
+ }
578
+ const relId = generateId('CALLS', `${sourceId}:route->${methodId}`);
579
+ graph.addRelationship({
580
+ id: relId,
581
+ sourceId,
582
+ targetId: methodId,
583
+ type: 'CALLS',
584
+ confidence,
585
+ reason: 'laravel-route',
586
+ });
587
+ }
588
+ onProgress?.(extractedRoutes.length, extractedRoutes.length);
589
+ };