gitnexus 1.4.1 → 1.4.6

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 (169) hide show
  1. package/README.md +215 -194
  2. package/dist/cli/ai-context.d.ts +2 -1
  3. package/dist/cli/ai-context.js +117 -90
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +57 -30
  6. package/dist/cli/augment.js +1 -1
  7. package/dist/cli/eval-server.d.ts +1 -1
  8. package/dist/cli/eval-server.js +14 -6
  9. package/dist/cli/index.js +18 -25
  10. package/dist/cli/lazy-action.d.ts +6 -0
  11. package/dist/cli/lazy-action.js +18 -0
  12. package/dist/cli/mcp.js +1 -1
  13. package/dist/cli/setup.js +42 -32
  14. package/dist/cli/skill-gen.d.ts +26 -0
  15. package/dist/cli/skill-gen.js +549 -0
  16. package/dist/cli/status.js +13 -4
  17. package/dist/cli/tool.d.ts +3 -2
  18. package/dist/cli/tool.js +48 -13
  19. package/dist/cli/wiki.js +2 -2
  20. package/dist/config/ignore-service.d.ts +25 -0
  21. package/dist/config/ignore-service.js +76 -0
  22. package/dist/config/supported-languages.d.ts +1 -0
  23. package/dist/config/supported-languages.js +1 -1
  24. package/dist/core/augmentation/engine.js +99 -72
  25. package/dist/core/embeddings/embedder.d.ts +1 -1
  26. package/dist/core/embeddings/embedder.js +1 -1
  27. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  28. package/dist/core/embeddings/embedding-pipeline.js +74 -47
  29. package/dist/core/embeddings/types.d.ts +1 -1
  30. package/dist/core/graph/types.d.ts +5 -2
  31. package/dist/core/ingestion/ast-cache.js +3 -2
  32. package/dist/core/ingestion/call-processor.d.ts +5 -7
  33. package/dist/core/ingestion/call-processor.js +430 -283
  34. package/dist/core/ingestion/call-routing.d.ts +53 -0
  35. package/dist/core/ingestion/call-routing.js +108 -0
  36. package/dist/core/ingestion/cluster-enricher.js +16 -16
  37. package/dist/core/ingestion/constants.d.ts +16 -0
  38. package/dist/core/ingestion/constants.js +16 -0
  39. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  40. package/dist/core/ingestion/entry-point-scoring.js +94 -24
  41. package/dist/core/ingestion/export-detection.d.ts +18 -0
  42. package/dist/core/ingestion/export-detection.js +231 -0
  43. package/dist/core/ingestion/filesystem-walker.js +4 -3
  44. package/dist/core/ingestion/framework-detection.d.ts +5 -1
  45. package/dist/core/ingestion/framework-detection.js +48 -8
  46. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  47. package/dist/core/ingestion/heritage-processor.js +109 -55
  48. package/dist/core/ingestion/import-processor.d.ts +16 -20
  49. package/dist/core/ingestion/import-processor.js +202 -696
  50. package/dist/core/ingestion/language-config.d.ts +46 -0
  51. package/dist/core/ingestion/language-config.js +167 -0
  52. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  53. package/dist/core/ingestion/mro-processor.js +369 -0
  54. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  55. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  56. package/dist/core/ingestion/parsing-processor.d.ts +3 -11
  57. package/dist/core/ingestion/parsing-processor.js +85 -181
  58. package/dist/core/ingestion/pipeline.d.ts +5 -1
  59. package/dist/core/ingestion/pipeline.js +192 -116
  60. package/dist/core/ingestion/process-processor.js +2 -1
  61. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  62. package/dist/core/ingestion/resolution-context.js +132 -0
  63. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  64. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  65. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  66. package/dist/core/ingestion/resolvers/go.js +42 -0
  67. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  68. package/dist/core/ingestion/resolvers/index.js +13 -0
  69. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  70. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  71. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  72. package/dist/core/ingestion/resolvers/php.js +35 -0
  73. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  74. package/dist/core/ingestion/resolvers/python.js +52 -0
  75. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  76. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  77. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  78. package/dist/core/ingestion/resolvers/rust.js +73 -0
  79. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  80. package/dist/core/ingestion/resolvers/standard.js +123 -0
  81. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  82. package/dist/core/ingestion/resolvers/utils.js +122 -0
  83. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  84. package/dist/core/ingestion/symbol-table.js +40 -12
  85. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -11
  86. package/dist/core/ingestion/tree-sitter-queries.js +642 -485
  87. package/dist/core/ingestion/type-env.d.ts +49 -0
  88. package/dist/core/ingestion/type-env.js +611 -0
  89. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  90. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  91. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  92. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  93. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  94. package/dist/core/ingestion/type-extractors/go.js +467 -0
  95. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  96. package/dist/core/ingestion/type-extractors/index.js +31 -0
  97. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  98. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  99. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  100. package/dist/core/ingestion/type-extractors/php.js +549 -0
  101. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  102. package/dist/core/ingestion/type-extractors/python.js +406 -0
  103. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  104. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  105. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  106. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  107. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  108. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  109. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  110. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  111. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  112. package/dist/core/ingestion/type-extractors/types.js +1 -0
  113. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  114. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  115. package/dist/core/ingestion/utils.d.ts +98 -0
  116. package/dist/core/ingestion/utils.js +1064 -9
  117. package/dist/core/ingestion/workers/parse-worker.d.ts +38 -4
  118. package/dist/core/ingestion/workers/parse-worker.js +251 -359
  119. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  120. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
  121. package/dist/core/{kuzu → lbug}/csv-generator.js +20 -4
  122. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
  123. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +82 -82
  124. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  125. package/dist/core/{kuzu → lbug}/schema.js +304 -289
  126. package/dist/core/search/bm25-index.d.ts +4 -4
  127. package/dist/core/search/bm25-index.js +17 -16
  128. package/dist/core/search/hybrid-search.d.ts +2 -2
  129. package/dist/core/search/hybrid-search.js +9 -9
  130. package/dist/core/tree-sitter/parser-loader.js +9 -2
  131. package/dist/core/wiki/generator.d.ts +4 -52
  132. package/dist/core/wiki/generator.js +53 -552
  133. package/dist/core/wiki/graph-queries.d.ts +4 -46
  134. package/dist/core/wiki/graph-queries.js +103 -282
  135. package/dist/core/wiki/html-viewer.js +192 -192
  136. package/dist/core/wiki/llm-client.js +11 -73
  137. package/dist/core/wiki/prompts.d.ts +8 -52
  138. package/dist/core/wiki/prompts.js +86 -200
  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} +7 -9
  142. package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +77 -79
  143. package/dist/mcp/local/local-backend.d.ts +7 -6
  144. package/dist/mcp/local/local-backend.js +176 -147
  145. package/dist/mcp/resources.js +42 -42
  146. package/dist/mcp/server.js +18 -19
  147. package/dist/mcp/tools.js +103 -104
  148. package/dist/server/api.js +12 -12
  149. package/dist/server/mcp-http.d.ts +1 -1
  150. package/dist/server/mcp-http.js +1 -1
  151. package/dist/storage/repo-manager.d.ts +20 -2
  152. package/dist/storage/repo-manager.js +55 -1
  153. package/dist/types/pipeline.d.ts +1 -1
  154. package/hooks/claude/gitnexus-hook.cjs +238 -155
  155. package/hooks/claude/pre-tool-use.sh +79 -79
  156. package/hooks/claude/session-start.sh +42 -42
  157. package/package.json +99 -96
  158. package/scripts/patch-tree-sitter-swift.cjs +74 -74
  159. package/skills/gitnexus-cli.md +82 -82
  160. package/skills/gitnexus-debugging.md +89 -89
  161. package/skills/gitnexus-exploring.md +78 -78
  162. package/skills/gitnexus-guide.md +64 -64
  163. package/skills/gitnexus-impact-analysis.md +97 -97
  164. package/skills/gitnexus-pr-review.md +163 -163
  165. package/skills/gitnexus-refactoring.md +121 -121
  166. package/vendor/leiden/index.cjs +355 -355
  167. package/vendor/leiden/utils.cjs +392 -392
  168. package/dist/core/wiki/diagrams.d.ts +0 -27
  169. package/dist/core/wiki/diagrams.js +0 -163
