spec-gen-cli 1.2.2 → 1.2.4

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 (141) hide show
  1. package/README.md +272 -25
  2. package/dist/api/generate.d.ts.map +1 -1
  3. package/dist/api/generate.js +11 -7
  4. package/dist/api/generate.js.map +1 -1
  5. package/dist/api/run.d.ts.map +1 -1
  6. package/dist/api/run.js +5 -3
  7. package/dist/api/run.js.map +1 -1
  8. package/dist/api/types.d.ts +4 -4
  9. package/dist/api/types.d.ts.map +1 -1
  10. package/dist/cli/commands/analyze.d.ts.map +1 -1
  11. package/dist/cli/commands/analyze.js +101 -41
  12. package/dist/cli/commands/analyze.js.map +1 -1
  13. package/dist/cli/commands/generate.d.ts.map +1 -1
  14. package/dist/cli/commands/generate.js +28 -23
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/cli/commands/mcp.d.ts +353 -10
  17. package/dist/cli/commands/mcp.d.ts.map +1 -1
  18. package/dist/cli/commands/mcp.js +241 -48
  19. package/dist/cli/commands/mcp.js.map +1 -1
  20. package/dist/cli/commands/view.d.ts.map +1 -1
  21. package/dist/cli/commands/view.js +33 -4
  22. package/dist/cli/commands/view.js.map +1 -1
  23. package/dist/constants.d.ts +11 -0
  24. package/dist/constants.d.ts.map +1 -1
  25. package/dist/constants.js +11 -0
  26. package/dist/constants.js.map +1 -1
  27. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
  28. package/dist/core/analyzer/artifact-generator.js +11 -3
  29. package/dist/core/analyzer/artifact-generator.js.map +1 -1
  30. package/dist/core/analyzer/ast-chunker.d.ts +24 -0
  31. package/dist/core/analyzer/ast-chunker.d.ts.map +1 -0
  32. package/dist/core/analyzer/ast-chunker.js +198 -0
  33. package/dist/core/analyzer/ast-chunker.js.map +1 -0
  34. package/dist/core/analyzer/call-graph.d.ts +52 -5
  35. package/dist/core/analyzer/call-graph.d.ts.map +1 -1
  36. package/dist/core/analyzer/call-graph.js +769 -48
  37. package/dist/core/analyzer/call-graph.js.map +1 -1
  38. package/dist/core/analyzer/code-shaper.d.ts.map +1 -1
  39. package/dist/core/analyzer/code-shaper.js +5 -0
  40. package/dist/core/analyzer/code-shaper.js.map +1 -1
  41. package/dist/core/analyzer/codebase-digest.d.ts +40 -0
  42. package/dist/core/analyzer/codebase-digest.d.ts.map +1 -0
  43. package/dist/core/analyzer/codebase-digest.js +194 -0
  44. package/dist/core/analyzer/codebase-digest.js.map +1 -0
  45. package/dist/core/analyzer/cpp-header-resolver.d.ts +30 -0
  46. package/dist/core/analyzer/cpp-header-resolver.d.ts.map +1 -0
  47. package/dist/core/analyzer/cpp-header-resolver.js +71 -0
  48. package/dist/core/analyzer/cpp-header-resolver.js.map +1 -0
  49. package/dist/core/analyzer/dependency-graph.d.ts +19 -0
  50. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -1
  51. package/dist/core/analyzer/dependency-graph.js +76 -0
  52. package/dist/core/analyzer/dependency-graph.js.map +1 -1
  53. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -1
  54. package/dist/core/analyzer/duplicate-detector.js +7 -1
  55. package/dist/core/analyzer/duplicate-detector.js.map +1 -1
  56. package/dist/core/analyzer/function-registry-trie.d.ts +21 -0
  57. package/dist/core/analyzer/function-registry-trie.d.ts.map +1 -0
  58. package/dist/core/analyzer/function-registry-trie.js +39 -0
  59. package/dist/core/analyzer/function-registry-trie.js.map +1 -0
  60. package/dist/core/analyzer/import-resolver-bridge.d.ts +25 -0
  61. package/dist/core/analyzer/import-resolver-bridge.d.ts.map +1 -0
  62. package/dist/core/analyzer/import-resolver-bridge.js +99 -0
  63. package/dist/core/analyzer/import-resolver-bridge.js.map +1 -0
  64. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -1
  65. package/dist/core/analyzer/signature-extractor.js +131 -3
  66. package/dist/core/analyzer/signature-extractor.js.map +1 -1
  67. package/dist/core/analyzer/subgraph-extractor.d.ts +10 -2
  68. package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -1
  69. package/dist/core/analyzer/subgraph-extractor.js +25 -7
  70. package/dist/core/analyzer/subgraph-extractor.js.map +1 -1
  71. package/dist/core/analyzer/type-inference-engine.d.ts +23 -0
  72. package/dist/core/analyzer/type-inference-engine.d.ts.map +1 -0
  73. package/dist/core/analyzer/type-inference-engine.js +130 -0
  74. package/dist/core/analyzer/type-inference-engine.js.map +1 -0
  75. package/dist/core/analyzer/vector-index.d.ts +35 -6
  76. package/dist/core/analyzer/vector-index.d.ts.map +1 -1
  77. package/dist/core/analyzer/vector-index.js +308 -54
  78. package/dist/core/analyzer/vector-index.js.map +1 -1
  79. package/dist/core/generator/spec-pipeline.d.ts +31 -11
  80. package/dist/core/generator/spec-pipeline.d.ts.map +1 -1
  81. package/dist/core/generator/spec-pipeline.js +170 -39
  82. package/dist/core/generator/spec-pipeline.js.map +1 -1
  83. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -1
  84. package/dist/core/generator/stages/stage2-entities.js +2 -1
  85. package/dist/core/generator/stages/stage2-entities.js.map +1 -1
  86. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -1
  87. package/dist/core/generator/stages/stage3-services.js +2 -1
  88. package/dist/core/generator/stages/stage3-services.js.map +1 -1
  89. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -1
  90. package/dist/core/generator/stages/stage4-api.js +2 -1
  91. package/dist/core/generator/stages/stage4-api.js.map +1 -1
  92. package/dist/core/generator/stages/stage5-architecture.d.ts +2 -1
  93. package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -1
  94. package/dist/core/generator/stages/stage5-architecture.js +15 -3
  95. package/dist/core/generator/stages/stage5-architecture.js.map +1 -1
  96. package/dist/core/services/chat-agent.d.ts +5 -0
  97. package/dist/core/services/chat-agent.d.ts.map +1 -1
  98. package/dist/core/services/chat-agent.js +14 -0
  99. package/dist/core/services/chat-agent.js.map +1 -1
  100. package/dist/core/services/chat-tools.d.ts.map +1 -1
  101. package/dist/core/services/chat-tools.js +172 -50
  102. package/dist/core/services/chat-tools.js.map +1 -1
  103. package/dist/core/services/llm-service.d.ts +23 -1
  104. package/dist/core/services/llm-service.d.ts.map +1 -1
  105. package/dist/core/services/llm-service.js +94 -2
  106. package/dist/core/services/llm-service.js.map +1 -1
  107. package/dist/core/services/mcp-handlers/analysis.d.ts +12 -0
  108. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -1
  109. package/dist/core/services/mcp-handlers/analysis.js +138 -2
  110. package/dist/core/services/mcp-handlers/analysis.js.map +1 -1
  111. package/dist/core/services/mcp-handlers/graph.d.ts +21 -1
  112. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -1
  113. package/dist/core/services/mcp-handlers/graph.js +142 -2
  114. package/dist/core/services/mcp-handlers/graph.js.map +1 -1
  115. package/dist/core/services/mcp-handlers/orient.d.ts +17 -0
  116. package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -0
  117. package/dist/core/services/mcp-handlers/orient.js +200 -0
  118. package/dist/core/services/mcp-handlers/orient.js.map +1 -0
  119. package/dist/core/services/mcp-handlers/semantic.d.ts +18 -4
  120. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -1
  121. package/dist/core/services/mcp-handlers/semantic.js +161 -17
  122. package/dist/core/services/mcp-handlers/semantic.js.map +1 -1
  123. package/dist/core/services/mcp-handlers/utils.d.ts +43 -0
  124. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -1
  125. package/dist/core/services/mcp-handlers/utils.js +66 -1
  126. package/dist/core/services/mcp-handlers/utils.js.map +1 -1
  127. package/dist/core/services/mcp-watcher.d.ts +41 -0
  128. package/dist/core/services/mcp-watcher.d.ts.map +1 -0
  129. package/dist/core/services/mcp-watcher.js +177 -0
  130. package/dist/core/services/mcp-watcher.js.map +1 -0
  131. package/dist/types/index.d.ts +2 -2
  132. package/dist/types/index.d.ts.map +1 -1
  133. package/dist/types/pipeline.d.ts +7 -0
  134. package/dist/types/pipeline.d.ts.map +1 -1
  135. package/package.json +4 -2
  136. package/src/viewer/InteractiveGraphViewer.jsx +39 -10
  137. package/src/viewer/components/ChatPanel.jsx +8 -5
  138. package/src/viewer/components/ClassGraph.jsx +782 -0
  139. package/src/viewer/components/FlatGraph.jsx +3 -3
  140. package/src/viewer/utils/graph-helpers.js +9 -1
  141. package/src/viewer/utils/themes.js +36 -0
