@optave/codegraph 3.5.0 → 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 (310) hide show
  1. package/README.md +35 -14
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +119 -127
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +14 -1
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  10. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  11. package/dist/db/connection.d.ts +12 -2
  12. package/dist/db/connection.d.ts.map +1 -1
  13. package/dist/db/connection.js +81 -53
  14. package/dist/db/connection.js.map +1 -1
  15. package/dist/db/index.d.ts +1 -1
  16. package/dist/db/index.d.ts.map +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/db/migrations.d.ts.map +1 -1
  20. package/dist/db/migrations.js +38 -32
  21. package/dist/db/migrations.js.map +1 -1
  22. package/dist/domain/analysis/context.d.ts.map +1 -1
  23. package/dist/domain/analysis/context.js +51 -66
  24. package/dist/domain/analysis/context.js.map +1 -1
  25. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  26. package/dist/domain/analysis/dependencies.js +62 -70
  27. package/dist/domain/analysis/dependencies.js.map +1 -1
  28. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  29. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  30. package/dist/domain/analysis/exports.d.ts.map +1 -1
  31. package/dist/domain/analysis/exports.js +29 -33
  32. package/dist/domain/analysis/exports.js.map +1 -1
  33. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  34. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  35. package/dist/domain/analysis/fn-impact.js +35 -65
  36. package/dist/domain/analysis/fn-impact.js.map +1 -1
  37. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  38. package/dist/domain/analysis/module-map.js +91 -6
  39. package/dist/domain/analysis/module-map.js.map +1 -1
  40. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  41. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  42. package/dist/domain/analysis/query-helpers.js +27 -0
  43. package/dist/domain/analysis/query-helpers.js.map +1 -0
  44. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/helpers.js +15 -9
  46. package/dist/domain/graph/builder/helpers.js.map +1 -1
  47. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/incremental.js +3 -2
  49. package/dist/domain/graph/builder/incremental.js.map +1 -1
  50. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/pipeline.js +69 -3
  52. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  54. package/dist/domain/graph/builder/stages/build-edges.js +7 -51
  55. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/build-structure.js +7 -5
  58. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/collect-files.js +2 -2
  60. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  63. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/finalize.js +124 -105
  66. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  68. package/dist/domain/graph/builder/stages/insert-nodes.js +28 -15
  69. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  71. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  72. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  73. package/dist/domain/graph/resolve.d.ts +0 -4
  74. package/dist/domain/graph/resolve.d.ts.map +1 -1
  75. package/dist/domain/graph/resolve.js +32 -48
  76. package/dist/domain/graph/resolve.js.map +1 -1
  77. package/dist/domain/graph/watcher.d.ts.map +1 -1
  78. package/dist/domain/graph/watcher.js +12 -12
  79. package/dist/domain/graph/watcher.js.map +1 -1
  80. package/dist/domain/parser.d.ts +1 -1
  81. package/dist/domain/parser.d.ts.map +1 -1
  82. package/dist/domain/parser.js +164 -101
  83. package/dist/domain/parser.js.map +1 -1
  84. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  85. package/dist/domain/search/search/cli-formatter.js +88 -83
  86. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  87. package/dist/extractors/bash.d.ts +6 -0
  88. package/dist/extractors/bash.d.ts.map +1 -0
  89. package/dist/extractors/bash.js +91 -0
  90. package/dist/extractors/bash.js.map +1 -0
  91. package/dist/extractors/c.d.ts +6 -0
  92. package/dist/extractors/c.d.ts.map +1 -0
  93. package/dist/extractors/c.js +204 -0
  94. package/dist/extractors/c.js.map +1 -0
  95. package/dist/extractors/cpp.d.ts +6 -0
  96. package/dist/extractors/cpp.d.ts.map +1 -0
  97. package/dist/extractors/cpp.js +283 -0
  98. package/dist/extractors/cpp.js.map +1 -0
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +42 -54
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/go.d.ts.map +1 -1
  103. package/dist/extractors/go.js +126 -130
  104. package/dist/extractors/go.js.map +1 -1
  105. package/dist/extractors/hcl.js +6 -6
  106. package/dist/extractors/hcl.js.map +1 -1
  107. package/dist/extractors/helpers.d.ts +32 -1
  108. package/dist/extractors/helpers.d.ts.map +1 -1
  109. package/dist/extractors/helpers.js +74 -0
  110. package/dist/extractors/helpers.js.map +1 -1
  111. package/dist/extractors/index.d.ts +6 -0
  112. package/dist/extractors/index.d.ts.map +1 -1
  113. package/dist/extractors/index.js +6 -0
  114. package/dist/extractors/index.js.map +1 -1
  115. package/dist/extractors/java.d.ts.map +1 -1
  116. package/dist/extractors/java.js +32 -47
  117. package/dist/extractors/java.js.map +1 -1
  118. package/dist/extractors/javascript.d.ts.map +1 -1
  119. package/dist/extractors/javascript.js +306 -292
  120. package/dist/extractors/javascript.js.map +1 -1
  121. package/dist/extractors/kotlin.d.ts +6 -0
  122. package/dist/extractors/kotlin.d.ts.map +1 -0
  123. package/dist/extractors/kotlin.js +275 -0
  124. package/dist/extractors/kotlin.js.map +1 -0
  125. package/dist/extractors/php.d.ts.map +1 -1
  126. package/dist/extractors/php.js +39 -44
  127. package/dist/extractors/php.js.map +1 -1
  128. package/dist/extractors/python.d.ts.map +1 -1
  129. package/dist/extractors/python.js +75 -93
  130. package/dist/extractors/python.js.map +1 -1
  131. package/dist/extractors/ruby.js +6 -13
  132. package/dist/extractors/ruby.js.map +1 -1
  133. package/dist/extractors/rust.d.ts.map +1 -1
  134. package/dist/extractors/rust.js +58 -83
  135. package/dist/extractors/rust.js.map +1 -1
  136. package/dist/extractors/scala.d.ts +6 -0
  137. package/dist/extractors/scala.d.ts.map +1 -0
  138. package/dist/extractors/scala.js +269 -0
  139. package/dist/extractors/scala.js.map +1 -0
  140. package/dist/extractors/swift.d.ts +6 -0
  141. package/dist/extractors/swift.d.ts.map +1 -0
  142. package/dist/extractors/swift.js +275 -0
  143. package/dist/extractors/swift.js.map +1 -0
  144. package/dist/features/ast.d.ts +2 -0
  145. package/dist/features/ast.d.ts.map +1 -1
  146. package/dist/features/ast.js +9 -24
  147. package/dist/features/ast.js.map +1 -1
  148. package/dist/features/audit.d.ts.map +1 -1
  149. package/dist/features/audit.js +17 -21
  150. package/dist/features/audit.js.map +1 -1
  151. package/dist/features/branch-compare.d.ts.map +1 -1
  152. package/dist/features/branch-compare.js +47 -3
  153. package/dist/features/branch-compare.js.map +1 -1
  154. package/dist/features/cfg.d.ts +7 -1
  155. package/dist/features/cfg.d.ts.map +1 -1
  156. package/dist/features/cfg.js +118 -62
  157. package/dist/features/cfg.js.map +1 -1
  158. package/dist/features/check.d.ts.map +1 -1
  159. package/dist/features/check.js +79 -62
  160. package/dist/features/check.js.map +1 -1
  161. package/dist/features/complexity-query.d.ts.map +1 -1
  162. package/dist/features/complexity-query.js +142 -137
  163. package/dist/features/complexity-query.js.map +1 -1
  164. package/dist/features/complexity.d.ts +7 -1
  165. package/dist/features/complexity.d.ts.map +1 -1
  166. package/dist/features/complexity.js +62 -1
  167. package/dist/features/complexity.js.map +1 -1
  168. package/dist/features/dataflow.d.ts +7 -1
  169. package/dist/features/dataflow.d.ts.map +1 -1
  170. package/dist/features/dataflow.js +356 -188
  171. package/dist/features/dataflow.js.map +1 -1
  172. package/dist/features/graph-enrichment.d.ts.map +1 -1
  173. package/dist/features/graph-enrichment.js +117 -104
  174. package/dist/features/graph-enrichment.js.map +1 -1
  175. package/dist/features/sequence.d.ts.map +1 -1
  176. package/dist/features/sequence.js +25 -4
  177. package/dist/features/sequence.js.map +1 -1
  178. package/dist/features/structure-query.d.ts.map +1 -1
  179. package/dist/features/structure-query.js +29 -4
  180. package/dist/features/structure-query.js.map +1 -1
  181. package/dist/features/structure.d.ts.map +1 -1
  182. package/dist/features/structure.js +35 -15
  183. package/dist/features/structure.js.map +1 -1
  184. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  185. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  186. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  187. package/dist/graph/algorithms/leiden/index.js +43 -28
  188. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  189. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  190. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  191. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  192. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  193. package/dist/graph/algorithms/leiden/partition.js +89 -106
  194. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  195. package/dist/graph/model.d.ts +2 -0
  196. package/dist/graph/model.d.ts.map +1 -1
  197. package/dist/graph/model.js +20 -8
  198. package/dist/graph/model.js.map +1 -1
  199. package/dist/infrastructure/config.d.ts +0 -8
  200. package/dist/infrastructure/config.d.ts.map +1 -1
  201. package/dist/infrastructure/config.js +73 -62
  202. package/dist/infrastructure/config.js.map +1 -1
  203. package/dist/infrastructure/registry.d.ts +0 -8
  204. package/dist/infrastructure/registry.d.ts.map +1 -1
  205. package/dist/infrastructure/registry.js +12 -14
  206. package/dist/infrastructure/registry.js.map +1 -1
  207. package/dist/mcp/server.d.ts.map +1 -1
  208. package/dist/mcp/server.js +45 -36
  209. package/dist/mcp/server.js.map +1 -1
  210. package/dist/presentation/audit.d.ts.map +1 -1
  211. package/dist/presentation/audit.js +61 -57
  212. package/dist/presentation/audit.js.map +1 -1
  213. package/dist/presentation/branch-compare.d.ts.map +1 -1
  214. package/dist/presentation/branch-compare.js +56 -38
  215. package/dist/presentation/branch-compare.js.map +1 -1
  216. package/dist/presentation/check.d.ts.map +1 -1
  217. package/dist/presentation/check.js +30 -32
  218. package/dist/presentation/check.js.map +1 -1
  219. package/dist/presentation/colors.d.ts.map +1 -1
  220. package/dist/presentation/colors.js +2 -0
  221. package/dist/presentation/colors.js.map +1 -1
  222. package/dist/presentation/complexity.d.ts.map +1 -1
  223. package/dist/presentation/complexity.js +25 -19
  224. package/dist/presentation/complexity.js.map +1 -1
  225. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  226. package/dist/presentation/queries-cli/exports.js +15 -15
  227. package/dist/presentation/queries-cli/exports.js.map +1 -1
  228. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  229. package/dist/presentation/queries-cli/impact.js +29 -19
  230. package/dist/presentation/queries-cli/impact.js.map +1 -1
  231. package/dist/types.d.ts +182 -7
  232. package/dist/types.d.ts.map +1 -1
  233. package/grammars/tree-sitter-bash.wasm +0 -0
  234. package/grammars/tree-sitter-c.wasm +0 -0
  235. package/grammars/tree-sitter-cpp.wasm +0 -0
  236. package/grammars/tree-sitter-kotlin.wasm +0 -0
  237. package/grammars/tree-sitter-scala.wasm +0 -0
  238. package/grammars/tree-sitter-swift.wasm +0 -0
  239. package/package.json +13 -7
  240. package/src/ast-analysis/engine.ts +147 -138
  241. package/src/ast-analysis/visitors/ast-store-visitor.ts +15 -2
  242. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  243. package/src/db/connection.ts +90 -59
  244. package/src/db/index.ts +1 -0
  245. package/src/db/migrations.ts +36 -32
  246. package/src/domain/analysis/context.ts +73 -75
  247. package/src/domain/analysis/dependencies.ts +78 -68
  248. package/src/domain/analysis/exports.ts +45 -34
  249. package/src/domain/analysis/fn-impact.ts +67 -64
  250. package/src/domain/analysis/module-map.ts +103 -8
  251. package/src/domain/analysis/query-helpers.ts +35 -0
  252. package/src/domain/graph/builder/helpers.ts +12 -6
  253. package/src/domain/graph/builder/incremental.ts +3 -2
  254. package/src/domain/graph/builder/pipeline.ts +71 -3
  255. package/src/domain/graph/builder/stages/build-edges.ts +10 -75
  256. package/src/domain/graph/builder/stages/build-structure.ts +9 -7
  257. package/src/domain/graph/builder/stages/collect-files.ts +2 -2
  258. package/src/domain/graph/builder/stages/detect-changes.ts +7 -2
  259. package/src/domain/graph/builder/stages/finalize.ts +159 -125
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +32 -21
  261. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  262. package/src/domain/graph/resolve.ts +34 -46
  263. package/src/domain/graph/watcher.ts +12 -14
  264. package/src/domain/parser.ts +168 -97
  265. package/src/domain/search/search/cli-formatter.ts +121 -94
  266. package/src/extractors/bash.ts +97 -0
  267. package/src/extractors/c.ts +212 -0
  268. package/src/extractors/cpp.ts +298 -0
  269. package/src/extractors/csharp.ts +53 -56
  270. package/src/extractors/go.ts +152 -134
  271. package/src/extractors/hcl.ts +6 -6
  272. package/src/extractors/helpers.ts +93 -1
  273. package/src/extractors/index.ts +6 -0
  274. package/src/extractors/java.ts +43 -48
  275. package/src/extractors/javascript.ts +328 -281
  276. package/src/extractors/kotlin.ts +293 -0
  277. package/src/extractors/php.ts +46 -40
  278. package/src/extractors/python.ts +81 -104
  279. package/src/extractors/ruby.ts +6 -13
  280. package/src/extractors/rust.ts +65 -85
  281. package/src/extractors/scala.ts +285 -0
  282. package/src/extractors/swift.ts +293 -0
  283. package/src/features/ast.ts +10 -25
  284. package/src/features/audit.ts +24 -20
  285. package/src/features/branch-compare.ts +51 -4
  286. package/src/features/cfg.ts +158 -65
  287. package/src/features/check.ts +90 -74
  288. package/src/features/complexity-query.ts +181 -163
  289. package/src/features/complexity.ts +64 -1
  290. package/src/features/dataflow.ts +462 -217
  291. package/src/features/graph-enrichment.ts +161 -117
  292. package/src/features/sequence.ts +27 -4
  293. package/src/features/structure-query.ts +43 -4
  294. package/src/features/structure.ts +50 -22
  295. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  296. package/src/graph/algorithms/leiden/index.ts +67 -28
  297. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  298. package/src/graph/algorithms/leiden/partition.ts +131 -98
  299. package/src/graph/model.ts +19 -7
  300. package/src/infrastructure/config.ts +60 -58
  301. package/src/infrastructure/registry.ts +17 -14
  302. package/src/mcp/server.ts +46 -37
  303. package/src/presentation/audit.ts +72 -67
  304. package/src/presentation/branch-compare.ts +54 -50
  305. package/src/presentation/check.ts +34 -34
  306. package/src/presentation/colors.ts +2 -0
  307. package/src/presentation/complexity.ts +39 -33
  308. package/src/presentation/queries-cli/exports.ts +17 -17
  309. package/src/presentation/queries-cli/impact.ts +30 -22
  310. package/src/types.ts +189 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optave/codegraph",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -131,12 +131,12 @@
131
131
  },
132
132
  "optionalDependencies": {
133
133
  "@modelcontextprotocol/sdk": "^1.0.0",
134
- "@optave/codegraph-darwin-arm64": "3.5.0",
135
- "@optave/codegraph-darwin-x64": "3.5.0",
136
- "@optave/codegraph-linux-arm64-gnu": "3.5.0",
137
- "@optave/codegraph-linux-x64-gnu": "3.5.0",
138
- "@optave/codegraph-linux-x64-musl": "3.5.0",
139
- "@optave/codegraph-win32-x64-msvc": "3.5.0"
134
+ "@optave/codegraph-darwin-arm64": "3.6.0",
135
+ "@optave/codegraph-darwin-x64": "3.6.0",
136
+ "@optave/codegraph-linux-arm64-gnu": "3.6.0",
137
+ "@optave/codegraph-linux-x64-gnu": "3.6.0",
138
+ "@optave/codegraph-linux-x64-musl": "3.6.0",
139
+ "@optave/codegraph-win32-x64-msvc": "3.6.0"
140
140
  },
141
141
  "devDependencies": {
142
142
  "@biomejs/biome": "^2.4.4",
@@ -148,15 +148,21 @@
148
148
  "@vitest/coverage-v8": "^4.0.18",
149
149
  "commit-and-tag-version": "^12.5",
150
150
  "husky": "^9.1",
151
+ "tree-sitter-bash": "^0.25.1",
152
+ "tree-sitter-c": "^0.24.1",
151
153
  "tree-sitter-c-sharp": "^0.23.1",
152
154
  "tree-sitter-cli": "^0.26.5",
155
+ "tree-sitter-cpp": "^0.23.4",
153
156
  "tree-sitter-go": "^0.25.0",
154
157
  "tree-sitter-java": "^0.23.5",
155
158
  "tree-sitter-javascript": "^0.25.0",
159
+ "tree-sitter-kotlin": "^0.3.8",
156
160
  "tree-sitter-php": "^0.24.2",
157
161
  "tree-sitter-python": "^0.25.0",
158
162
  "tree-sitter-ruby": "^0.23.1",
159
163
  "tree-sitter-rust": "^0.24.0",
164
+ "tree-sitter-scala": "^0.24.0",
165
+ "tree-sitter-swift": "^0.7.1",
160
166
  "tree-sitter-typescript": "^0.23.2",
161
167
  "typescript": "^6.0.2",
162
168
  "vitest": "^4.0.18"
@@ -102,11 +102,12 @@ async function ensureWasmTreesIfNeeded(
102
102
  opts: AnalysisOpts,
103
103
  rootDir: string,
104
104
  ): Promise<void> {
105
+ const doAst = opts.ast !== false;
105
106
  const doComplexity = opts.complexity !== false;
106
107
  const doCfg = opts.cfg !== false;
107
108
  const doDataflow = opts.dataflow !== false;
108
109
 
109
- if (!doComplexity && !doCfg && !doDataflow) return;
110
+ if (!doAst && !doComplexity && !doCfg && !doDataflow) return;
110
111
 
111
112
  let needsWasmTrees = false;
112
113
  for (const [relPath, symbols] of fileSymbols) {
@@ -131,6 +132,8 @@ async function ensureWasmTreesIfNeeded(
131
132
  d.endLine > d.line &&
132
133
  !d.name.includes('.');
133
134
 
135
+ // AST: need tree when native didn't provide non-call astNodes
136
+ const needsAst = doAst && !Array.isArray(symbols.astNodes) && WALK_EXTENSIONS.has(ext);
134
137
  const needsComplexity =
135
138
  doComplexity &&
136
139
  COMPLEXITY_EXTENSIONS.has(ext) &&
@@ -141,7 +144,7 @@ async function ensureWasmTreesIfNeeded(
141
144
  defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
142
145
  const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
143
146
 
144
- if (needsComplexity || needsCfg || needsDataflow) {
147
+ if (needsAst || needsComplexity || needsCfg || needsDataflow) {
145
148
  needsWasmTrees = true;
146
149
  break;
147
150
  }
@@ -159,6 +162,80 @@ async function ensureWasmTreesIfNeeded(
159
162
 
160
163
  // ─── Per-file visitor setup ─────────────────────────────────────────────
161
164
 
165
+ /** Check if a definition has a real function body (not a type signature). */
166
+ function hasFuncBody(d: {
167
+ name: string;
168
+ kind: string;
169
+ line: number;
170
+ endLine?: number | null;
171
+ }): boolean {
172
+ return (
173
+ (d.kind === 'function' || d.kind === 'method') &&
174
+ d.line > 0 &&
175
+ d.endLine != null &&
176
+ d.endLine > d.line &&
177
+ !d.name.includes('.')
178
+ );
179
+ }
180
+
181
+ /** Set up AST-store visitor if applicable. */
182
+ function setupAstVisitor(
183
+ db: BetterSqlite3Database,
184
+ relPath: string,
185
+ symbols: ExtractorOutput,
186
+ langId: string,
187
+ ext: string,
188
+ ): Visitor | null {
189
+ const astTypeMap = AST_TYPE_MAPS.get(langId);
190
+ if (!astTypeMap || !WALK_EXTENSIONS.has(ext) || Array.isArray(symbols.astNodes)) return null;
191
+ const nodeIdMap = new Map<string, number>();
192
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
193
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
194
+ }
195
+ return createAstStoreVisitor(astTypeMap, symbols.definitions || [], relPath, nodeIdMap);
196
+ }
197
+
198
+ /** Set up complexity visitor if any definitions need WASM complexity analysis. */
199
+ function setupComplexityVisitorForFile(
200
+ defs: Definition[],
201
+ langId: string,
202
+ walkerOpts: WalkOptions,
203
+ ): Visitor | null {
204
+ const cRules = COMPLEXITY_RULES.get(langId);
205
+ if (!cRules) return null;
206
+
207
+ const hRules = HALSTEAD_RULES.get(langId);
208
+ const needsWasmComplexity = defs.some((d) => hasFuncBody(d) && !d.complexity);
209
+ if (!needsWasmComplexity) return null;
210
+
211
+ const visitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
212
+
213
+ for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes?.add(t);
214
+
215
+ const dfRules = DATAFLOW_RULES.get(langId);
216
+ walkerOpts.getFunctionName = (node: TreeSitterNode): string | null => {
217
+ const nameNode = node.childForFieldName('name');
218
+ if (nameNode) return nameNode.text;
219
+ if (dfRules) return getFuncName(node, dfRules as any);
220
+ return null;
221
+ };
222
+
223
+ return visitor;
224
+ }
225
+
226
+ /** Set up CFG visitor if any definitions need WASM CFG analysis. */
227
+ function setupCfgVisitorForFile(defs: Definition[], langId: string, ext: string): Visitor | null {
228
+ const cfgRulesForLang = CFG_RULES.get(langId);
229
+ if (!cfgRulesForLang || !CFG_EXTENSIONS.has(ext)) return null;
230
+
231
+ const needsWasmCfg = defs.some(
232
+ (d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks),
233
+ );
234
+ if (!needsWasmCfg) return null;
235
+
236
+ return createCfgVisitor(cfgRulesForLang);
237
+ }
238
+
162
239
  function setupVisitors(
163
240
  db: BetterSqlite3Database,
164
241
  relPath: string,
@@ -168,10 +245,6 @@ function setupVisitors(
168
245
  ): SetupResult {
169
246
  const ext = path.extname(relPath).toLowerCase();
170
247
  const defs = symbols.definitions || [];
171
- const doAst = opts.ast !== false;
172
- const doComplexity = opts.complexity !== false;
173
- const doCfg = opts.cfg !== false;
174
- const doDataflow = opts.dataflow !== false;
175
248
 
176
249
  const visitors: Visitor[] = [];
177
250
  const walkerOpts: WalkOptions = {
@@ -180,75 +253,19 @@ function setupVisitors(
180
253
  getFunctionName: (_node: TreeSitterNode) => null,
181
254
  };
182
255
 
183
- // AST-store visitor
184
- let astVisitor: Visitor | null = null;
185
- const astTypeMap = AST_TYPE_MAPS.get(langId);
186
- if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !Array.isArray(symbols.astNodes)) {
187
- const nodeIdMap = new Map<string, number>();
188
- for (const row of bulkNodeIdsByFile(db, relPath)) {
189
- nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
190
- }
191
- astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
192
- visitors.push(astVisitor);
193
- }
256
+ const astVisitor = opts.ast !== false ? setupAstVisitor(db, relPath, symbols, langId, ext) : null;
257
+ if (astVisitor) visitors.push(astVisitor);
194
258
 
195
- // Complexity visitor (file-level mode)
196
- let complexityVisitor: Visitor | null = null;
197
- const cRules = COMPLEXITY_RULES.get(langId);
198
- const hRules = HALSTEAD_RULES.get(langId);
199
- if (doComplexity && cRules) {
200
- // Only trigger WASM complexity for definitions with real function bodies.
201
- // Interface/type property signatures (dotted names, single-line span)
202
- // correctly lack native complexity data and should not trigger a fallback.
203
- const needsWasmComplexity = defs.some(
204
- (d) =>
205
- (d.kind === 'function' || d.kind === 'method') &&
206
- d.line > 0 &&
207
- d.endLine != null &&
208
- d.endLine > d.line &&
209
- !d.name.includes('.') &&
210
- !d.complexity,
211
- );
212
- if (needsWasmComplexity) {
213
- complexityVisitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
214
- visitors.push(complexityVisitor);
215
-
216
- for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes?.add(t);
217
-
218
- const dfRules = DATAFLOW_RULES.get(langId);
219
- walkerOpts.getFunctionName = (node: TreeSitterNode): string | null => {
220
- const nameNode = node.childForFieldName('name');
221
- if (nameNode) return nameNode.text;
222
- if (dfRules) return getFuncName(node, dfRules as any);
223
- return null;
224
- };
225
- }
226
- }
259
+ const complexityVisitor =
260
+ opts.complexity !== false ? setupComplexityVisitorForFile(defs, langId, walkerOpts) : null;
261
+ if (complexityVisitor) visitors.push(complexityVisitor);
227
262
 
228
- // CFG visitor
229
- let cfgVisitor: Visitor | null = null;
230
- const cfgRulesForLang = CFG_RULES.get(langId);
231
- if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
232
- const needsWasmCfg = defs.some(
233
- (d) =>
234
- (d.kind === 'function' || d.kind === 'method') &&
235
- d.line > 0 &&
236
- d.endLine != null &&
237
- d.endLine > d.line &&
238
- !d.name.includes('.') &&
239
- d.cfg !== null &&
240
- !Array.isArray(d.cfg?.blocks),
241
- );
242
- if (needsWasmCfg) {
243
- cfgVisitor = createCfgVisitor(cfgRulesForLang);
244
- visitors.push(cfgVisitor);
245
- }
246
- }
263
+ const cfgVisitor = opts.cfg !== false ? setupCfgVisitorForFile(defs, langId, ext) : null;
264
+ if (cfgVisitor) visitors.push(cfgVisitor);
247
265
 
248
- // Dataflow visitor
249
266
  let dataflowVisitor: Visitor | null = null;
250
267
  const dfRules = DATAFLOW_RULES.get(langId);
251
- if (doDataflow && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
268
+ if (opts.dataflow !== false && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
252
269
  dataflowVisitor = createDataflowVisitor(dfRules);
253
270
  visitors.push(dataflowVisitor);
254
271
  }
@@ -258,88 +275,80 @@ function setupVisitors(
258
275
 
259
276
  // ─── Result storage helpers ─────────────────────────────────────────────
260
277
 
261
- function storeComplexityResults(results: WalkResults, defs: Definition[], langId: string): void {
262
- const complexityResults = (results.complexity || []) as ComplexityFuncResult[];
263
- const resultByLine = new Map<number, ComplexityFuncResult[]>();
264
- for (const r of complexityResults) {
265
- if (r.funcNode) {
266
- const line = r.funcNode.startPosition.row + 1;
267
- if (!resultByLine.has(line)) resultByLine.set(line, []);
268
- resultByLine.get(line)?.push(r);
269
- }
278
+ /** Index per-function results by start line for O(1) lookup. */
279
+ function indexByLine<T extends { funcNode: TreeSitterNode }>(results: T[]): Map<number, T[]> {
280
+ const byLine = new Map<number, T[]>();
281
+ for (const r of results) {
282
+ if (!r.funcNode) continue;
283
+ const line = r.funcNode.startPosition.row + 1;
284
+ if (!byLine.has(line)) byLine.set(line, []);
285
+ byLine.get(line)?.push(r);
270
286
  }
287
+ return byLine;
288
+ }
289
+
290
+ /** Find the best matching result for a definition by line + name. */
291
+ function matchResultToDef<T extends { funcNode: TreeSitterNode }>(
292
+ candidates: T[] | undefined,
293
+ defName: string,
294
+ ): T | undefined {
295
+ if (!candidates) return undefined;
296
+ if (candidates.length === 1) return candidates[0];
297
+ return (
298
+ candidates.find((r) => {
299
+ const n = r.funcNode.childForFieldName('name');
300
+ return n && n.text === defName;
301
+ }) ?? candidates[0]
302
+ );
303
+ }
304
+
305
+ function storeComplexityResults(results: WalkResults, defs: Definition[], langId: string): void {
306
+ const byLine = indexByLine((results.complexity || []) as ComplexityFuncResult[]);
271
307
  for (const def of defs) {
272
308
  if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
273
- const candidates = resultByLine.get(def.line);
274
- const funcResult = !candidates
275
- ? undefined
276
- : candidates.length === 1
277
- ? candidates[0]
278
- : (candidates.find((r) => {
279
- const n = r.funcNode.childForFieldName('name');
280
- return n && n.text === def.name;
281
- }) ?? candidates[0]);
282
- if (funcResult) {
283
- const { metrics } = funcResult;
284
- const loc = computeLOCMetrics(funcResult.funcNode, langId);
285
- const volume = metrics.halstead ? metrics.halstead.volume : 0;
286
- const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
287
- const mi = computeMaintainabilityIndex(volume, metrics.cyclomatic, loc.sloc, commentRatio);
288
-
289
- def.complexity = {
290
- cognitive: metrics.cognitive,
291
- cyclomatic: metrics.cyclomatic,
292
- maxNesting: metrics.maxNesting,
293
- halstead: metrics.halstead,
294
- loc,
295
- maintainabilityIndex: mi,
296
- };
297
- }
309
+ const funcResult = matchResultToDef(byLine.get(def.line), def.name);
310
+ if (!funcResult) continue;
311
+ const { metrics } = funcResult;
312
+ const loc = computeLOCMetrics(funcResult.funcNode, langId);
313
+ const volume = metrics.halstead ? metrics.halstead.volume : 0;
314
+ const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
315
+ const mi = computeMaintainabilityIndex(volume, metrics.cyclomatic, loc.sloc, commentRatio);
316
+ def.complexity = {
317
+ cognitive: metrics.cognitive,
318
+ cyclomatic: metrics.cyclomatic,
319
+ maxNesting: metrics.maxNesting,
320
+ halstead: metrics.halstead,
321
+ loc,
322
+ maintainabilityIndex: mi,
323
+ };
298
324
  }
299
325
  }
300
326
  }
301
327
 
302
328
  function storeCfgResults(results: WalkResults, defs: Definition[]): void {
303
- const cfgResults = (results.cfg || []) as CfgFuncResult[];
304
- const cfgByLine = new Map<number, CfgFuncResult[]>();
305
- for (const r of cfgResults) {
306
- if (r.funcNode) {
307
- const line = r.funcNode.startPosition.row + 1;
308
- if (!cfgByLine.has(line)) cfgByLine.set(line, []);
309
- cfgByLine.get(line)?.push(r);
310
- }
311
- }
329
+ const byLine = indexByLine((results.cfg || []) as CfgFuncResult[]);
312
330
  for (const def of defs) {
313
331
  if (
314
332
  (def.kind === 'function' || def.kind === 'method') &&
315
333
  def.line &&
316
334
  !def.cfg?.blocks?.length
317
335
  ) {
318
- const candidates = cfgByLine.get(def.line);
319
- const cfgResult = !candidates
320
- ? undefined
321
- : candidates.length === 1
322
- ? candidates[0]
323
- : (candidates.find((r) => {
324
- const n = r.funcNode.childForFieldName('name');
325
- return n && n.text === def.name;
326
- }) ?? candidates[0]);
327
- if (cfgResult) {
328
- def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
329
-
330
- // Override complexity's cyclomatic with CFG-derived value (single source of truth)
331
- if (def.complexity && cfgResult.cyclomatic != null) {
332
- def.complexity.cyclomatic = cfgResult.cyclomatic;
333
- const { loc, halstead } = def.complexity;
334
- const volume = halstead ? halstead.volume : 0;
335
- const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
336
- def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
337
- volume,
338
- cfgResult.cyclomatic,
339
- loc?.sloc ?? 0,
340
- commentRatio,
341
- );
342
- }
336
+ const cfgResult = matchResultToDef(byLine.get(def.line), def.name);
337
+ if (!cfgResult) continue;
338
+ def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
339
+
340
+ // Override complexity's cyclomatic with CFG-derived value (single source of truth)
341
+ if (def.complexity && cfgResult.cyclomatic != null) {
342
+ def.complexity.cyclomatic = cfgResult.cyclomatic;
343
+ const { loc, halstead } = def.complexity;
344
+ const volume = halstead ? halstead.volume : 0;
345
+ const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
346
+ def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
347
+ volume,
348
+ cfgResult.cyclomatic,
349
+ loc?.sloc ?? 0,
350
+ commentRatio,
351
+ );
343
352
  }
344
353
  }
345
354
  }
@@ -14,7 +14,7 @@ interface AstStoreRow {
14
14
  kind: string;
15
15
  name: string | null | undefined;
16
16
  text: string | null;
17
- receiver: null;
17
+ receiver: string | null;
18
18
  parentNodeId: number | null;
19
19
  }
20
20
 
@@ -52,6 +52,14 @@ function extractCallName(node: TreeSitterNode): string {
52
52
  return node.text?.split('(')[0] || '?';
53
53
  }
54
54
 
55
+ /** Extract receiver for call expressions (e.g. "obj" in "obj.method()"). */
56
+ function extractCallReceiver(node: TreeSitterNode): string | null {
57
+ const fn = node.childForFieldName('function');
58
+ if (!fn || fn.type !== 'member_expression') return null;
59
+ const obj = fn.childForFieldName('object');
60
+ return obj ? obj.text : null;
61
+ }
62
+
55
63
  function extractName(kind: string, node: TreeSitterNode): string | null {
56
64
  if (kind === 'throw') {
57
65
  for (let i = 0; i < node.childCount; i++) {
@@ -161,10 +169,12 @@ export function createAstStoreVisitor(
161
169
  const line = node.startPosition.row + 1;
162
170
  let name: string | null | undefined;
163
171
  let text: string | null = null;
172
+ let receiver: string | null = null;
164
173
 
165
174
  if (kind === 'call') {
166
175
  name = extractCallName(node);
167
176
  text = truncate(node.text);
177
+ receiver = extractCallReceiver(node);
168
178
  } else if (kind === 'new') {
169
179
  name = extractNewName(node);
170
180
  text = truncate(node.text);
@@ -190,7 +200,7 @@ export function createAstStoreVisitor(
190
200
  kind,
191
201
  name,
192
202
  text,
193
- receiver: null,
203
+ receiver,
194
204
  parentNodeId: resolveParentNodeId(line),
195
205
  });
196
206
 
@@ -201,6 +211,9 @@ export function createAstStoreVisitor(
201
211
  name: 'ast-store',
202
212
 
203
213
  enterNode(node: TreeSitterNode, _context: VisitorContext): EnterNodeResult | undefined {
214
+ // Guard: skip re-collection but do NOT skipChildren — node.id (memory address)
215
+ // can be reused by tree-sitter, so a collision would incorrectly suppress an
216
+ // unrelated subtree. The parent call's skipChildren handles the intended case.
204
217
  if (matched.has(node.id)) return;
205
218
 
206
219
  const kind = astTypeMap[node.type];
@@ -87,6 +87,16 @@ function classifyBranchNode(
87
87
  }
88
88
  }
89
89
 
90
+ function classifyLogicalOp(node: TreeSitterNode, cRules: AnyRules, acc: ComplexityAcc): void {
91
+ const op = node.child(1)?.type;
92
+ if (!op || !cRules.logicalOperators.has(op)) return;
93
+ acc.cyclomatic++;
94
+ const parent = node.parent;
95
+ const sameSequence =
96
+ parent != null && parent.type === cRules.logicalNodeType && parent.child(1)?.type === op;
97
+ if (!sameSequence) acc.cognitive++;
98
+ }
99
+
90
100
  function classifyPlainElse(
91
101
  node: TreeSitterNode,
92
102
  type: string,
@@ -215,17 +225,7 @@ export function createComplexityVisitor(
215
225
  if (nestingLevel > acc.maxNesting) acc.maxNesting = nestingLevel;
216
226
 
217
227
  if (type === cRules.logicalNodeType) {
218
- const op = node.child(1)?.type;
219
- if (op && cRules.logicalOperators.has(op)) {
220
- acc.cyclomatic++;
221
- const parent = node.parent;
222
- let sameSequence = false;
223
- if (parent && parent.type === cRules.logicalNodeType) {
224
- const parentOp = parent.child(1)?.type;
225
- if (parentOp === op) sameSequence = true;
226
- }
227
- if (!sameSequence) acc.cognitive++;
228
- }
228
+ classifyLogicalOp(node, cRules, acc);
229
229
  }
230
230
 
231
231
  if (type === cRules.optionalChainType) acc.cyclomatic++;
@@ -29,6 +29,23 @@ function getPackageVersion(): string {
29
29
  /** Warn once per process when DB version mismatches the running codegraph version. */
30
30
  let _versionWarned = false;
31
31
 
32
+ /** Check and warn (once) if the running codegraph version differs from the DB build version. */
33
+ function warnOnVersionMismatch(getBuildVersion: () => string | undefined | null): void {
34
+ if (_versionWarned) return;
35
+ _versionWarned = true;
36
+ try {
37
+ const buildVersion = getBuildVersion();
38
+ const currentVersion = getPackageVersion();
39
+ if (buildVersion && currentVersion && buildVersion !== currentVersion) {
40
+ warn(
41
+ `DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`,
42
+ );
43
+ }
44
+ } catch {
45
+ // build_meta table may not exist in older DBs — silently ignore
46
+ }
47
+ }
48
+
32
49
  /** DB instance with optional advisory lock path. */
33
50
  export type LockedDatabase = BetterSqlite3Database & { __lockPath?: string };
34
51
 
@@ -81,11 +98,6 @@ export function _resetRepoRootCache(): void {
81
98
  _cachedRepoRootCwd = undefined;
82
99
  }
83
100
 
84
- /** Reset the version warning flag (for testing). */
85
- export function _resetVersionWarning(): void {
86
- _versionWarned = false;
87
- }
88
-
89
101
  function isProcessAlive(pid: number): boolean {
90
102
  try {
91
103
  process.kill(pid, 0);
@@ -299,28 +311,41 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database {
299
311
  const Database = getDatabase();
300
312
  const db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;
301
313
 
302
- // Warn once per process if the DB was built with a different codegraph version
303
- if (!_versionWarned) {
304
- try {
305
- const row = db
306
- .prepare<{ value: string }>('SELECT value FROM build_meta WHERE key = ?')
307
- .get('codegraph_version');
308
- const buildVersion = row?.value;
309
- const currentVersion = getPackageVersion();
310
- if (buildVersion && currentVersion && buildVersion !== currentVersion) {
311
- warn(
312
- `DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`,
313
- );
314
- }
315
- } catch {
316
- // build_meta table may not exist in older DBs — silently ignore
317
- }
318
- _versionWarned = true;
319
- }
314
+ warnOnVersionMismatch(() => {
315
+ const row = db
316
+ .prepare<{ value: string }>('SELECT value FROM build_meta WHERE key = ?')
317
+ .get('codegraph_version');
318
+ return row?.value;
319
+ });
320
320
 
321
321
  return db;
322
322
  }
323
323
 
324
+ /** Open a NativeRepository via rusqlite, throwing DbError if the DB file is missing. */
325
+ function openRepoNative(customDbPath?: string): { repo: Repository; close(): void } {
326
+ const dbPath = findDbPath(customDbPath);
327
+ if (!fs.existsSync(dbPath)) {
328
+ throw new DbError(
329
+ `No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`,
330
+ { file: dbPath },
331
+ );
332
+ }
333
+ const native = getNative();
334
+ const ndb = native.NativeDatabase.openReadonly(dbPath);
335
+ try {
336
+ warnOnVersionMismatch(() => ndb.getBuildMeta('codegraph_version'));
337
+ return {
338
+ repo: new NativeRepository(ndb),
339
+ close() {
340
+ ndb.close();
341
+ },
342
+ };
343
+ } catch (innerErr) {
344
+ ndb.close();
345
+ throw innerErr;
346
+ }
347
+ }
348
+
324
349
  /**
325
350
  * Open a Repository from either an injected instance or a DB path.
326
351
  *
@@ -345,42 +370,7 @@ export function openRepo(
345
370
  // Try native rusqlite path first (Phase 6.14)
346
371
  if (isNativeAvailable()) {
347
372
  try {
348
- const dbPath = findDbPath(customDbPath);
349
- if (!fs.existsSync(dbPath)) {
350
- throw new DbError(
351
- `No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`,
352
- { file: dbPath },
353
- );
354
- }
355
- const native = getNative();
356
- const ndb = native.NativeDatabase.openReadonly(dbPath);
357
- try {
358
- // Version check (same logic as openReadonlyOrFail)
359
- if (!_versionWarned) {
360
- try {
361
- const buildVersion = ndb.getBuildMeta('codegraph_version');
362
- const currentVersion = getPackageVersion();
363
- if (buildVersion && currentVersion && buildVersion !== currentVersion) {
364
- warn(
365
- `DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`,
366
- );
367
- }
368
- } catch {
369
- // build_meta table may not exist in older DBs
370
- }
371
- _versionWarned = true;
372
- }
373
-
374
- return {
375
- repo: new NativeRepository(ndb),
376
- close() {
377
- ndb.close();
378
- },
379
- };
380
- } catch (innerErr) {
381
- ndb.close();
382
- throw innerErr;
383
- }
373
+ return openRepoNative(customDbPath);
384
374
  } catch (e) {
385
375
  // Re-throw user-visible errors (e.g. DB not found) — only silently
386
376
  // fall back for native-engine failures (e.g. incompatible native binary).
@@ -399,3 +389,44 @@ export function openRepo(
399
389
  },
400
390
  };
401
391
  }
392
+
393
+ /**
394
+ * Open a readonly DB with an optional NativeDatabase alongside it.
395
+ *
396
+ * Returns the better-sqlite3 handle (for backwards compat) plus an optional
397
+ * NativeDatabase for modules that can use batched Rust query methods.
398
+ * Callers should use nativeDb when available and fall back to db.prepare().
399
+ */
400
+ export function openReadonlyWithNative(customPath?: string): {
401
+ db: BetterSqlite3Database;
402
+ nativeDb: NativeDatabase | undefined;
403
+ close(): void;
404
+ } {
405
+ const db = openReadonlyOrFail(customPath);
406
+
407
+ let nativeDb: NativeDatabase | undefined;
408
+ if (isNativeAvailable()) {
409
+ try {
410
+ const dbPath = findDbPath(customPath);
411
+ const native = getNative();
412
+ nativeDb = native.NativeDatabase.openReadonly(dbPath);
413
+ } catch (e) {
414
+ debug(`openReadonlyWithNative: native path failed: ${(e as Error).message}`);
415
+ }
416
+ }
417
+
418
+ return {
419
+ db,
420
+ nativeDb,
421
+ close() {
422
+ db.close();
423
+ if (nativeDb) {
424
+ try {
425
+ nativeDb.close();
426
+ } catch {
427
+ // already closed or not closeable
428
+ }
429
+ }
430
+ },
431
+ };
432
+ }