@optave/codegraph 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/README.md +35 -14
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +119 -127
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +14 -1
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  10. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  11. package/dist/db/connection.d.ts +12 -2
  12. package/dist/db/connection.d.ts.map +1 -1
  13. package/dist/db/connection.js +81 -53
  14. package/dist/db/connection.js.map +1 -1
  15. package/dist/db/index.d.ts +1 -1
  16. package/dist/db/index.d.ts.map +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/db/migrations.d.ts.map +1 -1
  20. package/dist/db/migrations.js +38 -32
  21. package/dist/db/migrations.js.map +1 -1
  22. package/dist/domain/analysis/context.d.ts.map +1 -1
  23. package/dist/domain/analysis/context.js +51 -66
  24. package/dist/domain/analysis/context.js.map +1 -1
  25. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  26. package/dist/domain/analysis/dependencies.js +62 -70
  27. package/dist/domain/analysis/dependencies.js.map +1 -1
  28. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  29. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  30. package/dist/domain/analysis/exports.d.ts.map +1 -1
  31. package/dist/domain/analysis/exports.js +29 -33
  32. package/dist/domain/analysis/exports.js.map +1 -1
  33. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  34. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  35. package/dist/domain/analysis/fn-impact.js +35 -65
  36. package/dist/domain/analysis/fn-impact.js.map +1 -1
  37. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  38. package/dist/domain/analysis/module-map.js +91 -6
  39. package/dist/domain/analysis/module-map.js.map +1 -1
  40. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  41. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  42. package/dist/domain/analysis/query-helpers.js +27 -0
  43. package/dist/domain/analysis/query-helpers.js.map +1 -0
  44. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/helpers.js +15 -9
  46. package/dist/domain/graph/builder/helpers.js.map +1 -1
  47. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/incremental.js +3 -2
  49. package/dist/domain/graph/builder/incremental.js.map +1 -1
  50. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/pipeline.js +69 -3
  52. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  54. package/dist/domain/graph/builder/stages/build-edges.js +7 -51
  55. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/build-structure.js +7 -5
  58. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/collect-files.js +2 -2
  60. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  63. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/finalize.js +124 -105
  66. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  68. package/dist/domain/graph/builder/stages/insert-nodes.js +28 -15
  69. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  71. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  72. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  73. package/dist/domain/graph/resolve.d.ts +0 -4
  74. package/dist/domain/graph/resolve.d.ts.map +1 -1
  75. package/dist/domain/graph/resolve.js +32 -48
  76. package/dist/domain/graph/resolve.js.map +1 -1
  77. package/dist/domain/graph/watcher.d.ts.map +1 -1
  78. package/dist/domain/graph/watcher.js +12 -12
  79. package/dist/domain/graph/watcher.js.map +1 -1
  80. package/dist/domain/parser.d.ts +1 -1
  81. package/dist/domain/parser.d.ts.map +1 -1
  82. package/dist/domain/parser.js +164 -101
  83. package/dist/domain/parser.js.map +1 -1
  84. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  85. package/dist/domain/search/search/cli-formatter.js +88 -83
  86. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  87. package/dist/extractors/bash.d.ts +6 -0
  88. package/dist/extractors/bash.d.ts.map +1 -0
  89. package/dist/extractors/bash.js +91 -0
  90. package/dist/extractors/bash.js.map +1 -0
  91. package/dist/extractors/c.d.ts +6 -0
  92. package/dist/extractors/c.d.ts.map +1 -0
  93. package/dist/extractors/c.js +204 -0
  94. package/dist/extractors/c.js.map +1 -0
  95. package/dist/extractors/cpp.d.ts +6 -0
  96. package/dist/extractors/cpp.d.ts.map +1 -0
  97. package/dist/extractors/cpp.js +283 -0
  98. package/dist/extractors/cpp.js.map +1 -0
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +42 -54
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/go.d.ts.map +1 -1
  103. package/dist/extractors/go.js +126 -130
  104. package/dist/extractors/go.js.map +1 -1
  105. package/dist/extractors/hcl.js +6 -6
  106. package/dist/extractors/hcl.js.map +1 -1
  107. package/dist/extractors/helpers.d.ts +32 -1
  108. package/dist/extractors/helpers.d.ts.map +1 -1
  109. package/dist/extractors/helpers.js +74 -0
  110. package/dist/extractors/helpers.js.map +1 -1
  111. package/dist/extractors/index.d.ts +6 -0
  112. package/dist/extractors/index.d.ts.map +1 -1
  113. package/dist/extractors/index.js +6 -0
  114. package/dist/extractors/index.js.map +1 -1
  115. package/dist/extractors/java.d.ts.map +1 -1
  116. package/dist/extractors/java.js +32 -47
  117. package/dist/extractors/java.js.map +1 -1
  118. package/dist/extractors/javascript.d.ts.map +1 -1
  119. package/dist/extractors/javascript.js +306 -292
  120. package/dist/extractors/javascript.js.map +1 -1
  121. package/dist/extractors/kotlin.d.ts +6 -0
  122. package/dist/extractors/kotlin.d.ts.map +1 -0
  123. package/dist/extractors/kotlin.js +275 -0
  124. package/dist/extractors/kotlin.js.map +1 -0
  125. package/dist/extractors/php.d.ts.map +1 -1
  126. package/dist/extractors/php.js +39 -44
  127. package/dist/extractors/php.js.map +1 -1
  128. package/dist/extractors/python.d.ts.map +1 -1
  129. package/dist/extractors/python.js +75 -93
  130. package/dist/extractors/python.js.map +1 -1
  131. package/dist/extractors/ruby.js +6 -13
  132. package/dist/extractors/ruby.js.map +1 -1
  133. package/dist/extractors/rust.d.ts.map +1 -1
  134. package/dist/extractors/rust.js +58 -83
  135. package/dist/extractors/rust.js.map +1 -1
  136. package/dist/extractors/scala.d.ts +6 -0
  137. package/dist/extractors/scala.d.ts.map +1 -0
  138. package/dist/extractors/scala.js +269 -0
  139. package/dist/extractors/scala.js.map +1 -0
  140. package/dist/extractors/swift.d.ts +6 -0
  141. package/dist/extractors/swift.d.ts.map +1 -0
  142. package/dist/extractors/swift.js +275 -0
  143. package/dist/extractors/swift.js.map +1 -0
  144. package/dist/features/ast.d.ts +2 -0
  145. package/dist/features/ast.d.ts.map +1 -1
  146. package/dist/features/ast.js +9 -24
  147. package/dist/features/ast.js.map +1 -1
  148. package/dist/features/audit.d.ts.map +1 -1
  149. package/dist/features/audit.js +17 -21
  150. package/dist/features/audit.js.map +1 -1
  151. package/dist/features/branch-compare.d.ts.map +1 -1
  152. package/dist/features/branch-compare.js +47 -3
  153. package/dist/features/branch-compare.js.map +1 -1
  154. package/dist/features/cfg.d.ts +7 -1
  155. package/dist/features/cfg.d.ts.map +1 -1
  156. package/dist/features/cfg.js +118 -62
  157. package/dist/features/cfg.js.map +1 -1
  158. package/dist/features/check.d.ts.map +1 -1
  159. package/dist/features/check.js +79 -62
  160. package/dist/features/check.js.map +1 -1
  161. package/dist/features/complexity-query.d.ts.map +1 -1
  162. package/dist/features/complexity-query.js +142 -137
  163. package/dist/features/complexity-query.js.map +1 -1
  164. package/dist/features/complexity.d.ts +7 -1
  165. package/dist/features/complexity.d.ts.map +1 -1
  166. package/dist/features/complexity.js +62 -1
  167. package/dist/features/complexity.js.map +1 -1
  168. package/dist/features/dataflow.d.ts +7 -1
  169. package/dist/features/dataflow.d.ts.map +1 -1
  170. package/dist/features/dataflow.js +356 -188
  171. package/dist/features/dataflow.js.map +1 -1
  172. package/dist/features/graph-enrichment.d.ts.map +1 -1
  173. package/dist/features/graph-enrichment.js +117 -104
  174. package/dist/features/graph-enrichment.js.map +1 -1
  175. package/dist/features/sequence.d.ts.map +1 -1
  176. package/dist/features/sequence.js +25 -4
  177. package/dist/features/sequence.js.map +1 -1
  178. package/dist/features/structure-query.d.ts.map +1 -1
  179. package/dist/features/structure-query.js +29 -4
  180. package/dist/features/structure-query.js.map +1 -1
  181. package/dist/features/structure.d.ts.map +1 -1
  182. package/dist/features/structure.js +35 -15
  183. package/dist/features/structure.js.map +1 -1
  184. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  185. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  186. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  187. package/dist/graph/algorithms/leiden/index.js +43 -28
  188. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  189. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  190. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  191. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  192. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  193. package/dist/graph/algorithms/leiden/partition.js +89 -106
  194. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  195. package/dist/graph/model.d.ts +2 -0
  196. package/dist/graph/model.d.ts.map +1 -1
  197. package/dist/graph/model.js +20 -8
  198. package/dist/graph/model.js.map +1 -1
  199. package/dist/infrastructure/config.d.ts +0 -8
  200. package/dist/infrastructure/config.d.ts.map +1 -1
  201. package/dist/infrastructure/config.js +73 -62
  202. package/dist/infrastructure/config.js.map +1 -1
  203. package/dist/infrastructure/registry.d.ts +0 -8
  204. package/dist/infrastructure/registry.d.ts.map +1 -1
  205. package/dist/infrastructure/registry.js +12 -14
  206. package/dist/infrastructure/registry.js.map +1 -1
  207. package/dist/mcp/server.d.ts.map +1 -1
  208. package/dist/mcp/server.js +45 -36
  209. package/dist/mcp/server.js.map +1 -1
  210. package/dist/presentation/audit.d.ts.map +1 -1
  211. package/dist/presentation/audit.js +61 -57
  212. package/dist/presentation/audit.js.map +1 -1
  213. package/dist/presentation/branch-compare.d.ts.map +1 -1
  214. package/dist/presentation/branch-compare.js +56 -38
  215. package/dist/presentation/branch-compare.js.map +1 -1
  216. package/dist/presentation/check.d.ts.map +1 -1
  217. package/dist/presentation/check.js +30 -32
  218. package/dist/presentation/check.js.map +1 -1
  219. package/dist/presentation/colors.d.ts.map +1 -1
  220. package/dist/presentation/colors.js +2 -0
  221. package/dist/presentation/colors.js.map +1 -1
  222. package/dist/presentation/complexity.d.ts.map +1 -1
  223. package/dist/presentation/complexity.js +25 -19
  224. package/dist/presentation/complexity.js.map +1 -1
  225. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  226. package/dist/presentation/queries-cli/exports.js +15 -15
  227. package/dist/presentation/queries-cli/exports.js.map +1 -1
  228. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  229. package/dist/presentation/queries-cli/impact.js +29 -19
  230. package/dist/presentation/queries-cli/impact.js.map +1 -1
  231. package/dist/types.d.ts +182 -7
  232. package/dist/types.d.ts.map +1 -1
  233. package/grammars/tree-sitter-bash.wasm +0 -0
  234. package/grammars/tree-sitter-c.wasm +0 -0
  235. package/grammars/tree-sitter-cpp.wasm +0 -0
  236. package/grammars/tree-sitter-kotlin.wasm +0 -0
  237. package/grammars/tree-sitter-scala.wasm +0 -0
  238. package/grammars/tree-sitter-swift.wasm +0 -0
  239. package/package.json +13 -7
  240. package/src/ast-analysis/engine.ts +147 -138
  241. package/src/ast-analysis/visitors/ast-store-visitor.ts +15 -2
  242. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  243. package/src/db/connection.ts +90 -59
  244. package/src/db/index.ts +1 -0
  245. package/src/db/migrations.ts +36 -32
  246. package/src/domain/analysis/context.ts +73 -75
  247. package/src/domain/analysis/dependencies.ts +78 -68
  248. package/src/domain/analysis/exports.ts +45 -34
  249. package/src/domain/analysis/fn-impact.ts +67 -64
  250. package/src/domain/analysis/module-map.ts +103 -8
  251. package/src/domain/analysis/query-helpers.ts +35 -0
  252. package/src/domain/graph/builder/helpers.ts +12 -6
  253. package/src/domain/graph/builder/incremental.ts +3 -2
  254. package/src/domain/graph/builder/pipeline.ts +71 -3
  255. package/src/domain/graph/builder/stages/build-edges.ts +10 -75
  256. package/src/domain/graph/builder/stages/build-structure.ts +9 -7
  257. package/src/domain/graph/builder/stages/collect-files.ts +2 -2
  258. package/src/domain/graph/builder/stages/detect-changes.ts +7 -2
  259. package/src/domain/graph/builder/stages/finalize.ts +159 -125
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +32 -21
  261. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  262. package/src/domain/graph/resolve.ts +34 -46
  263. package/src/domain/graph/watcher.ts +12 -14
  264. package/src/domain/parser.ts +168 -97
  265. package/src/domain/search/search/cli-formatter.ts +121 -94
  266. package/src/extractors/bash.ts +97 -0
  267. package/src/extractors/c.ts +212 -0
  268. package/src/extractors/cpp.ts +298 -0
  269. package/src/extractors/csharp.ts +53 -56
  270. package/src/extractors/go.ts +152 -134
  271. package/src/extractors/hcl.ts +6 -6
  272. package/src/extractors/helpers.ts +93 -1
  273. package/src/extractors/index.ts +6 -0
  274. package/src/extractors/java.ts +43 -48
  275. package/src/extractors/javascript.ts +328 -281
  276. package/src/extractors/kotlin.ts +293 -0
  277. package/src/extractors/php.ts +46 -40
  278. package/src/extractors/python.ts +81 -104
  279. package/src/extractors/ruby.ts +6 -13
  280. package/src/extractors/rust.ts +65 -85
  281. package/src/extractors/scala.ts +285 -0
  282. package/src/extractors/swift.ts +293 -0
  283. package/src/features/ast.ts +10 -25
  284. package/src/features/audit.ts +24 -20
  285. package/src/features/branch-compare.ts +51 -4
  286. package/src/features/cfg.ts +158 -65
  287. package/src/features/check.ts +90 -74
  288. package/src/features/complexity-query.ts +181 -163
  289. package/src/features/complexity.ts +64 -1
  290. package/src/features/dataflow.ts +462 -217
  291. package/src/features/graph-enrichment.ts +161 -117
  292. package/src/features/sequence.ts +27 -4
  293. package/src/features/structure-query.ts +43 -4
  294. package/src/features/structure.ts +50 -22
  295. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  296. package/src/graph/algorithms/leiden/index.ts +67 -28
  297. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  298. package/src/graph/algorithms/leiden/partition.ts +131 -98
  299. package/src/graph/model.ts +19 -7
  300. package/src/infrastructure/config.ts +60 -58
  301. package/src/infrastructure/registry.ts +17 -14
  302. package/src/mcp/server.ts +46 -37
  303. package/src/presentation/audit.ts +72 -67
  304. package/src/presentation/branch-compare.ts +54 -50
  305. package/src/presentation/check.ts +34 -34
  306. package/src/presentation/colors.ts +2 -0
  307. package/src/presentation/complexity.ts +39 -33
  308. package/src/presentation/queries-cli/exports.ts +17 -17
  309. package/src/presentation/queries-cli/impact.ts +30 -22
  310. package/src/types.ts +189 -7