@@ -2,7 +2,7 @@
2
2
  * Call Graph Analyzer
3
3
  *
4
4
  * Performs static analysis of function calls across source files using tree-sitter.
5
- * Supports TypeScript/JavaScript, Python, Go, Rust, Ruby, Java — no LLM, pure AST.
5
+ * Supports TypeScript/JavaScript, Python, Go, Rust, Ruby, Java, Swift — no LLM, pure AST.
6
6
  *
7
7
  * Produces:
8
8
  * - FunctionNode[] — all identified functions/methods
@@ -12,6 +12,9 @@
12
12
  * - Layer violations — cross-layer calls in the wrong direction
13
13
  */
14
14
  import Parser from 'tree-sitter';
15
+ import { FunctionRegistryTrie } from './function-registry-trie.js';
16
+ import { inferTypesFromSource, resolveViaTypeInference } from './type-inference-engine.js';
17
+ import { extractAllHttpEdges } from './http-route-parser.js';
15
18
  // ============================================================================
16
19
  // CONSTANTS
17
20
  // ============================================================================
@@ -46,6 +49,11 @@ const IGNORED_CALLEES = new Set([
46
49
  'extend', 'attr_accessor', 'attr_reader', 'attr_writer',
47
50
  // Java common
48
51
  'toString', 'equals', 'hashCode', 'getClass', 'println', 'printf',
52
+ // Swift stdlib / builtins
53
+ 'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure',
54
+ 'assert', 'assertionFailure', 'withUnsafePointer', 'withUnsafeMutablePointer',
55
+ 'DispatchQueue', 'main', 'async', 'sync', 'append', 'remove', 'insert', 'contains',
56
+ 'map', 'filter', 'reduce', 'forEach', 'compactMap', 'flatMap', 'sorted', 'first', 'last',
49
57
  // C++ stdlib / builtins
50
58
  'cout', 'cin', 'cerr', 'endl', 'malloc', 'free', 'memcpy', 'memset', 'memcmp',
51
59
  'strlen', 'strcpy', 'strcat', 'strcmp', 'sprintf', 'snprintf', 'fprintf',
@@ -54,6 +62,15 @@ const IGNORED_CALLEES = new Set([
54
62
  'make_shared', 'make_unique', 'move', 'forward', 'swap',
55
63
  'static_cast', 'dynamic_cast', 'reinterpret_cast', 'const_cast',
56
64
  ]);
65
+ /** Returns true if the name should be skipped as a call target. */
66
+ function isIgnoredCallee(name) {
67
+ if (IGNORED_CALLEES.has(name))
68
+ return true;
69
+ // ALL_CAPS names (3+ chars) are almost certainly C/C++ macros, not functions
70
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(name))
71
+ return true;
72
+ return false;
73
+ }
57
74
  // ============================================================================
58
75
  // PARSER SINGLETONS (lazy init)
59
76
  // ============================================================================
@@ -64,6 +81,7 @@ let _rustParser;
64
81
  let _rubyParser;
65
82
  let _javaParser;
66
83
  let _cppParser;
84
+ let _swiftParser;
67
85
  let _TsLanguage;
68
86
  let _PyLanguage;
69
87
  let _GoLanguage;
@@ -71,6 +89,7 @@ let _RustLanguage;
71
89
  let _RubyLanguage;
72
90
  let _JavaLanguage;
73
91
  let _CppLanguage;
92
+ let _SwiftLanguage;
74
93
  async function getTSParser() {
75
94
  if (!_tsParser) {
76
95
  const tsModule = await import('tree-sitter-typescript');
@@ -134,6 +153,15 @@ async function getCppParser() {
134
153
  }
135
154
  return { parser: _cppParser, lang: _CppLanguage };
136
155
  }
156
+ async function getSwiftParser() {
157
+ if (!_swiftParser) {
158
+ const swiftModule = await import('tree-sitter-swift');
159
+ _SwiftLanguage = swiftModule.default;
160
+ _swiftParser = new Parser();
161
+ _swiftParser.setLanguage(_SwiftLanguage);
162
+ }
163
+ return { parser: _swiftParser, lang: _SwiftLanguage };
164
+ }
137
165
  // ============================================================================
138
166
  // ATTRIBUTION HELPER
139
167
  // ============================================================================
@@ -156,6 +184,200 @@ function findEnclosingFunction(nodes, callPos) {
156
184
  return best;
157
185
  }
158
186
  // ============================================================================
187
+ // DOCSTRING / SIGNATURE EXTRACTION HELPERS
188
+ // ============================================================================
189
+ /**
190
+ * Scan backward from `startIndex` in `source` to find the doc comment
191
+ * immediately preceding the function declaration. Skip blank lines.
192
+ *
193
+ * For Python, docstrings are INSIDE the function body — scan forward from
194
+ * `startIndex` past the `def name(...):` colon to find the triple-quoted string.
195
+ *
196
+ * Returns the first meaningful (non-empty, non-decorator) line of the comment.
197
+ */
198
+ function extractDocstringBefore(source, startIndex, language) {
199
+ // ── Python: scan forward past the colon into the function body ──────────
200
+ if (language === 'Python') {
201
+ // Find the colon that ends the `def` line
202
+ let i = startIndex;
203
+ while (i < source.length && source[i] !== ':')
204
+ i++;
205
+ // Skip past the colon
206
+ i++;
207
+ // Skip whitespace / newline
208
+ while (i < source.length && (source[i] === ' ' || source[i] === '\t' || source[i] === '\n' || source[i] === '\r'))
209
+ i++;
210
+ // Check for triple-quoted docstring
211
+ const tripleDouble = source.startsWith('"""', i);
212
+ const tripleSingle = source.startsWith("'''", i);
213
+ if (tripleDouble || tripleSingle) {
214
+ const quote = tripleDouble ? '"""' : "'''";
215
+ const bodyStart = i + 3;
216
+ const closeIdx = source.indexOf(quote, bodyStart);
217
+ if (closeIdx === -1)
218
+ return undefined;
219
+ const inner = source.slice(bodyStart, closeIdx);
220
+ const firstLine = inner.split('\n').map(l => l.trim()).find(l => l.length > 0);
221
+ return firstLine ?? undefined;
222
+ }
223
+ return undefined;
224
+ }
225
+ // ── All other languages: scan backward from startIndex ─────────────────
226
+ // Move to the character just before startIndex
227
+ let pos = startIndex - 1;
228
+ // Skip trailing whitespace / newlines before the declaration
229
+ while (pos >= 0 && (source[pos] === ' ' || source[pos] === '\t' || source[pos] === '\n' || source[pos] === '\r')) {
230
+ pos--;
231
+ }
232
+ if (pos < 0)
233
+ return undefined;
234
+ // ── TypeScript / JavaScript / Java / C++: JSDoc block /** ... */ ────────
235
+ if (language === 'TypeScript' || language === 'JavaScript' ||
236
+ language === 'Java' || language === 'C++') {
237
+ // Expect closing */ of a JSDoc block
238
+ if (source[pos] === '/' && pos > 0 && source[pos - 1] === '*') {
239
+ const closePos = pos - 1; // points at '*' of closing '*/'
240
+ // Find opening /**
241
+ const openIdx = source.lastIndexOf('/**', closePos);
242
+ if (openIdx === -1)
243
+ return undefined;
244
+ const inner = source.slice(openIdx + 3, closePos - 0);
245
+ // Remove leading * on each line, find first non-empty, non-@ line
246
+ const firstLine = inner
247
+ .split('\n')
248
+ .map(l => l.replace(/^\s*\*\s?/, '').trim())
249
+ .find(l => l.length > 0 && !l.startsWith('@'));
250
+ return firstLine ?? undefined;
251
+ }
252
+ return undefined;
253
+ }
254
+ // ── Go: // comment lines immediately before ──────────────────────────────
255
+ if (language === 'Go') {
256
+ const lines = [];
257
+ // Walk backward line by line
258
+ let lineEnd = pos;
259
+ while (lineEnd >= 0) {
260
+ // Find start of this line
261
+ let lineStart = lineEnd;
262
+ while (lineStart > 0 && source[lineStart - 1] !== '\n')
263
+ lineStart--;
264
+ const line = source.slice(lineStart, lineEnd + 1).trimEnd();
265
+ const trimmed = line.trim();
266
+ if (trimmed.startsWith('//')) {
267
+ lines.unshift(trimmed.slice(2).trim());
268
+ lineEnd = lineStart - 1;
269
+ // Skip over the newline
270
+ while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
271
+ lineEnd--;
272
+ }
273
+ else {
274
+ break;
275
+ }
276
+ }
277
+ return lines.find(l => l.length > 0) ?? undefined;
278
+ }
279
+ // ── Rust / Swift: /// doc comment lines immediately before ─────────────
280
+ if (language === 'Rust' || language === 'Swift') {
281
+ const lines = [];
282
+ let lineEnd = pos;
283
+ while (lineEnd >= 0) {
284
+ let lineStart = lineEnd;
285
+ while (lineStart > 0 && source[lineStart - 1] !== '\n')
286
+ lineStart--;
287
+ const line = source.slice(lineStart, lineEnd + 1).trimEnd();
288
+ const trimmed = line.trim();
289
+ if (trimmed.startsWith('///')) {
290
+ lines.unshift(trimmed.slice(3).trim());
291
+ lineEnd = lineStart - 1;
292
+ while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
293
+ lineEnd--;
294
+ }
295
+ else {
296
+ break;
297
+ }
298
+ }
299
+ return lines.find(l => l.length > 0) ?? undefined;
300
+ }
301
+ // ── Ruby: # comment lines immediately before ─────────────────────────────
302
+ if (language === 'Ruby') {
303
+ const lines = [];
304
+ let lineEnd = pos;
305
+ while (lineEnd >= 0) {
306
+ let lineStart = lineEnd;
307
+ while (lineStart > 0 && source[lineStart - 1] !== '\n')
308
+ lineStart--;
309
+ const line = source.slice(lineStart, lineEnd + 1).trimEnd();
310
+ const trimmed = line.trim();
311
+ if (trimmed.startsWith('#')) {
312
+ lines.unshift(trimmed.slice(1).trim());
313
+ lineEnd = lineStart - 1;
314
+ while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
315
+ lineEnd--;
316
+ }
317
+ else {
318
+ break;
319
+ }
320
+ }
321
+ return lines.find(l => l.length > 0) ?? undefined;
322
+ }
323
+ return undefined;
324
+ }
325
+ /**
326
+ * Extract the function declaration (signature without body) from
327
+ * `source.slice(startIndex, endIndex)`.
328
+ *
329
+ * Strategy:
330
+ * - TS/JS/Java/C++/Go/Rust/Ruby: take everything up to the first `{` at depth 0
331
+ * - Python: take everything up to the first `:` that ends the `def` line
332
+ *
333
+ * Whitespace is normalized (multiple spaces/newlines → single space).
334
+ * Limited to 300 characters max.
335
+ */
336
+ function extractDeclaration(source, startIndex, endIndex, language) {
337
+ const slice = source.slice(startIndex, Math.min(endIndex, startIndex + 1500));
338
+ let decl;
339
+ if (language === 'Python') {
340
+ // Take up to (not including) the first `:` that ends the def line
341
+ // We scan for `:` while tracking parenthesis depth to avoid matching
342
+ // colons inside type annotations (e.g., def f(x: int) -> dict[str, int]:)
343
+ let depth = 0;
344
+ let end = -1;
345
+ for (let i = 0; i < slice.length; i++) {
346
+ const ch = slice[i];
347
+ if (ch === '(' || ch === '[' || ch === '{')
348
+ depth++;
349
+ else if (ch === ')' || ch === ']' || ch === '}')
350
+ depth--;
351
+ else if (ch === ':' && depth === 0) {
352
+ end = i;
353
+ break;
354
+ }
355
+ }
356
+ decl = end !== -1 ? slice.slice(0, end) : slice.slice(0, 300);
357
+ }
358
+ else {
359
+ // Find first `{` at brace depth 0
360
+ let depth = 0;
361
+ let end = -1;
362
+ for (let i = 0; i < slice.length; i++) {
363
+ const ch = slice[i];
364
+ if (ch === '{') {
365
+ if (depth === 0) {
366
+ end = i;
367
+ break;
368
+ }
369
+ depth++;
370
+ }
371
+ else if (ch === '}') {
372
+ depth--;
373
+ }
374
+ }
375
+ decl = end !== -1 ? slice.slice(0, end) : slice.slice(0, 300);
376
+ }
377
+ // Normalize whitespace
378
+ return decl.replace(/\s+/g, ' ').trim().slice(0, 300);
379
+ }
380
+ // ============================================================================
159
381
  // TYPESCRIPT EXTRACTOR
160
382
  // ============================================================================
161
383
  const TS_FN_QUERY = `
@@ -178,6 +400,7 @@ const TS_CALL_QUERY = `
178
400
  (call_expression
179
401
  function: [(identifier) @call.name
180
402
  (member_expression
403
+ object: (identifier) @call.object
181
404
  property: (property_identifier) @call.name)]) @call.node
182
405
  `;
183
406
  async function extractTSGraph(filePath, content) {
@@ -224,6 +447,8 @@ async function extractTSGraph(filePath, content) {
224
447
  endIndex: fnNode.endIndex,
225
448
  fanIn: 0,
226
449
  fanOut: 0,
450
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'TypeScript'),
451
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'TypeScript'),
227
452
  });
228
453
  }
229
454
  // --- Extract calls ---
@@ -232,10 +457,11 @@ async function extractTSGraph(filePath, content) {
232
457
  for (const match of callMatches) {
233
458
  const nameCapture = match.captures.find(c => c.name === 'call.name');
234
459
  const nodeCapture = match.captures.find(c => c.name === 'call.node');
460
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
235
461
  if (!nameCapture || !nodeCapture)
236
462
  continue;
237
463
  const calleeName = nameCapture.node.text;
238
- if (IGNORED_CALLEES.has(calleeName))
464
+ if (isIgnoredCallee(calleeName))
239
465
  continue;
240
466
  const callPos = nodeCapture.node.startIndex;
241
467
  const caller = findEnclosingFunction(nodes, callPos);
@@ -245,6 +471,7 @@ async function extractTSGraph(filePath, content) {
245
471
  callerId: caller.id,
246
472
  calleeName,
247
473
  line: nodeCapture.node.startPosition.row + 1,
474
+ calleeObject: objectCapture?.node.text,
248
475
  });
249
476
  }
250
477
  return { nodes, rawEdges };
@@ -331,6 +558,8 @@ async function extractPyGraph(filePath, content) {
331
558
  endIndex: fnNode.endIndex,
332
559
  fanIn: 0,
333
560
  fanOut: 0,
561
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Python'),
562
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Python'),
334
563
  });
335
564
  }
336
565
  // --- Extract calls ---
@@ -344,7 +573,7 @@ async function extractPyGraph(filePath, content) {
344
573
  if (!nameCapture || !nodeCapture)
345
574
  continue;
346
575
  const calleeName = nameCapture.node.text;
347
- if (IGNORED_CALLEES.has(calleeName))
576
+ if (isIgnoredCallee(calleeName))
348
577
  continue;
349
578
  const callPos = nodeCapture.node.startIndex;
350
579
  const caller = findEnclosingFunction(nodes, callPos);
@@ -356,20 +585,15 @@ async function extractPyGraph(filePath, content) {
356
585
  line: nodeCapture.node.startPosition.row + 1,
357
586
  });
358
587
  }
359
- // Method calls: obj.method() — only resolve self.* and cls.* (internal object methods)
588
+ // Method calls: obj.method() — capture receiver for type-inference-based resolution
360
589
  for (const match of methodCallQuery.matches(tree.rootNode)) {
361
590
  const objectCapture = match.captures.find(c => c.name === 'call.object');
362
591
  const nameCapture = match.captures.find(c => c.name === 'call.name');
363
592
  const nodeCapture = match.captures.find(c => c.name === 'call.node');
364
593
  if (!objectCapture || !nameCapture || !nodeCapture)
365
594
  continue;
366
- const objectName = objectCapture.node.text;
367
- // Only track self.method() and cls.method() — external objects like
368
- // redis.get(), dict.get(), os.path.join() would create massive false positives
369
- if (objectName !== 'self' && objectName !== 'cls')
370
- continue;
371
595
  const calleeName = nameCapture.node.text;
372
- if (IGNORED_CALLEES.has(calleeName))
596
+ if (isIgnoredCallee(calleeName))
373
597
  continue;
374
598
  const callPos = nodeCapture.node.startIndex;
375
599
  const caller = findEnclosingFunction(nodes, callPos);
@@ -379,6 +603,7 @@ async function extractPyGraph(filePath, content) {
379
603
  callerId: caller.id,
380
604
  calleeName,
381
605
  line: nodeCapture.node.startPosition.row + 1,
606
+ calleeObject: objectCapture.node.text,
382
607
  });
383
608
  }
384
609
  return { nodes, rawEdges };
@@ -399,6 +624,7 @@ const GO_CALL_QUERY = `
399
624
 
400
625
  (call_expression
401
626
  function: (selector_expression
627
+ operand: (identifier) @call.object
402
628
  field: (field_identifier) @call.name)) @call.node
403
629
  `;
404
630
  async function extractGoGraph(filePath, content) {
@@ -434,21 +660,24 @@ async function extractGoGraph(filePath, content) {
434
660
  startIndex: fnNode.startIndex,
435
661
  endIndex: fnNode.endIndex,
436
662
  fanIn: 0, fanOut: 0,
663
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Go'),
664
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Go'),
437
665
  });
438
666
  }
439
667
  const rawEdges = [];
440
668
  for (const match of callQuery.matches(tree.rootNode)) {
441
669
  const nameCapture = match.captures.find(c => c.name === 'call.name');
442
670
  const nodeCapture = match.captures.find(c => c.name === 'call.node');
671
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
443
672
  if (!nameCapture || !nodeCapture)
444
673
  continue;
445
674
  const calleeName = nameCapture.node.text;
446
- if (IGNORED_CALLEES.has(calleeName))
675
+ if (isIgnoredCallee(calleeName))
447
676
  continue;
448
677
  const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
449
678
  if (!caller)
450
679
  continue;
451
- rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
680
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
452
681
  }
453
682
  return { nodes, rawEdges };
454
683
  }
@@ -465,6 +694,7 @@ const RUST_CALL_QUERY = `
465
694
 
466
695
  (call_expression
467
696
  function: (field_expression
697
+ value: (identifier) @call.object
468
698
  field: (field_identifier) @call.name)) @call.node
469
699
  `;
470
700
  async function extractRustGraph(filePath, content) {
@@ -502,21 +732,24 @@ async function extractRustGraph(filePath, content) {
502
732
  startIndex: fnNode.startIndex,
503
733
  endIndex: fnNode.endIndex,
504
734
  fanIn: 0, fanOut: 0,
735
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Rust'),
736
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Rust'),
505
737
  });
506
738
  }
507
739
  const rawEdges = [];
508
740
  for (const match of callQuery.matches(tree.rootNode)) {
509
741
  const nameCapture = match.captures.find(c => c.name === 'call.name');
510
742
  const nodeCapture = match.captures.find(c => c.name === 'call.node');
743
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
511
744
  if (!nameCapture || !nodeCapture)
512
745
  continue;
513
746
  const calleeName = nameCapture.node.text;
514
- if (IGNORED_CALLEES.has(calleeName))
747
+ if (isIgnoredCallee(calleeName))
515
748
  continue;
516
749
  const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
517
750
  if (!caller)
518
751
  continue;
519
- rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
752
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
520
753
  }
521
754
  return { nodes, rawEdges };
522
755
  }
@@ -530,8 +763,12 @@ const RUBY_FN_QUERY = `
530
763
  (singleton_method
531
764
  name: (identifier) @fn.name) @fn.node
532
765
  `;
533
- // Explicit calls: fetch(), obj.method()
766
+ // Explicit calls: fn(), obj.method()
534
767
  const RUBY_CALL_QUERY = `
768
+ (call
769
+ receiver: (identifier) @call.object
770
+ method: (identifier) @call.name) @call.node
771
+
535
772
  (call
536
773
  method: (identifier) @call.name) @call.node
537
774
  `;
@@ -576,30 +813,33 @@ async function extractRubyGraph(filePath, content) {
576
813
  startIndex: fnNode.startIndex,
577
814
  endIndex: fnNode.endIndex,
578
815
  fanIn: 0, fanOut: 0,
816
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Ruby'),
817
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Ruby'),
579
818
  });
580
819
  }
581
820
  const rawEdges = [];
582
- // Explicit calls: fetch(), obj.method()
821
+ // Explicit calls: fn(), obj.method()
583
822
  for (const match of callQuery.matches(tree.rootNode)) {
584
823
  const nameCapture = match.captures.find(c => c.name === 'call.name');
585
824
  const nodeCapture = match.captures.find(c => c.name === 'call.node');
825
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
586
826
  if (!nameCapture || !nodeCapture)
587
827
  continue;
588
828
  const calleeName = nameCapture.node.text;
589
- if (IGNORED_CALLEES.has(calleeName))
829
+ if (isIgnoredCallee(calleeName))
590
830
  continue;
591
831
  const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
592
832
  if (!caller)
593
833
  continue;
594
- rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
834
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
595
835
  }
596
- // Bareword calls: fetch (no parens) — identifier at statement level
836
+ // Bareword calls: identifier at statement level, no parens
597
837
  for (const match of barewordQuery.matches(tree.rootNode)) {
598
838
  const nameCapture = match.captures.find(c => c.name === 'call.name');
599
839
  if (!nameCapture)
600
840
  continue;
601
841
  const calleeName = nameCapture.node.text;
602
- if (IGNORED_CALLEES.has(calleeName))
842
+ if (isIgnoredCallee(calleeName))
603
843
  continue;
604
844
  const caller = findEnclosingFunction(nodes, nameCapture.node.startIndex);
605
845
  if (!caller)
@@ -619,6 +859,10 @@ const JAVA_FN_QUERY = `
619
859
  name: (identifier) @fn.name) @fn.node
620
860
  `;
621
861
  const JAVA_CALL_QUERY = `
862
+ (method_invocation
863
+ object: (identifier) @call.object
864
+ name: (identifier) @call.name) @call.node
865
+
622
866
  (method_invocation
623
867
  name: (identifier) @call.name) @call.node
624
868
  `;
@@ -656,21 +900,24 @@ async function extractJavaGraph(filePath, content) {
656
900
  startIndex: fnNode.startIndex,
657
901
  endIndex: fnNode.endIndex,
658
902
  fanIn: 0, fanOut: 0,
903
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Java'),
904
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Java'),
659
905
  });
660
906
  }
661
907
  const rawEdges = [];
662
908
  for (const match of callQuery.matches(tree.rootNode)) {
663
909
  const nameCapture = match.captures.find(c => c.name === 'call.name');
664
910
  const nodeCapture = match.captures.find(c => c.name === 'call.node');
911
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
665
912
  if (!nameCapture || !nodeCapture)
666
913
  continue;
667
914
  const calleeName = nameCapture.node.text;
668
- if (IGNORED_CALLEES.has(calleeName))
915
+ if (isIgnoredCallee(calleeName))
669
916
  continue;
670
917
  const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
671
918
  if (!caller)
672
919
  continue;
673
- rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
920
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
674
921
  }
675
922
  return { nodes, rawEdges };
676
923
  }
@@ -708,13 +955,16 @@ const CPP_FN_QUALIFIED_QUERY = `
708
955
  declarator: (qualified_identifier
709
956
  name: (identifier) @fn.name))) @fn.node
