@optave/codegraph 3.11.0 → 3.11.1

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 (223) 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/helpers.d.ts +4 -4
  28. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  29. package/dist/domain/graph/builder/helpers.js +47 -33
  30. package/dist/domain/graph/builder/helpers.js.map +1 -1
  31. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  32. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/incremental.js +142 -76
  34. package/dist/domain/graph/builder/incremental.js.map +1 -1
  35. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  36. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/pipeline.js +10 -766
  38. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  39. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  40. package/dist/domain/graph/builder/stages/build-edges.js +133 -96
  41. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  42. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  44. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  45. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  46. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  47. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  48. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  49. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  50. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  51. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  52. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  54. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  56. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  57. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  58. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  59. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  60. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  62. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  63. package/dist/domain/graph/cycles.d.ts +6 -4
  64. package/dist/domain/graph/cycles.d.ts.map +1 -1
  65. package/dist/domain/graph/cycles.js +50 -55
  66. package/dist/domain/graph/cycles.js.map +1 -1
  67. package/dist/domain/graph/journal.d.ts.map +1 -1
  68. package/dist/domain/graph/journal.js +89 -70
  69. package/dist/domain/graph/journal.js.map +1 -1
  70. package/dist/domain/graph/watcher.d.ts.map +1 -1
  71. package/dist/domain/graph/watcher.js +5 -2
  72. package/dist/domain/graph/watcher.js.map +1 -1
  73. package/dist/domain/parser.d.ts +12 -23
  74. package/dist/domain/parser.d.ts.map +1 -1
  75. package/dist/domain/parser.js +126 -79
  76. package/dist/domain/parser.js.map +1 -1
  77. package/dist/domain/search/generator.d.ts +3 -1
  78. package/dist/domain/search/generator.d.ts.map +1 -1
  79. package/dist/domain/search/generator.js +68 -45
  80. package/dist/domain/search/generator.js.map +1 -1
  81. package/dist/domain/search/models.d.ts +2 -0
  82. package/dist/domain/search/models.d.ts.map +1 -1
  83. package/dist/domain/search/models.js +37 -3
  84. package/dist/domain/search/models.js.map +1 -1
  85. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  86. package/dist/domain/search/search/hybrid.js +49 -40
  87. package/dist/domain/search/search/hybrid.js.map +1 -1
  88. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  89. package/dist/domain/search/search/semantic.js +69 -49
  90. package/dist/domain/search/search/semantic.js.map +1 -1
  91. package/dist/domain/wasm-worker-entry.js +201 -136
  92. package/dist/domain/wasm-worker-entry.js.map +1 -1
  93. package/dist/extractors/elixir.js +95 -71
  94. package/dist/extractors/elixir.js.map +1 -1
  95. package/dist/extractors/gleam.d.ts.map +1 -1
  96. package/dist/extractors/gleam.js +23 -31
  97. package/dist/extractors/gleam.js.map +1 -1
  98. package/dist/extractors/helpers.d.ts +79 -1
  99. package/dist/extractors/helpers.d.ts.map +1 -1
  100. package/dist/extractors/helpers.js +137 -0
  101. package/dist/extractors/helpers.js.map +1 -1
  102. package/dist/extractors/java.d.ts.map +1 -1
  103. package/dist/extractors/java.js +37 -49
  104. package/dist/extractors/java.js.map +1 -1
  105. package/dist/extractors/javascript.d.ts.map +1 -1
  106. package/dist/extractors/javascript.js +44 -44
  107. package/dist/extractors/javascript.js.map +1 -1
  108. package/dist/extractors/julia.js +27 -34
  109. package/dist/extractors/julia.js.map +1 -1
  110. package/dist/extractors/r.d.ts.map +1 -1
  111. package/dist/extractors/r.js +33 -58
  112. package/dist/extractors/r.js.map +1 -1
  113. package/dist/extractors/solidity.d.ts.map +1 -1
  114. package/dist/extractors/solidity.js +38 -61
  115. package/dist/extractors/solidity.js.map +1 -1
  116. package/dist/features/boundaries.d.ts.map +1 -1
  117. package/dist/features/boundaries.js +49 -39
  118. package/dist/features/boundaries.js.map +1 -1
  119. package/dist/features/cfg.d.ts.map +1 -1
  120. package/dist/features/cfg.js +90 -63
  121. package/dist/features/cfg.js.map +1 -1
  122. package/dist/features/check.d.ts.map +1 -1
  123. package/dist/features/check.js +43 -34
  124. package/dist/features/check.js.map +1 -1
  125. package/dist/features/cochange.d.ts.map +1 -1
  126. package/dist/features/cochange.js +68 -56
  127. package/dist/features/cochange.js.map +1 -1
  128. package/dist/features/complexity.d.ts.map +1 -1
  129. package/dist/features/complexity.js +105 -75
  130. package/dist/features/complexity.js.map +1 -1
  131. package/dist/features/dataflow.d.ts.map +1 -1
  132. package/dist/features/dataflow.js +37 -29
  133. package/dist/features/dataflow.js.map +1 -1
  134. package/dist/features/flow.d.ts.map +1 -1
  135. package/dist/features/flow.js +31 -22
  136. package/dist/features/flow.js.map +1 -1
  137. package/dist/features/graph-enrichment.d.ts.map +1 -1
  138. package/dist/features/graph-enrichment.js +77 -70
  139. package/dist/features/graph-enrichment.js.map +1 -1
  140. package/dist/features/owners.d.ts +17 -26
  141. package/dist/features/owners.d.ts.map +1 -1
  142. package/dist/features/owners.js +120 -109
  143. package/dist/features/owners.js.map +1 -1
  144. package/dist/features/sequence.d.ts.map +1 -1
  145. package/dist/features/sequence.js +59 -54
  146. package/dist/features/sequence.js.map +1 -1
  147. package/dist/features/structure-query.d.ts.map +1 -1
  148. package/dist/features/structure-query.js +60 -60
  149. package/dist/features/structure-query.js.map +1 -1
  150. package/dist/features/structure.js +28 -36
  151. package/dist/features/structure.js.map +1 -1
  152. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  153. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  154. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  155. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  156. package/dist/graph/classifiers/roles.js +63 -59
  157. package/dist/graph/classifiers/roles.js.map +1 -1
  158. package/dist/infrastructure/config.d.ts +1 -1
  159. package/dist/infrastructure/config.d.ts.map +1 -1
  160. package/dist/infrastructure/config.js +1 -1
  161. package/dist/infrastructure/config.js.map +1 -1
  162. package/dist/presentation/cfg.d.ts.map +1 -1
  163. package/dist/presentation/cfg.js +44 -29
  164. package/dist/presentation/cfg.js.map +1 -1
  165. package/dist/presentation/flow.d.ts.map +1 -1
  166. package/dist/presentation/flow.js +58 -38
  167. package/dist/presentation/flow.js.map +1 -1
  168. package/dist/types.d.ts +1 -1
  169. package/dist/types.d.ts.map +1 -1
  170. package/package.json +7 -7
  171. package/src/ast-analysis/engine.ts +145 -61
  172. package/src/ast-analysis/visitor-utils.ts +86 -46
  173. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  174. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  175. package/src/cli/commands/embed.ts +54 -4
  176. package/src/domain/analysis/dependencies.ts +166 -85
  177. package/src/domain/analysis/fn-impact.ts +120 -50
  178. package/src/domain/analysis/module-map.ts +175 -140
  179. package/src/domain/graph/builder/helpers.ts +85 -76
  180. package/src/domain/graph/builder/incremental.ts +217 -90
  181. package/src/domain/graph/builder/pipeline.ts +19 -957
  182. package/src/domain/graph/builder/stages/build-edges.ts +198 -140
  183. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  184. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  185. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  186. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  187. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  188. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  189. package/src/domain/graph/cycles.ts +51 -49
  190. package/src/domain/graph/journal.ts +84 -69
  191. package/src/domain/graph/watcher.ts +8 -2
  192. package/src/domain/parser.ts +143 -66
  193. package/src/domain/search/generator.ts +132 -74
  194. package/src/domain/search/models.ts +39 -3
  195. package/src/domain/search/search/hybrid.ts +53 -42
  196. package/src/domain/search/search/semantic.ts +105 -65
  197. package/src/domain/wasm-worker-entry.ts +235 -152
  198. package/src/extractors/elixir.ts +91 -64
  199. package/src/extractors/gleam.ts +33 -37
  200. package/src/extractors/helpers.ts +205 -1
  201. package/src/extractors/java.ts +42 -45
  202. package/src/extractors/javascript.ts +44 -43
  203. package/src/extractors/julia.ts +28 -35
  204. package/src/extractors/r.ts +38 -56
  205. package/src/extractors/solidity.ts +43 -71
  206. package/src/features/boundaries.ts +64 -46
  207. package/src/features/cfg.ts +145 -74
  208. package/src/features/check.ts +60 -43
  209. package/src/features/cochange.ts +95 -72
  210. package/src/features/complexity.ts +134 -79
  211. package/src/features/dataflow.ts +57 -34
  212. package/src/features/flow.ts +48 -24
  213. package/src/features/graph-enrichment.ts +105 -70
  214. package/src/features/owners.ts +186 -146
  215. package/src/features/sequence.ts +99 -69
  216. package/src/features/structure-query.ts +94 -79
  217. package/src/features/structure.ts +56 -56
  218. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  219. package/src/graph/classifiers/roles.ts +64 -54
  220. package/src/infrastructure/config.ts +1 -1
  221. package/src/presentation/cfg.ts +48 -32
  222. package/src/presentation/flow.ts +100 -52
  223. package/src/types.ts +1 -1
