@optave/codegraph 3.11.0 → 3.11.2

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 (230) hide show
  1. package/README.md +38 -31
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +91 -60
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  6. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  7. package/dist/ast-analysis/visitor-utils.js +83 -49
  8. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  11. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  12. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  14. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  15. package/dist/cli/commands/embed.d.ts.map +1 -1
  16. package/dist/cli/commands/embed.js +49 -4
  17. package/dist/cli/commands/embed.js.map +1 -1
  18. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  19. package/dist/domain/analysis/dependencies.js +106 -80
  20. package/dist/domain/analysis/dependencies.js.map +1 -1
  21. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  22. package/dist/domain/analysis/fn-impact.js +77 -52
  23. package/dist/domain/analysis/fn-impact.js.map +1 -1
  24. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  25. package/dist/domain/analysis/module-map.js +132 -121
  26. package/dist/domain/analysis/module-map.js.map +1 -1
  27. package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
  28. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  29. package/dist/domain/graph/builder/call-resolver.js +130 -0
  30. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  31. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  32. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/helpers.js +47 -33
  34. package/dist/domain/graph/builder/helpers.js.map +1 -1
  35. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  36. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/incremental.js +214 -127
  38. package/dist/domain/graph/builder/incremental.js.map +1 -1
  39. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  40. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/pipeline.js +10 -766
  42. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/stages/build-edges.js +151 -192
  45. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  48. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  56. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  58. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  60. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  62. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  63. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  67. package/dist/domain/graph/cycles.d.ts +6 -4
  68. package/dist/domain/graph/cycles.d.ts.map +1 -1
  69. package/dist/domain/graph/cycles.js +50 -55
  70. package/dist/domain/graph/cycles.js.map +1 -1
  71. package/dist/domain/graph/journal.d.ts.map +1 -1
  72. package/dist/domain/graph/journal.js +89 -70
  73. package/dist/domain/graph/journal.js.map +1 -1
  74. package/dist/domain/graph/watcher.d.ts.map +1 -1
  75. package/dist/domain/graph/watcher.js +10 -4
  76. package/dist/domain/graph/watcher.js.map +1 -1
  77. package/dist/domain/parser.d.ts +12 -23
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +126 -79
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/search/generator.d.ts +3 -1
  82. package/dist/domain/search/generator.d.ts.map +1 -1
  83. package/dist/domain/search/generator.js +68 -45
  84. package/dist/domain/search/generator.js.map +1 -1
  85. package/dist/domain/search/models.d.ts +2 -0
  86. package/dist/domain/search/models.d.ts.map +1 -1
  87. package/dist/domain/search/models.js +37 -3
  88. package/dist/domain/search/models.js.map +1 -1
  89. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  90. package/dist/domain/search/search/hybrid.js +49 -40
  91. package/dist/domain/search/search/hybrid.js.map +1 -1
  92. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  93. package/dist/domain/search/search/semantic.js +69 -49
  94. package/dist/domain/search/search/semantic.js.map +1 -1
  95. package/dist/domain/wasm-worker-entry.js +201 -136
  96. package/dist/domain/wasm-worker-entry.js.map +1 -1
  97. package/dist/extractors/elixir.js +95 -71
  98. package/dist/extractors/elixir.js.map +1 -1
  99. package/dist/extractors/gleam.d.ts.map +1 -1
  100. package/dist/extractors/gleam.js +23 -31
  101. package/dist/extractors/gleam.js.map +1 -1
  102. package/dist/extractors/helpers.d.ts +79 -1
  103. package/dist/extractors/helpers.d.ts.map +1 -1
  104. package/dist/extractors/helpers.js +137 -0
  105. package/dist/extractors/helpers.js.map +1 -1
  106. package/dist/extractors/java.d.ts.map +1 -1
  107. package/dist/extractors/java.js +37 -49
  108. package/dist/extractors/java.js.map +1 -1
  109. package/dist/extractors/javascript.d.ts.map +1 -1
  110. package/dist/extractors/javascript.js +44 -44
  111. package/dist/extractors/javascript.js.map +1 -1
  112. package/dist/extractors/julia.js +27 -34
  113. package/dist/extractors/julia.js.map +1 -1
  114. package/dist/extractors/r.d.ts.map +1 -1
  115. package/dist/extractors/r.js +33 -58
  116. package/dist/extractors/r.js.map +1 -1
  117. package/dist/extractors/solidity.d.ts.map +1 -1
  118. package/dist/extractors/solidity.js +38 -61
  119. package/dist/extractors/solidity.js.map +1 -1
  120. package/dist/features/boundaries.d.ts.map +1 -1
  121. package/dist/features/boundaries.js +49 -39
  122. package/dist/features/boundaries.js.map +1 -1
  123. package/dist/features/cfg.d.ts.map +1 -1
  124. package/dist/features/cfg.js +90 -63
  125. package/dist/features/cfg.js.map +1 -1
  126. package/dist/features/check.d.ts.map +1 -1
  127. package/dist/features/check.js +43 -34
  128. package/dist/features/check.js.map +1 -1
  129. package/dist/features/cochange.d.ts.map +1 -1
  130. package/dist/features/cochange.js +68 -56
  131. package/dist/features/cochange.js.map +1 -1
  132. package/dist/features/complexity.d.ts.map +1 -1
  133. package/dist/features/complexity.js +105 -75
  134. package/dist/features/complexity.js.map +1 -1
  135. package/dist/features/dataflow.d.ts.map +1 -1
  136. package/dist/features/dataflow.js +37 -29
  137. package/dist/features/dataflow.js.map +1 -1
  138. package/dist/features/flow.d.ts.map +1 -1
  139. package/dist/features/flow.js +31 -22
  140. package/dist/features/flow.js.map +1 -1
  141. package/dist/features/graph-enrichment.d.ts.map +1 -1
  142. package/dist/features/graph-enrichment.js +77 -70
  143. package/dist/features/graph-enrichment.js.map +1 -1
  144. package/dist/features/owners.d.ts +17 -26
  145. package/dist/features/owners.d.ts.map +1 -1
  146. package/dist/features/owners.js +120 -109
  147. package/dist/features/owners.js.map +1 -1
  148. package/dist/features/sequence.d.ts.map +1 -1
  149. package/dist/features/sequence.js +59 -54
  150. package/dist/features/sequence.js.map +1 -1
  151. package/dist/features/structure-query.d.ts.map +1 -1
  152. package/dist/features/structure-query.js +60 -60
  153. package/dist/features/structure-query.js.map +1 -1
  154. package/dist/features/structure.d.ts.map +1 -1
  155. package/dist/features/structure.js +149 -52
  156. package/dist/features/structure.js.map +1 -1
  157. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  158. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  159. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  160. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  161. package/dist/graph/classifiers/roles.js +63 -59
  162. package/dist/graph/classifiers/roles.js.map +1 -1
  163. package/dist/infrastructure/config.d.ts +1 -1
  164. package/dist/infrastructure/config.d.ts.map +1 -1
  165. package/dist/infrastructure/config.js +1 -1
  166. package/dist/infrastructure/config.js.map +1 -1
  167. package/dist/presentation/cfg.d.ts.map +1 -1
  168. package/dist/presentation/cfg.js +44 -29
  169. package/dist/presentation/cfg.js.map +1 -1
  170. package/dist/presentation/flow.d.ts.map +1 -1
  171. package/dist/presentation/flow.js +58 -38
  172. package/dist/presentation/flow.js.map +1 -1
  173. package/dist/types.d.ts +1 -1
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-erlang.wasm +0 -0
  176. package/package.json +9 -9
  177. package/src/ast-analysis/engine.ts +145 -61
  178. package/src/ast-analysis/visitor-utils.ts +86 -46
  179. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  180. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  181. package/src/cli/commands/embed.ts +54 -4
  182. package/src/domain/analysis/dependencies.ts +166 -85
  183. package/src/domain/analysis/fn-impact.ts +120 -50
  184. package/src/domain/analysis/module-map.ts +175 -140
  185. package/src/domain/graph/builder/call-resolver.ts +181 -0
  186. package/src/domain/graph/builder/helpers.ts +85 -76
  187. package/src/domain/graph/builder/incremental.ts +321 -152
  188. package/src/domain/graph/builder/pipeline.ts +19 -957
  189. package/src/domain/graph/builder/stages/build-edges.ts +229 -275
  190. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  191. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  192. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  193. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  194. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  195. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  196. package/src/domain/graph/cycles.ts +51 -49
  197. package/src/domain/graph/journal.ts +84 -69
  198. package/src/domain/graph/watcher.ts +12 -4
  199. package/src/domain/parser.ts +143 -66
  200. package/src/domain/search/generator.ts +132 -74
  201. package/src/domain/search/models.ts +39 -3
  202. package/src/domain/search/search/hybrid.ts +53 -42
  203. package/src/domain/search/search/semantic.ts +105 -65
  204. package/src/domain/wasm-worker-entry.ts +235 -152
  205. package/src/extractors/elixir.ts +91 -64
  206. package/src/extractors/gleam.ts +33 -37
  207. package/src/extractors/helpers.ts +205 -1
  208. package/src/extractors/java.ts +42 -45
  209. package/src/extractors/javascript.ts +44 -43
  210. package/src/extractors/julia.ts +28 -35
  211. package/src/extractors/r.ts +38 -56
  212. package/src/extractors/solidity.ts +43 -71
  213. package/src/features/boundaries.ts +64 -46
  214. package/src/features/cfg.ts +145 -74
  215. package/src/features/check.ts +60 -43
  216. package/src/features/cochange.ts +95 -72
  217. package/src/features/complexity.ts +134 -79
  218. package/src/features/dataflow.ts +57 -34
  219. package/src/features/flow.ts +48 -24
  220. package/src/features/graph-enrichment.ts +105 -70
  221. package/src/features/owners.ts +186 -146
  222. package/src/features/sequence.ts +99 -69
  223. package/src/features/structure-query.ts +94 -79
  224. package/src/features/structure.ts +199 -79
  225. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  226. package/src/graph/classifiers/roles.ts +64 -54
  227. package/src/infrastructure/config.ts +1 -1
  228. package/src/presentation/cfg.ts +48 -32
  229. package/src/presentation/flow.ts +100 -52
  230. package/src/types.ts +1 -1
