@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
@@ -2,14 +2,78 @@
2
2
  * Stage: collectFiles
3
3
  *
4
4
  * Collects all source files to process. Handles both normal and scoped rebuilds.
5
+ * For incremental builds with a valid journal, reconstructs the file list from
6
+ * the DB's file_hashes table + journal deltas, skipping the filesystem scan.
5
7
  */
6
8
  import fs from 'node:fs';
7
9
  import path from 'node:path';
8
- import { info } from '../../../../infrastructure/logger.js';
10
+ import { debug, info } from '../../../../infrastructure/logger.js';
9
11
  import { normalizePath } from '../../../../shared/constants.js';
12
+ import { readJournal } from '../../journal.js';
10
13
  import type { PipelineContext } from '../context.js';
11
14
  import { collectFiles as collectFilesUtil } from '../helpers.js';
12
15
 
16
+ /**
17
+ * Reconstruct allFiles from DB file_hashes + journal deltas.
18
+ * Returns null when the fast path isn't applicable (first build, no journal, etc).
19
+ */
20
+ function tryFastCollect(
21
+ ctx: PipelineContext,
22
+ ): { files: string[]; directories: Set<string> } | null {
23
+ const { db, rootDir } = ctx;
24
+
25
+ // 1. Check that file_hashes table exists and has entries
26
+ let dbFileCount: number;
27
+ try {
28
+ dbFileCount = (db.prepare('SELECT COUNT(*) as c FROM file_hashes').get() as { c: number }).c;
29
+ } catch {
30
+ return null;
31
+ }
32
+ if (dbFileCount === 0) return null;
33
+
34
+ // 2. Read the journal — only use fast path when journal has entries,
35
+ // proving the watcher was active and tracking changes. An empty-but-valid
36
+ // journal (no watcher) could miss file deletions.
37
+ const journal = readJournal(rootDir);
38
+ if (!journal.valid) return null;
39
+ const hasEntries =
40
+ (journal.changed && journal.changed.length > 0) ||
41
+ (journal.removed && journal.removed.length > 0);
42
+ if (!hasEntries) return null;
43
+
44
+ // 3. Load existing file list from file_hashes (relative paths)
45
+ const dbFiles = (db.prepare('SELECT file FROM file_hashes').all() as Array<{ file: string }>).map(
46
+ (r) => r.file,
47
+ );
48
+
49
+ // 4. Apply journal deltas: remove deleted files, add new/changed files
50
+ const fileSet = new Set(dbFiles);
51
+ if (journal.removed) {
52
+ for (const removed of journal.removed) {
53
+ fileSet.delete(removed);
54
+ }
55
+ }
56
+ if (journal.changed) {
57
+ for (const changed of journal.changed) {
58
+ fileSet.add(changed);
59
+ }
60
+ }
61
+
62
+ // 5. Convert to absolute paths and compute directories
63
+ const files: string[] = [];
64
+ const directories = new Set<string>();
65
+ for (const relPath of fileSet) {
66
+ const absPath = path.join(rootDir, relPath);
67
+ files.push(absPath);
68
+ directories.add(path.dirname(absPath));
69
+ }
70
+
71
+ debug(
72
+ `collectFiles fast path: ${dbFiles.length} from DB, journal: +${journal.changed?.length ?? 0}/-${journal.removed?.length ?? 0} → ${files.length} files`,
73
+ );
74
+ return { files, directories };
75
+ }
76
+
13
77
  export async function collectFiles(ctx: PipelineContext): Promise<void> {
14
78
  const { rootDir, config, opts } = ctx;
15
79
 
@@ -33,10 +97,23 @@ export async function collectFiles(ctx: PipelineContext): Promise<void> {
33
97
  ctx.removed = missing;
34
98
  ctx.isFullBuild = false;
35
99
  info(`Scoped rebuild: ${existing.length} files to rebuild, ${missing.length} to purge`);
36
- } else {
37
- const collected = collectFilesUtil(rootDir, [], config, new Set<string>());
38
- ctx.allFiles = collected.files;
39
- ctx.discoveredDirs = collected.directories;
40
- info(`Found ${ctx.allFiles.length} files to parse`);
100
+ return;
41
101
  }
102
+
103
+ // Incremental fast path: reconstruct file list from DB + journal deltas
104
+ // instead of full recursive filesystem scan (~8ms savings on 473 files).
105
+ if (ctx.incremental && !ctx.forceFullRebuild) {
106
+ const fast = tryFastCollect(ctx);
107
+ if (fast) {
108
+ ctx.allFiles = fast.files;
109
+ ctx.discoveredDirs = fast.directories;
110
+ info(`Found ${ctx.allFiles.length} files (cached)`);
111
+ return;
112
+ }
113
+ }
114
+
115
+ const collected = collectFilesUtil(rootDir, [], config, new Set<string>());
116
+ ctx.allFiles = collected.files;
117
+ ctx.discoveredDirs = collected.directories;
118
+ info(`Found ${ctx.allFiles.length} files to parse`);
42
119
  }