710
957
  `;
711
- /** Function calls: foo() and obj.method() / ptr->method() */
712
- const CPP_CALL_QUERY = `
958
+ /** Plain function calls: foo() */
959
+ const CPP_CALL_DIRECT_QUERY = `
713
960
  (call_expression
714
961
  function: (identifier) @call.name) @call.node
715
-
962
+ `;
963
+ /** Member calls: obj.method() and ptr->method() — captures receiver */
964
+ const CPP_CALL_MEMBER_QUERY = `
716
965
  (call_expression
717
966
  function: (field_expression
967
+ argument: (identifier) @call.object
718
968
  field: (field_identifier) @call.name)) @call.node
719
969
  `;
720
970
  async function extractCppGraph(filePath, content) {
@@ -732,6 +982,9 @@ async function extractCppGraph(filePath, content) {
732
982
  continue;
733
983
  seen.add(nameCapture.node.startIndex);
734
984
  const name = nameCapture.node.text;
985
+ // Skip ALL_CAPS names — these are almost certainly macros, not functions
986
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(name))
987
+ continue;
735
988
  const fnNode = nodeCapture.node;
736
989
  // Find enclosing class (inline method defined inside class body)
737
990
  let className;
@@ -765,37 +1018,392 @@ async function extractCppGraph(filePath, content) {
765
1018
  startIndex: fnNode.startIndex,
766
1019
  endIndex: fnNode.endIndex,
767
1020
  fanIn: 0, fanOut: 0,
1021
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'C++'),
1022
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'C++'),
768
1023
  });
