@optave/codegraph 3.5.0 → 3.7.0

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 (346) hide show
  1. package/README.md +47 -21
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +119 -127
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +14 -1
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  10. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  11. package/dist/db/connection.d.ts +12 -2
  12. package/dist/db/connection.d.ts.map +1 -1
  13. package/dist/db/connection.js +81 -53
  14. package/dist/db/connection.js.map +1 -1
  15. package/dist/db/index.d.ts +1 -1
  16. package/dist/db/index.d.ts.map +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/db/migrations.d.ts.map +1 -1
  20. package/dist/db/migrations.js +38 -32
  21. package/dist/db/migrations.js.map +1 -1
  22. package/dist/domain/analysis/context.d.ts.map +1 -1
  23. package/dist/domain/analysis/context.js +51 -66
  24. package/dist/domain/analysis/context.js.map +1 -1
  25. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  26. package/dist/domain/analysis/dependencies.js +62 -70
  27. package/dist/domain/analysis/dependencies.js.map +1 -1
  28. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  29. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  30. package/dist/domain/analysis/exports.d.ts.map +1 -1
  31. package/dist/domain/analysis/exports.js +29 -33
  32. package/dist/domain/analysis/exports.js.map +1 -1
  33. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  34. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  35. package/dist/domain/analysis/fn-impact.js +35 -65
  36. package/dist/domain/analysis/fn-impact.js.map +1 -1
  37. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  38. package/dist/domain/analysis/module-map.js +91 -6
  39. package/dist/domain/analysis/module-map.js.map +1 -1
  40. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  41. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  42. package/dist/domain/analysis/query-helpers.js +27 -0
  43. package/dist/domain/analysis/query-helpers.js.map +1 -0
  44. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/helpers.js +15 -9
  46. package/dist/domain/graph/builder/helpers.js.map +1 -1
  47. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/incremental.js +3 -2
  49. package/dist/domain/graph/builder/incremental.js.map +1 -1
  50. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/pipeline.js +69 -3
  52. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  54. package/dist/domain/graph/builder/stages/build-edges.js +7 -51
  55. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/build-structure.js +7 -5
  58. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/collect-files.js +2 -2
  60. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  63. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/finalize.js +124 -105
  66. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  68. package/dist/domain/graph/builder/stages/insert-nodes.js +28 -15
  69. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  71. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  72. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  73. package/dist/domain/graph/resolve.d.ts +0 -4
  74. package/dist/domain/graph/resolve.d.ts.map +1 -1
  75. package/dist/domain/graph/resolve.js +32 -48
  76. package/dist/domain/graph/resolve.js.map +1 -1
  77. package/dist/domain/graph/watcher.d.ts.map +1 -1
  78. package/dist/domain/graph/watcher.js +12 -12
  79. package/dist/domain/graph/watcher.js.map +1 -1
  80. package/dist/domain/parser.d.ts +1 -1
  81. package/dist/domain/parser.d.ts.map +1 -1
  82. package/dist/domain/parser.js +206 -101
  83. package/dist/domain/parser.js.map +1 -1
  84. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  85. package/dist/domain/search/search/cli-formatter.js +88 -83
  86. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  87. package/dist/extractors/bash.d.ts +6 -0
  88. package/dist/extractors/bash.d.ts.map +1 -0
  89. package/dist/extractors/bash.js +91 -0
  90. package/dist/extractors/bash.js.map +1 -0
  91. package/dist/extractors/c.d.ts +6 -0
  92. package/dist/extractors/c.d.ts.map +1 -0
  93. package/dist/extractors/c.js +204 -0
  94. package/dist/extractors/c.js.map +1 -0
  95. package/dist/extractors/cpp.d.ts +6 -0
  96. package/dist/extractors/cpp.d.ts.map +1 -0
  97. package/dist/extractors/cpp.js +283 -0
  98. package/dist/extractors/cpp.js.map +1 -0
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +42 -54
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/dart.d.ts +6 -0
  103. package/dist/extractors/dart.d.ts.map +1 -0
  104. package/dist/extractors/dart.js +277 -0
  105. package/dist/extractors/dart.js.map +1 -0
  106. package/dist/extractors/elixir.d.ts +9 -0
  107. package/dist/extractors/elixir.d.ts.map +1 -0
  108. package/dist/extractors/elixir.js +223 -0
  109. package/dist/extractors/elixir.js.map +1 -0
  110. package/dist/extractors/go.d.ts.map +1 -1
  111. package/dist/extractors/go.js +126 -130
  112. package/dist/extractors/go.js.map +1 -1
  113. package/dist/extractors/haskell.d.ts +8 -0
  114. package/dist/extractors/haskell.d.ts.map +1 -0
  115. package/dist/extractors/haskell.js +217 -0
  116. package/dist/extractors/haskell.js.map +1 -0
  117. package/dist/extractors/hcl.js +6 -6
  118. package/dist/extractors/hcl.js.map +1 -1
  119. package/dist/extractors/helpers.d.ts +32 -1
  120. package/dist/extractors/helpers.d.ts.map +1 -1
  121. package/dist/extractors/helpers.js +74 -0
  122. package/dist/extractors/helpers.js.map +1 -1
  123. package/dist/extractors/index.d.ts +12 -0
  124. package/dist/extractors/index.d.ts.map +1 -1
  125. package/dist/extractors/index.js +12 -0
  126. package/dist/extractors/index.js.map +1 -1
  127. package/dist/extractors/java.d.ts.map +1 -1
  128. package/dist/extractors/java.js +32 -47
  129. package/dist/extractors/java.js.map +1 -1
  130. package/dist/extractors/javascript.d.ts.map +1 -1
  131. package/dist/extractors/javascript.js +306 -292
  132. package/dist/extractors/javascript.js.map +1 -1
  133. package/dist/extractors/kotlin.d.ts +6 -0
  134. package/dist/extractors/kotlin.d.ts.map +1 -0
  135. package/dist/extractors/kotlin.js +275 -0
  136. package/dist/extractors/kotlin.js.map +1 -0
  137. package/dist/extractors/lua.d.ts +6 -0
  138. package/dist/extractors/lua.d.ts.map +1 -0
  139. package/dist/extractors/lua.js +162 -0
  140. package/dist/extractors/lua.js.map +1 -0
  141. package/dist/extractors/ocaml.d.ts +6 -0
  142. package/dist/extractors/ocaml.d.ts.map +1 -0
  143. package/dist/extractors/ocaml.js +236 -0
  144. package/dist/extractors/ocaml.js.map +1 -0
  145. package/dist/extractors/php.d.ts.map +1 -1
  146. package/dist/extractors/php.js +39 -44
  147. package/dist/extractors/php.js.map +1 -1
  148. package/dist/extractors/python.d.ts.map +1 -1
  149. package/dist/extractors/python.js +75 -93
  150. package/dist/extractors/python.js.map +1 -1
  151. package/dist/extractors/ruby.js +6 -13
  152. package/dist/extractors/ruby.js.map +1 -1
  153. package/dist/extractors/rust.d.ts.map +1 -1
  154. package/dist/extractors/rust.js +58 -83
  155. package/dist/extractors/rust.js.map +1 -1
  156. package/dist/extractors/scala.d.ts +6 -0
  157. package/dist/extractors/scala.d.ts.map +1 -0
  158. package/dist/extractors/scala.js +269 -0
  159. package/dist/extractors/scala.js.map +1 -0
  160. package/dist/extractors/swift.d.ts +6 -0
  161. package/dist/extractors/swift.d.ts.map +1 -0
  162. package/dist/extractors/swift.js +275 -0
  163. package/dist/extractors/swift.js.map +1 -0
  164. package/dist/extractors/zig.d.ts +9 -0
  165. package/dist/extractors/zig.d.ts.map +1 -0
  166. package/dist/extractors/zig.js +276 -0
  167. package/dist/extractors/zig.js.map +1 -0
  168. package/dist/features/ast.d.ts +2 -0
  169. package/dist/features/ast.d.ts.map +1 -1
  170. package/dist/features/ast.js +9 -24
  171. package/dist/features/ast.js.map +1 -1
  172. package/dist/features/audit.d.ts.map +1 -1
  173. package/dist/features/audit.js +17 -21
  174. package/dist/features/audit.js.map +1 -1
  175. package/dist/features/branch-compare.d.ts.map +1 -1
  176. package/dist/features/branch-compare.js +47 -3
  177. package/dist/features/branch-compare.js.map +1 -1
  178. package/dist/features/cfg.d.ts +7 -1
  179. package/dist/features/cfg.d.ts.map +1 -1
  180. package/dist/features/cfg.js +72 -61
  181. package/dist/features/cfg.js.map +1 -1
  182. package/dist/features/check.d.ts.map +1 -1
  183. package/dist/features/check.js +79 -62
  184. package/dist/features/check.js.map +1 -1
  185. package/dist/features/complexity-query.d.ts.map +1 -1
  186. package/dist/features/complexity-query.js +142 -137
  187. package/dist/features/complexity-query.js.map +1 -1
  188. package/dist/features/complexity.d.ts +7 -1
  189. package/dist/features/complexity.d.ts.map +1 -1
  190. package/dist/features/complexity.js +62 -1
  191. package/dist/features/complexity.js.map +1 -1
  192. package/dist/features/dataflow.d.ts +7 -1
  193. package/dist/features/dataflow.d.ts.map +1 -1
  194. package/dist/features/dataflow.js +356 -188
  195. package/dist/features/dataflow.js.map +1 -1
  196. package/dist/features/graph-enrichment.d.ts.map +1 -1
  197. package/dist/features/graph-enrichment.js +117 -104
  198. package/dist/features/graph-enrichment.js.map +1 -1
  199. package/dist/features/sequence.d.ts.map +1 -1
  200. package/dist/features/sequence.js +25 -4
  201. package/dist/features/sequence.js.map +1 -1
  202. package/dist/features/structure-query.d.ts.map +1 -1
  203. package/dist/features/structure-query.js +29 -4
  204. package/dist/features/structure-query.js.map +1 -1
  205. package/dist/features/structure.d.ts.map +1 -1
  206. package/dist/features/structure.js +35 -15
  207. package/dist/features/structure.js.map +1 -1
  208. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  209. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  210. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  211. package/dist/graph/algorithms/leiden/index.js +43 -28
  212. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  213. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  214. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  215. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  216. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  217. package/dist/graph/algorithms/leiden/partition.js +89 -106
  218. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  219. package/dist/graph/model.d.ts +2 -0
  220. package/dist/graph/model.d.ts.map +1 -1
  221. package/dist/graph/model.js +20 -8
  222. package/dist/graph/model.js.map +1 -1
  223. package/dist/infrastructure/config.d.ts +0 -8
  224. package/dist/infrastructure/config.d.ts.map +1 -1
  225. package/dist/infrastructure/config.js +73 -62
  226. package/dist/infrastructure/config.js.map +1 -1
  227. package/dist/infrastructure/registry.d.ts +0 -8
  228. package/dist/infrastructure/registry.d.ts.map +1 -1
  229. package/dist/infrastructure/registry.js +12 -14
  230. package/dist/infrastructure/registry.js.map +1 -1
  231. package/dist/mcp/server.d.ts.map +1 -1
  232. package/dist/mcp/server.js +45 -36
  233. package/dist/mcp/server.js.map +1 -1
  234. package/dist/presentation/audit.d.ts.map +1 -1
  235. package/dist/presentation/audit.js +61 -57
  236. package/dist/presentation/audit.js.map +1 -1
  237. package/dist/presentation/branch-compare.d.ts.map +1 -1
  238. package/dist/presentation/branch-compare.js +56 -38
  239. package/dist/presentation/branch-compare.js.map +1 -1
  240. package/dist/presentation/check.d.ts.map +1 -1
  241. package/dist/presentation/check.js +30 -32
  242. package/dist/presentation/check.js.map +1 -1
  243. package/dist/presentation/colors.d.ts.map +1 -1
  244. package/dist/presentation/colors.js +2 -0
  245. package/dist/presentation/colors.js.map +1 -1
  246. package/dist/presentation/complexity.d.ts.map +1 -1
  247. package/dist/presentation/complexity.js +25 -19
  248. package/dist/presentation/complexity.js.map +1 -1
  249. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  250. package/dist/presentation/queries-cli/exports.js +15 -15
  251. package/dist/presentation/queries-cli/exports.js.map +1 -1
  252. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  253. package/dist/presentation/queries-cli/impact.js +29 -19
  254. package/dist/presentation/queries-cli/impact.js.map +1 -1
  255. package/dist/types.d.ts +182 -7
  256. package/dist/types.d.ts.map +1 -1
  257. package/grammars/tree-sitter-bash.wasm +0 -0
  258. package/grammars/tree-sitter-c.wasm +0 -0
  259. package/grammars/tree-sitter-cpp.wasm +0 -0
  260. package/grammars/tree-sitter-dart.wasm +0 -0
  261. package/grammars/tree-sitter-elixir.wasm +0 -0
  262. package/grammars/tree-sitter-haskell.wasm +0 -0
  263. package/grammars/tree-sitter-kotlin.wasm +0 -0
  264. package/grammars/tree-sitter-lua.wasm +0 -0
  265. package/grammars/tree-sitter-ocaml.wasm +0 -0
  266. package/grammars/tree-sitter-scala.wasm +0 -0
  267. package/grammars/tree-sitter-swift.wasm +0 -0
  268. package/grammars/tree-sitter-zig.wasm +0 -0
  269. package/package.json +19 -7
  270. package/src/ast-analysis/engine.ts +147 -138
  271. package/src/ast-analysis/visitors/ast-store-visitor.ts +15 -2
  272. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  273. package/src/db/connection.ts +90 -59
  274. package/src/db/index.ts +1 -0
  275. package/src/db/migrations.ts +36 -32
  276. package/src/domain/analysis/context.ts +73 -75
  277. package/src/domain/analysis/dependencies.ts +78 -68
  278. package/src/domain/analysis/exports.ts +45 -34
  279. package/src/domain/analysis/fn-impact.ts +67 -64
  280. package/src/domain/analysis/module-map.ts +103 -8
  281. package/src/domain/analysis/query-helpers.ts +35 -0
  282. package/src/domain/graph/builder/helpers.ts +12 -6
  283. package/src/domain/graph/builder/incremental.ts +3 -2
  284. package/src/domain/graph/builder/pipeline.ts +71 -3
  285. package/src/domain/graph/builder/stages/build-edges.ts +10 -75
  286. package/src/domain/graph/builder/stages/build-structure.ts +9 -7
  287. package/src/domain/graph/builder/stages/collect-files.ts +2 -2
  288. package/src/domain/graph/builder/stages/detect-changes.ts +7 -2
  289. package/src/domain/graph/builder/stages/finalize.ts +159 -125
  290. package/src/domain/graph/builder/stages/insert-nodes.ts +32 -21
  291. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  292. package/src/domain/graph/resolve.ts +34 -46
  293. package/src/domain/graph/watcher.ts +12 -14
  294. package/src/domain/parser.ts +222 -97
  295. package/src/domain/search/search/cli-formatter.ts +121 -94
  296. package/src/extractors/bash.ts +97 -0
  297. package/src/extractors/c.ts +212 -0
  298. package/src/extractors/cpp.ts +298 -0
  299. package/src/extractors/csharp.ts +53 -56
  300. package/src/extractors/dart.ts +304 -0
  301. package/src/extractors/elixir.ts +251 -0
  302. package/src/extractors/go.ts +152 -134
  303. package/src/extractors/haskell.ts +235 -0
  304. package/src/extractors/hcl.ts +6 -6
  305. package/src/extractors/helpers.ts +93 -1
  306. package/src/extractors/index.ts +12 -0
  307. package/src/extractors/java.ts +43 -48
  308. package/src/extractors/javascript.ts +328 -281
  309. package/src/extractors/kotlin.ts +293 -0
  310. package/src/extractors/lua.ts +169 -0
  311. package/src/extractors/ocaml.ts +259 -0
  312. package/src/extractors/php.ts +46 -40
  313. package/src/extractors/python.ts +81 -104
  314. package/src/extractors/ruby.ts +6 -13
  315. package/src/extractors/rust.ts +65 -85
  316. package/src/extractors/scala.ts +285 -0
  317. package/src/extractors/swift.ts +293 -0
  318. package/src/extractors/zig.ts +294 -0
  319. package/src/features/ast.ts +10 -25
  320. package/src/features/audit.ts +24 -20
  321. package/src/features/branch-compare.ts +51 -4
  322. package/src/features/cfg.ts +113 -65
  323. package/src/features/check.ts +90 -74
  324. package/src/features/complexity-query.ts +181 -163
  325. package/src/features/complexity.ts +64 -1
  326. package/src/features/dataflow.ts +462 -217
  327. package/src/features/graph-enrichment.ts +161 -117
  328. package/src/features/sequence.ts +27 -4
  329. package/src/features/structure-query.ts +43 -4
  330. package/src/features/structure.ts +50 -22
  331. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  332. package/src/graph/algorithms/leiden/index.ts +67 -28
  333. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  334. package/src/graph/algorithms/leiden/partition.ts +131 -98
  335. package/src/graph/model.ts +19 -7
  336. package/src/infrastructure/config.ts +60 -58
  337. package/src/infrastructure/registry.ts +17 -14
  338. package/src/mcp/server.ts +46 -37
  339. package/src/presentation/audit.ts +72 -67
  340. package/src/presentation/branch-compare.ts +54 -50
  341. package/src/presentation/check.ts +34 -34
  342. package/src/presentation/colors.ts +2 -0
  343. package/src/presentation/complexity.ts +39 -33
  344. package/src/presentation/queries-cli/exports.ts +17 -17
  345. package/src/presentation/queries-cli/impact.ts +30 -22
  346. package/src/types.ts +195 -7