@@ -10,7 +10,7 @@ import path from 'node:path';
10
10
  import { closeDb } from '../../../../db/index.js';
11
11
  import { debug, info } from '../../../../infrastructure/logger.js';
12
12
  import { normalizePath } from '../../../../shared/constants.js';
13
- import type { BetterSqlite3Database, ExtractorOutput } from '../../../../types.js';
13
+ import type { BetterSqlite3Database, ExtractorOutput, NativeDatabase } from '../../../../types.js';
14
14
  import { parseFilesAuto } from '../../../parser.js';
15
15
  import { readJournal, writeJournalHeader } from '../../journal.js';
16
16
  import type { PipelineContext } from '../context.js';
@@ -58,10 +58,16 @@ function getChangedFiles(
58
58
  db: BetterSqlite3Database,
59
59
  allFiles: string[],
60
60
  rootDir: string,
61
+ nativeDb?: NativeDatabase,
61
62
  ): ChangeResult {
62
63
  let hasTable = false;
63
64
  try {
64
- db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
65
+ if (nativeDb) {
66
+ nativeDb.queryGet('SELECT 1 FROM file_hashes LIMIT 1', []);
67
+ } else {
68
+ db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
69
+ }
70
+ // Query succeeded → table exists (result may be undefined if table is empty)
65
71
  hasTable = true;
66
72
  } catch {
67
73
  /* table doesn't exist */
@@ -75,11 +81,11 @@ function getChangedFiles(
75
81
  };
76
82
  }
77
83
 
78
- const existing = new Map<string, FileHashRow>(
79
- (db.prepare('SELECT file, hash, mtime, size FROM file_hashes').all() as FileHashRow[]).map(
80
- (r) => [r.file, r],
81
- ),
82
- );
84
+ const sql = 'SELECT file, hash, mtime, size FROM file_hashes';
85
+ const rows = nativeDb
86
+ ? (nativeDb.queryAll(sql, []) as unknown as FileHashRow[])
87
+ : (db.prepare(sql).all() as FileHashRow[]);
88
+ const existing = new Map<string, FileHashRow>(rows.map((r) => [r.file, r]));
83
89
 
84
90
  const removed = detectRemovedFiles(existing, allFiles, rootDir);
85
91
  const journalResult = tryJournalTier(db, existing, rootDir, removed);
@@ -325,21 +331,33 @@ function purgeAndAddReverseDeps(
325
331
  reverseDeps: Set<string>,
326
332
  ): void {
327
333
  const { db, rootDir } = ctx;
328
- if (changePaths.length > 0 || ctx.removed.length > 0) {
329
- purgeFilesFromGraph(db, [...ctx.removed, ...changePaths], { purgeHashes: false });
330
- }
331
- if (reverseDeps.size > 0) {
332
- const deleteOutgoingEdgesForFile = db.prepare(
333
- 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
334
- );
335
- for (const relPath of reverseDeps) {
336
- deleteOutgoingEdgesForFile.run(relPath);
337
- }
338
- for (const relPath of reverseDeps) {
339
- const absPath = path.join(rootDir, relPath);
340
- ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
334
+ const hasPurge = changePaths.length > 0 || ctx.removed.length > 0;
335
+ const hasReverseDeps = reverseDeps.size > 0;
336
+ const reverseDepList = hasReverseDeps ? [...reverseDeps] : [];
337
+
338
+ if (hasPurge || hasReverseDeps) {
339
+ const filesToPurge = hasPurge ? [...ctx.removed, ...changePaths] : [];
340
+ // Prefer NativeDatabase: purge + reverse-dep edge deletion in one transaction (#670)
341
+ if (ctx.engineName === 'native' && ctx.nativeDb?.purgeFilesData) {
342
+ ctx.nativeDb.purgeFilesData(filesToPurge, false, hasReverseDeps ? reverseDepList : undefined);
343
+ } else {
344
+ if (hasPurge) {
345
+ purgeFilesFromGraph(db, filesToPurge, { purgeHashes: false });
346
+ }
347
+ if (hasReverseDeps) {
348
+ const deleteOutgoingEdgesForFile = db.prepare(
349
+ 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
350
+ );
351
+ for (const relPath of reverseDepList) {
352
+ deleteOutgoingEdgesForFile.run(relPath);
353
+ }
354
+ }
341
355
  }
342
356
  }
357
+ for (const relPath of reverseDeps) {
358
+ const absPath = path.join(rootDir, relPath);
359
+ ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
360
+ }
343
361
  }
344
362
 
345
363
  function detectHasEmbeddings(db: BetterSqlite3Database): boolean {
@@ -415,7 +433,12 @@ export async function detectChanges(ctx: PipelineContext): Promise<void> {
415
433
  }
416
434
  const increResult =
417
435
  incremental && !forceFullRebuild
418
- ? getChangedFiles(db, allFiles, rootDir)
436
+ ? getChangedFiles(
437
+ db,
438
+ allFiles,
439
+ rootDir,
440
+ ctx.engineName === 'native' ? ctx.nativeDb : undefined,
441
+ )
419
442
  : {
420
443
  changed: allFiles.map((f): ChangedFile => ({ file: f })),
421
444
  removed: [] as string[],
@@ -6,18 +6,19 @@
6
6
  import { tmpdir } from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { performance } from 'node:perf_hooks';
9
- import { closeDb, closeDbDeferred, getBuildMeta, setBuildMeta } from '../../../../db/index.js';
9
+ import {
10
+ closeDbPair,
11
+ closeDbPairDeferred,
12
+ getBuildMeta,
13
+ setBuildMeta,
14
+ } from '../../../../db/index.js';
10
15
  import { debug, info, warn } from '../../../../infrastructure/logger.js';
11
16
  import { CODEGRAPH_VERSION } from '../../../../shared/version.js';
12
17
  import { writeJournalHeader } from '../../journal.js';
13
18
  import type { PipelineContext } from '../context.js';
14
19
 
15
- export async function finalize(ctx: PipelineContext): Promise<void> {
16
- const { db, allSymbols, rootDir, isFullBuild, hasEmbeddings, config, opts, schemaVersion } = ctx;
17
-
18
- const t0 = performance.now();
19
-
20
- // Release cached WASM trees
20
+ /** Release cached WASM parse trees to free memory. */
21
+ function releaseWasmTrees(allSymbols: PipelineContext['allSymbols']): void {
21
22
  for (const [, symbols] of allSymbols) {
22
23
  const tree = symbols._tree as { delete?: () => void } | undefined;
23
24
  if (tree && typeof tree.delete === 'function') {
@@ -30,113 +31,141 @@ export async function finalize(ctx: PipelineContext): Promise<void> {
30
31
  symbols._tree = undefined;
31
32
  symbols._langId = undefined;
32
33
  }
34
+ }
33
35
 
34
- // Capture a single wall-clock timestamp for the current build — used for
35
- // both the stale-embeddings comparison and the persisted built_at metadata.
36
- const buildNow = new Date();
36
+ /**
37
+ * Detect significant drift between current and previous node/edge counts.
38
+ * Skipped for small incremental changes where count fluctuation is expected.
39
+ */
40
+ function detectIncrementalDrift(
41
+ ctx: PipelineContext,
42
+ nodeCount: number,
43
+ actualEdgeCount: number,
44
+ ): void {
45
+ const { db, allSymbols, config } = ctx;
46
+ const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
47
+ if (ctx.isFullBuild || allSymbols.size <= 3) return;
37
48
 
38
- const nodeCount = (db.prepare('SELECT COUNT(*) as c FROM nodes').get() as { c: number }).c;
39
- const actualEdgeCount = (db.prepare('SELECT COUNT(*) as c FROM edges').get() as { c: number }).c;
40
- info(`Graph built: ${nodeCount} nodes, ${actualEdgeCount} edges`);
41
- info(`Stored in ${ctx.dbPath}`);
49
+ const prevNodes = useNativeDb
50
+ ? ctx.nativeDb!.getBuildMeta('node_count')
51
+ : getBuildMeta(db, 'node_count');
52
+ const prevEdges = useNativeDb
53
+ ? ctx.nativeDb!.getBuildMeta('edge_count')
54
+ : getBuildMeta(db, 'edge_count');
55
+ if (!prevNodes || !prevEdges) return;
42
56
 
43
- // Incremental drift detection — skip for small incremental changes where
44
- // count fluctuation is expected (reverse-dep edge churn).
45
- if (!isFullBuild && allSymbols.size > 3) {
46
- const prevNodes = getBuildMeta(db, 'node_count');
47
- const prevEdges = getBuildMeta(db, 'edge_count');
48
- if (prevNodes && prevEdges) {
49
- const prevN = Number(prevNodes);
50
- const prevE = Number(prevEdges);
51
- if (prevN > 0) {
52
- const nodeDrift = Math.abs(nodeCount - prevN) / prevN;
53
- const edgeDrift = prevE > 0 ? Math.abs(actualEdgeCount - prevE) / prevE : 0;
54
- const driftThreshold =
55
- (config as { build?: { driftThreshold?: number } }).build?.driftThreshold ?? 0.2;
56
- if (nodeDrift > driftThreshold || edgeDrift > driftThreshold) {
57
- warn(
58
- `Incremental build diverged significantly from previous counts (nodes: ${prevN}\u2192${nodeCount} [${(nodeDrift * 100).toFixed(1)}%], edges: ${prevE}\u2192${actualEdgeCount} [${(edgeDrift * 100).toFixed(1)}%], threshold: ${(driftThreshold * 100).toFixed(0)}%). Consider rebuilding with --no-incremental.`,
59
- );
60
- }
61
- }
62
- }
57
+ const prevN = Number(prevNodes);
58
+ const prevE = Number(prevEdges);
59
+ if (prevN <= 0) return;
60
+
61
+ const nodeDrift = Math.abs(nodeCount - prevN) / prevN;
62
+ const edgeDrift = prevE > 0 ? Math.abs(actualEdgeCount - prevE) / prevE : 0;
63
+ const driftThreshold =
64
+ (config as { build?: { driftThreshold?: number } }).build?.driftThreshold ?? 0.2;
65
+ if (nodeDrift > driftThreshold || edgeDrift > driftThreshold) {
66
+ warn(
67
+ `Incremental build diverged significantly from previous counts (nodes: ${prevN}\u2192${nodeCount} [${(nodeDrift * 100).toFixed(1)}%], edges: ${prevE}\u2192${actualEdgeCount} [${(edgeDrift * 100).toFixed(1)}%], threshold: ${(driftThreshold * 100).toFixed(0)}%). Consider rebuilding with --no-incremental.`,
68
+ );
63
69
  }
70
+ }
64
71
 
65
- // For small incremental builds, skip persisting build metadata — the
66
- // engine/version/schema haven't changed (would have triggered a full rebuild),
67
- // built_at is only used by stale-embeddings check (skipped for incremental),
68
- // and counts are only used by drift detection (skipped for ≤3 files).
69
- // This avoids a transaction commit + WAL fsync (~15-30ms).
70
- if (isFullBuild || allSymbols.size > 5) {
71
- try {
72
- setBuildMeta(db, {
72
+ /**
73
+ * Persist build metadata (engine, version, counts, timestamp).
74
+ * Skipped for small incremental builds to avoid WAL fsync cost.
75
+ */
76
+ function persistBuildMetadata(
77
+ ctx: PipelineContext,
78
+ nodeCount: number,
79
+ actualEdgeCount: number,
80
+ buildNow: Date,
81
+ ): void {
82
+ const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
83
+ if (!ctx.isFullBuild && ctx.allSymbols.size <= 3) return;
84
+ try {
85
+ if (useNativeDb) {
86
+ ctx.nativeDb!.setBuildMeta(
87
+ Object.entries({
88
+ engine: ctx.engineName,
89
+ engine_version: ctx.engineVersion || '',
90
+ codegraph_version: CODEGRAPH_VERSION,
91
+ schema_version: String(ctx.schemaVersion),
92
+ built_at: buildNow.toISOString(),
93
+ node_count: String(nodeCount),
94
+ edge_count: String(actualEdgeCount),
95
+ }).map(([key, value]) => ({ key, value: String(value) })),
96
+ );
97
+ } else {
98
+ setBuildMeta(ctx.db, {
73
99
  engine: ctx.engineName,
74
100
  engine_version: ctx.engineVersion || '',
75
101
  codegraph_version: CODEGRAPH_VERSION,
76
- schema_version: String(schemaVersion),
102
+ schema_version: String(ctx.schemaVersion),
77
103
  built_at: buildNow.toISOString(),
78
104
  node_count: nodeCount,
79
105
  edge_count: actualEdgeCount,
80
106
  });
81
- } catch (err) {
82
- warn(`Failed to write build metadata: ${(err as Error).message}`);
83
107
  }
108
+ } catch (err) {
109
+ warn(`Failed to write build metadata: ${(err as Error).message}`);
84
110
  }
111
+ }
85
112
 
86
- // Skip expensive advisory queries for incremental builds — these are
87
- // informational warnings that don't affect correctness and cost ~40-60ms.
88
- if (!isFullBuild) {
89
- debug(
90
- 'Finalize: skipping advisory queries (orphaned/stale embeddings, unused exports) for incremental build',
91
- );
92
- } else {
93
- // Orphaned embeddings warning
94
- if (hasEmbeddings) {
95
- try {
96
- const orphaned = (
97
- db
98
- .prepare(
99
- 'SELECT COUNT(*) as c FROM embeddings WHERE node_id NOT IN (SELECT id FROM nodes)',
100
- )
101
- .get() as { c: number }
102
- ).c;
103
- if (orphaned > 0) {
104
- warn(
105
- `${orphaned} embeddings are orphaned (nodes changed). Run "codegraph embed" to refresh.`,
106
- );
107
- }
108
- } catch {
109
- /* ignore - embeddings table may have been dropped */
113
+ /**
114
+ * Run advisory checks on full builds: orphaned embeddings, stale embeddings,
115
+ * and unused exports. Informational only — does not affect correctness.
116
+ */
117
+ function runAdvisoryChecks(
118
+ db: PipelineContext['db'],
119
+ hasEmbeddings: boolean,
120
+ buildNow: Date,
121
+ ): void {
122
+ // Orphaned embeddings warning
123
+ if (hasEmbeddings) {
124
+ try {
125
+ const orphaned = (
126
+ db
127
+ .prepare(
128
+ 'SELECT COUNT(*) as c FROM embeddings WHERE node_id NOT IN (SELECT id FROM nodes)',
129
+ )
130
+ .get() as { c: number }
131
+ ).c;
132
+ if (orphaned > 0) {
133
+ warn(
134
+ `${orphaned} embeddings are orphaned (nodes changed). Run "codegraph embed" to refresh.`,
135
+ );
110
136
  }
137
+ } catch {
138
+ /* ignore - embeddings table may have been dropped */
111
139
  }
140
+ }
112
141
 
113
- // Stale embeddings warning (built before current graph rebuild)
114
- if (hasEmbeddings) {
115
- try {
116
- const embedBuiltAt = (
117
- db.prepare("SELECT value FROM embedding_meta WHERE key = 'built_at'").get() as
118
- | { value: string }
119
- | undefined
120
- )?.value;
121
- if (embedBuiltAt) {
122
- const embedTime = new Date(embedBuiltAt).getTime();
123
- if (!Number.isNaN(embedTime) && embedTime < buildNow.getTime()) {
124
- warn(
125
- 'Embeddings were built before the last graph rebuild. Run "codegraph embed" to update.',
126
- );
127
- }
142
+ // Stale embeddings warning (built before current graph rebuild)
143
+ if (hasEmbeddings) {
144
+ try {
145
+ const embedBuiltAt = (
146
+ db.prepare("SELECT value FROM embedding_meta WHERE key = 'built_at'").get() as
147
+ | { value: string }
148
+ | undefined
149
+ )?.value;
150
+ if (embedBuiltAt) {
151
+ const embedTime = new Date(embedBuiltAt).getTime();
152
+ if (!Number.isNaN(embedTime) && embedTime < buildNow.getTime()) {
153
+ warn(
154
+ 'Embeddings were built before the last graph rebuild. Run "codegraph embed" to update.',
155
+ );
128
156
  }
129
- } catch {
130
- /* ignore - embedding_meta table may not exist */
131
157
  }
158
+ } catch {
159
+ /* ignore - embedding_meta table may not exist */
132
160
  }
161
+ }
133
162
 
134
- // Unused exports warning
135
- try {
136
- const unusedCount = (
137
- db
138
- .prepare(
139
- `SELECT COUNT(*) as c FROM nodes
163
+ // Unused exports warning
164
+ try {
165
+ const unusedCount = (
166
+ db
167
+ .prepare(
168
+ `SELECT COUNT(*) as c FROM nodes
140
169
  WHERE exported = 1 AND kind != 'file'
141
170
  AND id NOT IN (
142
171
  SELECT DISTINCT e.target_id FROM edges e
@@ -144,30 +173,65 @@ export async function finalize(ctx: PipelineContext): Promise<void> {
144
173
  JOIN nodes target ON e.target_id = target.id
145
174
  WHERE e.kind = 'calls' AND caller.file != target.file
146
175
  )`,
147
- )
148
- .get() as { c: number }
149
- ).c;
150
- if (unusedCount > 0) {
151
- warn(
152
- `${unusedCount} exported symbol${unusedCount > 1 ? 's have' : ' has'} zero cross-file consumers. Run "codegraph exports <file> --unused" to inspect.`,
153
- );
154
- }
155
- } catch {
156
- /* exported column may not exist on older DBs */
176
+ )
177
+ .get() as { c: number }
178
+ ).c;
179
+ if (unusedCount > 0) {
180
+ warn(
181
+ `${unusedCount} exported symbol${unusedCount > 1 ? 's have' : ' has'} zero cross-file consumers. Run "codegraph exports <file> --unused" to inspect.`,
182
+ );
157
183
  }
184
+ } catch {
185
+ /* exported column may not exist on older DBs */
186
+ }
187
+ }
188
+
189
+ export async function finalize(ctx: PipelineContext): Promise<void> {
190
+ const { allSymbols, rootDir, isFullBuild, hasEmbeddings, opts } = ctx;
191
+
192
+ const t0 = performance.now();
193
+
194
+ releaseWasmTrees(allSymbols);
195
+
196
+ // Capture a single wall-clock timestamp for the current build — used for
197
+ // both the stale-embeddings comparison and the persisted built_at metadata.
198
+ const buildNow = new Date();
199
+
200
+ const nodeCount = (ctx.db.prepare('SELECT COUNT(*) as c FROM nodes').get() as { c: number }).c;
201
+ const actualEdgeCount = (ctx.db.prepare('SELECT COUNT(*) as c FROM edges').get() as { c: number })
202
+ .c;
203
+ info(`Graph built: ${nodeCount} nodes, ${actualEdgeCount} edges`);
204
+ info(`Stored in ${ctx.dbPath}`);
205
+
206
+ detectIncrementalDrift(ctx, nodeCount, actualEdgeCount);
207
+ persistBuildMetadata(ctx, nodeCount, actualEdgeCount, buildNow);
208
+
209
+ // Skip expensive advisory queries for incremental builds — these are
210
+ // informational warnings that don't affect correctness and cost ~40-60ms.
211
+ if (!isFullBuild) {
212
+ debug(
213
+ 'Finalize: skipping advisory queries (orphaned/stale embeddings, unused exports) for incremental build',
214
+ );
215
+ } else {
216
+ runAdvisoryChecks(ctx.db, hasEmbeddings, buildNow);
158
217
  }
159
218
 
219
+ // Intentionally measured before closeDb / writeJournalHeader / auto-registration:
220
+ // for the deferred-close path the close is async (setImmediate), and for full
221
+ // builds the metric captures finalize logic only — DB close cost is tracked
222
+ // separately via timing.closeDbMs when available.
160
223
  ctx.timing.finalizeMs = performance.now() - t0;
161
224
 
162
- // For small incremental builds, defer db.close() to the next event loop tick.
163
- // The WAL checkpoint in db.close() costs ~250ms on Windows NTFS due to fsync.
164
- // Deferring lets buildGraph() return immediately; the checkpoint runs after.
165
- // Skip for temp directories (tests) — they rmSync immediately after build.
225
+ // Close NativeDatabase (fast, ~1ms) then better-sqlite3 (WAL checkpoint).
226
+ // For small incremental builds, defer the expensive WAL checkpoint to the
227
+ // next event loop tick. Skip for temp directories (tests) — they rmSync
228
+ // immediately after build.
229
+ const pair = { db: ctx.db, nativeDb: ctx.nativeDb };
166
230
  const isTempDir = path.resolve(rootDir).startsWith(path.resolve(tmpdir()));
167
231
  if (!isFullBuild && allSymbols.size <= 5 && !isTempDir) {
168
- closeDbDeferred(db);
232
+ closeDbPairDeferred(pair);
169
233
  } else {
170
- closeDb(db);
234
+ closeDbPair(pair);
171
235
  }
172
236
 
173
237
  // Write journal header after successful build
@@ -177,7 +241,6 @@ export async function finalize(ctx: PipelineContext): Promise<void> {
177
241
  // registered during the initial full build. The dynamic import + file I/O
178
242
  // costs ~100ms which dominates incremental finalize time.
179
243
  if (!opts.skipRegistry && isFullBuild) {
180
- const { tmpdir } = await import('node:os');
181
244
  const tmpDir = path.resolve(tmpdir());
182
245
  const resolvedRoot = path.resolve(rootDir);
183
246
  if (resolvedRoot.startsWith(tmpDir)) {