769
1024
  }
770
1025
  }
771
1026
  const rawEdges = [];
772
- for (const match of safeQuery(lang, CPP_CALL_QUERY, tree.rootNode)) {
1027
+ // Plain calls: foo()
1028
+ for (const match of safeQuery(lang, CPP_CALL_DIRECT_QUERY, tree.rootNode)) {
773
1029
  const nameCapture = match.captures.find(c => c.name === 'call.name');
774
1030
  const nodeCapture = match.captures.find(c => c.name === 'call.node');
775
1031
  if (!nameCapture || !nodeCapture)
776
1032
  continue;
777
1033
  const calleeName = nameCapture.node.text;
778
- if (IGNORED_CALLEES.has(calleeName))
1034
+ if (isIgnoredCallee(calleeName))
779
1035
  continue;
780
1036
  const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
781
1037
  if (!caller)
782
1038
  continue;
783
1039
  rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
784
1040
  }
1041
+ // Member calls: obj.method() / ptr->method()
1042
+ for (const match of safeQuery(lang, CPP_CALL_MEMBER_QUERY, tree.rootNode)) {
1043
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
1044
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
1045
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
1046
+ if (!nameCapture || !nodeCapture)
1047
+ continue;
1048
+ const calleeName = nameCapture.node.text;
1049
+ if (isIgnoredCallee(calleeName))
1050
+ continue;
1051
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
1052
+ if (!caller)
1053
+ continue;
1054
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
1055
+ }
785
1056
  return { nodes, rawEdges };
786
1057
  }