@@ -1,161 +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
- // Kotlin (function_declaration already included above via JS/TS)
35
- 'anonymous_function',
36
- 'lambda_literal',
37
- // PHP — no additional node types needed
38
- // Swift
39
- 'init_declaration',
40
- 'deinit_declaration',
41
- ]);
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';
42
11
  /**
43
12
  * Walk up the AST from a node to find the enclosing function/method.
44
13
  * Returns null if the call is at module/file level (top-level code).
45
14
  */
46
- const findEnclosingFunction = (node, filePath, symbolTable) => {
15
+ const findEnclosingFunction = (node, filePath, ctx) => {
47
16
  let current = node.parent;
48
17
  while (current) {
49
18
  if (FUNCTION_NODE_TYPES.has(current.type)) {
50
- // Found enclosing function - try to get its name
51
- let funcName = null;
52
- let label = 'Function';
53
- // Different node types have different name locations
54
- // Swift init/deinit — handle before generic cases (more specific)
55
- if (current.type === 'init_declaration' || current.type === 'deinit_declaration') {
56
- const funcName = current.type === 'init_declaration' ? 'init' : 'deinit';
57
- const startLine = current.startPosition?.row ?? 0;
58
- return generateId('Constructor', `${filePath}:${funcName}:${startLine}`);
59
- }
60
- if (current.type === 'function_declaration' ||
61
- current.type === 'function_definition' ||
62
- current.type === 'async_function_declaration' ||
63
- current.type === 'generator_function_declaration' ||
64
- current.type === 'function_item') { // Rust function
65
- // Named function: function foo() {}
66
- const nameNode = current.childForFieldName?.('name') ||
67
- current.children?.find((c) => c.type === 'identifier' || c.type === 'property_identifier');
68
- funcName = nameNode?.text;
69
- }
70
- else if (current.type === 'impl_item') {
71
- // Rust method inside impl block: wrapper around function_item or const_item
72
- // We need to look inside for the function_item
73
- const funcItem = current.children?.find((c) => c.type === 'function_item');
74
- if (funcItem) {
75
- const nameNode = funcItem.childForFieldName?.('name') ||
76
- funcItem.children?.find((c) => c.type === 'identifier');
77
- funcName = nameNode?.text;
78
- 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;
79
24
  }
25
+ return generateId(label, `${filePath}:${funcName}`);
80
26
  }
81
- else if (current.type === 'method_definition') {
82
- // Method: foo() {} inside class (JS/TS)
83
- const nameNode = current.childForFieldName?.('name') ||
84
- current.children?.find((c) => c.type === 'property_identifier');
85
- funcName = nameNode?.text;
86
- label = 'Method';
87
- }
88
- else if (current.type === 'method_declaration') {
89
- // Java method: public void foo() {}
90
- const nameNode = current.childForFieldName?.('name') ||
91
- current.children?.find((c) => c.type === 'identifier');
92
- funcName = nameNode?.text;
93
- label = 'Method';
94
- }
95
- else if (current.type === 'constructor_declaration') {
96
- // Java constructor: public ClassName() {}
97
- const nameNode = current.childForFieldName?.('name') ||
98
- current.children?.find((c) => c.type === 'identifier');
99
- funcName = nameNode?.text;
100
- label = 'Method'; // Treat constructors as methods for process detection
101
- }
102
- else if (current.type === 'arrow_function' || current.type === 'function_expression') {
103
- // Arrow/expression: const foo = () => {} - check parent variable declarator
104
- const parent = current.parent;
105
- if (parent?.type === 'variable_declarator') {
106
- const nameNode = parent.childForFieldName?.('name') ||
107
- parent.children?.find((c) => c.type === 'identifier');
108
- 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
+ }
109
69
  }
110
70
  }
111
- if (funcName) {
112
- // Look up the function in symbol table to get its node ID
113
- // Try exact match first
114
- const nodeId = symbolTable.lookupExact(filePath, funcName);
115
- if (nodeId)
116
- return nodeId;
117
- // Try construct ID manually if lookup fails (common for non-exported internal functions)
118
- // Format must match parsing-processor: "Label:path/to/file:funcName:startLine"
119
- const startLine = current.startPosition?.row ?? 0;
120
- return generateId(label, `${filePath}:${funcName}:${startLine}`);
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
+ }
121
76
  }
122
- // Couldn't determine function name - try parent (might be nested)
123
77
  }
124
- current = current.parent;
125
78
  }