@@ -19,7 +19,7 @@ import {
19
19
  } from '../ast-analysis/shared.js';
20
20
  import { walkWithVisitors } from '../ast-analysis/visitor.js';
21
21
  import { createDataflowVisitor } from '../ast-analysis/visitors/dataflow-visitor.js';
22
- import { hasDataflowTable, openReadonlyOrFail } from '../db/index.js';
22
+ import { hasDataflowTable, openReadonlyOrFail, openReadonlyWithNative } from '../db/index.js';
23
23
  import { ALL_SYMBOL_KINDS, normalizeSymbol } from '../domain/queries.js';
24
24
  import { debug, info } from '../infrastructure/logger.js';
25
25
  import { isTestFile } from '../infrastructure/test-filter.js';
@@ -241,9 +241,121 @@ export async function buildDataflowEdges(
241
241
  db: BetterSqlite3Database,
242
242
  fileSymbols: Map<string, FileSymbolsDataflow>,
243
243
  rootDir: string,
244
- _engineOpts?: unknown,
244
+ engineOpts?: {
245
+ nativeDb?: { bulkInsertDataflow?(edges: Array<Record<string, unknown>>): number };
246
+ suspendJsDb?: () => void;
247
+ resumeJsDb?: () => void;
248
+ },
245
249
  ): Promise<void> {
246
250
  const extToLang = buildExtToLangMap();
251
+
252
+ // ── Native bulk-insert fast path ──────────────────────────────────────
253
+ const nativeDb = engineOpts?.nativeDb;
254
+ if (nativeDb?.bulkInsertDataflow) {
255
+ let needsJsFallback = false;
256
+ const nativeEdges: Array<Record<string, unknown>> = [];
257
+
258
+ const getNodeByNameAndFile = db.prepare<{
259
+ id: number;
260
+ name: string;
261
+ kind: string;
262
+ file: string;
263
+ line: number;
264
+ }>(
265
+ `SELECT id, name, kind, file, line FROM nodes
266
+ WHERE name = ? AND file = ? AND kind IN ('function', 'method')`,
267
+ );
268
+ const getNodeByName = db.prepare<{
269
+ id: number;
270
+ name: string;
271
+ kind: string;
272
+ file: string;
273
+ line: number;
274
+ }>(
275
+ `SELECT id, name, kind, file, line FROM nodes
276
+ WHERE name = ? AND kind IN ('function', 'method')
277
+ ORDER BY file, line LIMIT 10`,
278
+ );
279
+
280
+ for (const [relPath, symbols] of fileSymbols) {
281
+ const ext = path.extname(relPath).toLowerCase();
282
+ if (!DATAFLOW_EXTENSIONS.has(ext)) continue;
283
+ if (!symbols.dataflow) {
284
+ needsJsFallback = true;
285
+ break;
286
+ }
287
+
288
+ const resolveNode = (funcName: string): { id: number } | null => {
289
+ const local = getNodeByNameAndFile.all(funcName, relPath);
290
+ if (local.length > 0) return local[0]!;
291
+ const global = getNodeByName.all(funcName);
292
+ return global.length > 0 ? global[0]! : null;
293
+ };
294
+
295
+ const data = symbols.dataflow;
296
+ for (const flow of data.argFlows as ArgFlow[]) {
297
+ const sourceNode = resolveNode(flow.callerFunc);
298
+ const targetNode = resolveNode(flow.calleeName);
299
+ if (sourceNode && targetNode) {
300
+ nativeEdges.push({
301
+ sourceId: sourceNode.id,
302
+ targetId: targetNode.id,
303
+ kind: 'flows_to',
304
+ paramIndex: flow.argIndex,
305
+ expression: flow.expression,
306
+ line: flow.line,
307
+ confidence: flow.confidence,
308
+ });
309
+ }
310
+ }
311
+ for (const assignment of data.assignments as Assignment[]) {
312
+ const producerNode = resolveNode(assignment.sourceCallName);
313
+ const consumerNode = resolveNode(assignment.callerFunc);
314
+ if (producerNode && consumerNode) {
315
+ nativeEdges.push({
316
+ sourceId: producerNode.id,
317
+ targetId: consumerNode.id,
318
+ kind: 'returns',
319
+ paramIndex: null,
320
+ expression: assignment.expression,
321
+ line: assignment.line,
322
+ confidence: 1.0,
323
+ });
324
+ }
325
+ }
326
+ for (const mut of data.mutations as Mutation[]) {
327
+ const mutatorNode = resolveNode(mut.funcName);
328
+ if (mutatorNode && mut.binding?.type === 'param') {
329
+ nativeEdges.push({
330
+ sourceId: mutatorNode.id,
331
+ targetId: mutatorNode.id,
332
+ kind: 'mutates',
333
+ paramIndex: null,
334
+ expression: mut.mutatingExpr,
335
+ line: mut.line,
336
+ confidence: 1.0,
337
+ });
338
+ }
339
+ }
340
+ }
341
+
342
+ if (!needsJsFallback) {
343
+ if (nativeEdges.length > 0) {
344
+ let inserted: number;
345
+ try {
346
+ engineOpts?.suspendJsDb?.();
347
+ inserted = nativeDb.bulkInsertDataflow(nativeEdges);
348
+ } finally {
349
+ engineOpts?.resumeJsDb?.();
350
+ }
351
+ info(`Dataflow (native bulk): ${inserted} edges inserted`);
352
+ }
353
+ return;
354
+ }
355
+ debug('Dataflow: some files lack pre-computed data — falling back to JS');
356
+ }
357
+
358
+ // ── JS fallback path ─────────────────────────────────────────────────
247
359
  const { parsers, getParserFn } = await initDataflowParsers(fileSymbols);
248
360
 
249
361
  const insert = db.prepare(
@@ -303,12 +415,137 @@ export async function buildDataflowEdges(
303
415
 
304
416
  // findNodes imported from ./shared/find-nodes.js
305
417
 
418
+ interface DataflowStmts {
419
+ flowsToOut: ReturnType<BetterSqlite3Database['prepare']>;
420
+ flowsToIn: ReturnType<BetterSqlite3Database['prepare']>;
421
+ returnsOut: ReturnType<BetterSqlite3Database['prepare']>;
422
+ returnsIn: ReturnType<BetterSqlite3Database['prepare']>;
423
+ mutatesOut: ReturnType<BetterSqlite3Database['prepare']>;
424
+ mutatesIn: ReturnType<BetterSqlite3Database['prepare']>;
425
+ }
426
+
427
+ function prepareDataflowStmts(db: BetterSqlite3Database): DataflowStmts {
428
+ return {
429
+ flowsToOut: db.prepare(
430
+ `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
431
+ FROM dataflow d JOIN nodes n ON d.target_id = n.id
432
+ WHERE d.source_id = ? AND d.kind = 'flows_to'`,
433
+ ),
434
+ flowsToIn: db.prepare(
435
+ `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
436
+ FROM dataflow d JOIN nodes n ON d.source_id = n.id
437
+ WHERE d.target_id = ? AND d.kind = 'flows_to'`,
438
+ ),
439
+ returnsOut: db.prepare(
440
+ `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
441
+ FROM dataflow d JOIN nodes n ON d.target_id = n.id
442
+ WHERE d.source_id = ? AND d.kind = 'returns'`,
443
+ ),
444
+ returnsIn: db.prepare(
445
+ `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
446
+ FROM dataflow d JOIN nodes n ON d.source_id = n.id
447
+ WHERE d.target_id = ? AND d.kind = 'returns'`,
448
+ ),
449
+ mutatesOut: db.prepare(
450
+ `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
451
+ FROM dataflow d JOIN nodes n ON d.target_id = n.id
452
+ WHERE d.source_id = ? AND d.kind = 'mutates'`,
453
+ ),
454
+ mutatesIn: db.prepare(
455
+ `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
456
+ FROM dataflow d JOIN nodes n ON d.source_id = n.id
457
+ WHERE d.target_id = ? AND d.kind = 'mutates'`,
458
+ ),
459
+ };
460
+ }
461
+
462
+ function buildNodeDataflowResult(
463
+ node: NodeRow,
464
+ stmts: DataflowStmts,
465
+ db: BetterSqlite3Database,
466
+ hc: Map<string, string | null>,
467
+ noTests: boolean,
468
+ ): Record<string, unknown> {
469
+ const sym = normalizeSymbol(node, db, hc);
470
+
471
+ const flowsTo = stmts.flowsToOut.all(node.id).map((r: any) => ({
472
+ target: r.target_name,
473
+ kind: r.target_kind,
474
+ file: r.target_file,
475
+ line: r.line,
476
+ paramIndex: r.param_index,
477
+ expression: r.expression,
478
+ confidence: r.confidence,
479
+ }));
480
+
481
+ const flowsFrom = stmts.flowsToIn.all(node.id).map((r: any) => ({
482
+ source: r.source_name,
483
+ kind: r.source_kind,
484
+ file: r.source_file,
485
+ line: r.line,
486
+ paramIndex: r.param_index,
487
+ expression: r.expression,
488
+ confidence: r.confidence,
489
+ }));
490
+
491
+ const returnConsumers = stmts.returnsOut.all(node.id).map((r: any) => ({
492
+ consumer: r.target_name,
493
+ kind: r.target_kind,
494
+ file: r.target_file,
495
+ line: r.line,
496
+ expression: r.expression,
497
+ }));
498
+
499
+ const returnedBy = stmts.returnsIn.all(node.id).map((r: any) => ({
500
+ producer: r.source_name,
501
+ kind: r.source_kind,
502
+ file: r.source_file,
503
+ line: r.line,
504
+ expression: r.expression,
505
+ }));
506
+
507
+ const mutatesTargets = stmts.mutatesOut.all(node.id).map((r: any) => ({
508
+ target: r.target_name,
509
+ expression: r.expression,
510
+ line: r.line,
511
+ }));
512
+
513
+ const mutatedBy = stmts.mutatesIn.all(node.id).map((r: any) => ({
514
+ source: r.source_name,
515
+ expression: r.expression,
516
+ line: r.line,
517
+ }));
518
+
519
+ if (noTests) {
520
+ const filter = (arr: any[]) => arr.filter((r: any) => !isTestFile(r.file));
521
+ return {
522
+ ...sym,
523
+ flowsTo: filter(flowsTo),
524
+ flowsFrom: filter(flowsFrom),
525
+ returns: returnConsumers.filter((r) => !isTestFile(r.file)),
526
+ returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
527
+ mutates: mutatesTargets,
528
+ mutatedBy,
529
+ };
530
+ }
531
+
532
+ return {
533
+ ...sym,
534
+ flowsTo,
535
+ flowsFrom,
536
+ returns: returnConsumers,
537
+ returnedBy,
538
+ mutates: mutatesTargets,
539
+ mutatedBy,
540
+ };
541
+ }
542
+
306
543
  export function dataflowData(
307
544
  name: string,
308
545
  customDbPath?: string,
309
546
  opts: { noTests?: boolean; file?: string; kind?: string; limit?: number; offset?: number } = {},
310
547
  ): Record<string, unknown> {
311
- const db = openReadonlyOrFail(customDbPath);
548
+ const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
312
549
  try {
313
550
  const noTests = opts.noTests || false;
314
551
 
@@ -331,120 +568,195 @@ export function dataflowData(
331
568
  return { name, results: [] };
332
569
  }
333
570
 
334
- const flowsToOut = db.prepare(
335
- `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
336
- FROM dataflow d JOIN nodes n ON d.target_id = n.id
337
- WHERE d.source_id = ? AND d.kind = 'flows_to'`,
338
- );
339
- const flowsToIn = db.prepare(
340
- `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
341
- FROM dataflow d JOIN nodes n ON d.source_id = n.id
342
- WHERE d.target_id = ? AND d.kind = 'flows_to'`,
343
- );
344
- const returnsOut = db.prepare(
345
- `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
346
- FROM dataflow d JOIN nodes n ON d.target_id = n.id
347
- WHERE d.source_id = ? AND d.kind = 'returns'`,
348
- );
349
- const returnsIn = db.prepare(
350
- `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
351
- FROM dataflow d JOIN nodes n ON d.source_id = n.id
352
- WHERE d.target_id = ? AND d.kind = 'returns'`,
353
- );
354
- const mutatesOut = db.prepare(
355
- `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
356
- FROM dataflow d JOIN nodes n ON d.target_id = n.id
357
- WHERE d.source_id = ? AND d.kind = 'mutates'`,
358
- );
359
- const mutatesIn = db.prepare(
360
- `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
361
- FROM dataflow d JOIN nodes n ON d.source_id = n.id
362
- WHERE d.target_id = ? AND d.kind = 'mutates'`,
363
- );
364
-
365
- const hc = new Map<string, string | null>();
366
- const results = nodes.map((node: NodeRow) => {
367
- const sym = normalizeSymbol(node, db, hc);
368
-
369
- const flowsTo = flowsToOut.all(node.id).map((r: any) => ({
370
- target: r.target_name,
371
- kind: r.target_kind,
372
- file: r.target_file,
373
- line: r.line,
374
- paramIndex: r.param_index,
375
- expression: r.expression,
376
- confidence: r.confidence,
377
- }));
378
-
379
- const flowsFrom = flowsToIn.all(node.id).map((r: any) => ({
380
- source: r.source_name,
381
- kind: r.source_kind,
382
- file: r.source_file,
383
- line: r.line,
384
- paramIndex: r.param_index,
385
- expression: r.expression,
386
- confidence: r.confidence,
387
- }));
388
-
389
- const returnConsumers = returnsOut.all(node.id).map((r: any) => ({
390
- consumer: r.target_name,
391
- kind: r.target_kind,
392
- file: r.target_file,
393
- line: r.line,
394
- expression: r.expression,
395
- }));
396
-
397
- const returnedBy = returnsIn.all(node.id).map((r: any) => ({
398
- producer: r.source_name,
399
- kind: r.source_kind,
400
- file: r.source_file,
401
- line: r.line,
402
- expression: r.expression,
403
- }));
404
-
405
- const mutatesTargets = mutatesOut.all(node.id).map((r: any) => ({
406
- target: r.target_name,
407
- expression: r.expression,
408
- line: r.line,
409
- }));
410
-
411
- const mutatedBy = mutatesIn.all(node.id).map((r: any) => ({
412
- source: r.source_name,
413
- expression: r.expression,
414
- line: r.line,
415
- }));
416
-
417
- if (noTests) {
418
- const filter = (arr: any[]) => arr.filter((r: any) => !isTestFile(r.file));
571
+ // ── Native fast path: 6 queries per node → 1 napi call per node ──
572
+ if (nativeDb?.getDataflowEdges) {
573
+ const hc = new Map<string, string | null>();
574
+ const results = nodes.map((node: NodeRow) => {
575
+ const sym = normalizeSymbol(node, db, hc);
576
+ const d = nativeDb.getDataflowEdges!(node.id);
577
+
578
+ const flowsTo = d.flowsToOut.map((r) => ({
579
+ target: r.name,
580
+ kind: r.kind,
581
+ file: r.file,
582
+ line: r.line,
583
+ paramIndex: r.paramIndex,
584
+ expression: r.expression,
585
+ confidence: r.confidence,
586
+ }));
587
+ const flowsFrom = d.flowsToIn.map((r) => ({
588
+ source: r.name,
589
+ kind: r.kind,
590
+ file: r.file,
591
+ line: r.line,
592
+ paramIndex: r.paramIndex,
593
+ expression: r.expression,
594
+ confidence: r.confidence,
595
+ }));
596
+ const returnConsumers = d.returnsOut.map((r) => ({
597
+ consumer: r.name,
598
+ kind: r.kind,
599
+ file: r.file,
600
+ line: r.line,
601
+ expression: r.expression,
602
+ }));
603
+ const returnedBy = d.returnsIn.map((r) => ({
604
+ producer: r.name,
605
+ kind: r.kind,
606
+ file: r.file,
607
+ line: r.line,
608
+ expression: r.expression,
609
+ }));
610
+ const mutatesTargets = d.mutatesOut.map((r) => ({
611
+ target: r.name,
612
+ expression: r.expression,
613
+ line: r.line,
614
+ }));
615
+ const mutatedBy = d.mutatesIn.map((r) => ({
616
+ source: r.name,
617
+ expression: r.expression,
618
+ line: r.line,
619
+ }));
620
+
621
+ if (noTests) {
622
+ const filter = (arr: any[]) => arr.filter((r: any) => !isTestFile(r.file));
623
+ return {
624
+ ...sym,
625
+ flowsTo: filter(flowsTo),
626
+ flowsFrom: filter(flowsFrom),
627
+ returns: returnConsumers.filter((r) => !isTestFile(r.file)),
628
+ returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
629
+ mutates: mutatesTargets,
630
+ mutatedBy,
631
+ };
632
+ }
419
633
  return {
420
634
  ...sym,
421
- flowsTo: filter(flowsTo),
422
- flowsFrom: filter(flowsFrom),
423
- returns: returnConsumers.filter((r) => !isTestFile(r.file)),
424
- returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
635
+ flowsTo,
636
+ flowsFrom,
637
+ returns: returnConsumers,
638
+ returnedBy,
425
639
  mutates: mutatesTargets,
426
640
  mutatedBy,
427
641
  };
428
- }
642
+ });
643
+ const base = { name, results };
644
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
645
+ }
429
646
 
430
- return {
431
- ...sym,
432
- flowsTo,
433
- flowsFrom,
434
- returns: returnConsumers,
435
- returnedBy,
436
- mutates: mutatesTargets,
437
- mutatedBy,
438
- };
439
- });
647
+ // ── JS fallback ───────────────────────────────────────────────────
648
+ const stmts = prepareDataflowStmts(db);
649
+ const hc = new Map<string, string | null>();
650
+ const results = nodes.map((node: NodeRow) =>
651
+ buildNodeDataflowResult(node, stmts, db, hc, noTests),
652
+ );
440
653
 
441
654
  const base = { name, results };
442
655
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
443
656
  } finally {
444
- db.close();
657
+ close();
445
658
  }
446
659
  }
447
660
 
661
+ interface BfsParentEntry {
662
+ parentId: number;
663
+ edgeKind: string;
664
+ expression: string;
665
+ }
666
+
667
+ /** BFS through dataflow edges to find a path from source to target. */
668
+ function bfsDataflowPath(
669
+ db: BetterSqlite3Database,
670
+ sourceId: number,
671
+ targetId: number,
672
+ maxDepth: number,
673
+ noTests: boolean,
674
+ ): Map<number, BfsParentEntry> | null {
675
+ const neighborStmt = db.prepare(
676
+ `SELECT n.id, n.name, n.kind, n.file, n.line, d.kind AS edge_kind, d.expression
677
+ FROM dataflow d JOIN nodes n ON d.target_id = n.id
678
+ WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`,
679
+ );
680
+
681
+ const visited = new Set<number>([sourceId]);
682
+ const parent = new Map<number, BfsParentEntry>();
683
+ let queue = [sourceId];
684
+ let found = false;
685
+
686
+ for (let depth = 1; depth <= maxDepth; depth++) {
687
+ const nextQueue: number[] = [];
688
+ for (const currentId of queue) {
689
+ const neighbors = neighborStmt.all(currentId) as Array<{
690
+ id: number;
691
+ file: string;
692
+ edge_kind: string;
693
+ expression: string;
694
+ }>;
695
+ for (const n of neighbors) {
696
+ if (noTests && isTestFile(n.file)) continue;
697
+ if (n.id === targetId) {
698
+ if (!found) {
699
+ found = true;
700
+ parent.set(n.id, {
701
+ parentId: currentId,
702
+ edgeKind: n.edge_kind,
703
+ expression: n.expression,
704
+ });
705
+ }
706
+ continue;
707
+ }
708
+ if (!visited.has(n.id)) {
709
+ visited.add(n.id);
710
+ parent.set(n.id, {
711
+ parentId: currentId,
712
+ edgeKind: n.edge_kind,
713
+ expression: n.expression,
714
+ });
715
+ nextQueue.push(n.id);
716
+ }
717
+ }
718
+ }
719
+ if (found) break;
720
+ queue = nextQueue;
721
+ if (queue.length === 0) break;
722
+ }
723
+
724
+ return found ? parent : null;
725
+ }
726
+
727
+ /** Reconstruct a path from BFS parent map. */
728
+ function reconstructDataflowPath(
729
+ db: BetterSqlite3Database,
730
+ parent: Map<number, BfsParentEntry>,
731
+ sourceId: number,
732
+ targetId: number,
733
+ ): Array<Record<string, unknown>> {
734
+ const nodeById = db.prepare('SELECT * FROM nodes WHERE id = ?');
735
+ const hc = new Map<string, string | null>();
736
+ const pathItems: Array<Record<string, unknown>> = [];
737
+ let cur: number | undefined = targetId;
738
+ while (cur !== undefined) {
739
+ const nodeRow = nodeById.get(cur) as NodeRow;
740
+ const parentInfo = parent.get(cur);
741
+ pathItems.unshift({
742
+ ...normalizeSymbol(nodeRow, db, hc),
743
+ edgeKind: parentInfo?.edgeKind ?? null,
744
+ expression: parentInfo?.expression ?? null,
745
+ });
746
+ cur = parentInfo?.parentId;
747
+ if (cur === sourceId) {
748
+ const srcRow = nodeById.get(cur) as NodeRow;
749
+ pathItems.unshift({
750
+ ...normalizeSymbol(srcRow, db, hc),
751
+ edgeKind: null,
752
+ expression: null,
753
+ });
754
+ break;
755
+ }
756
+ }
757
+ return pathItems;
758
+ }
759
+
448
760
  export function dataflowPathData(
449
761
  from: string,
450
762
  to: string,
@@ -500,103 +812,54 @@ export function dataflowPathData(
500
812
  if (sourceNode.id === targetNode.id) {
501
813
  const hc = new Map<string, string | null>();
502
814
  const sym = normalizeSymbol(sourceNode, db, hc);
503
- return {
504
- from,
505
- to,
506
- found: true,
507
- hops: 0,
508
- path: [{ ...sym, edgeKind: null }],
509
- };
815
+ return { from, to, found: true, hops: 0, path: [{ ...sym, edgeKind: null }] };
510
816
  }
511
817
 
512
- // BFS through flows_to and returns edges
513
- const neighborStmt = db.prepare(
514
- `SELECT n.id, n.name, n.kind, n.file, n.line, d.kind AS edge_kind, d.expression
515
- FROM dataflow d JOIN nodes n ON d.target_id = n.id
516
- WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`,
517
- );
518
-
519
- const visited = new Set<number>([sourceNode.id]);
520
- const parent = new Map<number, { parentId: number; edgeKind: string; expression: string }>();
521
- let queue = [sourceNode.id];
522
- let found = false;
523
-
524
- for (let depth = 1; depth <= maxDepth; depth++) {
525
- const nextQueue: number[] = [];
526
- for (const currentId of queue) {
527
- const neighbors = neighborStmt.all(currentId) as Array<{
528
- id: number;
529
- name: string;
530
- kind: string;
531
- file: string;
532
- line: number;
533
- edge_kind: string;
534
- expression: string;
535
- }>;
536
- for (const n of neighbors) {
537
- if (noTests && isTestFile(n.file)) continue;
538
- if (n.id === targetNode.id) {
539
- if (!found) {
540
- found = true;
541
- parent.set(n.id, {
542
- parentId: currentId,
543
- edgeKind: n.edge_kind,
544
- expression: n.expression,
545
- });
546
- }
547
- continue;
548
- }
549
- if (!visited.has(n.id)) {
550
- visited.add(n.id);
551
- parent.set(n.id, {
552
- parentId: currentId,
553
- edgeKind: n.edge_kind,
554
- expression: n.expression,
555
- });
556
- nextQueue.push(n.id);
557
- }
558
- }
559
- }
560
- if (found) break;
561
- queue = nextQueue;
562
- if (queue.length === 0) break;
563
- }
564
-
565
- if (!found) {
818
+ const parent = bfsDataflowPath(db, sourceNode.id, targetNode.id, maxDepth, noTests);
819
+ if (!parent) {
566
820
  return { from, to, found: false };
567
821
  }
568
822
 
569
- // Reconstruct path
570
- const nodeById = db.prepare('SELECT * FROM nodes WHERE id = ?');
571
- const hc = new Map<string, string | null>();
572
- const pathItems: Array<Record<string, unknown>> = [];
573
- let cur: number | undefined = targetNode.id;
574
- while (cur !== undefined) {
575
- const nodeRow = nodeById.get(cur) as NodeRow;
576
- const parentInfo = parent.get(cur);
577
- pathItems.unshift({
578
- ...normalizeSymbol(nodeRow, db, hc),
579
- edgeKind: parentInfo?.edgeKind ?? null,
580
- expression: parentInfo?.expression ?? null,
581
- });
582
- cur = parentInfo?.parentId;
583
- if (cur === sourceNode.id) {
584
- const srcRow = nodeById.get(cur) as NodeRow;
585
- pathItems.unshift({
586
- ...normalizeSymbol(srcRow, db, hc),
587
- edgeKind: null,
588
- expression: null,
589
- });
590
- break;
591
- }
592
- }
593
-
823
+ const pathItems = reconstructDataflowPath(db, parent, sourceNode.id, targetNode.id);
594
824
  return { from, to, found: true, hops: pathItems.length - 1, path: pathItems };
595
825
  } finally {
596
826
  db.close();
597
827
  }
598
828
  }
599
829
 
830
+ /** BFS forward through return-value consumers to build impact levels. */
831
+ function bfsReturnConsumers(
832
+ node: NodeRow,
833
+ consumersStmt: ReturnType<BetterSqlite3Database['prepare']>,
834
+ db: BetterSqlite3Database,
835
+ hc: Map<string, string | null>,
836
+ maxDepth: number,
837
+ noTests: boolean,
838
+ ): { levels: Record<number, unknown[]>; totalAffected: number } {
839
+ const visited = new Set<number>([node.id]);
840
+ const levels: Record<number, unknown[]> = {};
841
+ let frontier = [node.id];
842
+
843
+ for (let d = 1; d <= maxDepth; d++) {
844
+ const nextFrontier: number[] = [];
845
+ for (const fid of frontier) {
846
+ const consumers = consumersStmt.all(fid) as NodeRow[];
847
+ for (const c of consumers) {
848
+ if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
849
+ visited.add(c.id);
850
+ nextFrontier.push(c.id);
851
+ if (!levels[d]) levels[d] = [];
852
+ levels[d]!.push(normalizeSymbol(c, db, hc));
853
+ }
854
+ }
855
+ }
856
+ frontier = nextFrontier;
857
+ if (frontier.length === 0) break;
858
+ }
859
+
860
+ return { levels, totalAffected: visited.size - 1 };
861
+ }
862
+
600
863
  export function dataflowImpactData(
601
864
  name: string,
602
865
  customDbPath?: string,
@@ -633,7 +896,6 @@ export function dataflowImpactData(
633
896
  return { name, results: [] };
634
897
  }
635
898
 
636
- // Forward BFS: who consumes this function's return value (directly or transitively)?
637
899
  const consumersStmt = db.prepare(
638
900
  `SELECT DISTINCT n.*
639
901
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
@@ -643,32 +905,15 @@ export function dataflowImpactData(
643
905
  const hc = new Map<string, string | null>();
644
906
  const results = nodes.map((node: NodeRow) => {
645
907
  const sym = normalizeSymbol(node, db, hc);
646
- const visited = new Set<number>([node.id]);
647
- const levels: Record<number, unknown[]> = {};
648
- let frontier = [node.id];
649
-
650
- for (let d = 1; d <= maxDepth; d++) {
651
- const nextFrontier: number[] = [];
652
- for (const fid of frontier) {
653
- const consumers = consumersStmt.all(fid) as NodeRow[];
654
- for (const c of consumers) {
655
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
656
- visited.add(c.id);
657
- nextFrontier.push(c.id);
658
- if (!levels[d]) levels[d] = [];
659
- levels[d]!.push(normalizeSymbol(c, db, hc));
660
- }
661
- }
662
- }
663
- frontier = nextFrontier;
664
- if (frontier.length === 0) break;
665
- }
666
-
667
- return {
668
- ...sym,
669
- levels,
670
- totalAffected: visited.size - 1,
671
- };
908
+ const { levels, totalAffected } = bfsReturnConsumers(
909
+ node,
910
+ consumersStmt,
911
+ db,
912
+ hc,
913
+ maxDepth,
914
+ noTests,
915
+ );
916
+ return { ...sym, levels, totalAffected };
672
917
  });
673
918
 
674
919
  const base = { name, results };