@@ -30,6 +30,7 @@ export interface IncrementalStmts {
30
30
  insertEdge: { run: (...params: unknown[]) => unknown };
31
31
  getNodeId: { get: (...params: unknown[]) => { id: number } | undefined };
32
32
  countNodes: { get: (...params: unknown[]) => { c: number } | undefined };
33
+ countEdges: { get: (...params: unknown[]) => { c: number } | undefined };
33
34
  listSymbols: { all: (...params: unknown[]) => unknown[] };
34
35
  findNodeInFile: { all: (...params: unknown[]) => unknown[] };
35
36
  findNodeByName: { all: (...params: unknown[]) => unknown[] };
@@ -40,6 +41,7 @@ interface RebuildResult {
40
41
  nodesAdded: number;
41
42
  nodesRemoved: number;
42
43
  edgesAdded: number;
44
+ edgesBefore: number;
43
45
  deleted?: boolean;
44
46
  event?: string;
45
47
  symbolDiff?: unknown;
@@ -307,6 +309,63 @@ function resolveBarrelImportEdges(
307
309
  return edgesAdded;
308
310
  }
309
311
 
312
+ /** Emit symbol-level `imports-type` edges for a single `import type` statement. */
313
+ function emitTypeOnlySymbolEdges(
314
+ db: BetterSqlite3Database | null,
315
+ stmts: IncrementalStmts,
316
+ imp: ExtractorOutput['imports'][number],
317
+ resolvedPath: string,
318
+ fileNodeId: number,
319
+ ): number {
320
+ let edgesAdded = 0;
321
+ for (const name of imp.names) {
322
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
323
+ let targetFile = resolvedPath;
324
+ if (db && isBarrelFile(db, resolvedPath)) {
325
+ const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
326
+ if (actual) targetFile = actual;
327
+ }
328
+ const candidates = stmts.findNodeInFile.all(cleanName, targetFile) as Array<{
329
+ id: number;
330
+ file: string;
331
+ }>;
332
+ if (candidates.length === 0) continue;
333
+ stmts.insertEdge.run(fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0);
334
+ edgesAdded++;
335
+ }
336
+ return edgesAdded;
337
+ }
338
+
339
+ /**
340
+ * Process a single import statement: emit the file→file edge, any
341
+ * symbol-level type-only edges, and barrel re-export edges.
342
+ */
343
+ function emitEdgesForImport(
344
+ stmts: IncrementalStmts,
345
+ imp: ExtractorOutput['imports'][number],
346
+ fileNodeId: number,
347
+ relPath: string,
348
+ rootDir: string,
349
+ aliases: PathAliases,
350
+ db: BetterSqlite3Database | null,
351
+ ): number {
352
+ const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
353
+ const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
354
+ if (!targetRow) return 0;
355
+
356
+ const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
357
+ stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
358
+ let edgesAdded = 1;
359
+
360
+ if (imp.typeOnly) {
361
+ edgesAdded += emitTypeOnlySymbolEdges(db, stmts, imp, resolvedPath, fileNodeId);
362
+ }
363
+ if (!imp.reexport && db) {
364
+ edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeId, resolvedPath, imp);
365
+ }
366
+ return edgesAdded;
367
+ }
368
+
310
369
  function buildImportEdges(
311
370
  stmts: IncrementalStmts,
312
371
  relPath: string,
@@ -318,44 +377,7 @@ function buildImportEdges(
318
377
  ): number {
319
378
  let edgesAdded = 0;
320
379
  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
- }
380
+ edgesAdded += emitEdgesForImport(stmts, imp, fileNodeId, relPath, rootDir, aliases, db);
359
381
  }
360
382
  return edgesAdded;
361
383
  }