@@ -21,6 +21,12 @@ import type {
21
21
  } from '../../../types.js';
22
22
  import { parseFileIncremental } from '../../parser.js';
23
23
  import { computeConfidence, resolveImportPath } from '../resolve.js';
24
+ import {
25
+ type CallNodeLookup,
26
+ findCaller,
27
+ resolveCallTargets,
28
+ resolveReceiverEdge,
29
+ } from './call-resolver.js';
24
30
  import { BUILTIN_RECEIVERS, readFileSafe } from './helpers.js';
25
31
 
26
32
  // ── Local types ─────────────────────────────────────────────────────────
@@ -30,6 +36,7 @@ export interface IncrementalStmts {
30
36
  insertEdge: { run: (...params: unknown[]) => unknown };
31
37
  getNodeId: { get: (...params: unknown[]) => { id: number } | undefined };
32
38
  countNodes: { get: (...params: unknown[]) => { c: number } | undefined };
39
+ countEdges: { get: (...params: unknown[]) => { c: number } | undefined };
33
40
  listSymbols: { all: (...params: unknown[]) => unknown[] };
34
41
  findNodeInFile: { all: (...params: unknown[]) => unknown[] };
35
42
  findNodeByName: { all: (...params: unknown[]) => unknown[] };
@@ -40,6 +47,7 @@ interface RebuildResult {
40
47
  nodesAdded: number;
41
48
  nodesRemoved: number;
42
49
  edgesAdded: number;
50
+ edgesBefore: number;
43
51
  deleted?: boolean;
44
52
  event?: string;
45
53
  symbolDiff?: unknown;
@@ -183,8 +191,9 @@ function rebuildReverseDepEdges(
183
191
  aliases,
184
192
  skipBarrel ? null : db,
185
193
  );
186
- const importedNames = buildImportedNamesMap(symbols, rootDir, depRelPath, aliases);
187
- edgesAdded += buildCallEdges(stmts, depRelPath, symbols, fileNodeRow, importedNames);
194
+ const importedNames = buildImportedNamesMap(symbols, rootDir, depRelPath, aliases, db);
195
+ edgesAdded += buildCallEdges(db, stmts, depRelPath, symbols, fileNodeRow, importedNames);
196
+ edgesAdded += buildClassHierarchyEdges(stmts, depRelPath, symbols);
188
197
  return edgesAdded;
189
198
  }
190
199
 
@@ -307,6 +316,69 @@ function resolveBarrelImportEdges(
307
316
  return edgesAdded;
308
317
  }
309
318
 
319
+ /** Emit symbol-level `imports-type` edges for a single `import type` statement. */
320
+ function emitTypeOnlySymbolEdges(
321
+ db: BetterSqlite3Database | null,
322
+ stmts: IncrementalStmts,
323
+ imp: ExtractorOutput['imports'][number],
324
+ resolvedPath: string,
325
+ fileNodeId: number,
326
+ ): number {
327
+ let edgesAdded = 0;
328
+ for (const name of imp.names) {
329
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
330
+ let targetFile = resolvedPath;
331
+ if (db && isBarrelFile(db, resolvedPath)) {
332
+ const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
333
+ if (actual) targetFile = actual;
334
+ }
335
+ const candidates = stmts.findNodeInFile.all(cleanName, targetFile) as Array<{
336
+ id: number;
337
+ file: string;
338
+ }>;
339
+ if (candidates.length === 0) continue;
340
+ stmts.insertEdge.run(fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0);
341
+ edgesAdded++;
342
+ }
343
+ return edgesAdded;
344
+ }
345
+
346
+ /**
347
+ * Process a single import statement: emit the file→file edge, any
348
+ * symbol-level type-only edges, and barrel re-export edges.
349
+ */
350
+ function emitEdgesForImport(
351
+ stmts: IncrementalStmts,
352
+ imp: ExtractorOutput['imports'][number],
353
+ fileNodeId: number,
354
+ relPath: string,
355
+ rootDir: string,
356
+ aliases: PathAliases,
357
+ db: BetterSqlite3Database | null,
358
+ ): number {
359
+ const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
360
+ const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
361
+ if (!targetRow) return 0;
362
+
363
+ const edgeKind = imp.reexport
364
+ ? 'reexports'
365
+ : imp.typeOnly
366
+ ? 'imports-type'
367
+ : imp.dynamicImport
368
+ ? 'dynamic-imports'
369
+ : 'imports';
370
+ stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
371
+ let edgesAdded = 1;
372
+
373
+ if (imp.typeOnly) {
374
+ edgesAdded += emitTypeOnlySymbolEdges(db, stmts, imp, resolvedPath, fileNodeId);
375
+ }
376
+ if (!imp.reexport && db) {
377
+ edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeId, resolvedPath, imp);
378
+ }
379
+ return edgesAdded;
380
+ }
381
+
310
382
  function buildImportEdges(
311
383
  stmts: IncrementalStmts,
312
384
  relPath: string,
@@ -318,44 +390,7 @@ function buildImportEdges(
318
390
  ): number {
319
391
  let edgesAdded = 0;
320
392
  for (const imp of symbols.imports) {
321
- const resolvedPath = resolveImportPath(
322
- path.join(rootDir, relPath),
323
- imp.source,
324
- rootDir,
325
- aliases,
326
- );
327
- const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
328
- if (targetRow) {
329
- const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
330
- stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
331
- edgesAdded++;
332
-
333
- // Type-only imports: create symbol-level edges so the target symbols
334
- // get fan-in credit and aren't falsely classified as dead code.
335
- if (imp.typeOnly) {
336
- for (const name of imp.names) {
337
- const cleanName = name.replace(/^\*\s+as\s+/, '');
338
- let targetFile = resolvedPath;
339
- if (db && isBarrelFile(db, resolvedPath)) {
340
- const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
341
- if (actual) targetFile = actual;
342
- }
343
- const candidates = stmts.findNodeInFile.all(cleanName, targetFile) as Array<{
344
- id: number;
345
- file: string;
346
- }>;
347
- if (candidates.length > 0) {
348
- stmts.insertEdge.run(fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0);
349
- edgesAdded++;
350
- }
351
- }
352
- }
353
-
354
- // Barrel resolution: create edges through re-export chains
355
- if (!imp.reexport && db) {
356
- edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeId, resolvedPath, imp);
357
- }
358
- }
393
+ edgesAdded += emitEdgesForImport(stmts, imp, fileNodeId, relPath, rootDir, aliases, db);
359
394
  }
360
395
  return edgesAdded;
361
396
  }
