@optave/codegraph 3.8.0 → 3.9.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 +13 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +137 -86
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +81 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/branch-compare.d.ts.map +1 -1
  28. package/dist/cli/commands/branch-compare.js +4 -0
  29. package/dist/cli/commands/branch-compare.js.map +1 -1
  30. package/dist/cli/commands/diff-impact.d.ts.map +1 -1
  31. package/dist/cli/commands/diff-impact.js +2 -1
  32. package/dist/cli/commands/diff-impact.js.map +1 -1
  33. package/dist/cli/commands/info.d.ts.map +1 -1
  34. package/dist/cli/commands/info.js +3 -2
  35. package/dist/cli/commands/info.js.map +1 -1
  36. package/dist/cli/commands/watch.d.ts.map +1 -1
  37. package/dist/cli/commands/watch.js +16 -2
  38. package/dist/cli/commands/watch.js.map +1 -1
  39. package/dist/db/connection.d.ts.map +1 -1
  40. package/dist/db/connection.js +29 -26
  41. package/dist/db/connection.js.map +1 -1
  42. package/dist/db/query-builder.d.ts.map +1 -1
  43. package/dist/db/query-builder.js +16 -5
  44. package/dist/db/query-builder.js.map +1 -1
  45. package/dist/db/repository/base.d.ts +16 -0
  46. package/dist/db/repository/base.d.ts.map +1 -1
  47. package/dist/db/repository/base.js +31 -0
  48. package/dist/db/repository/base.js.map +1 -1
  49. package/dist/db/repository/native-repository.d.ts +7 -1
  50. package/dist/db/repository/native-repository.d.ts.map +1 -1
  51. package/dist/db/repository/native-repository.js +100 -1
  52. package/dist/db/repository/native-repository.js.map +1 -1
  53. package/dist/db/repository/nodes.d.ts.map +1 -1
  54. package/dist/db/repository/nodes.js +8 -4
  55. package/dist/db/repository/nodes.js.map +1 -1
  56. package/dist/db/repository/sqlite-repository.d.ts +4 -0
  57. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  58. package/dist/db/repository/sqlite-repository.js +51 -0
  59. package/dist/db/repository/sqlite-repository.js.map +1 -1
  60. package/dist/domain/analysis/brief.d.ts.map +1 -1
  61. package/dist/domain/analysis/brief.js +13 -17
  62. package/dist/domain/analysis/brief.js.map +1 -1
  63. package/dist/domain/analysis/context.d.ts.map +1 -1
  64. package/dist/domain/analysis/context.js +14 -11
  65. package/dist/domain/analysis/context.js.map +1 -1
  66. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  67. package/dist/domain/analysis/dependencies.js +64 -59
  68. package/dist/domain/analysis/dependencies.js.map +1 -1
  69. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  70. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  71. package/dist/domain/analysis/fn-impact.js +33 -31
  72. package/dist/domain/analysis/fn-impact.js.map +1 -1
  73. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  74. package/dist/domain/analysis/implementations.js +11 -19
  75. package/dist/domain/analysis/implementations.js.map +1 -1
  76. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  77. package/dist/domain/analysis/module-map.js +55 -76
  78. package/dist/domain/analysis/module-map.js.map +1 -1
  79. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  80. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  81. package/dist/domain/analysis/query-helpers.js +15 -1
  82. package/dist/domain/analysis/query-helpers.js.map +1 -1
  83. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/pipeline.js +352 -107
  85. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/build-edges.js +49 -18
  88. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  90. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  91. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  92. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  93. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  94. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  95. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  96. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  97. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  98. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  99. package/dist/domain/graph/cycles.d.ts +6 -0
  100. package/dist/domain/graph/cycles.d.ts.map +1 -1
  101. package/dist/domain/graph/cycles.js +114 -22
  102. package/dist/domain/graph/cycles.js.map +1 -1
  103. package/dist/domain/graph/resolve.js +1 -1
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts +2 -0
  106. package/dist/domain/graph/watcher.d.ts.map +1 -1
  107. package/dist/domain/graph/watcher.js +170 -75
  108. package/dist/domain/graph/watcher.js.map +1 -1
  109. package/dist/domain/parser.d.ts +3 -4
  110. package/dist/domain/parser.d.ts.map +1 -1
  111. package/dist/domain/parser.js +141 -89
  112. package/dist/domain/parser.js.map +1 -1
  113. package/dist/domain/search/generator.js +1 -1
  114. package/dist/domain/search/generator.js.map +1 -1
  115. package/dist/domain/search/models.d.ts +4 -3
  116. package/dist/domain/search/models.d.ts.map +1 -1
  117. package/dist/domain/search/models.js +23 -8
  118. package/dist/domain/search/models.js.map +1 -1
  119. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  120. package/dist/domain/search/search/hybrid.js +29 -18
  121. package/dist/domain/search/search/hybrid.js.map +1 -1
  122. package/dist/extractors/go.js +36 -33
  123. package/dist/extractors/go.js.map +1 -1
  124. package/dist/extractors/helpers.d.ts.map +1 -1
  125. package/dist/extractors/helpers.js +40 -29
  126. package/dist/extractors/helpers.js.map +1 -1
  127. package/dist/extractors/java.js +58 -46
  128. package/dist/extractors/java.js.map +1 -1
  129. package/dist/extractors/javascript.js +65 -54
  130. package/dist/extractors/javascript.js.map +1 -1
  131. package/dist/extractors/kotlin.js +84 -78
  132. package/dist/extractors/kotlin.js.map +1 -1
  133. package/dist/extractors/python.js +29 -24
  134. package/dist/extractors/python.js.map +1 -1
  135. package/dist/extractors/rust.js +41 -32
  136. package/dist/extractors/rust.js.map +1 -1
  137. package/dist/extractors/solidity.js +58 -67
  138. package/dist/extractors/solidity.js.map +1 -1
  139. package/dist/extractors/swift.js +83 -81
  140. package/dist/extractors/swift.js.map +1 -1
  141. package/dist/extractors/zig.js +58 -60
  142. package/dist/extractors/zig.js.map +1 -1
  143. package/dist/features/ast.d.ts +16 -14
  144. package/dist/features/ast.d.ts.map +1 -1
  145. package/dist/features/ast.js +83 -81
  146. package/dist/features/ast.js.map +1 -1
  147. package/dist/features/audit.d.ts.map +1 -1
  148. package/dist/features/audit.js +8 -6
  149. package/dist/features/audit.js.map +1 -1
  150. package/dist/features/branch-compare.d.ts.map +1 -1
  151. package/dist/features/branch-compare.js +69 -72
  152. package/dist/features/branch-compare.js.map +1 -1
  153. package/dist/features/communities.d.ts.map +1 -1
  154. package/dist/features/communities.js +19 -7
  155. package/dist/features/communities.js.map +1 -1
  156. package/dist/features/complexity.d.ts.map +1 -1
  157. package/dist/features/complexity.js +120 -125
  158. package/dist/features/complexity.js.map +1 -1
  159. package/dist/features/dataflow.d.ts.map +1 -1
  160. package/dist/features/dataflow.js +136 -137
  161. package/dist/features/dataflow.js.map +1 -1
  162. package/dist/features/flow.d.ts.map +1 -1
  163. package/dist/features/flow.js +84 -79
  164. package/dist/features/flow.js.map +1 -1
  165. package/dist/features/structure-query.d.ts.map +1 -1
  166. package/dist/features/structure-query.js +69 -65
  167. package/dist/features/structure-query.js.map +1 -1
  168. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  169. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  170. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  171. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  172. package/dist/graph/algorithms/leiden/partition.js +288 -266
  173. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  174. package/dist/graph/model.d.ts.map +1 -1
  175. package/dist/graph/model.js +5 -1
  176. package/dist/graph/model.js.map +1 -1
  177. package/dist/infrastructure/config.d.ts.map +1 -1
  178. package/dist/infrastructure/config.js +6 -4
  179. package/dist/infrastructure/config.js.map +1 -1
  180. package/dist/infrastructure/suppress.d.ts +25 -0
  181. package/dist/infrastructure/suppress.d.ts.map +1 -0
  182. package/dist/infrastructure/suppress.js +43 -0
  183. package/dist/infrastructure/suppress.js.map +1 -0
  184. package/dist/mcp/server.d.ts.map +1 -1
  185. package/dist/mcp/server.js +29 -24
  186. package/dist/mcp/server.js.map +1 -1
  187. package/dist/presentation/dataflow.d.ts.map +1 -1
  188. package/dist/presentation/dataflow.js +47 -38
  189. package/dist/presentation/dataflow.js.map +1 -1
  190. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  191. package/dist/presentation/diff-impact-mermaid.js +60 -51
  192. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  193. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/exports.js +20 -14
  195. package/dist/presentation/queries-cli/exports.js.map +1 -1
  196. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  197. package/dist/presentation/queries-cli/impact.js +15 -13
  198. package/dist/presentation/queries-cli/impact.js.map +1 -1
  199. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  200. package/dist/presentation/queries-cli/inspect.js +101 -79
  201. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  202. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  203. package/dist/presentation/queries-cli/overview.js +25 -16
  204. package/dist/presentation/queries-cli/overview.js.map +1 -1
  205. package/dist/presentation/queries-cli/path.js +26 -20
  206. package/dist/presentation/queries-cli/path.js.map +1 -1
  207. package/dist/presentation/result-formatter.d.ts +10 -0
  208. package/dist/presentation/result-formatter.d.ts.map +1 -1
  209. package/dist/presentation/result-formatter.js +16 -1
  210. package/dist/presentation/result-formatter.js.map +1 -1
  211. package/dist/presentation/viewer.d.ts.map +1 -1
  212. package/dist/presentation/viewer.js +18 -12
  213. package/dist/presentation/viewer.js.map +1 -1
  214. package/dist/shared/errors.d.ts +5 -0
  215. package/dist/shared/errors.d.ts.map +1 -1
  216. package/dist/shared/errors.js +5 -0
  217. package/dist/shared/errors.js.map +1 -1
  218. package/dist/shared/hierarchy.d.ts +8 -2
  219. package/dist/shared/hierarchy.d.ts.map +1 -1
  220. package/dist/shared/hierarchy.js +42 -1
  221. package/dist/shared/hierarchy.js.map +1 -1
  222. package/dist/shared/normalize.d.ts +6 -1
  223. package/dist/shared/normalize.d.ts.map +1 -1
  224. package/dist/shared/normalize.js +20 -12
  225. package/dist/shared/normalize.js.map +1 -1
  226. package/dist/shared/paginate.d.ts +0 -9
  227. package/dist/shared/paginate.d.ts.map +1 -1
  228. package/dist/shared/paginate.js +0 -15
  229. package/dist/shared/paginate.js.map +1 -1
  230. package/dist/types.d.ts +12 -5
  231. package/dist/types.d.ts.map +1 -1
  232. package/grammars/tree-sitter-erlang.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +9 -9
  235. package/src/ast-analysis/engine.ts +176 -104
  236. package/src/ast-analysis/metrics.ts +33 -11
  237. package/src/ast-analysis/shared.ts +33 -24
  238. package/src/ast-analysis/visitor-utils.ts +52 -32
  239. package/src/ast-analysis/visitor.ts +132 -71
  240. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  241. package/src/ast-analysis/visitors/complexity-visitor.ts +89 -40
  242. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  243. package/src/cli/commands/branch-compare.ts +4 -0
  244. package/src/cli/commands/diff-impact.ts +2 -1
  245. package/src/cli/commands/info.ts +3 -2
  246. package/src/cli/commands/watch.ts +16 -2
  247. package/src/db/connection.ts +29 -28
  248. package/src/db/query-builder.ts +15 -3
  249. package/src/db/repository/base.ts +34 -0
  250. package/src/db/repository/native-repository.ts +104 -1
  251. package/src/db/repository/nodes.ts +13 -8
  252. package/src/db/repository/sqlite-repository.ts +55 -0
  253. package/src/domain/analysis/brief.ts +15 -25
  254. package/src/domain/analysis/context.ts +17 -10
  255. package/src/domain/analysis/dependencies.ts +77 -81
  256. package/src/domain/analysis/fn-impact.ts +36 -43
  257. package/src/domain/analysis/implementations.ts +11 -17
  258. package/src/domain/analysis/module-map.ts +58 -92
  259. package/src/domain/analysis/query-helpers.ts +18 -1
  260. package/src/domain/graph/builder/pipeline.ts +409 -99
  261. package/src/domain/graph/builder/stages/build-edges.ts +45 -19
  262. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  263. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  264. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  265. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  266. package/src/domain/graph/cycles.ts +110 -23
  267. package/src/domain/graph/resolve.ts +1 -1
  268. package/src/domain/graph/watcher.ts +202 -96
  269. package/src/domain/parser.ts +143 -89
  270. package/src/domain/search/generator.ts +1 -1
  271. package/src/domain/search/models.ts +26 -7
  272. package/src/domain/search/search/hybrid.ts +69 -51
  273. package/src/extractors/go.ts +43 -33
  274. package/src/extractors/helpers.ts +37 -23
  275. package/src/extractors/java.ts +66 -47
  276. package/src/extractors/javascript.ts +66 -54
  277. package/src/extractors/kotlin.ts +84 -77
  278. package/src/extractors/python.ts +31 -25
  279. package/src/extractors/rust.ts +37 -29
  280. package/src/extractors/solidity.ts +57 -61
  281. package/src/extractors/swift.ts +81 -80
  282. package/src/extractors/zig.ts +58 -61
  283. package/src/features/ast.ts +130 -110
  284. package/src/features/audit.ts +8 -6
  285. package/src/features/branch-compare.ts +105 -79
  286. package/src/features/communities.ts +25 -10
  287. package/src/features/complexity.ts +171 -134
  288. package/src/features/dataflow.ts +165 -175
  289. package/src/features/flow.ts +129 -92
  290. package/src/features/structure-query.ts +79 -64
  291. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  292. package/src/graph/algorithms/leiden/partition.ts +359 -294
  293. package/src/graph/model.ts +6 -1
  294. package/src/infrastructure/config.ts +6 -4
  295. package/src/infrastructure/suppress.ts +47 -0
  296. package/src/mcp/server.ts +53 -37
  297. package/src/presentation/dataflow.ts +50 -44
  298. package/src/presentation/diff-impact-mermaid.ts +104 -62
  299. package/src/presentation/queries-cli/exports.ts +21 -13
  300. package/src/presentation/queries-cli/impact.ts +15 -13
  301. package/src/presentation/queries-cli/inspect.ts +100 -81
  302. package/src/presentation/queries-cli/overview.ts +26 -16
  303. package/src/presentation/queries-cli/path.ts +33 -25
  304. package/src/presentation/result-formatter.ts +19 -1
  305. package/src/presentation/viewer.ts +42 -14
  306. package/src/shared/errors.ts +6 -0
  307. package/src/shared/hierarchy.ts +50 -2
  308. package/src/shared/normalize.ts +31 -12
  309. package/src/shared/paginate.ts +0 -17
  310. package/src/types.ts +26 -5
