@optave/codegraph 3.7.0 → 3.8.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 (148) hide show
  1. package/README.md +25 -14
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +158 -1
  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 +0 -1
  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 +2 -75
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/cli/commands/ast.js +2 -2
  12. package/dist/cli/commands/ast.js.map +1 -1
  13. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  14. package/dist/domain/graph/builder/pipeline.js +128 -6
  15. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  16. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  17. package/dist/domain/graph/builder/stages/build-edges.js +101 -1
  18. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  19. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  20. package/dist/domain/graph/builder/stages/collect-files.js +17 -5
  21. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  22. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/stages/detect-changes.js +98 -50
  24. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  25. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  26. package/dist/domain/graph/builder/stages/finalize.js +32 -5
  27. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  28. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  29. package/dist/domain/graph/builder/stages/insert-nodes.js +20 -7
  30. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  31. package/dist/domain/parser.d.ts +1 -1
  32. package/dist/domain/parser.d.ts.map +1 -1
  33. package/dist/domain/parser.js +88 -4
  34. package/dist/domain/parser.js.map +1 -1
  35. package/dist/extractors/clojure.d.ts +12 -0
  36. package/dist/extractors/clojure.d.ts.map +1 -0
  37. package/dist/extractors/clojure.js +245 -0
  38. package/dist/extractors/clojure.js.map +1 -0
  39. package/dist/extractors/cuda.d.ts +11 -0
  40. package/dist/extractors/cuda.d.ts.map +1 -0
  41. package/dist/extractors/cuda.js +302 -0
  42. package/dist/extractors/cuda.js.map +1 -0
  43. package/dist/extractors/erlang.d.ts +14 -0
  44. package/dist/extractors/erlang.d.ts.map +1 -0
  45. package/dist/extractors/erlang.js +239 -0
  46. package/dist/extractors/erlang.js.map +1 -0
  47. package/dist/extractors/fsharp.d.ts +13 -0
  48. package/dist/extractors/fsharp.d.ts.map +1 -0
  49. package/dist/extractors/fsharp.js +218 -0
  50. package/dist/extractors/fsharp.js.map +1 -0
  51. package/dist/extractors/gleam.d.ts +14 -0
  52. package/dist/extractors/gleam.d.ts.map +1 -0
  53. package/dist/extractors/gleam.js +229 -0
  54. package/dist/extractors/gleam.js.map +1 -0
  55. package/dist/extractors/groovy.d.ts +10 -0
  56. package/dist/extractors/groovy.d.ts.map +1 -0
  57. package/dist/extractors/groovy.js +304 -0
  58. package/dist/extractors/groovy.js.map +1 -0
  59. package/dist/extractors/index.d.ts +11 -0
  60. package/dist/extractors/index.d.ts.map +1 -1
  61. package/dist/extractors/index.js +11 -0
  62. package/dist/extractors/index.js.map +1 -1
  63. package/dist/extractors/julia.d.ts +16 -0
  64. package/dist/extractors/julia.d.ts.map +1 -0
  65. package/dist/extractors/julia.js +287 -0
  66. package/dist/extractors/julia.js.map +1 -0
  67. package/dist/extractors/objc.d.ts +9 -0
  68. package/dist/extractors/objc.d.ts.map +1 -0
  69. package/dist/extractors/objc.js +406 -0
  70. package/dist/extractors/objc.js.map +1 -0
  71. package/dist/extractors/ocaml.js +74 -0
  72. package/dist/extractors/ocaml.js.map +1 -1
  73. package/dist/extractors/r.d.ts +13 -0
  74. package/dist/extractors/r.d.ts.map +1 -0
  75. package/dist/extractors/r.js +251 -0
  76. package/dist/extractors/r.js.map +1 -0
  77. package/dist/extractors/solidity.d.ts +9 -0
  78. package/dist/extractors/solidity.d.ts.map +1 -0
  79. package/dist/extractors/solidity.js +374 -0
  80. package/dist/extractors/solidity.js.map +1 -0
  81. package/dist/extractors/verilog.d.ts +9 -0
  82. package/dist/extractors/verilog.d.ts.map +1 -0
  83. package/dist/extractors/verilog.js +286 -0
  84. package/dist/extractors/verilog.js.map +1 -0
  85. package/dist/features/ast.d.ts.map +1 -1
  86. package/dist/features/ast.js +1 -2
  87. package/dist/features/ast.js.map +1 -1
  88. package/dist/graph/algorithms/bfs.d.ts +2 -0
  89. package/dist/graph/algorithms/bfs.d.ts.map +1 -1
  90. package/dist/graph/algorithms/bfs.js +27 -0
  91. package/dist/graph/algorithms/bfs.js.map +1 -1
  92. package/dist/graph/algorithms/centrality.d.ts +2 -0
  93. package/dist/graph/algorithms/centrality.d.ts.map +1 -1
  94. package/dist/graph/algorithms/centrality.js +28 -0
  95. package/dist/graph/algorithms/centrality.js.map +1 -1
  96. package/dist/graph/algorithms/louvain.d.ts +3 -4
  97. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  98. package/dist/graph/algorithms/louvain.js +29 -0
  99. package/dist/graph/algorithms/louvain.js.map +1 -1
  100. package/dist/graph/algorithms/shortest-path.d.ts +2 -0
  101. package/dist/graph/algorithms/shortest-path.d.ts.map +1 -1
  102. package/dist/graph/algorithms/shortest-path.js +18 -1
  103. package/dist/graph/algorithms/shortest-path.js.map +1 -1
  104. package/dist/types.d.ts +122 -2
  105. package/dist/types.d.ts.map +1 -1
  106. package/grammars/tree-sitter-clojure.wasm +0 -0
  107. package/grammars/tree-sitter-cuda.wasm +0 -0
  108. package/grammars/tree-sitter-erlang.wasm +0 -0
  109. package/grammars/tree-sitter-fsharp.wasm +0 -0
  110. package/grammars/tree-sitter-gleam.wasm +0 -0
  111. package/grammars/tree-sitter-groovy.wasm +0 -0
  112. package/grammars/tree-sitter-julia.wasm +0 -0
  113. package/grammars/tree-sitter-objc.wasm +0 -0
  114. package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
  115. package/grammars/tree-sitter-r.wasm +0 -0
  116. package/grammars/tree-sitter-solidity.wasm +0 -0
  117. package/grammars/tree-sitter-verilog.wasm +0 -0
  118. package/package.json +18 -7
  119. package/src/ast-analysis/engine.ts +183 -1
  120. package/src/ast-analysis/rules/javascript.ts +0 -1
  121. package/src/ast-analysis/visitors/ast-store-visitor.ts +2 -75
  122. package/src/cli/commands/ast.ts +2 -2
  123. package/src/domain/graph/builder/pipeline.ts +142 -6
  124. package/src/domain/graph/builder/stages/build-edges.ts +158 -1
  125. package/src/domain/graph/builder/stages/collect-files.ts +18 -7
  126. package/src/domain/graph/builder/stages/detect-changes.ts +109 -55
  127. package/src/domain/graph/builder/stages/finalize.ts +39 -9
  128. package/src/domain/graph/builder/stages/insert-nodes.ts +18 -7
  129. package/src/domain/parser.ts +108 -2
  130. package/src/extractors/clojure.ts +273 -0
  131. package/src/extractors/cuda.ts +316 -0
  132. package/src/extractors/erlang.ts +252 -0
  133. package/src/extractors/fsharp.ts +253 -0
  134. package/src/extractors/gleam.ts +246 -0
  135. package/src/extractors/groovy.ts +332 -0
  136. package/src/extractors/index.ts +11 -0
  137. package/src/extractors/julia.ts +318 -0
  138. package/src/extractors/objc.ts +431 -0
  139. package/src/extractors/ocaml.ts +78 -0
  140. package/src/extractors/r.ts +253 -0
  141. package/src/extractors/solidity.ts +398 -0
  142. package/src/extractors/verilog.ts +315 -0
  143. package/src/features/ast.ts +1 -2
  144. package/src/graph/algorithms/bfs.ts +34 -0
  145. package/src/graph/algorithms/centrality.ts +30 -0
  146. package/src/graph/algorithms/louvain.ts +31 -4
  147. package/src/graph/algorithms/shortest-path.ts +20 -1
  148. package/src/types.ts +117 -2