@@ -491,6 +513,139 @@ function buildCallEdges(
491
513
 
492
514
  // ── Main entry point ────────────────────────────────────────────────────
493
515
 
516
+ /** Build the "this file was deleted" result returned by `rebuildFile`. */
517
+ function buildDeletionResult(
518
+ relPath: string,
519
+ oldNodes: number,
520
+ edgesBefore: number,
521
+ oldSymbols: unknown[],
522
+ diffSymbols: ((old: unknown[], new_: unknown[]) => unknown) | undefined,
523
+ ): RebuildResult {
524
+ const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, []) : null;
525
+ return {
526
+ file: relPath,
527
+ nodesAdded: 0,
528
+ nodesRemoved: oldNodes,
529
+ edgesAdded: 0,
530
+ edgesBefore,
531
+ deleted: true,
532
+ event: 'deleted',
533
+ symbolDiff,
534
+ nodesBefore: oldNodes,
535
+ nodesAfter: 0,
536
+ };
537
+ }
538
+
539
+ /** Rebuild all edges originating in the single (just-parsed) target file. */
540
+ function rebuildEdgesForTargetFile(
541
+ db: BetterSqlite3Database,
542
+ stmts: IncrementalStmts,
543
+ relPath: string,
544
+ symbols: ExtractorOutput,
545
+ fileNodeRow: { id: number },
546
+ rootDir: string,
547
+ ): number {
548
+ const aliases: PathAliases = { baseUrl: null, paths: {} };
549
+ let edgesAdded = buildContainmentEdges(db, stmts, relPath, symbols);
550
+ edgesAdded += rebuildDirContainment(db, stmts, relPath);
551
+ edgesAdded += buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeRow.id, aliases, db);
552
+ const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases);
553
+ edgesAdded += buildCallEdges(stmts, relPath, symbols, fileNodeRow, importedNames);
554
+ return edgesAdded;
555
+ }
556
+
557
+ /**
558
+ * Re-parse the reverse-deps and delete their outgoing edges so the cascade
559
+ * can rebuild them. Returns the parsed symbols map together with the total
560
+ * edge count across all deps measured *before* deletion — callers add this
561
+ * to their own `edgesBefore` so the net delta stays correct even when the
562
+ * reverse-dep cascade re-inserts edges.
563
+ */
564
+ async function parseReverseDeps(
565
+ db: BetterSqlite3Database,
566
+ rootDir: string,
567
+ reverseDeps: string[],
568
+ stmts: IncrementalStmts,
569
+ engineOpts: EngineOpts,
570
+ cache: unknown,
571
+ ): Promise<{ depSymbols: Map<string, ExtractorOutput>; reverseDepsEdgesBefore: number }> {
572
+ const depSymbols = new Map<string, ExtractorOutput>();
573
+ let reverseDepsEdgesBefore = 0;
574
+ for (const depRelPath of reverseDeps) {
575
+ const symbols_ = await parseReverseDep(rootDir, depRelPath, engineOpts, cache);
576
+ if (symbols_) {
577
+ reverseDepsEdgesBefore += stmts.countEdges.get(depRelPath)?.c ?? 0;
578
+ deleteOutgoingEdges(db, depRelPath);
579
+ depSymbols.set(depRelPath, symbols_);
580
+ }
581
+ }
582
+ return { depSymbols, reverseDepsEdgesBefore };
583
+ }
584
+
585
+ /**
586
+ * Pass 2 of the reverse-dep cascade: now that the changed file's `reexports`
587
+ * edges exist, resolve barrel imports for every reverse-dep so transitive
588
+ * call edges through the barrel still find their targets.
589
+ */
590
+ function emitBarrelImportEdgesForReverseDeps(
591
+ db: BetterSqlite3Database,
592
+ stmts: IncrementalStmts,
593
+ depSymbols: Map<string, ExtractorOutput>,
594
+ rootDir: string,
595
+ ): number {
596
+ let edgesAdded = 0;
597
+ for (const [depRelPath, symbols_] of depSymbols) {
598
+ const fileNodeRow_ = stmts.getNodeId.get(depRelPath, 'file', depRelPath, 0);
599
+ if (!fileNodeRow_) continue;
600
+ const aliases_: PathAliases = { baseUrl: null, paths: {} };
601
+ for (const imp of symbols_.imports) {
602
+ if (imp.reexport) continue;
603
+ const resolvedPath = resolveImportPath(
604
+ path.join(rootDir, depRelPath),
605
+ imp.source,
606
+ rootDir,
607
+ aliases_,
608
+ );
609
+ edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeRow_.id, resolvedPath, imp);
610
+ }
611
+ }
612
+ return edgesAdded;
613
+ }
614
+
615
+ /**
616
+ * Two-pass reverse-dep cascade:
617
+ * 1. Rebuild direct edges (creating `reexports` edges for barrels).
618
+ * 2. Add barrel import edges (which need `reexports` edges to exist).
619
+ * Returns both the gross edges-added count and the pre-deletion edge count
620
+ * for all reverse deps so callers can compute a true net delta.
621
+ */
622
+ async function runReverseDepCascade(
623
+ db: BetterSqlite3Database,
624
+ rootDir: string,
625
+ reverseDeps: string[],
626
+ stmts: IncrementalStmts,
627
+ engineOpts: EngineOpts,
628
+ cache: unknown,
629
+ ): Promise<{ edgesAdded: number; reverseDepsEdgesBefore: number }> {
630
+ const { depSymbols, reverseDepsEdgesBefore } = await parseReverseDeps(
631
+ db,
632
+ rootDir,
633
+ reverseDeps,
634
+ stmts,
635
+ engineOpts,
636
+ cache,
637
+ );
638
+
639
+ let edgesAdded = 0;
640
+ // Pass 1: direct edges only (no barrel resolution) — creates reexports edges
641
+ for (const [depRelPath, symbols_] of depSymbols) {
642
+ edgesAdded += rebuildReverseDepEdges(db, rootDir, depRelPath, symbols_, stmts, true);
643
+ }
644
+ // Pass 2: add barrel import edges (reexports edges now exist)
645
+ edgesAdded += emitBarrelImportEdgesForReverseDeps(db, stmts, depSymbols, rootDir);
646
+ return { edgesAdded, reverseDepsEdgesBefore };
647
+ }
648
+
494
649
  /**
495
650
  * Parse a single file and update the database incrementally.
496
651
  */