@@ -6,12 +6,21 @@
6
6
  */
7
7
  import path from 'node:path';
8
8
  import { performance } from 'node:perf_hooks';
9
- import { closeDbPair, getBuildMeta, initSchema, MIGRATIONS, openDb } from '../../../db/index.js';
9
+ import {
10
+ closeDbPair,
11
+ getBuildMeta,
12
+ initSchema,
13
+ MIGRATIONS,
14
+ openDb,
15
+ setBuildMeta,
16
+ } from '../../../db/index.js';
10
17
  import { detectWorkspaces, loadConfig } from '../../../infrastructure/config.js';
11
- import { info, warn } from '../../../infrastructure/logger.js';
18
+ import { debug, info, warn } from '../../../infrastructure/logger.js';
12
19
  import { loadNative } from '../../../infrastructure/native.js';
20
+ import { semverCompare } from '../../../infrastructure/update-check.js';
21
+ import { toErrorMessage } from '../../../shared/errors.js';
13
22
  import { CODEGRAPH_VERSION } from '../../../shared/version.js';
14
- import type { BuildGraphOpts, BuildResult } from '../../../types.js';
23
+ import type { BuildGraphOpts, BuildResult, Definition, ExtractorOutput } from '../../../types.js';
15
24
  import { getActiveEngine } from '../../parser.js';