126
- return null; // Top-level call (not inside any function)
79
+ return verified;
127
80
  };
128
- export const processCalls = async (graph, files, astCache, symbolTable, importMap, onProgress) => {
81
+ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
129
82
  const parser = await loadParser();
83
+ const collectedHeritage = [];
84
+ const logSkipped = isVerboseIngestionEnabled();
85
+ const skippedByLang = logSkipped ? new Map() : null;
130
86
  for (let i = 0; i < files.length; i++) {
131
87
  const file = files[i];
132
88
  onProgress?.(i + 1, files.length);
133
89
  if (i % 20 === 0)
134
90
  await yieldToEventLoop();
135
- // 1. Check language support first
136
91
  const language = getLanguageFromFilename(file.path);
137
92
  if (!language)
138
93
  continue;
94
+ if (!isLanguageAvailable(language)) {
95
+ if (skippedByLang) {
96
+ skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
97
+ }
98
+ continue;
99
+ }
139
100
  const queryStr = LANGUAGE_QUERIES[language];
140
101
  if (!queryStr)
141
102
  continue;
142
- // 2. ALWAYS load the language before querying (parser is stateful)
143
103
  await loadLanguage(language, file.path);
144
- // 3. Get AST (Try Cache First)
145
104
  let tree = astCache.get(file.path);
146
- let wasReparsed = false;
147
105
  if (!tree) {
148
- // Cache Miss: Re-parse
149
- // Use larger bufferSize for files > 32KB
150
106
  try {
151
- tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
107
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
152
108
  }
153
109
  catch (parseError) {
154
- // Skip files that can't be parsed
155
110
  continue;
156
111
  }
157
- wasReparsed = true;
158
- // Cache re-parsed tree so heritage phase gets hits
159
112
  astCache.set(file.path, tree);
160
113
  }
161
114
  let query;
@@ -169,28 +122,130 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
169
122
  console.warn(`Query error for ${file.path}:`, queryError);
170
123
  continue;
171
124
  }
172
- // 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);
173
132
  matches.forEach(match => {
174
133
  const captureMap = {};
175
134
  match.captures.forEach(c => captureMap[c.name] = c.node);
176
- // Only process @call captures
177
135
  if (!captureMap['call'])
178
136
  return;
179
137
  const nameNode = captureMap['call.name'];
180
138
  if (!nameNode)
181
139
  return;
182
140
  const calledName = nameNode.text;
183
- // 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
+ }
184
192
  if (isBuiltInOrNoise(calledName))
185
193
  return;
186
- // 4. Resolve the target using priority strategy (returns confidence)
187
- 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);
188
246
  if (!resolved)
189
247
  return;
190
- // 5. Find the enclosing function (caller)
191
- const callNode = captureMap['call'];
192
- const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
193
- // Use enclosing function as source, fallback to file for top-level calls
248
+ const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
194
249
  const sourceId = enclosingFuncId || generateId('File', file.path);
195
250
  const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
196
251
  graph.addRelationship({
@@ -202,152 +257,216 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
202
257
  reason: resolved.reason,
203
258
  });
204
259
  });