@@ -4,13 +4,12 @@ import {
4
4
  findImplementors,
5
5
  findImportDependents,
6
6
  findNodeById,
7
- openReadonlyOrFail,
8
7
  } from '../../db/index.js';
9
- import { loadConfig } from '../../infrastructure/config.js';
10
8
  import { isTestFile } from '../../infrastructure/test-filter.js';
11
9
  import { normalizeSymbol } from '../../shared/normalize.js';
12
10
  import { paginateResult } from '../../shared/paginate.js';
13
11
  import type { BetterSqlite3Database, NodeRow, RelatedNodeRow } from '../../types.js';
12
+ import { resolveAnalysisOpts, withReadonlyDb } from './query-helpers.js';
14
13
  import { findMatchingNodes } from './symbol-lookup.js';
15
14
 
16
15
  // --- Shared BFS: transitive callers ---
@@ -36,6 +35,62 @@ function hasImplementsEdges(db: BetterSqlite3Database): boolean {
36
35
  * during traversal), its concrete implementors are also added to the frontier
37
36
  * so that changes to an interface signature propagate to all implementors.
38
37
  */
38
+ type BfsLevel = Array<{
39
+ name: string;
40
+ kind: string;
41
+ file: string;
42
+ line: number;
43
+ viaImplements?: boolean;
44
+ }>;
45
+ type BfsLevels = Record<number, BfsLevel>;
46
+ type BfsOnVisit = (
47
+ caller: RelatedNodeRow & { viaImplements?: boolean },
48
+ parentId: number,
49
+ depth: number,
50
+ ) => void;
51
+
52
+ /** Record an implementor node at the given depth, adding to frontier and levels. */
53
+ function recordImplementor(
54
+ impl: RelatedNodeRow,
55
+ parentId: number,
56
+ depth: number,
57
+ visited: Set<number>,
58
+ frontier: number[],
59
+ levels: BfsLevels,
60
+ noTests: boolean,
61
+ onVisit?: BfsOnVisit,
62
+ ): void {
63
+ if (visited.has(impl.id) || (noTests && isTestFile(impl.file))) return;
64
+ visited.add(impl.id);
65
+ frontier.push(impl.id);
66
+ if (!levels[depth]) levels[depth] = [];
67
+ levels[depth].push({
68
+ name: impl.name,
69
+ kind: impl.kind,
70
+ file: impl.file,
71
+ line: impl.line,
72
+ viaImplements: true,
73
+ });
74
+ if (onVisit) onVisit({ ...impl, viaImplements: true }, parentId, depth);
75
+ }
76
+
77
+ /** Expand implementors for an interface/trait node into the BFS frontier. */
78
+ function expandImplementors(
79
+ db: BetterSqlite3Database,
80
+ nodeId: number,
81
+ depth: number,
82
+ visited: Set<number>,
83
+ frontier: number[],
84
+ levels: BfsLevels,
85
+ noTests: boolean,
86
+ onVisit?: BfsOnVisit,
87
+ ): void {
88
+ const impls = findImplementors(db, nodeId) as RelatedNodeRow[];
89
+ for (const impl of impls) {
90
+ recordImplementor(impl, nodeId, depth, visited, frontier, levels, noTests, onVisit);
91
+ }
92
+ }
93
+
39
94
  export function bfsTransitiveCallers(
40
95
  db: BetterSqlite3Database,
41
96
  startId: number,
@@ -48,50 +103,24 @@ export function bfsTransitiveCallers(
48
103
  noTests?: boolean;
49
104
  maxDepth?: number;
50
105
  includeImplementors?: boolean;
51
- onVisit?: (
52
- caller: RelatedNodeRow & { viaImplements?: boolean },
53
- parentId: number,
54
- depth: number,
55
- ) => void;
106
+ onVisit?: BfsOnVisit;
56
107
  } = {},
57
108
  ) {
58
- // Skip all implementor lookups when the graph has no implements edges
59
109
  const resolveImplementors = includeImplementors && hasImplementsEdges(db);
60
-
61
110
  const visited = new Set([startId]);
62
- const levels: Record<
63
- number,
64
- Array<{ name: string; kind: string; file: string; line: number; viaImplements?: boolean }>
65
- > = {};
111
+ const levels: BfsLevels = {};
66
112
  let frontier = [startId];
67
113
 
68
- // Seed: if start node is an interface/trait, include its implementors at depth 1.
69
- // Implementors go into a separate list so their callers appear at depth 2, not depth 1.
114
+ // Seed: if start node is an interface/trait, include its implementors at depth 1
70
115
  const implNextFrontier: number[] = [];
71
116
  if (resolveImplementors) {
72
117
  const startNode = findNodeById(db, startId) as NodeRow | undefined;
73
118
  if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
74
- const impls = findImplementors(db, startId) as RelatedNodeRow[];
75
- for (const impl of impls) {
76
- if (!visited.has(impl.id) && (!noTests || !isTestFile(impl.file))) {
77
- visited.add(impl.id);
78
- implNextFrontier.push(impl.id);
79
- if (!levels[1]) levels[1] = [];
80
- levels[1].push({
81
- name: impl.name,
82
- kind: impl.kind,
83
- file: impl.file,
84
- line: impl.line,
85
- viaImplements: true,
86
- });
87
- if (onVisit) onVisit({ ...impl, viaImplements: true }, startId, 1);
88
- }
89
- }
119
+ expandImplementors(db, startId, 1, visited, implNextFrontier, levels, noTests, onVisit);
90
120
  }
91
121
  }
