@optave/codegraph 3.8.0 → 3.8.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 (296) hide show
  1. package/README.md +9 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +95 -87
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +32 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/watch.d.ts.map +1 -1
  28. package/dist/cli/commands/watch.js +16 -2
  29. package/dist/cli/commands/watch.js.map +1 -1
  30. package/dist/db/connection.d.ts.map +1 -1
  31. package/dist/db/connection.js +29 -26
  32. package/dist/db/connection.js.map +1 -1
  33. package/dist/db/query-builder.d.ts.map +1 -1
  34. package/dist/db/query-builder.js +16 -5
  35. package/dist/db/query-builder.js.map +1 -1
  36. package/dist/db/repository/base.d.ts +10 -0
  37. package/dist/db/repository/base.d.ts.map +1 -1
  38. package/dist/db/repository/base.js +17 -0
  39. package/dist/db/repository/base.js.map +1 -1
  40. package/dist/db/repository/native-repository.d.ts +6 -1
  41. package/dist/db/repository/native-repository.d.ts.map +1 -1
  42. package/dist/db/repository/native-repository.js +77 -1
  43. package/dist/db/repository/native-repository.js.map +1 -1
  44. package/dist/db/repository/nodes.d.ts.map +1 -1
  45. package/dist/db/repository/nodes.js +8 -4
  46. package/dist/db/repository/nodes.js.map +1 -1
  47. package/dist/db/repository/sqlite-repository.d.ts +3 -0
  48. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  49. package/dist/db/repository/sqlite-repository.js +26 -0
  50. package/dist/db/repository/sqlite-repository.js.map +1 -1
  51. package/dist/domain/analysis/brief.d.ts.map +1 -1
  52. package/dist/domain/analysis/brief.js +13 -17
  53. package/dist/domain/analysis/brief.js.map +1 -1
  54. package/dist/domain/analysis/context.d.ts.map +1 -1
  55. package/dist/domain/analysis/context.js +14 -11
  56. package/dist/domain/analysis/context.js.map +1 -1
  57. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  58. package/dist/domain/analysis/dependencies.js +53 -52
  59. package/dist/domain/analysis/dependencies.js.map +1 -1
  60. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  61. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  62. package/dist/domain/analysis/fn-impact.js +33 -31
  63. package/dist/domain/analysis/fn-impact.js.map +1 -1
  64. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  65. package/dist/domain/analysis/implementations.js +11 -19
  66. package/dist/domain/analysis/implementations.js.map +1 -1
  67. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  68. package/dist/domain/analysis/module-map.js +55 -76
  69. package/dist/domain/analysis/module-map.js.map +1 -1
  70. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  71. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  72. package/dist/domain/analysis/query-helpers.js +15 -1
  73. package/dist/domain/analysis/query-helpers.js.map +1 -1
  74. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  75. package/dist/domain/graph/builder/pipeline.js +255 -105
  76. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  77. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  78. package/dist/domain/graph/builder/stages/build-edges.js +22 -17
  79. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  80. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  81. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  82. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  83. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  84. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  85. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  86. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  88. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  89. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  90. package/dist/domain/graph/cycles.d.ts +6 -0
  91. package/dist/domain/graph/cycles.d.ts.map +1 -1
  92. package/dist/domain/graph/cycles.js +114 -22
  93. package/dist/domain/graph/cycles.js.map +1 -1
  94. package/dist/domain/graph/resolve.js +1 -1
  95. package/dist/domain/graph/resolve.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts +2 -0
  97. package/dist/domain/graph/watcher.d.ts.map +1 -1
  98. package/dist/domain/graph/watcher.js +170 -75
  99. package/dist/domain/graph/watcher.js.map +1 -1
  100. package/dist/domain/parser.d.ts +0 -5
  101. package/dist/domain/parser.d.ts.map +1 -1
  102. package/dist/domain/parser.js +13 -28
  103. package/dist/domain/parser.js.map +1 -1
  104. package/dist/domain/search/generator.js +1 -1
  105. package/dist/domain/search/generator.js.map +1 -1
  106. package/dist/domain/search/models.d.ts +4 -3
  107. package/dist/domain/search/models.d.ts.map +1 -1
  108. package/dist/domain/search/models.js +18 -5
  109. package/dist/domain/search/models.js.map +1 -1
  110. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  111. package/dist/domain/search/search/hybrid.js +29 -18
  112. package/dist/domain/search/search/hybrid.js.map +1 -1
  113. package/dist/extractors/go.js +36 -33
  114. package/dist/extractors/go.js.map +1 -1
  115. package/dist/extractors/helpers.d.ts.map +1 -1
  116. package/dist/extractors/helpers.js +40 -29
  117. package/dist/extractors/helpers.js.map +1 -1
  118. package/dist/extractors/java.js +58 -46
  119. package/dist/extractors/java.js.map +1 -1
  120. package/dist/extractors/javascript.js +46 -45
  121. package/dist/extractors/javascript.js.map +1 -1
  122. package/dist/extractors/kotlin.js +84 -78
  123. package/dist/extractors/kotlin.js.map +1 -1
  124. package/dist/extractors/python.js +29 -24
  125. package/dist/extractors/python.js.map +1 -1
  126. package/dist/extractors/rust.js +41 -32
  127. package/dist/extractors/rust.js.map +1 -1
  128. package/dist/extractors/solidity.js +58 -67
  129. package/dist/extractors/solidity.js.map +1 -1
  130. package/dist/extractors/swift.js +83 -81
  131. package/dist/extractors/swift.js.map +1 -1
  132. package/dist/extractors/zig.js +58 -60
  133. package/dist/extractors/zig.js.map +1 -1
  134. package/dist/features/ast.d.ts +16 -14
  135. package/dist/features/ast.d.ts.map +1 -1
  136. package/dist/features/ast.js +83 -81
  137. package/dist/features/ast.js.map +1 -1
  138. package/dist/features/audit.d.ts.map +1 -1
  139. package/dist/features/audit.js +8 -6
  140. package/dist/features/audit.js.map +1 -1
  141. package/dist/features/branch-compare.d.ts.map +1 -1
  142. package/dist/features/branch-compare.js +69 -72
  143. package/dist/features/branch-compare.js.map +1 -1
  144. package/dist/features/communities.d.ts.map +1 -1
  145. package/dist/features/communities.js +19 -7
  146. package/dist/features/communities.js.map +1 -1
  147. package/dist/features/complexity.d.ts.map +1 -1
  148. package/dist/features/complexity.js +120 -125
  149. package/dist/features/complexity.js.map +1 -1
  150. package/dist/features/dataflow.d.ts.map +1 -1
  151. package/dist/features/dataflow.js +136 -137
  152. package/dist/features/dataflow.js.map +1 -1
  153. package/dist/features/flow.d.ts.map +1 -1
  154. package/dist/features/flow.js +84 -79
  155. package/dist/features/flow.js.map +1 -1
  156. package/dist/features/structure-query.d.ts.map +1 -1
  157. package/dist/features/structure-query.js +69 -65
  158. package/dist/features/structure-query.js.map +1 -1
  159. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  160. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  161. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  162. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  163. package/dist/graph/algorithms/leiden/partition.js +288 -266
  164. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  165. package/dist/graph/model.d.ts.map +1 -1
  166. package/dist/graph/model.js +5 -1
  167. package/dist/graph/model.js.map +1 -1
  168. package/dist/infrastructure/config.d.ts.map +1 -1
  169. package/dist/infrastructure/config.js +6 -4
  170. package/dist/infrastructure/config.js.map +1 -1
  171. package/dist/infrastructure/suppress.d.ts +25 -0
  172. package/dist/infrastructure/suppress.d.ts.map +1 -0
  173. package/dist/infrastructure/suppress.js +43 -0
  174. package/dist/infrastructure/suppress.js.map +1 -0
  175. package/dist/mcp/server.d.ts.map +1 -1
  176. package/dist/mcp/server.js +29 -24
  177. package/dist/mcp/server.js.map +1 -1
  178. package/dist/presentation/dataflow.d.ts.map +1 -1
  179. package/dist/presentation/dataflow.js +47 -38
  180. package/dist/presentation/dataflow.js.map +1 -1
  181. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  182. package/dist/presentation/diff-impact-mermaid.js +60 -51
  183. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  184. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  185. package/dist/presentation/queries-cli/exports.js +20 -14
  186. package/dist/presentation/queries-cli/exports.js.map +1 -1
  187. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  188. package/dist/presentation/queries-cli/impact.js +15 -13
  189. package/dist/presentation/queries-cli/impact.js.map +1 -1
  190. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  191. package/dist/presentation/queries-cli/inspect.js +101 -79
  192. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  193. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/overview.js +25 -16
  195. package/dist/presentation/queries-cli/overview.js.map +1 -1
  196. package/dist/presentation/queries-cli/path.js +26 -20
  197. package/dist/presentation/queries-cli/path.js.map +1 -1
  198. package/dist/presentation/result-formatter.d.ts +10 -0
  199. package/dist/presentation/result-formatter.d.ts.map +1 -1
  200. package/dist/presentation/result-formatter.js +16 -1
  201. package/dist/presentation/result-formatter.js.map +1 -1
  202. package/dist/presentation/viewer.d.ts.map +1 -1
  203. package/dist/presentation/viewer.js +18 -12
  204. package/dist/presentation/viewer.js.map +1 -1
  205. package/dist/shared/errors.d.ts +5 -0
  206. package/dist/shared/errors.d.ts.map +1 -1
  207. package/dist/shared/errors.js +5 -0
  208. package/dist/shared/errors.js.map +1 -1
  209. package/dist/shared/hierarchy.d.ts +8 -2
  210. package/dist/shared/hierarchy.d.ts.map +1 -1
  211. package/dist/shared/hierarchy.js +42 -1
  212. package/dist/shared/hierarchy.js.map +1 -1
  213. package/dist/shared/normalize.d.ts +6 -1
  214. package/dist/shared/normalize.d.ts.map +1 -1
  215. package/dist/shared/normalize.js +20 -12
  216. package/dist/shared/normalize.js.map +1 -1
  217. package/dist/shared/paginate.d.ts +0 -9
  218. package/dist/shared/paginate.d.ts.map +1 -1
  219. package/dist/shared/paginate.js +0 -15
  220. package/dist/shared/paginate.js.map +1 -1
  221. package/dist/types.d.ts +10 -4
  222. package/dist/types.d.ts.map +1 -1
  223. package/package.json +7 -7
  224. package/src/ast-analysis/engine.ts +126 -105
  225. package/src/ast-analysis/metrics.ts +33 -11
  226. package/src/ast-analysis/shared.ts +33 -24
  227. package/src/ast-analysis/visitor-utils.ts +52 -32
  228. package/src/ast-analysis/visitor.ts +132 -71
  229. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  230. package/src/ast-analysis/visitors/complexity-visitor.ts +35 -40
  231. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  232. package/src/cli/commands/watch.ts +16 -2
  233. package/src/db/connection.ts +29 -28
  234. package/src/db/query-builder.ts +15 -3
  235. package/src/db/repository/base.ts +20 -0
  236. package/src/db/repository/native-repository.ts +79 -1
  237. package/src/db/repository/nodes.ts +13 -8
  238. package/src/db/repository/sqlite-repository.ts +29 -0
  239. package/src/domain/analysis/brief.ts +15 -25
  240. package/src/domain/analysis/context.ts +17 -10
  241. package/src/domain/analysis/dependencies.ts +67 -76
  242. package/src/domain/analysis/fn-impact.ts +36 -43
  243. package/src/domain/analysis/implementations.ts +11 -17
  244. package/src/domain/analysis/module-map.ts +58 -92
  245. package/src/domain/analysis/query-helpers.ts +18 -1
  246. package/src/domain/graph/builder/pipeline.ts +286 -97
  247. package/src/domain/graph/builder/stages/build-edges.ts +22 -18
  248. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  249. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  250. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  251. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  252. package/src/domain/graph/cycles.ts +110 -23
  253. package/src/domain/graph/resolve.ts +1 -1
  254. package/src/domain/graph/watcher.ts +202 -96
  255. package/src/domain/parser.ts +14 -26
  256. package/src/domain/search/generator.ts +1 -1
  257. package/src/domain/search/models.ts +17 -4
  258. package/src/domain/search/search/hybrid.ts +69 -51
  259. package/src/extractors/go.ts +43 -33
  260. package/src/extractors/helpers.ts +37 -23
  261. package/src/extractors/java.ts +66 -47
  262. package/src/extractors/javascript.ts +45 -46
  263. package/src/extractors/kotlin.ts +84 -77
  264. package/src/extractors/python.ts +31 -25
  265. package/src/extractors/rust.ts +37 -29
  266. package/src/extractors/solidity.ts +57 -61
  267. package/src/extractors/swift.ts +81 -80
  268. package/src/extractors/zig.ts +58 -61
  269. package/src/features/ast.ts +130 -110
  270. package/src/features/audit.ts +8 -6
  271. package/src/features/branch-compare.ts +105 -79
  272. package/src/features/communities.ts +25 -10
  273. package/src/features/complexity.ts +171 -134
  274. package/src/features/dataflow.ts +165 -175
  275. package/src/features/flow.ts +129 -92
  276. package/src/features/structure-query.ts +79 -64
  277. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  278. package/src/graph/algorithms/leiden/partition.ts +359 -294
  279. package/src/graph/model.ts +6 -1
  280. package/src/infrastructure/config.ts +6 -4
  281. package/src/infrastructure/suppress.ts +47 -0
  282. package/src/mcp/server.ts +53 -37
  283. package/src/presentation/dataflow.ts +50 -44
  284. package/src/presentation/diff-impact-mermaid.ts +104 -62
  285. package/src/presentation/queries-cli/exports.ts +21 -13
  286. package/src/presentation/queries-cli/impact.ts +15 -13
  287. package/src/presentation/queries-cli/inspect.ts +100 -81
  288. package/src/presentation/queries-cli/overview.ts +26 -16
  289. package/src/presentation/queries-cli/path.ts +33 -25
  290. package/src/presentation/result-formatter.ts +19 -1
  291. package/src/presentation/viewer.ts +42 -14
  292. package/src/shared/errors.ts +6 -0
  293. package/src/shared/hierarchy.ts +50 -2
  294. package/src/shared/normalize.ts +31 -12
  295. package/src/shared/paginate.ts +0 -17
  296. package/src/types.ts +24 -4