205
- // Tree is now owned by the LRU cache — no manual delete needed
260
+ ctx.clearCache();
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));
206
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);
207
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
+ });
208
306
  /**
209
- * Resolve a function call to its target node ID using priority strategy:
210
- * A. Check imported files first (highest confidence)
211
- * B. Check local file definitions
212
- * 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()`).
213
310
  *
214
- * 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.
215
319
  */
216
- const resolveCallTarget = (calledName, currentFile, symbolTable, importMap) => {
217
- // Strategy B first (cheapest — single map lookup): Check local file
218
- const localNodeId = symbolTable.lookupExact(currentFile, calledName);
219
- if (localNodeId) {
220
- 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;
221
334
  }
222
- // Strategy A: Check if any definition of calledName is in an imported file
223
- // Reversed: instead of iterating all imports and checking each, get all definitions
224
- // and check if any is imported. O(definitions) instead of O(imports).
225
- const allDefs = symbolTable.lookupFuzzy(calledName);
226
- if (allDefs.length > 0) {
227
- const importedFiles = importMap.get(currentFile);
228
- if (importedFiles) {
229
- for (const def of allDefs) {
230
- if (importedFiles.has(def.filePath)) {
231
- return { nodeId: def.nodeId, confidence: 0.9, reason: 'import-resolved' };
232
- }
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);
233
381
  }
382
+ if (fileFiltered.length > 1 || ownerFiltered.length > 1)
383
+ return null;
234
384
  }
235
- // Strategy C: Fuzzy global (no import match found)
236
- const confidence = allDefs.length === 1 ? 0.5 : 0.3;
237
- return { nodeId: allDefs[0].nodeId, confidence, reason: 'fuzzy-global' };
238
385
  }
