@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.
- package/README.md +25 -14
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +158 -1
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
- package/dist/ast-analysis/rules/javascript.js +0 -1
- package/dist/ast-analysis/rules/javascript.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +2 -75
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/cli/commands/ast.js +2 -2
- package/dist/cli/commands/ast.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +128 -6
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +101 -1
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.js +17 -5
- package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +98 -50
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +32 -5
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +20 -7
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/parser.d.ts +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +88 -4
- package/dist/domain/parser.js.map +1 -1
- package/dist/extractors/clojure.d.ts +12 -0
- package/dist/extractors/clojure.d.ts.map +1 -0
- package/dist/extractors/clojure.js +245 -0
- package/dist/extractors/clojure.js.map +1 -0
- package/dist/extractors/cuda.d.ts +11 -0
- package/dist/extractors/cuda.d.ts.map +1 -0
- package/dist/extractors/cuda.js +302 -0
- package/dist/extractors/cuda.js.map +1 -0
- package/dist/extractors/erlang.d.ts +14 -0
- package/dist/extractors/erlang.d.ts.map +1 -0
- package/dist/extractors/erlang.js +239 -0
- package/dist/extractors/erlang.js.map +1 -0
- package/dist/extractors/fsharp.d.ts +13 -0
- package/dist/extractors/fsharp.d.ts.map +1 -0
- package/dist/extractors/fsharp.js +218 -0
- package/dist/extractors/fsharp.js.map +1 -0
- package/dist/extractors/gleam.d.ts +14 -0
- package/dist/extractors/gleam.d.ts.map +1 -0
- package/dist/extractors/gleam.js +229 -0
- package/dist/extractors/gleam.js.map +1 -0
- package/dist/extractors/groovy.d.ts +10 -0
- package/dist/extractors/groovy.d.ts.map +1 -0
- package/dist/extractors/groovy.js +304 -0
- package/dist/extractors/groovy.js.map +1 -0
- package/dist/extractors/index.d.ts +11 -0
- package/dist/extractors/index.d.ts.map +1 -1
- package/dist/extractors/index.js +11 -0
- package/dist/extractors/index.js.map +1 -1
- package/dist/extractors/julia.d.ts +16 -0
- package/dist/extractors/julia.d.ts.map +1 -0
- package/dist/extractors/julia.js +287 -0
- package/dist/extractors/julia.js.map +1 -0
- package/dist/extractors/objc.d.ts +9 -0
- package/dist/extractors/objc.d.ts.map +1 -0
- package/dist/extractors/objc.js +406 -0
- package/dist/extractors/objc.js.map +1 -0
- package/dist/extractors/ocaml.js +74 -0
- package/dist/extractors/ocaml.js.map +1 -1
- package/dist/extractors/r.d.ts +13 -0
- package/dist/extractors/r.d.ts.map +1 -0
- package/dist/extractors/r.js +251 -0
- package/dist/extractors/r.js.map +1 -0
- package/dist/extractors/solidity.d.ts +9 -0
- package/dist/extractors/solidity.d.ts.map +1 -0
- package/dist/extractors/solidity.js +374 -0
- package/dist/extractors/solidity.js.map +1 -0
- package/dist/extractors/verilog.d.ts +9 -0
- package/dist/extractors/verilog.d.ts.map +1 -0
- package/dist/extractors/verilog.js +286 -0
- package/dist/extractors/verilog.js.map +1 -0
- package/dist/features/ast.d.ts.map +1 -1
- package/dist/features/ast.js +1 -2
- package/dist/features/ast.js.map +1 -1
- package/dist/graph/algorithms/bfs.d.ts +2 -0
- package/dist/graph/algorithms/bfs.d.ts.map +1 -1
- package/dist/graph/algorithms/bfs.js +27 -0
- package/dist/graph/algorithms/bfs.js.map +1 -1
- package/dist/graph/algorithms/centrality.d.ts +2 -0
- package/dist/graph/algorithms/centrality.d.ts.map +1 -1
- package/dist/graph/algorithms/centrality.js +28 -0
- package/dist/graph/algorithms/centrality.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts +3 -4
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +29 -0
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/graph/algorithms/shortest-path.d.ts +2 -0
- package/dist/graph/algorithms/shortest-path.d.ts.map +1 -1
- package/dist/graph/algorithms/shortest-path.js +18 -1
- package/dist/graph/algorithms/shortest-path.js.map +1 -1
- package/dist/types.d.ts +122 -2
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-clojure.wasm +0 -0
- package/grammars/tree-sitter-cuda.wasm +0 -0
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-fsharp.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/grammars/tree-sitter-groovy.wasm +0 -0
- package/grammars/tree-sitter-julia.wasm +0 -0
- package/grammars/tree-sitter-objc.wasm +0 -0
- package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
- package/grammars/tree-sitter-r.wasm +0 -0
- package/grammars/tree-sitter-solidity.wasm +0 -0
- package/grammars/tree-sitter-verilog.wasm +0 -0
- package/package.json +18 -7
- package/src/ast-analysis/engine.ts +183 -1
- package/src/ast-analysis/rules/javascript.ts +0 -1
- package/src/ast-analysis/visitors/ast-store-visitor.ts +2 -75
- package/src/cli/commands/ast.ts +2 -2
- package/src/domain/graph/builder/pipeline.ts +142 -6
- package/src/domain/graph/builder/stages/build-edges.ts +158 -1
- package/src/domain/graph/builder/stages/collect-files.ts +18 -7
- package/src/domain/graph/builder/stages/detect-changes.ts +109 -55
- package/src/domain/graph/builder/stages/finalize.ts +39 -9
- package/src/domain/graph/builder/stages/insert-nodes.ts +18 -7
- package/src/domain/parser.ts +108 -2
- package/src/extractors/clojure.ts +273 -0
- package/src/extractors/cuda.ts +316 -0
- package/src/extractors/erlang.ts +252 -0
- package/src/extractors/fsharp.ts +253 -0
- package/src/extractors/gleam.ts +246 -0
- package/src/extractors/groovy.ts +332 -0
- package/src/extractors/index.ts +11 -0
- package/src/extractors/julia.ts +318 -0
- package/src/extractors/objc.ts +431 -0
- package/src/extractors/ocaml.ts +78 -0
- package/src/extractors/r.ts +253 -0
- package/src/extractors/solidity.ts +398 -0
- package/src/extractors/verilog.ts +315 -0
- package/src/features/ast.ts +1 -2
- package/src/graph/algorithms/bfs.ts +34 -0
- package/src/graph/algorithms/centrality.ts +30 -0
- package/src/graph/algorithms/louvain.ts +31 -4
- package/src/graph/algorithms/shortest-path.ts +20 -1
- package/src/types.ts +117 -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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
46
|
-
(
|
|
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
|
-
|
|
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
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
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
|
-
|
|
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 ────────────────────────────────────────────
|