@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
@@ -58,9 +58,32 @@ export function fileDepsData(
58
58
  *
59
59
  * Uses Repository.findCallers() so it works with both native and WASM engines.
60
60
  */
61
+ type CallerRow = { id: number; name: string; kind: string; file: string; line: number };
62
+
63
+ /** Compute the next BFS frontier from a batched upstream-callers lookup. */
64
+ function buildNextCallerFrontier(
65
+ unvisited: CallerRow[],
66
+ batchCallers: Map<number, CallerRow[]>,
67
+ visited: Set<number>,
68
+ noTests: boolean,
69
+ ): CallerRow[] {
70
+ const nextFrontier: CallerRow[] = [];
71
+ const nextFrontierIds = new Set<number>();
72
+ for (const f of unvisited) {
73
+ const upstream = batchCallers.get(f.id) || [];
74
+ for (const u of upstream) {
75
+ if (noTests && isTestFile(u.file)) continue;
76
+ if (visited.has(u.id) || nextFrontierIds.has(u.id)) continue;
77
+ nextFrontierIds.add(u.id);
78
+ nextFrontier.push(u);
79
+ }
80
+ }
81
+ return nextFrontier;
82
+ }
83
+
61
84
  function buildTransitiveCallers(
62
85
  repo: InstanceType<typeof Repository>,
63
- callers: Array<{ id: number; name: string; kind: string; file: string; line: number }>,
86
+ callers: CallerRow[],
64
87
  nodeId: number,
65
88
  depth: number,
66
89
  noTests: boolean,
@@ -81,18 +104,8 @@ function buildTransitiveCallers(
81
104
  if (unvisited.length === 0) break;
82
105
 
83
106
  const batchCallers = repo.findCallersBatch(unvisited.map((f) => f.id));
84
- const nextFrontier: typeof frontier = [];
85
- const nextFrontierIds = new Set<number>();
86
- for (const f of unvisited) {
87
- const upstream = batchCallers.get(f.id) || [];
88
- for (const u of upstream) {
89
- if (noTests && isTestFile(u.file)) continue;
90
- if (!visited.has(u.id) && !nextFrontierIds.has(u.id)) {
91
- nextFrontierIds.add(u.id);
92
- nextFrontier.push(u);
93
- }
94
- }
95
- }
107
+ const nextFrontier = buildNextCallerFrontier(unvisited, batchCallers, visited, noTests);
108
+
96
109
  if (nextFrontier.length > 0) {
97
110
  transitiveCallers[d] = nextFrontier.map((n) => ({
98
111
  name: n.name,
@@ -258,22 +271,30 @@ function resolveEndpoints(
258
271
  };
259
272
  }
260
273
 
261
- /**
262
- * BFS from sourceId toward targetId.
263
- * Returns { found, parent, alternateCount, foundDepth }.
264
- * `parent` maps nodeId -> { parentId, edgeKind }.
265
- */
266
- function bfsShortestPath(
274
+ type NeighborRow = {
275
+ id: number;
276
+ name: string;
277
+ kind: string;
278
+ file: string;
279
+ line: number;
280
+ edge_kind: string;
281
+ };
282
+
283
+ type BfsShortestState = {
284
+ visited: Set<number>;
285
+ parent: Map<number, { parentId: number; edgeKind: string }>;
286
+ found: boolean;
287
+ foundDepth: number;
288
+ alternateCount: number;
289
+ };
290
+
291
+ /** Build the SQL statement that yields neighbors of a node id in the requested direction. */
292
+ function buildNeighborStmt(
267
293
  db: BetterSqlite3Database,
268
- sourceId: number,
269
- targetId: number,
270
294
  edgeKinds: string[],
271
295
  reverse: boolean,
272
- maxDepth: number,
273
- noTests: boolean,
274
- ) {
296
+ ): ReturnType<BetterSqlite3Database['prepare']> {
275
297
  const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
276
-
277
298
  // Forward: source_id -> target_id (A calls... calls B)
278
299
  // Reverse: target_id -> source_id (B is called by... called by A)
279
300
  const neighborQuery = reverse
@@ -283,50 +304,78 @@ function bfsShortestPath(
283
304
  : `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
284
305
  FROM edges e JOIN nodes n ON e.target_id = n.id
285
306
  WHERE e.source_id = ? AND e.kind IN (${kindPlaceholders})`;
286
- const neighborStmt = db.prepare(neighborQuery);
307
+ return db.prepare(neighborQuery);
308
+ }
309
+
310
+ /** Process a single neighbor row during BFS; returns true once the target has been reached. */
311
+ function visitNeighbor(
312
+ n: NeighborRow,
313
+ currentId: number,
314
+ depth: number,
315
+ targetId: number,
316
+ state: BfsShortestState,
317
+ nextQueue: number[],
318
+ noTests: boolean,
319
+ ): void {
320
+ if (noTests && isTestFile(n.file)) return;
321
+ if (n.id === targetId) {
322
+ if (!state.found) {
323
+ state.found = true;
324
+ state.foundDepth = depth;
325
+ state.parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
326
+ }
327
+ state.alternateCount++;
328
+ return;
329
+ }
330
+ if (state.visited.has(n.id)) return;
331
+ state.visited.add(n.id);
332
+ state.parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
333
+ nextQueue.push(n.id);
334
+ }
287
335
 
288
- const visited = new Set([sourceId]);
289
- const parent = new Map<number, { parentId: number; edgeKind: string }>();
336
+ /**
337
+ * BFS from sourceId toward targetId.
338
+ * Returns { found, parent, alternateCount, foundDepth }.
339
+ * `parent` maps nodeId -> { parentId, edgeKind }.
340
+ */
341
+ function bfsShortestPath(
342
+ db: BetterSqlite3Database,
343
+ sourceId: number,
344
+ targetId: number,
345
+ edgeKinds: string[],
346
+ reverse: boolean,
347
+ maxDepth: number,
348
+ noTests: boolean,
349
+ ) {
350
+ const neighborStmt = buildNeighborStmt(db, edgeKinds, reverse);
351
+ const state: BfsShortestState = {
352
+ visited: new Set([sourceId]),
353
+ parent: new Map(),
354
+ found: false,
355
+ foundDepth: -1,
356
+ alternateCount: 0,
357
+ };
290
358
  let queue = [sourceId];
291
- let found = false;
292
- let alternateCount = 0;
293
- let foundDepth = -1;
294
359
 
295
360
  for (let depth = 1; depth <= maxDepth; depth++) {
296
361
  const nextQueue: number[] = [];
297
362
  for (const currentId of queue) {
298
- const neighbors = neighborStmt.all(currentId, ...edgeKinds) as Array<{
299
- id: number;
300
- name: string;
301
- kind: string;
302
- file: string;
303
- line: number;
304
- edge_kind: string;
305
- }>;
363
+ const neighbors = neighborStmt.all(currentId, ...edgeKinds) as NeighborRow[];
306
364
  for (const n of neighbors) {
307
- if (noTests && isTestFile(n.file)) continue;
308
- if (n.id === targetId) {
309
- if (!found) {
310
- found = true;
311
- foundDepth = depth;
312
- parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
313
- }
314
- alternateCount++;
315
- continue;
316
- }
317
- if (!visited.has(n.id)) {
318
- visited.add(n.id);
319
- parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
320
- nextQueue.push(n.id);
321
- }
365
+ visitNeighbor(n, currentId, depth, targetId, state, nextQueue, noTests);
322
366
  }
323
367
  }
324
- if (found) break;
368
+ if (state.found) break;
325
369
  queue = nextQueue;
326
370
  if (queue.length === 0) break;
327
371
  }
328
372
 
329
- return { found, parent, alternateCount, foundDepth };
373
+ return {
374
+ found: state.found,
375
+ parent: state.parent,
376
+ alternateCount: state.alternateCount,
377
+ foundDepth: state.foundDepth,
378
+ };
330
379
  }
331
380
 
332
381
  /**
@@ -474,6 +523,53 @@ export function pathData(
474
523
 
475
524
  // ── File-level shortest path ────────────────────────────────────────────
476
525
 
526
+ type FileBfsState = {
527
+ visited: Set<string>;
528
+ parentMap: Map<string, string>;
529
+ found: boolean;
530
+ alternateCount: number;
531
+ };
532
+
533
+ /** Process a neighbor file during file-level BFS; updates state in place. */
534
+ function visitFileNeighbor(
535
+ neighborFile: string,
536
+ currentFile: string,
537
+ targetFile: string,
538
+ state: FileBfsState,
539
+ nextQueue: string[],
540
+ noTests: boolean,
541
+ ): void {
542
+ if (noTests && isTestFile(neighborFile)) return;
543
+ if (neighborFile === targetFile) {
544
+ if (!state.found) {
545
+ state.found = true;
546
+ state.parentMap.set(neighborFile, currentFile);
547
+ }
548
+ state.alternateCount++;
549
+ return;
550
+ }
551
+ if (state.visited.has(neighborFile)) return;
552
+ state.visited.add(neighborFile);
553
+ state.parentMap.set(neighborFile, currentFile);
554
+ nextQueue.push(neighborFile);
555
+ }
556
+
557
+ /** Reconstruct file path from target back to source using parent links. */
558
+ function reconstructFilePath(
559
+ parentMap: Map<string, string>,
560
+ sourceFile: string,
561
+ targetFile: string,
562
+ ): string[] {
563
+ const filePath: string[] = [targetFile];
564
+ let cur = targetFile;
565
+ while (cur !== sourceFile) {
566
+ cur = parentMap.get(cur)!;
567
+ filePath.push(cur);
568
+ }
569
+ filePath.reverse();
570
+ return filePath;
571
+ }
572
+
477
573
  /** BFS over file adjacency graph to find shortest path. */
478
574
  function bfsFilePath(
479
575
  neighborStmt: ReturnType<BetterSqlite3Database['prepare']>,
@@ -483,11 +579,13 @@ function bfsFilePath(
483
579
  maxDepth: number,
484
580
  noTests: boolean,
485
581
  ): { found: boolean; path: string[]; alternateCount: number } {
486
- const visited = new Set([sourceFile]);
487
- const parentMap = new Map<string, string>();
582
+ const state: FileBfsState = {
583
+ visited: new Set([sourceFile]),
584
+ parentMap: new Map<string, string>(),
585
+ found: false,
586
+ alternateCount: 0,
587
+ };
488
588
  let queue = [sourceFile];
489
- let found = false;
490
- let alternateCount = 0;
491
589
 
492
590
  for (let depth = 1; depth <= maxDepth; depth++) {
493
591
  const nextQueue: string[] = [];
@@ -496,38 +594,21 @@ function bfsFilePath(
496
594
  neighbor_file: string;
497
595
  }>;
498
596
  for (const n of neighbors) {
499
- if (noTests && isTestFile(n.neighbor_file)) continue;
500
- if (n.neighbor_file === targetFile) {
501
- if (!found) {
502
- found = true;
503
- parentMap.set(n.neighbor_file, currentFile);
504
- }
505
- alternateCount++;
506
- continue;
507
- }
508
- if (!visited.has(n.neighbor_file)) {
509
- visited.add(n.neighbor_file);
510
- parentMap.set(n.neighbor_file, currentFile);
511
- nextQueue.push(n.neighbor_file);
512
- }
597
+ visitFileNeighbor(n.neighbor_file, currentFile, targetFile, state, nextQueue, noTests);
513
598
  }
514
599
  }
515
- if (found) break;
600
+ if (state.found) break;
516
601
  queue = nextQueue;
517
602
  if (queue.length === 0) break;
518
603
  }
519
604
 
520
- if (!found) return { found: false, path: [], alternateCount: 0 };
605
+ if (!state.found) return { found: false, path: [], alternateCount: 0 };
521
606
 
522
- // Reconstruct path
523
- const filePath: string[] = [targetFile];
524
- let cur = targetFile;
525
- while (cur !== sourceFile) {
526
- cur = parentMap.get(cur)!;
527
- filePath.push(cur);
528
- }
529
- filePath.reverse();
530
- return { found: true, path: filePath, alternateCount: Math.max(0, alternateCount - 1) };
607
+ return {
608
+ found: true,
609
+ path: reconstructFilePath(state.parentMap, sourceFile, targetFile),
610
+ alternateCount: Math.max(0, state.alternateCount - 1),
611
+ };
531
612
  }
532
613
 
533
614
  /**
@@ -83,6 +83,63 @@ function expandImplementors(
83
83
  }
84
84
  }
85
85
 
86
+ /** Record a caller node at depth `d`, adding to frontier and levels. */
87
+ function recordCaller(
88
+ caller: RelatedNodeRow,
89
+ parentId: number,
90
+ depth: number,
91
+ visited: Set<number>,
92
+ nextFrontier: number[],
93
+ levels: BfsLevels,
94
+ noTests: boolean,
95
+ onVisit?: BfsOnVisit,
96
+ ): void {
97
+ if (visited.has(caller.id) || (noTests && isTestFile(caller.file))) return;
98
+ visited.add(caller.id);
99
+ nextFrontier.push(caller.id);
100
+ if (!levels[depth]) levels[depth] = [];
101
+ levels[depth]!.push(toSymbolRef(caller));
102
+ if (onVisit) onVisit(caller, parentId, depth);
103
+ }
104
+
105
+ /** Process all callers of one frontier node, recording new nodes and expanding implementors. */
106
+ function processFrontierNode(
107
+ repo: InstanceType<typeof Repository>,
108
+ fid: number,
109
+ depth: number,
110
+ visited: Set<number>,
111
+ nextFrontier: number[],
112
+ levels: BfsLevels,
113
+ noTests: boolean,
114
+ resolveImplementors: boolean,
115
+ onVisit?: BfsOnVisit,
116
+ ): void {
117
+ const callers = repo.findDistinctCallers(fid) as RelatedNodeRow[];
118
+ for (const c of callers) {
119
+ recordCaller(c, fid, depth, visited, nextFrontier, levels, noTests, onVisit);
120
+ if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
121
+ expandImplementors(repo, c.id, depth + 1, visited, nextFrontier, levels, noTests, onVisit);
122
+ }
123
+ }
124
+ }
125
+
126
+ /** Seed BFS with implementors of the start node when it is an interface/trait. */
127
+ function seedInterfaceImplementors(
128
+ repo: InstanceType<typeof Repository>,
129
+ startId: number,
130
+ visited: Set<number>,
131
+ levels: BfsLevels,
132
+ noTests: boolean,
133
+ onVisit?: BfsOnVisit,
134
+ ): number[] {
135
+ const implNextFrontier: number[] = [];
136
+ const startNode = repo.findNodeById(startId) as NodeRow | undefined;
137
+ if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
138
+ expandImplementors(repo, startId, 1, visited, implNextFrontier, levels, noTests, onVisit);
139
+ }
140
+ return implNextFrontier;
141
+ }
142
+
86
143
  export function bfsTransitiveCallers(
87
144
  dbOrRepo: BetterSqlite3Database | InstanceType<typeof Repository>,
88
145
  startId: number,
@@ -105,13 +162,9 @@ export function bfsTransitiveCallers(
105
162
  let frontier = [startId];
106
163
 
107
164
  // Seed: if start node is an interface/trait, include its implementors at depth 1
108
- const implNextFrontier: number[] = [];
109
- if (resolveImplementors) {
110
- const startNode = repo.findNodeById(startId) as NodeRow | undefined;
111
- if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
112
- expandImplementors(repo, startId, 1, visited, implNextFrontier, levels, noTests, onVisit);
113
- }
114
- }
165
+ const implNextFrontier = resolveImplementors
166
+ ? seedInterfaceImplementors(repo, startId, visited, levels, noTests, onVisit)
167
+ : [];
115
168
 
116
169
  for (let d = 1; d <= maxDepth; d++) {
117
170
  if (d === 1 && implNextFrontier.length > 0) {
@@ -119,19 +172,17 @@ export function bfsTransitiveCallers(
119
172
  }
120
173
  const nextFrontier: number[] = [];
121
174
  for (const fid of frontier) {
122
- const callers = repo.findDistinctCallers(fid) as RelatedNodeRow[];
123
- for (const c of callers) {
124
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
125
- visited.add(c.id);
126
- nextFrontier.push(c.id);
127
- if (!levels[d]) levels[d] = [];
128
- levels[d]!.push(toSymbolRef(c));
129
- if (onVisit) onVisit(c, fid, d);
130
- }
131
- if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
132
- expandImplementors(repo, c.id, d + 1, visited, nextFrontier, levels, noTests, onVisit);
133
- }
134
- }
175
+ processFrontierNode(
176
+ repo,
177
+ fid,
178
+ d,
179
+ visited,
180
+ nextFrontier,
181
+ levels,
182
+ noTests,
183
+ resolveImplementors,
184
+ onVisit,
185
+ );
135
186
  }
136
187
  frontier = nextFrontier;
137
188
  if (frontier.length === 0) break;
@@ -140,6 +191,53 @@ export function bfsTransitiveCallers(
140
191
  return { totalDependents: visited.size - 1, levels };
141
192
  }
142
193
 
194
+ /** BFS over import dependents, returning visited node IDs and depth-per-id map. */
195
+ function bfsImportDependents(
196
+ repo: InstanceType<typeof Repository>,
197
+ seedNodes: NodeRow[],
198
+ noTests: boolean,
199
+ ): { visited: Set<number>; levels: Map<number, number> } {
200
+ const visited = new Set<number>();
201
+ const queue: number[] = [];
202
+ const levels = new Map<number, number>();
203
+
204
+ for (const fn of seedNodes) {
205
+ visited.add(fn.id);
206
+ queue.push(fn.id);
207
+ levels.set(fn.id, 0);
208
+ }
209
+
210
+ while (queue.length > 0) {
211
+ const current = queue.shift()!;
212
+ const level = levels.get(current)!;
213
+ const dependents = repo.findImportDependents(current) as RelatedNodeRow[];
214
+ for (const dep of dependents) {
215
+ if (visited.has(dep.id)) continue;
216
+ if (noTests && isTestFile(dep.file)) continue;
217
+ visited.add(dep.id);
218
+ queue.push(dep.id);
219
+ levels.set(dep.id, level + 1);
220
+ }
221
+ }
222
+
223
+ return { visited, levels };
224
+ }
225
+
226
+ /** Group visited dependents by depth (excluding seed depth 0). */
227
+ function groupDependentsByLevel(
228
+ repo: InstanceType<typeof Repository>,
229
+ levels: Map<number, number>,
230
+ ): Record<number, Array<{ file: string }>> {
231
+ const byLevel: Record<number, Array<{ file: string }>> = {};
232
+ for (const [id, level] of levels) {
233
+ if (level === 0) continue;
234
+ if (!byLevel[level]) byLevel[level] = [];
235
+ const node = repo.findNodeById(id) as NodeRow | undefined;
236
+ if (node) byLevel[level].push({ file: node.file });
237
+ }
238
+ return byLevel;
239
+ }
240
+
143
241
  export function impactAnalysisData(
144
242
  file: string,
145
243
  customDbPath: string,
@@ -152,36 +250,8 @@ export function impactAnalysisData(
152
250
  return { file, sources: [], levels: {}, totalDependents: 0 };
153
251
  }
154
252
 
155
- const visited = new Set<number>();
156
- const queue: number[] = [];
157
- const levels = new Map<number, number>();
158
-
159
- for (const fn of fileNodes) {
160
- visited.add(fn.id);
161
- queue.push(fn.id);
162
- levels.set(fn.id, 0);
163
- }
164
-
165
- while (queue.length > 0) {
166
- const current = queue.shift()!;
167
- const level = levels.get(current)!;
168
- const dependents = repo.findImportDependents(current) as RelatedNodeRow[];
169
- for (const dep of dependents) {
170
- if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
171
- visited.add(dep.id);
172
- queue.push(dep.id);
173
- levels.set(dep.id, level + 1);
174
- }
175
- }
176
- }
177
-
178
- const byLevel: Record<number, Array<{ file: string }>> = {};
179
- for (const [id, level] of levels) {
180
- if (level === 0) continue;
181
- if (!byLevel[level]) byLevel[level] = [];
182
- const node = repo.findNodeById(id) as NodeRow | undefined;
183
- if (node) byLevel[level].push({ file: node.file });
184
- }
253
+ const { visited, levels } = bfsImportDependents(repo, fileNodes, noTests);
254
+ const byLevel = groupDependentsByLevel(repo, levels);
185
255
 
186
256
  return {
187
257
  file,