239
- 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) : '';
240
406
  };
241
407
  /**
242
- * Filter out common built-in functions and noise
243
- * 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.
244
411
  */
245
- /** Pre-built set (module-level singleton) to avoid re-creating per call */
246
- const BUILT_IN_NAMES = new Set([
247
- // JavaScript/TypeScript built-ins
248
- 'console', 'log', 'warn', 'error', 'info', 'debug',
249
- 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
250
- 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
251
- 'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
252
- 'JSON', 'parse', 'stringify',
253
- 'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt',
254
- 'Map', 'Set', 'WeakMap', 'WeakSet',
255
- 'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
256
- 'Math', 'Date', 'RegExp', 'Error',
257
- 'require', 'import', 'export',
258
- 'fetch', 'Response', 'Request',
259
- // React hooks and common functions
260
- 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext',
261
- 'useReducer', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue',
262
- 'createElement', 'createContext', 'createRef', 'forwardRef', 'memo', 'lazy',
263
- // Common array/object methods
264
- 'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', 'some', 'every',
265
- 'includes', 'indexOf', 'slice', 'splice', 'concat', 'join', 'split',
266
- 'push', 'pop', 'shift', 'unshift', 'sort', 'reverse',
267
- 'keys', 'values', 'entries', 'assign', 'freeze', 'seal',
268
- 'hasOwnProperty', 'toString', 'valueOf',
269
- // Python built-ins
270
- 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
271
- 'open', 'read', 'write', 'close', 'append', 'extend', 'update',
272
- 'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
273
- 'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
274
- // Kotlin stdlib (IMPORTANT: keep in sync with parse-worker.ts BUILT_IN_NAMES)
275
- 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error',
276
- 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf',
277
- 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless',
278
- 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet',
279
- 'repeat', 'synchronized',
280
- // Kotlin coroutine builders & scope functions
281
- 'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope',
282
- 'supervisorScope', 'delay',
283
- // Kotlin Flow operators
284
- 'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch',
285
- 'buffer', 'conflate', 'distinctUntilChanged',
286
- 'flatMapLatest', 'flatMapMerge', 'combine',
287
- 'stateIn', 'shareIn', 'launchIn',
288
- // Kotlin infix stdlib functions
289
- 'to', 'until', 'downTo', 'step',
290
- // C/C++ standard library and common kernel helpers
291
- 'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
292
- 'scanf', 'fscanf', 'sscanf',
293
- 'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp',
294
- 'strlen', 'strcpy', 'strncpy', 'strcat', 'strncat', 'strcmp', 'strncmp', 'strstr', 'strchr', 'strrchr',
295
- 'atoi', 'atol', 'atof', 'strtol', 'strtoul', 'strtoll', 'strtoull', 'strtod',
296
- 'sizeof', 'offsetof', 'typeof',
297
- 'assert', 'abort', 'exit', '_exit',
298
- 'fopen', 'fclose', 'fread', 'fwrite', 'fseek', 'ftell', 'rewind', 'fflush', 'fgets', 'fputs',
299
- // Linux kernel common macros/helpers (not real call targets)
300
- 'likely', 'unlikely', 'BUG', 'BUG_ON', 'WARN', 'WARN_ON', 'WARN_ONCE',
301
- 'IS_ERR', 'PTR_ERR', 'ERR_PTR', 'IS_ERR_OR_NULL',
302
- 'ARRAY_SIZE', 'container_of', 'list_for_each_entry', 'list_for_each_entry_safe',
303
- 'min', 'max', 'clamp', 'abs', 'swap',
304
- 'pr_info', 'pr_warn', 'pr_err', 'pr_debug', 'pr_notice', 'pr_crit', 'pr_emerg',
305
- 'printk', 'dev_info', 'dev_warn', 'dev_err', 'dev_dbg',
306
- 'GFP_KERNEL', 'GFP_ATOMIC',
307
- 'spin_lock', 'spin_unlock', 'spin_lock_irqsave', 'spin_unlock_irqrestore',
308
- 'mutex_lock', 'mutex_unlock', 'mutex_init',
309
- 'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree',
310
- 'get', 'put',
311
- // Swift/iOS built-ins and standard library
312
- 'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure',
313
- 'assert', 'assertionFailure', 'NSLog',
314
- 'abs', 'min', 'max', 'zip', 'stride', 'sequence', 'repeatElement',
315
- 'swap', 'withUnsafePointer', 'withUnsafeMutablePointer', 'withUnsafeBytes',
316
- 'autoreleasepool', 'unsafeBitCast', 'unsafeDowncast', 'numericCast',
317
- 'type', 'MemoryLayout',
318
- // Swift collection/string methods (common noise)
319
- 'map', 'flatMap', 'compactMap', 'filter', 'reduce', 'forEach', 'contains',
320
- 'first', 'last', 'prefix', 'suffix', 'dropFirst', 'dropLast',
321
- 'sorted', 'reversed', 'enumerated', 'joined', 'split',
322
- 'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast',
323
- 'isEmpty', 'count', 'index', 'startIndex', 'endIndex',
324
- // UIKit/Foundation common methods (noise in call graph)
325
- 'addSubview', 'removeFromSuperview', 'layoutSubviews', 'setNeedsLayout',
326
- 'layoutIfNeeded', 'setNeedsDisplay', 'invalidateIntrinsicContentSize',
327
- 'addTarget', 'removeTarget', 'addGestureRecognizer',
328
- 'addConstraint', 'addConstraints', 'removeConstraint', 'removeConstraints',
329
- 'NSLocalizedString', 'Bundle',
330
- 'reloadData', 'reloadSections', 'reloadRows', 'performBatchUpdates',
331
- 'register', 'dequeueReusableCell', 'dequeueReusableSupplementaryView',
332
- 'beginUpdates', 'endUpdates', 'insertRows', 'deleteRows', 'insertSections', 'deleteSections',
333
- 'present', 'dismiss', 'pushViewController', 'popViewController', 'popToRootViewController',
334
- 'performSegue', 'prepare',
335
- // GCD / async
336
- 'DispatchQueue', 'async', 'sync', 'asyncAfter',
337
- 'Task', 'withCheckedContinuation', 'withCheckedThrowingContinuation',
338
- // Combine
339
- 'sink', 'store', 'assign', 'receive', 'subscribe',
340
- // Notification / KVO
341
- 'addObserver', 'removeObserver', 'post', 'NotificationCenter',
342
- ]);
343
- 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
+ };
344
453
  /**
345
454
  * Fast path: resolve pre-extracted call sites from workers.
346
455
  * No AST parsing — workers already extracted calledName + sourceId.
347
- * This function only does symbol table lookups + graph mutations.
348
456
  */
