@optave/codegraph 3.9.0 → 3.9.2
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 +12 -13
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +78 -48
- package/dist/ast-analysis/engine.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 +15 -18
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +5 -17
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/structure.d.ts.map +1 -1
- package/dist/cli/commands/structure.js +18 -1
- package/dist/cli/commands/structure.js.map +1 -1
- package/dist/db/connection.d.ts +3 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +24 -6
- package/dist/db/connection.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/repository/base.d.ts +35 -0
- package/dist/db/repository/base.d.ts.map +1 -1
- package/dist/db/repository/base.js +8 -0
- package/dist/db/repository/base.js.map +1 -1
- package/dist/db/repository/index.d.ts +1 -0
- package/dist/db/repository/index.d.ts.map +1 -1
- package/dist/db/repository/index.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts +7 -1
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +46 -1
- package/dist/db/repository/native-repository.js.map +1 -1
- package/dist/domain/analysis/context.d.ts.map +1 -1
- package/dist/domain/analysis/context.js +5 -15
- package/dist/domain/analysis/context.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts +6 -33
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +18 -16
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.js +2 -2
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/implementations.d.ts.map +1 -1
- package/dist/domain/analysis/implementations.js +3 -13
- package/dist/domain/analysis/implementations.js.map +1 -1
- package/dist/domain/graph/builder/context.d.ts +4 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +4 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +18 -0
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/native-db-proxy.d.ts +24 -0
- package/dist/domain/graph/builder/native-db-proxy.d.ts.map +1 -0
- package/dist/domain/graph/builder/native-db-proxy.js +87 -0
- package/dist/domain/graph/builder/native-db-proxy.js.map +1 -0
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +410 -349
- 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 +44 -4
- 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/build-structure.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 +6 -28
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +1 -1
- 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 +16 -12
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +21 -26
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +99 -95
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +7 -2
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/queries.d.ts +1 -1
- package/dist/domain/queries.d.ts.map +1 -1
- package/dist/domain/queries.js +1 -1
- package/dist/domain/queries.js.map +1 -1
- package/dist/extractors/go.js +53 -35
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/javascript.js +66 -27
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/audit.d.ts.map +1 -1
- package/dist/features/audit.js +3 -2
- package/dist/features/audit.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +3 -5
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/branch-compare.d.ts.map +1 -1
- package/dist/features/branch-compare.js +2 -1
- package/dist/features/branch-compare.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +78 -58
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +109 -118
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +2 -1
- package/dist/features/flow.js.map +1 -1
- package/dist/features/manifesto.d.ts.map +1 -1
- package/dist/features/manifesto.js +15 -1
- package/dist/features/manifesto.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +147 -97
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +4 -2
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts +2 -0
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +13 -5
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -0
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -0
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/presentation/batch.d.ts.map +1 -1
- package/dist/presentation/batch.js +1 -0
- package/dist/presentation/batch.js.map +1 -1
- package/dist/presentation/communities.d.ts.map +1 -1
- package/dist/presentation/communities.js +38 -34
- package/dist/presentation/communities.js.map +1 -1
- package/dist/presentation/manifesto.d.ts.map +1 -1
- package/dist/presentation/manifesto.js +31 -33
- package/dist/presentation/manifesto.js.map +1 -1
- package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
- package/dist/presentation/queries-cli/inspect.js +47 -46
- package/dist/presentation/queries-cli/inspect.js.map +1 -1
- package/dist/presentation/structure.d.ts +1 -1
- package/dist/presentation/structure.d.ts.map +1 -1
- package/dist/presentation/structure.js +1 -1
- package/dist/presentation/structure.js.map +1 -1
- package/dist/shared/file-utils.d.ts.map +1 -1
- package/dist/shared/file-utils.js +94 -72
- package/dist/shared/file-utils.js.map +1 -1
- package/dist/shared/normalize.d.ts +12 -0
- package/dist/shared/normalize.d.ts.map +1 -1
- package/dist/shared/normalize.js +4 -0
- package/dist/shared/normalize.js.map +1 -1
- package/dist/types.d.ts +82 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +99 -55
- package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
- package/src/cli/commands/batch.ts +5 -26
- package/src/cli/commands/structure.ts +21 -1
- package/src/db/connection.ts +26 -7
- package/src/db/index.ts +2 -0
- package/src/db/repository/base.ts +43 -0
- package/src/db/repository/index.ts +1 -0
- package/src/db/repository/native-repository.ts +67 -1
- package/src/domain/analysis/context.ts +5 -15
- package/src/domain/analysis/dependencies.ts +19 -16
- package/src/domain/analysis/fn-impact.ts +2 -2
- package/src/domain/analysis/implementations.ts +3 -13
- package/src/domain/graph/builder/context.ts +4 -0
- package/src/domain/graph/builder/incremental.ts +21 -0
- package/src/domain/graph/builder/native-db-proxy.ts +98 -0
- package/src/domain/graph/builder/pipeline.ts +514 -416
- package/src/domain/graph/builder/stages/build-edges.ts +45 -3
- package/src/domain/graph/builder/stages/build-structure.ts +2 -2
- package/src/domain/graph/builder/stages/detect-changes.ts +11 -33
- package/src/domain/graph/builder/stages/finalize.ts +1 -1
- package/src/domain/graph/builder/stages/insert-nodes.ts +17 -14
- package/src/domain/graph/builder/stages/resolve-imports.ts +22 -23
- package/src/domain/graph/watcher.ts +118 -98
- package/src/domain/parser.ts +8 -2
- package/src/domain/queries.ts +1 -1
- package/src/extractors/go.ts +57 -32
- package/src/extractors/javascript.ts +67 -27
- package/src/features/audit.ts +3 -2
- package/src/features/boundaries.ts +3 -5
- package/src/features/branch-compare.ts +2 -3
- package/src/features/complexity.ts +94 -58
- package/src/features/dataflow.ts +153 -132
- package/src/features/flow.ts +2 -1
- package/src/features/manifesto.ts +15 -1
- package/src/features/structure.ts +167 -95
- package/src/graph/algorithms/louvain.ts +5 -2
- package/src/graph/classifiers/roles.ts +14 -5
- package/src/infrastructure/config.ts +1 -0
- package/src/presentation/batch.ts +1 -0
- package/src/presentation/communities.ts +44 -39
- package/src/presentation/manifesto.ts +35 -38
- package/src/presentation/queries-cli/inspect.ts +48 -46
- package/src/presentation/structure.ts +2 -2
- package/src/shared/file-utils.ts +116 -77
- package/src/shared/normalize.ts +10 -0
- package/src/types.ts +86 -0
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
* This is the heart of the builder refactor (ROADMAP 3.9): the monolithic buildGraph()
|
|
5
5
|
* is decomposed into independently testable stages that communicate via PipelineContext.
|
|
6
6
|
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { performance } from 'node:perf_hooks';
|
|
9
10
|
import {
|
|
11
|
+
acquireAdvisoryLock,
|
|
10
12
|
closeDbPair,
|
|
11
13
|
getBuildMeta,
|
|
12
14
|
initSchema,
|
|
13
15
|
MIGRATIONS,
|
|
14
16
|
openDb,
|
|
17
|
+
releaseAdvisoryLock,
|
|
15
18
|
setBuildMeta,
|
|
16
19
|
} from '../../../db/index.js';
|
|
17
20
|
import { detectWorkspaces, loadConfig } from '../../../infrastructure/config.js';
|
|
@@ -25,6 +28,7 @@ import { getActiveEngine } from '../../parser.js';
|
|
|
25
28
|
import { setWorkspaces } from '../resolve.js';
|
|
26
29
|
import { PipelineContext } from './context.js';
|
|
27
30
|
import { loadPathAliases } from './helpers.js';
|
|
31
|
+
import { NativeDbProxy } from './native-db-proxy.js';
|
|
28
32
|
import { buildEdges } from './stages/build-edges.js';
|
|
29
33
|
import { buildStructure } from './stages/build-structure.js';
|
|
30
34
|
// Pipeline stages
|
|
@@ -43,29 +47,11 @@ function initializeEngine(ctx: PipelineContext): void {
|
|
|
43
47
|
engine: ctx.opts.engine || 'auto',
|
|
44
48
|
dataflow: ctx.opts.dataflow !== false,
|
|
45
49
|
ast: ctx.opts.ast !== false,
|
|
46
|
-
nativeDb
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// After native writes, resumeJsDb checkpoints through rusqlite so
|
|
52
|
-
// better-sqlite3 never reads WAL frames from a different SQLite library.
|
|
53
|
-
suspendJsDb: ctx.nativeDb
|
|
54
|
-
? () => {
|
|
55
|
-
ctx.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
56
|
-
}
|
|
57
|
-
: undefined,
|
|
58
|
-
resumeJsDb: ctx.nativeDb
|
|
59
|
-
? () => {
|
|
60
|
-
try {
|
|
61
|
-
ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
62
|
-
} catch (e) {
|
|
63
|
-
debug(
|
|
64
|
-
`resumeJsDb: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
: undefined,
|
|
50
|
+
// nativeDb and WAL callbacks are set later when NativeDatabase is opened
|
|
51
|
+
// (deferred to skip overhead on no-op rebuilds).
|
|
52
|
+
nativeDb: undefined,
|
|
53
|
+
suspendJsDb: undefined,
|
|
54
|
+
resumeJsDb: undefined,
|
|
69
55
|
};
|
|
70
56
|
const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
|
|
71
57
|
ctx.engineName = engineName as 'native' | 'wasm';
|
|
@@ -79,11 +65,10 @@ function checkEngineSchemaMismatch(ctx: PipelineContext): void {
|
|
|
79
65
|
ctx.forceFullRebuild = false;
|
|
80
66
|
if (!ctx.incremental) return;
|
|
81
67
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
const meta = (key: string): string | null =>
|
|
86
|
-
useNativeDb ? ctx.nativeDb!.getBuildMeta(key) : getBuildMeta(ctx.db, key);
|
|
68
|
+
// NativeDatabase is deferred until after change detection, so always use
|
|
69
|
+
// better-sqlite3 for metadata reads here. Reads are safe — WAL conflicts
|
|
70
|
+
// only arise from concurrent writes.
|
|
71
|
+
const meta = (key: string): string | null => getBuildMeta(ctx.db, key);
|
|
87
72
|
|
|
88
73
|
const prevEngine = meta('engine');
|
|
89
74
|
if (prevEngine && prevEngine !== ctx.engineName) {
|
|
@@ -129,34 +114,46 @@ function loadAliases(ctx: PipelineContext): void {
|
|
|
129
114
|
function setupPipeline(ctx: PipelineContext): void {
|
|
130
115
|
ctx.rootDir = path.resolve(ctx.rootDir);
|
|
131
116
|
ctx.dbPath = path.join(ctx.rootDir, '.codegraph', 'graph.db');
|
|
132
|
-
ctx.db = openDb(ctx.dbPath);
|
|
133
117
|
|
|
134
|
-
//
|
|
135
|
-
// better-sqlite3 (ctx.db) is still always opened — needed for queries and stages
|
|
136
|
-
// that haven't been migrated to rusqlite yet.
|
|
137
|
-
// Skip native DB entirely when user explicitly requested --engine wasm.
|
|
118
|
+
// Detect whether native engine is available.
|
|
138
119
|
const enginePref = ctx.opts.engine || 'auto';
|
|
139
120
|
const native = enginePref !== 'wasm' ? loadNative() : null;
|
|
140
|
-
|
|
121
|
+
ctx.nativeAvailable = !!native?.NativeDatabase;
|
|
122
|
+
|
|
123
|
+
// Native-first: use only rusqlite for the entire pipeline (no better-sqlite3).
|
|
124
|
+
// This eliminates the dual-connection WAL corruption problem and enables all
|
|
125
|
+
// native fast-paths (bulkInsertNodes, classifyRolesFull, etc.).
|
|
126
|
+
// Fallback: if native is unavailable or FORCE_JS is set, use better-sqlite3.
|
|
127
|
+
if (
|
|
128
|
+
ctx.nativeAvailable &&
|
|
129
|
+
native?.NativeDatabase &&
|
|
130
|
+
process.env.CODEGRAPH_FORCE_JS_PIPELINE !== '1'
|
|
131
|
+
) {
|
|
141
132
|
try {
|
|
133
|
+
const dir = path.dirname(ctx.dbPath);
|
|
134
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
acquireAdvisoryLock(ctx.dbPath);
|
|
142
136
|
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
143
137
|
ctx.nativeDb.initSchema();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
ctx.
|
|
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;
|
|
147
142
|
} catch (err) {
|
|
148
|
-
warn(`NativeDatabase setup failed, falling back to
|
|
143
|
+
warn(`NativeDatabase setup failed, falling back to better-sqlite3: ${toErrorMessage(err)}`);
|
|
149
144
|
try {
|
|
150
145
|
ctx.nativeDb?.close();
|
|
151
|
-
} catch
|
|
152
|
-
|
|
146
|
+
} catch {
|
|
147
|
+
/* ignore */
|
|
153
148
|
}
|
|
154
149
|
ctx.nativeDb = undefined;
|
|
150
|
+
ctx.nativeFirstProxy = false;
|
|
151
|
+
releaseAdvisoryLock(`${ctx.dbPath}.lock`);
|
|
152
|
+
ctx.db = openDb(ctx.dbPath);
|
|
153
|
+
initSchema(ctx.db);
|
|
155
154
|
}
|
|
156
|
-
// Always run JS initSchema so better-sqlite3 sees the schema —
|
|
157
|
-
// nativeDb is closed during pipeline stages and reopened for analyses.
|
|
158
|
-
initSchema(ctx.db);
|
|
159
155
|
} else {
|
|
156
|
+
ctx.db = openDb(ctx.dbPath);
|
|
160
157
|
initSchema(ctx.db);
|
|
161
158
|
}
|
|
162
159
|
|
|
@@ -251,15 +248,453 @@ function refreshJsDb(ctx: PipelineContext): void {
|
|
|
251
248
|
ctx.db = openDb(ctx.dbPath);
|
|
252
249
|
}
|
|
253
250
|
|
|
251
|
+
// ── Native orchestrator types ──────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
interface NativeOrchestratorResult {
|
|
254
|
+
phases: Record<string, number>;
|
|
255
|
+
earlyExit?: boolean;
|
|
256
|
+
nodeCount?: number;
|
|
257
|
+
edgeCount?: number;
|
|
258
|
+
fileCount?: number;
|
|
259
|
+
changedFiles?: string[];
|
|
260
|
+
changedCount?: number;
|
|
261
|
+
removedCount?: number;
|
|
262
|
+
isFullBuild?: boolean;
|
|
263
|
+
/** Full changed files including reverse-dep files — used by JS structure fallback. */
|
|
264
|
+
structureScope?: string[];
|
|
265
|
+
/** Whether the Rust pipeline handled the structure phase (small-incremental fast path). */
|
|
266
|
+
structureHandled?: boolean;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Native orchestrator helpers ───────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
/** Determine whether the native orchestrator should be skipped. Returns a reason string, or null if it should run. */
|
|
272
|
+
function shouldSkipNativeOrchestrator(ctx: PipelineContext): string | null {
|
|
273
|
+
if (process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1') return 'CODEGRAPH_FORCE_JS_PIPELINE=1';
|
|
274
|
+
if (ctx.forceFullRebuild) return 'forceFullRebuild';
|
|
275
|
+
// v3.9.0 addon had buggy incremental purge (wrong SQL on analysis tables,
|
|
276
|
+
// scoped removal over-detection). Fixed in v3.9.1 by PR #865. Gate on
|
|
277
|
+
// < 3.9.1 so v3.9.1+ uses the fast Rust orchestrator path.
|
|
278
|
+
const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.9.1') < 0;
|
|
279
|
+
if (orchestratorBuggy) return `buggy addon ${ctx.engineVersion}`;
|
|
280
|
+
if (ctx.engineName !== 'native') return `engine=${ctx.engineName}`;
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Checkpoint WAL through rusqlite, close nativeDb, and reopen better-sqlite3.
|
|
285
|
+
* Returns false if the DB reopen fails (caller should return partial result). */
|
|
286
|
+
function handoffWalAfterNativeBuild(ctx: PipelineContext): boolean {
|
|
287
|
+
closeNativeDb(ctx, 'post-native-build');
|
|
288
|
+
try {
|
|
289
|
+
ctx.db.close();
|
|
290
|
+
} catch (e) {
|
|
291
|
+
debug(`handoffWal JS db close failed: ${toErrorMessage(e)}`);
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
ctx.db = openDb(ctx.dbPath);
|
|
295
|
+
return true;
|
|
296
|
+
} catch (reopenErr) {
|
|
297
|
+
warn(`Failed to reopen DB after native build: ${(reopenErr as Error).message}`);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Reconstruct fileSymbols from the DB after a native orchestrator build.
|
|
304
|
+
* When `scopeFiles` is provided, only loads those files (for analysis-only).
|
|
305
|
+
* When omitted, loads all files (needed for structure rebuilds).
|
|
306
|
+
*/
|
|
307
|
+
function reconstructFileSymbolsFromDb(
|
|
308
|
+
ctx: PipelineContext,
|
|
309
|
+
scopeFiles?: string[],
|
|
310
|
+
): Map<string, ExtractorOutput> {
|
|
311
|
+
let query =
|
|
312
|
+
'SELECT file, name, kind, line, end_line as endLine FROM nodes WHERE file IS NOT NULL';
|
|
313
|
+
const params: string[] = [];
|
|
314
|
+
if (scopeFiles && scopeFiles.length > 0) {
|
|
315
|
+
const placeholders = scopeFiles.map(() => '?').join(',');
|
|
316
|
+
query += ` AND file IN (${placeholders})`;
|
|
317
|
+
params.push(...scopeFiles);
|
|
318
|
+
}
|
|
319
|
+
query += ' ORDER BY file, line';
|
|
320
|
+
|
|
321
|
+
const rows = ctx.db.prepare(query).all(...params) as {
|
|
322
|
+
file: string;
|
|
323
|
+
name: string;
|
|
324
|
+
kind: string;
|
|
325
|
+
line: number;
|
|
326
|
+
endLine: number | null;
|
|
327
|
+
}[];
|
|
328
|
+
|
|
329
|
+
const fileSymbols = new Map<string, ExtractorOutput>();
|
|
330
|
+
for (const row of rows) {
|
|
331
|
+
let entry = fileSymbols.get(row.file);
|
|
332
|
+
if (!entry) {
|
|
333
|
+
entry = {
|
|
334
|
+
definitions: [],
|
|
335
|
+
calls: [],
|
|
336
|
+
imports: [],
|
|
337
|
+
classes: [],
|
|
338
|
+
exports: [],
|
|
339
|
+
typeMap: new Map(),
|
|
340
|
+
};
|
|
341
|
+
fileSymbols.set(row.file, entry);
|
|
342
|
+
}
|
|
343
|
+
entry.definitions.push({
|
|
344
|
+
name: row.name,
|
|
345
|
+
kind: row.kind as Definition['kind'],
|
|
346
|
+
line: row.line,
|
|
347
|
+
endLine: row.endLine ?? undefined,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Populate import/export counts from DB edges so buildStructure
|
|
352
|
+
// computes correct import_count/export_count in node_metrics.
|
|
353
|
+
// The extractor arrays aren't persisted to the DB, so we derive
|
|
354
|
+
// counts from edge data instead (#804).
|
|
355
|
+
const importCountRows = ctx.db
|
|
356
|
+
.prepare(
|
|
357
|
+
`SELECT n.file, COUNT(*) AS cnt
|
|
358
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
359
|
+
WHERE e.kind IN ('imports', 'imports-type', 'dynamic-imports')
|
|
360
|
+
AND n.file IS NOT NULL
|
|
361
|
+
GROUP BY n.file`,
|
|
362
|
+
)
|
|
363
|
+
.all() as { file: string; cnt: number }[];
|
|
364
|
+
for (const row of importCountRows) {
|
|
365
|
+
const entry = fileSymbols.get(row.file);
|
|
366
|
+
if (entry) entry.imports = new Array(row.cnt) as ExtractorOutput['imports'];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const exportCountRows = ctx.db
|
|
370
|
+
.prepare(
|
|
371
|
+
`SELECT n_tgt.file, COUNT(DISTINCT n_tgt.id) AS cnt
|
|
372
|
+
FROM edges e
|
|
373
|
+
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
374
|
+
JOIN nodes n_src ON e.source_id = n_src.id
|
|
375
|
+
WHERE e.kind IN ('imports', 'imports-type', 'reexports')
|
|
376
|
+
AND n_tgt.file IS NOT NULL
|
|
377
|
+
AND n_src.file != n_tgt.file
|
|
378
|
+
GROUP BY n_tgt.file`,
|
|
379
|
+
)
|
|
380
|
+
.all() as { file: string; cnt: number }[];
|
|
381
|
+
for (const row of exportCountRows) {
|
|
382
|
+
const entry = fileSymbols.get(row.file);
|
|
383
|
+
if (entry) entry.exports = new Array(row.cnt) as ExtractorOutput['exports'];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return fileSymbols;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Run JS buildStructure() after native orchestrator to fill directory nodes + contains edges.
|
|
391
|
+
* For full builds, passes changedFiles=null (full rebuild).
|
|
392
|
+
* For incremental builds, passes the changed file list to scope the update.
|
|
393
|
+
*/
|
|
394
|
+
async function runPostNativeStructure(
|
|
395
|
+
ctx: PipelineContext,
|
|
396
|
+
allFileSymbols: Map<string, ExtractorOutput>,
|
|
397
|
+
isFullBuild: boolean,
|
|
398
|
+
changedFiles: string[] | undefined,
|
|
399
|
+
): Promise<number> {
|
|
400
|
+
const structureStart = performance.now();
|
|
401
|
+
try {
|
|
402
|
+
const directories = new Set<string>();
|
|
403
|
+
for (const relPath of allFileSymbols.keys()) {
|
|
404
|
+
const parts = relPath.split('/');
|
|
405
|
+
for (let i = 1; i < parts.length; i++) {
|
|
406
|
+
directories.add(parts.slice(0, i).join('/'));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const lineCountMap = new Map<string, number>();
|
|
411
|
+
const cachedLineCounts = ctx.db
|
|
412
|
+
.prepare(
|
|
413
|
+
`SELECT n.name AS file, m.line_count
|
|
414
|
+
FROM node_metrics m JOIN nodes n ON m.node_id = n.id
|
|
415
|
+
WHERE n.kind = 'file'`,
|
|
416
|
+
)
|
|
417
|
+
.all() as Array<{ file: string; line_count: number }>;
|
|
418
|
+
for (const row of cachedLineCounts) {
|
|
419
|
+
lineCountMap.set(row.file, row.line_count);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Full builds need null (rebuild everything). Incremental builds pass the
|
|
423
|
+
// changed file list so buildStructure only updates those files' metrics
|
|
424
|
+
// and contains edges — matching the JS pipeline's medium-incremental path.
|
|
425
|
+
const changedFilePaths = isFullBuild || !changedFiles?.length ? null : changedFiles;
|
|
426
|
+
const { buildStructure: buildStructureFn } = (await import(
|
|
427
|
+
'../../../features/structure.js'
|
|
428
|
+
)) as {
|
|
429
|
+
buildStructure: (
|
|
430
|
+
db: typeof ctx.db,
|
|
431
|
+
fileSymbols: Map<string, ExtractorOutput>,
|
|
432
|
+
rootDir: string,
|
|
433
|
+
lineCountMap: Map<string, number>,
|
|
434
|
+
directories: Set<string>,
|
|
435
|
+
changedFiles: string[] | null,
|
|
436
|
+
) => void;
|
|
437
|
+
};
|
|
438
|
+
buildStructureFn(
|
|
439
|
+
ctx.db,
|
|
440
|
+
allFileSymbols,
|
|
441
|
+
ctx.rootDir,
|
|
442
|
+
lineCountMap,
|
|
443
|
+
directories,
|
|
444
|
+
changedFilePaths,
|
|
445
|
+
);
|
|
446
|
+
debug(
|
|
447
|
+
`Structure phase completed after native orchestrator${changedFilePaths ? ` (${changedFilePaths.length} files)` : ' (full)'}`,
|
|
448
|
+
);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
warn(`Structure phase failed after native build: ${toErrorMessage(err)}`);
|
|
451
|
+
}
|
|
452
|
+
return performance.now() - structureStart;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Run AST/complexity/CFG/dataflow analysis after native orchestrator. */
|
|
456
|
+
async function runPostNativeAnalysis(
|
|
457
|
+
ctx: PipelineContext,
|
|
458
|
+
allFileSymbols: Map<string, ExtractorOutput>,
|
|
459
|
+
changedFiles: string[] | undefined,
|
|
460
|
+
): Promise<{ astMs: number; complexityMs: number; cfgMs: number; dataflowMs: number }> {
|
|
461
|
+
const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
462
|
+
|
|
463
|
+
// Scope analysis fileSymbols to changed files only
|
|
464
|
+
let analysisFileSymbols: Map<string, ExtractorOutput>;
|
|
465
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
466
|
+
analysisFileSymbols = new Map();
|
|
467
|
+
for (const f of changedFiles) {
|
|
468
|
+
const entry = allFileSymbols.get(f);
|
|
469
|
+
if (entry) analysisFileSymbols.set(f, entry);
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
analysisFileSymbols = allFileSymbols;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// In native-first mode, nativeDb is already open — no reopen needed.
|
|
476
|
+
if (!ctx.nativeFirstProxy) {
|
|
477
|
+
const native = loadNative();
|
|
478
|
+
if (native?.NativeDatabase) {
|
|
479
|
+
try {
|
|
480
|
+
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
481
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
482
|
+
} catch {
|
|
483
|
+
ctx.nativeDb = undefined;
|
|
484
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} else if (ctx.engineOpts) {
|
|
488
|
+
ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const { runAnalyses: runAnalysesFn } = await import('../../../ast-analysis/engine.js');
|
|
493
|
+
const result = await runAnalysesFn(
|
|
494
|
+
ctx.db,
|
|
495
|
+
analysisFileSymbols,
|
|
496
|
+
ctx.rootDir,
|
|
497
|
+
ctx.opts,
|
|
498
|
+
ctx.engineOpts,
|
|
499
|
+
);
|
|
500
|
+
timing.astMs = result.astMs ?? 0;
|
|
501
|
+
timing.complexityMs = result.complexityMs ?? 0;
|
|
502
|
+
timing.cfgMs = result.cfgMs ?? 0;
|
|
503
|
+
timing.dataflowMs = result.dataflowMs ?? 0;
|
|
504
|
+
} catch (err) {
|
|
505
|
+
warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Close nativeDb after analyses (skip in native-first — single connection stays open)
|
|
509
|
+
if (ctx.nativeDb && !ctx.nativeFirstProxy) {
|
|
510
|
+
try {
|
|
511
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
512
|
+
} catch {
|
|
513
|
+
/* ignore checkpoint errors */
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
ctx.nativeDb.close();
|
|
517
|
+
} catch {
|
|
518
|
+
/* ignore close errors */
|
|
519
|
+
}
|
|
520
|
+
ctx.nativeDb = undefined;
|
|
521
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return timing;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Format timing result from native orchestrator phases + JS post-processing. */
|
|
528
|
+
function formatNativeTimingResult(
|
|
529
|
+
p: Record<string, number>,
|
|
530
|
+
structurePatchMs: number,
|
|
531
|
+
analysisTiming: { astMs: number; complexityMs: number; cfgMs: number; dataflowMs: number },
|
|
532
|
+
): BuildResult {
|
|
533
|
+
return {
|
|
534
|
+
phases: {
|
|
535
|
+
setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
|
|
536
|
+
parseMs: +(p.parseMs ?? 0).toFixed(1),
|
|
537
|
+
insertMs: +(p.insertMs ?? 0).toFixed(1),
|
|
538
|
+
resolveMs: +(p.resolveMs ?? 0).toFixed(1),
|
|
539
|
+
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
|
|
540
|
+
structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
|
|
541
|
+
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
|
|
542
|
+
astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
|
|
543
|
+
complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
|
|
544
|
+
cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
|
|
545
|
+
dataflowMs: +(analysisTiming.dataflowMs ?? 0).toFixed(1),
|
|
546
|
+
finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Try the native build orchestrator. Returns a BuildResult on success, undefined to fall through to JS pipeline. */
|
|
552
|
+
async function tryNativeOrchestrator(
|
|
553
|
+
ctx: PipelineContext,
|
|
554
|
+
): Promise<BuildResult | undefined | 'early-exit'> {
|
|
555
|
+
const skipReason = shouldSkipNativeOrchestrator(ctx);
|
|
556
|
+
if (skipReason) {
|
|
557
|
+
debug(`Skipping native orchestrator: ${skipReason}`);
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// In native-first mode, nativeDb is already open from setupPipeline.
|
|
562
|
+
// Otherwise, open it on demand (deferred to skip overhead on no-op rebuilds).
|
|
563
|
+
if (!ctx.nativeDb && ctx.nativeAvailable) {
|
|
564
|
+
const native = loadNative();
|
|
565
|
+
if (native?.NativeDatabase) {
|
|
566
|
+
try {
|
|
567
|
+
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
568
|
+
ctx.nativeDb.initSchema();
|
|
569
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
570
|
+
} catch (err) {
|
|
571
|
+
warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
|
|
572
|
+
try {
|
|
573
|
+
ctx.nativeDb?.close();
|
|
574
|
+
} catch (e) {
|
|
575
|
+
debug(`tryNativeOrchestrator: close failed during fallback: ${toErrorMessage(e)}`);
|
|
576
|
+
}
|
|
577
|
+
ctx.nativeDb = undefined;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!ctx.nativeDb?.buildGraph) return undefined;
|
|
583
|
+
|
|
584
|
+
const resultJson = ctx.nativeDb.buildGraph(
|
|
585
|
+
ctx.rootDir,
|
|
586
|
+
JSON.stringify(ctx.config),
|
|
587
|
+
JSON.stringify(ctx.aliases),
|
|
588
|
+
JSON.stringify(ctx.opts),
|
|
589
|
+
);
|
|
590
|
+
const result = JSON.parse(resultJson) as NativeOrchestratorResult;
|
|
591
|
+
|
|
592
|
+
if (result.earlyExit) {
|
|
593
|
+
info('No changes detected');
|
|
594
|
+
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
595
|
+
return 'early-exit';
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Log incremental status to match JS pipeline output
|
|
599
|
+
const changed = result.changedCount ?? 0;
|
|
600
|
+
const removed = result.removedCount ?? 0;
|
|
601
|
+
if (!result.isFullBuild && (changed > 0 || removed > 0)) {
|
|
602
|
+
info(`Incremental: ${changed} changed, ${removed} removed`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const p = result.phases;
|
|
606
|
+
|
|
607
|
+
// Sync build_meta so JS-side version/engine checks work on next build.
|
|
608
|
+
setBuildMeta(ctx.db, {
|
|
609
|
+
engine: ctx.engineName,
|
|
610
|
+
engine_version: ctx.engineVersion || '',
|
|
611
|
+
codegraph_version: CODEGRAPH_VERSION,
|
|
612
|
+
schema_version: String(ctx.schemaVersion),
|
|
613
|
+
built_at: new Date().toISOString(),
|
|
614
|
+
node_count: String(result.nodeCount ?? 0),
|
|
615
|
+
edge_count: String(result.edgeCount ?? 0),
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
info(
|
|
619
|
+
`Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// ── Post-native structure + analysis ──────────────────────────────
|
|
623
|
+
let analysisTiming = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
624
|
+
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
|
+
// Skip JS structure when the Rust pipeline's small-incremental fast path
|
|
631
|
+
// already handled it. For full builds and large incrementals where Rust
|
|
632
|
+
// skipped structure, we must run the JS fallback.
|
|
633
|
+
const needsStructure = !result.structureHandled;
|
|
634
|
+
|
|
635
|
+
if (needsAnalysis || needsStructure) {
|
|
636
|
+
// In native-first mode the proxy is already wired — no WAL handoff needed.
|
|
637
|
+
if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
|
|
638
|
+
// DB reopen failed — return partial result
|
|
639
|
+
return formatNativeTimingResult(p, 0, analysisTiming);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// When structure was handled by Rust, we only need changed files for
|
|
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);
|
|
647
|
+
|
|
648
|
+
if (needsStructure) {
|
|
649
|
+
structurePatchMs = await runPostNativeStructure(
|
|
650
|
+
ctx,
|
|
651
|
+
fileSymbols,
|
|
652
|
+
!!result.isFullBuild,
|
|
653
|
+
result.structureScope ?? result.changedFiles,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (needsAnalysis) {
|
|
658
|
+
analysisTiming = await runPostNativeAnalysis(ctx, fileSymbols, result.changedFiles);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
663
|
+
return formatNativeTimingResult(p, structurePatchMs, analysisTiming);
|
|
664
|
+
}
|
|
665
|
+
|
|
254
666
|
// ── Pipeline stages execution ───────────────────────────────────────────
|
|
255
667
|
|
|
256
668
|
async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
669
|
+
// ── Native-first mode ────────────────────────────────────────────────
|
|
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) ────────────────────
|
|
693
|
+
// NativeDatabase is deferred — not opened during setup. collectFiles and
|
|
694
|
+
// detectChanges only need better-sqlite3. If no files changed, we exit
|
|
695
|
+
// early without ever opening the native connection, saving ~5ms.
|
|
696
|
+
// If nativeDb was opened by tryNativeOrchestrator (which fell through),
|
|
697
|
+
// suspend it now to avoid dual-connection WAL corruption during stages.
|
|
263
698
|
if (ctx.db && ctx.nativeDb) {
|
|
264
699
|
suspendNativeDb(ctx, 'pre-collect');
|
|
265
700
|
}
|
|
@@ -271,10 +706,13 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
271
706
|
|
|
272
707
|
await parseFiles(ctx);
|
|
273
708
|
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
709
|
+
// For small incremental builds (≤smallFilesThreshold files), skip the nativeDb open/close
|
|
710
|
+
// cycle for insertNodes — the WAL checkpoint + connection churn (~5-10ms)
|
|
711
|
+
// exceeds the napi bulk-insert savings on a handful of files. The JS
|
|
712
|
+
// fallback path inside insertNodes handles this case efficiently.
|
|
713
|
+
const smallIncremental =
|
|
714
|
+
!ctx.isFullBuild && ctx.allSymbols.size <= ctx.config.build.smallFilesThreshold;
|
|
715
|
+
if (ctx.nativeAvailable && ctx.engineName === 'native' && !smallIncremental) {
|
|
278
716
|
reopenNativeDb(ctx, 'insertNodes');
|
|
279
717
|
}
|
|
280
718
|
|
|
@@ -292,13 +730,28 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
292
730
|
|
|
293
731
|
// Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow)
|
|
294
732
|
// which use suspendJsDb/resumeJsDb WAL checkpoint before native writes.
|
|
295
|
-
|
|
733
|
+
// Skip for small incremental builds — same rationale as insertNodes above.
|
|
734
|
+
if (ctx.nativeAvailable && !smallIncremental) {
|
|
296
735
|
reopenNativeDb(ctx, 'analyses');
|
|
297
736
|
if (ctx.nativeDb && ctx.engineOpts) {
|
|
298
737
|
ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
738
|
+
ctx.engineOpts.suspendJsDb = () => {
|
|
739
|
+
ctx.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
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
|
+
};
|
|
299
750
|
}
|
|
300
751
|
if (!ctx.nativeDb && ctx.engineOpts) {
|
|
301
752
|
ctx.engineOpts.nativeDb = undefined;
|
|
753
|
+
ctx.engineOpts.suspendJsDb = undefined;
|
|
754
|
+
ctx.engineOpts.resumeJsDb = undefined;
|
|
302
755
|
}
|
|
303
756
|
}
|
|
304
757
|
|
|
@@ -338,368 +791,13 @@ export async function buildGraph(
|
|
|
338
791
|
// When available, run the entire build pipeline in Rust with zero
|
|
339
792
|
// napi crossings (eliminates WAL dual-connection dance). Falls back
|
|
340
793
|
// to the JS pipeline on failure or when native is unavailable.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// Skip the orchestrator for affected versions (fixed in 3.9.0+).
|
|
349
|
-
const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.8.1') <= 0;
|
|
350
|
-
const forceJs =
|
|
351
|
-
process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1' ||
|
|
352
|
-
ctx.forceFullRebuild ||
|
|
353
|
-
orchestratorBuggy ||
|
|
354
|
-
ctx.engineName !== 'native';
|
|
355
|
-
if (forceJs) {
|
|
356
|
-
const reason =
|
|
357
|
-
process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1'
|
|
358
|
-
? 'CODEGRAPH_FORCE_JS_PIPELINE=1'
|
|
359
|
-
: ctx.forceFullRebuild
|
|
360
|
-
? 'forceFullRebuild'
|
|
361
|
-
: orchestratorBuggy
|
|
362
|
-
? `buggy addon ${ctx.engineVersion}`
|
|
363
|
-
: `engine=${ctx.engineName}`;
|
|
364
|
-
debug(`Skipping native orchestrator: ${reason}`);
|
|
365
|
-
}
|
|
366
|
-
if (!forceJs && ctx.nativeDb?.buildGraph) {
|
|
367
|
-
try {
|
|
368
|
-
const resultJson = ctx.nativeDb.buildGraph(
|
|
369
|
-
ctx.rootDir,
|
|
370
|
-
JSON.stringify(ctx.config),
|
|
371
|
-
JSON.stringify(ctx.aliases),
|
|
372
|
-
JSON.stringify(opts),
|
|
373
|
-
);
|
|
374
|
-
const result = JSON.parse(resultJson) as {
|
|
375
|
-
phases: Record<string, number>;
|
|
376
|
-
earlyExit?: boolean;
|
|
377
|
-
nodeCount?: number;
|
|
378
|
-
edgeCount?: number;
|
|
379
|
-
fileCount?: number;
|
|
380
|
-
changedFiles?: string[];
|
|
381
|
-
changedCount?: number;
|
|
382
|
-
removedCount?: number;
|
|
383
|
-
isFullBuild?: boolean;
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
if (result.earlyExit) {
|
|
387
|
-
info('No changes detected');
|
|
388
|
-
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Log incremental status to match JS pipeline output
|
|
393
|
-
const changed = result.changedCount ?? 0;
|
|
394
|
-
const removed = result.removedCount ?? 0;
|
|
395
|
-
if (!result.isFullBuild && (changed > 0 || removed > 0)) {
|
|
396
|
-
info(`Incremental: ${changed} changed, ${removed} removed`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Map Rust timing fields to the JS BuildResult format.
|
|
400
|
-
// Rust handles collect+detect+parse+insert+resolve+edges+structure+roles.
|
|
401
|
-
const p = result.phases;
|
|
402
|
-
|
|
403
|
-
// Sync build_meta so JS-side version/engine checks work on next build.
|
|
404
|
-
// Note: the Rust orchestrator also writes codegraph_version (using
|
|
405
|
-
// CARGO_PKG_VERSION). We intentionally overwrite it here with the npm
|
|
406
|
-
// package version so that the JS-side "version changed → full rebuild"
|
|
407
|
-
// detection (line ~97) compares against the authoritative JS version.
|
|
408
|
-
// The two versions are kept in lockstep by the release process.
|
|
409
|
-
setBuildMeta(ctx.db, {
|
|
410
|
-
engine: ctx.engineName,
|
|
411
|
-
engine_version: ctx.engineVersion || '',
|
|
412
|
-
codegraph_version: CODEGRAPH_VERSION,
|
|
413
|
-
schema_version: String(ctx.schemaVersion),
|
|
414
|
-
built_at: new Date().toISOString(),
|
|
415
|
-
node_count: String(result.nodeCount ?? 0),
|
|
416
|
-
edge_count: String(result.edgeCount ?? 0),
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
info(
|
|
420
|
-
`Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`,
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
// ── Run structure + analysis phases after native orchestrator ──
|
|
424
|
-
// Structure (directory nodes, contains edges, metrics) is not fully
|
|
425
|
-
// ported to Rust — the native pipeline only handles the small
|
|
426
|
-
// incremental fast path (≤5 changed files). For full builds and
|
|
427
|
-
// larger incremental builds, run JS buildStructure() to fill the gap.
|
|
428
|
-
// Analysis phases (AST, complexity, CFG, dataflow) are also not yet
|
|
429
|
-
// ported; run via JS engine after reconstructing fileSymbols from DB.
|
|
430
|
-
let analysisTiming = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
431
|
-
let structurePatchMs = 0;
|
|
432
|
-
const needsAnalysis =
|
|
433
|
-
opts.ast !== false ||
|
|
434
|
-
opts.complexity !== false ||
|
|
435
|
-
opts.cfg !== false ||
|
|
436
|
-
opts.dataflow !== false;
|
|
437
|
-
|
|
438
|
-
// The native fast path only runs structure for small incremental
|
|
439
|
-
// builds: !isFullBuild && changedCount <= 5 && existingFileCount > 20.
|
|
440
|
-
// For all other cases (full builds, large incrementals), we must
|
|
441
|
-
// run JS buildStructure() to create directory nodes + contains edges (#804).
|
|
442
|
-
// Always run JS structure — the native fast-path has an additional
|
|
443
|
-
// existingFileCount > 20 guard that isn't reflected in the result JSON,
|
|
444
|
-
// so we can't reliably detect whether native actually ran structure.
|
|
445
|
-
const nativeHandledStructure = false;
|
|
446
|
-
const needsStructure = !nativeHandledStructure;
|
|
447
|
-
|
|
448
|
-
if (needsAnalysis || needsStructure) {
|
|
449
|
-
// WAL handoff: checkpoint through rusqlite, close nativeDb,
|
|
450
|
-
// reopen better-sqlite3 with a fresh page cache (#715, #736).
|
|
451
|
-
try {
|
|
452
|
-
ctx.nativeDb!.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
453
|
-
} catch {
|
|
454
|
-
/* ignore checkpoint errors */
|
|
455
|
-
}
|
|
456
|
-
try {
|
|
457
|
-
ctx.nativeDb!.close();
|
|
458
|
-
} catch {
|
|
459
|
-
/* ignore close errors */
|
|
460
|
-
}
|
|
461
|
-
ctx.nativeDb = undefined;
|
|
462
|
-
try {
|
|
463
|
-
ctx.db.close();
|
|
464
|
-
} catch {
|
|
465
|
-
/* ignore close errors */
|
|
466
|
-
}
|
|
467
|
-
ctx.db = null!; // avoid closeDbPair operating on a stale handle
|
|
468
|
-
try {
|
|
469
|
-
ctx.db = openDb(ctx.dbPath);
|
|
470
|
-
} catch (reopenErr) {
|
|
471
|
-
warn(`Failed to reopen DB after native build: ${(reopenErr as Error).message}`);
|
|
472
|
-
// Native build succeeded but we can't run post-processing — return partial result
|
|
473
|
-
return {
|
|
474
|
-
phases: {
|
|
475
|
-
setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
|
|
476
|
-
parseMs: +(p.parseMs ?? 0).toFixed(1),
|
|
477
|
-
insertMs: +(p.insertMs ?? 0).toFixed(1),
|
|
478
|
-
resolveMs: +(p.resolveMs ?? 0).toFixed(1),
|
|
479
|
-
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
|
|
480
|
-
structureMs: +(p.structureMs ?? 0).toFixed(1),
|
|
481
|
-
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
|
|
482
|
-
astMs: 0,
|
|
483
|
-
complexityMs: 0,
|
|
484
|
-
cfgMs: 0,
|
|
485
|
-
dataflowMs: 0,
|
|
486
|
-
finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
|
|
487
|
-
},
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Reconstruct fileSymbols from DB. For structure we need ALL files
|
|
492
|
-
// (to build complete directory tree); for analysis we scope to
|
|
493
|
-
// changed files only. Load all files, then scope analysis later.
|
|
494
|
-
const allFileRows = ctx.db
|
|
495
|
-
.prepare(
|
|
496
|
-
'SELECT file, name, kind, line, end_line as endLine FROM nodes WHERE file IS NOT NULL ORDER BY file, line',
|
|
497
|
-
)
|
|
498
|
-
.all() as {
|
|
499
|
-
file: string;
|
|
500
|
-
name: string;
|
|
501
|
-
kind: string;
|
|
502
|
-
line: number;
|
|
503
|
-
endLine: number | null;
|
|
504
|
-
}[];
|
|
505
|
-
|
|
506
|
-
const allFileSymbols = new Map<string, ExtractorOutput>();
|
|
507
|
-
for (const row of allFileRows) {
|
|
508
|
-
let entry = allFileSymbols.get(row.file);
|
|
509
|
-
if (!entry) {
|
|
510
|
-
entry = {
|
|
511
|
-
definitions: [],
|
|
512
|
-
calls: [],
|
|
513
|
-
imports: [],
|
|
514
|
-
classes: [],
|
|
515
|
-
exports: [],
|
|
516
|
-
typeMap: new Map(),
|
|
517
|
-
};
|
|
518
|
-
allFileSymbols.set(row.file, entry);
|
|
519
|
-
}
|
|
520
|
-
entry.definitions.push({
|
|
521
|
-
name: row.name,
|
|
522
|
-
kind: row.kind as Definition['kind'],
|
|
523
|
-
line: row.line,
|
|
524
|
-
endLine: row.endLine ?? undefined,
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Populate import/export counts from DB edges so buildStructure
|
|
529
|
-
// computes correct import_count/export_count in node_metrics.
|
|
530
|
-
// The extractor arrays aren't persisted to the DB, so we derive
|
|
531
|
-
// counts from edge data instead (#804).
|
|
532
|
-
const importCountRows = ctx.db
|
|
533
|
-
.prepare(
|
|
534
|
-
`SELECT n.file, COUNT(*) AS cnt
|
|
535
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
536
|
-
WHERE e.kind IN ('imports', 'imports-type', 'dynamic-imports')
|
|
537
|
-
AND n.file IS NOT NULL
|
|
538
|
-
GROUP BY n.file`,
|
|
539
|
-
)
|
|
540
|
-
.all() as { file: string; cnt: number }[];
|
|
541
|
-
for (const row of importCountRows) {
|
|
542
|
-
const entry = allFileSymbols.get(row.file);
|
|
543
|
-
if (entry) entry.imports = new Array(row.cnt) as ExtractorOutput['imports'];
|
|
544
|
-
}
|
|
545
|
-
// Export count: definitions in this file that are imported by other files
|
|
546
|
-
const exportCountRows = ctx.db
|
|
547
|
-
.prepare(
|
|
548
|
-
`SELECT n_tgt.file, COUNT(DISTINCT n_tgt.id) AS cnt
|
|
549
|
-
FROM edges e
|
|
550
|
-
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
551
|
-
JOIN nodes n_src ON e.source_id = n_src.id
|
|
552
|
-
WHERE e.kind IN ('imports', 'imports-type', 'reexports')
|
|
553
|
-
AND n_tgt.file IS NOT NULL
|
|
554
|
-
AND n_src.file != n_tgt.file
|
|
555
|
-
GROUP BY n_tgt.file`,
|
|
556
|
-
)
|
|
557
|
-
.all() as { file: string; cnt: number }[];
|
|
558
|
-
for (const row of exportCountRows) {
|
|
559
|
-
const entry = allFileSymbols.get(row.file);
|
|
560
|
-
if (entry) entry.exports = new Array(row.cnt) as ExtractorOutput['exports'];
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// ── Structure phase: directory nodes + contains edges (#804) ──
|
|
564
|
-
if (needsStructure) {
|
|
565
|
-
const structureStart = performance.now();
|
|
566
|
-
try {
|
|
567
|
-
// Derive directories from file paths
|
|
568
|
-
const directories = new Set<string>();
|
|
569
|
-
for (const relPath of allFileSymbols.keys()) {
|
|
570
|
-
const parts = relPath.split('/');
|
|
571
|
-
for (let i = 1; i < parts.length; i++) {
|
|
572
|
-
directories.add(parts.slice(0, i).join('/'));
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Build line count map from DB metrics or file content
|
|
577
|
-
const lineCountMap = new Map<string, number>();
|
|
578
|
-
const cachedLineCounts = ctx.db
|
|
579
|
-
.prepare(
|
|
580
|
-
`SELECT n.name AS file, m.line_count
|
|
581
|
-
FROM node_metrics m JOIN nodes n ON m.node_id = n.id
|
|
582
|
-
WHERE n.kind = 'file'`,
|
|
583
|
-
)
|
|
584
|
-
.all() as Array<{ file: string; line_count: number }>;
|
|
585
|
-
for (const row of cachedLineCounts) {
|
|
586
|
-
lineCountMap.set(row.file, row.line_count);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Native ran no structure at all — always do a full rebuild so
|
|
590
|
-
// every directory gets nodes + contains edges (#804).
|
|
591
|
-
const changedFilePaths = null;
|
|
592
|
-
|
|
593
|
-
const { buildStructure: buildStructureFn } = (await import(
|
|
594
|
-
'../../../features/structure.js'
|
|
595
|
-
)) as {
|
|
596
|
-
buildStructure: (
|
|
597
|
-
db: typeof ctx.db,
|
|
598
|
-
fileSymbols: Map<string, ExtractorOutput>,
|
|
599
|
-
rootDir: string,
|
|
600
|
-
lineCountMap: Map<string, number>,
|
|
601
|
-
directories: Set<string>,
|
|
602
|
-
changedFiles: string[] | null,
|
|
603
|
-
) => void;
|
|
604
|
-
};
|
|
605
|
-
buildStructureFn(
|
|
606
|
-
ctx.db,
|
|
607
|
-
allFileSymbols,
|
|
608
|
-
ctx.rootDir,
|
|
609
|
-
lineCountMap,
|
|
610
|
-
directories,
|
|
611
|
-
changedFilePaths,
|
|
612
|
-
);
|
|
613
|
-
debug('Structure phase completed after native orchestrator');
|
|
614
|
-
} catch (err) {
|
|
615
|
-
warn(`Structure phase failed after native build: ${toErrorMessage(err)}`);
|
|
616
|
-
}
|
|
617
|
-
structurePatchMs = performance.now() - structureStart;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ── Analysis phase ──
|
|
621
|
-
if (needsAnalysis) {
|
|
622
|
-
// Scope analysis fileSymbols to changed files only
|
|
623
|
-
const changedFiles = result.changedFiles;
|
|
624
|
-
let analysisFileSymbols: Map<string, ExtractorOutput>;
|
|
625
|
-
if (changedFiles && changedFiles.length > 0) {
|
|
626
|
-
analysisFileSymbols = new Map();
|
|
627
|
-
for (const f of changedFiles) {
|
|
628
|
-
const entry = allFileSymbols.get(f);
|
|
629
|
-
if (entry) analysisFileSymbols.set(f, entry);
|
|
630
|
-
}
|
|
631
|
-
} else {
|
|
632
|
-
analysisFileSymbols = allFileSymbols;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Reopen nativeDb for analysis features (suspend/resume WAL pattern).
|
|
636
|
-
const native = loadNative();
|
|
637
|
-
if (native?.NativeDatabase) {
|
|
638
|
-
try {
|
|
639
|
-
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
640
|
-
if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
641
|
-
} catch {
|
|
642
|
-
ctx.nativeDb = undefined;
|
|
643
|
-
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
try {
|
|
648
|
-
const { runAnalyses: runAnalysesFn } = await import(
|
|
649
|
-
'../../../ast-analysis/engine.js'
|
|
650
|
-
);
|
|
651
|
-
analysisTiming = await runAnalysesFn(
|
|
652
|
-
ctx.db,
|
|
653
|
-
analysisFileSymbols,
|
|
654
|
-
ctx.rootDir,
|
|
655
|
-
opts,
|
|
656
|
-
ctx.engineOpts,
|
|
657
|
-
);
|
|
658
|
-
} catch (err) {
|
|
659
|
-
warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Close nativeDb after analyses
|
|
663
|
-
if (ctx.nativeDb) {
|
|
664
|
-
try {
|
|
665
|
-
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
666
|
-
} catch {
|
|
667
|
-
/* ignore checkpoint errors */
|
|
668
|
-
}
|
|
669
|
-
try {
|
|
670
|
-
ctx.nativeDb.close();
|
|
671
|
-
} catch {
|
|
672
|
-
/* ignore close errors */
|
|
673
|
-
}
|
|
674
|
-
ctx.nativeDb = undefined;
|
|
675
|
-
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
681
|
-
return {
|
|
682
|
-
phases: {
|
|
683
|
-
setupMs: +((p.setupMs ?? 0) + (p.collectMs ?? 0) + (p.detectMs ?? 0)).toFixed(1),
|
|
684
|
-
parseMs: +(p.parseMs ?? 0).toFixed(1),
|
|
685
|
-
insertMs: +(p.insertMs ?? 0).toFixed(1),
|
|
686
|
-
resolveMs: +(p.resolveMs ?? 0).toFixed(1),
|
|
687
|
-
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
|
|
688
|
-
structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
|
|
689
|
-
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
|
|
690
|
-
astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
|
|
691
|
-
complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
|
|
692
|
-
cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
|
|
693
|
-
dataflowMs: +(analysisTiming.dataflowMs ?? 0).toFixed(1),
|
|
694
|
-
finalizeMs: +(p.finalizeMs ?? 0).toFixed(1),
|
|
695
|
-
},
|
|
696
|
-
};
|
|
697
|
-
} catch (err) {
|
|
698
|
-
warn(
|
|
699
|
-
`Native build orchestrator failed, falling back to JS pipeline: ${toErrorMessage(err)}`,
|
|
700
|
-
);
|
|
701
|
-
// Fall through to JS pipeline
|
|
702
|
-
}
|
|
794
|
+
try {
|
|
795
|
+
const nativeResult = await tryNativeOrchestrator(ctx);
|
|
796
|
+
if (nativeResult === 'early-exit') return;
|
|
797
|
+
if (nativeResult) return nativeResult;
|
|
798
|
+
} catch (err) {
|
|
799
|
+
warn(`Native build orchestrator failed, falling back to JS pipeline: ${toErrorMessage(err)}`);
|
|
800
|
+
// Fall through to JS pipeline
|
|
703
801
|
}
|
|
704
802
|
|
|
705
803
|
await runPipelineStages(ctx);
|