787
1058
  // ============================================================================
1059
+ // SWIFT EXTRACTOR
1060
+ // ============================================================================
1061
+ // function_declaration covers free functions and methods inside class_body
1062
+ const SWIFT_FN_QUERY = `
1063
+ (function_declaration
1064
+ name: (simple_identifier) @fn.name) @fn.node
1065
+
1066
+ (init_declaration) @fn.node
1067
+ `;
1068
+ // Direct calls: foo()
1069
+ const SWIFT_CALL_DIRECT_QUERY = `
1070
+ (call_expression
1071
+ (simple_identifier) @call.name) @call.node
1072
+ `;
1073
+ // Method calls: obj.method() / self.method()
1074
+ const SWIFT_CALL_NAV_QUERY = `
1075
+ (call_expression
1076
+ (navigation_expression
1077
+ (navigation_suffix
1078
+ (simple_identifier) @call.name))) @call.node
1079
+ `;
1080
+ async function extractSwiftGraph(filePath, content) {
1081
+ const { parser, lang } = await getSwiftParser();
1082
+ const tree = parser.parse(content);
1083
+ const fnQuery = new Parser.Query(lang, SWIFT_FN_QUERY);
1084
+ const directCallQuery = new Parser.Query(lang, SWIFT_CALL_DIRECT_QUERY);
1085
+ const navCallQuery = new Parser.Query(lang, SWIFT_CALL_NAV_QUERY);
1086
+ const nodes = [];
1087
+ for (const match of fnQuery.matches(tree.rootNode)) {
1088
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
1089
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
1090
+ if (!nodeCapture)
1091
+ continue;
1092
+ const fnNode = nodeCapture.node;
1093
+ const name = nameCapture?.node.text ?? 'init';
1094
+ // Find enclosing class/struct/actor/enum/extension (all are class_declaration in this grammar)
1095
+ let className;
1096
+ let cursor = fnNode.parent;
1097
+ while (cursor) {
1098
+ if (cursor.type === 'class_declaration') {
1099
+ const nameNode = cursor.children.find(c => c.type === 'type_identifier');
1100
+ if (nameNode)
1101
+ className = nameNode.text;
1102
+ break;
1103
+ }
1104
+ cursor = cursor.parent;
1105
+ }
1106
+ const isAsync = content.slice(fnNode.startIndex, fnNode.endIndex).includes(' async ');
1107
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
1108
+ nodes.push({
1109
+ id, name, filePath, className,
1110
+ isAsync,
1111
+ language: 'Swift',
1112
+ startIndex: fnNode.startIndex,
1113
+ endIndex: fnNode.endIndex,
1114
+ fanIn: 0, fanOut: 0,
1115
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Swift'),
1116
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Swift'),
1117
+ });
1118
+ }
1119
+ const rawEdges = [];
1120
+ // Direct calls: foo()
1121
+ for (const match of directCallQuery.matches(tree.rootNode)) {
1122
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
1123
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
1124
+ if (!nameCapture || !nodeCapture)
1125
+ continue;
1126
+ const calleeName = nameCapture.node.text;
1127
+ if (isIgnoredCallee(calleeName))
1128
+ continue;
1129
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
1130
+ if (!caller)
1131
+ continue;
1132
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
1133
+ }
1134
+ // Method calls: obj.method() / self.method()
1135
+ for (const match of navCallQuery.matches(tree.rootNode)) {
1136
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
1137
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
1138
+ if (!nameCapture || !nodeCapture)
1139
+ continue;
1140
+ const calleeName = nameCapture.node.text;
1141
+ if (isIgnoredCallee(calleeName))
1142
+ continue;
1143
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
1144
+ if (!caller)
1145
+ continue;
1146
+ // Extract the receiver object (first child of navigation_expression)
1147
+ const navExpr = nodeCapture.node.firstChild;
1148
+ const objText = navExpr?.firstChild?.type === 'self_expression'
1149
+ ? 'self'
1150
+ : navExpr?.firstChild?.text;
1151
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objText });
1152
+ }
1153
+ return { nodes, rawEdges };
1154
+ }
1155
+ // ============================================================================
1156
+ // CLASS HIERARCHY EXTRACTION
1157
+ // ============================================================================
1158
+ /**
1159
+ * Extract parent class / interface relationships from source files using
1160
+ * tree-sitter. Returns a map from `filePath::ClassName` → relationship info.
1161
+ * Uses safeQuery so any query that doesn't match a grammar version is silently
1162
+ * skipped rather than crashing.
1163
+ */
1164
+ async function extractClassRelationships(files) {
1165
+ const out = new Map();
1166
+ // Helper to merge into map keyed by `filePath::ClassName`
1167
+ function merge(filePath, className, parents, ifaces) {
1168
+ const key = `${filePath}::${className}`;
1169
+ const existing = out.get(key) ?? { parentClasses: [], interfaces: [] };
1170
+ for (const p of parents)
1171
+ if (!existing.parentClasses.includes(p))
1172
+ existing.parentClasses.push(p);
1173
+ for (const i of ifaces)
1174
+ if (!existing.interfaces.includes(i))
1175
+ existing.interfaces.push(i);
1176
+ out.set(key, existing);
1177
+ }
1178
+ for (const file of files) {
1179
+ try {
1180
+ if (file.language === 'TypeScript' || file.language === 'JavaScript') {
1181
+ const { parser, lang } = await getTSParser();
1182
+ const tree = parser.parse(file.content);
1183
+ // class Foo extends Bar implements Baz, Qux
1184
+ const EXTENDS_Q = `
1185
+ (class_declaration
1186
+ name: (type_identifier) @cls
1187
+ (class_heritage (extends_clause value: (identifier) @parent)))`;
1188
+ const IMPLEMENTS_Q = `
1189
+ (class_declaration
1190
+ name: (type_identifier) @cls
1191
+ (class_heritage (implements_clause (type_identifier) @iface)))`;
1192
+ for (const m of safeQuery(lang, EXTENDS_Q, tree.rootNode)) {
1193
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1194
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1195
+ if (cls && parent)
1196
+ merge(file.path, cls, [parent], []);
1197
+ }
1198
+ for (const m of safeQuery(lang, IMPLEMENTS_Q, tree.rootNode)) {
1199
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1200
+ const iface = m.captures.find(c => c.name === 'iface')?.node.text;
1201
+ if (cls && iface)
1202
+ merge(file.path, cls, [], [iface]);
1203
+ }
1204
+ }
1205
+ else if (file.language === 'Python') {
1206
+ const { parser, lang } = await getPyParser();
1207
+ const tree = parser.parse(file.content);
1208
+ // class Foo(Bar, Baz):
1209
+ const Q = `
1210
+ (class_definition
1211
+ name: (identifier) @cls
1212
+ superclasses: (argument_list (identifier) @parent))`;
1213
+ for (const m of safeQuery(lang, Q, tree.rootNode)) {
1214
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1215
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1216
+ if (cls && parent && parent !== 'object')
1217
+ merge(file.path, cls, [parent], []);
1218
+ }
1219
+ }
1220
+ else if (file.language === 'Java') {
1221
+ const { parser, lang } = await getJavaParser();
1222
+ const tree = parser.parse(file.content);
1223
+ const EXTENDS_Q = `
1224
+ (class_declaration
1225
+ name: (identifier) @cls
1226
+ (superclass (type_identifier) @parent))`;
1227
+ const IMPLEMENTS_Q = `
1228
+ (class_declaration
1229
+ name: (identifier) @cls
1230
+ (super_interfaces (type_list (type_identifier) @iface)))`;
1231
+ for (const m of safeQuery(lang, EXTENDS_Q, tree.rootNode)) {
1232
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1233
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1234
+ if (cls && parent)
1235
+ merge(file.path, cls, [parent], []);
1236
+ }
1237
+ for (const m of safeQuery(lang, IMPLEMENTS_Q, tree.rootNode)) {
1238
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1239
+ const iface = m.captures.find(c => c.name === 'iface')?.node.text;
1240
+ if (cls && iface)
1241
+ merge(file.path, cls, [], [iface]);
1242
+ }
1243
+ }
1244
+ else if (file.language === 'C++') {
1245
+ const { parser, lang } = await getCppParser();
1246
+ const tree = parser.parse(file.content);
1247
+ // class Foo : public Bar
1248
+ const Q = `
1249
+ (class_specifier
1250
+ name: (type_identifier) @cls
1251
+ (base_class_clause (type_identifier) @parent))`;
1252
+ for (const m of safeQuery(lang, Q, tree.rootNode)) {
1253
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1254
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1255
+ if (cls && parent)
1256
+ merge(file.path, cls, [parent], []);
1257
+ }
1258
+ }
1259
+ else if (file.language === 'Ruby') {
1260
+ const { parser, lang } = await getRubyParser();
1261
+ const tree = parser.parse(file.content);
1262
+ // class Foo < Bar
1263
+ const Q = `
1264
+ (class
1265
+ name: (constant) @cls
1266
+ superclass: (superclass (constant) @parent))`;
1267
+ for (const m of safeQuery(lang, Q, tree.rootNode)) {
1268
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1269
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1270
+ if (cls && parent)
1271
+ merge(file.path, cls, [parent], []);
1272
+ }
1273
+ }
1274
+ else if (file.language === 'Go') {
1275
+ // Go has no inheritance but has struct embedding; treat as 'embeds' edges
1276
+ const { parser, lang } = await getGoParser();
1277
+ const tree = parser.parse(file.content);
1278
+ // Anonymous (embedded) field in a struct: type Foo struct { Bar }
1279
+ const Q = `
1280
+ (type_declaration
1281
+ (type_spec
1282
+ name: (type_identifier) @cls
1283
+ type: (struct_type
1284
+ (field_declaration_list
1285
+ (field_declaration
1286
+ type: (type_identifier) @embedded)))))`;
1287
+ for (const m of safeQuery(lang, Q, tree.rootNode)) {
1288
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1289
+ const embedded = m.captures.find(c => c.name === 'embedded')?.node.text;
1290
+ if (cls && embedded) {
1291
+ const key = `${file.path}::${cls}`;
1292
+ const existing = out.get(key) ?? { parentClasses: [], interfaces: [] };
1293
+ // Store Go embeds as parentClasses (will be tagged as 'embeds' when building edges)
1294
+ if (!existing.parentClasses.includes(embedded))
1295
+ existing.parentClasses.push(embedded);
1296
+ out.set(key, existing);
1297
+ }
1298
+ }
1299
+ }
1300
+ // Rust: trait impls are structural but less like OOP inheritance; skip for now
1301
+ }
1302
+ catch {
1303
+ // Best-effort; skip unparseable files
1304
+ }
1305
+ }
1306
+ return out;
1307
+ }
1308
+ /**
1309
+ * Build ClassNode[] from the set of extracted FunctionNodes (which carry
1310
+ * `className`), enriched with inheritance data from `extractClassRelationships`.
1311
+ *
1312
+ * Functions without a className are grouped by file into synthetic module nodes
1313
+ * (e.g. `[call-graph]`) so every function appears in the class graph, not just
1314
+ * class methods. This is essential for codebases that use mostly module-level
1315
+ * exports rather than OOP classes.
1316
+ */
1317
+ function buildClassNodes(allNodes, relationships) {
1318
+ // Group FunctionNodes by (filePath, className).
1319
+ // Free functions use a synthetic "[basename]" module name keyed by filePath alone.
1320
+ const groups = new Map();
1321
+ for (const fn of allNodes.values()) {
1322
+ let key;
1323
+ let name;
1324
+ let isModule;
1325
+ if (fn.className) {
1326
+ key = `${fn.filePath}::${fn.className}`;
1327
+ name = fn.className;
1328
+ isModule = false;
1329
+ }
1330
+ else {
1331
+ // Synthetic module node — one per file
1332
+ key = fn.filePath;
1333
+ const base = fn.filePath.split('/').pop() ?? fn.filePath;
1334
+ name = '[' + base.replace(/\.[^.]+$/, '') + ']';
1335
+ isModule = true;
1336
+ }
1337
+ if (!groups.has(key)) {
1338
+ groups.set(key, { name, filePath: fn.filePath, language: fn.language, isModule, methods: [] });
1339
+ }
1340
+ groups.get(key).methods.push(fn);
1341
+ }
1342
+ // Build ClassNode[]
1343
+ const classMap = new Map();
1344
+ for (const [id, g] of groups) {
1345
+ const rel = relationships.get(id) ?? { parentClasses: [], interfaces: [] };
1346
+ const cls = {
1347
+ id,
1348
+ name: g.name,
1349
+ filePath: g.filePath,
1350
+ language: g.language,
1351
+ parentClasses: rel.parentClasses,
1352
+ interfaces: rel.interfaces,
1353
+ methodIds: g.methods.map(m => m.id),
1354
+ fanIn: g.methods.reduce((s, m) => s + m.fanIn, 0),
1355
+ fanOut: g.methods.reduce((s, m) => s + m.fanOut, 0),
1356
+ isModule: g.isModule,
1357
+ };
1358
+ classMap.set(id, cls);
1359
+ }
1360
+ // Build InheritanceEdge[] — only when both parent and child are in our graph
1361
+ // Parent lookup: match by class name across all ClassNodes (first match wins)
1362
+ const byName = new Map();
1363
+ for (const cls of classMap.values()) {
1364
+ if (!byName.has(cls.name))
1365
+ byName.set(cls.name, cls);
1366
+ }
1367
+ const inheritanceEdges = [];
1368
+ const seenEdges = new Set();
1369
+ for (const cls of classMap.values()) {
1370
+ for (const parentName of cls.parentClasses) {
1371
+ const parent = byName.get(parentName);
1372
+ if (!parent)
1373
+ continue;
1374
+ const edgeId = `${parent.id}->${cls.id}`;
1375
+ if (seenEdges.has(edgeId))
1376
+ continue;
1377
+ seenEdges.add(edgeId);
1378
+ // Go embedding vs OOP inheritance
1379
+ const kind = cls.language === 'Go' ? 'embeds' : 'extends';
1380
+ inheritanceEdges.push({ id: edgeId, parentId: parent.id, childId: cls.id, kind });
1381
+ }
1382
+ for (const ifaceName of cls.interfaces) {
1383
+ const parent = byName.get(ifaceName);
1384
+ if (!parent)
1385
+ continue;
1386
+ const edgeId = `${parent.id}->${cls.id}`;
1387
+ if (seenEdges.has(edgeId))
1388
+ continue;
1389
+ seenEdges.add(edgeId);
1390
+ inheritanceEdges.push({ id: edgeId, parentId: parent.id, childId: cls.id, kind: 'implements' });
1391
+ }
1392
+ }
1393
+ return { classes: Array.from(classMap.values()), inheritanceEdges };
1394
+ }
1395
+ // ============================================================================
788
1396
  // CALL GRAPH BUILDER