349
- export const processCallsFromExtracted = async (graph, extractedCalls, symbolTable, importMap, onProgress) => {
350
- // 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
+ }
351
470
  const byFile = new Map();
352
471
  for (const call of extractedCalls) {
353
472
  let list = byFile.get(call.filePath);
@@ -359,33 +478,72 @@ export const processCallsFromExtracted = async (graph, extractedCalls, symbolTab
359
478
  }
360
479
  const totalFiles = byFile.size;
361
480
  let filesProcessed = 0;
362
- for (const [_filePath, calls] of byFile) {
481
+ for (const [filePath, calls] of byFile) {
363
482
  filesProcessed++;
364
483
  if (filesProcessed % 100 === 0) {
365
484
  onProgress?.(filesProcessed, totalFiles);
366
485
  await yieldToEventLoop();
367
486
  }
487
+ ctx.enableCache(filePath);
488
+ const receiverMap = fileReceiverTypes.get(filePath);
368
489
  for (const call of calls) {
369
- 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);
370
527
  if (!resolved)
371
528
  continue;
372
- const relId = generateId('CALLS', `${call.sourceId}:${call.calledName}->${resolved.nodeId}`);
529
+ const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
373
530
  graph.addRelationship({
374
531
  id: relId,
375
- sourceId: call.sourceId,
532
+ sourceId: effectiveCall.sourceId,
376
533
  targetId: resolved.nodeId,
377
534
  type: 'CALLS',
378
535
  confidence: resolved.confidence,
379
536
  reason: resolved.reason,
380
537
  });
381
538
  }
539
+ ctx.clearCache();
382
540
  }
383
541
  onProgress?.(totalFiles, totalFiles);
384
542
  };