@@ -24,7 +24,7 @@ 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';
26
26
  import { paginateResult } from '../shared/paginate.js';
27
- import type { BetterSqlite3Database, NodeRow, TreeSitterNode } from '../types.js';
27
+ import type { BetterSqlite3Database, NativeDatabase, NodeRow, TreeSitterNode } from '../types.js';
28
28
  import { findNodes } from './shared/find-nodes.js';
29
29
 
30
30
  // Re-export for backward compatibility
@@ -237,6 +237,86 @@ function insertDataflowEdges(
237
237
 
238
238
  // ── buildDataflowEdges ──────────────────────────────────────────────────────
239
239
 
240
+ function prepareNodeResolvers(db: BetterSqlite3Database): {
241
+ getNodeByNameAndFile: ReturnType<BetterSqlite3Database['prepare']>;
242
+ getNodeByName: ReturnType<BetterSqlite3Database['prepare']>;
243
+ } {
244
+ return {
245
+ getNodeByNameAndFile: db.prepare(
246
+ `SELECT id, name, kind, file, line FROM nodes
247
+ WHERE name = ? AND file = ? AND kind IN ('function', 'method')`,
248
+ ),
249
+ getNodeByName: db.prepare(
250
+ `SELECT id, name, kind, file, line FROM nodes
251
+ WHERE name = ? AND kind IN ('function', 'method')
252
+ ORDER BY file, line LIMIT 10`,
253
+ ),
254
+ };
255
+ }
256
+
257
+ function makeNodeResolver(
258
+ stmts: ReturnType<typeof prepareNodeResolvers>,
259
+ relPath: string,
260
+ ): (funcName: string) => { id: number } | null {
261
+ return (funcName: string): { id: number } | null => {
262
+ const local = stmts.getNodeByNameAndFile.all(funcName, relPath) as { id: number }[];
263
+ if (local.length > 0) return local[0]!;
264
+ const global = stmts.getNodeByName.all(funcName) as { id: number }[];
265
+ return global.length > 0 ? global[0]! : null;
266
+ };
267
+ }
268
+
269
+ function collectNativeEdges(
270
+ data: DataflowResult,
271
+ resolveNode: (name: string) => { id: number } | null,
272
+ edges: Array<Record<string, unknown>>,
273
+ ): void {
274
+ for (const flow of data.argFlows as ArgFlow[]) {
275
+ const sourceNode = resolveNode(flow.callerFunc);
276
+ const targetNode = resolveNode(flow.calleeName);
277
+ if (sourceNode && targetNode) {
278
+ edges.push({
279
+ sourceId: sourceNode.id,
280
+ targetId: targetNode.id,
281
+ kind: 'flows_to',
282
+ paramIndex: flow.argIndex,
283
+ expression: flow.expression,
284
+ line: flow.line,
285
+ confidence: flow.confidence,
286
+ });
287
+ }
288
+ }
289
+ for (const assignment of data.assignments as Assignment[]) {
290
+ const producerNode = resolveNode(assignment.sourceCallName);
291
+ const consumerNode = resolveNode(assignment.callerFunc);
292
+ if (producerNode && consumerNode) {
293
+ edges.push({
294
+ sourceId: producerNode.id,
295
+ targetId: consumerNode.id,
296
+ kind: 'returns',
297
+ paramIndex: undefined,
298
+ expression: assignment.expression,
299
+ line: assignment.line,
300
+ confidence: 1.0,
301
+ });
302
+ }
303
+ }
304
+ for (const mut of data.mutations as Mutation[]) {
305
+ const mutatorNode = resolveNode(mut.funcName);
306
+ if (mutatorNode && mut.binding?.type === 'param') {
307
+ edges.push({
308
+ sourceId: mutatorNode.id,
309
+ targetId: mutatorNode.id,
310
+ kind: 'mutates',
311
+ paramIndex: undefined,
312
+ expression: mut.mutatingExpr,
313
+ line: mut.line,
314
+ confidence: 1.0,
315
+ });
316
+ }
317
+ }
318
+ }
319
+
240
320
  export async function buildDataflowEdges(
241
321
  db: BetterSqlite3Database,
242
322
  fileSymbols: Map<string, FileSymbolsDataflow>,
@@ -254,28 +334,7 @@ export async function buildDataflowEdges(
254
334
  if (nativeDb?.bulkInsertDataflow) {
255
335
  let needsJsFallback = false;
256
336
  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
- );
337
+ const stmts = prepareNodeResolvers(db);
279
338
 
280
339
  for (const [relPath, symbols] of fileSymbols) {
281
340
  const ext = path.extname(relPath).toLowerCase();
@@ -285,58 +344,7 @@ export async function buildDataflowEdges(
285
344
  break;
286
345
  }
287
346
 
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
- }
347
+ collectNativeEdges(symbols.dataflow, makeNodeResolver(stmts, relPath), nativeEdges);
340
348
  }
341
349
 
342
350
  if (!needsJsFallback) {
@@ -363,29 +371,7 @@ export async function buildDataflowEdges(
363
371
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
364
372
  );
365
373
 
366
- const getNodeByNameAndFile = db.prepare<{
367
- id: number;
368
- name: string;
369
- kind: string;
370
- file: string;
371
- line: number;
372
- }>(
373
- `SELECT id, name, kind, file, line FROM nodes
374
- WHERE name = ? AND file = ? AND kind IN ('function', 'method')`,
375
- );
376
-
377
- const getNodeByName = db.prepare<{
378
- id: number;
379
- name: string;
380
- kind: string;
381
- file: string;
382
- line: number;
383
- }>(
384
- `SELECT id, name, kind, file, line FROM nodes
385
- WHERE name = ? AND kind IN ('function', 'method')
386
- ORDER BY file, line LIMIT 10`,
387
- );
388
-
374
+ const stmts = prepareNodeResolvers(db);
389
375
  let totalEdges = 0;
390
376
 
391
377
  const tx = db.transaction(() => {
@@ -396,14 +382,7 @@ export async function buildDataflowEdges(
396
382
  const data = getDataflowForFile(symbols, relPath, rootDir, extToLang, parsers, getParserFn);
397
383
  if (!data) continue;
398
384
 
399
- const resolveNode = (funcName: string): { id: number } | null => {
400
- const local = getNodeByNameAndFile.all(funcName, relPath);
401
- if (local.length > 0) return local[0]!;
402
- const global = getNodeByName.all(funcName);
403
- return global.length > 0 ? global[0]! : null;
404
- };
405
-
406
- totalEdges += insertDataflowEdges(insert, data, resolveNode);
385
+ totalEdges += insertDataflowEdges(insert, data, makeNodeResolver(stmts, relPath));
407
386
  }
408
387
  });
409
388
 
@@ -540,6 +519,83 @@ function buildNodeDataflowResult(
540
519
  };
541
520
  }
542
521
 
522
+ function buildNativeDataflowResult(
523
+ node: NodeRow,
524
+ nativeDb: NativeDatabase,
525
+ db: BetterSqlite3Database,
526
+ hc: Map<string, string | null>,
527
+ noTests: boolean,
528
+ ): Record<string, unknown> {
529
+ const sym = normalizeSymbol(node, db, hc);
530
+ const d = nativeDb.getDataflowEdges!(node.id);
531
+
532
+ const flowsTo = d.flowsToOut.map((r: any) => ({
533
+ target: r.name,
534
+ kind: r.kind,
535
+ file: r.file,
536
+ line: r.line,
537
+ paramIndex: r.paramIndex,
538
+ expression: r.expression,
539
+ confidence: r.confidence,
540
+ }));
541
+ const flowsFrom = d.flowsToIn.map((r: any) => ({
542
+ source: r.name,
543
+ kind: r.kind,
544
+ file: r.file,
545
+ line: r.line,
546
+ paramIndex: r.paramIndex,
547
+ expression: r.expression,
548
+ confidence: r.confidence,
549
+ }));
550
+ const returnConsumers = d.returnsOut.map((r: any) => ({
551
+ consumer: r.name,
552
+ kind: r.kind,
553
+ file: r.file,
554
+ line: r.line,
555
+ expression: r.expression,
556
+ }));
557
+ const returnedBy = d.returnsIn.map((r: any) => ({
558
+ producer: r.name,
559
+ kind: r.kind,
560
+ file: r.file,
561
+ line: r.line,
562
+ expression: r.expression,
563
+ }));
564
+ const mutatesTargets = d.mutatesOut.map((r: any) => ({
565
+ target: r.name,
566
+ expression: r.expression,
567
+ line: r.line,
568
+ }));
569
+ const mutatedBy = d.mutatesIn.map((r: any) => ({
570
+ source: r.name,
571
+ expression: r.expression,
572
+ line: r.line,
573
+ }));
574
+
575
+ if (noTests) {
576
+ const filter = (arr: any[]) => arr.filter((r: any) => !isTestFile(r.file));
577
+ return {
578
+ ...sym,
579
+ flowsTo: filter(flowsTo),
580
+ flowsFrom: filter(flowsFrom),
581
+ returns: returnConsumers.filter((r: any) => !isTestFile(r.file)),
582
+ returnedBy: returnedBy.filter((r: any) => !isTestFile(r.file)),
583
+ mutates: mutatesTargets,
584
+ mutatedBy,
585
+ };
586
+ }
587
+
588
+ return {
589
+ ...sym,
590
+ flowsTo,
591
+ flowsFrom,
592
+ returns: returnConsumers,
593
+ returnedBy,
594
+ mutates: mutatesTargets,
595
+ mutatedBy,
596
+ };
597
+ }
598
+
543
599
  export function dataflowData(
544
600
  name: string,
545
601
  customDbPath?: string,
@@ -571,75 +627,9 @@ export function dataflowData(
571
627
  // ── Native fast path: 6 queries per node → 1 napi call per node ──
572
628
  if (nativeDb?.getDataflowEdges) {
573
629
  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
- }
633
- return {
634
- ...sym,
635
- flowsTo,
636
- flowsFrom,
637
- returns: returnConsumers,
638
- returnedBy,
639
- mutates: mutatesTargets,
640
- mutatedBy,
641
- };
642
- });
630
+ const results = nodes.map((node: NodeRow) =>
631
+ buildNativeDataflowResult(node, nativeDb, db, hc, noTests),
632
+ );
643
633
  const base = { name, results };
644
634
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
645
635
  }
@@ -96,6 +96,128 @@ interface NodeInfo {
96
96
  type?: string;
97
97
  }
98
98
 
99
+ /** Resolve the entry node by direct match or framework-prefix matching. */
100
+ function resolveEntryNode(
101
+ db: ReturnType<typeof openReadonlyOrFail>,
102
+ name: string,
103
+ flowOpts: { noTests?: boolean; file?: string; kinds?: string[] },
104
+ ): {
105
+ id: number;
106
+ name: string;
107
+ kind: string;
108
+ file: string;
109
+ line: number;
110
+ role?: string | null;
111
+ } | null {
112
+ // Phase 1: Direct LIKE match on full name
113
+ let matchNode = findMatchingNodes(db, name, flowOpts)[0] ?? null;
114
+
115
+ // Phase 2: Prefix-stripped matching — try adding framework prefixes
116
+ if (!matchNode) {
117
+ for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
118
+ matchNode = findMatchingNodes(db, `${prefix}${name}`, flowOpts)[0] ?? null;
119
+ if (matchNode) break;
120
+ }
121
+ }
122
+
123
+ return matchNode;
124
+ }
125
+
126
+ interface BfsState {
127
+ visited: Set<number>;
128
+ steps: Array<{ depth: number; nodes: NodeInfo[] }>;
129
+ cycles: Array<{ from: string; to: string; depth: number }>;
130
+ nodeDepths: Map<number, number>;
131
+ idToNode: Map<number, NodeInfo>;
132
+ truncated: boolean;
133
+ }
134
+
135
+ /** Forward BFS through callees, collecting steps, cycles, and node depth info. */
136
+ function bfsCallees(
137
+ db: ReturnType<typeof openReadonlyOrFail>,
138
+ entryId: number,
139
+ entryInfo: NodeInfo,
140
+ maxDepth: number,
141
+ noTests: boolean,
142
+ ): BfsState {
143
+ const visited = new Set<number>([entryId]);
144
+ let frontier = [entryId];
145
+ const steps: Array<{ depth: number; nodes: NodeInfo[] }> = [];
146
+ const cycles: Array<{ from: string; to: string; depth: number }> = [];
147
+ const nodeDepths = new Map<number, number>();
148
+ const idToNode = new Map<number, NodeInfo>();
149
+ idToNode.set(entryId, entryInfo);
150
+ let truncated = false;
151
+
152
+ const calleesStmt = db.prepare<CalleeRow>(
153
+ `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.role
154
+ FROM edges e JOIN nodes n ON e.target_id = n.id
155
+ WHERE e.source_id = ? AND e.kind = 'calls'`,
156
+ );
157
+
158
+ for (let d = 1; d <= maxDepth; d++) {
159
+ const nextFrontier: number[] = [];
160
+ const levelNodes: NodeInfo[] = [];
161
+
162
+ for (const fid of frontier) {
163
+ const callees = calleesStmt.all(fid);
164
+
165
+ for (const c of callees) {
166
+ if (noTests && isTestFile(c.file)) continue;
167
+
168
+ if (visited.has(c.id)) {
169
+ const fromNode = idToNode.get(fid);
170
+ if (fromNode) {
171
+ cycles.push({ from: fromNode.name, to: c.name, depth: d });
172
+ }
173
+ continue;
174
+ }
175
+
176
+ visited.add(c.id);
177
+ nextFrontier.push(c.id);
178
+ const nodeInfo: NodeInfo = { name: c.name, kind: c.kind, file: c.file, line: c.line };
179
+ levelNodes.push(nodeInfo);
180
+ nodeDepths.set(c.id, d);
181
+ idToNode.set(c.id, nodeInfo);
182
+ }
183
+ }
184
+
185
+ if (levelNodes.length > 0) {
186
+ steps.push({ depth: d, nodes: levelNodes });
187
+ }
188
+
189
+ frontier = nextFrontier;
190
+ if (frontier.length === 0) break;
191
+ if (d === maxDepth && frontier.length > 0) truncated = true;
192
+ }
193
+
194
+ return { visited, steps, cycles, nodeDepths, idToNode, truncated };
195
+ }
196
+
197
+ /** Identify leaf nodes — visited nodes with no outgoing 'calls' edges. */
198
+ function findLeafNodes(
199
+ db: ReturnType<typeof openReadonlyOrFail>,
200
+ nodeDepths: Map<number, number>,
201
+ idToNode: Map<number, NodeInfo>,
202
+ ): Array<NodeInfo & { depth: number }> {
203
+ const leaves: Array<NodeInfo & { depth: number }> = [];
204
+ const outgoingStmt = db.prepare<{ id: number }>(
205
+ `SELECT DISTINCT n.id
206
+ FROM edges e JOIN nodes n ON e.target_id = n.id
207
+ WHERE e.source_id = ? AND e.kind = 'calls'`,
208
+ );
209
+
210
+ for (const [id, depth] of nodeDepths) {
211
+ const outgoing = outgoingStmt.all(id);
212
+ if (outgoing.length === 0) {
213
+ const node = idToNode.get(id);
214
+ if (node) leaves.push({ ...node, depth });
215
+ }
216
+ }
217
+
218
+ return leaves;
219
+ }
220
+
99
221
  export function flowData(
100
222
  name: string,
101
223
  dbPath?: string,
@@ -117,17 +239,7 @@ export function flowData(
117
239
  kinds: opts.kind ? [opts.kind] : (CORE_SYMBOL_KINDS as unknown as string[]),
118
240
  };
119
241
 
120
- // Phase 1: Direct LIKE match on full name (use all 10 core symbol kinds,
121
- // not just FUNCTION_KINDS, so flow can trace from interfaces/types/structs/etc.)
122
- let matchNode = findMatchingNodes(db, name, flowOpts)[0] ?? null;
123
-
124
- // Phase 2: Prefix-stripped matching — try adding framework prefixes
125
- if (!matchNode) {
126
- for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
127
- matchNode = findMatchingNodes(db, `${prefix}${name}`, flowOpts)[0] ?? null;
128
- if (matchNode) break;
129
- }
130
- }
242
+ const matchNode = resolveEntryNode(db, name, flowOpts);
131
243
 