16
25
  import { setWorkspaces } from '../resolve.js';
17
26
  import { PipelineContext } from './context.js';
@@ -50,8 +59,10 @@ function initializeEngine(ctx: PipelineContext): void {
50
59
  ? () => {
51
60
  try {
52
61
  ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
53
- } catch {
54
- /* ignore — nativeDb may already be closed */
62
+ } catch (e) {
63
+ debug(
64
+ `resumeJsDb: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`,
65
+ );
55
66
  }
56
67
  }
57
68
  : undefined,
@@ -123,7 +134,9 @@ function setupPipeline(ctx: PipelineContext): void {
123
134
  // Use NativeDatabase for schema init when native engine is available (Phase 6.13).
124
135
  // better-sqlite3 (ctx.db) is still always opened — needed for queries and stages
125
136
  // that haven't been migrated to rusqlite yet.
126
- const native = loadNative();
137
+ // Skip native DB entirely when user explicitly requested --engine wasm.
138
+ const enginePref = ctx.opts.engine || 'auto';
139
+ const native = enginePref !== 'wasm' ? loadNative() : null;
127
140
  if (native?.NativeDatabase) {
128
141
  try {
129
142
  ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
@@ -132,11 +145,11 @@ function setupPipeline(ctx: PipelineContext): void {
132
145
  // with no cross-library WAL frames (#715, #717).
133
146
  ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
134
147
  } catch (err) {
135
- warn(`NativeDatabase setup failed, falling back to JS: ${(err as Error).message}`);
148
+ warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
136
149
  try {
137
150
  ctx.nativeDb?.close();
138
- } catch {
139
- /* ignore close errors */
151
+ } catch (e) {
152
+ debug(`setupNativeDb: close failed during fallback: ${toErrorMessage(e)}`);
140
153
  }
141
154
  ctx.nativeDb = undefined;
142
155
  }
@@ -185,6 +198,59 @@ function formatTimingResult(ctx: PipelineContext): BuildResult {
185
198
  };
186
199
  }
187
200
 
201
+ // ── NativeDb lifecycle helpers ──────────────────────────────────────────
202
+
203
+ /** Checkpoint WAL through rusqlite and close the native connection. */
204
+ function closeNativeDb(ctx: PipelineContext, label: string): void {
205
+ if (!ctx.nativeDb) return;
206
+ try {
207
+ ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
208
+ } catch (e) {
209
+ debug(`${label} WAL checkpoint failed: ${toErrorMessage(e)}`);
210
+ }
211
+ try {
212
+ ctx.nativeDb.close();
213
+ } catch (e) {
214
+ debug(`${label} nativeDb close failed: ${toErrorMessage(e)}`);
215
+ }
216
+ ctx.nativeDb = undefined;
217
+ }
218
+
219
+ /** Try to reopen the native connection for a given pipeline phase. */
220
+ function reopenNativeDb(ctx: PipelineContext, label: string): void {
221
+ if ((ctx.opts.engine ?? 'auto') === 'wasm') return;
222
+ const native = loadNative();
223
+ if (!native?.NativeDatabase) return;
224
+ try {
225
+ ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
226
+ } catch (e) {
227
+ debug(`reopen nativeDb for ${label} failed: ${toErrorMessage(e)}`);
228
+ ctx.nativeDb = undefined;
229
+ }
230
+ }
231
+
232
+ /** Close nativeDb and clear stale references in engineOpts. */
233
+ function suspendNativeDb(ctx: PipelineContext, label: string): void {
234
+ closeNativeDb(ctx, label);
235
+ if (ctx.engineOpts?.nativeDb) {
236
+ ctx.engineOpts.nativeDb = undefined;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * After native writes, reopen the JS db connection to get a fresh page cache.
242
+ * Rusqlite WAL truncation invalidates better-sqlite3's internal WAL index,
243
+ * causing SQLITE_CORRUPT on the next read (#715, #736).
244
+ */
245
+ function refreshJsDb(ctx: PipelineContext): void {
246
+ try {
247
+ ctx.db.close();
248
+ } catch (e) {
249
+ debug(`refreshJsDb close failed: ${toErrorMessage(e)}`);
250
+ }
251
+ ctx.db = openDb(ctx.dbPath);
252
+ }
253
+
188
254
  // ── Pipeline stages execution ───────────────────────────────────────────
189
255
 
190
256
  async function runPipelineStages(ctx: PipelineContext): Promise<void> {
@@ -195,26 +261,7 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
195
261
  // that use suspendJsDb/resumeJsDb WAL checkpoint pattern (#696).
196
262
  const hadNativeDb = !!ctx.nativeDb;
197
263
  if (ctx.db && ctx.nativeDb) {
198
- // Checkpoint WAL through rusqlite before closing so better-sqlite3 never
199
- // needs to apply WAL frames written by a different SQLite library (#715, #717).
200
- // Separate try/catch blocks ensure close() always runs even if checkpoint throws,
201
- // preventing a live rusqlite connection from lingering until GC.
202
- try {
203
- ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
204
- } catch {
205
- /* ignore checkpoint errors */
206
- }
207
- try {
208
- ctx.nativeDb.close();
209
- } catch {
210
- /* ignore close errors */
211
- }
212
- ctx.nativeDb = undefined;
213
- // Also clear stale reference in engineOpts to prevent stages from
214
- // calling methods on the closed NativeDatabase.
215
- if (ctx.engineOpts?.nativeDb) {
216
- ctx.engineOpts.nativeDb = undefined;
217
- }
264
+ suspendNativeDb(ctx, 'pre-collect');
218
265
  }
219
266
 
220
267
  await collectFiles(ctx);
@@ -228,44 +275,15 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
228
275
  // guard internally (same pattern as feature modules). Closed again before
229
276
  // resolveImports/buildEdges which don't yet have the guard (#709).
230
277
  if (hadNativeDb && ctx.engineName === 'native') {
231
- const native = loadNative();
232
- if (native?.NativeDatabase) {
233
- try {
234
- ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
235
- } catch {
236
- ctx.nativeDb = undefined;
237
- }
238
- }
278
+ reopenNativeDb(ctx, 'insertNodes');
239
279
  }
240
280
 
241
281
  await insertNodes(ctx);
242
282
 
243
283
  // Close nativeDb after insertNodes — remaining pipeline stages use JS paths.
244
284
  if (ctx.nativeDb && ctx.db) {
245
- // Checkpoint WAL through rusqlite before closing so better-sqlite3 never
246
- // needs to apply WAL frames written by a different SQLite library (#715, #717).
247
- try {
248
- ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
249
- } catch {
250
- /* ignore checkpoint errors */
251
- }
252
- try {
253
- ctx.nativeDb.close();
254
- } catch {
255
- /* ignore close errors */
256
- }
257
- ctx.nativeDb = undefined;
258
- // Reopen better-sqlite3 connection to get a fresh page cache.
259
- // After rusqlite truncates the WAL, better-sqlite3's internal WAL index
260
- // (shared-memory mapping) may reference frames that no longer exist,
261
- // causing SQLITE_CORRUPT on the next read. Closing and reopening
262
- // forces a clean slate — the only reliable cross-library handoff (#715, #736).
263
- try {
264
- ctx.db.close();
265
- } catch {
266
- /* ignore close errors */
267
- }
268
- ctx.db = openDb(ctx.dbPath);
285
+ closeNativeDb(ctx, 'post-insertNodes');
286
+ refreshJsDb(ctx);
269
287
  }
270
288
 
271
289
  await resolveImports(ctx);
@@ -275,41 +293,23 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
275
293
  // Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow)
276
294
  // which use suspendJsDb/resumeJsDb WAL checkpoint before native writes.
277
295
  if (hadNativeDb) {
278
- const native = loadNative();
279
- if (native?.NativeDatabase) {
280
- try {
281
- ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
282
- if (ctx.engineOpts) {
283
- ctx.engineOpts.nativeDb = ctx.nativeDb;
284
- }
285
- } catch {
286
- ctx.nativeDb = undefined;
287
- if (ctx.engineOpts) {
288
- ctx.engineOpts.nativeDb = undefined;
289
- }
290
- }
296
+ reopenNativeDb(ctx, 'analyses');
297
+ if (ctx.nativeDb && ctx.engineOpts) {
298
+ ctx.engineOpts.nativeDb = ctx.nativeDb;
299
+ }
300
+ if (!ctx.nativeDb && ctx.engineOpts) {
301
+ ctx.engineOpts.nativeDb = undefined;
291
302
  }
292
303
  }
293
304
 
294
305
  await runAnalyses(ctx);
295
306
 
296
- // Close nativeDb after analyses finalize uses JS paths for setBuildMeta
297
- // and closeDbPair handles cleanup. Avoids dual-connection during finalize.
307
+ // Keep nativeDb open through finalize so persistBuildMetadata, advisory
308
+ // checks, and count queries use the native path. closeDbPair inside
309
+ // finalize handles both connections. Refresh the JS db so it has a
310
+ // valid page cache in case finalize falls back to JS paths (#751).
298
311
  if (ctx.nativeDb) {
299
- // Checkpoint WAL through rusqlite before closing so better-sqlite3 never
300
- // needs to apply WAL frames written by a different SQLite library (#715, #717).
301
- // Separate try/catch blocks ensure close() always runs even if checkpoint throws.
302
- try {
303
- ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
304
- } catch {
305
- /* ignore checkpoint errors */
306
- }
307
- try {
308
- ctx.nativeDb.close();
309
- } catch {
310
- /* ignore close errors */
311
- }
312
- ctx.nativeDb = undefined;
312
+ refreshJsDb(ctx);
313
313
  }
314
314
 
315
315
  await finalize(ctx);
@@ -338,7 +338,31 @@ export async function buildGraph(
338
338
  // When available, run the entire build pipeline in Rust with zero
339
339
  // napi crossings (eliminates WAL dual-connection dance). Falls back
340
340
  // to the JS pipeline on failure or when native is unavailable.
341
- const forceJs = process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1';
341
+ //
342
+ // Native addon ≤3.8.0 has a path bug: file_symbols keys are absolute
343
+ // paths but known_files are relative, causing zero import/call edges.
344
+ // Native addon ≤3.8.1 has an incremental barrel bug: the Rust pipeline
345
+ // doesn't re-parse barrel files that are imported by changed files,
346
+ // causing missing barrel import edges and lost analysis data for
347
+ // reverse-dep files during incremental builds.
348
+ // Skip the orchestrator for affected versions (fixed in 3.9.0+).
349
+ const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.8.1') <= 0;
350
+ const forceJs =
351
+ process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1' ||
352
+ ctx.forceFullRebuild ||
353
+ orchestratorBuggy ||
354
+ ctx.engineName !== 'native';
355
+ if (forceJs) {
356
+ const reason =
357
+ process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1'
358
+ ? 'CODEGRAPH_FORCE_JS_PIPELINE=1'
359
+ : ctx.forceFullRebuild
360
+ ? 'forceFullRebuild'
361
+ : orchestratorBuggy
362
+ ? `buggy addon ${ctx.engineVersion}`
363
+ : `engine=${ctx.engineName}`;
364
+ debug(`Skipping native orchestrator: ${reason}`);
365
+ }
342
366
  if (!forceJs && ctx.nativeDb?.buildGraph) {
343
367
  try {
344
368
  const resultJson = ctx.nativeDb.buildGraph(
@@ -353,21 +377,307 @@ export async function buildGraph(
353
377
  nodeCount?: number;
354
378
  edgeCount?: number;
355
379
  fileCount?: number;
380
+ changedFiles?: string[];
381
+ changedCount?: number;
382
+ removedCount?: number;
383
+ isFullBuild?: boolean;
356
384
  };
357
385
 
358
386
  if (result.earlyExit) {
387
+ info('No changes detected');
359
388
  closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
360
389
  return;
361
390
  }
362
391
 
392
+ // Log incremental status to match JS pipeline output
393
+ const changed = result.changedCount ?? 0;
394
+ const removed = result.removedCount ?? 0;
395
+ if (!result.isFullBuild && (changed > 0 || removed > 0)) {
396
+ info(`Incremental: ${changed} changed, ${removed} removed`);
397
+ }
398
+
363
399
  // Map Rust timing fields to the JS BuildResult format.
364
400
  // Rust handles collect+detect+parse+insert+resolve+edges+structure+roles.
365
- // AST/complexity/CFG/dataflow analyses are not yet ported to Rust.
366
401
  const p = result.phases;
367
- closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
402
+
403
+ // Sync build_meta so JS-side version/engine checks work on next build.
404
+ // Note: the Rust orchestrator also writes codegraph_version (using
405
+ // CARGO_PKG_VERSION). We intentionally overwrite it here with the npm
406
+ // package version so that the JS-side "version changed → full rebuild"
407
+ // detection (line ~97) compares against the authoritative JS version.
408
+ // The two versions are kept in lockstep by the release process.
409
+ setBuildMeta(ctx.db, {
410
+ engine: ctx.engineName,
411
+ engine_version: ctx.engineVersion || '',
412
+ codegraph_version: CODEGRAPH_VERSION,
413
+ schema_version: String(ctx.schemaVersion),
414
+ built_at: new Date().toISOString(),
415
+ node_count: String(result.nodeCount ?? 0),
416
+ edge_count: String(result.edgeCount ?? 0),
417
+ });
418
+
368
419
  info(
369
420
  `Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`,
370
421
  );
422
+
423
+ // ── Run structure + analysis phases after native orchestrator ──
424
+ // Structure (directory nodes, contains edges, metrics) is not fully
425
+ // ported to Rust — the native pipeline only handles the small
426
+ // incremental fast path (≤5 changed files). For full builds and
427
+ // larger incremental builds, run JS buildStructure() to fill the gap.
428
+ // Analysis phases (AST, complexity, CFG, dataflow) are also not yet
429
+ // ported; run via JS engine after reconstructing fileSymbols from DB.
430
+ let analysisTiming = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
431
+ let structurePatchMs = 0;
432
+ const needsAnalysis =
433
+ opts.ast !== false ||
434
+ opts.complexity !== false ||
435
+ opts.cfg !== false ||
436
+ opts.dataflow !== false;
437
+
438
+ // The native fast path only runs structure for small incremental
439
+ // builds: !isFullBuild && changedCount <= 5 && existingFileCount > 20.
440
+ // For all other cases (full builds, large incrementals), we must
441
+ // run JS buildStructure() to create directory nodes + contains edges (#804).
442
+ // Always run JS structure — the native fast-path has an additional
443
+ // existingFileCount > 20 guard that isn't reflected in the result JSON,
444
+ // so we can't reliably detect whether native actually ran structure.
445
+ const nativeHandledStructure = false;
446
+ const needsStructure = !nativeHandledStructure;
447
+
448
+ if (needsAnalysis || needsStructure) {
449
+ // WAL handoff: checkpoint through rusqlite, close nativeDb,
450
+ // reopen better-sqlite3 with a fresh page cache (#715, #736).
451
+ try {
452
+ ctx.nativeDb!.exec('PRAGMA wal_checkpoint(TRUNCATE)');
453
+ } catch {
454
+ /* ignore checkpoint errors */
455
+ }
456
+ try {
457
+ ctx.nativeDb!.close();
458
+ } catch {
459
+ /* ignore close errors */
460
+ }
461
+ ctx.nativeDb = undefined;
462
+ try {
463
+ ctx.db.close();
464
+ } catch {
465
+ /* ignore close errors */
466
+ }
467
+ ctx.db = null!; // avoid closeDbPair operating on a stale handle
468
+ try {
469
+ ctx.db = openDb(ctx.dbPath);
470
+ } catch (reopenErr) {
471
+ warn(`Failed to reopen DB after native build: ${(reopenErr as Error).message}`);
472
+ // Native build succeeded but we can't run post-processing — return partial result
473
+ return {
474
+ phases: {
475
+ setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
476
+ parseMs: +(p.parseMs ?? 0).toFixed(1),
477
+ insertMs: +(p.insertMs ?? 0).toFixed(1),
478
+ resolveMs: +(p.resolveMs ?? 0).toFixed(1),
479
+ edgesMs: +(p.edgesMs ?? 0).toFixed(1),
480
+ structureMs: +(p.structureMs ?? 0).toFixed(1),
481
+ rolesMs: +(p.rolesMs ?? 0).toFixed(1),
482
+ astMs: 0,
483
+ complexityMs: 0,
484
+ cfgMs: 0,
485
+ dataflowMs: 0,
486
+ finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
487
+ },
488
+ };
489
+ }
490
+
491
+ // Reconstruct fileSymbols from DB. For structure we need ALL files
492
+ // (to build complete directory tree); for analysis we scope to
493
+ // changed files only. Load all files, then scope analysis later.
494
+ const allFileRows = ctx.db
495
+ .prepare(
496
+ 'SELECT file, name, kind, line, end_line as endLine FROM nodes WHERE file IS NOT NULL ORDER BY file, line',
497
+ )
498
+ .all() as {
499
+ file: string;
500
+ name: string;
501
+ kind: string;
502
+ line: number;
503
+ endLine: number | null;
504
+ }[];
505
+
506
+ const allFileSymbols = new Map<string, ExtractorOutput>();
507
+ for (const row of allFileRows) {
508
+ let entry = allFileSymbols.get(row.file);
509
+ if (!entry) {
510
+ entry = {
511
+ definitions: [],
512
+ calls: [],
513
+ imports: [],
514
+ classes: [],
515
+ exports: [],
516
+ typeMap: new Map(),
517
+ };
518
+ allFileSymbols.set(row.file, entry);
519
+ }
520
+ entry.definitions.push({
521
+ name: row.name,
522
+ kind: row.kind as Definition['kind'],
523
+ line: row.line,
524
+ endLine: row.endLine ?? undefined,
525
+ });
526
+ }
527
+
528
+ // Populate import/export counts from DB edges so buildStructure
529
+ // computes correct import_count/export_count in node_metrics.
530
+ // The extractor arrays aren't persisted to the DB, so we derive
531
+ // counts from edge data instead (#804).
532
+ const importCountRows = ctx.db
533
+ .prepare(
534
+ `SELECT n.file, COUNT(*) AS cnt
535
+ FROM edges e JOIN nodes n ON e.source_id = n.id
536
+ WHERE e.kind IN ('imports', 'imports-type', 'dynamic-imports')
537
+ AND n.file IS NOT NULL
538
+ GROUP BY n.file`,
539
+ )
540
+ .all() as { file: string; cnt: number }[];
541
+ for (const row of importCountRows) {
542
+ const entry = allFileSymbols.get(row.file);
543
+ if (entry) entry.imports = new Array(row.cnt) as ExtractorOutput['imports'];
544
+ }
545
+ // Export count: definitions in this file that are imported by other files
546
+ const exportCountRows = ctx.db
547
+ .prepare(
548
+ `SELECT n_tgt.file, COUNT(DISTINCT n_tgt.id) AS cnt
549
+ FROM edges e
550
+ JOIN nodes n_tgt ON e.target_id = n_tgt.id
551
+ JOIN nodes n_src ON e.source_id = n_src.id
552
+ WHERE e.kind IN ('imports', 'imports-type', 'reexports')
553
+ AND n_tgt.file IS NOT NULL
554
+ AND n_src.file != n_tgt.file
555
+ GROUP BY n_tgt.file`,
556
+ )
557
+ .all() as { file: string; cnt: number }[];
558
+ for (const row of exportCountRows) {
559
+ const entry = allFileSymbols.get(row.file);
560
+ if (entry) entry.exports = new Array(row.cnt) as ExtractorOutput['exports'];
561
+ }
562
+
563
+ // ── Structure phase: directory nodes + contains edges (#804) ──
564
+ if (needsStructure) {
565
+ const structureStart = performance.now();
566
+ try {
567
+ // Derive directories from file paths
568
+ const directories = new Set<string>();
569
+ for (const relPath of allFileSymbols.keys()) {
570
+ const parts = relPath.split('/');
571
+ for (let i = 1; i < parts.length; i++) {
572
+ directories.add(parts.slice(0, i).join('/'));
573
+ }
574
+ }
575
+
576
+ // Build line count map from DB metrics or file content
577
+ const lineCountMap = new Map<string, number>();
578
+ const cachedLineCounts = ctx.db
579
+ .prepare(
580
+ `SELECT n.name AS file, m.line_count
581
+ FROM node_metrics m JOIN nodes n ON m.node_id = n.id
582
+ WHERE n.kind = 'file'`,
583
+ )
584
+ .all() as Array<{ file: string; line_count: number }>;
585
+ for (const row of cachedLineCounts) {
586
+ lineCountMap.set(row.file, row.line_count);
587
+ }
588
+
589
+ // Native ran no structure at all — always do a full rebuild so
590
+ // every directory gets nodes + contains edges (#804).
591
+ const changedFilePaths = null;
592
+
593
+ const { buildStructure: buildStructureFn } = (await import(
594
+ '../../../features/structure.js'
595
+ )) as {
596
+ buildStructure: (
597
+ db: typeof ctx.db,
598
+ fileSymbols: Map<string, ExtractorOutput>,
599
+ rootDir: string,
600
+ lineCountMap: Map<string, number>,
601
+ directories: Set<string>,
602
+ changedFiles: string[] | null,
603
+ ) => void;
604
+ };
605
+ buildStructureFn(
606
+ ctx.db,
607
+ allFileSymbols,
608
+ ctx.rootDir,
609
+ lineCountMap,
610
+ directories,
611
+ changedFilePaths,
612
+ );
613
+ debug('Structure phase completed after native orchestrator');
614
+ } catch (err) {
615
+ warn(`Structure phase failed after native build: ${toErrorMessage(err)}`);
616
+ }
617
+ structurePatchMs = performance.now() - structureStart;
618
+ }
619
+
620
+ // ── Analysis phase ──
621
+ if (needsAnalysis) {
622
+ // Scope analysis fileSymbols to changed files only
623
+ const changedFiles = result.changedFiles;
624
+ let analysisFileSymbols: Map<string, ExtractorOutput>;
625
+ if (changedFiles && changedFiles.length > 0) {
626
+ analysisFileSymbols = new Map();
627
+ for (const f of changedFiles) {
628
+ const entry = allFileSymbols.get(f);
629
+ if (entry) analysisFileSymbols.set(f, entry);
630
+ }
631
+ } else {
632
+ analysisFileSymbols = allFileSymbols;
633
+ }
634
+
635
+ // Reopen nativeDb for analysis features (suspend/resume WAL pattern).
636
+ const native = loadNative();
637
+ if (native?.NativeDatabase) {
638
+ try {
639
+ ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
640
+ if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
641
+ } catch {
642
+ ctx.nativeDb = undefined;
643
+ if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
644
+ }
645
+ }
646
+
647
+ try {
648
+ const { runAnalyses: runAnalysesFn } = await import(
649
+ '../../../ast-analysis/engine.js'
650
+ );
651
+ analysisTiming = await runAnalysesFn(
652
+ ctx.db,
653
+ analysisFileSymbols,
654
+ ctx.rootDir,
655
+ opts,
656
+ ctx.engineOpts,
657
+ );
658
+ } catch (err) {
659
+ warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
660
+ }
661
+
662
+ // Close nativeDb after analyses
663
+ if (ctx.nativeDb) {
664
+ try {
665
+ ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
666
+ } catch {
667
+ /* ignore checkpoint errors */
668
+ }
669
+ try {
670
+ ctx.nativeDb.close();
671
+ } catch {
672
+ /* ignore close errors */
673
+ }
674
+ ctx.nativeDb = undefined;
675
+ if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
676
+ }
677
+ }
678
+ }
679
+
680
+ closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
371
681
  return {
372
682
  phases: {
373
683
  setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
@@ -375,18 +685,18 @@ export async function buildGraph(
375
685
  insertMs: +(p.insertMs ?? 0).toFixed(1),
376
686
  resolveMs: +(p.resolveMs ?? 0).toFixed(1),
377
687
  edgesMs: +(p.edgesMs ?? 0).toFixed(1),
378
- structureMs: +(p.structureMs ?? 0).toFixed(1),
688
+ structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
379
689
  rolesMs: +(p.rolesMs ?? 0).toFixed(1),
380
- astMs: 0,
381
- complexityMs: 0,
382
- cfgMs: 0,
383
- dataflowMs: 0,
690
+ astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
691
+ complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
692
+ cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
693
+ dataflowMs: +(analysisTiming.dataflowMs ?? 0).toFixed(1),
384
694
  finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
385
695
  },
386
696
  };
387
697
  } catch (err) {
388
698
  warn(
389
- `Native build orchestrator failed, falling back to JS pipeline: ${(err as Error).message}`,
699
+ `Native build orchestrator failed, falling back to JS pipeline: ${toErrorMessage(err)}`,
390
700
  );
391
701
  // Fall through to JS pipeline
392
702
  }