@@ -365,6 +400,7 @@ function buildImportedNamesMap(
365
400
  rootDir: string,
366
401
  relPath: string,
367
402
  aliases: PathAliases,
403
+ db: BetterSqlite3Database,
368
404
  ): Map<string, string> {
369
405
  const importedNames = new Map<string, string>();
370
406
  for (const imp of symbols.imports) {
@@ -375,78 +411,79 @@ function buildImportedNamesMap(
375
411
  aliases,
376
412
  );
377
413
  for (const name of imp.names) {
378
- importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
414
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
415
+ // Mirror full-build's `buildImportedNamesMap`: follow barrel re-exports so
416
+ // `importedNames` maps to the *defining* file, not the barrel. This ensures
417
+ // `computeConfidence` gets `importedFrom === targetFile` and returns 1.0
418
+ // instead of the cross-directory fallback (0.3).
419
+ let targetFile = resolvedPath;
420
+ if (isBarrelFile(db, resolvedPath)) {
421
+ const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
422
+ if (actual) targetFile = actual;
423
+ }
424
+ importedNames.set(cleanName, targetFile);
379
425
  }
380
426
  }
381
427
  return importedNames;
382
428
  }
383
429
 
384
- // ── Call edge building ──────────────────────────────────────────────────
430
+ // ── Class hierarchy edges ───────────────────────────────────────────────
385
431
 