92
122
 
93
123
  for (let d = 1; d <= maxDepth; d++) {
94
- // On the first wave, merge seeded implementors so their callers appear at d=2
95
124
  if (d === 1 && implNextFrontier.length > 0) {
96
125
  frontier = [...frontier, ...implNextFrontier];
97
126
  }
@@ -106,27 +135,8 @@ export function bfsTransitiveCallers(
106
135
  levels[d]!.push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
107
136
  if (onVisit) onVisit(c, fid, d);
108
137
  }
109
-
110
- // If a caller is an interface/trait, also pull in its implementors
111
- // Implementors are one extra hop away, so record at d+1
112
138
  if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
113
- const impls = findImplementors(db, c.id) as RelatedNodeRow[];
114
- for (const impl of impls) {
115
- if (!visited.has(impl.id) && (!noTests || !isTestFile(impl.file))) {
116
- visited.add(impl.id);
117
- nextFrontier.push(impl.id);
118
- const implDepth = d + 1;
119
- if (!levels[implDepth]) levels[implDepth] = [];
120
- levels[implDepth].push({
121
- name: impl.name,
122
- kind: impl.kind,
123
- file: impl.file,
124
- line: impl.line,
125
- viaImplements: true,
126
- });
127
- if (onVisit) onVisit({ ...impl, viaImplements: true }, c.id, implDepth);
128
- }
129
- }
139
+ expandImplementors(db, c.id, d + 1, visited, nextFrontier, levels, noTests, onVisit);
130
140
  }
