@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
@@ -5,7 +5,6 @@ import {
5
5
  findImportSources,
6
6
  findImportTargets,
7
7
  findNodesByFile,
8
- openReadonlyOrFail,
9
8
  } from '../../db/index.js';
10
9
  import { cachedStmt } from '../../db/repository/cached-stmt.js';
11
10
  import { isTestFile } from '../../infrastructure/test-filter.js';
@@ -19,6 +18,7 @@ import type {
19
18
  RelatedNodeRow,
20
19
  StmtCache,
21
20
  } from '../../types.js';
21
+ import { withReadonlyDb } from './query-helpers.js';
22
22
  import { findMatchingNodes } from './symbol-lookup.js';
23
23
 
24
24
  type UpstreamRow = { id: number; name: string; kind: string; file: string; line: number };
@@ -32,8 +32,7 @@ export function fileDepsData(
32
32
  customDbPath: string,
33
33
  opts: { noTests?: boolean; limit?: number; offset?: number } = {},
34
34
  ) {
35
- const db = openReadonlyOrFail(customDbPath);
36
- try {
35
+ return withReadonlyDb(customDbPath, (db) => {
37
36
  const noTests = opts.noTests || false;
38
37
  const fileNodes = findFileNodes(db, `%${file}%`) as NodeRow[];
39
38
  if (fileNodes.length === 0) {
@@ -59,9 +58,7 @@ export function fileDepsData(
59
58
 
60
59
  const base = { file, results };
61
60
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
62
- } finally {
63
- db.close();
64
- }
61
+ });
65
62
  }
66
63
 
67
64
  /**
@@ -140,8 +137,7 @@ export function fnDepsData(
140
137
  offset?: number;
141
138
  } = {},
142
139
  ) {
143
- const db = openReadonlyOrFail(customDbPath);
144
- try {
140
+ return withReadonlyDb(customDbPath, (db) => {
145
141
  const depth = opts.depth || 3;
146
142
  const noTests = opts.noTests || false;
147
143
  const hc = new Map();
@@ -194,9 +190,7 @@ export function fnDepsData(
194
190
 
195
191
  const base = { name, results };
196
192
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
197
- } finally {
198
- db.close();
199
- }
193
+ });
200
194
  }
201
195
 
202
196
  /**
@@ -384,8 +378,7 @@ export function pathData(
384
378
  kind?: string;
385
379
  } = {},
386
380
  ) {
387
- const db = openReadonlyOrFail(customDbPath);
388
- try {
381
+ return withReadonlyDb(customDbPath, (db) => {
389
382
  const noTests = opts.noTests || false;
390
383
  const maxDepth = opts.maxDepth || 10;
391
384
  const edgeKinds = opts.edgeKinds || ['calls'];
@@ -477,13 +470,67 @@ export function pathData(
477
470
  reverse,
478
471
  maxDepth,
479
472
  };
480
- } finally {
481
- db.close();
482
- }
473
+ });
483
474
  }
484
475
 
485
476
  // ── File-level shortest path ────────────────────────────────────────────
486
477
 
478
+ /** BFS over file adjacency graph to find shortest path. */
479
+ function bfsFilePath(
480
+ neighborStmt: ReturnType<BetterSqlite3Database['prepare']>,
481
+ sourceFile: string,
482
+ targetFile: string,
483
+ edgeKinds: string[],
484
+ maxDepth: number,
485
+ noTests: boolean,
486
+ ): { found: boolean; path: string[]; alternateCount: number } {
487
+ const visited = new Set([sourceFile]);
488
+ const parentMap = new Map<string, string>();
489
+ let queue = [sourceFile];
490
+ let found = false;
491
+ let alternateCount = 0;
492
+
493
+ for (let depth = 1; depth <= maxDepth; depth++) {
494
+ const nextQueue: string[] = [];
495
+ for (const currentFile of queue) {
496
+ const neighbors = neighborStmt.all(currentFile, ...edgeKinds) as Array<{
497
+ neighbor_file: string;
498
+ }>;
499
+ for (const n of neighbors) {
500
+ if (noTests && isTestFile(n.neighbor_file)) continue;
501
+ if (n.neighbor_file === targetFile) {
502
+ if (!found) {
503
+ found = true;
504
+ parentMap.set(n.neighbor_file, currentFile);
505
+ }
506
+ alternateCount++;
507
+ continue;
508
+ }
509
+ if (!visited.has(n.neighbor_file)) {
510
+ visited.add(n.neighbor_file);
511
+ parentMap.set(n.neighbor_file, currentFile);
512
+ nextQueue.push(n.neighbor_file);
513
+ }
514
+ }
515
+ }
516
+ if (found) break;
517
+ queue = nextQueue;
518
+ if (queue.length === 0) break;
519
+ }
520
+
521
+ if (!found) return { found: false, path: [], alternateCount: 0 };
522
+
523
+ // Reconstruct path
524
+ const filePath: string[] = [targetFile];
525
+ let cur = targetFile;
526
+ while (cur !== sourceFile) {
527
+ cur = parentMap.get(cur)!;
528
+ filePath.push(cur);
529
+ }
530
+ filePath.reverse();
531
+ return { found: true, path: filePath, alternateCount: Math.max(0, alternateCount - 1) };
532
+ }
533
+
487
534
  /**
488
535
  * BFS at the file level: find shortest import/edge path between two files.
489
536
  * Adjacency: file A → file B if any symbol in A has an edge to any symbol in B.
@@ -499,8 +546,7 @@ export function filePathData(
499
546
  reverse?: boolean;
500
547
  } = {},
501
548
  ) {
502
- const db = openReadonlyOrFail(customDbPath);
503
- try {
549
+ return withReadonlyDb(customDbPath, (db) => {
504
550
  const noTests = opts.noTests || false;
505
551
  const maxDepth = opts.maxDepth || 10;
506
552
  const edgeKinds = opts.edgeKinds || ['imports', 'imports-type'];
@@ -569,42 +615,17 @@ export function filePathData(
569
615
  WHERE n_src.file = ? AND e.kind IN (${kindPlaceholders}) AND n_tgt.file != n_src.file`;
570
616
  const neighborStmt = db.prepare(neighborQuery);
571
617
 
572
- // BFS
573
- const visited = new Set([sourceFile]);
574
- const parentMap = new Map<string, string>();
575
- let queue = [sourceFile];
576
- let found = false;
577
- let alternateCount = 0;
578
-
579
- for (let depth = 1; depth <= maxDepth; depth++) {
580
- const nextQueue: string[] = [];
581
- for (const currentFile of queue) {
582
- const neighbors = neighborStmt.all(currentFile, ...edgeKinds) as Array<{
583
- neighbor_file: string;
584
- }>;
585
- for (const n of neighbors) {
586
- if (noTests && isTestFile(n.neighbor_file)) continue;
587
- if (n.neighbor_file === targetFile) {
588
- if (!found) {
589
- found = true;
590
- parentMap.set(n.neighbor_file, currentFile);
591
- }
592
- alternateCount++;
593
- continue;
594
- }
595
- if (!visited.has(n.neighbor_file)) {
596
- visited.add(n.neighbor_file);
597
- parentMap.set(n.neighbor_file, currentFile);
598
- nextQueue.push(n.neighbor_file);
599
- }
600
- }
601
- }
602
- if (found) break;
603
- queue = nextQueue;
604
- if (queue.length === 0) break;
605
- }
618
+ // BFS to find shortest file path
619
+ const bfsResult = bfsFilePath(
620
+ neighborStmt,
621
+ sourceFile,
622
+ targetFile,
623
+ edgeKinds,
624
+ maxDepth,
625
+ noTests,
626
+ );
606
627
 
607
- if (!found) {
628
+ if (!bfsResult.found) {
608
629
  return {
609
630
  from,
610
631
  to,
@@ -620,29 +641,18 @@ export function filePathData(
620
641
  };
621
642
  }
622
643
 
623
- // Reconstruct path
624
- const filePath: string[] = [targetFile];
625
- let cur = targetFile;
626
- while (cur !== sourceFile) {
627
- cur = parentMap.get(cur)!;
628
- filePath.push(cur);
629
- }
630
- filePath.reverse();
631
-
632
644
  return {
633
645
  from,
634
646
  to,
635
647
  fromCandidates,
636
648
  toCandidates,
637
649
  found: true,
638
- hops: filePath.length - 1,
639
- path: filePath,
640
- alternateCount: Math.max(0, alternateCount - 1),
650
+ hops: bfsResult.path.length - 1,
651
+ path: bfsResult.path,
652
+ alternateCount: bfsResult.alternateCount,
641
653
  edgeKinds,
642
654
  reverse,
643
655
  maxDepth,
644
656
  };
645
- } finally {
646
- db.close();
647
- }
657
+ });
648
658
  }
@@ -4,10 +4,8 @@ import {
4
4
  findDbPath,
5
5
  findFileNodes,
6
6
  findNodesByFile,
7
- openReadonlyOrFail,
8
7
  } from '../../db/index.js';
9
8
  import { cachedStmt } from '../../db/repository/cached-stmt.js';
10
- import { loadConfig } from '../../infrastructure/config.js';
11
9
  import { debug } from '../../infrastructure/logger.js';
12
10
  import { isTestFile } from '../../infrastructure/test-filter.js';
13
11
  import {
@@ -17,6 +15,7 @@ import {
17
15
  } from '../../shared/file-utils.js';
18
16
  import { paginateResult } from '../../shared/paginate.js';
19
17
  import type { BetterSqlite3Database, NodeRow, StmtCache } from '../../types.js';
18
+ import { resolveAnalysisOpts, withReadonlyDb } from './query-helpers.js';
20
19
 
21
20
  /** Cache the schema probe for the `exported` column per db handle. */
22
21
  const _hasExportedColCache: WeakMap<BetterSqlite3Database, boolean> = new WeakMap();
@@ -37,12 +36,8 @@ export function exportsData(
37
36
  config?: any;
38
37
  } = {},
39
38
  ) {
40
- const db = openReadonlyOrFail(customDbPath);
41
- try {
42
- const noTests = opts.noTests || false;
43
-
44
- const config = opts.config || loadConfig();
45
- const displayOpts = config.display || {};
39
+ return withReadonlyDb(customDbPath, (db) => {
40
+ const { noTests, displayOpts } = resolveAnalysisOpts(opts);
46
41
 
47
42
  const dbFilePath = findDbPath(customDbPath);
48
43
  const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
@@ -101,9 +96,39 @@ export function exportsData(
101
96
  }
102
97
  }
103
98
  return paginated;
104
- } finally {
105
- db.close();
99
+ });
100
+ }
101
+
102
+ /** Collect symbols re-exported through barrel files. */
103
+ function collectReexportedSymbols(
104
+ db: BetterSqlite3Database,
105
+ fileNodeId: number,
106
+ reexportsToStmt: ReturnType<BetterSqlite3Database['prepare']>,
107
+ exportedNodesStmt: ReturnType<BetterSqlite3Database['prepare']> | null,
108
+ hasExportedCol: boolean,
109
+ getFileLines: (file: string) => string[] | null,
110
+ buildSymbolResult: (s: NodeRow, fileLines: string[] | null) => any,
111
+ ) {
112
+ const reexportTargets = reexportsToStmt.all(fileNodeId) as Array<{ file: string }>;
113
+ const reexportedSymbols: Array<ReturnType<typeof buildSymbolResult> & { originFile: string }> =
114
+ [];
115
+ for (const reexTarget of reexportTargets) {
116
+ let targetExported: NodeRow[];
117
+ if (hasExportedCol) {
118
+ targetExported = exportedNodesStmt!.all(reexTarget.file) as NodeRow[];
119
+ } else {
120
+ const targetSymbols = findNodesByFile(db, reexTarget.file) as NodeRow[];
121
+ const exportedIds = findCrossFileCallTargets(db, reexTarget.file) as Set<number>;
122
+ targetExported = targetSymbols.filter((s) => exportedIds.has(s.id));
123
+ }
124
+ for (const s of targetExported) {
125
+ reexportedSymbols.push({
126
+ ...buildSymbolResult(s, getFileLines(reexTarget.file)),
127
+ originFile: reexTarget.file,
128
+ });
129
+ }
106
130
  }
131
+ return reexportedSymbols;
107
132
  }
108
133
 
109
134
  function exportsFileImpl(
@@ -197,34 +222,20 @@ function exportsFileImpl(
197
222
 
198
223
  const totalUnused = results.filter((r) => r.consumerCount === 0).length;
199
224
 
200
- // Files that re-export this file (barrel -> this file)
201
225
  const reexports = (reexportsFromStmt.all(fn.id) as Array<{ file: string }>).map((r) => ({
202
226
  file: r.file,
203
227
  }));
204
228
 
205
- // For barrel files: gather symbols re-exported from target modules
206
- const reexportTargets = reexportsToStmt.all(fn.id) as Array<{ file: string }>;
207
-
208
- const reexportedSymbols: Array<ReturnType<typeof buildSymbolResult> & { originFile: string }> =
209
- [];
210
- for (const reexTarget of reexportTargets) {
211
- let targetExported: NodeRow[];
212
- if (hasExportedCol) {
213
- targetExported = exportedNodesStmt!.all(reexTarget.file) as NodeRow[];
214
- } else {
215
- // Fallback: same heuristic as direct exports — symbols called from other files
216
- const targetSymbols = findNodesByFile(db, reexTarget.file) as NodeRow[];
217
- const exportedIds = findCrossFileCallTargets(db, reexTarget.file) as Set<number>;
218
- targetExported = targetSymbols.filter((s) => exportedIds.has(s.id));
219
- }
220
- for (const s of targetExported) {
221
- const fileLines = getFileLines(reexTarget.file);
222
- reexportedSymbols.push({
223
- ...buildSymbolResult(s, fileLines),
224
- originFile: reexTarget.file,
225
- });
226
- }
227
- }
229
+ // Gather symbols re-exported from target modules (barrel file support)
230
+ const reexportedSymbols = collectReexportedSymbols(
231
+ db,
232
+ fn.id,
233
+ reexportsToStmt,
234
+ exportedNodesStmt,
235
+ hasExportedCol,
236
+ getFileLines,
237
+ buildSymbolResult,
238
+ );
228
239
 
229
240
  let filteredResults = results;
230
241
  let filteredReexported = reexportedSymbols;
@@ -4,13 +4,12 @@ import {
4
4
  findImplementors,
5
5
  findImportDependents,
6
6
  findNodeById,
7
- openReadonlyOrFail,
8
7
  } from '../../db/index.js';
9
- import { loadConfig } from '../../infrastructure/config.js';
10
8
  import { isTestFile } from '../../infrastructure/test-filter.js';
11
9
  import { normalizeSymbol } from '../../shared/normalize.js';
12
10
  import { paginateResult } from '../../shared/paginate.js';
13
11
  import type { BetterSqlite3Database, NodeRow, RelatedNodeRow } from '../../types.js';
12
+ import { resolveAnalysisOpts, withReadonlyDb } from './query-helpers.js';
14
13
  import { findMatchingNodes } from './symbol-lookup.js';
15
14
 
16
15
  // --- Shared BFS: transitive callers ---
@@ -36,6 +35,62 @@ function hasImplementsEdges(db: BetterSqlite3Database): boolean {
36
35
  * during traversal), its concrete implementors are also added to the frontier
37
36
  * so that changes to an interface signature propagate to all implementors.
38
37
  */
38
+ type BfsLevel = Array<{
39
+ name: string;
40
+ kind: string;
41
+ file: string;
42
+ line: number;
43
+ viaImplements?: boolean;
44
+ }>;
45
+ type BfsLevels = Record<number, BfsLevel>;
46
+ type BfsOnVisit = (
47
+ caller: RelatedNodeRow & { viaImplements?: boolean },
48
+ parentId: number,
49
+ depth: number,
50
+ ) => void;
51
+
52
+ /** Record an implementor node at the given depth, adding to frontier and levels. */
53
+ function recordImplementor(
54
+ impl: RelatedNodeRow,
55
+ parentId: number,
56
+ depth: number,
57
+ visited: Set<number>,
58
+ frontier: number[],
59
+ levels: BfsLevels,
60
+ noTests: boolean,
61
+ onVisit?: BfsOnVisit,
62
+ ): void {
63
+ if (visited.has(impl.id) || (noTests && isTestFile(impl.file))) return;
64
+ visited.add(impl.id);
65
+ frontier.push(impl.id);
66
+ if (!levels[depth]) levels[depth] = [];
67
+ levels[depth].push({
68
+ name: impl.name,
69
+ kind: impl.kind,
70
+ file: impl.file,
71
+ line: impl.line,
72
+ viaImplements: true,
73
+ });
74
+ if (onVisit) onVisit({ ...impl, viaImplements: true }, parentId, depth);
75
+ }
76
+
77
+ /** Expand implementors for an interface/trait node into the BFS frontier. */
78
+ function expandImplementors(
79
+ db: BetterSqlite3Database,
80
+ nodeId: number,
81
+ depth: number,
82
+ visited: Set<number>,
83
+ frontier: number[],
84
+ levels: BfsLevels,
85
+ noTests: boolean,
86
+ onVisit?: BfsOnVisit,
87
+ ): void {
88
+ const impls = findImplementors(db, nodeId) as RelatedNodeRow[];
89
+ for (const impl of impls) {
90
+ recordImplementor(impl, nodeId, depth, visited, frontier, levels, noTests, onVisit);
91
+ }
92
+ }
93
+
39
94
  export function bfsTransitiveCallers(
40
95
  db: BetterSqlite3Database,
41
96
  startId: number,
@@ -48,50 +103,24 @@ export function bfsTransitiveCallers(
48
103
  noTests?: boolean;
49
104
  maxDepth?: number;
50
105
  includeImplementors?: boolean;
51
- onVisit?: (
52
- caller: RelatedNodeRow & { viaImplements?: boolean },
53
- parentId: number,
54
- depth: number,
55
- ) => void;
106
+ onVisit?: BfsOnVisit;
56
107
  } = {},
57
108
  ) {
58
- // Skip all implementor lookups when the graph has no implements edges
59
109
  const resolveImplementors = includeImplementors && hasImplementsEdges(db);
60
-
61
110
  const visited = new Set([startId]);
62
- const levels: Record<
63
- number,
64
- Array<{ name: string; kind: string; file: string; line: number; viaImplements?: boolean }>
65
- > = {};
111
+ const levels: BfsLevels = {};
66
112
  let frontier = [startId];
67
113
 
68
- // Seed: if start node is an interface/trait, include its implementors at depth 1.
69
- // Implementors go into a separate list so their callers appear at depth 2, not depth 1.
114
+ // Seed: if start node is an interface/trait, include its implementors at depth 1
70
115
  const implNextFrontier: number[] = [];
71
116
  if (resolveImplementors) {
72
117
  const startNode = findNodeById(db, startId) as NodeRow | undefined;
73
118
  if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
74
- const impls = findImplementors(db, startId) as RelatedNodeRow[];
75
- for (const impl of impls) {
76
- if (!visited.has(impl.id) && (!noTests || !isTestFile(impl.file))) {
77
- visited.add(impl.id);
78
- implNextFrontier.push(impl.id);
79
- if (!levels[1]) levels[1] = [];
80
- levels[1].push({
81
- name: impl.name,
82
- kind: impl.kind,
83
- file: impl.file,
84
- line: impl.line,
85
- viaImplements: true,
86
- });
87
- if (onVisit) onVisit({ ...impl, viaImplements: true }, startId, 1);
88
- }
89
- }
119
+ expandImplementors(db, startId, 1, visited, implNextFrontier, levels, noTests, onVisit);
90
120
  }
91
121
  }
92
122
 
93
123
  for (let d = 1; d <= maxDepth; d++) {
94
- // On the first wave, merge seeded implementors so their callers appear at d=2
95
124
  if (d === 1 && implNextFrontier.length > 0) {
96
125
  frontier = [...frontier, ...implNextFrontier];
97
126
  }
@@ -106,27 +135,8 @@ export function bfsTransitiveCallers(
106
135
  levels[d]!.push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
107
136
  if (onVisit) onVisit(c, fid, d);
108
137
  }
109
-
110
- // If a caller is an interface/trait, also pull in its implementors
111
- // Implementors are one extra hop away, so record at d+1
112
138
  if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
113
- const impls = findImplementors(db, c.id) as RelatedNodeRow[];
114
- for (const impl of impls) {
115
- if (!visited.has(impl.id) && (!noTests || !isTestFile(impl.file))) {
116
- visited.add(impl.id);
117
- nextFrontier.push(impl.id);
118
- const implDepth = d + 1;
119
- if (!levels[implDepth]) levels[implDepth] = [];
120
- levels[implDepth].push({
121
- name: impl.name,
122
- kind: impl.kind,
123
- file: impl.file,
124
- line: impl.line,
125
- viaImplements: true,
126
- });
127
- if (onVisit) onVisit({ ...impl, viaImplements: true }, c.id, implDepth);
128
- }
129
- }
139
+ expandImplementors(db, c.id, d + 1, visited, nextFrontier, levels, noTests, onVisit);
130
140
  }
131
141
  }
132
142
  }
@@ -142,8 +152,7 @@ export function impactAnalysisData(
142
152
  customDbPath: string,
143
153
  opts: { noTests?: boolean } = {},
144
154
  ) {
145
- const db = openReadonlyOrFail(customDbPath);
146
- try {
155
+ return withReadonlyDb(customDbPath, (db) => {
147
156
  const noTests = opts.noTests || false;
148
157
  const fileNodes = findFileNodes(db, `%${file}%`) as NodeRow[];
149
158
  if (fileNodes.length === 0) {
@@ -187,9 +196,7 @@ export function impactAnalysisData(
187
196
  levels: byLevel,
188
197
  totalDependents: visited.size - fileNodes.length,
189
198
  };
190
- } finally {
191
- db.close();
192
- }
199
+ });
193
200
  }
194
201
 
195
202
  export function fnImpactData(
@@ -206,11 +213,9 @@ export function fnImpactData(
206
213
  config?: any;
207
214
  } = {},
208
215
  ) {
209
- const db = openReadonlyOrFail(customDbPath);
210
- try {
211
- const config = opts.config || loadConfig();
216
+ return withReadonlyDb(customDbPath, (db) => {
217
+ const { noTests, config } = resolveAnalysisOpts(opts);
212
218
  const maxDepth = opts.depth || config.analysis?.fnImpactDepth || 5;
213
- const noTests = opts.noTests || false;
214
219
  const hc = new Map();
215
220
 
216
221
  const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
@@ -235,7 +240,5 @@ export function fnImpactData(
235
240
 
236
241
  const base = { name, results };
237
242
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
238
- } finally {
239
- db.close();
240
- }
243
+ });
241
244
  }
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js';
2
+ import { openReadonlyOrFail, openReadonlyWithNative, testFilterSQL } from '../../db/index.js';
3
3
  import { cachedStmt } from '../../db/repository/cached-stmt.js';
4
4
  import { loadConfig } from '../../infrastructure/config.js';
5
5
  import { debug } from '../../infrastructure/logger.js';
@@ -381,20 +381,115 @@ export function moduleMapData(customDbPath: string, limit = 20, opts: { noTests?
381
381
  }
382
382
 
383
383
  export function statsData(customDbPath: string, opts: { noTests?: boolean; config?: any } = {}) {
384
- const db = openReadonlyOrFail(customDbPath);
384
+ const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
385
385
  try {
386
386
  const noTests = opts.noTests || false;
387
387
  const config = opts.config || loadConfig();
388
- const testFilter = testFilterSQL('n.file', noTests);
389
388
 
389
+ // These always need JS (non-SQL logic)
390
+ const files = countFilesByLanguage(db, noTests);
391
+ const fileCycles = findCycles(db, { fileLevel: true, noTests });
392
+ const fnCycles = findCycles(db, { fileLevel: false, noTests });
393
+
394
+ // ── Native fast path: batch all SQL aggregations in one napi call ──
395
+ if (nativeDb?.getGraphStats) {
396
+ const s = nativeDb.getGraphStats(noTests);
397
+ const nodesByKind: Record<string, number> = {};
398
+ for (const k of s.nodesByKind) nodesByKind[k.kind] = k.count;
399
+ const edgesByKind: Record<string, number> = {};
400
+ for (const k of s.edgesByKind) edgesByKind[k.kind] = k.count;
401
+ const roles: Record<string, number> & { dead?: number } = {};
402
+ let deadTotal = 0;
403
+ for (const r of s.roleCounts) {
404
+ roles[r.role] = r.count;
405
+ if (r.role.startsWith(DEAD_ROLE_PREFIX)) deadTotal += r.count;
406
+ }
407
+ if (deadTotal > 0) roles.dead = deadTotal;
408
+
409
+ const callerCoverage =
410
+ s.quality.callableTotal > 0 ? s.quality.callableWithCallers / s.quality.callableTotal : 0;
411
+ const callConfidence =
412
+ s.quality.callEdges > 0 ? s.quality.highConfCallEdges / s.quality.callEdges : 0;
413
+
414
+ // False-positive analysis still uses JS (needs FALSE_POSITIVE_NAMES set)
415
+ const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
416
+ const fpRows = db
417
+ .prepare(`
418
+ SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
419
+ FROM nodes n
420
+ LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
421
+ WHERE n.kind IN ('function', 'method')
422
+ GROUP BY n.id
423
+ HAVING caller_count > ?
424
+ ORDER BY caller_count DESC
425
+ `)
426
+ .all(fpThreshold) as Array<{
427
+ name: string;
428
+ file: string;
429
+ line: number;
430
+ caller_count: number;
431
+ }>;
432
+ const falsePositiveWarnings = fpRows
433
+ .filter((r) =>
434
+ FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop()! : r.name),
435
+ )
436
+ .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
437
+ let fpEdgeCount = 0;
438
+ for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
439
+ const falsePositiveRatio = s.quality.callEdges > 0 ? fpEdgeCount / s.quality.callEdges : 0;
440
+ const score = Math.round(
441
+ callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
442
+ );
443
+
444
+ return {
445
+ nodes: { total: s.totalNodes, byKind: nodesByKind },
446
+ edges: { total: s.totalEdges, byKind: edgesByKind },
447
+ files,
448
+ cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
449
+ hotspots: s.hotspots.map((h) => ({ file: h.file, fanIn: h.fanIn, fanOut: h.fanOut })),
450
+ embeddings: s.embeddings
451
+ ? {
452
+ count: s.embeddings.count,
453
+ model: s.embeddings.model,
454
+ dim: s.embeddings.dim,
455
+ builtAt: s.embeddings.builtAt,
456
+ }
457
+ : null,
458
+ quality: {
459
+ score,
460
+ callerCoverage: {
461
+ ratio: callerCoverage,
462
+ covered: s.quality.callableWithCallers,
463
+ total: s.quality.callableTotal,
464
+ },
465
+ callConfidence: {
466
+ ratio: callConfidence,
467
+ highConf: s.quality.highConfCallEdges,
468
+ total: s.quality.callEdges,
469
+ },
470
+ falsePositiveWarnings,
471
+ },
472
+ roles,
473
+ complexity: s.complexity
474
+ ? {
475
+ analyzed: s.complexity.analyzed,
476
+ avgCognitive: s.complexity.avgCognitive,
477
+ avgCyclomatic: s.complexity.avgCyclomatic,
478
+ maxCognitive: s.complexity.maxCognitive,
479
+ maxCyclomatic: s.complexity.maxCyclomatic,
480
+ avgMI: s.complexity.avgMi,
481
+ minMI: s.complexity.minMi,
482
+ }
483
+ : null,
484
+ };
485
+ }
486
+
487
+ // ── JS fallback ───────────────────────────────────────────────────
488
+ const testFilter = testFilterSQL('n.file', noTests);
390
489
  const testFileIds = noTests ? buildTestFileIds(db) : null;
391
490
 
392
491
  const { total: totalNodes, byKind: nodesByKind } = countNodesByKind(db, testFileIds);
393
492
  const { total: totalEdges, byKind: edgesByKind } = countEdgesByKind(db, testFileIds);
394
- const files = countFilesByLanguage(db, noTests);
395
-
396
- const fileCycles = findCycles(db, { fileLevel: true, noTests });
397
- const fnCycles = findCycles(db, { fileLevel: false, noTests });
398
493
 
399
494
  const hotspots = findHotspots(db, noTests, 5);
400
495
  const embeddings = getEmbeddingsInfo(db);
@@ -415,6 +510,6 @@ export function statsData(customDbPath: string, opts: { noTests?: boolean; confi
415
510
  complexity,
416
511
  };
417
512
  } finally {
418
- db.close();
513
+ close();
419
514
  }
420
515
  }