386
- function findCaller(
387
- call: ExtractorOutput['calls'][number],
388
- definitions: ExtractorOutput['definitions'],
389
- relPath: string,
390
- stmts: IncrementalStmts,
391
- ): { id: number } | null {
392
- let caller: { id: number } | null = null;
393
- let callerSpan = Infinity;
394
- for (const def of definitions) {
395
- if (def.line <= call.line) {
396
- const end = def.endLine || Infinity;
397
- if (call.line <= end) {
398
- const span = end - def.line;
399
- if (span < callerSpan) {
400
- const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
401
- if (row) {
402
- caller = row;
403
- callerSpan = span;
404
- }
405
- }
406
- }
407
- }
408
- }
409
- return caller;
410
- }
432
+ type NodeWithKind = { id: number; kind: string; file: string };
411
433
 
412
- function resolveCallTargets(
434
+ const HIERARCHY_SOURCE_KINDS = new Set(['class', 'struct', 'record', 'enum']);
435
+ const EXTENDS_TARGET_KINDS = new Set(['class', 'struct', 'trait', 'record']);
436
+ const IMPLEMENTS_TARGET_KINDS = new Set(['interface', 'trait', 'class']);
437
+
438
+ function buildClassHierarchyEdges(
413
439
  stmts: IncrementalStmts,
414
- call: ExtractorOutput['calls'][number],
415
440
  relPath: string,
416
- importedNames: Map<string, string>,
417
- typeMap: Map<string, unknown>,
418
- ): { targets: Array<{ id: number; file: string }>; importedFrom: string | undefined } {
419
- const importedFrom = importedNames.get(call.name);
420
- let targets: Array<{ id: number; file: string }> | undefined;
421
- if (importedFrom) {
422
- targets = stmts.findNodeInFile.all(call.name, importedFrom) as Array<{
423
- id: number;
424
- file: string;
425
- }>;
426
- }
427
- if (!targets || targets.length === 0) {
428
- targets = stmts.findNodeInFile.all(call.name, relPath) as Array<{ id: number; file: string }>;
429
- if (targets.length === 0) {
430
- targets = stmts.findNodeByName.all(call.name) as Array<{ id: number; file: string }>;
441
+ symbols: ExtractorOutput,
442
+ ): number {
443
+ let edgesAdded = 0;
444
+ for (const cls of symbols.classes) {
445
+ const sourceRow = (stmts.findNodeInFile.all(cls.name, relPath) as NodeWithKind[]).find((n) =>
446
+ HIERARCHY_SOURCE_KINDS.has(n.kind),
447
+ );
448
+ if (!sourceRow) continue;
449
+
450
+ if (cls.extends) {
451
+ for (const t of (stmts.findNodeByName.all(cls.extends) as NodeWithKind[]).filter((n) =>
452
+ EXTENDS_TARGET_KINDS.has(n.kind),
453
+ )) {
454
+ stmts.insertEdge.run(sourceRow.id, t.id, 'extends', 1.0, 0);
455
+ edgesAdded++;
456
+ }
431
457
  }
432
- }
433
- // Type-aware resolution: translate variable receiver to declared type
434
- if ((!targets || targets.length === 0) && call.receiver && typeMap) {
435
- const typeEntry = typeMap.get(call.receiver);
436
- const typeName = typeEntry
437
- ? typeof typeEntry === 'string'
438
- ? typeEntry
439
- : (typeEntry as { type?: string }).type
440
- : null;
441
- if (typeName) {
442
- const qualified = `${typeName}.${call.name}`;
443
- targets = stmts.findNodeByName.all(qualified) as Array<{ id: number; file: string }>;
458
+ if (cls.implements) {
459
+ for (const t of (stmts.findNodeByName.all(cls.implements) as NodeWithKind[]).filter((n) =>
460
+ IMPLEMENTS_TARGET_KINDS.has(n.kind),
461
+ )) {
462
+ stmts.insertEdge.run(sourceRow.id, t.id, 'implements', 1.0, 0);
463
+ edgesAdded++;
464
+ }
444
465
  }
445
466
  }
446
- return { targets: targets ?? [], importedFrom };
467
+ return edgesAdded;
468
+ }
469
+
470
+ // ── Call edge building ──────────────────────────────────────────────────
471
+
472
+ function makeIncrementalLookup(db: BetterSqlite3Database, stmts: IncrementalStmts): CallNodeLookup {
473
+ return {
474
+ byNameAndFile: (name, file) =>
475
+ stmts.findNodeInFile.all(name, file) as Array<{ id: number; file: string; kind?: string }>,
476
+ byName: (name) =>
477
+ stmts.findNodeByName.all(name) as Array<{ id: number; file: string; kind?: string }>,
478
+ isBarrel: (file) => isBarrelFile(db, file),
479
+ resolveBarrel: (barrelFile, symbolName) => resolveBarrelTarget(db, barrelFile, symbolName),
480
+ nodeId: (name, kind, file, line) =>
481
+ stmts.getNodeId.get(name, kind, file, line) as { id: number } | undefined,
482
+ };
447
483
  }
448
484
 
449
485
  function buildCallEdges(
486
+ db: BetterSqlite3Database,
450
487
  stmts: IncrementalStmts,
451
488
  relPath: string,
452
489
  symbols: ExtractorOutput,
@@ -465,13 +502,16 @@ function buildCallEdges(
465
502
  ]),
466
503
  )
467
504
  : new Map();
505
+ const seenCallEdges = new Set<string>();
506
+ const lookup = makeIncrementalLookup(db, stmts);
468
507
  let edgesAdded = 0;
508
+
469
509
  for (const call of symbols.calls) {
470
510
  if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
471
511
 
472
- const caller = findCaller(call, symbols.definitions, relPath, stmts) || fileNodeRow;
512
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
473
513
  const { targets, importedFrom } = resolveCallTargets(
474
- stmts,
514
+ lookup,
475
515
  call,
476
516
  relPath,
477
517
  importedNames,
@@ -479,18 +519,175 @@ function buildCallEdges(
479
519
  );
480
520
 
481
521
  for (const t of targets) {
482
- if (t.id !== caller.id) {
522
+ const edgeKey = `${caller.id}|${t.id}`;
523
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
524
+ seenCallEdges.add(edgeKey);
483
525
  const confidence = computeConfidence(relPath, t.file, importedFrom ?? null);
484
526
  stmts.insertEdge.run(caller.id, t.id, 'calls', confidence, call.dynamic ? 1 : 0);
485
527
  edgesAdded++;
486
528
  }
487
529
  }
530
+
531
+ if (
532
+ call.receiver &&
533
+ !BUILTIN_RECEIVERS.has(call.receiver) &&
534
+ call.receiver !== 'this' &&
535
+ call.receiver !== 'self' &&
536
+ call.receiver !== 'super'
537
+ ) {
538
+ const recv = resolveReceiverEdge(
539
+ lookup,
540
+ { name: call.name, receiver: call.receiver },
541
+ caller,
542
+ relPath,
543
+ typeMap,
544
+ seenCallEdges,
545
+ );
546
+ if (recv) {
547
+ stmts.insertEdge.run(recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0);
548
+ edgesAdded++;
549
+ }
550
+ }
488
551
  }
489
552
  return edgesAdded;
490
553
  }
491
554
 
492
555
  // ── Main entry point ────────────────────────────────────────────────────
493
556
 
557
+ /** Build the "this file was deleted" result returned by `rebuildFile`. */
558
+ function buildDeletionResult(
559
+ relPath: string,
560
+ oldNodes: number,
561
+ edgesBefore: number,
562
+ oldSymbols: unknown[],
563
+ diffSymbols: ((old: unknown[], new_: unknown[]) => unknown) | undefined,
564
+ ): RebuildResult {
565
+ const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, []) : null;
566
+ return {
567
+ file: relPath,
568
+ nodesAdded: 0,
569
+ nodesRemoved: oldNodes,
570
+ edgesAdded: 0,
571
+ edgesBefore,
572
+ deleted: true,
573
+ event: 'deleted',
574
+ symbolDiff,
575
+ nodesBefore: oldNodes,
576
+ nodesAfter: 0,
577
+ };
578
+ }
579
+
580
+ /** Rebuild all edges originating in the single (just-parsed) target file. */
581
+ function rebuildEdgesForTargetFile(
582
+ db: BetterSqlite3Database,
583
+ stmts: IncrementalStmts,
584
+ relPath: string,
585
+ symbols: ExtractorOutput,
586
+ fileNodeRow: { id: number },
587
+ rootDir: string,
588
+ ): number {
589
+ const aliases: PathAliases = { baseUrl: null, paths: {} };
590
+ let edgesAdded = buildContainmentEdges(db, stmts, relPath, symbols);
591
+ edgesAdded += rebuildDirContainment(db, stmts, relPath);
592
+ edgesAdded += buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeRow.id, aliases, db);
593
+ const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases, db);
594
+ edgesAdded += buildCallEdges(db, stmts, relPath, symbols, fileNodeRow, importedNames);
595
+ edgesAdded += buildClassHierarchyEdges(stmts, relPath, symbols);
596
+ return edgesAdded;
597
+ }
598
+
599
+ /**
600
+ * Re-parse the reverse-deps and delete their outgoing edges so the cascade
601
+ * can rebuild them. Returns the parsed symbols map together with the total
602
+ * edge count across all deps measured *before* deletion — callers add this
603
+ * to their own `edgesBefore` so the net delta stays correct even when the
604
+ * reverse-dep cascade re-inserts edges.
605
+ */
606
+ async function parseReverseDeps(
607
+ db: BetterSqlite3Database,
608
+ rootDir: string,
609
+ reverseDeps: string[],
610
+ stmts: IncrementalStmts,
611
+ engineOpts: EngineOpts,
612
+ cache: unknown,
613
+ ): Promise<{ depSymbols: Map<string, ExtractorOutput>; reverseDepsEdgesBefore: number }> {
614
+ const depSymbols = new Map<string, ExtractorOutput>();
615
+ let reverseDepsEdgesBefore = 0;
616
+ for (const depRelPath of reverseDeps) {
617
+ const symbols_ = await parseReverseDep(rootDir, depRelPath, engineOpts, cache);
618
+ if (symbols_) {
619
+ reverseDepsEdgesBefore += stmts.countEdges.get(depRelPath)?.c ?? 0;
620
+ deleteOutgoingEdges(db, depRelPath);
621
+ depSymbols.set(depRelPath, symbols_);
622
+ }
623
+ }
624
+ return { depSymbols, reverseDepsEdgesBefore };
625
+ }
626
+
627
+ /**
628
+ * Pass 2 of the reverse-dep cascade: now that the changed file's `reexports`
629
+ * edges exist, resolve barrel imports for every reverse-dep so transitive
630
+ * call edges through the barrel still find their targets.
631
+ */
632
+ function emitBarrelImportEdgesForReverseDeps(
633
+ db: BetterSqlite3Database,
634
+ stmts: IncrementalStmts,
635
+ depSymbols: Map<string, ExtractorOutput>,
636
+ rootDir: string,
637
+ ): number {
638
+ let edgesAdded = 0;
639
+ for (const [depRelPath, symbols_] of depSymbols) {
640
+ const fileNodeRow_ = stmts.getNodeId.get(depRelPath, 'file', depRelPath, 0);
641
+ if (!fileNodeRow_) continue;
642
+ const aliases_: PathAliases = { baseUrl: null, paths: {} };
643
+ for (const imp of symbols_.imports) {
644
+ if (imp.reexport) continue;
645
+ const resolvedPath = resolveImportPath(
646
+ path.join(rootDir, depRelPath),
647
+ imp.source,
648
+ rootDir,
649
+ aliases_,
650
+ );
651
+ edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeRow_.id, resolvedPath, imp);
652
+ }
653
+ }
654
+ return edgesAdded;
655
+ }
656
+
657
+ /**
658
+ * Two-pass reverse-dep cascade:
659
+ * 1. Rebuild direct edges (creating `reexports` edges for barrels).
660
+ * 2. Add barrel import edges (which need `reexports` edges to exist).
661
+ * Returns both the gross edges-added count and the pre-deletion edge count
662
+ * for all reverse deps so callers can compute a true net delta.
663
+ */
664
+ async function runReverseDepCascade(
665
+ db: BetterSqlite3Database,
666
+ rootDir: string,
667
+ reverseDeps: string[],
668
+ stmts: IncrementalStmts,
669
+ engineOpts: EngineOpts,
670
+ cache: unknown,
671
+ ): Promise<{ edgesAdded: number; reverseDepsEdgesBefore: number }> {
672
+ const { depSymbols, reverseDepsEdgesBefore } = await parseReverseDeps(
673
+ db,
674
+ rootDir,
675
+ reverseDeps,
676
+ stmts,
677
+ engineOpts,
678
+ cache,
679
+ );
680
+
681
+ let edgesAdded = 0;
682
+ // Pass 1: direct edges only (no barrel resolution) — creates reexports edges
683
+ for (const [depRelPath, symbols_] of depSymbols) {
684
+ edgesAdded += rebuildReverseDepEdges(db, rootDir, depRelPath, symbols_, stmts, true);
685
+ }
686
+ // Pass 2: add barrel import edges (reexports edges now exist)
687
+ edgesAdded += emitBarrelImportEdgesForReverseDeps(db, stmts, depSymbols, rootDir);
688
+ return { edgesAdded, reverseDepsEdgesBefore };
689
+ }
690
+
494
691
  /**
495
692
  * Parse a single file and update the database incrementally.
496
693
  */
