@optave/codegraph 3.6.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 (186) hide show
  1. package/README.md +32 -16
  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 +129 -3
  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/dart.d.ts +6 -0
  44. package/dist/extractors/dart.d.ts.map +1 -0
  45. package/dist/extractors/dart.js +277 -0
  46. package/dist/extractors/dart.js.map +1 -0
  47. package/dist/extractors/elixir.d.ts +9 -0
  48. package/dist/extractors/elixir.d.ts.map +1 -0
  49. package/dist/extractors/elixir.js +223 -0
  50. package/dist/extractors/elixir.js.map +1 -0
  51. package/dist/extractors/erlang.d.ts +14 -0
  52. package/dist/extractors/erlang.d.ts.map +1 -0
  53. package/dist/extractors/erlang.js +239 -0
  54. package/dist/extractors/erlang.js.map +1 -0
  55. package/dist/extractors/fsharp.d.ts +13 -0
  56. package/dist/extractors/fsharp.d.ts.map +1 -0
  57. package/dist/extractors/fsharp.js +218 -0
  58. package/dist/extractors/fsharp.js.map +1 -0
  59. package/dist/extractors/gleam.d.ts +14 -0
  60. package/dist/extractors/gleam.d.ts.map +1 -0
  61. package/dist/extractors/gleam.js +229 -0
  62. package/dist/extractors/gleam.js.map +1 -0
  63. package/dist/extractors/groovy.d.ts +10 -0
  64. package/dist/extractors/groovy.d.ts.map +1 -0
  65. package/dist/extractors/groovy.js +304 -0
  66. package/dist/extractors/groovy.js.map +1 -0
  67. package/dist/extractors/haskell.d.ts +8 -0
  68. package/dist/extractors/haskell.d.ts.map +1 -0
  69. package/dist/extractors/haskell.js +217 -0
  70. package/dist/extractors/haskell.js.map +1 -0
  71. package/dist/extractors/index.d.ts +17 -0
  72. package/dist/extractors/index.d.ts.map +1 -1
  73. package/dist/extractors/index.js +17 -0
  74. package/dist/extractors/index.js.map +1 -1
  75. package/dist/extractors/julia.d.ts +16 -0
  76. package/dist/extractors/julia.d.ts.map +1 -0
  77. package/dist/extractors/julia.js +287 -0
  78. package/dist/extractors/julia.js.map +1 -0
  79. package/dist/extractors/lua.d.ts +6 -0
  80. package/dist/extractors/lua.d.ts.map +1 -0
  81. package/dist/extractors/lua.js +162 -0
  82. package/dist/extractors/lua.js.map +1 -0
  83. package/dist/extractors/objc.d.ts +9 -0
  84. package/dist/extractors/objc.d.ts.map +1 -0
  85. package/dist/extractors/objc.js +406 -0
  86. package/dist/extractors/objc.js.map +1 -0
  87. package/dist/extractors/ocaml.d.ts +6 -0
  88. package/dist/extractors/ocaml.d.ts.map +1 -0
  89. package/dist/extractors/ocaml.js +310 -0
  90. package/dist/extractors/ocaml.js.map +1 -0
  91. package/dist/extractors/r.d.ts +13 -0
  92. package/dist/extractors/r.d.ts.map +1 -0
  93. package/dist/extractors/r.js +251 -0
  94. package/dist/extractors/r.js.map +1 -0
  95. package/dist/extractors/solidity.d.ts +9 -0
  96. package/dist/extractors/solidity.d.ts.map +1 -0
  97. package/dist/extractors/solidity.js +374 -0
  98. package/dist/extractors/solidity.js.map +1 -0
  99. package/dist/extractors/verilog.d.ts +9 -0
  100. package/dist/extractors/verilog.d.ts.map +1 -0
  101. package/dist/extractors/verilog.js +286 -0
  102. package/dist/extractors/verilog.js.map +1 -0
  103. package/dist/extractors/zig.d.ts +9 -0
  104. package/dist/extractors/zig.d.ts.map +1 -0
  105. package/dist/extractors/zig.js +276 -0
  106. package/dist/extractors/zig.js.map +1 -0
  107. package/dist/features/ast.d.ts.map +1 -1
  108. package/dist/features/ast.js +1 -2
  109. package/dist/features/ast.js.map +1 -1
  110. package/dist/features/cfg.d.ts +1 -1
  111. package/dist/features/cfg.d.ts.map +1 -1
  112. package/dist/features/cfg.js +6 -51
  113. package/dist/features/cfg.js.map +1 -1
  114. package/dist/graph/algorithms/bfs.d.ts +2 -0
  115. package/dist/graph/algorithms/bfs.d.ts.map +1 -1
  116. package/dist/graph/algorithms/bfs.js +27 -0
  117. package/dist/graph/algorithms/bfs.js.map +1 -1
  118. package/dist/graph/algorithms/centrality.d.ts +2 -0
  119. package/dist/graph/algorithms/centrality.d.ts.map +1 -1
  120. package/dist/graph/algorithms/centrality.js +28 -0
  121. package/dist/graph/algorithms/centrality.js.map +1 -1
  122. package/dist/graph/algorithms/louvain.d.ts +3 -4
  123. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  124. package/dist/graph/algorithms/louvain.js +29 -0
  125. package/dist/graph/algorithms/louvain.js.map +1 -1
  126. package/dist/graph/algorithms/shortest-path.d.ts +2 -0
  127. package/dist/graph/algorithms/shortest-path.d.ts.map +1 -1
  128. package/dist/graph/algorithms/shortest-path.js +18 -1
  129. package/dist/graph/algorithms/shortest-path.js.map +1 -1
  130. package/dist/types.d.ts +122 -2
  131. package/dist/types.d.ts.map +1 -1
  132. package/grammars/tree-sitter-clojure.wasm +0 -0
  133. package/grammars/tree-sitter-cuda.wasm +0 -0
  134. package/grammars/tree-sitter-dart.wasm +0 -0
  135. package/grammars/tree-sitter-elixir.wasm +0 -0
  136. package/grammars/tree-sitter-erlang.wasm +0 -0
  137. package/grammars/tree-sitter-fsharp.wasm +0 -0
  138. package/grammars/tree-sitter-gleam.wasm +0 -0
  139. package/grammars/tree-sitter-groovy.wasm +0 -0
  140. package/grammars/tree-sitter-haskell.wasm +0 -0
  141. package/grammars/tree-sitter-julia.wasm +0 -0
  142. package/grammars/tree-sitter-lua.wasm +0 -0
  143. package/grammars/tree-sitter-objc.wasm +0 -0
  144. package/grammars/tree-sitter-ocaml.wasm +0 -0
  145. package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
  146. package/grammars/tree-sitter-r.wasm +0 -0
  147. package/grammars/tree-sitter-solidity.wasm +0 -0
  148. package/grammars/tree-sitter-verilog.wasm +0 -0
  149. package/grammars/tree-sitter-zig.wasm +0 -0
  150. package/package.json +24 -7
  151. package/src/ast-analysis/engine.ts +183 -1
  152. package/src/ast-analysis/rules/javascript.ts +0 -1
  153. package/src/ast-analysis/visitors/ast-store-visitor.ts +2 -75
  154. package/src/cli/commands/ast.ts +2 -2
  155. package/src/domain/graph/builder/pipeline.ts +142 -6
  156. package/src/domain/graph/builder/stages/build-edges.ts +158 -1
  157. package/src/domain/graph/builder/stages/collect-files.ts +18 -7
  158. package/src/domain/graph/builder/stages/detect-changes.ts +109 -55
  159. package/src/domain/graph/builder/stages/finalize.ts +39 -9
  160. package/src/domain/graph/builder/stages/insert-nodes.ts +18 -7
  161. package/src/domain/parser.ts +161 -1
  162. package/src/extractors/clojure.ts +273 -0
  163. package/src/extractors/cuda.ts +316 -0
  164. package/src/extractors/dart.ts +304 -0
  165. package/src/extractors/elixir.ts +251 -0
  166. package/src/extractors/erlang.ts +252 -0
  167. package/src/extractors/fsharp.ts +253 -0
  168. package/src/extractors/gleam.ts +246 -0
  169. package/src/extractors/groovy.ts +332 -0
  170. package/src/extractors/haskell.ts +235 -0
  171. package/src/extractors/index.ts +17 -0
  172. package/src/extractors/julia.ts +318 -0
  173. package/src/extractors/lua.ts +169 -0
  174. package/src/extractors/objc.ts +431 -0
  175. package/src/extractors/ocaml.ts +337 -0
  176. package/src/extractors/r.ts +253 -0
  177. package/src/extractors/solidity.ts +398 -0
  178. package/src/extractors/verilog.ts +315 -0
  179. package/src/extractors/zig.ts +294 -0
  180. package/src/features/ast.ts +1 -2
  181. package/src/features/cfg.ts +6 -51
  182. package/src/graph/algorithms/bfs.ts +34 -0
  183. package/src/graph/algorithms/centrality.ts +30 -0
  184. package/src/graph/algorithms/louvain.ts +31 -4
  185. package/src/graph/algorithms/shortest-path.ts +20 -1
  186. package/src/types.ts +123 -2