789
1397
  // ============================================================================
790
1398
  export class CallGraphBuilder {
791
1399
  /**
792
1400
  * Build a call graph from a list of source files.
793
1401
  *
794
- * @param files Source files with path, content, and language
795
- * @param layers Optional layer map { layerName: [path prefix, ...] }
796
- * e.g. { api: ['routes/', 'controllers/'], storage: ['models/'] }
1402
+ * @param files Source files with path, content, and language
1403
+ * @param layers Optional layer map { layerName: [path prefix, ...] }
1404
+ * @param importMap Optional per-file import map (from ImportResolverBridge)
797
1405
  */
798
- async build(files, layers) {
1406
+ async build(files, layers, importMap) {
799
1407
  const allNodes = new Map();
800
1408
  const allRawEdges = [];
801
1409
  // Pass 1: Extract nodes and raw edges from each file
@@ -823,6 +1431,9 @@ export class CallGraphBuilder {
823
1431
  else if (file.language === 'C++') {
824
1432
  result = await extractCppGraph(file.path, file.content);
825
1433
  }
1434
+ else if (file.language === 'Swift') {
1435
+ result = await extractSwiftGraph(file.path, file.content);
1436
+ }
826
1437
  else {
827
1438
  continue;
828
1439
  }
@@ -838,35 +1449,136 @@ export class CallGraphBuilder {
838
1449
  }
839
1450
  }
840
1451
  }
841
- // Pass 2: Resolve raw edges — find callee FunctionNode by name
842
- const nodesByName = new Map();
843
- for (const node of allNodes.values()) {
844
- const list = nodesByName.get(node.name) ?? [];
845
- list.push(node);
846
- nodesByName.set(node.name, list);
847
- }
1452
+ // Pass 2: Resolve raw edges — multi-strategy resolution
1453
+ const trie = new FunctionRegistryTrie();
1454
+ for (const node of allNodes.values())
1455
+ trie.insert(node);
1456
+ // Build per-function-body content slices for type inference (keyed by functionId)
1457
+ const fileContents = new Map();
1458
+ for (const file of files)
1459
+ fileContents.set(file.path, file.content);
848
1460
  const edges = [];
849
1461
  for (const raw of allRawEdges) {
850
- const candidates = nodesByName.get(raw.calleeName);
851
- if (!candidates || candidates.length === 0)
852
- continue; // external call
1462
+ const callerNode = allNodes.get(raw.callerId);
1463
+ if (!callerNode)
1464
+ continue;
853
1465
  let calleeNode;
854
- if (candidates.length === 1) {
855
- calleeNode = candidates[0];
1466
+ let confidence = 'name_only';
1467
+ // Strategy 1 — self/cls intra-class (Python self.*, cls.* or same-class method)
1468
+ if (raw.calleeObject === 'self' || raw.calleeObject === 'cls') {
1469
+ if (callerNode.className) {
1470
+ const candidates = trie.findByQualifiedName(callerNode.className, raw.calleeName);
1471
+ if (candidates.length > 0) {
1472
+ calleeNode = candidates[0];
1473
+ confidence = 'self_cls';
1474
+ }
1475
+ }
856
1476
  }
857
- else {
858
- // Prefer same file as caller
859
- const callerNode = allNodes.get(raw.callerId);
860
- const sameFile = candidates.find(c => c.filePath === callerNode?.filePath);
861
- calleeNode = sameFile ?? candidates[0];
1477
+ // Strategy 1b — Swift/C++ type-name resolution (capitalized receiver = type/class reference)
1478
+ // In Swift and C++, there are no intra-module imports, so cross-file calls appear as
1479
+ // TypeName.method() or TypeName::method(). A capitalized receiver with no same-file
1480
+ // class of that name is a reliable signal for a cross-file type reference.
1481
+ if (!calleeNode && raw.calleeObject && (callerNode.language === 'Swift' || callerNode.language === 'C++')) {
1482
+ const ch = raw.calleeObject.charCodeAt(0);
1483
+ const isCapitalized = ch >= 65 && ch <= 90; // A-Z
1484
+ if (isCapitalized) {
1485
+ const candidates = trie.findByQualifiedName(raw.calleeObject, raw.calleeName);
1486
+ if (candidates.length > 0) {
1487
+ calleeNode = candidates[0];
1488
+ confidence = 'type_name';
1489
+ }
1490
+ }
1491
+ }
1492
+ // Strategy 2 — type inference on receiver variable
1493
+ if (!calleeNode && raw.calleeObject) {
1494
+ const fileContent = fileContents.get(callerNode.filePath);
1495
+ if (fileContent) {
1496
+ const bodySlice = fileContent.slice(callerNode.startIndex, callerNode.endIndex);
1497
+ const inferredTypes = inferTypesFromSource(bodySlice, callerNode.language);
1498
+ const resolved = resolveViaTypeInference(raw.calleeObject, raw.calleeName, inferredTypes, trie);
1499
+ if (resolved) {
1500
+ calleeNode = resolved;
1501
+ confidence = 'type_inference';
1502
+ }
1503
+ }
1504
+ }
1505
+ // Strategy 3 — import resolution (TS/JS/Python/Go/Rust/Ruby/Java)
1506
+ if (!calleeNode && importMap) {
1507
+ const importedFile = importMap.get(callerNode.filePath)?.get(raw.calleeName)
1508
+ ?? (raw.calleeObject ? importMap.get(callerNode.filePath)?.get(raw.calleeObject) : undefined);
1509
+ if (importedFile) {
1510
+ const candidates = trie.findBySimpleName(raw.calleeName).filter(n => n.filePath.startsWith(importedFile));
1511
+ if (candidates.length > 0) {
1512
+ calleeNode = candidates[0];
1513
+ confidence = 'import';
1514
+ }
1515
+ }
862
1516
  }
1517
+ // Strategy 4 — same-file preference (only for calls without a typed receiver)
1518
+ // When a receiver is explicitly present but unresolvable (e.g. redis_client.get()),
1519
+ // skip name_only fallback to avoid false-positive edges.
1520
+ if (!calleeNode && !raw.calleeObject) {
1521
+ const candidates = trie.findBySimpleName(raw.calleeName);
1522
+ if (candidates.length === 0)
1523
+ continue; // external call, skip
1524
+ const sameFile = candidates.find(c => c.filePath === callerNode.filePath);
1525
+ if (sameFile) {
1526
+ calleeNode = sameFile;
1527
+ confidence = 'same_file';
1528
+ }
1529
+ else {
1530
+ calleeNode = candidates[0];
1531
+ confidence = 'name_only';
1532
+ }
1533
+ }
1534
+ if (!calleeNode)
1535
+ continue;
863
1536
  edges.push({
864
1537
  callerId: raw.callerId,
865
1538
  calleeId: calleeNode.id,
866
1539
  calleeName: raw.calleeName,
867
1540
  line: raw.line,
1541
+ confidence,
868
1542
  });
869
1543
  }
1544
+ // Pass 2b: HTTP cross-language edges (JS/TS caller → Python handler)
1545
+ try {
1546
+ const filePaths = files.map(f => f.path);
1547
+ const { edges: httpEdges } = await extractAllHttpEdges(filePaths);
1548
+ for (const he of httpEdges) {
1549
+ // Find callee: handler function by name in handlerFile
1550
+ const calleeNode = trie.findBySimpleName(he.route.handlerName)
1551
+ .find(n => n.filePath === he.handlerFile);
1552
+ if (!calleeNode)
1553
+ continue;
1554
+ // Find caller: any function in callerFile that encloses the HTTP call's line
1555
+ const callerContent = fileContents.get(he.callerFile);
1556
+ const callerNode = callerContent
1557
+ ? (() => {
1558
+ let offset = 0;
1559
+ const lines = callerContent.split('\n');
1560
+ for (let i = 0; i < he.call.line - 1 && i < lines.length; i++) {
1561
+ offset += lines[i].length + 1;
1562
+ }
1563
+ const candidates = Array.from(allNodes.values())
1564
+ .filter(n => n.filePath === he.callerFile);
1565
+ return findEnclosingFunction(candidates, offset);
1566
+ })()
1567
+ : undefined;
1568
+ if (!callerNode)
1569
+ continue;
1570
+ edges.push({
1571
+ callerId: callerNode.id,
1572
+ calleeId: calleeNode.id,
1573
+ calleeName: he.route.handlerName,
1574
+ line: he.call.line,
1575
+ confidence: 'http_endpoint',
1576
+ });
1577
+ }
1578
+ }
1579
+ catch {
1580
+ // HTTP edge extraction is best-effort; don't fail the whole build
1581
+ }
870
1582
  // Pass 3: Calculate fanIn / fanOut (count unique caller→callee pairs, not call sites)
871
1583
  const seenPairs = new Set();
872
1584
  for (const edge of edges) {
@@ -895,9 +1607,14 @@ export class CallGraphBuilder {
895
1607
  : [];
896
1608
  const totalFanIn = nodes.reduce((s, n) => s + n.fanIn, 0);
897
1609
  const totalFanOut = nodes.reduce((s, n) => s + n.fanOut, 0);
1610
+ // Pass 5: Build class hierarchy (inheritance + grouping)
1611
+ const relationships = await extractClassRelationships(files);
1612
+ const { classes, inheritanceEdges } = buildClassNodes(allNodes, relationships);
898
1613
  return {
899
1614
  nodes: allNodes,
900
1615
  edges,
1616
+ classes,
1617
+ inheritanceEdges,
901
1618
  hubFunctions,
902
1619
  entryPoints,
903
1620
  layerViolations,
@@ -931,6 +1648,8 @@ export class CallGraphBuilder {
931
1648
  continue;
932
1649
  const callerIdx = layerOrder.indexOf(callerLayer);
933
1650
  const calleeIdx = layerOrder.indexOf(calleeLayer);
1651
+ if (callerIdx === -1 || calleeIdx === -1)
1652
+ continue;
934
1653
  if (callerIdx > calleeIdx) {
935
1654
  // Lower layer calling upper layer — violation
936
1655
  violations.push({
@@ -952,6 +1671,8 @@ export function serializeCallGraph(result) {
952
1671
  return {
953
1672
  nodes: Array.from(result.nodes.values()),
954
1673
  edges: result.edges,
1674
+ classes: result.classes,
1675
+ inheritanceEdges: result.inheritanceEdges,
955
1676
  hubFunctions: result.hubFunctions,
956
1677
  entryPoints: result.entryPoints,
957
1678
  layerViolations: result.layerViolations,