@@ -506,6 +703,7 @@ export async function rebuildFile(
506
703
  const { diffSymbols } = options;
507
704
  const relPath = normalizePath(path.relative(rootDir, filePath));
508
705
  const oldNodes = stmts.countNodes.get(relPath)?.c || 0;
706
+ const edgesBefore = stmts.countEdges.get(relPath)?.c || 0;
509
707
  const oldSymbols: unknown[] = diffSymbols ? stmts.listSymbols.all(relPath) : [];
510
708
 
511
709
  // Find reverse-deps BEFORE purging (edges still reference the old nodes)
@@ -519,18 +717,7 @@ export async function rebuildFile(
519
717
 
520
718
  if (!fs.existsSync(filePath)) {
521
719
  if (cache) (cache as { remove(p: string): void }).remove(filePath);
522
- const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, []) : null;
523
- return {
524
- file: relPath,
525
- nodesAdded: 0,
526
- nodesRemoved: oldNodes,
527
- edgesAdded: 0,
528
- deleted: true,
529
- event: 'deleted',
530
- symbolDiff,
531
- nodesBefore: oldNodes,
532
- nodesAfter: 0,
533
- };
720
+ return buildDeletionResult(relPath, oldNodes, edgesBefore, oldSymbols, diffSymbols);
534
721
  }
535
722
 
536
723
  let code: string;
@@ -551,47 +738,28 @@ export async function rebuildFile(
551
738
 
552
739
  const fileNodeRow = stmts.getNodeId.get(relPath, 'file', relPath, 0);
553
740
  if (!fileNodeRow)
554
- return { file: relPath, nodesAdded: newNodes, nodesRemoved: oldNodes, edgesAdded: 0 };
555
-
556
- const aliases: PathAliases = { baseUrl: null, paths: {} };
557
-
558
- let edgesAdded = buildContainmentEdges(db, stmts, relPath, symbols);
559
- edgesAdded += rebuildDirContainment(db, stmts, relPath);
560
- edgesAdded += buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeRow.id, aliases, db);
561
- const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases);
562
- edgesAdded += buildCallEdges(stmts, relPath, symbols, fileNodeRow, importedNames);
741
+ return {
742
+ file: relPath,
743
+ nodesAdded: newNodes,
744
+ nodesRemoved: oldNodes,
745
+ edgesAdded: 0,
746
+ edgesBefore,
747
+ };
563
748
 