131
141
  }
132
142
  }
@@ -142,8 +152,7 @@ export function impactAnalysisData(
142
152
  customDbPath: string,
143
153
  opts: { noTests?: boolean } = {},
144
154
  ) {
145
- const db = openReadonlyOrFail(customDbPath);
146
- try {
155
+ return withReadonlyDb(customDbPath, (db) => {
147
156
  const noTests = opts.noTests || false;
148
157
  const fileNodes = findFileNodes(db, `%${file}%`) as NodeRow[];
149
158
  if (fileNodes.length === 0) {
@@ -187,9 +196,7 @@ export function impactAnalysisData(
187
196
  levels: byLevel,
188
197
  totalDependents: visited.size - fileNodes.length,
189
198
  };
190
- } finally {
191
- db.close();
192
- }
199
+ });
193
200
  }
194
201
 
195
202
  export function fnImpactData(
@@ -206,11 +213,9 @@ export function fnImpactData(
206
213
  config?: any;
207
214
  } = {},
208
215
  ) {
209
- const db = openReadonlyOrFail(customDbPath);
210
- try {
211
- const config = opts.config || loadConfig();
216
+ return withReadonlyDb(customDbPath, (db) => {
217
+ const { noTests, config } = resolveAnalysisOpts(opts);
212
218
  const maxDepth = opts.depth || config.analysis?.fnImpactDepth || 5;
213
- const noTests = opts.noTests || false;
214
219
  const hc = new Map();
215
220
 
216
221
  const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
@@ -235,7 +240,5 @@ export function fnImpactData(
235
240
 
236
241
  const base = { name, results };
237
242
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
238
- } finally {
239
- db.close();
240
- }
243
+ });
241
244
  }
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js';
2
+ import { openReadonlyOrFail, openReadonlyWithNative, testFilterSQL } from '../../db/index.js';
3
3
  import { cachedStmt } from '../../db/repository/cached-stmt.js';
4
4
  import { loadConfig } from '../../infrastructure/config.js';
5
5
  import { debug } from '../../infrastructure/logger.js';
@@ -381,20 +381,115 @@ export function moduleMapData(customDbPath: string, limit = 20, opts: { noTests?
381
381
  }
382
382
 
383
383
  export function statsData(customDbPath: string, opts: { noTests?: boolean; config?: any } = {}) {
384
- const db = openReadonlyOrFail(customDbPath);
384
+ const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
385
385
  try {
386
386
  const noTests = opts.noTests || false;
387
387
  const config = opts.config || loadConfig();
388
- const testFilter = testFilterSQL('n.file', noTests);
389
388
 
389
+ // These always need JS (non-SQL logic)
390
+ const files = countFilesByLanguage(db, noTests);
391
+ const fileCycles = findCycles(db, { fileLevel: true, noTests });
392
+ const fnCycles = findCycles(db, { fileLevel: false, noTests });
393
+
394
+ // ── Native fast path: batch all SQL aggregations in one napi call ──
395
+ if (nativeDb?.getGraphStats) {
396
+ const s = nativeDb.getGraphStats(noTests);
397
+ const nodesByKind: Record<string, number> = {};
398
+ for (const k of s.nodesByKind) nodesByKind[k.kind] = k.count;
399
+ const edgesByKind: Record<string, number> = {};
400
+ for (const k of s.edgesByKind) edgesByKind[k.kind] = k.count;
401
+ const roles: Record<string, number> & { dead?: number } = {};
402
+ let deadTotal = 0;
403
+ for (const r of s.roleCounts) {
404
+ roles[r.role] = r.count;
405
+ if (r.role.startsWith(DEAD_ROLE_PREFIX)) deadTotal += r.count;
406
+ }
407
+ if (deadTotal > 0) roles.dead = deadTotal;
408
+
409
+ const callerCoverage =
410
+ s.quality.callableTotal > 0 ? s.quality.callableWithCallers / s.quality.callableTotal : 0;
411
+ const callConfidence =
412
+ s.quality.callEdges > 0 ? s.quality.highConfCallEdges / s.quality.callEdges : 0;
413
+
414
+ // False-positive analysis still uses JS (needs FALSE_POSITIVE_NAMES set)
415
+ const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
416
+ const fpRows = db
417
+ .prepare(`
418
+ SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
419
+ FROM nodes n
420
+ LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
421
+ WHERE n.kind IN ('function', 'method')
422
+ GROUP BY n.id
423
+ HAVING caller_count > ?
424
+ ORDER BY caller_count DESC
425
+ `)
426
+ .all(fpThreshold) as Array<{
427
+ name: string;
428
+ file: string;
429
+ line: number;
430
+ caller_count: number;
431
+ }>;
432
+ const falsePositiveWarnings = fpRows
433
+ .filter((r) =>
434
+ FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop()! : r.name),
435
+ )
436
+ .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
437
+ let fpEdgeCount = 0;
438
+ for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
439
+ const falsePositiveRatio = s.quality.callEdges > 0 ? fpEdgeCount / s.quality.callEdges : 0;
440
+ const score = Math.round(
441
+ callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
442
+ );
443
+
444
+ return {
445
+ nodes: { total: s.totalNodes, byKind: nodesByKind },
446
+ edges: { total: s.totalEdges, byKind: edgesByKind },
447
+ files,
448
+ cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
449
+ hotspots: s.hotspots.map((h) => ({ file: h.file, fanIn: h.fanIn, fanOut: h.fanOut })),
450
+ embeddings: s.embeddings
451
+ ? {
452
+ count: s.embeddings.count,
453
+ model: s.embeddings.model,
454
+ dim: s.embeddings.dim,
455
+ builtAt: s.embeddings.builtAt,
456
+ }
457
+ : null,
458
+ quality: {
459
+ score,
460
+ callerCoverage: {
461
+ ratio: callerCoverage,
462
+ covered: s.quality.callableWithCallers,
463
+ total: s.quality.callableTotal,
464
+ },
465
+ callConfidence: {
466
+ ratio: callConfidence,
467
+ highConf: s.quality.highConfCallEdges,
468
+ total: s.quality.callEdges,
469
+ },
470
+ falsePositiveWarnings,
471
+ },
472
+ roles,
473
+ complexity: s.complexity
474
+ ? {
475
+ analyzed: s.complexity.analyzed,
476
+ avgCognitive: s.complexity.avgCognitive,
477
+ avgCyclomatic: s.complexity.avgCyclomatic,
478
+ maxCognitive: s.complexity.maxCognitive,
479
+ maxCyclomatic: s.complexity.maxCyclomatic,
480
+ avgMI: s.complexity.avgMi,
481
+ minMI: s.complexity.minMi,
482
+ }
483
+ : null,
484
+ };
485
+ }
486
+
487
+ // ── JS fallback ───────────────────────────────────────────────────
488
+ const testFilter = testFilterSQL('n.file', noTests);
390
489
  const testFileIds = noTests ? buildTestFileIds(db) : null;
391
490
 
392
491
  const { total: totalNodes, byKind: nodesByKind } = countNodesByKind(db, testFileIds);
393
492
  const { total: totalEdges, byKind: edgesByKind } = countEdgesByKind(db, testFileIds);
394
- const files = countFilesByLanguage(db, noTests);
395
-
396
- const fileCycles = findCycles(db, { fileLevel: true, noTests });
397
- const fnCycles = findCycles(db, { fileLevel: false, noTests });
398
493
 
399
494
  const hotspots = findHotspots(db, noTests, 5);
400
495
  const embeddings = getEmbeddingsInfo(db);
@@ -415,6 +510,6 @@ export function statsData(customDbPath: string, opts: { noTests?: boolean; confi
415
510
  complexity,
416
511
  };
417
512
  } finally {
418
- db.close();
513
+ close();
419
514
  }
420
515
  }
@@ -0,0 +1,35 @@
1
+ import { openReadonlyOrFail } from '../../db/index.js';
2
+ import { loadConfig } from '../../infrastructure/config.js';
3
+ import type { BetterSqlite3Database, CodegraphConfig } from '../../types.js';
4
+
5
+ /**
6
+ * Open a readonly DB connection, run `fn`, and close the DB on completion.
7
+ * Eliminates the duplicated `openReadonlyOrFail` + `try/finally/db.close()` pattern
8
+ * that appears in every analysis query function.
9
+ */
10
+ export function withReadonlyDb<T>(
11
+ customDbPath: string | undefined,
12
+ fn: (db: BetterSqlite3Database) => T,
13
+ ): T {
14
+ const db = openReadonlyOrFail(customDbPath);
15
+ try {
16
+ return fn(db);
17
+ } finally {
18
+ db.close();
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Resolve common analysis options into a normalized form.
24
+ * Shared across fn-impact, context, dependencies, and exports modules.
25
+ */
26
+ export function resolveAnalysisOpts(opts: { noTests?: boolean; config?: CodegraphConfig }): {
27
+ noTests: boolean;
28
+ config: CodegraphConfig;
29
+ displayOpts: Record<string, unknown>;
30
+ } {
31
+ const noTests = opts.noTests || false;
32
+ const config = opts.config || loadConfig();
33
+ const displayOpts = config.display || {};
34
+ return { noTests, config, displayOpts };
35
+ }
@@ -47,6 +47,17 @@ export const BUILTIN_RECEIVERS: Set<string> = new Set([
47
47
  'require',
48
48
  ]);
49
49
 
50
+ /** Check if a directory entry should be skipped (ignored dirs, dotfiles). */
51
+ function shouldSkipEntry(entry: fs.Dirent, extraIgnore: Set<string> | null): boolean {
52
+ if (entry.name.startsWith('.') && entry.name !== '.') {
53
+ if (IGNORE_DIRS.has(entry.name)) return true;
54
+ if (entry.isDirectory()) return true;
55
+ }
56
+ if (IGNORE_DIRS.has(entry.name)) return true;
57
+ if (extraIgnore?.has(entry.name)) return true;
58
+ return false;
59
+ }
60
+
50
61
  /**
51
62
  * Recursively collect all source files under `dir`.
52
63
  * When `directories` is a Set, also tracks which directories contain files.
@@ -100,12 +111,7 @@ export function collectFiles(
100
111
  }
101
112
 
102
113
  for (const entry of entries) {
103
- if (entry.name.startsWith('.') && entry.name !== '.') {
104
- if (IGNORE_DIRS.has(entry.name)) continue;
105
- if (entry.isDirectory()) continue;
106
- }
107
- if (IGNORE_DIRS.has(entry.name)) continue;
108
- if (extraIgnore?.has(entry.name)) continue;
114
+ if (shouldSkipEntry(entry, extraIgnore)) continue;
109
115
 
110
116
  const full = path.join(dir, entry.name);
111
117
  if (entry.isDirectory()) {
@@ -10,7 +10,7 @@
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { bulkNodeIdsByFile } from '../../../db/index.js';
13
- import { warn } from '../../../infrastructure/logger.js';
13
+ import { debug, warn } from '../../../infrastructure/logger.js';
14
14
  import { normalizePath } from '../../../shared/constants.js';
15
15
  import type {
16
16
  BetterSqlite3Database,
@@ -154,7 +154,8 @@ async function parseReverseDep(
154
154
  let code: string;
155
155
  try {
156
156
  code = readFileSafe(absPath);
157
- } catch {
157
+ } catch (e: unknown) {
158
+ debug(`parseReverseDep: cannot read ${absPath}: ${e instanceof Error ? e.message : String(e)}`);
158
159
  return null;
159
160
  }
160
161
 
@@ -35,6 +35,18 @@ function initializeEngine(ctx: PipelineContext): void {
35
35
  dataflow: ctx.opts.dataflow !== false,
36
36
  ast: ctx.opts.ast !== false,
37
37
  nativeDb: ctx.nativeDb,
38
+ // WAL checkpoint callbacks for dual-connection WAL guard (#696).
39
+ // Feature modules (ast, cfg, complexity, dataflow) receive `db` as a
40
+ // parameter and cannot tolerate close/reopen (stale reference). Instead,
41
+ // checkpoint the WAL so native writes start with a clean slate. Features
42
+ // return early on native success and never read native-written WAL data
43
+ // through the JS connection, so a post-write checkpoint is unnecessary.
44
+ suspendJsDb: ctx.nativeDb
45
+ ? () => {
46
+ ctx.db.pragma('wal_checkpoint(TRUNCATE)');
47
+ }
48
+ : undefined,
49
+ resumeJsDb: ctx.nativeDb ? () => {} : undefined,
38
50
  };
39
51
  const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
40
52
  ctx.engineName = engineName as 'native' | 'wasm';
@@ -48,9 +60,11 @@ function checkEngineSchemaMismatch(ctx: PipelineContext): void {
48
60
  ctx.forceFullRebuild = false;
49
61
  if (!ctx.incremental) return;
50
62
 
51
- // Route metadata reads through NativeDatabase when available (Phase 6.13)
63
+ // Route metadata reads through NativeDatabase only when using the native engine,
64
+ // to avoid dual-SQLite WAL conflicts (rusqlite + better-sqlite3 on same file).
65
+ const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
52
66
  const meta = (key: string): string | null =>
53
- ctx.nativeDb ? ctx.nativeDb.getBuildMeta(key) : getBuildMeta(ctx.db, key);
67
+ useNativeDb ? ctx.nativeDb!.getBuildMeta(key) : getBuildMeta(ctx.db, key);
54
68
 
55
69
  const prevEngine = meta('engine');
56
70
  if (prevEngine && prevEngine !== ctx.engineName) {
@@ -109,8 +123,10 @@ function setupPipeline(ctx: PipelineContext): void {
109
123
  } catch (err) {
110
124
  warn(`NativeDatabase init failed, falling back to JS: ${(err as Error).message}`);
111
125
  ctx.nativeDb = undefined;
112
- initSchema(ctx.db);
113
126
  }
127
+ // Always run JS initSchema so better-sqlite3 sees the schema —
128
+ // nativeDb is closed during pipeline stages and reopened for analyses.
129
+ initSchema(ctx.db);
114
130
  } else {
115
131
  initSchema(ctx.db);
116
132
  }
@@ -156,6 +172,26 @@ function formatTimingResult(ctx: PipelineContext): BuildResult {
156
172
  // ── Pipeline stages execution ───────────────────────────────────────────
157
173
 
158
174
  async function runPipelineStages(ctx: PipelineContext): Promise<void> {
175
+ // Prevent dual-connection WAL corruption during pipeline stages: when both
176
+ // better-sqlite3 (ctx.db) and rusqlite (ctx.nativeDb) are open to the same
177
+ // WAL-mode file, native writes corrupt the DB. Close nativeDb so stages
178
+ // use JS fallback paths. Reopened before runAnalyses for feature modules
179
+ // that use suspendJsDb/resumeJsDb WAL checkpoint pattern (#696).
180
+ const hadNativeDb = !!ctx.nativeDb;
181
+ if (ctx.db && ctx.nativeDb) {
182
+ try {
183
+ ctx.nativeDb.close();
184
+ } catch {
185
+ /* ignore close errors */
186
+ }
187
+ ctx.nativeDb = undefined;
188
+ // Also clear stale reference in engineOpts to prevent stages from
189
+ // calling methods on the closed NativeDatabase.
190
+ if (ctx.engineOpts?.nativeDb) {
191
+ ctx.engineOpts.nativeDb = undefined;
192
+ }
193
+ }
194
+
159
195
  await collectFiles(ctx);
160
196
  await detectChanges(ctx);
161
197
 
@@ -166,7 +202,39 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
166
202
  await resolveImports(ctx);
167
203
  await buildEdges(ctx);
168
204
  await buildStructure(ctx);
205
+
206
+ // Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow)
207
+ // which use suspendJsDb/resumeJsDb WAL checkpoint before native writes.
208
+ if (hadNativeDb) {
209
+ const native = loadNative();
210
+ if (native?.NativeDatabase) {
211
+ try {
212
+ ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
213
+ if (ctx.engineOpts) {
214
+ ctx.engineOpts.nativeDb = ctx.nativeDb;
215
+ }
216
+ } catch {
217
+ ctx.nativeDb = undefined;
218
+ if (ctx.engineOpts) {
219
+ ctx.engineOpts.nativeDb = undefined;
220
+ }
221
+ }
222
+ }
223
+ }
224
+
169
225
  await runAnalyses(ctx);
226
+
227
+ // Close nativeDb after analyses — finalize uses JS paths for setBuildMeta
228
+ // and closeDbPair handles cleanup. Avoids dual-connection during finalize.
229
+ if (ctx.nativeDb) {
230
+ try {
231
+ ctx.nativeDb.close();
232
+ } catch {
233
+ /* ignore close errors */
234
+ }
235
+ ctx.nativeDb = undefined;
236
+ }
237
+
170
238
  await finalize(ctx);
171
239
  }
172
240