@optave/codegraph 3.9.3 → 3.9.4
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 +8 -8
- package/dist/ast-analysis/visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitor.js +14 -0
- package/dist/ast-analysis/visitor.js.map +1 -1
- package/dist/domain/graph/builder/context.d.ts +15 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +7 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +92 -48
- 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 +67 -6
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +2 -2
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +51 -10
- 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 +10 -4
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/run-analyses.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/run-analyses.js +5 -20
- package/dist/domain/graph/builder/stages/run-analyses.js.map +1 -1
- package/dist/extractors/javascript.js +120 -0
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +14 -1
- package/dist/features/structure.js.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/visitor.ts +15 -0
- package/src/domain/graph/builder/context.ts +17 -0
- package/src/domain/graph/builder/pipeline.ts +93 -46
- package/src/domain/graph/builder/stages/build-edges.ts +80 -6
- package/src/domain/graph/builder/stages/build-structure.ts +2 -2
- package/src/domain/graph/builder/stages/detect-changes.ts +61 -12
- package/src/domain/graph/builder/stages/finalize.ts +11 -4
- package/src/domain/graph/builder/stages/run-analyses.ts +5 -26
- package/src/extractors/javascript.ts +142 -0
- package/src/features/structure.ts +17 -1
|
@@ -88,10 +88,17 @@ function checkEngineSchemaMismatch(ctx: PipelineContext): void {
|
|
|
88
88
|
);
|
|
89
89
|
ctx.forceFullRebuild = true;
|
|
90
90
|
}
|
|
91
|
+
// When the native engine is active, the Rust addon's version (ctx.engineVersion)
|
|
92
|
+
// is written into codegraph_version by setBuildMeta after a native orchestrator
|
|
93
|
+
// build. The check must compare against the same version, otherwise JS and Rust
|
|
94
|
+
// fight over which version to record — causing every incremental build to be
|
|
95
|
+
// promoted to a full rebuild when npm and crate versions diverge.
|
|
96
|
+
const effectiveVersion =
|
|
97
|
+
ctx.engineName === 'native' && ctx.engineVersion ? ctx.engineVersion : CODEGRAPH_VERSION;
|
|
91
98
|
const prevVersion = meta('codegraph_version');
|
|
92
|
-
if (prevVersion && prevVersion !==
|
|
99
|
+
if (prevVersion && prevVersion !== effectiveVersion) {
|
|
93
100
|
info(
|
|
94
|
-
`Codegraph version changed (${prevVersion} → ${
|
|
101
|
+
`Codegraph version changed (${prevVersion} → ${effectiveVersion}), promoting to full rebuild.`,
|
|
95
102
|
);
|
|
96
103
|
ctx.forceFullRebuild = true;
|
|
97
104
|
}
|
|
@@ -126,38 +133,15 @@ function setupPipeline(ctx: PipelineContext): void {
|
|
|
126
133
|
const native = enginePref !== 'wasm' ? loadNative() : null;
|
|
127
134
|
ctx.nativeAvailable = !!native?.NativeDatabase;
|
|
128
135
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
139
|
-
ctx.nativeDb.initSchema();
|
|
140
|
-
const proxy = new NativeDbProxy(ctx.nativeDb);
|
|
141
|
-
proxy.__lockPath = `${ctx.dbPath}.lock`;
|
|
142
|
-
ctx.db = proxy as unknown as typeof ctx.db;
|
|
143
|
-
ctx.nativeFirstProxy = true;
|
|
144
|
-
} catch (err) {
|
|
145
|
-
warn(`NativeDatabase setup failed, falling back to better-sqlite3: ${toErrorMessage(err)}`);
|
|
146
|
-
try {
|
|
147
|
-
ctx.nativeDb?.close();
|
|
148
|
-
} catch {
|
|
149
|
-
/* ignore */
|
|
150
|
-
}
|
|
151
|
-
ctx.nativeDb = undefined;
|
|
152
|
-
ctx.nativeFirstProxy = false;
|
|
153
|
-
releaseAdvisoryLock(`${ctx.dbPath}.lock`);
|
|
154
|
-
ctx.db = openDb(ctx.dbPath);
|
|
155
|
-
initSchema(ctx.db);
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
ctx.db = openDb(ctx.dbPath);
|
|
159
|
-
initSchema(ctx.db);
|
|
160
|
-
}
|
|
136
|
+
// Always use better-sqlite3 for setup — it's cheap (~4ms) and only needed
|
|
137
|
+
// for metadata reads (schema mismatch check). NativeDatabase.openReadWrite
|
|
138
|
+
// is deferred to tryNativeOrchestrator, saving ~60ms on incremental builds
|
|
139
|
+
// where the Rust orchestrator handles the full pipeline, and avoiding the
|
|
140
|
+
// cost entirely on no-op builds that exit before reaching the orchestrator.
|
|
141
|
+
const dir = path.dirname(ctx.dbPath);
|
|
142
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
143
|
+
ctx.db = openDb(ctx.dbPath);
|
|
144
|
+
initSchema(ctx.db);
|
|
161
145
|
|
|
162
146
|
ctx.config = loadConfig(ctx.rootDir);
|
|
163
147
|
ctx.incremental =
|
|
@@ -262,8 +246,6 @@ interface NativeOrchestratorResult {
|
|
|
262
246
|
changedCount?: number;
|
|
263
247
|
removedCount?: number;
|
|
264
248
|
isFullBuild?: boolean;
|
|
265
|
-
/** Full changed files including reverse-dep files — used by JS structure fallback. */
|
|
266
|
-
structureScope?: string[];
|
|
267
249
|
/** Whether the Rust pipeline handled the structure phase (small-incremental fast path). */
|
|
268
250
|
structureHandled?: boolean;
|
|
269
251
|
/** Whether the Rust pipeline wrote AST/complexity/CFG/dataflow to DB. */
|
|
@@ -584,15 +566,26 @@ async function tryNativeOrchestrator(
|
|
|
584
566
|
return undefined;
|
|
585
567
|
}
|
|
586
568
|
|
|
587
|
-
//
|
|
588
|
-
//
|
|
569
|
+
// Open NativeDatabase on demand — deferred from setupPipeline to skip the
|
|
570
|
+
// ~60ms cost on no-op/early-exit builds. Close the better-sqlite3 connection
|
|
571
|
+
// first to avoid dual-connection WAL corruption.
|
|
589
572
|
if (!ctx.nativeDb && ctx.nativeAvailable) {
|
|
590
573
|
const native = loadNative();
|
|
591
574
|
if (native?.NativeDatabase) {
|
|
592
575
|
try {
|
|
576
|
+
// Close better-sqlite3 before opening rusqlite to avoid WAL conflicts.
|
|
577
|
+
// Uses raw close() instead of closeDb() intentionally — the advisory lock
|
|
578
|
+
// is kept and transferred to the NativeDbProxy below, not released here.
|
|
579
|
+
ctx.db.close();
|
|
580
|
+
acquireAdvisoryLock(ctx.dbPath);
|
|
593
581
|
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
594
582
|
ctx.nativeDb.initSchema();
|
|
595
|
-
ctx.
|
|
583
|
+
// Replace ctx.db with a NativeDbProxy so post-native JS fallback
|
|
584
|
+
// (structure, analysis) can use it without reopening better-sqlite3.
|
|
585
|
+
const proxy = new NativeDbProxy(ctx.nativeDb);
|
|
586
|
+
proxy.__lockPath = `${ctx.dbPath}.lock`;
|
|
587
|
+
ctx.db = proxy as unknown as typeof ctx.db;
|
|
588
|
+
ctx.nativeFirstProxy = true;
|
|
596
589
|
} catch (err) {
|
|
597
590
|
warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
|
|
598
591
|
try {
|
|
@@ -601,6 +594,10 @@ async function tryNativeOrchestrator(
|
|
|
601
594
|
debug(`tryNativeOrchestrator: close failed during fallback: ${toErrorMessage(e)}`);
|
|
602
595
|
}
|
|
603
596
|
ctx.nativeDb = undefined;
|
|
597
|
+
ctx.nativeFirstProxy = false; // defensive: reset in case future refactors move the assignment above throwing lines
|
|
598
|
+
releaseAdvisoryLock(`${ctx.dbPath}.lock`);
|
|
599
|
+
// Reopen better-sqlite3 for JS pipeline fallback
|
|
600
|
+
ctx.db = openDb(ctx.dbPath);
|
|
604
601
|
}
|
|
605
602
|
}
|
|
606
603
|
}
|
|
@@ -631,14 +628,18 @@ async function tryNativeOrchestrator(
|
|
|
631
628
|
const p = result.phases;
|
|
632
629
|
|
|
633
630
|
// Sync build_meta so JS-side version/engine checks work on next build.
|
|
631
|
+
// Use the Rust addon version as codegraph_version when the native
|
|
632
|
+
// orchestrator performed the build — the Rust side's check_version_mismatch
|
|
633
|
+
// compares this value against CARGO_PKG_VERSION. Writing the JS
|
|
634
|
+
// CODEGRAPH_VERSION here would create a permanent mismatch whenever the
|
|
635
|
+
// npm package version diverges from the Rust crate version, forcing every
|
|
636
|
+
// subsequent native build to be a full rebuild (no incremental).
|
|
634
637
|
setBuildMeta(ctx.db, {
|
|
635
638
|
engine: ctx.engineName,
|
|
636
639
|
engine_version: ctx.engineVersion || '',
|
|
637
|
-
codegraph_version: CODEGRAPH_VERSION,
|
|
640
|
+
codegraph_version: ctx.engineVersion || CODEGRAPH_VERSION,
|
|
638
641
|
schema_version: String(ctx.schemaVersion),
|
|
639
642
|
built_at: new Date().toISOString(),
|
|
640
|
-
node_count: String(result.nodeCount ?? 0),
|
|
641
|
-
edge_count: String(result.edgeCount ?? 0),
|
|
642
643
|
});
|
|
643
644
|
|
|
644
645
|
info(
|
|
@@ -686,7 +687,7 @@ async function tryNativeOrchestrator(
|
|
|
686
687
|
ctx,
|
|
687
688
|
fileSymbols,
|
|
688
689
|
!!result.isFullBuild,
|
|
689
|
-
result.
|
|
690
|
+
result.changedFiles,
|
|
690
691
|
);
|
|
691
692
|
}
|
|
692
693
|
|
|
@@ -772,6 +773,23 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
772
773
|
|
|
773
774
|
await runAnalyses(ctx);
|
|
774
775
|
|
|
776
|
+
// Release WASM trees deterministically on the success path — same cleanup
|
|
777
|
+
// as the error-path catch block. Without this, trees stay allocated until
|
|
778
|
+
// GC collects ctx, holding WASM memory for the rest of the build (#931).
|
|
779
|
+
if (ctx.allSymbols?.size > 0) {
|
|
780
|
+
for (const [, symbols] of ctx.allSymbols) {
|
|
781
|
+
const tree = symbols._tree as { delete?: () => void } | undefined;
|
|
782
|
+
if (tree && typeof tree.delete === 'function') {
|
|
783
|
+
try {
|
|
784
|
+
tree.delete();
|
|
785
|
+
} catch {
|
|
786
|
+
/* ignore cleanup errors */
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
symbols._tree = undefined;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
775
793
|
// Flush Rust WAL writes (AST, complexity, CFG, dataflow) so the JS
|
|
776
794
|
// connection and any post-build readers can see them. One TRUNCATE
|
|
777
795
|
// here replaces the N per-feature resumeJsDb checkpoints (#checkpoint-opt).
|
|
@@ -816,13 +834,42 @@ export async function buildGraph(
|
|
|
816
834
|
if (nativeResult) return nativeResult;
|
|
817
835
|
} catch (err) {
|
|
818
836
|
warn(`Native build orchestrator failed, falling back to JS pipeline: ${toErrorMessage(err)}`);
|
|
819
|
-
//
|
|
837
|
+
// The version gate in checkEngineSchemaMismatch was skipped because
|
|
838
|
+
// nativeAvailable was true. Now that we're falling back to the JS
|
|
839
|
+
// pipeline, perform the codegraph_version check here so a version
|
|
840
|
+
// bump still promotes to a full rebuild (#928).
|
|
841
|
+
if (ctx.incremental && !ctx.forceFullRebuild) {
|
|
842
|
+
const prevVersion = getBuildMeta(ctx.db, 'codegraph_version');
|
|
843
|
+
if (prevVersion && prevVersion !== CODEGRAPH_VERSION) {
|
|
844
|
+
info(
|
|
845
|
+
`Codegraph version changed (${prevVersion} → ${CODEGRAPH_VERSION}), promoting to full rebuild.`,
|
|
846
|
+
);
|
|
847
|
+
ctx.forceFullRebuild = true;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
820
850
|
}
|
|
821
851
|
|
|
822
852
|
await runPipelineStages(ctx);
|
|
823
853
|
} catch (err) {
|
|
824
|
-
if (!ctx.earlyExit
|
|
825
|
-
|
|
854
|
+
if (!ctx.earlyExit) {
|
|
855
|
+
// Release WASM trees before closing DB to prevent V8 crash during
|
|
856
|
+
// GC cleanup of orphaned WASM objects (#931).
|
|
857
|
+
if (ctx.allSymbols?.size > 0) {
|
|
858
|
+
for (const [, symbols] of ctx.allSymbols) {
|
|
859
|
+
const tree = symbols._tree as { delete?: () => void } | undefined;
|
|
860
|
+
if (tree && typeof tree.delete === 'function') {
|
|
861
|
+
try {
|
|
862
|
+
tree.delete();
|
|
863
|
+
} catch {
|
|
864
|
+
/* ignore cleanup errors */
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
symbols._tree = undefined;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (ctx.db) {
|
|
871
|
+
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
872
|
+
}
|
|
826
873
|
}
|
|
827
874
|
throw err;
|
|
828
875
|
}
|
|
@@ -699,6 +699,69 @@ function buildClassHierarchyEdges(
|
|
|
699
699
|
}
|
|
700
700
|
}
|
|
701
701
|
|
|
702
|
+
// ── Reverse-dep edge reconnection (#932, #933) ─────────────────────────
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Reconnect edges that were saved before changed-file purge.
|
|
706
|
+
*
|
|
707
|
+
* Each saved edge records: sourceId (still valid — reverse-dep nodes were not
|
|
708
|
+
* purged) and target attributes (name, kind, file, line). The target node was
|
|
709
|
+
* deleted and re-inserted with a new ID by insertNodes. We look up the new ID
|
|
710
|
+
* by (name, kind, file) and re-create the edge.
|
|
711
|
+
*/
|
|
712
|
+
function reconnectReverseDepEdges(ctx: PipelineContext): void {
|
|
713
|
+
const { db } = ctx;
|
|
714
|
+
const findNodeStmt = db.prepare(
|
|
715
|
+
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? ORDER BY ABS(line - ?) LIMIT 1',
|
|
716
|
+
);
|
|
717
|
+
const reconnectedRows: EdgeRowTuple[] = [];
|
|
718
|
+
let dropped = 0;
|
|
719
|
+
|
|
720
|
+
for (const saved of ctx.savedReverseDepEdges) {
|
|
721
|
+
const newTarget = findNodeStmt.get(
|
|
722
|
+
saved.tgtName,
|
|
723
|
+
saved.tgtKind,
|
|
724
|
+
saved.tgtFile,
|
|
725
|
+
saved.tgtLine,
|
|
726
|
+
) as { id: number } | undefined;
|
|
727
|
+
if (newTarget) {
|
|
728
|
+
reconnectedRows.push([
|
|
729
|
+
saved.sourceId,
|
|
730
|
+
newTarget.id,
|
|
731
|
+
saved.edgeKind,
|
|
732
|
+
saved.confidence,
|
|
733
|
+
saved.dynamic,
|
|
734
|
+
]);
|
|
735
|
+
} else {
|
|
736
|
+
// Target was removed or renamed in the changed file — edge is stale
|
|
737
|
+
dropped++;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (reconnectedRows.length > 0) {
|
|
742
|
+
if (ctx.nativeDb?.bulkInsertEdges) {
|
|
743
|
+
const nativeEdges = reconnectedRows.map((r) => ({
|
|
744
|
+
sourceId: r[0],
|
|
745
|
+
targetId: r[1],
|
|
746
|
+
kind: r[2],
|
|
747
|
+
confidence: r[3],
|
|
748
|
+
dynamic: r[4],
|
|
749
|
+
}));
|
|
750
|
+
const ok = ctx.nativeDb.bulkInsertEdges(nativeEdges);
|
|
751
|
+
if (!ok) {
|
|
752
|
+
batchInsertEdges(db, reconnectedRows);
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
batchInsertEdges(db, reconnectedRows);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
debug(
|
|
760
|
+
`Reconnected ${reconnectedRows.length} reverse-dep edges` +
|
|
761
|
+
(dropped > 0 ? ` (${dropped} dropped — targets removed/renamed)` : ''),
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
702
765
|
// ── Main entry point ────────────────────────────────────────────────────
|
|
703
766
|
|
|
704
767
|
/**
|
|
@@ -798,10 +861,11 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
|
|
|
798
861
|
}
|
|
799
862
|
}
|
|
800
863
|
|
|
801
|
-
// Skip native import-edge path for small incremental builds
|
|
802
|
-
//
|
|
864
|
+
// Skip native import-edge path for small incremental builds: napi-rs
|
|
865
|
+
// marshaling overhead (~13ms) exceeds Rust computation savings at this scale.
|
|
803
866
|
const useNativeImportEdges =
|
|
804
|
-
native?.buildImportEdges &&
|
|
867
|
+
native?.buildImportEdges &&
|
|
868
|
+
(ctx.isFullBuild || ctx.fileSymbols.size > ctx.config.build.smallFilesThreshold);
|
|
805
869
|
if (useNativeImportEdges) {
|
|
806
870
|
const beforeLen = allEdgeRows.length;
|
|
807
871
|
buildImportEdgesNative(ctx, getNodeIdStmt, allEdgeRows, native!);
|
|
@@ -821,10 +885,11 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
|
|
|
821
885
|
buildImportEdges(ctx, getNodeIdStmt, allEdgeRows);
|
|
822
886
|
}
|
|
823
887
|
|
|
824
|
-
// Skip native call-edge path for small incremental builds
|
|
825
|
-
//
|
|
888
|
+
// Skip native call-edge path for small incremental builds: napi-rs
|
|
889
|
+
// marshaling overhead for allNodes exceeds Rust computation savings.
|
|
826
890
|
const useNativeCallEdges =
|
|
827
|
-
native?.buildCallEdges &&
|
|
891
|
+
native?.buildCallEdges &&
|
|
892
|
+
(ctx.isFullBuild || ctx.fileSymbols.size > ctx.config.build.smallFilesThreshold);
|
|
828
893
|
if (useNativeCallEdges) {
|
|
829
894
|
buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodesBefore, native!);
|
|
830
895
|
} else {
|
|
@@ -858,5 +923,14 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
|
|
|
858
923
|
}
|
|
859
924
|
}
|
|
860
925
|
|
|
926
|
+
// Phase 3: Reconnect saved reverse-dep edges (#932, #933).
|
|
927
|
+
// When the WASM/JS path purged changed files, edges FROM reverse-dep files TO
|
|
928
|
+
// those files were deleted (target-side). The reverse-dep files were NOT
|
|
929
|
+
// reparsed — instead we saved the edge topology before purge and now reconnect
|
|
930
|
+
// each edge to the new node IDs created by insertNodes.
|
|
931
|
+
if (ctx.savedReverseDepEdges.length > 0) {
|
|
932
|
+
reconnectReverseDepEdges(ctx);
|
|
933
|
+
}
|
|
934
|
+
|
|
861
935
|
ctx.timing.edgesMs = performance.now() - t0;
|
|
862
936
|
}
|
|
@@ -174,13 +174,13 @@ function updateChangedFileMetrics(ctx: PipelineContext, changedFiles: string[]):
|
|
|
174
174
|
SELECT COUNT(DISTINCT n_src.file) AS cnt FROM edges e
|
|
175
175
|
JOIN nodes n_src ON e.source_id = n_src.id
|
|
176
176
|
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
177
|
-
WHERE e.kind
|
|
177
|
+
WHERE e.kind IN ('imports', 'imports-type') AND n_tgt.file = ? AND n_src.file != n_tgt.file
|
|
178
178
|
`);
|
|
179
179
|
const getFanOut = db.prepare(`
|
|
180
180
|
SELECT COUNT(DISTINCT n_tgt.file) AS cnt FROM edges e
|
|
181
181
|
JOIN nodes n_src ON e.source_id = n_src.id
|
|
182
182
|
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
183
|
-
WHERE e.kind
|
|
183
|
+
WHERE e.kind IN ('imports', 'imports-type') AND n_src.file = ? AND n_src.file != n_tgt.file
|
|
184
184
|
`);
|
|
185
185
|
const upsertMetric = db.prepare(`
|
|
186
186
|
INSERT OR REPLACE INTO node_metrics
|
|
@@ -374,24 +374,73 @@ function purgeAndAddReverseDeps(
|
|
|
374
374
|
// Prefer NativeDatabase: purge + reverse-dep edge deletion in one transaction (#670)
|
|
375
375
|
if (ctx.engineName === 'native' && ctx.nativeDb?.purgeFilesData) {
|
|
376
376
|
ctx.nativeDb.purgeFilesData(filesToPurge, false, hasReverseDeps ? reverseDepList : undefined);
|
|
377
|
+
// Native path still reparses reverse-deps (works correctly with native edge builder)
|
|
378
|
+
for (const relPath of reverseDeps) {
|
|
379
|
+
const absPath = path.join(rootDir, relPath);
|
|
380
|
+
ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
|
|
381
|
+
}
|
|
377
382
|
} else {
|
|
383
|
+
// WASM/JS path: save edges from reverse-dep files → changed files BEFORE
|
|
384
|
+
// purge, then reconnect them to new node IDs after insertNodes (#932, #933).
|
|
385
|
+
//
|
|
386
|
+
// purgeFilesFromGraph deletes edges in BOTH directions for changed files,
|
|
387
|
+
// which already removes the reverse-dep → changed-file edges. The old
|
|
388
|
+
// approach then over-deleted ALL outgoing edges from reverse-dep files and
|
|
389
|
+
// reparsed them to rebuild everything — expensive (87 extra parses) and
|
|
390
|
+
// lossy (442 missing edges due to imperfect resolution on rebuild).
|
|
391
|
+
//
|
|
392
|
+
// New approach: save the edge topology, let purge handle deletion, then
|
|
393
|
+
// reconnect using new node IDs. No reparse needed.
|
|
394
|
+
if (hasReverseDeps && hasPurge) {
|
|
395
|
+
const changePathSet = new Set(changePaths);
|
|
396
|
+
const saveEdgesStmt = db.prepare(`
|
|
397
|
+
SELECT e.source_id, n_tgt.name AS tgt_name, n_tgt.kind AS tgt_kind,
|
|
398
|
+
n_tgt.file AS tgt_file, n_tgt.line AS tgt_line,
|
|
399
|
+
e.kind AS edge_kind, e.confidence, e.dynamic,
|
|
400
|
+
n_src.file AS src_file
|
|
401
|
+
FROM edges e
|
|
402
|
+
JOIN nodes n_src ON e.source_id = n_src.id
|
|
403
|
+
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
404
|
+
WHERE n_tgt.file = ? AND n_src.file != n_tgt.file
|
|
405
|
+
`);
|
|
406
|
+
for (const changedPath of changePaths) {
|
|
407
|
+
for (const row of saveEdgesStmt.all(changedPath) as Array<{
|
|
408
|
+
source_id: number;
|
|
409
|
+
tgt_name: string;
|
|
410
|
+
tgt_kind: string;
|
|
411
|
+
tgt_file: string;
|
|
412
|
+
tgt_line: number;
|
|
413
|
+
edge_kind: string;
|
|
414
|
+
confidence: number;
|
|
415
|
+
dynamic: number;
|
|
416
|
+
src_file: string;
|
|
417
|
+
}>) {
|
|
418
|
+
// Skip edges whose source is also being purged — buildEdges will
|
|
419
|
+
// re-create them with correct new IDs.
|
|
420
|
+
if (changePathSet.has(row.src_file)) continue;
|
|
421
|
+
ctx.savedReverseDepEdges.push({
|
|
422
|
+
sourceId: row.source_id,
|
|
423
|
+
tgtName: row.tgt_name,
|
|
424
|
+
tgtKind: row.tgt_kind,
|
|
425
|
+
tgtFile: row.tgt_file,
|
|
426
|
+
tgtLine: row.tgt_line,
|
|
427
|
+
edgeKind: row.edge_kind,
|
|
428
|
+
confidence: row.confidence,
|
|
429
|
+
dynamic: row.dynamic,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
debug(`Saved ${ctx.savedReverseDepEdges.length} reverse-dep edges for reconnection`);
|
|
434
|
+
}
|
|
435
|
+
|
|
378
436
|
if (hasPurge) {
|
|
379
437
|
purgeFilesFromGraph(db, filesToPurge, { purgeHashes: false });
|
|
380
438
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
);
|
|
385
|
-
for (const relPath of reverseDepList) {
|
|
386
|
-
deleteOutgoingEdgesForFile.run(relPath);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
439
|
+
// No outgoing-edge deletion for reverse-deps — purge already removed
|
|
440
|
+
// edges targeting the changed files, and other outgoing edges are valid.
|
|
441
|
+
// No reverse-deps added to parseChanges — no reparse needed.
|
|
389
442
|
}
|
|
390
443
|
}
|
|
391
|
-
for (const relPath of reverseDeps) {
|
|
392
|
-
const absPath = path.join(rootDir, relPath);
|
|
393
|
-
ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
|
|
394
|
-
}
|
|
395
444
|
}
|
|
396
445
|
|
|
397
446
|
function detectHasEmbeddings(db: BetterSqlite3Database, nativeDb?: NativeDatabase): boolean {
|
|
@@ -81,13 +81,20 @@ function persistBuildMetadata(
|
|
|
81
81
|
): void {
|
|
82
82
|
const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
|
|
83
83
|
if (!ctx.isFullBuild && ctx.allSymbols.size <= 3) return;
|
|
84
|
+
// When the native engine is active, persist the Rust addon version so that
|
|
85
|
+
// checkEngineSchemaMismatch compares against the same value on the next build.
|
|
86
|
+
// Writing CODEGRAPH_VERSION (the npm package version) here would create a
|
|
87
|
+
// permanent mismatch whenever npm and crate versions diverge, forcing every
|
|
88
|
+
// subsequent build to be a full rebuild.
|
|
89
|
+
const codeVersionToWrite =
|
|
90
|
+
ctx.engineName === 'native' && ctx.engineVersion ? ctx.engineVersion : CODEGRAPH_VERSION;
|
|
84
91
|
try {
|
|
85
92
|
if (useNativeDb) {
|
|
86
93
|
ctx.nativeDb!.setBuildMeta(
|
|
87
94
|
Object.entries({
|
|
88
95
|
engine: ctx.engineName,
|
|
89
|
-
engine_version:
|
|
90
|
-
codegraph_version:
|
|
96
|
+
engine_version: codeVersionToWrite,
|
|
97
|
+
codegraph_version: codeVersionToWrite,
|
|
91
98
|
schema_version: String(ctx.schemaVersion),
|
|
92
99
|
built_at: buildNow.toISOString(),
|
|
93
100
|
node_count: String(nodeCount),
|
|
@@ -97,8 +104,8 @@ function persistBuildMetadata(
|
|
|
97
104
|
} else {
|
|
98
105
|
setBuildMeta(ctx.db, {
|
|
99
106
|
engine: ctx.engineName,
|
|
100
|
-
engine_version:
|
|
101
|
-
codegraph_version:
|
|
107
|
+
engine_version: codeVersionToWrite,
|
|
108
|
+
codegraph_version: codeVersionToWrite,
|
|
102
109
|
schema_version: String(ctx.schemaVersion),
|
|
103
110
|
built_at: buildNow.toISOString(),
|
|
104
111
|
node_count: nodeCount,
|
|
@@ -2,39 +2,18 @@
|
|
|
2
2
|
* Stage: runAnalyses
|
|
3
3
|
*
|
|
4
4
|
* Dispatches to the unified AST analysis engine (AST nodes, complexity, CFG, dataflow).
|
|
5
|
-
*
|
|
5
|
+
* Reverse-dep files are no longer in allSymbols (they are not reparsed since #932/#933),
|
|
6
|
+
* so no filtering is needed here.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import type { ExtractorOutput } from '../../../../types.js';
|
|
8
|
+
import { warn } from '../../../../infrastructure/logger.js';
|
|
9
9
|
import type { PipelineContext } from '../context.js';
|
|
10
10
|
|
|
11
11
|
export async function runAnalyses(ctx: PipelineContext): Promise<void> {
|
|
12
|
-
const { db, allSymbols, rootDir, opts, engineOpts
|
|
13
|
-
|
|
14
|
-
// For incremental builds, exclude reverse-dep-only files
|
|
15
|
-
let astComplexitySymbols: Map<string, ExtractorOutput> = allSymbols;
|
|
16
|
-
if (!isFullBuild) {
|
|
17
|
-
const reverseDepFiles = new Set(
|
|
18
|
-
filesToParse
|
|
19
|
-
.filter((item) => (item as { _reverseDepOnly?: boolean })._reverseDepOnly)
|
|
20
|
-
.map((item) => item.relPath),
|
|
21
|
-
);
|
|
22
|
-
if (reverseDepFiles.size > 0) {
|
|
23
|
-
astComplexitySymbols = new Map();
|
|
24
|
-
for (const [relPath, symbols] of allSymbols) {
|
|
25
|
-
if (!reverseDepFiles.has(relPath)) {
|
|
26
|
-
astComplexitySymbols.set(relPath, symbols);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
debug(
|
|
30
|
-
`AST/complexity/CFG/dataflow: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
12
|
+
const { db, allSymbols, rootDir, opts, engineOpts } = ctx;
|
|
34
13
|
|
|
35
14
|
const { runAnalyses: runAnalysesFn } = await import('../../../../ast-analysis/engine.js');
|
|
36
15
|
try {
|
|
37
|
-
const analysisTiming = await runAnalysesFn(db,
|
|
16
|
+
const analysisTiming = await runAnalysesFn(db, allSymbols, rootDir, opts, engineOpts);
|
|
38
17
|
ctx.timing.astMs = analysisTiming.astMs;
|
|
39
18
|
ctx.timing.complexityMs = analysisTiming.complexityMs;
|
|
40
19
|
ctx.timing.cfgMs = analysisTiming.cfgMs;
|