@@ -155,6 +155,155 @@ function buildBarrelEdges(
155
155
  }
156
156
  }
157
157
 
158
+ // ── Import edges (native engine) ────────────────────────────────────────
159
+
160
+ function buildImportEdgesNative(
161
+ ctx: PipelineContext,
162
+ getNodeIdStmt: NodeIdStmt,
163
+ allEdgeRows: EdgeRowTuple[],
164
+ native: NativeAddon,
165
+ ): void {
166
+ const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
167
+
168
+ // 1. Build per-file input data
169
+ const files: Array<{
170
+ file: string;
171
+ fileNodeId: number;
172
+ isBarrelOnly: boolean;
173
+ imports: Array<{
174
+ source: string;
175
+ names: string[];
176
+ reexport: boolean;
177
+ typeOnly: boolean;
178
+ dynamicImport: boolean;
179
+ wildcardReexport: boolean;
180
+ }>;
181
+ definitionNames: string[];
182
+ }> = [];
183
+
184
+ // Collect all file node IDs we'll need (sources + targets)
185
+ const fileNodeIds: Array<{ file: string; nodeId: number }> = [];
186
+ const seenNodeFiles = new Set<string>();
187
+
188
+ const addFileNodeId = (relPath: string): { id: number } | undefined => {
189
+ if (seenNodeFiles.has(relPath)) return fileNodeRowCache.get(relPath);
190
+ const row = getNodeIdStmt.get(relPath, 'file', relPath, 0);
191
+ if (row) {
192
+ seenNodeFiles.add(relPath);
193
+ fileNodeIds.push({ file: relPath, nodeId: row.id });
194
+ fileNodeRowCache.set(relPath, row);
195
+ }
196
+ return row;
197
+ };
198
+ const fileNodeRowCache = new Map<string, { id: number }>();
199
+
200
+ // 2. Pre-resolve all imports and collect supplemental resolved entries
201
+ const supplementalResolved: Array<{ key: string; resolvedPath: string }> = [];
202
+
203
+ for (const [relPath, symbols] of fileSymbols) {
204
+ const fileNodeRow = addFileNodeId(relPath);
205
+ if (!fileNodeRow) continue;
206
+
207
+ const importInfos: Array<{
208
+ source: string;
209
+ names: string[];
210
+ reexport: boolean;
211
+ typeOnly: boolean;
212
+ dynamicImport: boolean;
213
+ wildcardReexport: boolean;
214
+ }> = [];
215
+
216
+ for (const imp of symbols.imports) {
217
+ // Pre-resolve and register target file node
218
+ const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
219
+ addFileNodeId(resolvedPath);
220
+
221
+ // Check if this resolution is in batchResolved; if not, add supplemental
222
+ const resolveKey = `${path.join(rootDir, relPath)}|${imp.source}`;
223
+ if (!ctx.batchResolved?.has(resolveKey)) {
224
+ supplementalResolved.push({ key: resolveKey, resolvedPath });
225
+ }
226
+
227
+ importInfos.push({
228
+ source: imp.source,
229
+ names: imp.names,
230
+ reexport: !!imp.reexport,
231
+ typeOnly: !!imp.typeOnly,
232
+ dynamicImport: !!imp.dynamicImport,
233
+ wildcardReexport: !!imp.wildcardReexport,
234
+ });
235
+ }
236
+
237
+ files.push({
238
+ file: relPath,
239
+ fileNodeId: fileNodeRow.id,
240
+ isBarrelOnly: barrelOnlyFiles.has(relPath),
241
+ imports: importInfos,
242
+ definitionNames: symbols.definitions.map((d) => d.name),
243
+ });
244
+ }
245
+
246
+ // 3. Flatten batchResolved + supplemental into resolved imports array
247
+ const resolvedImports: Array<{ key: string; resolvedPath: string }> = [];
248
+ if (ctx.batchResolved) {
249
+ for (const [key, resolvedPath] of ctx.batchResolved) {
250
+ resolvedImports.push({ key, resolvedPath });
251
+ }
252
+ }
253
+ for (const entry of supplementalResolved) {
254
+ resolvedImports.push(entry);
255
+ }
256
+
257
+ // 4. Flatten reexportMap
258
+ const fileReexports: Array<{
259
+ file: string;
260
+ reexports: Array<{
261
+ source: string;
262
+ names: string[];
263
+ wildcardReexport: boolean;
264
+ }>;
265
+ }> = [];
266
+ if (ctx.reexportMap) {
267
+ for (const [file, entries] of ctx.reexportMap) {
268
+ const reexports = (
269
+ entries as Array<{ source: string; names: string[]; wildcardReexport: boolean }>
270
+ ).map((re) => ({
271
+ source: re.source,
272
+ names: re.names,
273
+ wildcardReexport: !!re.wildcardReexport,
274
+ }));
275
+ fileReexports.push({ file, reexports });
276
+
277
+ // Register reexport target files for node ID lookup
278
+ for (const re of reexports) {
279
+ addFileNodeId(re.source);
280
+ }
281
+ }
282
+ }
283
+
284
+ // 5. Compute barrel file list
285
+ const barrelFiles: string[] = [];
286
+ for (const [relPath] of fileSymbols) {
287
+ if (isBarrelFile(ctx, relPath)) {
288
+ barrelFiles.push(relPath);
289
+ }
290
+ }
291
+
292
+ // 6. Call native
293
+ const nativeEdges = native.buildImportEdges!(
294
+ files,
295
+ resolvedImports,
296
+ fileReexports,
297
+ fileNodeIds,
298
+ barrelFiles,
299
+ rootDir,
300
+ ) as NativeEdge[];
301
+
302
+ for (const e of nativeEdges) {
303
+ allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
304
+ }
305
+ }
306
+
158
307
  // ── Call edges (native engine) ──────────────────────────────────────────
