@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.
Files changed (40) hide show
  1. package/README.md +8 -8
  2. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  3. package/dist/ast-analysis/visitor.js +14 -0
  4. package/dist/ast-analysis/visitor.js.map +1 -1
  5. package/dist/domain/graph/builder/context.d.ts +15 -0
  6. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  7. package/dist/domain/graph/builder/context.js +7 -0
  8. package/dist/domain/graph/builder/context.js.map +1 -1
  9. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  10. package/dist/domain/graph/builder/pipeline.js +92 -48
  11. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  12. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  13. package/dist/domain/graph/builder/stages/build-edges.js +67 -6
  14. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  15. package/dist/domain/graph/builder/stages/build-structure.js +2 -2
  16. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  17. package/dist/domain/graph/builder/stages/detect-changes.js +51 -10
  18. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  19. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  20. package/dist/domain/graph/builder/stages/finalize.js +10 -4
  21. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  22. package/dist/domain/graph/builder/stages/run-analyses.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/stages/run-analyses.js +5 -20
  24. package/dist/domain/graph/builder/stages/run-analyses.js.map +1 -1
  25. package/dist/extractors/javascript.js +120 -0
  26. package/dist/extractors/javascript.js.map +1 -1
  27. package/dist/features/structure.d.ts.map +1 -1
  28. package/dist/features/structure.js +14 -1
  29. package/dist/features/structure.js.map +1 -1
  30. package/package.json +7 -7
  31. package/src/ast-analysis/visitor.ts +15 -0
  32. package/src/domain/graph/builder/context.ts +17 -0
  33. package/src/domain/graph/builder/pipeline.ts +93 -46
  34. package/src/domain/graph/builder/stages/build-edges.ts +80 -6
  35. package/src/domain/graph/builder/stages/build-structure.ts +2 -2
  36. package/src/domain/graph/builder/stages/detect-changes.ts +61 -12
  37. package/src/domain/graph/builder/stages/finalize.ts +11 -4
  38. package/src/domain/graph/builder/stages/run-analyses.ts +5 -26
  39. package/src/extractors/javascript.ts +142 -0
  40. 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 !== CODEGRAPH_VERSION) {
99
+ if (prevVersion && prevVersion !== effectiveVersion) {
93
100
  info(
94
- `Codegraph version changed (${prevVersion} → ${CODEGRAPH_VERSION}), promoting to full rebuild.`,
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
- // When native is available, use a NativeDbProxy backed by a single rusqlite
130
- // connection. This eliminates the dual-connection WAL corruption problem.
131
- // The Rust orchestrator handles the full pipeline; the proxy is used for any
132
- // JS post-processing (e.g. structure fallback on large builds).
133
- if (ctx.nativeAvailable && native?.NativeDatabase) {
134
- try {
135
- const dir = path.dirname(ctx.dbPath);
136
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
137
- acquireAdvisoryLock(ctx.dbPath);
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
- // In native-first mode, nativeDb is already open from setupPipeline.
588
- // Otherwise, open it on demand (deferred to skip overhead on no-op rebuilds).
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.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
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.structureScope ?? result.changedFiles,
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
- // Fall through to JS pipeline
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 && ctx.db) {
825
- closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
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 (≤3 files):
802
- // napi-rs marshaling overhead exceeds computation savings.
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 && (ctx.isFullBuild || ctx.fileSymbols.size > 3);
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 (≤3 files):
825
- // napi-rs marshaling overhead for allNodes exceeds computation savings.
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 && (ctx.isFullBuild || ctx.fileSymbols.size > 3);
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 = 'imports' AND n_tgt.file = ? AND n_src.file != n_tgt.file
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 = 'imports' AND n_src.file = ? AND n_src.file != n_tgt.file
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
- if (hasReverseDeps) {
382
- const deleteOutgoingEdgesForFile = db.prepare(
383
- 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
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: CODEGRAPH_VERSION,
90
- codegraph_version: 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: CODEGRAPH_VERSION,
101
- codegraph_version: 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
- * Filters out reverse-dep files for incremental builds.
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 { debug, warn } from '../../../../infrastructure/logger.js';
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, isFullBuild, filesToParse } = ctx;
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, astComplexitySymbols, rootDir, opts, engineOpts);
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;