@@ -15,10 +15,12 @@
15
15
  * output). This eliminates redundant tree traversals per file.
16
16
  */
17
17
 
18
+ import fs from 'node:fs';
18
19
  import path from 'node:path';
19
20
  import { performance } from 'node:perf_hooks';
20
21
  import { bulkNodeIdsByFile } from '../db/index.js';
21
22
  import { debug } from '../infrastructure/logger.js';
23
+ import { loadNative } from '../infrastructure/native.js';
22
24
  import type {
23
25
  AnalysisOpts,
24
26
  AnalysisTiming,
@@ -30,6 +32,9 @@ import type {
30
32
  Definition,
31
33
  EngineOpts,
32
34
  ExtractorOutput,
35
+ NativeAddon,
36
+ NativeFunctionCfgResult,
37
+ NativeFunctionComplexityResult,
33
38
  TreeSitterNode,
34
39
  Visitor,
35
40
  WalkOptions,
@@ -95,6 +100,173 @@ async function getParserModule(): Promise<typeof import('../domain/parser.js')>
95
100
  return _parserModule;
96
101
  }
97
102
 
103
+ // ─── Native standalone analysis ─────────────────────────────────────────
104
+
105
+ /**
106
+ * Try native Rust analysis for files missing complexity/CFG/dataflow data.
107
+ * Reads source from disk, calls the native standalone functions, and stores
108
+ * results directly on definitions/symbols. Returns the set of files that
109
+ * were fully handled (no remaining gaps except possibly AST store).
110
+ */
111
+ function runNativeAnalysis(
112
+ native: NativeAddon,
113
+ fileSymbols: Map<string, ExtractorOutput>,
114
+ rootDir: string,
115
+ opts: AnalysisOpts,
116
+ extToLang: Map<string, string>,
117
+ ): void {
118
+ const doComplexity = opts.complexity !== false;
119
+ const doCfg = opts.cfg !== false;
120
+ const doDataflow = opts.dataflow !== false;
121
+
122
+ for (const [relPath, symbols] of fileSymbols) {
123
+ if (symbols._tree) continue; // already has WASM tree, skip native
124
+ const ext = path.extname(relPath).toLowerCase();
125
+ const langId = symbols._langId || extToLang.get(ext);
126
+ if (!langId) continue;
127
+
128
+ const defs = symbols.definitions || [];
129
+
130
+ const needsComplexity =
131
+ doComplexity &&
132
+ COMPLEXITY_EXTENSIONS.has(ext) &&
133
+ defs.some((d) => hasFuncBody(d) && !d.complexity);
134
+ const needsCfg =
135
+ doCfg &&
136
+ CFG_EXTENSIONS.has(ext) &&
137
+ defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
138
+ const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
139
+
140
+ if (!needsComplexity && !needsCfg && !needsDataflow) continue;
141
+
142
+ // Read source from disk
143
+ const absPath = path.join(rootDir, relPath);
144
+ let source: string;
145
+ try {
146
+ source = fs.readFileSync(absPath, 'utf-8');
147
+ } catch {
148
+ continue;
149
+ }
150
+
151
+ // Complexity
152
+ if (needsComplexity && native.analyzeComplexity) {
153
+ try {
154
+ const results = native.analyzeComplexity(source, absPath);
155
+ storeNativeComplexityResults(results, defs);
156
+ } catch (err: unknown) {
157
+ debug(`native analyzeComplexity failed for ${relPath}: ${(err as Error).message}`);
158
+ }
159
+ }
160
+
161
+ // CFG
162
+ if (needsCfg && native.buildCfgAnalysis) {
163
+ try {
164
+ const results = native.buildCfgAnalysis(source, absPath);
165
+ storeNativeCfgResults(results, defs);
166
+ } catch (err: unknown) {
167
+ debug(`native buildCfgAnalysis failed for ${relPath}: ${(err as Error).message}`);
168
+ }
169
+ }
170
+
171
+ // Dataflow
172
+ if (needsDataflow && native.extractDataflowAnalysis) {
173
+ try {
174
+ const result = native.extractDataflowAnalysis(source, absPath);
175
+ if (result) symbols.dataflow = result;
176
+ } catch (err: unknown) {
177
+ debug(`native extractDataflowAnalysis failed for ${relPath}: ${(err as Error).message}`);
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ /** Store native complexity results on definitions, matched by line number. */
184
+ function storeNativeComplexityResults(
185
+ results: NativeFunctionComplexityResult[],
186
+ defs: Definition[],
187
+ ): void {
188
+ const byLine = new Map<number, NativeFunctionComplexityResult[]>();
189
+ for (const r of results) {
190
+ if (!byLine.has(r.line)) byLine.set(r.line, []);
191
+ byLine.get(r.line)!.push(r);
192
+ }
193
+
194
+ for (const def of defs) {
195
+ if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
196
+ const candidates = byLine.get(def.line);
197
+ if (!candidates) continue;
198
+ const match =
199
+ candidates.length === 1
200
+ ? candidates[0]
201
+ : (candidates.find((r) => r.name === def.name) ?? candidates[0]);
202
+ if (!match) continue;
203
+ const { complexity: c } = match;
204
+ def.complexity = {
205
+ cognitive: c.cognitive,
206
+ cyclomatic: c.cyclomatic,
207
+ maxNesting: c.maxNesting,
208
+ halstead: c.halstead
209
+ ? {
210
+ volume: c.halstead.volume,
211
+ difficulty: c.halstead.difficulty,
212
+ effort: c.halstead.effort,
213
+ bugs: c.halstead.bugs,
214
+ }
215
+ : undefined,
216
+ loc: c.loc
217
+ ? { loc: c.loc.loc, sloc: c.loc.sloc, commentLines: c.loc.commentLines }
218
+ : undefined,
219
+ maintainabilityIndex: c.maintainabilityIndex ?? undefined,
220
+ };
221
+ }
222
+ }
223
+ }
224
+
225
+ /** Store native CFG results on definitions, matched by line number. */
226
+ function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definition[]): void {
227
+ const byLine = new Map<number, NativeFunctionCfgResult[]>();
228
+ for (const r of results) {
229
+ if (!byLine.has(r.line)) byLine.set(r.line, []);
230
+ byLine.get(r.line)!.push(r);
231
+ }
232
+
233
+ for (const def of defs) {
234
+ if (
235
+ (def.kind === 'function' || def.kind === 'method') &&
236
+ def.line &&
237
+ def.cfg !== null &&
238
+ !def.cfg?.blocks?.length
239
+ ) {
240
+ const candidates = byLine.get(def.line);
241
+ if (!candidates) continue;
242
+ const match =
243
+ candidates.length === 1
244
+ ? candidates[0]
245
+ : (candidates.find((r) => r.name === def.name) ?? candidates[0]);
246
+ if (!match) continue;
247
+ def.cfg = match.cfg;
248
+
249
+ // Override complexity cyclomatic with CFG-derived value
250
+ const { edges, blocks } = match.cfg;
251
+ if (def.complexity && edges && blocks) {
252
+ const cfgCyclomatic = edges.length - blocks.length + 2;
253
+ if (cfgCyclomatic > 0) {
254
+ def.complexity.cyclomatic = cfgCyclomatic;
255
+ const { loc, halstead } = def.complexity;
256
+ const volume = halstead ? halstead.volume : 0;
257
+ const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
258
+ def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
259
+ volume,
260
+ cfgCyclomatic,
261
+ loc?.sloc ?? 0,
262
+ commentRatio,
263
+ );
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+
98
270
  // ─── WASM pre-parse ─────────────────────────────────────────────────────
99
271
 
100
272
  async function ensureWasmTreesIfNeeded(
@@ -429,7 +601,17 @@ export async function runAnalyses(
429
601
 
430
602
  const extToLang = buildExtToLangMap();
431
603
 
432
- // WASM pre-parse for files that need it
604
+ // Native analysis pass: try Rust standalone functions before WASM fallback.
605
+ // This fills in complexity/CFG/dataflow for files that the native parse pipeline
606
+ // missed, avoiding the need to parse with WASM + run JS visitors.
607
+ const native = loadNative();
608
+ if (native?.analyzeComplexity ?? native?.buildCfgAnalysis ?? native?.extractDataflowAnalysis) {
609
+ const t0native = performance.now();
610
+ runNativeAnalysis(native, fileSymbols, rootDir, opts, extToLang);
611
+ debug(`native standalone analysis: ${(performance.now() - t0native).toFixed(1)}ms`);
612
+ }
613
+
614
+ // WASM pre-parse for files that still need it (AST store, or native gaps)
433
615
  await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
434
616
 
435
617
  // Unified pre-walk: run all applicable visitors in a single DFS per file
@@ -237,7 +237,6 @@ export const dataflow: DataflowRulesConfig = makeDataflowRules({
237
237
  // ─── AST Node Types ───────────────────────────────────────────────────────
238
238
 
239
239
  export const astTypes: Record<string, string> | null = {
240
- call_expression: 'call',
241
240
  new_expression: 'new',
242
241
  throw_statement: 'throw',
243
242
  await_expression: 'await',
@@ -44,22 +44,6 @@ function extractExpressionText(node: TreeSitterNode): string | null {
44
44
  return truncate(node.text);
45
45
  }
46
46
 
47
- function extractCallName(node: TreeSitterNode): string {
48
- for (const field of ['function', 'method', 'name']) {
49
- const fn = node.childForFieldName(field);
50
- if (fn) return fn.text;
51
- }
52
- return node.text?.split('(')[0] || '?';
53
- }
54
-
55
- /** Extract receiver for call expressions (e.g. "obj" in "obj.method()"). */
56
- function extractCallReceiver(node: TreeSitterNode): string | null {
57
- const fn = node.childForFieldName('function');
58
- if (!fn || fn.type !== 'member_expression') return null;
59
- const obj = fn.childForFieldName('object');
60
- return obj ? obj.text : null;
61
- }
62
-
63
47
  function extractName(kind: string, node: TreeSitterNode): string | null {
64
48
  if (kind === 'throw') {
65
49
  for (let i = 0; i < node.childCount; i++) {
@@ -118,64 +102,14 @@ export function createAstStoreVisitor(
118
102
  return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
119
103
  }
120
104
 
121
- /** Recursively walk a subtree collecting AST nodes — used for arguments-only traversal. */
122
- function walkSubtree(node: TreeSitterNode | null): void {
123
- if (!node) return;
124
- if (matched.has(node.id)) return;
125
-
126
- const kind = astTypeMap[node.type];
127
- if (kind === 'call') {
128
- // Capture this call and recurse only into its arguments
129
- collectNode(node, kind);
130
- walkCallArguments(node);
131
- return;
132
- }
133
- if (kind) {
134
- collectNode(node, kind);
135
- if (kind !== 'string' && kind !== 'regex') return; // skipChildren for non-leaf kinds
136
- }
137
- for (let i = 0; i < node.childCount; i++) {
138
- walkSubtree(node.child(i));
139
- }
140
- }
141
-
142
- /**
143
- * Recurse into only the arguments of a call node — mirrors the native engine's
144
- * strategy that prevents double-counting nested calls in the function field
145
- * (e.g. chained calls like `a().b()`).
146
- */
147
- function walkCallArguments(callNode: TreeSitterNode): void {
148
- // Try field-based lookup first, fall back to kind-based matching
149
- const argsNode =
150
- callNode.childForFieldName('arguments') ??
151
- findChildByKind(callNode, ['arguments', 'argument_list', 'method_arguments']);
152
- if (!argsNode) return;
153
- for (let i = 0; i < argsNode.childCount; i++) {
154
- walkSubtree(argsNode.child(i));
155
- }
156
- }
157
-
158
- function findChildByKind(node: TreeSitterNode, kinds: string[]): TreeSitterNode | null {
159
- for (let i = 0; i < node.childCount; i++) {
160
- const child = node.child(i);
161
- if (child && kinds.includes(child.type)) return child;
162
- }
163
- return null;
164
- }
165
-
166
105
  function collectNode(node: TreeSitterNode, kind: string): void {
167
106
  if (matched.has(node.id)) return;
168
107
 
169
108
  const line = node.startPosition.row + 1;
170
109
  let name: string | null | undefined;
171
110
  let text: string | null = null;
172
- let receiver: string | null = null;
173
111
 
174
- if (kind === 'call') {
175
- name = extractCallName(node);
176
- text = truncate(node.text);
177
- receiver = extractCallReceiver(node);
178
- } else if (kind === 'new') {
112
+ if (kind === 'new') {
179
113
  name = extractNewName(node);
180
114
  text = truncate(node.text);
181
115
  } else if (kind === 'throw') {
@@ -200,7 +134,7 @@ export function createAstStoreVisitor(
200
134
  kind,
201
135
  name,
202
136
  text,
203
- receiver,
137
+ receiver: null,
204
138
  parentNodeId: resolveParentNodeId(line),
205
139
  });
206
140
 
@@ -221,13 +155,6 @@ export function createAstStoreVisitor(
221
155
 
222
156
  collectNode(node, kind);
223
157
 
224
- if (kind === 'call') {
225
- // Mirror native: skip full subtree, recurse only into arguments.
226
- // Prevents double-counting chained calls like service.getUser().getName().
227
- walkCallArguments(node);
228
- return { skipChildren: true };
229
- }
230
-
231
158
  if (kind !== 'string' && kind !== 'regex') {
232
159
  return { skipChildren: true };
233
160
  }
@@ -4,10 +4,10 @@ import type { CommandDefinition } from '../types.js';
4
4
 
5
5
  export const command: CommandDefinition = {
6
6
  name: 'ast [pattern]',
7
- description: 'Search stored AST nodes (calls, new, string, regex, throw, await) by pattern',
7
+ description: 'Search stored AST nodes (new, string, regex, throw, await) by pattern',
8
8
  queryOpts: true,
9
9
  options: [
10
- ['-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)'],
10
+ ['-k, --kind <kind>', 'Filter by AST node kind (new, string, regex, throw, await)'],
11
11
  ['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
12
12
  ],
13
13
  async execute([pattern], opts, ctx) {
@@ -35,18 +35,26 @@ 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).
38
+ // WAL checkpoint callbacks for dual-connection WAL guard (#696, #715).
39
39
  // Feature modules (ast, cfg, complexity, dataflow) receive `db` as a
40
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.
41
+ // checkpoint the WAL so native writes start with a clean slate.
42
+ // After native writes, resumeJsDb checkpoints through rusqlite so
43
+ // better-sqlite3 never reads WAL frames from a different SQLite library.
44
44
  suspendJsDb: ctx.nativeDb
45
45
  ? () => {
46
46
  ctx.db.pragma('wal_checkpoint(TRUNCATE)');
47
47
  }
48
48
  : undefined,
49
- resumeJsDb: ctx.nativeDb ? () => {} : undefined,
49
+ resumeJsDb: ctx.nativeDb
50
+ ? () => {
51
+ try {
52
+ ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
53
+ } catch {
54
+ /* ignore — nativeDb may already be closed */
55
+ }
56
+ }
57
+ : undefined,
50
58
  };
51
59
  const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
52
60
  ctx.engineName = engineName as 'native' | 'wasm';
@@ -120,8 +128,16 @@ function setupPipeline(ctx: PipelineContext): void {
120
128
  try {
121
129
  ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
122
130
  ctx.nativeDb.initSchema();
131
+ // Checkpoint WAL through rusqlite so better-sqlite3 sees a clean DB
132
+ // with no cross-library WAL frames (#715, #717).
133
+ ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
123
134
  } catch (err) {
124
- warn(`NativeDatabase init failed, falling back to JS: ${(err as Error).message}`);
135
+ warn(`NativeDatabase setup failed, falling back to JS: ${(err as Error).message}`);
136
+ try {
137
+ ctx.nativeDb?.close();
138
+ } catch {
139
+ /* ignore close errors */
140
+ }
125
141
  ctx.nativeDb = undefined;
126
142
  }
127
143
  // Always run JS initSchema so better-sqlite3 sees the schema —
@@ -179,6 +195,15 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
179
195
  // that use suspendJsDb/resumeJsDb WAL checkpoint pattern (#696).
180
196
  const hadNativeDb = !!ctx.nativeDb;
181
197
  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
+ }
182
207
  try {
183
208
  ctx.nativeDb.close();
184
209
  } catch {
@@ -198,7 +223,51 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
198
223
  if (ctx.earlyExit) return;
199
224
 
200
225
  await parseFiles(ctx);
226
+
227
+ // Temporarily reopen nativeDb for insertNodes — it uses the WAL checkpoint
228
+ // guard internally (same pattern as feature modules). Closed again before
229
+ // resolveImports/buildEdges which don't yet have the guard (#709).
230
+ 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
+ }
239
+ }
240
+
201
241
  await insertNodes(ctx);
242
+
243
+ // Close nativeDb after insertNodes — remaining pipeline stages use JS paths.
244
+ 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);
269
+ }
270
+
202
271
  await resolveImports(ctx);
203
272
  await buildEdges(ctx);
204
273
  await buildStructure(ctx);
@@ -227,6 +296,14 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
227
296
  // Close nativeDb after analyses — finalize uses JS paths for setBuildMeta
228
297
  // and closeDbPair handles cleanup. Avoids dual-connection during finalize.
229
298
  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
+ }
230
307
  try {
231
308
  ctx.nativeDb.close();
232
309
  } catch {
@@ -256,6 +333,65 @@ export async function buildGraph(
256
333
 
257
334
  try {
258
335
  setupPipeline(ctx);
336
+
337
+ // ── Rust orchestrator fast path (#695) ────────────────────────────
338
+ // When available, run the entire build pipeline in Rust with zero
339
+ // napi crossings (eliminates WAL dual-connection dance). Falls back
340
+ // to the JS pipeline on failure or when native is unavailable.
341
+ const forceJs = process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1';
342
+ if (!forceJs && ctx.nativeDb?.buildGraph) {
343
+ try {
344
+ const resultJson = ctx.nativeDb.buildGraph(
345
+ ctx.rootDir,
346
+ JSON.stringify(ctx.config),
347
+ JSON.stringify(ctx.aliases),
348
+ JSON.stringify(opts),
349
+ );
350
+ const result = JSON.parse(resultJson) as {
351
+ phases: Record<string, number>;
352
+ earlyExit?: boolean;
353
+ nodeCount?: number;
354
+ edgeCount?: number;
355
+ fileCount?: number;
356
+ };
357
+
358
+ if (result.earlyExit) {
359
+ closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
360
+ return;
361
+ }
362
+
363
+ // Map Rust timing fields to the JS BuildResult format.
364
+ // Rust handles collect+detect+parse+insert+resolve+edges+structure+roles.
365
+ // AST/complexity/CFG/dataflow analyses are not yet ported to Rust.
366
+ const p = result.phases;
367
+ closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
368
+ info(
369
+ `Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`,
370
+ );
371
+ return {
372
+ phases: {
373
+ setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
374
+ parseMs: +(p.parseMs ?? 0).toFixed(1),
375
+ insertMs: +(p.insertMs ?? 0).toFixed(1),
376
+ resolveMs: +(p.resolveMs ?? 0).toFixed(1),
377
+ edgesMs: +(p.edgesMs ?? 0).toFixed(1),
378
+ structureMs: +(p.structureMs ?? 0).toFixed(1),
379
+ rolesMs: +(p.rolesMs ?? 0).toFixed(1),
380
+ astMs: 0,
381
+ complexityMs: 0,
382
+ cfgMs: 0,
383
+ dataflowMs: 0,
384
+ finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
385
+ },
386
+ };
387
+ } catch (err) {
388
+ warn(
389
+ `Native build orchestrator failed, falling back to JS pipeline: ${(err as Error).message}`,
390
+ );
391
+ // Fall through to JS pipeline
392
+ }
393
+ }
394
+
259
395
  await runPipelineStages(ctx);
260
396
  } catch (err) {
261
397
  if (!ctx.earlyExit && ctx.db) {