@optave/codegraph 3.4.1 → 3.6.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 (349) hide show
  1. package/README.md +50 -28
  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/rules/javascript.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/javascript.js +1 -0
  7. package/dist/ast-analysis/rules/javascript.js.map +1 -1
  8. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.js +116 -35
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  12. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  13. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  14. package/dist/db/better-sqlite3.d.ts +3 -0
  15. package/dist/db/better-sqlite3.d.ts.map +1 -0
  16. package/dist/db/better-sqlite3.js +19 -0
  17. package/dist/db/better-sqlite3.js.map +1 -0
  18. package/dist/db/connection.d.ts +25 -4
  19. package/dist/db/connection.d.ts.map +1 -1
  20. package/dist/db/connection.js +125 -23
  21. package/dist/db/connection.js.map +1 -1
  22. package/dist/db/index.d.ts +2 -2
  23. package/dist/db/index.d.ts.map +1 -1
  24. package/dist/db/index.js +1 -1
  25. package/dist/db/index.js.map +1 -1
  26. package/dist/db/migrations.d.ts.map +1 -1
  27. package/dist/db/migrations.js +40 -32
  28. package/dist/db/migrations.js.map +1 -1
  29. package/dist/db/query-builder.d.ts +5 -5
  30. package/dist/db/query-builder.d.ts.map +1 -1
  31. package/dist/db/query-builder.js +20 -4
  32. package/dist/db/query-builder.js.map +1 -1
  33. package/dist/db/repository/index.d.ts +1 -0
  34. package/dist/db/repository/index.d.ts.map +1 -1
  35. package/dist/db/repository/index.js +1 -0
  36. package/dist/db/repository/index.js.map +1 -1
  37. package/dist/db/repository/native-repository.d.ts +58 -0
  38. package/dist/db/repository/native-repository.d.ts.map +1 -0
  39. package/dist/db/repository/native-repository.js +261 -0
  40. package/dist/db/repository/native-repository.js.map +1 -0
  41. package/dist/db/repository/nodes.d.ts +4 -4
  42. package/dist/db/repository/nodes.d.ts.map +1 -1
  43. package/dist/db/repository/nodes.js +6 -6
  44. package/dist/db/repository/nodes.js.map +1 -1
  45. package/dist/domain/analysis/context.d.ts.map +1 -1
  46. package/dist/domain/analysis/context.js +51 -66
  47. package/dist/domain/analysis/context.js.map +1 -1
  48. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  49. package/dist/domain/analysis/dependencies.js +62 -70
  50. package/dist/domain/analysis/dependencies.js.map +1 -1
  51. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  52. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  53. package/dist/domain/analysis/exports.d.ts.map +1 -1
  54. package/dist/domain/analysis/exports.js +29 -33
  55. package/dist/domain/analysis/exports.js.map +1 -1
  56. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  57. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  58. package/dist/domain/analysis/fn-impact.js +35 -65
  59. package/dist/domain/analysis/fn-impact.js.map +1 -1
  60. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  61. package/dist/domain/analysis/module-map.js +91 -6
  62. package/dist/domain/analysis/module-map.js.map +1 -1
  63. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  64. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  65. package/dist/domain/analysis/query-helpers.js +27 -0
  66. package/dist/domain/analysis/query-helpers.js.map +1 -0
  67. package/dist/domain/graph/builder/context.d.ts +2 -1
  68. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/context.js +1 -0
  70. package/dist/domain/graph/builder/context.js.map +1 -1
  71. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  72. package/dist/domain/graph/builder/helpers.js +15 -9
  73. package/dist/domain/graph/builder/helpers.js.map +1 -1
  74. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  75. package/dist/domain/graph/builder/incremental.js +3 -2
  76. package/dist/domain/graph/builder/incremental.js.map +1 -1
  77. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  78. package/dist/domain/graph/builder/pipeline.js +95 -7
  79. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  80. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  81. package/dist/domain/graph/builder/stages/build-edges.js +101 -57
  82. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  83. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/stages/build-structure.js +33 -3
  85. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/collect-files.js +70 -6
  88. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  90. package/dist/domain/graph/builder/stages/detect-changes.js +36 -14
  91. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  92. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  93. package/dist/domain/graph/builder/stages/finalize.js +130 -88
  94. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  95. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  96. package/dist/domain/graph/builder/stages/insert-nodes.js +124 -16
  97. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  98. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  99. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  100. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  101. package/dist/domain/graph/resolve.d.ts +0 -4
  102. package/dist/domain/graph/resolve.d.ts.map +1 -1
  103. package/dist/domain/graph/resolve.js +32 -48
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts.map +1 -1
  106. package/dist/domain/graph/watcher.js +12 -12
  107. package/dist/domain/graph/watcher.js.map +1 -1
  108. package/dist/domain/parser.d.ts +1 -1
  109. package/dist/domain/parser.d.ts.map +1 -1
  110. package/dist/domain/parser.js +165 -101
  111. package/dist/domain/parser.js.map +1 -1
  112. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  113. package/dist/domain/search/search/cli-formatter.js +88 -83
  114. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  115. package/dist/extractors/bash.d.ts +6 -0
  116. package/dist/extractors/bash.d.ts.map +1 -0
  117. package/dist/extractors/bash.js +91 -0
  118. package/dist/extractors/bash.js.map +1 -0
  119. package/dist/extractors/c.d.ts +6 -0
  120. package/dist/extractors/c.d.ts.map +1 -0
  121. package/dist/extractors/c.js +204 -0
  122. package/dist/extractors/c.js.map +1 -0
  123. package/dist/extractors/cpp.d.ts +6 -0
  124. package/dist/extractors/cpp.d.ts.map +1 -0
  125. package/dist/extractors/cpp.js +283 -0
  126. package/dist/extractors/cpp.js.map +1 -0
  127. package/dist/extractors/csharp.d.ts.map +1 -1
  128. package/dist/extractors/csharp.js +42 -54
  129. package/dist/extractors/csharp.js.map +1 -1
  130. package/dist/extractors/go.d.ts.map +1 -1
  131. package/dist/extractors/go.js +126 -130
  132. package/dist/extractors/go.js.map +1 -1
  133. package/dist/extractors/hcl.js +6 -6
  134. package/dist/extractors/hcl.js.map +1 -1
  135. package/dist/extractors/helpers.d.ts +32 -1
  136. package/dist/extractors/helpers.d.ts.map +1 -1
  137. package/dist/extractors/helpers.js +74 -0
  138. package/dist/extractors/helpers.js.map +1 -1
  139. package/dist/extractors/index.d.ts +6 -0
  140. package/dist/extractors/index.d.ts.map +1 -1
  141. package/dist/extractors/index.js +6 -0
  142. package/dist/extractors/index.js.map +1 -1
  143. package/dist/extractors/java.d.ts.map +1 -1
  144. package/dist/extractors/java.js +32 -47
  145. package/dist/extractors/java.js.map +1 -1
  146. package/dist/extractors/javascript.d.ts.map +1 -1
  147. package/dist/extractors/javascript.js +359 -330
  148. package/dist/extractors/javascript.js.map +1 -1
  149. package/dist/extractors/kotlin.d.ts +6 -0
  150. package/dist/extractors/kotlin.d.ts.map +1 -0
  151. package/dist/extractors/kotlin.js +275 -0
  152. package/dist/extractors/kotlin.js.map +1 -0
  153. package/dist/extractors/php.d.ts.map +1 -1
  154. package/dist/extractors/php.js +39 -44
  155. package/dist/extractors/php.js.map +1 -1
  156. package/dist/extractors/python.d.ts.map +1 -1
  157. package/dist/extractors/python.js +75 -93
  158. package/dist/extractors/python.js.map +1 -1
  159. package/dist/extractors/ruby.js +6 -13
  160. package/dist/extractors/ruby.js.map +1 -1
  161. package/dist/extractors/rust.d.ts.map +1 -1
  162. package/dist/extractors/rust.js +58 -82
  163. package/dist/extractors/rust.js.map +1 -1
  164. package/dist/extractors/scala.d.ts +6 -0
  165. package/dist/extractors/scala.d.ts.map +1 -0
  166. package/dist/extractors/scala.js +269 -0
  167. package/dist/extractors/scala.js.map +1 -0
  168. package/dist/extractors/swift.d.ts +6 -0
  169. package/dist/extractors/swift.d.ts.map +1 -0
  170. package/dist/extractors/swift.js +275 -0
  171. package/dist/extractors/swift.js.map +1 -0
  172. package/dist/features/ast.d.ts +16 -1
  173. package/dist/features/ast.d.ts.map +1 -1
  174. package/dist/features/ast.js +45 -23
  175. package/dist/features/ast.js.map +1 -1
  176. package/dist/features/audit.d.ts.map +1 -1
  177. package/dist/features/audit.js +17 -21
  178. package/dist/features/audit.js.map +1 -1
  179. package/dist/features/branch-compare.d.ts.map +1 -1
  180. package/dist/features/branch-compare.js +50 -4
  181. package/dist/features/branch-compare.js.map +1 -1
  182. package/dist/features/cfg.d.ts +7 -1
  183. package/dist/features/cfg.d.ts.map +1 -1
  184. package/dist/features/cfg.js +118 -62
  185. package/dist/features/cfg.js.map +1 -1
  186. package/dist/features/check.d.ts.map +1 -1
  187. package/dist/features/check.js +79 -62
  188. package/dist/features/check.js.map +1 -1
  189. package/dist/features/complexity-query.d.ts.map +1 -1
  190. package/dist/features/complexity-query.js +142 -137
  191. package/dist/features/complexity-query.js.map +1 -1
  192. package/dist/features/complexity.d.ts +7 -1
  193. package/dist/features/complexity.d.ts.map +1 -1
  194. package/dist/features/complexity.js +62 -1
  195. package/dist/features/complexity.js.map +1 -1
  196. package/dist/features/dataflow.d.ts +7 -1
  197. package/dist/features/dataflow.d.ts.map +1 -1
  198. package/dist/features/dataflow.js +356 -188
  199. package/dist/features/dataflow.js.map +1 -1
  200. package/dist/features/graph-enrichment.d.ts.map +1 -1
  201. package/dist/features/graph-enrichment.js +117 -104
  202. package/dist/features/graph-enrichment.js.map +1 -1
  203. package/dist/features/sequence.d.ts.map +1 -1
  204. package/dist/features/sequence.js +25 -4
  205. package/dist/features/sequence.js.map +1 -1
  206. package/dist/features/snapshot.d.ts.map +1 -1
  207. package/dist/features/snapshot.js +2 -1
  208. package/dist/features/snapshot.js.map +1 -1
  209. package/dist/features/structure-query.d.ts.map +1 -1
  210. package/dist/features/structure-query.js +29 -4
  211. package/dist/features/structure-query.js.map +1 -1
  212. package/dist/features/structure.d.ts.map +1 -1
  213. package/dist/features/structure.js +35 -15
  214. package/dist/features/structure.js.map +1 -1
  215. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  216. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  217. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  218. package/dist/graph/algorithms/leiden/index.js +43 -28
  219. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  220. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  221. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  222. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  223. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  224. package/dist/graph/algorithms/leiden/partition.js +89 -106
  225. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  226. package/dist/graph/model.d.ts +2 -0
  227. package/dist/graph/model.d.ts.map +1 -1
  228. package/dist/graph/model.js +20 -8
  229. package/dist/graph/model.js.map +1 -1
  230. package/dist/infrastructure/config.d.ts +0 -8
  231. package/dist/infrastructure/config.d.ts.map +1 -1
  232. package/dist/infrastructure/config.js +73 -62
  233. package/dist/infrastructure/config.js.map +1 -1
  234. package/dist/infrastructure/registry.d.ts +0 -8
  235. package/dist/infrastructure/registry.d.ts.map +1 -1
  236. package/dist/infrastructure/registry.js +12 -14
  237. package/dist/infrastructure/registry.js.map +1 -1
  238. package/dist/mcp/server.d.ts.map +1 -1
  239. package/dist/mcp/server.js +47 -45
  240. package/dist/mcp/server.js.map +1 -1
  241. package/dist/presentation/audit.d.ts.map +1 -1
  242. package/dist/presentation/audit.js +61 -57
  243. package/dist/presentation/audit.js.map +1 -1
  244. package/dist/presentation/branch-compare.d.ts.map +1 -1
  245. package/dist/presentation/branch-compare.js +56 -38
  246. package/dist/presentation/branch-compare.js.map +1 -1
  247. package/dist/presentation/check.d.ts.map +1 -1
  248. package/dist/presentation/check.js +30 -32
  249. package/dist/presentation/check.js.map +1 -1
  250. package/dist/presentation/colors.d.ts.map +1 -1
  251. package/dist/presentation/colors.js +2 -0
  252. package/dist/presentation/colors.js.map +1 -1
  253. package/dist/presentation/complexity.d.ts.map +1 -1
  254. package/dist/presentation/complexity.js +25 -19
  255. package/dist/presentation/complexity.js.map +1 -1
  256. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  257. package/dist/presentation/queries-cli/exports.js +15 -15
  258. package/dist/presentation/queries-cli/exports.js.map +1 -1
  259. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  260. package/dist/presentation/queries-cli/impact.js +29 -19
  261. package/dist/presentation/queries-cli/impact.js.map +1 -1
  262. package/dist/types.d.ts +406 -3
  263. package/dist/types.d.ts.map +1 -1
  264. package/grammars/tree-sitter-bash.wasm +0 -0
  265. package/grammars/tree-sitter-c.wasm +0 -0
  266. package/grammars/tree-sitter-cpp.wasm +0 -0
  267. package/grammars/tree-sitter-kotlin.wasm +0 -0
  268. package/grammars/tree-sitter-scala.wasm +0 -0
  269. package/grammars/tree-sitter-swift.wasm +0 -0
  270. package/package.json +67 -11
  271. package/src/ast-analysis/engine.ts +147 -138
  272. package/src/ast-analysis/rules/javascript.ts +1 -0
  273. package/src/ast-analysis/visitors/ast-store-visitor.ts +116 -34
  274. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  275. package/src/db/better-sqlite3.ts +20 -0
  276. package/src/db/connection.ts +148 -26
  277. package/src/db/index.ts +4 -1
  278. package/src/db/migrations.ts +38 -32
  279. package/src/db/query-builder.ts +30 -5
  280. package/src/db/repository/index.ts +1 -0
  281. package/src/db/repository/native-repository.ts +361 -0
  282. package/src/db/repository/nodes.ts +7 -3
  283. package/src/domain/analysis/context.ts +73 -75
  284. package/src/domain/analysis/dependencies.ts +78 -68
  285. package/src/domain/analysis/exports.ts +45 -34
  286. package/src/domain/analysis/fn-impact.ts +67 -64
  287. package/src/domain/analysis/module-map.ts +103 -8
  288. package/src/domain/analysis/query-helpers.ts +35 -0
  289. package/src/domain/graph/builder/context.ts +2 -0
  290. package/src/domain/graph/builder/helpers.ts +12 -6
  291. package/src/domain/graph/builder/incremental.ts +3 -2
  292. package/src/domain/graph/builder/pipeline.ts +98 -6
  293. package/src/domain/graph/builder/stages/build-edges.ts +116 -83
  294. package/src/domain/graph/builder/stages/build-structure.ts +46 -8
  295. package/src/domain/graph/builder/stages/collect-files.ts +83 -6
  296. package/src/domain/graph/builder/stages/detect-changes.ts +44 -21
  297. package/src/domain/graph/builder/stages/finalize.ts +172 -109
  298. package/src/domain/graph/builder/stages/insert-nodes.ts +147 -17
  299. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  300. package/src/domain/graph/resolve.ts +34 -46
  301. package/src/domain/graph/watcher.ts +12 -14
  302. package/src/domain/parser.ts +169 -97
  303. package/src/domain/search/search/cli-formatter.ts +121 -94
  304. package/src/extractors/bash.ts +97 -0
  305. package/src/extractors/c.ts +212 -0
  306. package/src/extractors/cpp.ts +298 -0
  307. package/src/extractors/csharp.ts +53 -56
  308. package/src/extractors/go.ts +152 -134
  309. package/src/extractors/hcl.ts +6 -6
  310. package/src/extractors/helpers.ts +93 -1
  311. package/src/extractors/index.ts +6 -0
  312. package/src/extractors/java.ts +43 -48
  313. package/src/extractors/javascript.ts +382 -317
  314. package/src/extractors/kotlin.ts +293 -0
  315. package/src/extractors/php.ts +46 -40
  316. package/src/extractors/python.ts +81 -104
  317. package/src/extractors/ruby.ts +6 -13
  318. package/src/extractors/rust.ts +65 -84
  319. package/src/extractors/scala.ts +285 -0
  320. package/src/extractors/swift.ts +293 -0
  321. package/src/features/ast.ts +74 -24
  322. package/src/features/audit.ts +24 -20
  323. package/src/features/branch-compare.ts +54 -5
  324. package/src/features/cfg.ts +158 -65
  325. package/src/features/check.ts +90 -74
  326. package/src/features/complexity-query.ts +181 -163
  327. package/src/features/complexity.ts +64 -1
  328. package/src/features/dataflow.ts +462 -217
  329. package/src/features/graph-enrichment.ts +161 -117
  330. package/src/features/sequence.ts +27 -4
  331. package/src/features/snapshot.ts +2 -1
  332. package/src/features/structure-query.ts +43 -4
  333. package/src/features/structure.ts +50 -22
  334. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  335. package/src/graph/algorithms/leiden/index.ts +67 -28
  336. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  337. package/src/graph/algorithms/leiden/partition.ts +131 -98
  338. package/src/graph/model.ts +19 -7
  339. package/src/infrastructure/config.ts +60 -58
  340. package/src/infrastructure/registry.ts +17 -14
  341. package/src/mcp/server.ts +48 -47
  342. package/src/presentation/audit.ts +72 -67
  343. package/src/presentation/branch-compare.ts +54 -50
  344. package/src/presentation/check.ts +34 -34
  345. package/src/presentation/colors.ts +2 -0
  346. package/src/presentation/complexity.ts +39 -33
  347. package/src/presentation/queries-cli/exports.ts +17 -17
  348. package/src/presentation/queries-cli/impact.ts +30 -22
  349. package/src/types.ts +458 -3