@@ -506,6 +661,7 @@ export async function rebuildFile(
506
661
  const { diffSymbols } = options;
507
662
  const relPath = normalizePath(path.relative(rootDir, filePath));
508
663
  const oldNodes = stmts.countNodes.get(relPath)?.c || 0;
664
+ const edgesBefore = stmts.countEdges.get(relPath)?.c || 0;
509
665
  const oldSymbols: unknown[] = diffSymbols ? stmts.listSymbols.all(relPath) : [];
510
666
 
511
667
  // Find reverse-deps BEFORE purging (edges still reference the old nodes)
@@ -519,18 +675,7 @@ export async function rebuildFile(
519
675
 
520
676
  if (!fs.existsSync(filePath)) {
521
677
  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
- };
678
+ return buildDeletionResult(relPath, oldNodes, edgesBefore, oldSymbols, diffSymbols);
534
679
  }
535
680
 
536
681
  let code: string;
@@ -551,47 +696,28 @@ export async function rebuildFile(
551
696
 
552
697
  const fileNodeRow = stmts.getNodeId.get(relPath, 'file', relPath, 0);
553
698
  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);
699
+ return {
700
+ file: relPath,
701
+ nodesAdded: newNodes,
702
+ nodesRemoved: oldNodes,
703
+ edgesAdded: 0,
704
+ edgesBefore,
705
+ };
563
706
 
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
- }
707
+ let edgesAdded = rebuildEdgesForTargetFile(db, stmts, relPath, symbols, fileNodeRow, rootDir);
708
+ const { edgesAdded: cascadeEdges, reverseDepsEdgesBefore } = await runReverseDepCascade(
709
+ db,
710
+ rootDir,
711
+ reverseDeps,
712
+ stmts,
713
+ engineOpts,
714
+ cache,
715
+ );
716
+ edgesAdded += cascadeEdges;
717
+ // Include pre-deletion edge counts from reverse deps so the net delta
718
+ // (edgesAdded - edgesBefore) is correct even when the cascade re-inserts
719
+ // their edges unchanged.
720
+ const totalEdgesBefore = edgesBefore + reverseDepsEdgesBefore;
595
721
 
596
722
  const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, newSymbols) : null;
597
723
  const event = oldNodes === 0 ? 'added' : 'modified';
@@ -601,6 +727,7 @@ export async function rebuildFile(
601
727
  nodesAdded: newNodes,
602
728
  nodesRemoved: oldNodes,
603
729
  edgesAdded,
730
+ edgesBefore: totalEdgesBefore,
604
731
  deleted: false,
605
732
  event,
606
733
  symbolDiff,