385
543
  /**
386
544
  * Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
387
545
  */
388
- export const processRoutesFromExtracted = async (graph, extractedRoutes, symbolTable, importMap, onProgress) => {
546
+ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, onProgress) => {
389
547
  for (let i = 0; i < extractedRoutes.length; i++) {
390
548
  const route = extractedRoutes[i];
391
549
  if (i % 50 === 0) {
@@ -394,28 +552,17 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, symbolT
394
552
  }
395
553
  if (!route.controllerName || !route.methodName)
396
554
  continue;
397
- // Resolve controller class in symbol table
398
- const controllerDefs = symbolTable.lookupFuzzy(route.controllerName);
399
- if (controllerDefs.length === 0)
555
+ const controllerResolved = ctx.resolve(route.controllerName, route.filePath);
556
+ if (!controllerResolved || controllerResolved.candidates.length === 0)
400
557
  continue;
401
- // Prefer import-resolved match
402
- const importedFiles = importMap.get(route.filePath);
403
- let controllerDef = controllerDefs[0];
404
- let confidence = controllerDefs.length === 1 ? 0.7 : 0.5;
405
- if (importedFiles) {
406
- for (const def of controllerDefs) {
407
- if (importedFiles.has(def.filePath)) {
408
- controllerDef = def;
409
- confidence = 0.9;
410
- break;
411
- }
412
- }
413
- }
414
- // Find the method on the controller
415
- const methodId = symbolTable.lookupExact(controllerDef.filePath, route.methodName);
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;
416
564
  const sourceId = generateId('File', route.filePath);
417
565
  if (!methodId) {
418
- // Construct method ID manually
419
566
  const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
420
567
  const relId = generateId('CALLS', `${sourceId}:route->${guessedId}`);
421
568
  graph.addRelationship({