@@ -14,7 +14,7 @@ import { DATAFLOW_RULES } from '../ast-analysis/rules/index.js';
14
14
  import { makeDataflowRules as _makeDataflowRules, buildExtensionSet, buildExtToLangMap, } from '../ast-analysis/shared.js';
15
15
  import { walkWithVisitors } from '../ast-analysis/visitor.js';
16
16
  import { createDataflowVisitor } from '../ast-analysis/visitors/dataflow-visitor.js';
17
- import { hasDataflowTable, openReadonlyOrFail } from '../db/index.js';
17
+ import { hasDataflowTable, openReadonlyOrFail, openReadonlyWithNative } from '../db/index.js';
18
18
  import { ALL_SYMBOL_KINDS, normalizeSymbol } from '../domain/queries.js';
19
19
  import { debug, info } from '../infrastructure/logger.js';
20
20
  import { isTestFile } from '../infrastructure/test-filter.js';
@@ -125,8 +125,96 @@ function insertDataflowEdges(insert, data, resolveNode) {
125
125
  return edgeCount;
126
126
  }
127
127
  // ── buildDataflowEdges ──────────────────────────────────────────────────────
128
- export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts) {
128
+ export async function buildDataflowEdges(db, fileSymbols, rootDir, engineOpts) {
129
129
  const extToLang = buildExtToLangMap();
130
+ // ── Native bulk-insert fast path ──────────────────────────────────────
131
+ const nativeDb = engineOpts?.nativeDb;
132
+ if (nativeDb?.bulkInsertDataflow) {
133
+ let needsJsFallback = false;
134
+ const nativeEdges = [];
135
+ const getNodeByNameAndFile = db.prepare(`SELECT id, name, kind, file, line FROM nodes
136
+ WHERE name = ? AND file = ? AND kind IN ('function', 'method')`);
137
+ const getNodeByName = db.prepare(`SELECT id, name, kind, file, line FROM nodes
138
+ WHERE name = ? AND kind IN ('function', 'method')
139
+ ORDER BY file, line LIMIT 10`);
140
+ for (const [relPath, symbols] of fileSymbols) {
141
+ const ext = path.extname(relPath).toLowerCase();
142
+ if (!DATAFLOW_EXTENSIONS.has(ext))
143
+ continue;
144
+ if (!symbols.dataflow) {
145
+ needsJsFallback = true;
146
+ break;
147
+ }
148
+ const resolveNode = (funcName) => {
149
+ const local = getNodeByNameAndFile.all(funcName, relPath);
150
+ if (local.length > 0)
151
+ return local[0];
152
+ const global = getNodeByName.all(funcName);
153
+ return global.length > 0 ? global[0] : null;
154
+ };
155
+ const data = symbols.dataflow;
156
+ for (const flow of data.argFlows) {
157
+ const sourceNode = resolveNode(flow.callerFunc);
158
+ const targetNode = resolveNode(flow.calleeName);
159
+ if (sourceNode && targetNode) {
160
+ nativeEdges.push({
161
+ sourceId: sourceNode.id,
162
+ targetId: targetNode.id,
163
+ kind: 'flows_to',
164
+ paramIndex: flow.argIndex,
165
+ expression: flow.expression,
166
+ line: flow.line,
167
+ confidence: flow.confidence,
168
+ });
169
+ }
170
+ }
171
+ for (const assignment of data.assignments) {
172
+ const producerNode = resolveNode(assignment.sourceCallName);
173
+ const consumerNode = resolveNode(assignment.callerFunc);
174
+ if (producerNode && consumerNode) {
175
+ nativeEdges.push({
176
+ sourceId: producerNode.id,
177
+ targetId: consumerNode.id,
178
+ kind: 'returns',
179
+ paramIndex: null,
180
+ expression: assignment.expression,
181
+ line: assignment.line,
182
+ confidence: 1.0,
183
+ });
184
+ }
185
+ }
186
+ for (const mut of data.mutations) {
187
+ const mutatorNode = resolveNode(mut.funcName);
188
+ if (mutatorNode && mut.binding?.type === 'param') {
189
+ nativeEdges.push({
190
+ sourceId: mutatorNode.id,
191
+ targetId: mutatorNode.id,
192
+ kind: 'mutates',
193
+ paramIndex: null,
194
+ expression: mut.mutatingExpr,
195
+ line: mut.line,
196
+ confidence: 1.0,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ if (!needsJsFallback) {
202
+ if (nativeEdges.length > 0) {
203
+ let inserted;
204
+ try {
205
+ engineOpts?.suspendJsDb?.();
206
+ inserted = nativeDb.bulkInsertDataflow(nativeEdges);
207
+ }
208
+ finally {
209
+ engineOpts?.resumeJsDb?.();
210
+ }
211
+ info(`Dataflow (native bulk): ${inserted} edges inserted`);
212
+ }
213
+ return;
214
+ }
215
+ debug('Dataflow: some files lack pre-computed data — falling back to JS');
216
+ }
217
+ // ── JS fallback path ─────────────────────────────────────────────────
130
218
  const { parsers, getParserFn } = await initDataflowParsers(fileSymbols);
131
219
  const insert = db.prepare(`INSERT INTO dataflow (source_id, target_id, kind, param_index, expression, line, confidence)
132
220
  VALUES (?, ?, ?, ?, ?, ?, ?)`);
@@ -157,10 +245,96 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
157
245
  tx();
158
246
  info(`Dataflow: ${totalEdges} edges inserted`);
159
247
  }
160
- // ── Query functions ─────────────────────────────────────────────────────────
161
- // findNodes imported from ./shared/find-nodes.js
248
+ function prepareDataflowStmts(db) {
249
+ return {
250
+ flowsToOut: db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
251
+ FROM dataflow d JOIN nodes n ON d.target_id = n.id
252
+ WHERE d.source_id = ? AND d.kind = 'flows_to'`),
253
+ flowsToIn: db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
254
+ FROM dataflow d JOIN nodes n ON d.source_id = n.id
255
+ WHERE d.target_id = ? AND d.kind = 'flows_to'`),
256
+ returnsOut: db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
257
+ FROM dataflow d JOIN nodes n ON d.target_id = n.id
258
+ WHERE d.source_id = ? AND d.kind = 'returns'`),
259
+ returnsIn: db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
260
+ FROM dataflow d JOIN nodes n ON d.source_id = n.id
261
+ WHERE d.target_id = ? AND d.kind = 'returns'`),
262
+ mutatesOut: db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
263
+ FROM dataflow d JOIN nodes n ON d.target_id = n.id
264
+ WHERE d.source_id = ? AND d.kind = 'mutates'`),
265
+ mutatesIn: db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
266
+ FROM dataflow d JOIN nodes n ON d.source_id = n.id
267
+ WHERE d.target_id = ? AND d.kind = 'mutates'`),
268
+ };
269
+ }
270
+ function buildNodeDataflowResult(node, stmts, db, hc, noTests) {
271
+ const sym = normalizeSymbol(node, db, hc);
272
+ const flowsTo = stmts.flowsToOut.all(node.id).map((r) => ({
273
+ target: r.target_name,
274
+ kind: r.target_kind,
275
+ file: r.target_file,
276
+ line: r.line,
277
+ paramIndex: r.param_index,
278
+ expression: r.expression,
279
+ confidence: r.confidence,
280
+ }));
281
+ const flowsFrom = stmts.flowsToIn.all(node.id).map((r) => ({
282
+ source: r.source_name,
283
+ kind: r.source_kind,
284
+ file: r.source_file,
285
+ line: r.line,
286
+ paramIndex: r.param_index,
287
+ expression: r.expression,
288
+ confidence: r.confidence,
289
+ }));
290
+ const returnConsumers = stmts.returnsOut.all(node.id).map((r) => ({
291
+ consumer: r.target_name,
292
+ kind: r.target_kind,
293
+ file: r.target_file,
294
+ line: r.line,
295
+ expression: r.expression,
296
+ }));
297
+ const returnedBy = stmts.returnsIn.all(node.id).map((r) => ({
298
+ producer: r.source_name,
299
+ kind: r.source_kind,
300
+ file: r.source_file,
301
+ line: r.line,
302
+ expression: r.expression,
303
+ }));
304
+ const mutatesTargets = stmts.mutatesOut.all(node.id).map((r) => ({
305
+ target: r.target_name,
306
+ expression: r.expression,
307
+ line: r.line,
308
+ }));
309
+ const mutatedBy = stmts.mutatesIn.all(node.id).map((r) => ({
310
+ source: r.source_name,
311
+ expression: r.expression,
312
+ line: r.line,
313
+ }));
314
+ if (noTests) {
315
+ const filter = (arr) => arr.filter((r) => !isTestFile(r.file));
316
+ return {
317
+ ...sym,
318
+ flowsTo: filter(flowsTo),
319
+ flowsFrom: filter(flowsFrom),
320
+ returns: returnConsumers.filter((r) => !isTestFile(r.file)),
321
+ returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
322
+ mutates: mutatesTargets,
323
+ mutatedBy,
324
+ };
325
+ }
326
+ return {
327
+ ...sym,
328
+ flowsTo,
329
+ flowsFrom,
330
+ returns: returnConsumers,
331
+ returnedBy,
332
+ mutates: mutatesTargets,
333
+ mutatedBy,
334
+ };
335
+ }
162
336
  export function dataflowData(name, customDbPath, opts = {}) {
163
- const db = openReadonlyOrFail(customDbPath);
337
+ const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
164
338
  try {
165
339
  const noTests = opts.noTests || false;
166
340
  if (!hasDataflowTable(db)) {
@@ -174,98 +348,163 @@ export function dataflowData(name, customDbPath, opts = {}) {
174
348
  if (nodes.length === 0) {
175
349
  return { name, results: [] };
176
350
  }
177
- const flowsToOut = db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
178
- FROM dataflow d JOIN nodes n ON d.target_id = n.id
179
- WHERE d.source_id = ? AND d.kind = 'flows_to'`);
180
- const flowsToIn = db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
181
- FROM dataflow d JOIN nodes n ON d.source_id = n.id
182
- WHERE d.target_id = ? AND d.kind = 'flows_to'`);
183
- const returnsOut = db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
184
- FROM dataflow d JOIN nodes n ON d.target_id = n.id
185
- WHERE d.source_id = ? AND d.kind = 'returns'`);
186
- const returnsIn = db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
187
- FROM dataflow d JOIN nodes n ON d.source_id = n.id
188
- WHERE d.target_id = ? AND d.kind = 'returns'`);
189
- const mutatesOut = db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
190
- FROM dataflow d JOIN nodes n ON d.target_id = n.id
191
- WHERE d.source_id = ? AND d.kind = 'mutates'`);
192
- const mutatesIn = db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
193
- FROM dataflow d JOIN nodes n ON d.source_id = n.id
194
- WHERE d.target_id = ? AND d.kind = 'mutates'`);
195
- const hc = new Map();
196
- const results = nodes.map((node) => {
197
- const sym = normalizeSymbol(node, db, hc);
198
- const flowsTo = flowsToOut.all(node.id).map((r) => ({
199
- target: r.target_name,
200
- kind: r.target_kind,
201
- file: r.target_file,
202
- line: r.line,
203
- paramIndex: r.param_index,
204
- expression: r.expression,
205
- confidence: r.confidence,
206
- }));
207
- const flowsFrom = flowsToIn.all(node.id).map((r) => ({
208
- source: r.source_name,
209
- kind: r.source_kind,
210
- file: r.source_file,
211
- line: r.line,
212
- paramIndex: r.param_index,
213
- expression: r.expression,
214
- confidence: r.confidence,
215
- }));
216
- const returnConsumers = returnsOut.all(node.id).map((r) => ({
217
- consumer: r.target_name,
218
- kind: r.target_kind,
219
- file: r.target_file,
220
- line: r.line,
221
- expression: r.expression,
222
- }));
223
- const returnedBy = returnsIn.all(node.id).map((r) => ({
224
- producer: r.source_name,
225
- kind: r.source_kind,
226
- file: r.source_file,
227
- line: r.line,
228
- expression: r.expression,
229
- }));
230
- const mutatesTargets = mutatesOut.all(node.id).map((r) => ({
231
- target: r.target_name,
232
- expression: r.expression,
233
- line: r.line,
234
- }));
235
- const mutatedBy = mutatesIn.all(node.id).map((r) => ({
236
- source: r.source_name,
237
- expression: r.expression,
238
- line: r.line,
239
- }));
240
- if (noTests) {
241
- const filter = (arr) => arr.filter((r) => !isTestFile(r.file));
351
+ // ── Native fast path: 6 queries per node 1 napi call per node ──
352
+ if (nativeDb?.getDataflowEdges) {
353
+ const hc = new Map();
354
+ const results = nodes.map((node) => {
355
+ const sym = normalizeSymbol(node, db, hc);
356
+ const d = nativeDb.getDataflowEdges(node.id);
357
+ const flowsTo = d.flowsToOut.map((r) => ({
358
+ target: r.name,
359
+ kind: r.kind,
360
+ file: r.file,
361
+ line: r.line,
362
+ paramIndex: r.paramIndex,
363
+ expression: r.expression,
364
+ confidence: r.confidence,
365
+ }));
366
+ const flowsFrom = d.flowsToIn.map((r) => ({
367
+ source: r.name,
368
+ kind: r.kind,
369
+ file: r.file,
370
+ line: r.line,
371
+ paramIndex: r.paramIndex,
372
+ expression: r.expression,
373
+ confidence: r.confidence,
374
+ }));
375
+ const returnConsumers = d.returnsOut.map((r) => ({
376
+ consumer: r.name,
377
+ kind: r.kind,
378
+ file: r.file,
379
+ line: r.line,
380
+ expression: r.expression,
381
+ }));
382
+ const returnedBy = d.returnsIn.map((r) => ({
383
+ producer: r.name,
384
+ kind: r.kind,
385
+ file: r.file,
386
+ line: r.line,
387
+ expression: r.expression,
388
+ }));
389
+ const mutatesTargets = d.mutatesOut.map((r) => ({
390
+ target: r.name,
391
+ expression: r.expression,
392
+ line: r.line,
393
+ }));
394
+ const mutatedBy = d.mutatesIn.map((r) => ({
395
+ source: r.name,
396
+ expression: r.expression,
397
+ line: r.line,
398
+ }));
399
+ if (noTests) {
400
+ const filter = (arr) => arr.filter((r) => !isTestFile(r.file));
401
+ return {
402
+ ...sym,
403
+ flowsTo: filter(flowsTo),
404
+ flowsFrom: filter(flowsFrom),
405
+ returns: returnConsumers.filter((r) => !isTestFile(r.file)),
406
+ returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
407
+ mutates: mutatesTargets,
408
+ mutatedBy,
409
+ };
410
+ }
242
411
  return {
243
412
  ...sym,
244
- flowsTo: filter(flowsTo),
245
- flowsFrom: filter(flowsFrom),
246
- returns: returnConsumers.filter((r) => !isTestFile(r.file)),
247
- returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
413
+ flowsTo,
414
+ flowsFrom,
415
+ returns: returnConsumers,
416
+ returnedBy,
248
417
  mutates: mutatesTargets,
249
418
  mutatedBy,
250
419
  };
251
- }
252
- return {
253
- ...sym,
254
- flowsTo,
255
- flowsFrom,
256
- returns: returnConsumers,
257
- returnedBy,
258
- mutates: mutatesTargets,
259
- mutatedBy,
260
- };
261
- });
420
+ });
421
+ const base = { name, results };
422
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
423
+ }
424
+ // ── JS fallback ───────────────────────────────────────────────────
425
+ const stmts = prepareDataflowStmts(db);
426
+ const hc = new Map();
427
+ const results = nodes.map((node) => buildNodeDataflowResult(node, stmts, db, hc, noTests));
262
428
  const base = { name, results };
263
429
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
264
430
  }
265
431
  finally {
266
- db.close();
432
+ close();
267
433
  }
268
434
  }
435
+ /** BFS through dataflow edges to find a path from source to target. */
436
+ function bfsDataflowPath(db, sourceId, targetId, maxDepth, noTests) {
437
+ const neighborStmt = db.prepare(`SELECT n.id, n.name, n.kind, n.file, n.line, d.kind AS edge_kind, d.expression
438
+ FROM dataflow d JOIN nodes n ON d.target_id = n.id
439
+ WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`);
440
+ const visited = new Set([sourceId]);
441
+ const parent = new Map();
442
+ let queue = [sourceId];
443
+ let found = false;
444
+ for (let depth = 1; depth <= maxDepth; depth++) {
445
+ const nextQueue = [];
446
+ for (const currentId of queue) {
447
+ const neighbors = neighborStmt.all(currentId);
448
+ for (const n of neighbors) {
449
+ if (noTests && isTestFile(n.file))
450
+ continue;
451
+ if (n.id === targetId) {
452
+ if (!found) {
453
+ found = true;
454
+ parent.set(n.id, {
455
+ parentId: currentId,
456
+ edgeKind: n.edge_kind,
457
+ expression: n.expression,
458
+ });
459
+ }
460
+ continue;
461
+ }
462
+ if (!visited.has(n.id)) {
463
+ visited.add(n.id);
464
+ parent.set(n.id, {
465
+ parentId: currentId,
466
+ edgeKind: n.edge_kind,
467
+ expression: n.expression,
468
+ });
469
+ nextQueue.push(n.id);
470
+ }
471
+ }
472
+ }
473
+ if (found)
474
+ break;
475
+ queue = nextQueue;
476
+ if (queue.length === 0)
477
+ break;
478
+ }
479
+ return found ? parent : null;
480
+ }
481
+ /** Reconstruct a path from BFS parent map. */
482
+ function reconstructDataflowPath(db, parent, sourceId, targetId) {
483
+ const nodeById = db.prepare('SELECT * FROM nodes WHERE id = ?');
484
+ const hc = new Map();
485
+ const pathItems = [];
486
+ let cur = targetId;
487
+ while (cur !== undefined) {
488
+ const nodeRow = nodeById.get(cur);
489
+ const parentInfo = parent.get(cur);
490
+ pathItems.unshift({
491
+ ...normalizeSymbol(nodeRow, db, hc),
492
+ edgeKind: parentInfo?.edgeKind ?? null,
493
+ expression: parentInfo?.expression ?? null,
494
+ });
495
+ cur = parentInfo?.parentId;
496
+ if (cur === sourceId) {
497
+ const srcRow = nodeById.get(cur);
498
+ pathItems.unshift({
499
+ ...normalizeSymbol(srcRow, db, hc),
500
+ edgeKind: null,
501
+ expression: null,
502
+ });
503
+ break;
504
+ }
505
+ }
506
+ return pathItems;
507
+ }
269
508
  export function dataflowPathData(from, to, customDbPath, opts = {}) {
270
509
  const db = openReadonlyOrFail(customDbPath);
271
510
  try {
@@ -292,90 +531,44 @@ export function dataflowPathData(from, to, customDbPath, opts = {}) {
292
531
  if (sourceNode.id === targetNode.id) {
293
532
  const hc = new Map();
294
533
  const sym = normalizeSymbol(sourceNode, db, hc);
295
- return {
296
- from,
297
- to,
298
- found: true,
299
- hops: 0,
300
- path: [{ ...sym, edgeKind: null }],
301
- };
302
- }
303
- // BFS through flows_to and returns edges
304
- const neighborStmt = db.prepare(`SELECT n.id, n.name, n.kind, n.file, n.line, d.kind AS edge_kind, d.expression
305
- FROM dataflow d JOIN nodes n ON d.target_id = n.id
306
- WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`);
307
- const visited = new Set([sourceNode.id]);
308
- const parent = new Map();
309
- let queue = [sourceNode.id];
310
- let found = false;
311
- for (let depth = 1; depth <= maxDepth; depth++) {
312
- const nextQueue = [];
313
- for (const currentId of queue) {
314
- const neighbors = neighborStmt.all(currentId);
315
- for (const n of neighbors) {
316
- if (noTests && isTestFile(n.file))
317
- continue;
318
- if (n.id === targetNode.id) {
319
- if (!found) {
320
- found = true;
321
- parent.set(n.id, {
322
- parentId: currentId,
323
- edgeKind: n.edge_kind,
324
- expression: n.expression,
325
- });
326
- }
327
- continue;
328
- }
329
- if (!visited.has(n.id)) {
330
- visited.add(n.id);
331
- parent.set(n.id, {
332
- parentId: currentId,
333
- edgeKind: n.edge_kind,
334
- expression: n.expression,
335
- });
336
- nextQueue.push(n.id);
337
- }
338
- }
339
- }
340
- if (found)
341
- break;
342
- queue = nextQueue;
343
- if (queue.length === 0)
344
- break;
534
+ return { from, to, found: true, hops: 0, path: [{ ...sym, edgeKind: null }] };
345
535
  }
346
- if (!found) {
536
+ const parent = bfsDataflowPath(db, sourceNode.id, targetNode.id, maxDepth, noTests);
537
+ if (!parent) {
347
538
  return { from, to, found: false };
348
539
  }
349
- // Reconstruct path
350
- const nodeById = db.prepare('SELECT * FROM nodes WHERE id = ?');
351
- const hc = new Map();
352
- const pathItems = [];
353
- let cur = targetNode.id;
354
- while (cur !== undefined) {
355
- const nodeRow = nodeById.get(cur);
356
- const parentInfo = parent.get(cur);
357
- pathItems.unshift({
358
- ...normalizeSymbol(nodeRow, db, hc),
359
- edgeKind: parentInfo?.edgeKind ?? null,
360
- expression: parentInfo?.expression ?? null,
361
- });
362
- cur = parentInfo?.parentId;
363
- if (cur === sourceNode.id) {
364
- const srcRow = nodeById.get(cur);
365
- pathItems.unshift({
366
- ...normalizeSymbol(srcRow, db, hc),
367
- edgeKind: null,
368
- expression: null,
369
- });
370
- break;
371
- }
372
- }
540
+ const pathItems = reconstructDataflowPath(db, parent, sourceNode.id, targetNode.id);
373
541
  return { from, to, found: true, hops: pathItems.length - 1, path: pathItems };
374
542
  }
375
543
  finally {
376
544
  db.close();
377
545
  }
378
546
  }
547
+ /** BFS forward through return-value consumers to build impact levels. */
548
+ function bfsReturnConsumers(node, consumersStmt, db, hc, maxDepth, noTests) {
549
+ const visited = new Set([node.id]);
550
+ const levels = {};
551
+ let frontier = [node.id];
552
+ for (let d = 1; d <= maxDepth; d++) {
553
+ const nextFrontier = [];
554
+ for (const fid of frontier) {
555
+ const consumers = consumersStmt.all(fid);
556
+ for (const c of consumers) {
557
+ if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
558
+ visited.add(c.id);
559
+ nextFrontier.push(c.id);
560
+ if (!levels[d])
561
+ levels[d] = [];
562
+ levels[d].push(normalizeSymbol(c, db, hc));
563
+ }
564
+ }
565
+ }
566
+ frontier = nextFrontier;
567
+ if (frontier.length === 0)
568
+ break;
569
+ }
570
+ return { levels, totalAffected: visited.size - 1 };
571
+ }
379
572
  export function dataflowImpactData(name, customDbPath, opts = {}) {
380
573
  const db = openReadonlyOrFail(customDbPath);
381
574
  try {
@@ -392,39 +585,14 @@ export function dataflowImpactData(name, customDbPath, opts = {}) {
392
585
  if (nodes.length === 0) {
393
586
  return { name, results: [] };
394
587
  }
395
- // Forward BFS: who consumes this function's return value (directly or transitively)?
396
588
  const consumersStmt = db.prepare(`SELECT DISTINCT n.*
397
589
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
398
590
  WHERE d.source_id = ? AND d.kind = 'returns'`);
399
591
  const hc = new Map();
400
592
  const results = nodes.map((node) => {
401
593
  const sym = normalizeSymbol(node, db, hc);
402
- const visited = new Set([node.id]);
403
- const levels = {};
404
- let frontier = [node.id];
405
- for (let d = 1; d <= maxDepth; d++) {
406
- const nextFrontier = [];
407
- for (const fid of frontier) {
408
- const consumers = consumersStmt.all(fid);
409
- for (const c of consumers) {
410
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
411
- visited.add(c.id);
412
- nextFrontier.push(c.id);
413
- if (!levels[d])
414
- levels[d] = [];
415
- levels[d].push(normalizeSymbol(c, db, hc));
416
- }
417
- }
418
- }
419
- frontier = nextFrontier;
420
- if (frontier.length === 0)
421
- break;
422
- }
423
- return {
424
- ...sym,
425
- levels,
426
- totalAffected: visited.size - 1,
427
- };
594
+ const { levels, totalAffected } = bfsReturnConsumers(node, consumersStmt, db, hc, maxDepth, noTests);
595
+ return { ...sym, levels, totalAffected };
428
596
  });
429
597
  const base = { name, results };
430
598
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });