@optave/codegraph 3.5.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/README.md +47 -21
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +119 -127
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +14 -1
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  10. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  11. package/dist/db/connection.d.ts +12 -2
  12. package/dist/db/connection.d.ts.map +1 -1
  13. package/dist/db/connection.js +81 -53
  14. package/dist/db/connection.js.map +1 -1
  15. package/dist/db/index.d.ts +1 -1
  16. package/dist/db/index.d.ts.map +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/db/migrations.d.ts.map +1 -1
  20. package/dist/db/migrations.js +38 -32
  21. package/dist/db/migrations.js.map +1 -1
  22. package/dist/domain/analysis/context.d.ts.map +1 -1
  23. package/dist/domain/analysis/context.js +51 -66
  24. package/dist/domain/analysis/context.js.map +1 -1
  25. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  26. package/dist/domain/analysis/dependencies.js +62 -70
  27. package/dist/domain/analysis/dependencies.js.map +1 -1
  28. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  29. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  30. package/dist/domain/analysis/exports.d.ts.map +1 -1
  31. package/dist/domain/analysis/exports.js +29 -33
  32. package/dist/domain/analysis/exports.js.map +1 -1
  33. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  34. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  35. package/dist/domain/analysis/fn-impact.js +35 -65
  36. package/dist/domain/analysis/fn-impact.js.map +1 -1
  37. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  38. package/dist/domain/analysis/module-map.js +91 -6
  39. package/dist/domain/analysis/module-map.js.map +1 -1
  40. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  41. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  42. package/dist/domain/analysis/query-helpers.js +27 -0
  43. package/dist/domain/analysis/query-helpers.js.map +1 -0
  44. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/helpers.js +15 -9
  46. package/dist/domain/graph/builder/helpers.js.map +1 -1
  47. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/incremental.js +3 -2
  49. package/dist/domain/graph/builder/incremental.js.map +1 -1
  50. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/pipeline.js +69 -3
  52. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  54. package/dist/domain/graph/builder/stages/build-edges.js +7 -51
  55. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/build-structure.js +7 -5
  58. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/collect-files.js +2 -2
  60. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  63. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/finalize.js +124 -105
  66. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  68. package/dist/domain/graph/builder/stages/insert-nodes.js +28 -15
  69. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  71. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  72. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  73. package/dist/domain/graph/resolve.d.ts +0 -4
  74. package/dist/domain/graph/resolve.d.ts.map +1 -1
  75. package/dist/domain/graph/resolve.js +32 -48
  76. package/dist/domain/graph/resolve.js.map +1 -1
  77. package/dist/domain/graph/watcher.d.ts.map +1 -1
  78. package/dist/domain/graph/watcher.js +12 -12
  79. package/dist/domain/graph/watcher.js.map +1 -1
  80. package/dist/domain/parser.d.ts +1 -1
  81. package/dist/domain/parser.d.ts.map +1 -1
  82. package/dist/domain/parser.js +206 -101
  83. package/dist/domain/parser.js.map +1 -1
  84. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  85. package/dist/domain/search/search/cli-formatter.js +88 -83
  86. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  87. package/dist/extractors/bash.d.ts +6 -0
  88. package/dist/extractors/bash.d.ts.map +1 -0
  89. package/dist/extractors/bash.js +91 -0
  90. package/dist/extractors/bash.js.map +1 -0
  91. package/dist/extractors/c.d.ts +6 -0
  92. package/dist/extractors/c.d.ts.map +1 -0
  93. package/dist/extractors/c.js +204 -0
  94. package/dist/extractors/c.js.map +1 -0
  95. package/dist/extractors/cpp.d.ts +6 -0
  96. package/dist/extractors/cpp.d.ts.map +1 -0
  97. package/dist/extractors/cpp.js +283 -0
  98. package/dist/extractors/cpp.js.map +1 -0
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +42 -54
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/dart.d.ts +6 -0
  103. package/dist/extractors/dart.d.ts.map +1 -0
  104. package/dist/extractors/dart.js +277 -0
  105. package/dist/extractors/dart.js.map +1 -0
  106. package/dist/extractors/elixir.d.ts +9 -0
  107. package/dist/extractors/elixir.d.ts.map +1 -0
  108. package/dist/extractors/elixir.js +223 -0
  109. package/dist/extractors/elixir.js.map +1 -0
  110. package/dist/extractors/go.d.ts.map +1 -1
  111. package/dist/extractors/go.js +126 -130
  112. package/dist/extractors/go.js.map +1 -1
  113. package/dist/extractors/haskell.d.ts +8 -0
  114. package/dist/extractors/haskell.d.ts.map +1 -0
  115. package/dist/extractors/haskell.js +217 -0
  116. package/dist/extractors/haskell.js.map +1 -0
  117. package/dist/extractors/hcl.js +6 -6
  118. package/dist/extractors/hcl.js.map +1 -1
  119. package/dist/extractors/helpers.d.ts +32 -1
  120. package/dist/extractors/helpers.d.ts.map +1 -1
  121. package/dist/extractors/helpers.js +74 -0
  122. package/dist/extractors/helpers.js.map +1 -1
  123. package/dist/extractors/index.d.ts +12 -0
  124. package/dist/extractors/index.d.ts.map +1 -1
  125. package/dist/extractors/index.js +12 -0
  126. package/dist/extractors/index.js.map +1 -1
  127. package/dist/extractors/java.d.ts.map +1 -1
  128. package/dist/extractors/java.js +32 -47
  129. package/dist/extractors/java.js.map +1 -1
  130. package/dist/extractors/javascript.d.ts.map +1 -1
  131. package/dist/extractors/javascript.js +306 -292
  132. package/dist/extractors/javascript.js.map +1 -1
  133. package/dist/extractors/kotlin.d.ts +6 -0
  134. package/dist/extractors/kotlin.d.ts.map +1 -0
  135. package/dist/extractors/kotlin.js +275 -0
  136. package/dist/extractors/kotlin.js.map +1 -0
  137. package/dist/extractors/lua.d.ts +6 -0
  138. package/dist/extractors/lua.d.ts.map +1 -0
  139. package/dist/extractors/lua.js +162 -0
  140. package/dist/extractors/lua.js.map +1 -0
  141. package/dist/extractors/ocaml.d.ts +6 -0
  142. package/dist/extractors/ocaml.d.ts.map +1 -0
  143. package/dist/extractors/ocaml.js +236 -0
  144. package/dist/extractors/ocaml.js.map +1 -0
  145. package/dist/extractors/php.d.ts.map +1 -1
  146. package/dist/extractors/php.js +39 -44
  147. package/dist/extractors/php.js.map +1 -1
  148. package/dist/extractors/python.d.ts.map +1 -1
  149. package/dist/extractors/python.js +75 -93
  150. package/dist/extractors/python.js.map +1 -1
  151. package/dist/extractors/ruby.js +6 -13
  152. package/dist/extractors/ruby.js.map +1 -1
  153. package/dist/extractors/rust.d.ts.map +1 -1
  154. package/dist/extractors/rust.js +58 -83
  155. package/dist/extractors/rust.js.map +1 -1
  156. package/dist/extractors/scala.d.ts +6 -0
  157. package/dist/extractors/scala.d.ts.map +1 -0
  158. package/dist/extractors/scala.js +269 -0
  159. package/dist/extractors/scala.js.map +1 -0
  160. package/dist/extractors/swift.d.ts +6 -0
  161. package/dist/extractors/swift.d.ts.map +1 -0
  162. package/dist/extractors/swift.js +275 -0
  163. package/dist/extractors/swift.js.map +1 -0
  164. package/dist/extractors/zig.d.ts +9 -0
  165. package/dist/extractors/zig.d.ts.map +1 -0
  166. package/dist/extractors/zig.js +276 -0
  167. package/dist/extractors/zig.js.map +1 -0
  168. package/dist/features/ast.d.ts +2 -0
  169. package/dist/features/ast.d.ts.map +1 -1
  170. package/dist/features/ast.js +9 -24
  171. package/dist/features/ast.js.map +1 -1
  172. package/dist/features/audit.d.ts.map +1 -1
  173. package/dist/features/audit.js +17 -21
  174. package/dist/features/audit.js.map +1 -1
  175. package/dist/features/branch-compare.d.ts.map +1 -1
  176. package/dist/features/branch-compare.js +47 -3
  177. package/dist/features/branch-compare.js.map +1 -1
  178. package/dist/features/cfg.d.ts +7 -1
  179. package/dist/features/cfg.d.ts.map +1 -1
  180. package/dist/features/cfg.js +72 -61
  181. package/dist/features/cfg.js.map +1 -1
  182. package/dist/features/check.d.ts.map +1 -1
  183. package/dist/features/check.js +79 -62
  184. package/dist/features/check.js.map +1 -1
  185. package/dist/features/complexity-query.d.ts.map +1 -1
  186. package/dist/features/complexity-query.js +142 -137
  187. package/dist/features/complexity-query.js.map +1 -1
  188. package/dist/features/complexity.d.ts +7 -1
  189. package/dist/features/complexity.d.ts.map +1 -1
  190. package/dist/features/complexity.js +62 -1
  191. package/dist/features/complexity.js.map +1 -1
  192. package/dist/features/dataflow.d.ts +7 -1
  193. package/dist/features/dataflow.d.ts.map +1 -1
  194. package/dist/features/dataflow.js +356 -188
  195. package/dist/features/dataflow.js.map +1 -1
  196. package/dist/features/graph-enrichment.d.ts.map +1 -1
  197. package/dist/features/graph-enrichment.js +117 -104
  198. package/dist/features/graph-enrichment.js.map +1 -1
  199. package/dist/features/sequence.d.ts.map +1 -1
  200. package/dist/features/sequence.js +25 -4
  201. package/dist/features/sequence.js.map +1 -1
  202. package/dist/features/structure-query.d.ts.map +1 -1
  203. package/dist/features/structure-query.js +29 -4
  204. package/dist/features/structure-query.js.map +1 -1
  205. package/dist/features/structure.d.ts.map +1 -1
  206. package/dist/features/structure.js +35 -15
  207. package/dist/features/structure.js.map +1 -1
  208. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  209. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  210. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  211. package/dist/graph/algorithms/leiden/index.js +43 -28
  212. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  213. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  214. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  215. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  216. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  217. package/dist/graph/algorithms/leiden/partition.js +89 -106
  218. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  219. package/dist/graph/model.d.ts +2 -0
  220. package/dist/graph/model.d.ts.map +1 -1
  221. package/dist/graph/model.js +20 -8
  222. package/dist/graph/model.js.map +1 -1
  223. package/dist/infrastructure/config.d.ts +0 -8
  224. package/dist/infrastructure/config.d.ts.map +1 -1
  225. package/dist/infrastructure/config.js +73 -62
  226. package/dist/infrastructure/config.js.map +1 -1
  227. package/dist/infrastructure/registry.d.ts +0 -8
  228. package/dist/infrastructure/registry.d.ts.map +1 -1
  229. package/dist/infrastructure/registry.js +12 -14
  230. package/dist/infrastructure/registry.js.map +1 -1
  231. package/dist/mcp/server.d.ts.map +1 -1
  232. package/dist/mcp/server.js +45 -36
  233. package/dist/mcp/server.js.map +1 -1
  234. package/dist/presentation/audit.d.ts.map +1 -1
  235. package/dist/presentation/audit.js +61 -57
  236. package/dist/presentation/audit.js.map +1 -1
  237. package/dist/presentation/branch-compare.d.ts.map +1 -1
  238. package/dist/presentation/branch-compare.js +56 -38
  239. package/dist/presentation/branch-compare.js.map +1 -1
  240. package/dist/presentation/check.d.ts.map +1 -1
  241. package/dist/presentation/check.js +30 -32
  242. package/dist/presentation/check.js.map +1 -1
  243. package/dist/presentation/colors.d.ts.map +1 -1
  244. package/dist/presentation/colors.js +2 -0
  245. package/dist/presentation/colors.js.map +1 -1
  246. package/dist/presentation/complexity.d.ts.map +1 -1
  247. package/dist/presentation/complexity.js +25 -19
  248. package/dist/presentation/complexity.js.map +1 -1
  249. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  250. package/dist/presentation/queries-cli/exports.js +15 -15
  251. package/dist/presentation/queries-cli/exports.js.map +1 -1
  252. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  253. package/dist/presentation/queries-cli/impact.js +29 -19
  254. package/dist/presentation/queries-cli/impact.js.map +1 -1
  255. package/dist/types.d.ts +182 -7
  256. package/dist/types.d.ts.map +1 -1
  257. package/grammars/tree-sitter-bash.wasm +0 -0
  258. package/grammars/tree-sitter-c.wasm +0 -0
  259. package/grammars/tree-sitter-cpp.wasm +0 -0
  260. package/grammars/tree-sitter-dart.wasm +0 -0
  261. package/grammars/tree-sitter-elixir.wasm +0 -0
  262. package/grammars/tree-sitter-haskell.wasm +0 -0
  263. package/grammars/tree-sitter-kotlin.wasm +0 -0
  264. package/grammars/tree-sitter-lua.wasm +0 -0
  265. package/grammars/tree-sitter-ocaml.wasm +0 -0
  266. package/grammars/tree-sitter-scala.wasm +0 -0
  267. package/grammars/tree-sitter-swift.wasm +0 -0
  268. package/grammars/tree-sitter-zig.wasm +0 -0
  269. package/package.json +19 -7
  270. package/src/ast-analysis/engine.ts +147 -138
  271. package/src/ast-analysis/visitors/ast-store-visitor.ts +15 -2
  272. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  273. package/src/db/connection.ts +90 -59
  274. package/src/db/index.ts +1 -0
  275. package/src/db/migrations.ts +36 -32
  276. package/src/domain/analysis/context.ts +73 -75
  277. package/src/domain/analysis/dependencies.ts +78 -68
  278. package/src/domain/analysis/exports.ts +45 -34
  279. package/src/domain/analysis/fn-impact.ts +67 -64
  280. package/src/domain/analysis/module-map.ts +103 -8
  281. package/src/domain/analysis/query-helpers.ts +35 -0
  282. package/src/domain/graph/builder/helpers.ts +12 -6
  283. package/src/domain/graph/builder/incremental.ts +3 -2
  284. package/src/domain/graph/builder/pipeline.ts +71 -3
  285. package/src/domain/graph/builder/stages/build-edges.ts +10 -75
  286. package/src/domain/graph/builder/stages/build-structure.ts +9 -7
  287. package/src/domain/graph/builder/stages/collect-files.ts +2 -2
  288. package/src/domain/graph/builder/stages/detect-changes.ts +7 -2
  289. package/src/domain/graph/builder/stages/finalize.ts +159 -125
  290. package/src/domain/graph/builder/stages/insert-nodes.ts +32 -21
  291. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  292. package/src/domain/graph/resolve.ts +34 -46
  293. package/src/domain/graph/watcher.ts +12 -14
  294. package/src/domain/parser.ts +222 -97
  295. package/src/domain/search/search/cli-formatter.ts +121 -94
  296. package/src/extractors/bash.ts +97 -0
  297. package/src/extractors/c.ts +212 -0
  298. package/src/extractors/cpp.ts +298 -0
  299. package/src/extractors/csharp.ts +53 -56
  300. package/src/extractors/dart.ts +304 -0
  301. package/src/extractors/elixir.ts +251 -0
  302. package/src/extractors/go.ts +152 -134
  303. package/src/extractors/haskell.ts +235 -0
  304. package/src/extractors/hcl.ts +6 -6
  305. package/src/extractors/helpers.ts +93 -1
  306. package/src/extractors/index.ts +12 -0
  307. package/src/extractors/java.ts +43 -48
  308. package/src/extractors/javascript.ts +328 -281
  309. package/src/extractors/kotlin.ts +293 -0
  310. package/src/extractors/lua.ts +169 -0
  311. package/src/extractors/ocaml.ts +259 -0
  312. package/src/extractors/php.ts +46 -40
  313. package/src/extractors/python.ts +81 -104
  314. package/src/extractors/ruby.ts +6 -13
  315. package/src/extractors/rust.ts +65 -85
  316. package/src/extractors/scala.ts +285 -0
  317. package/src/extractors/swift.ts +293 -0
  318. package/src/extractors/zig.ts +294 -0
  319. package/src/features/ast.ts +10 -25
  320. package/src/features/audit.ts +24 -20
  321. package/src/features/branch-compare.ts +51 -4
  322. package/src/features/cfg.ts +113 -65
  323. package/src/features/check.ts +90 -74
  324. package/src/features/complexity-query.ts +181 -163
  325. package/src/features/complexity.ts +64 -1
  326. package/src/features/dataflow.ts +462 -217
  327. package/src/features/graph-enrichment.ts +161 -117
  328. package/src/features/sequence.ts +27 -4
  329. package/src/features/structure-query.ts +43 -4
  330. package/src/features/structure.ts +50 -22
  331. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  332. package/src/graph/algorithms/leiden/index.ts +67 -28
  333. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  334. package/src/graph/algorithms/leiden/partition.ts +131 -98
  335. package/src/graph/model.ts +19 -7
  336. package/src/infrastructure/config.ts +60 -58
  337. package/src/infrastructure/registry.ts +17 -14
  338. package/src/mcp/server.ts +46 -37
  339. package/src/presentation/audit.ts +72 -67
  340. package/src/presentation/branch-compare.ts +54 -50
  341. package/src/presentation/check.ts +34 -34
  342. package/src/presentation/colors.ts +2 -0
  343. package/src/presentation/complexity.ts +39 -33
  344. package/src/presentation/queries-cli/exports.ts +17 -17
  345. package/src/presentation/queries-cli/impact.ts +30 -22
  346. package/src/types.ts +195 -7
@@ -5,8 +5,10 @@ import path from 'node:path';
5
5
  import { getDatabase } from '../db/better-sqlite3.js';
6
6
  import { buildGraph } from '../domain/graph/builder.js';
7
7
  import { kindIcon } from '../domain/queries.js';
8
+ import { debug } from '../infrastructure/logger.js';
9
+ import { getNative, isNativeAvailable } from '../infrastructure/native.js';
8
10
  import { isTestFile } from '../infrastructure/test-filter.js';
9
- import type { EngineMode } from '../types.js';
11
+ import type { EngineMode, NativeDatabase } from '../types.js';
10
12
 
11
13
  // ─── Git Helpers ────────────────────────────────────────────────────────
12
14
 
@@ -107,6 +109,18 @@ function loadSymbolsFromDb(
107
109
  ): Map<string, SymbolInfo> {
108
110
  const Database = getDatabase();
109
111
  const db = new Database(dbPath, { readonly: true });
112
+
113
+ // Try opening a NativeDatabase for batched fan metrics
114
+ let nativeDb: NativeDatabase | undefined;
115
+ if (isNativeAvailable()) {
116
+ try {
117
+ const native = getNative();
118
+ nativeDb = native.NativeDatabase.openReadonly(dbPath);
119
+ } catch (e) {
120
+ debug(`loadSymbolsFromDb: native path failed: ${(e as Error).message}`);
121
+ }
122
+ }
123
+
110
124
  try {
111
125
  const symbols = new Map<string, SymbolInfo>();
112
126
 
@@ -132,6 +146,34 @@ function loadSymbolsFromDb(
132
146
  end_line: number | null;
133
147
  }>;
134
148
 
149
+ // Filter first, then batch fan metrics for all surviving rows
150
+ const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
151
+
152
+ // ── Native fast path: batch all fan-in/fan-out in one napi call ──
153
+ if (nativeDb?.batchFanMetrics && filtered.length > 0) {
154
+ const nodeIds = filtered.map((r) => r.id);
155
+ const metrics = nativeDb.batchFanMetrics(nodeIds);
156
+ const metricsMap = new Map(metrics.map((m) => [m.nodeId, m]));
157
+
158
+ for (const row of filtered) {
159
+ const lineCount = row.end_line ? row.end_line - row.line + 1 : 0;
160
+ const m = metricsMap.get(row.id);
161
+ const key = makeSymbolKey(row.kind, row.file, row.name);
162
+ symbols.set(key, {
163
+ id: row.id,
164
+ name: row.name,
165
+ kind: row.kind,
166
+ file: row.file,
167
+ line: row.line,
168
+ lineCount,
169
+ fanIn: m?.fanIn ?? 0,
170
+ fanOut: m?.fanOut ?? 0,
171
+ });
172
+ }
173
+ return symbols;
174
+ }
175
+
176
+ // ── JS fallback ───────────────────────────────────────────────────
135
177
  const fanInStmt = db.prepare(
136
178
  `SELECT COUNT(*) AS cnt FROM edges WHERE target_id = ? AND kind = 'calls'`,
137
179
  );
@@ -139,9 +181,7 @@ function loadSymbolsFromDb(
139
181
  `SELECT COUNT(*) AS cnt FROM edges WHERE source_id = ? AND kind = 'calls'`,
140
182
  );
141
183
 
142
- for (const row of rows) {
143
- if (noTests && isTestFile(row.file)) continue;
144
-
184
+ for (const row of filtered) {
145
185
  const lineCount = row.end_line ? row.end_line - row.line + 1 : 0;
146
186
  const fanIn = (fanInStmt.get(row.id) as { cnt: number }).cnt;
147
187
  const fanOut = (fanOutStmt.get(row.id) as { cnt: number }).cnt;
@@ -162,6 +202,13 @@ function loadSymbolsFromDb(
162
202
  return symbols;
163
203
  } finally {
164
204
  db.close();
205
+ if (nativeDb) {
206
+ try {
207
+ nativeDb.close();
208
+ } catch {
209
+ /* already closed */
210
+ }
211
+ }
165
212
  }
166
213
  }
167
214
 
@@ -275,16 +275,116 @@ function allCfgNative(fileSymbols: Map<string, FileSymbols>): boolean {
275
275
  return hasCfgFile;
276
276
  }
277
277
 
278
+ /** Persist native CFG data for a single file (fast path — no tree/visitor needed). */
279
+ function persistNativeFileCfg(
280
+ db: BetterSqlite3Database,
281
+ symbols: FileSymbols,
282
+ relPath: string,
283
+ insertBlock: ReturnType<BetterSqlite3Database['prepare']>,
284
+ insertEdge: ReturnType<BetterSqlite3Database['prepare']>,
285
+ ): number {
286
+ let count = 0;
287
+ for (const def of symbols.definitions) {
288
+ if (def.kind !== 'function' && def.kind !== 'method') continue;
289
+ if (!def.line) continue;
290
+
291
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
292
+ if (!nodeId) continue;
293
+
294
+ deleteCfgForNode(db, nodeId);
295
+ if (!def.cfg?.blocks?.length) continue;
296
+
297
+ persistCfg(
298
+ def.cfg as unknown as { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] },
299
+ nodeId,
300
+ insertBlock,
301
+ insertEdge,
302
+ );
303
+ count++;
304
+ }
305
+ return count;
306
+ }
307
+
308
+ /** Resolve CFG for a definition from native data or visitor results. */
309
+ function resolveCfgForDef(
310
+ def: Definition,
311
+ visitorCfgByLine: Map<number, VisitorCfgResult[]> | null,
312
+ ): { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] } | null {
313
+ if (def.cfg?.blocks?.length) {
314
+ return def.cfg as unknown as { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] };
315
+ }
316
+ if (!visitorCfgByLine) return null;
317
+ const candidates = visitorCfgByLine.get(def.line);
318
+ if (!candidates) return null;
319
+ const r =
320
+ candidates.length === 1
321
+ ? candidates[0]
322
+ : (candidates.find((c) => {
323
+ const n = c.funcNode.childForFieldName?.('name');
324
+ return n && n.text === def.name;
325
+ }) ?? candidates[0]);
326
+ return r ? { blocks: r.blocks, edges: r.edges } : null;
327
+ }
328
+
329
+ /** Persist CFG data for a single file using visitor/native hybrid path. */
330
+ function persistVisitorFileCfg(
331
+ db: BetterSqlite3Database,
332
+ symbols: FileSymbols,
333
+ relPath: string,
334
+ rootDir: string,
335
+ extToLang: Map<string, string>,
336
+ parsers: unknown,
337
+ getParserFn: unknown,
338
+ insertBlock: ReturnType<BetterSqlite3Database['prepare']>,
339
+ insertEdge: ReturnType<BetterSqlite3Database['prepare']>,
340
+ ): number {
341
+ const treeLang = getTreeAndLang(symbols, relPath, rootDir, extToLang, parsers, getParserFn);
342
+ if (!treeLang) return 0;
343
+ const { tree, langId } = treeLang;
344
+
345
+ const cfgRules = CFG_RULES.get(langId);
346
+ if (!cfgRules) return 0;
347
+
348
+ const visitorCfgByLine = buildVisitorCfgMap(tree, cfgRules, symbols, langId);
349
+ let count = 0;
350
+
351
+ for (const def of symbols.definitions) {
352
+ if (def.kind !== 'function' && def.kind !== 'method') continue;
353
+ if (!def.line) continue;
354
+
355
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
356
+ if (!nodeId) continue;
357
+
358
+ const cfg = resolveCfgForDef(def, visitorCfgByLine);
359
+ deleteCfgForNode(db, nodeId);
360
+ if (!cfg || cfg.blocks.length === 0) continue;
361
+
362
+ persistCfg(cfg, nodeId, insertBlock, insertEdge);
363
+ count++;
364
+ }
365
+ return count;
366
+ }
367
+
278
368
  export async function buildCFGData(
279
369
  db: BetterSqlite3Database,
280
370
  fileSymbols: Map<string, FileSymbols>,
281
371
  rootDir: string,
282
- _engineOpts?: unknown,
372
+ _engineOpts?: {
373
+ nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
374
+ suspendJsDb?: () => void;
375
+ resumeJsDb?: () => void;
376
+ },
283
377
  ): Promise<void> {
284
378
  // Fast path: when all function/method defs already have native CFG data,
285
379
  // skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
286
380
  const allNative = allCfgNative(fileSymbols);
287
381
 
382
+ // NOTE: nativeDb.bulkInsertCfg is intentionally NOT used here.
383
+ // The CFG path requires delete-before-insert (deleteCfgForNode) which creates
384
+ // a dual-connection WAL conflict when deletes go through JS (better-sqlite3)
385
+ // and inserts go through native (rusqlite). The JS-only persistNativeFileCfg
386
+ // path below handles both on a single connection safely.
387
+
288
388
  const extToLang = buildExtToLangMap();
289
389
  let parsers: unknown = null;
290
390
  let getParserFn: unknown = null;
@@ -308,74 +408,22 @@ export async function buildCFGData(
308
408
  const ext = path.extname(relPath).toLowerCase();
309
409
  if (!CFG_EXTENSIONS.has(ext)) continue;
310
410
 
311
- // Native fast path: skip tree/visitor setup when all CFG is pre-computed.
312
- // Only apply to files without _tree — files with _tree were WASM-parsed
313
- // and need the slow path (visitor) to compute CFG.
314
411
  if (allNative && !symbols._tree) {
315
- for (const def of symbols.definitions) {
316
- if (def.kind !== 'function' && def.kind !== 'method') continue;
317
- if (!def.line) continue;
318
-
319
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
320
- if (!nodeId) continue;
321
-
322
- // Always delete stale CFG rows (handles body-removed case)
323
- deleteCfgForNode(db, nodeId);
324
- if (!def.cfg?.blocks?.length) continue;
325
-
326
- persistCfg(
327
- def.cfg as unknown as { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] },
328
- nodeId,
329
- insertBlock,
330
- insertEdge,
331
- );
332
- analyzed++;
333
- }
412
+ analyzed += persistNativeFileCfg(db, symbols, relPath, insertBlock, insertEdge);
334
413
  continue;
335
414
  }
336
415
 
337
- // When allNative=true, parsers/getParserFn are null. This is safe because
338
- // _tree files use symbols._tree directly in getTreeAndLang (the parser
339
- // code path is never reached). Non-_tree files are handled by the fast path above.
340
- const treeLang = getTreeAndLang(symbols, relPath, rootDir, extToLang, parsers, getParserFn);
341
- if (!treeLang) continue;
342
- const { tree, langId } = treeLang;
343
-
344
- const cfgRules = CFG_RULES.get(langId);
345
- if (!cfgRules) continue;
346
-
347
- const visitorCfgByLine = buildVisitorCfgMap(tree, cfgRules, symbols, langId);
348
-
349
- for (const def of symbols.definitions) {
350
- if (def.kind !== 'function' && def.kind !== 'method') continue;
351
- if (!def.line) continue;
352
-
353
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
354
- if (!nodeId) continue;
355
-
356
- let cfg: { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] } | null = null;
357
- if (def.cfg?.blocks?.length) {
358
- cfg = def.cfg as unknown as { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] };
359
- } else if (visitorCfgByLine) {
360
- const candidates = visitorCfgByLine.get(def.line);
361
- const r = !candidates
362
- ? undefined
363
- : candidates.length === 1
364
- ? candidates[0]
365
- : (candidates.find((c) => {
366
- const n = c.funcNode.childForFieldName?.('name');
367
- return n && n.text === def.name;
368
- }) ?? candidates[0]);
369
- if (r) cfg = { blocks: r.blocks, edges: r.edges };
370
- }
371
-
372
- // Always purge stale rows (handles body-removed case)
373
- deleteCfgForNode(db, nodeId);
374
- if (!cfg || cfg.blocks.length === 0) continue;
375
-
376
- persistCfg(cfg, nodeId, insertBlock, insertEdge);
377
- analyzed++;
378
- }
416
+ analyzed += persistVisitorFileCfg(
417
+ db,
418
+ symbols,
419
+ relPath,
420
+ rootDir,
421
+ extToLang,
422
+ parsers,
423
+ getParserFn,
424
+ insertBlock,
425
+ insertEdge,
426
+ );
379
427
  }
380
428
  });
381
429
 
@@ -291,6 +291,85 @@ interface CheckOpts {
291
291
  config?: CodegraphConfig;
292
292
  }
293
293
 
294
+ /** Walk up from repoRoot to find the nearest .git directory. */
295
+ function findGitRoot(repoRoot: string): string | null {
296
+ let dir = repoRoot;
297
+ while (dir) {
298
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
299
+ const parent = path.dirname(dir);
300
+ if (parent === dir) break;
301
+ dir = parent;
302
+ }
303
+ return null;
304
+ }
305
+
306
+ /** Run git diff and return the raw output string. */
307
+ function getGitDiff(repoRoot: string, opts: { staged?: boolean; ref?: string }): string {
308
+ const args = opts.staged
309
+ ? ['diff', '--cached', '--unified=0', '--no-color']
310
+ : ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
311
+ return execFileSync('git', args, {
312
+ cwd: repoRoot,
313
+ encoding: 'utf-8',
314
+ maxBuffer: 10 * 1024 * 1024,
315
+ stdio: ['pipe', 'pipe', 'pipe'],
316
+ });
317
+ }
318
+
319
+ /** Resolve which check predicates are enabled from opts + config. */
320
+ function resolveCheckFlags(opts: CheckOpts, config: CodegraphConfig) {
321
+ const checkConfig = config.check || ({} as CodegraphConfig['check']);
322
+ return {
323
+ enableCycles: opts.cycles ?? checkConfig.cycles ?? true,
324
+ enableSignatures: opts.signatures ?? checkConfig.signatures ?? true,
325
+ enableBoundaries: opts.boundaries ?? checkConfig.boundaries ?? true,
326
+ blastRadiusThreshold: opts.blastRadius ?? checkConfig.blastRadius ?? null,
327
+ };
328
+ }
329
+
330
+ /** Run all enabled check predicates and return the results. */
331
+ function runPredicates(
332
+ db: BetterSqlite3Database,
333
+ diff: ParsedDiff,
334
+ flags: ReturnType<typeof resolveCheckFlags>,
335
+ repoRoot: string,
336
+ noTests: boolean,
337
+ maxDepth: number,
338
+ ): PredicateResult[] {
339
+ const changedFiles = new Set(diff.changedRanges.keys());
340
+ const predicates: PredicateResult[] = [];
341
+
342
+ if (flags.enableCycles) {
343
+ predicates.push({ name: 'cycles', ...checkNoNewCycles(db, changedFiles, noTests) });
344
+ }
345
+ if (flags.blastRadiusThreshold != null) {
346
+ predicates.push({
347
+ name: 'blast-radius',
348
+ ...checkMaxBlastRadius(db, diff.changedRanges, flags.blastRadiusThreshold, noTests, maxDepth),
349
+ });
350
+ }
351
+ if (flags.enableSignatures) {
352
+ predicates.push({
353
+ name: 'signatures',
354
+ ...checkNoSignatureChanges(db, diff.oldRanges, noTests),
355
+ });
356
+ }
357
+ if (flags.enableBoundaries) {
358
+ predicates.push({
359
+ name: 'boundaries',
360
+ ...checkNoBoundaryViolations(db, changedFiles, repoRoot, noTests),
361
+ });
362
+ }
363
+
364
+ return predicates;
365
+ }
366
+
367
+ const EMPTY_CHECK: CheckResult = {
368
+ predicates: [],
369
+ summary: { total: 0, passed: 0, failed: 0, changedFiles: 0, newFiles: 0 },
370
+ passed: true,
371
+ };
372
+
294
373
  export function checkData(customDbPath: string | undefined, opts: CheckOpts = {}): CheckResult {
295
374
  const db = openReadonlyOrFail(customDbPath);
296
375
 
@@ -301,89 +380,26 @@ export function checkData(customDbPath: string | undefined, opts: CheckOpts = {}
301
380
  const maxDepth = opts.depth || 3;
302
381
 
303
382
  const config = opts.config || loadConfig(repoRoot);
304
- const checkConfig = config.check || ({} as CodegraphConfig['check']);
305
-
306
- const enableCycles = opts.cycles ?? checkConfig.cycles ?? true;
307
- const enableSignatures = opts.signatures ?? checkConfig.signatures ?? true;
308
- const enableBoundaries = opts.boundaries ?? checkConfig.boundaries ?? true;
309
- const blastRadiusThreshold = opts.blastRadius ?? checkConfig.blastRadius ?? null;
310
-
311
- let checkDir = repoRoot;
312
- let isGitRepo = false;
313
- while (checkDir) {
314
- if (fs.existsSync(path.join(checkDir, '.git'))) {
315
- isGitRepo = true;
316
- break;
317
- }
318
- const parent = path.dirname(checkDir);
319
- if (parent === checkDir) break;
320
- checkDir = parent;
321
- }
322
- if (!isGitRepo) {
383
+ const flags = resolveCheckFlags(opts, config);
384
+
385
+ const gitRoot = findGitRoot(repoRoot);
386
+ if (!gitRoot) {
323
387
  return { error: `Not a git repository: ${repoRoot}` };
324
388
  }
325
389
 
326
390
  let diffOutput: string;
327
391
  try {
328
- const args = opts.staged
329
- ? ['diff', '--cached', '--unified=0', '--no-color']
330
- : ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
331
- diffOutput = execFileSync('git', args, {
332
- cwd: repoRoot,
333
- encoding: 'utf-8',
334
- maxBuffer: 10 * 1024 * 1024,
335
- stdio: ['pipe', 'pipe', 'pipe'],
336
- });
392
+ diffOutput = getGitDiff(repoRoot, opts);
337
393
  } catch (e) {
338
394
  return { error: `Failed to run git diff: ${(e as Error).message}` };
339
395
  }
340
396
 
341
- if (!diffOutput.trim()) {
342
- return {
343
- predicates: [],
344
- summary: { total: 0, passed: 0, failed: 0, changedFiles: 0, newFiles: 0 },
345
- passed: true,
346
- };
347
- }
348
-
349
- const { changedRanges, oldRanges, newFiles } = parseDiffOutput(diffOutput);
350
- if (changedRanges.size === 0) {
351
- return {
352
- predicates: [],
353
- summary: { total: 0, passed: 0, failed: 0, changedFiles: 0, newFiles: 0 },
354
- passed: true,
355
- };
356
- }
357
-
358
- const changedFiles = new Set(changedRanges.keys());
397
+ if (!diffOutput.trim()) return EMPTY_CHECK;
359
398
 
360
- const predicates: PredicateResult[] = [];
399
+ const diff = parseDiffOutput(diffOutput);
400
+ if (diff.changedRanges.size === 0) return EMPTY_CHECK;
361
401
 
362
- if (enableCycles) {
363
- const result = checkNoNewCycles(db, changedFiles, noTests);
364
- predicates.push({ name: 'cycles', ...result });
365
- }
366
-
367
- if (blastRadiusThreshold != null) {
368
- const result = checkMaxBlastRadius(
369
- db,
370
- changedRanges,
371
- blastRadiusThreshold,
372
- noTests,
373
- maxDepth,
374
- );
375
- predicates.push({ name: 'blast-radius', ...result });
376
- }
377
-
378
- if (enableSignatures) {
379
- const result = checkNoSignatureChanges(db, oldRanges, noTests);
380
- predicates.push({ name: 'signatures', ...result });
381
- }
382
-
383
- if (enableBoundaries) {
384
- const result = checkNoBoundaryViolations(db, changedFiles, repoRoot, noTests);
385
- predicates.push({ name: 'boundaries', ...result });
386
- }
402
+ const predicates = runPredicates(db, diff, flags, repoRoot, noTests, maxDepth);
387
403
 
388
404
  const passedCount = predicates.filter((p) => p.passed).length;
389
405
  const failedCount = predicates.length - passedCount;
@@ -394,8 +410,8 @@ export function checkData(customDbPath: string | undefined, opts: CheckOpts = {}
394
410
  total: predicates.length,
395
411
  passed: passedCount,
396
412
  failed: failedCount,
397
- changedFiles: changedFiles.size,
398
- newFiles: newFiles.size,
413
+ changedFiles: diff.changedRanges.size,
414
+ newFiles: diff.newFiles.size,
399
415
  },
400
416
  passed: failedCount === 0,
401
417
  };