@optave/codegraph 3.9.2 → 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 +93 -10
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +64 -0
- package/dist/ast-analysis/engine.js.map +1 -1
- 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/analysis/diff-impact.d.ts +12 -0
- package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
- package/dist/domain/analysis/diff-impact.js +20 -1
- package/dist/domain/analysis/diff-impact.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/native-db-proxy.d.ts.map +1 -1
- package/dist/domain/graph/builder/native-db-proxy.js +8 -4
- package/dist/domain/graph/builder/native-db-proxy.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +176 -127
- 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/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +6 -2
- package/dist/domain/parser.js.map +1 -1
- package/dist/extractors/javascript.js +120 -0
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/ast.js +2 -2
- package/dist/features/ast.js.map +1 -1
- package/dist/features/cfg.d.ts +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +52 -6
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +7 -0
- package/dist/features/complexity.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/dist/infrastructure/update-check.d.ts +1 -1
- package/dist/infrastructure/update-check.js +3 -3
- package/dist/infrastructure/update-check.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +83 -0
- package/src/ast-analysis/visitor.ts +15 -0
- package/src/domain/analysis/diff-impact.ts +28 -1
- package/src/domain/graph/builder/context.ts +17 -0
- package/src/domain/graph/builder/native-db-proxy.ts +10 -4
- package/src/domain/graph/builder/pipeline.ts +196 -130
- 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/domain/parser.ts +6 -2
- package/src/extractors/javascript.ts +142 -0
- package/src/features/ast.ts +2 -2
- package/src/features/cfg.ts +51 -6
- package/src/features/complexity.ts +7 -0
- package/src/features/structure.ts +17 -1
- package/src/infrastructure/update-check.ts +3 -3
- package/src/types.ts +1 -0
|
@@ -23,7 +23,13 @@ import { loadNative } from '../../../infrastructure/native.js';
|
|
|
23
23
|
import { semverCompare } from '../../../infrastructure/update-check.js';
|
|
24
24
|
import { toErrorMessage } from '../../../shared/errors.js';
|
|
25
25
|
import { CODEGRAPH_VERSION } from '../../../shared/version.js';
|
|
26
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
BetterSqlite3Database,
|
|
28
|
+
BuildGraphOpts,
|
|
29
|
+
BuildResult,
|
|
30
|
+
Definition,
|
|
31
|
+
ExtractorOutput,
|
|
32
|
+
} from '../../../types.js';
|
|
27
33
|
import { getActiveEngine } from '../../parser.js';
|
|
28
34
|
import { setWorkspaces } from '../resolve.js';
|
|
29
35
|
import { PipelineContext } from './context.js';
|
|
@@ -82,10 +88,17 @@ function checkEngineSchemaMismatch(ctx: PipelineContext): void {
|
|
|
82
88
|
);
|
|
83
89
|
ctx.forceFullRebuild = true;
|
|
84
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;
|
|
85
98
|
const prevVersion = meta('codegraph_version');
|
|
86
|
-
if (prevVersion && prevVersion !==
|
|
99
|
+
if (prevVersion && prevVersion !== effectiveVersion) {
|
|
87
100
|
info(
|
|
88
|
-
`Codegraph version changed (${prevVersion} → ${
|
|
101
|
+
`Codegraph version changed (${prevVersion} → ${effectiveVersion}), promoting to full rebuild.`,
|
|
89
102
|
);
|
|
90
103
|
ctx.forceFullRebuild = true;
|
|
91
104
|
}
|
|
@@ -120,42 +133,15 @@ function setupPipeline(ctx: PipelineContext): void {
|
|
|
120
133
|
const native = enginePref !== 'wasm' ? loadNative() : null;
|
|
121
134
|
ctx.nativeAvailable = !!native?.NativeDatabase;
|
|
122
135
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
try {
|
|
133
|
-
const dir = path.dirname(ctx.dbPath);
|
|
134
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
135
|
-
acquireAdvisoryLock(ctx.dbPath);
|
|
136
|
-
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
137
|
-
ctx.nativeDb.initSchema();
|
|
138
|
-
const proxy = new NativeDbProxy(ctx.nativeDb);
|
|
139
|
-
proxy.__lockPath = `${ctx.dbPath}.lock`;
|
|
140
|
-
ctx.db = proxy as unknown as typeof ctx.db;
|
|
141
|
-
ctx.nativeFirstProxy = true;
|
|
142
|
-
} catch (err) {
|
|
143
|
-
warn(`NativeDatabase setup failed, falling back to better-sqlite3: ${toErrorMessage(err)}`);
|
|
144
|
-
try {
|
|
145
|
-
ctx.nativeDb?.close();
|
|
146
|
-
} catch {
|
|
147
|
-
/* ignore */
|
|
148
|
-
}
|
|
149
|
-
ctx.nativeDb = undefined;
|
|
150
|
-
ctx.nativeFirstProxy = false;
|
|
151
|
-
releaseAdvisoryLock(`${ctx.dbPath}.lock`);
|
|
152
|
-
ctx.db = openDb(ctx.dbPath);
|
|
153
|
-
initSchema(ctx.db);
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
ctx.db = openDb(ctx.dbPath);
|
|
157
|
-
initSchema(ctx.db);
|
|
158
|
-
}
|
|
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);
|
|
159
145
|
|
|
160
146
|
ctx.config = loadConfig(ctx.rootDir);
|
|
161
147
|
ctx.incremental =
|
|
@@ -260,17 +246,16 @@ interface NativeOrchestratorResult {
|
|
|
260
246
|
changedCount?: number;
|
|
261
247
|
removedCount?: number;
|
|
262
248
|
isFullBuild?: boolean;
|
|
263
|
-
/** Full changed files including reverse-dep files — used by JS structure fallback. */
|
|
264
|
-
structureScope?: string[];
|
|
265
249
|
/** Whether the Rust pipeline handled the structure phase (small-incremental fast path). */
|
|
266
250
|
structureHandled?: boolean;
|
|
251
|
+
/** Whether the Rust pipeline wrote AST/complexity/CFG/dataflow to DB. */
|
|
252
|
+
analysisComplete?: boolean;
|
|
267
253
|
}
|
|
268
254
|
|
|
269
255
|
// ── Native orchestrator helpers ───────────────────────────────────────
|
|
270
256
|
|
|
271
257
|
/** Determine whether the native orchestrator should be skipped. Returns a reason string, or null if it should run. */
|
|
272
258
|
function shouldSkipNativeOrchestrator(ctx: PipelineContext): string | null {
|
|
273
|
-
if (process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1') return 'CODEGRAPH_FORCE_JS_PIPELINE=1';
|
|
274
259
|
if (ctx.forceFullRebuild) return 'forceFullRebuild';
|
|
275
260
|
// v3.9.0 addon had buggy incremental purge (wrong SQL on analysis tables,
|
|
276
261
|
// scoped removal over-detection). Fixed in v3.9.1 by PR #865. Gate on
|
|
@@ -452,7 +437,11 @@ async function runPostNativeStructure(
|
|
|
452
437
|
return performance.now() - structureStart;
|
|
453
438
|
}
|
|
454
439
|
|
|
455
|
-
/**
|
|
440
|
+
/**
|
|
441
|
+
* JS fallback for AST/complexity/CFG/dataflow analysis after native orchestrator.
|
|
442
|
+
* Used when the Rust addon doesn't include analysis persistence (older addon
|
|
443
|
+
* version) or when analysis failed on the Rust side.
|
|
444
|
+
*/
|
|
456
445
|
async function runPostNativeAnalysis(
|
|
457
446
|
ctx: PipelineContext,
|
|
458
447
|
allFileSymbols: Map<string, ExtractorOutput>,
|
|
@@ -472,30 +461,43 @@ async function runPostNativeAnalysis(
|
|
|
472
461
|
analysisFileSymbols = allFileSymbols;
|
|
473
462
|
}
|
|
474
463
|
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
485
|
-
}
|
|
464
|
+
// Reopen nativeDb for analysis features (suspend/resume WAL pattern).
|
|
465
|
+
const native = loadNative();
|
|
466
|
+
if (native?.NativeDatabase) {
|
|
467
|
+
try {
|
|
468
|
+
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
469
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
470
|
+
} catch {
|
|
471
|
+
ctx.nativeDb = undefined;
|
|
472
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
486
473
|
}
|
|
487
|
-
}
|
|
488
|
-
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Flush JS WAL pages once so Rust can see them, then no-op callbacks.
|
|
477
|
+
// Previously each feature called wal_checkpoint(TRUNCATE) individually
|
|
478
|
+
// (~68ms each × 3-4 features). One FULL checkpoint suffices.
|
|
479
|
+
if (ctx.nativeDb && ctx.engineOpts) {
|
|
480
|
+
ctx.db.pragma('wal_checkpoint(FULL)');
|
|
481
|
+
ctx.engineOpts.suspendJsDb = () => {};
|
|
482
|
+
ctx.engineOpts.resumeJsDb = () => {};
|
|
489
483
|
}
|
|
490
484
|
|
|
491
485
|
try {
|
|
492
|
-
const { runAnalyses: runAnalysesFn } = await import('../../../ast-analysis/engine.js')
|
|
486
|
+
const { runAnalyses: runAnalysesFn } = (await import('../../../ast-analysis/engine.js')) as {
|
|
487
|
+
runAnalyses: (
|
|
488
|
+
db: BetterSqlite3Database,
|
|
489
|
+
fileSymbols: Map<string, ExtractorOutput>,
|
|
490
|
+
rootDir: string,
|
|
491
|
+
opts: Record<string, unknown>,
|
|
492
|
+
engineOpts?: Record<string, unknown>,
|
|
493
|
+
) => Promise<{ astMs?: number; complexityMs?: number; cfgMs?: number; dataflowMs?: number }>;
|
|
494
|
+
};
|
|
493
495
|
const result = await runAnalysesFn(
|
|
494
496
|
ctx.db,
|
|
495
497
|
analysisFileSymbols,
|
|
496
498
|
ctx.rootDir,
|
|
497
|
-
ctx.opts,
|
|
498
|
-
ctx.engineOpts,
|
|
499
|
+
ctx.opts as Record<string, unknown>,
|
|
500
|
+
ctx.engineOpts as unknown as Record<string, unknown> | undefined,
|
|
499
501
|
);
|
|
500
502
|
timing.astMs = result.astMs ?? 0;
|
|
501
503
|
timing.complexityMs = result.complexityMs ?? 0;
|
|
@@ -505,8 +507,10 @@ async function runPostNativeAnalysis(
|
|
|
505
507
|
warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
|
|
506
508
|
}
|
|
507
509
|
|
|
508
|
-
// Close nativeDb after analyses
|
|
509
|
-
|
|
510
|
+
// Close nativeDb after analyses — TRUNCATE checkpoint flushes all Rust
|
|
511
|
+
// WAL writes so JS and external readers can see them. Runs once after
|
|
512
|
+
// all analysis features complete (not per-feature).
|
|
513
|
+
if (ctx.nativeDb) {
|
|
510
514
|
try {
|
|
511
515
|
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
512
516
|
} catch {
|
|
@@ -518,7 +522,11 @@ async function runPostNativeAnalysis(
|
|
|
518
522
|
/* ignore close errors */
|
|
519
523
|
}
|
|
520
524
|
ctx.nativeDb = undefined;
|
|
521
|
-
if (ctx.engineOpts)
|
|
525
|
+
if (ctx.engineOpts) {
|
|
526
|
+
ctx.engineOpts.nativeDb = undefined;
|
|
527
|
+
ctx.engineOpts.suspendJsDb = undefined;
|
|
528
|
+
ctx.engineOpts.resumeJsDb = undefined;
|
|
529
|
+
}
|
|
522
530
|
}
|
|
523
531
|
|
|
524
532
|
return timing;
|
|
@@ -558,15 +566,26 @@ async function tryNativeOrchestrator(
|
|
|
558
566
|
return undefined;
|
|
559
567
|
}
|
|
560
568
|
|
|
561
|
-
//
|
|
562
|
-
//
|
|
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.
|
|
563
572
|
if (!ctx.nativeDb && ctx.nativeAvailable) {
|
|
564
573
|
const native = loadNative();
|
|
565
574
|
if (native?.NativeDatabase) {
|
|
566
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);
|
|
567
581
|
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
568
582
|
ctx.nativeDb.initSchema();
|
|
569
|
-
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;
|
|
570
589
|
} catch (err) {
|
|
571
590
|
warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
|
|
572
591
|
try {
|
|
@@ -575,6 +594,10 @@ async function tryNativeOrchestrator(
|
|
|
575
594
|
debug(`tryNativeOrchestrator: close failed during fallback: ${toErrorMessage(e)}`);
|
|
576
595
|
}
|
|
577
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);
|
|
578
601
|
}
|
|
579
602
|
}
|
|
580
603
|
}
|
|
@@ -605,14 +628,18 @@ async function tryNativeOrchestrator(
|
|
|
605
628
|
const p = result.phases;
|
|
606
629
|
|
|
607
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).
|
|
608
637
|
setBuildMeta(ctx.db, {
|
|
609
638
|
engine: ctx.engineName,
|
|
610
639
|
engine_version: ctx.engineVersion || '',
|
|
611
|
-
codegraph_version: CODEGRAPH_VERSION,
|
|
640
|
+
codegraph_version: ctx.engineVersion || CODEGRAPH_VERSION,
|
|
612
641
|
schema_version: String(ctx.schemaVersion),
|
|
613
642
|
built_at: new Date().toISOString(),
|
|
614
|
-
node_count: String(result.nodeCount ?? 0),
|
|
615
|
-
edge_count: String(result.edgeCount ?? 0),
|
|
616
643
|
});
|
|
617
644
|
|
|
618
645
|
info(
|
|
@@ -620,41 +647,51 @@ async function tryNativeOrchestrator(
|
|
|
620
647
|
);
|
|
621
648
|
|
|
622
649
|
// ── Post-native structure + analysis ──────────────────────────────
|
|
623
|
-
let analysisTiming = {
|
|
650
|
+
let analysisTiming = {
|
|
651
|
+
astMs: +(p.astMs ?? 0),
|
|
652
|
+
complexityMs: +(p.complexityMs ?? 0),
|
|
653
|
+
cfgMs: +(p.cfgMs ?? 0),
|
|
654
|
+
dataflowMs: +(p.dataflowMs ?? 0),
|
|
655
|
+
};
|
|
624
656
|
let structurePatchMs = 0;
|
|
625
|
-
const needsAnalysis =
|
|
626
|
-
ctx.opts.ast !== false ||
|
|
627
|
-
ctx.opts.complexity !== false ||
|
|
628
|
-
ctx.opts.cfg !== false ||
|
|
629
|
-
ctx.opts.dataflow !== false;
|
|
630
657
|
// Skip JS structure when the Rust pipeline's small-incremental fast path
|
|
631
658
|
// already handled it. For full builds and large incrementals where Rust
|
|
632
659
|
// skipped structure, we must run the JS fallback.
|
|
633
660
|
const needsStructure = !result.structureHandled;
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
661
|
+
// When the Rust addon doesn't include analysis persistence (older addon
|
|
662
|
+
// version or analysis failed), fall back to JS-side analysis.
|
|
663
|
+
const needsAnalysisFallback =
|
|
664
|
+
!result.analysisComplete &&
|
|
665
|
+
(ctx.opts.ast !== false ||
|
|
666
|
+
ctx.opts.complexity !== false ||
|
|
667
|
+
ctx.opts.cfg !== false ||
|
|
668
|
+
ctx.opts.dataflow !== false);
|
|
669
|
+
|
|
670
|
+
if (needsStructure || needsAnalysisFallback) {
|
|
671
|
+
// When analysis fallback is needed, handoff to better-sqlite3 — the
|
|
672
|
+
// analysis engine uses the suspend/resume WAL pattern that requires a
|
|
673
|
+
// real better-sqlite3 connection, not the NativeDbProxy.
|
|
674
|
+
if (needsAnalysisFallback && ctx.nativeFirstProxy) {
|
|
675
|
+
closeNativeDb(ctx, 'pre-analysis-fallback');
|
|
676
|
+
ctx.db = openDb(ctx.dbPath);
|
|
677
|
+
ctx.nativeFirstProxy = false;
|
|
678
|
+
} else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
|
|
638
679
|
// DB reopen failed — return partial result
|
|
639
680
|
return formatNativeTimingResult(p, 0, analysisTiming);
|
|
640
681
|
}
|
|
641
682
|
|
|
642
|
-
|
|
643
|
-
// analysis — no need to load the entire graph from DB. When structure
|
|
644
|
-
// was NOT handled, we need all files to build the complete directory tree.
|
|
645
|
-
const scopeFiles = needsStructure ? undefined : result.changedFiles;
|
|
646
|
-
const fileSymbols = reconstructFileSymbolsFromDb(ctx, scopeFiles);
|
|
683
|
+
const fileSymbols = reconstructFileSymbolsFromDb(ctx);
|
|
647
684
|
|
|
648
685
|
if (needsStructure) {
|
|
649
686
|
structurePatchMs = await runPostNativeStructure(
|
|
650
687
|
ctx,
|
|
651
688
|
fileSymbols,
|
|
652
689
|
!!result.isFullBuild,
|
|
653
|
-
result.
|
|
690
|
+
result.changedFiles,
|
|
654
691
|
);
|
|
655
692
|
}
|
|
656
693
|
|
|
657
|
-
if (
|
|
694
|
+
if (needsAnalysisFallback) {
|
|
658
695
|
analysisTiming = await runPostNativeAnalysis(ctx, fileSymbols, result.changedFiles);
|
|
659
696
|
}
|
|
660
697
|
}
|
|
@@ -666,30 +703,7 @@ async function tryNativeOrchestrator(
|
|
|
666
703
|
// ── Pipeline stages execution ───────────────────────────────────────────
|
|
667
704
|
|
|
668
705
|
async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
669
|
-
// ──
|
|
670
|
-
// When ctx.nativeFirstProxy is true, ctx.db is a NativeDbProxy backed by
|
|
671
|
-
// the single rusqlite connection (ctx.nativeDb). No dual-connection WAL
|
|
672
|
-
// dance is needed — every stage uses the same connection transparently.
|
|
673
|
-
if (ctx.nativeFirstProxy) {
|
|
674
|
-
// Ensure engineOpts.nativeDb is set so stages can use dedicated native methods.
|
|
675
|
-
if (ctx.engineOpts) {
|
|
676
|
-
ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
await collectFiles(ctx);
|
|
680
|
-
await detectChanges(ctx);
|
|
681
|
-
if (ctx.earlyExit) return;
|
|
682
|
-
await parseFiles(ctx);
|
|
683
|
-
await insertNodes(ctx);
|
|
684
|
-
await resolveImports(ctx);
|
|
685
|
-
await buildEdges(ctx);
|
|
686
|
-
await buildStructure(ctx);
|
|
687
|
-
await runAnalyses(ctx);
|
|
688
|
-
await finalize(ctx);
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// ── Legacy dual-connection mode (WASM / fallback) ────────────────────
|
|
706
|
+
// ── WASM / fallback dual-connection mode ─────────────────────────────
|
|
693
707
|
// NativeDatabase is deferred — not opened during setup. collectFiles and
|
|
694
708
|
// detectChanges only need better-sqlite3. If no files changed, we exit
|
|
695
709
|
// early without ever opening the native connection, saving ~5ms.
|
|
@@ -697,6 +711,13 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
697
711
|
// suspend it now to avoid dual-connection WAL corruption during stages.
|
|
698
712
|
if (ctx.db && ctx.nativeDb) {
|
|
699
713
|
suspendNativeDb(ctx, 'pre-collect');
|
|
714
|
+
// When nativeFirstProxy is true, ctx.db is a NativeDbProxy wrapping the
|
|
715
|
+
// now-closed NativeDatabase. Replace it with a real better-sqlite3
|
|
716
|
+
// connection so the JS pipeline stages can operate normally.
|
|
717
|
+
if (ctx.nativeFirstProxy) {
|
|
718
|
+
ctx.db = openDb(ctx.dbPath);
|
|
719
|
+
ctx.nativeFirstProxy = false;
|
|
720
|
+
}
|
|
700
721
|
}
|
|
701
722
|
|
|
702
723
|
await collectFiles(ctx);
|
|
@@ -728,25 +749,20 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
728
749
|
await buildEdges(ctx);
|
|
729
750
|
await buildStructure(ctx);
|
|
730
751
|
|
|
731
|
-
// Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow)
|
|
732
|
-
// which use suspendJsDb/resumeJsDb WAL checkpoint before native writes.
|
|
752
|
+
// Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow).
|
|
733
753
|
// Skip for small incremental builds — same rationale as insertNodes above.
|
|
754
|
+
//
|
|
755
|
+
// Perf: do ONE upfront FULL checkpoint to flush JS WAL pages so Rust
|
|
756
|
+
// can see the latest rows, then make suspendJsDb/resumeJsDb no-ops.
|
|
757
|
+
// Previously each feature called wal_checkpoint(TRUNCATE) individually
|
|
758
|
+
// (~68ms each × 3-4 features = ~200-270ms overhead on incremental builds).
|
|
734
759
|
if (ctx.nativeAvailable && !smallIncremental) {
|
|
735
760
|
reopenNativeDb(ctx, 'analyses');
|
|
736
761
|
if (ctx.nativeDb && ctx.engineOpts) {
|
|
762
|
+
ctx.db.pragma('wal_checkpoint(FULL)');
|
|
737
763
|
ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
738
|
-
ctx.engineOpts.suspendJsDb = () => {
|
|
739
|
-
|
|
740
|
-
};
|
|
741
|
-
ctx.engineOpts.resumeJsDb = () => {
|
|
742
|
-
try {
|
|
743
|
-
ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
744
|
-
} catch (e) {
|
|
745
|
-
debug(
|
|
746
|
-
`resumeJsDb: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`,
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
};
|
|
764
|
+
ctx.engineOpts.suspendJsDb = () => {};
|
|
765
|
+
ctx.engineOpts.resumeJsDb = () => {};
|
|
750
766
|
}
|
|
751
767
|
if (!ctx.nativeDb && ctx.engineOpts) {
|
|
752
768
|
ctx.engineOpts.nativeDb = undefined;
|
|
@@ -757,11 +773,32 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
757
773
|
|
|
758
774
|
await runAnalyses(ctx);
|
|
759
775
|
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
|
|
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
|
+
|
|
793
|
+
// Flush Rust WAL writes (AST, complexity, CFG, dataflow) so the JS
|
|
794
|
+
// connection and any post-build readers can see them. One TRUNCATE
|
|
795
|
+
// here replaces the N per-feature resumeJsDb checkpoints (#checkpoint-opt).
|
|
764
796
|
if (ctx.nativeDb) {
|
|
797
|
+
try {
|
|
798
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
799
|
+
} catch (e) {
|
|
800
|
+
debug(`post-analyses WAL checkpoint failed: ${toErrorMessage(e)}`);
|
|
801
|
+
}
|
|
765
802
|
refreshJsDb(ctx);
|
|
766
803
|
}
|
|
767
804
|
|
|
@@ -797,13 +834,42 @@ export async function buildGraph(
|
|
|
797
834
|
if (nativeResult) return nativeResult;
|
|
798
835
|
} catch (err) {
|
|
799
836
|
warn(`Native build orchestrator failed, falling back to JS pipeline: ${toErrorMessage(err)}`);
|
|
800
|
-
//
|
|
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
|
+
}
|
|
801
850
|
}
|
|
802
851
|
|
|
803
852
|
await runPipelineStages(ctx);
|
|
804
853
|
} catch (err) {
|
|
805
|
-
if (!ctx.earlyExit
|
|
806
|
-
|
|
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
|
+
}
|
|
807
873
|
}
|
|
808
874
|
throw err;
|
|
809
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
|