564
- // Cascade: rebuild outgoing edges for reverse-dep files.
565
- // Two-pass approach: first rebuild direct edges (creating reexports edges for barrels),
566
- // then add barrel import edges (which need reexports edges to exist for resolution).
567
- const depSymbols = new Map<string, ExtractorOutput>();
568
- for (const depRelPath of reverseDeps) {
569
- const symbols_ = await parseReverseDep(rootDir, depRelPath, engineOpts, cache);
570
- if (symbols_) {
571
- deleteOutgoingEdges(db, depRelPath);
572
- depSymbols.set(depRelPath, symbols_);
573
- }
574
- }
575
- // Pass 1: direct edges only (no barrel resolution) creates reexports edges
576
- for (const [depRelPath, symbols_] of depSymbols) {
577
- edgesAdded += rebuildReverseDepEdges(db, rootDir, depRelPath, symbols_, stmts, true);
578
- }
579
- // Pass 2: add barrel import edges (reexports edges now exist)
580
- for (const [depRelPath, symbols_] of depSymbols) {
581
- const fileNodeRow_ = stmts.getNodeId.get(depRelPath, 'file', depRelPath, 0);
582
- if (!fileNodeRow_) continue;
583
- const aliases_: PathAliases = { baseUrl: null, paths: {} };
584
- for (const imp of symbols_.imports) {
585
- if (imp.reexport) continue;
586
- const resolvedPath = resolveImportPath(
587
- path.join(rootDir, depRelPath),
588
- imp.source,
589
- rootDir,
590
- aliases_,
591
- );
592
- edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeRow_.id, resolvedPath, imp);
593
- }
594
- }
749
+ let edgesAdded = rebuildEdgesForTargetFile(db, stmts, relPath, symbols, fileNodeRow, rootDir);
750
+ const { edgesAdded: cascadeEdges, reverseDepsEdgesBefore } = await runReverseDepCascade(
751
+ db,
752
+ rootDir,
753
+ reverseDeps,
754
+ stmts,
755
+ engineOpts,
756
+ cache,
757
+ );
758
+ edgesAdded += cascadeEdges;
759
+ // Include pre-deletion edge counts from reverse deps so the net delta
760
+ // (edgesAdded - edgesBefore) is correct even when the cascade re-inserts
761
+ // their edges unchanged.
762
+ const totalEdgesBefore = edgesBefore + reverseDepsEdgesBefore;
595
763
 
596
764
  const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, newSymbols) : null;
597
765
  const event = oldNodes === 0 ? 'added' : 'modified';
@@ -601,6 +769,7 @@ export async function rebuildFile(
601
769
  nodesAdded: newNodes,
602
770
  nodesRemoved: oldNodes,
603
771
  edgesAdded,
772
+ edgesBefore: totalEdgesBefore,
604
773
  deleted: false,
605
774
  event,
606
775
  symbolDiff,