132
244
  if (!matchNode) {
133
245
  return {
@@ -151,92 +263,17 @@ export function flowData(
151
263
  role: matchNode.role,
152
264
  };
153
265
 
154
- // Forward BFS through callees
155
- const visited = new Set<number>([matchNode.id]);
156
- let frontier = [matchNode.id];
157
- const steps: Array<{ depth: number; nodes: NodeInfo[] }> = [];
158
- const cycles: Array<{ from: string; to: string; depth: number }> = [];
159
- let truncated = false;
160
-
161
- // Track which nodes are at each depth and their depth for leaf detection
162
- const nodeDepths = new Map<number, number>();
163
- const idToNode = new Map<number, NodeInfo>();
164
- idToNode.set(matchNode.id, entry);
165
-
166
- for (let d = 1; d <= maxDepth; d++) {
167
- const nextFrontier: number[] = [];
168
- const levelNodes: NodeInfo[] = [];
169
-
170
- for (const fid of frontier) {
171
- const callees = db
172
- .prepare<CalleeRow>(
173
- `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.role
174
- FROM edges e JOIN nodes n ON e.target_id = n.id
175
- WHERE e.source_id = ? AND e.kind = 'calls'`,
176
- )
177
- .all(fid);
178
-
179
- for (const c of callees) {
180
- if (noTests && isTestFile(c.file)) continue;
181
-
182
- if (visited.has(c.id)) {
183
- // Cycle detected
184
- const fromNode = idToNode.get(fid);
185
- if (fromNode) {
186
- cycles.push({ from: fromNode.name, to: c.name, depth: d });
187
- }
188
- continue;
189
- }
190
-
191
- visited.add(c.id);
192
- nextFrontier.push(c.id);
193
- const nodeInfo: NodeInfo = { name: c.name, kind: c.kind, file: c.file, line: c.line };
194
- levelNodes.push(nodeInfo);
195
- nodeDepths.set(c.id, d);
196
- idToNode.set(c.id, nodeInfo);
197
- }
198
- }
199
-
200
- if (levelNodes.length > 0) {
201
- steps.push({ depth: d, nodes: levelNodes });
202
- }
203
-
204
- frontier = nextFrontier;
205
- if (frontier.length === 0) break;
206
-
207
- if (d === maxDepth && frontier.length > 0) {
208
- truncated = true;
209
- }
210
- }
211
-
212
- // Identify leaves: visited nodes that have no outgoing 'calls' edges to other visited nodes
213
- // (or no outgoing calls at all)
214
- const leaves: Array<NodeInfo & { depth: number }> = [];
215
- for (const [id, depth] of nodeDepths) {
216
- const outgoing = db
217
- .prepare<{ id: number }>(
218
- `SELECT DISTINCT n.id
219
- FROM edges e JOIN nodes n ON e.target_id = n.id
220
- WHERE e.source_id = ? AND e.kind = 'calls'`,
221
- )
222
- .all(id);
223
-
224
- if (outgoing.length === 0) {
225
- const node = idToNode.get(id);
226
- if (node) {
227
- leaves.push({ ...node, depth });
228
- }
229
- }
230
- }
266
+ const bfs = bfsCallees(db, matchNode.id, entry, maxDepth, noTests);
267
+ const leaves = findLeafNodes(db, bfs.nodeDepths, bfs.idToNode);
231
268
 
232
269
  const base = {
233
270
  entry,
234
271
  depth: maxDepth,
235
- steps,
272
+ steps: bfs.steps,
236
273
  leaves,
237
- cycles,
238
- totalReached: visited.size - 1, // exclude the entry node itself
239
- truncated,
274
+ cycles: bfs.cycles,
275
+ totalReached: bfs.visited.size - 1,
276
+ truncated: bfs.truncated,
240
277
  };
241
278
  return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
242
279
  } finally {