159
308
 
160
309
  function buildCallEdgesNative(
@@ -594,7 +743,15 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
594
743
  }
595
744
  }
596
745
 
597
- buildImportEdges(ctx, getNodeIdStmt, allEdgeRows);
746
+ // Skip native import-edge path for small incremental builds (≤3 files):
747
+ // napi-rs marshaling overhead exceeds computation savings.
748
+ const useNativeImportEdges =
749
+ native?.buildImportEdges && (ctx.isFullBuild || ctx.fileSymbols.size > 3);
750
+ if (useNativeImportEdges) {
751
+ buildImportEdgesNative(ctx, getNodeIdStmt, allEdgeRows, native!);
752
+ } else {
753
+ buildImportEdges(ctx, getNodeIdStmt, allEdgeRows);
754
+ }
598
755
 
599
756
  // Skip native call-edge path for small incremental builds (≤3 files):
600
757
  // napi-rs marshaling overhead for allNodes exceeds computation savings.
@@ -21,13 +21,22 @@ function tryFastCollect(
21
21
  ctx: PipelineContext,
22
22
  ): { files: string[]; directories: Set<string> } | null {
23
23
  const { db, rootDir } = ctx;
24
+ const useNative = ctx.engineName === 'native' && !!ctx.nativeDb?.getCollectFilesData;
24
25
 
25
26
  // 1. Check that file_hashes table exists and has entries
26
27
  let dbFileCount: number;
27
- try {
28
- dbFileCount = (db.prepare('SELECT COUNT(*) as c FROM file_hashes').get() as { c: number }).c;
29
- } catch {
30
- return null;
28
+ let dbFiles: string[];
29
+ if (useNative) {
30
+ const data = ctx.nativeDb!.getCollectFilesData!();
31
+ dbFileCount = data.count;
32
+ dbFiles = data.files;
33
+ } else {
34
+ try {
35
+ dbFileCount = (db.prepare('SELECT COUNT(*) as c FROM file_hashes').get() as { c: number }).c;
36
+ } catch {
37
+ return null;
38
+ }
39
+ dbFiles = []; // deferred — loaded below only if needed
31
40
  }
32
41
  if (dbFileCount === 0) return null;
33
42
 
@@ -42,9 +51,11 @@ function tryFastCollect(
42
51
  if (!hasEntries) return null;
43
52
 
44
53
  // 3. Load existing file list from file_hashes (relative paths)
45
- const dbFiles = (db.prepare('SELECT file FROM file_hashes').all() as Array<{ file: string }>).map(
46
- (r) => r.file,
47
- );
54
+ if (!useNative) {
55
+ dbFiles = (db.prepare('SELECT file FROM file_hashes').all() as Array<{ file: string }>).map(
56
+ (r) => r.file,
57
+ );
58
+ }
48
59
 
49
60
  // 4. Apply journal deltas: remove deleted files, add new/changed files
50
61
  const fileSet = new Set(dbFiles);
@@ -60,14 +60,27 @@ function getChangedFiles(
60
60
  rootDir: string,
61
61
  nativeDb?: NativeDatabase,
62
62
  ): ChangeResult {
63
+ // Batched native path: single napi call for table check + all rows + max mtime
64
+ if (nativeDb?.getFileHashData) {
65
+ const data = nativeDb.getFileHashData();
66
+ if (!data.exists) {
67
+ return {
68
+ changed: allFiles.map((f) => ({ file: f })),
69
+ removed: [],
70
+ isFullBuild: true,
71
+ };
72
+ }
73
+ const existing = new Map<string, FileHashRow>(data.rows.map((r) => [r.file, r]));
74
+ const removed = detectRemovedFiles(existing, allFiles, rootDir);
75
+ const journalResult = tryJournalTier(db, existing, rootDir, removed, data.maxMtime);
76
+ if (journalResult) return journalResult;
77
+ return mtimeAndHashTiers(existing, allFiles, rootDir, removed);
78
+ }
79
+
80
+ // WASM / fallback path
63
81
  let hasTable = false;
64
82
  try {
65
- if (nativeDb) {
66
- nativeDb.queryGet('SELECT 1 FROM file_hashes LIMIT 1', []);
67
- } else {
68
- db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
69
- }
70
- // Query succeeded → table exists (result may be undefined if table is empty)
83
+ db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
71
84
  hasTable = true;
72
85
  } catch {
73
86
  /* table doesn't exist */
@@ -81,10 +94,7 @@ function getChangedFiles(
81
94
  };
82
95
  }
83
96
 
84
- const sql = 'SELECT file, hash, mtime, size FROM file_hashes';
85
- const rows = nativeDb
86
- ? (nativeDb.queryAll(sql, []) as unknown as FileHashRow[])
87
- : (db.prepare(sql).all() as FileHashRow[]);
97
+ const rows = db.prepare('SELECT file, hash, mtime, size FROM file_hashes').all() as FileHashRow[];
88
98
  const existing = new Map<string, FileHashRow>(rows.map((r) => [r.file, r]));
89
99
 
90
100
  const removed = detectRemovedFiles(existing, allFiles, rootDir);
@@ -116,14 +126,19 @@ function tryJournalTier(
116
126
  existing: Map<string, FileHashRow>,
117
127
  rootDir: string,
118
128
  removed: string[],
129
+ precomputedMaxMtime?: number,
119
130
  ): ChangeResult | null {
120
131
  const journal = readJournal(rootDir);
121
132
  if (!journal.valid) return null;
122
133
 
123
- const dbMtimes = db.prepare('SELECT MAX(mtime) as latest FROM file_hashes').get() as
124
- | { latest: number | null }
125
- | undefined;
126
- const latestDbMtime = dbMtimes?.latest || 0;
134
+ const latestDbMtime =
135
+ precomputedMaxMtime ??
136
+ ((
137
+ db.prepare('SELECT MAX(mtime) as latest FROM file_hashes').get() as
138
+ | { latest: number | null }
139
+ | undefined
140
+ )?.latest ||
141
+ 0);
127
142
  const hasJournalEntries = journal.changed!.length > 0 || journal.removed!.length > 0;
128
143
 
129
144
  if (!hasJournalEntries || journal.timestamp! < latestDbMtime) {
@@ -231,30 +246,42 @@ function mtimeAndHashTiers(
231
246
 
232
247
  async function runPendingAnalysis(ctx: PipelineContext): Promise<boolean> {
233
248
  const { db, opts, engineOpts, allFiles, rootDir } = ctx;
234
- const needsCfg =
235
- (opts as Record<string, unknown>).cfg !== false &&
236
- (() => {
237
- try {
238
- return (
239
- (db.prepare('SELECT COUNT(*) as c FROM cfg_blocks').get() as { c: number } | undefined)
240
- ?.c === 0
241
- );
242
- } catch {
243
- return true;
244
- }
245
- })();
246
- const needsDataflow =
247
- (opts as Record<string, unknown>).dataflow !== false &&
248
- (() => {
249
- try {
250
- return (
251
- (db.prepare('SELECT COUNT(*) as c FROM dataflow').get() as { c: number } | undefined)
252
- ?.c === 0
253
- );
254
- } catch {
255
- return true;
256
- }
257
- })();
249
+ const useNative = ctx.engineName === 'native' && !!ctx.nativeDb?.checkPendingAnalysis;
250
+
251
+ let needsCfg: boolean;
252
+ let needsDataflow: boolean;
253
+
254
+ if (useNative) {
255
+ const counts = ctx.nativeDb!.checkPendingAnalysis!();
256
+ needsCfg = (opts as Record<string, unknown>).cfg !== false && counts.cfgCount <= 0;
257
+ needsDataflow =
258
+ (opts as Record<string, unknown>).dataflow !== false && counts.dataflowCount <= 0;
259
+ } else {
260
+ needsCfg =
261
+ (opts as Record<string, unknown>).cfg !== false &&
262
+ (() => {
263
+ try {
264
+ return (
265
+ (db.prepare('SELECT COUNT(*) as c FROM cfg_blocks').get() as { c: number } | undefined)
266
+ ?.c === 0
267
+ );
268
+ } catch {
269
+ return true;
270
+ }
271
+ })();
272
+ needsDataflow =
273
+ (opts as Record<string, unknown>).dataflow !== false &&
274
+ (() => {
275
+ try {
276
+ return (
277
+ (db.prepare('SELECT COUNT(*) as c FROM dataflow').get() as { c: number } | undefined)
278
+ ?.c === 0
279
+ );
280
+ } catch {
281
+ return true;
282
+ }
283
+ })();
284
+ }
258
285
  if (!needsCfg && !needsDataflow) return false;
259
286
 
260
287
  info('No file changes. Running pending analysis pass...');
@@ -282,17 +309,27 @@ function healMetadata(ctx: PipelineContext): void {
282
309
  const { db, metadataUpdates } = ctx;
283
310
  if (!metadataUpdates || metadataUpdates.length === 0) return;
284
311
  try {
285
- const healHash = db.prepare(
286
- 'INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)',
287
- );
288
- const healTx = db.transaction(() => {
289
- for (const item of metadataUpdates) {
290
- const mtime = item.stat ? Math.floor(item.stat.mtime) : 0;
291
- const size = item.stat ? item.stat.size : 0;
292
- healHash.run(item.relPath, item.hash, mtime, size);
293
- }
294
- });
295
- healTx();
312
+ if (ctx.engineName === 'native' && ctx.nativeDb?.healFileMetadata) {
313
+ const entries = metadataUpdates.map((item) => ({
314
+ file: item.relPath,
315
+ hash: item.hash,
316
+ mtime: item.stat ? Math.floor(item.stat.mtime) : 0,
317
+ size: item.stat ? item.stat.size : 0,
318
+ }));
319
+ ctx.nativeDb.healFileMetadata(entries);
320
+ } else {
321
+ const healHash = db.prepare(
322
+ 'INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)',
323
+ );
324
+ const healTx = db.transaction(() => {
325
+ for (const item of metadataUpdates) {
326
+ const mtime = item.stat ? Math.floor(item.stat.mtime) : 0;
327
+ const size = item.stat ? item.stat.size : 0;
328
+ healHash.run(item.relPath, item.hash, mtime, size);
329
+ }
330
+ });
331
+ healTx();
332
+ }
296
333
  debug(`Self-healed mtime/size for ${metadataUpdates.length} files`);
297
334
  } catch {
298
335
  /* ignore heal errors */
@@ -303,9 +340,23 @@ function findReverseDependencies(
303
340
  db: BetterSqlite3Database,
304
341
  changedRelPaths: Set<string>,
305
342
  rootDir: string,
343
+ nativeDb?: NativeDatabase,
306
344
  ): Set<string> {
307
345
  const reverseDeps = new Set<string>();
308
346
  if (changedRelPaths.size === 0) return reverseDeps;
347
+
348
+ if (nativeDb?.findReverseDependencies) {
349
+ const changedArray = [...changedRelPaths];
350
+ const nativeResults = nativeDb.findReverseDependencies(changedArray);
351
+ for (const dep of nativeResults) {
352
+ const absPath = path.join(rootDir, dep);
353
+ if (fs.existsSync(absPath)) {
354
+ reverseDeps.add(dep);
355
+ }
356
+ }
357
+ return reverseDeps;
358
+ }
359
+
309
360
  const findReverseDepsStmt = db.prepare(`
310
361
  SELECT DISTINCT n_src.file FROM edges e
311
362
  JOIN nodes n_src ON e.source_id = n_src.id
@@ -360,7 +411,10 @@ function purgeAndAddReverseDeps(
360
411
  }
361
412
  }
362
413
 
363
- function detectHasEmbeddings(db: BetterSqlite3Database): boolean {
414
+ function detectHasEmbeddings(db: BetterSqlite3Database, nativeDb?: NativeDatabase): boolean {
415
+ if (nativeDb?.hasEmbeddings) {
416
+ return nativeDb.hasEmbeddings();
417
+ }
364
418
  try {
365
419
  db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
366
420
  return true;
@@ -371,14 +425,14 @@ function detectHasEmbeddings(db: BetterSqlite3Database): boolean {
371
425
 
372
426
  function handleScopedBuild(ctx: PipelineContext): void {
373
427
  const { db, rootDir, opts } = ctx;
374
- ctx.hasEmbeddings = detectHasEmbeddings(db);
428
+ ctx.hasEmbeddings = detectHasEmbeddings(db, ctx.nativeDb);
375
429
  const changePaths = ctx.parseChanges.map(
376
430
  (item) => item.relPath || normalizePath(path.relative(rootDir, item.file)),
377
431
  );
378
432
  let reverseDeps = new Set<string>();
379
433
  if (!(opts as Record<string, unknown>).noReverseDeps) {
380
434
  const changedRelPaths = new Set<string>([...changePaths, ...ctx.removed]);
381
- reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir);
435
+ reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir, ctx.nativeDb);
382
436
  }
383
437
  purgeAndAddReverseDeps(ctx, changePaths, reverseDeps);
384
438
  info(
@@ -388,7 +442,7 @@ function handleScopedBuild(ctx: PipelineContext): void {
388
442
 
389
443
  function handleFullBuild(ctx: PipelineContext): void {
390
444
  const { db } = ctx;
391
- const hasEmbeddings = detectHasEmbeddings(db);
445
+ const hasEmbeddings = detectHasEmbeddings(db, ctx.nativeDb);
392
446
  ctx.hasEmbeddings = hasEmbeddings;
393
447
  const deletions =
394
448
  'PRAGMA foreign_keys = OFF; DELETE FROM cfg_edges; DELETE FROM cfg_blocks; DELETE FROM node_metrics; DELETE FROM edges; DELETE FROM function_complexity; DELETE FROM dataflow; DELETE FROM ast_nodes; DELETE FROM nodes; PRAGMA foreign_keys = ON;';
@@ -401,7 +455,7 @@ function handleFullBuild(ctx: PipelineContext): void {
401
455
 
402
456
  function handleIncrementalBuild(ctx: PipelineContext): void {
403
457
  const { db, rootDir, opts } = ctx;
404
- ctx.hasEmbeddings = detectHasEmbeddings(db);
458
+ ctx.hasEmbeddings = detectHasEmbeddings(db, ctx.nativeDb);
405
459
  let reverseDeps = new Set<string>();
406
460
  if (!(opts as Record<string, unknown>).noReverseDeps) {
407
461
  const changedRelPaths = new Set<string>();
@@ -411,7 +465,7 @@ function handleIncrementalBuild(ctx: PipelineContext): void {
411
465
  for (const relPath of ctx.removed) {
412
466
  changedRelPaths.add(relPath);
413
467
  }
414
- reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir);
468
+ reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir, ctx.nativeDb);
415
469
  }
416
470
  info(
417
471
  `Incremental: ${ctx.parseChanges.length} changed, ${ctx.removed.length} removed${reverseDeps.size > 0 ? `, ${reverseDeps.size} reverse-deps` : ''}`,
@@ -114,11 +114,33 @@ function persistBuildMetadata(
114
114
  * Run advisory checks on full builds: orphaned embeddings, stale embeddings,
115
115
  * and unused exports. Informational only — does not affect correctness.
116
116
  */
117
- function runAdvisoryChecks(
118
- db: PipelineContext['db'],
119
- hasEmbeddings: boolean,
120
- buildNow: Date,
121
- ): void {
117
+ function runAdvisoryChecks(ctx: PipelineContext, hasEmbeddings: boolean, buildNow: Date): void {
118
+ // Batched native path: single napi call for all 3 advisory checks
119
+ if (ctx.engineName === 'native' && ctx.nativeDb?.runAdvisoryChecks) {
120
+ const result = ctx.nativeDb.runAdvisoryChecks(hasEmbeddings);
121
+ if (result.orphanedEmbeddings > 0) {
122
+ warn(
123
+ `${result.orphanedEmbeddings} embeddings are orphaned (nodes changed). Run "codegraph embed" to refresh.`,
124
+ );
125
+ }
126
+ if (result.embedBuiltAt) {
127
+ const embedTime = new Date(result.embedBuiltAt).getTime();
128
+ if (!Number.isNaN(embedTime) && embedTime < buildNow.getTime()) {
129
+ warn(
130
+ 'Embeddings were built before the last graph rebuild. Run "codegraph embed" to update.',
131
+ );
132
+ }
133
+ }
134
+ if (result.unusedExports > 0) {
135
+ warn(
136
+ `${result.unusedExports} exported symbol${result.unusedExports > 1 ? 's have' : ' has'} zero cross-file consumers. Run "codegraph exports <file> --unused" to inspect.`,
137
+ );
138
+ }
139
+ return;
140
+ }
141
+
142
+ const { db } = ctx;
143
+
122
144
  // Orphaned embeddings warning
123
145
  if (hasEmbeddings) {
124
146
  try {
@@ -197,9 +219,17 @@ export async function finalize(ctx: PipelineContext): Promise<void> {
197
219
  // both the stale-embeddings comparison and the persisted built_at metadata.
198
220
  const buildNow = new Date();
199
221
 
200
- const nodeCount = (ctx.db.prepare('SELECT COUNT(*) as c FROM nodes').get() as { c: number }).c;
201
- const actualEdgeCount = (ctx.db.prepare('SELECT COUNT(*) as c FROM edges').get() as { c: number })
202
- .c;
222
+ const useNative = ctx.engineName === 'native' && !!ctx.nativeDb?.getFinalizeCounts;
223
+ let nodeCount: number;
224
+ let actualEdgeCount: number;
225
+ if (useNative) {
226
+ const counts = ctx.nativeDb!.getFinalizeCounts!();
227
+ nodeCount = counts.nodeCount;
228
+ actualEdgeCount = counts.edgeCount;
229
+ } else {
230
+ nodeCount = (ctx.db.prepare('SELECT COUNT(*) as c FROM nodes').get() as { c: number }).c;
231
+ actualEdgeCount = (ctx.db.prepare('SELECT COUNT(*) as c FROM edges').get() as { c: number }).c;
232
+ }
203
233
  info(`Graph built: ${nodeCount} nodes, ${actualEdgeCount} edges`);
204
234
  info(`Stored in ${ctx.dbPath}`);
205
235
 
@@ -213,7 +243,7 @@ export async function finalize(ctx: PipelineContext): Promise<void> {
213
243
  'Finalize: skipping advisory queries (orphaned/stale embeddings, unused exports) for incremental build',
214
244
  );
215
245
  } else {
216
- runAdvisoryChecks(ctx.db, hasEmbeddings, buildNow);
246
+ runAdvisoryChecks(ctx, hasEmbeddings, buildNow);
217
247
  }
218
248
 
219
249
  // Intentionally measured before closeDb / writeJournalHeader / auto-registration:
@@ -39,12 +39,6 @@ interface PrecomputedFileData {
39
39
  // ── Native fast-path ─────────────────────────────────────────────────
40
40
 
41
41
  function tryNativeInsert(ctx: PipelineContext): boolean {
42
- // Disabled: bulkInsertNodes corrupts the DB when both the JS (better-sqlite3)
43
- // and Rust (rusqlite) connections are open to the same WAL-mode file.
44
- // The native path was never operational before — it always crashed on null
45
- // visibility serialisation. See #696 for the dual-connection fix.
46
- if (ctx.db) return false;
47
-
48
42
  // Use NativeDatabase persistent connection (Phase 6.15+).
49
43
  // Standalone napi functions were removed in 6.17 — falls through to JS if nativeDb unavailable.
50
44
  if (!ctx.nativeDb?.bulkInsertNodes) return false;
@@ -144,7 +138,24 @@ function tryNativeInsert(ctx: PipelineContext): boolean {
144
138
  fileHashes.push({ file: item.relPath, hash: item.hash, mtime, size });
145
139
  }
146
140
 
147
- return ctx.nativeDb!.bulkInsertNodes(batches, fileHashes, removed);
141
+ // WAL guard: same suspendJsDb/resumeJsDb pattern used by feature modules
142
+ // (ast, cfg, complexity, dataflow). Checkpoint JS side before native write,
143
+ // then checkpoint native side after, so neither library reads WAL frames
144
+ // written by the other (#696, #709, #715, #717).
145
+ let result: boolean;
146
+ try {
147
+ if (ctx.db) {
148
+ ctx.db.pragma('wal_checkpoint(TRUNCATE)');
149
+ }
150
+ result = ctx.nativeDb!.bulkInsertNodes(batches, fileHashes, removed);
151
+ } finally {
152
+ try {
153
+ ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
154
+ } catch {
155
+ /* ignore — nativeDb may already be closed */
156
+ }
157
+ }
158
+ return result;
148
159
  }
149
160
 
150
161
  // ── JS fallback: Phase 1 ────────────────────────────────────────────