@optave/codegraph 3.9.1 → 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 -14
- 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 +2 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +2 -2
- 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/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 +5 -5
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +6 -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/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 +133 -69
- 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 +15 -2
- 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 +2 -3
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +5 -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/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/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/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/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/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 +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/cli/commands/batch.ts +5 -26
- package/src/cli/commands/structure.ts +21 -1
- package/src/db/connection.ts +2 -2
- package/src/db/index.ts +2 -0
- package/src/domain/analysis/context.ts +5 -15
- package/src/domain/analysis/dependencies.ts +6 -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/native-db-proxy.ts +98 -0
- package/src/domain/graph/builder/pipeline.ts +135 -67
- package/src/domain/graph/builder/stages/build-edges.ts +15 -2
- 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 +2 -3
- package/src/domain/parser.ts +6 -2
- package/src/domain/queries.ts +1 -1
- 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/flow.ts +2 -1
- package/src/features/manifesto.ts +15 -1
- package/src/infrastructure/config.ts +1 -0
- package/src/presentation/batch.ts +1 -0
- package/src/presentation/structure.ts +2 -2
- package/src/shared/normalize.ts +10 -0
- package/src/types.ts +1 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeDbProxy — wraps a NativeDatabase (rusqlite via napi-rs) to satisfy the
|
|
3
|
+
* BetterSqlite3Database interface. When the native addon is available, the
|
|
4
|
+
* build pipeline uses this proxy as `ctx.db` so that every stage operates on a
|
|
5
|
+
* single rusqlite connection — no dual-connection WAL corruption, no
|
|
6
|
+
* open/close/reopen dance.
|
|
7
|
+
*
|
|
8
|
+
* When native is unavailable, the pipeline falls back to real better-sqlite3.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { BetterSqlite3Database, NativeDatabase, SqliteStatement } from '../../../types.js';
|
|
12
|
+
|
|
13
|
+
/** Sanitize params for napi-rs: better-sqlite3 treats `undefined` as NULL,
|
|
14
|
+
* but serde_json cannot represent `undefined`. Replace with `null`. */
|
|
15
|
+
function sanitize(params: unknown[]): Array<string | number | null> {
|
|
16
|
+
return params.map((p) => (p === undefined ? null : p)) as Array<string | number | null>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class NativeDbProxy implements BetterSqlite3Database {
|
|
20
|
+
readonly #ndb: NativeDatabase;
|
|
21
|
+
/** Advisory lock path — set by the pipeline so closeDb() can release it. */
|
|
22
|
+
__lockPath?: string;
|
|
23
|
+
|
|
24
|
+
constructor(nativeDb: NativeDatabase) {
|
|
25
|
+
this.#ndb = nativeDb;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
prepare<TRow = unknown>(sql: string): SqliteStatement<TRow> {
|
|
29
|
+
const ndb = this.#ndb;
|
|
30
|
+
const stmt: SqliteStatement<TRow> = {
|
|
31
|
+
all(...params: unknown[]): TRow[] {
|
|
32
|
+
return ndb.queryAll(sql, sanitize(params)) as TRow[];
|
|
33
|
+
},
|
|
34
|
+
get(...params: unknown[]): TRow | undefined {
|
|
35
|
+
return (ndb.queryGet(sql, sanitize(params)) ?? undefined) as TRow | undefined;
|
|
36
|
+
},
|
|
37
|
+
run(...params: unknown[]): { changes: number; lastInsertRowid: number | bigint } {
|
|
38
|
+
ndb.queryAll(sql, sanitize(params));
|
|
39
|
+
// Retrieve last_insert_rowid via SQLite scalar function so callers
|
|
40
|
+
// that depend on it (e.g. CFG block edge mapping) get correct values.
|
|
41
|
+
const row = ndb.queryGet('SELECT last_insert_rowid() AS rid', []) as { rid: number } | null;
|
|
42
|
+
return { changes: 0, lastInsertRowid: row?.rid ?? 0 };
|
|
43
|
+
},
|
|
44
|
+
iterate(): IterableIterator<TRow> {
|
|
45
|
+
throw new Error('iterate() is not supported via NativeDbProxy');
|
|
46
|
+
},
|
|
47
|
+
raw(): SqliteStatement<TRow> {
|
|
48
|
+
return stmt; // no-op — .raw() is not used in the build pipeline
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return stmt;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
exec(sql: string): this {
|
|
55
|
+
this.#ndb.exec(sql);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pragma(sql: string): unknown {
|
|
60
|
+
return this.#ndb.pragma(sql);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
close(): void {
|
|
64
|
+
// No-op: the pipeline manages the NativeDatabase lifecycle directly.
|
|
65
|
+
// closeDbPair() calls nativeDb.close() separately.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get open(): boolean {
|
|
69
|
+
return this.#ndb.isOpen;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get name(): string {
|
|
73
|
+
return this.#ndb.dbPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
transaction<F extends (...args: any[]) => any>(
|
|
77
|
+
fn: F,
|
|
78
|
+
): (...args: F extends (...a: infer A) => unknown ? A : never) => ReturnType<F> {
|
|
79
|
+
const ndb = this.#ndb;
|
|
80
|
+
return ((...args: unknown[]) => {
|
|
81
|
+
// NOTE: nested transactions (savepoints) are not supported — ensure callers
|
|
82
|
+
// do not invoke a transaction() wrapper from within an existing transaction.
|
|
83
|
+
ndb.exec('BEGIN');
|
|
84
|
+
try {
|
|
85
|
+
const result = fn(...args);
|
|
86
|
+
ndb.exec('COMMIT');
|
|
87
|
+
return result;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
try {
|
|
90
|
+
ndb.exec('ROLLBACK');
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore rollback errors — the original error is more important
|
|
93
|
+
}
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}) as any;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -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
|
|
|
@@ -275,7 +272,10 @@ interface NativeOrchestratorResult {
|
|
|
275
272
|
function shouldSkipNativeOrchestrator(ctx: PipelineContext): string | null {
|
|
276
273
|
if (process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1') return 'CODEGRAPH_FORCE_JS_PIPELINE=1';
|
|
277
274
|
if (ctx.forceFullRebuild) return 'forceFullRebuild';
|
|
278
|
-
|
|
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
279
|
if (orchestratorBuggy) return `buggy addon ${ctx.engineVersion}`;
|
|
280
280
|
if (ctx.engineName !== 'native') return `engine=${ctx.engineName}`;
|
|
281
281
|
return null;
|
|
@@ -472,16 +472,20 @@ async function runPostNativeAnalysis(
|
|
|
472
472
|
analysisFileSymbols = allFileSymbols;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
}
|
|
484
486
|
}
|
|
487
|
+
} else if (ctx.engineOpts) {
|
|
488
|
+
ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
485
489
|
}
|
|
486
490
|
|
|
487
491
|
try {
|
|
@@ -501,8 +505,8 @@ async function runPostNativeAnalysis(
|
|
|
501
505
|
warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
|
|
502
506
|
}
|
|
503
507
|
|
|
504
|
-
// Close nativeDb after analyses
|
|
505
|
-
if (ctx.nativeDb) {
|
|
508
|
+
// Close nativeDb after analyses (skip in native-first — single connection stays open)
|
|
509
|
+
if (ctx.nativeDb && !ctx.nativeFirstProxy) {
|
|
506
510
|
try {
|
|
507
511
|
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
508
512
|
} catch {
|
|
@@ -553,6 +557,28 @@ async function tryNativeOrchestrator(
|
|
|
553
557
|
debug(`Skipping native orchestrator: ${skipReason}`);
|
|
554
558
|
return undefined;
|
|
555
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
|
+
|
|
556
582
|
if (!ctx.nativeDb?.buildGraph) return undefined;
|
|
557
583
|
|
|
558
584
|
const resultJson = ctx.nativeDb.buildGraph(
|
|
@@ -607,7 +633,8 @@ async function tryNativeOrchestrator(
|
|
|
607
633
|
const needsStructure = !result.structureHandled;
|
|
608
634
|
|
|
609
635
|
if (needsAnalysis || needsStructure) {
|
|
610
|
-
|
|
636
|
+
// In native-first mode the proxy is already wired — no WAL handoff needed.
|
|
637
|
+
if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
|
|
611
638
|
// DB reopen failed — return partial result
|
|
612
639
|
return formatNativeTimingResult(p, 0, analysisTiming);
|
|
613
640
|
}
|
|
@@ -639,12 +666,35 @@ async function tryNativeOrchestrator(
|
|
|
639
666
|
// ── Pipeline stages execution ───────────────────────────────────────────
|
|
640
667
|
|
|
641
668
|
async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
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.
|
|
648
698
|
if (ctx.db && ctx.nativeDb) {
|
|
649
699
|
suspendNativeDb(ctx, 'pre-collect');
|
|
650
700
|
}
|
|
@@ -656,10 +706,13 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
656
706
|
|
|
657
707
|
await parseFiles(ctx);
|
|
658
708
|
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
|
|
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) {
|
|
663
716
|
reopenNativeDb(ctx, 'insertNodes');
|
|
664
717
|
}
|
|
665
718
|
|
|
@@ -677,13 +730,28 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
677
730
|
|
|
678
731
|
// Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow)
|
|
679
732
|
// which use suspendJsDb/resumeJsDb WAL checkpoint before native writes.
|
|
680
|
-
|
|
733
|
+
// Skip for small incremental builds — same rationale as insertNodes above.
|
|
734
|
+
if (ctx.nativeAvailable && !smallIncremental) {
|
|
681
735
|
reopenNativeDb(ctx, 'analyses');
|
|
682
736
|
if (ctx.nativeDb && ctx.engineOpts) {
|
|
683
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
|
+
};
|
|
684
750
|
}
|
|
685
751
|
if (!ctx.nativeDb && ctx.engineOpts) {
|
|
686
752
|
ctx.engineOpts.nativeDb = undefined;
|
|
753
|
+
ctx.engineOpts.suspendJsDb = undefined;
|
|
754
|
+
ctx.engineOpts.resumeJsDb = undefined;
|
|
687
755
|
}
|
|
688
756
|
}
|
|
689
757
|
|
|
@@ -342,7 +342,7 @@ function buildCallEdgesNative(
|
|
|
342
342
|
if (!fileNodeRow) continue;
|
|
343
343
|
|
|
344
344
|
const importedNames = buildImportedNamesForNative(ctx, relPath, symbols, rootDir);
|
|
345
|
-
const
|
|
345
|
+
const typeMapRaw: Array<{ name: string; typeName: string; confidence: number }> =
|
|
346
346
|
symbols.typeMap instanceof Map
|
|
347
347
|
? [...symbols.typeMap.entries()].map(([name, entry]) => ({
|
|
348
348
|
name,
|
|
@@ -352,6 +352,19 @@ function buildCallEdgesNative(
|
|
|
352
352
|
: Array.isArray(symbols.typeMap)
|
|
353
353
|
? (symbols.typeMap as Array<{ name: string; typeName: string; confidence: number }>)
|
|
354
354
|
: [];
|
|
355
|
+
// Deduplicate: keep highest-confidence entry per name (first-wins on tie),
|
|
356
|
+
// matching JS setTypeMapEntry semantics. The Map branch is already
|
|
357
|
+
// deduped by setTypeMapEntry — this loop is only needed for the Array
|
|
358
|
+
// branch (pre-rebuilt native addon) but runs unconditionally as
|
|
359
|
+
// belt-and-suspenders since it's a cheap O(n) pass.
|
|
360
|
+
const typeMapDedup = new Map<string, { name: string; typeName: string; confidence: number }>();
|
|
361
|
+
for (const entry of typeMapRaw) {
|
|
362
|
+
const existing = typeMapDedup.get(entry.name);
|
|
363
|
+
if (!existing || entry.confidence > existing.confidence) {
|
|
364
|
+
typeMapDedup.set(entry.name, entry);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const typeMap = [...typeMapDedup.values()];
|
|
355
368
|
nativeFiles.push({
|
|
356
369
|
file: relPath,
|
|
357
370
|
fileNodeId: fileNodeRow.id,
|
|
@@ -699,7 +712,7 @@ function loadNodes(ctx: PipelineContext): { rows: QueryNodeRow[]; scoped: boolea
|
|
|
699
712
|
const nodeKindFilter = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`;
|
|
700
713
|
|
|
701
714
|
// Gate: only scope for small incremental on large codebases
|
|
702
|
-
if (!isFullBuild && fileSymbols.size <=
|
|
715
|
+
if (!isFullBuild && fileSymbols.size <= ctx.config.build.smallFilesThreshold) {
|
|
703
716
|
const existingFileCount = (
|
|
704
717
|
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get() as { c: number }
|
|
705
718
|
).c;
|
|
@@ -37,7 +37,7 @@ export async function buildStructure(ctx: PipelineContext): Promise<void> {
|
|
|
37
37
|
// For small incremental builds on large codebases, use a fast path that
|
|
38
38
|
// updates only the changed files' metrics via targeted SQL instead of
|
|
39
39
|
// loading ALL definitions from DB (~8ms) and recomputing ALL metrics (~15ms).
|
|
40
|
-
// Gate: ≤
|
|
40
|
+
// Gate: ≤smallFilesThreshold changed files AND significantly more existing files (>20) to
|
|
41
41
|
// avoid triggering on small test fixtures where directory metrics matter.
|
|
42
42
|
const useNativeReads = ctx.engineName === 'native' && !!ctx.nativeDb;
|
|
43
43
|
const existingFileCount = !isFullBuild
|
|
@@ -52,7 +52,7 @@ export async function buildStructure(ctx: PipelineContext): Promise<void> {
|
|
|
52
52
|
const useSmallIncrementalFastPath =
|
|
53
53
|
!isFullBuild &&
|
|
54
54
|
changedFileList != null &&
|
|
55
|
-
changedFileList.length <=
|
|
55
|
+
changedFileList.length <= ctx.config.build.smallFilesThreshold &&
|
|
56
56
|
existingFileCount > 20;
|
|
57
57
|
|
|
58
58
|
if (!isFullBuild && !useSmallIncrementalFastPath) {
|
|
@@ -58,26 +58,9 @@ function getChangedFiles(
|
|
|
58
58
|
db: BetterSqlite3Database,
|
|
59
59
|
allFiles: string[],
|
|
60
60
|
rootDir: string,
|
|
61
|
-
nativeDb?: NativeDatabase,
|
|
62
61
|
): ChangeResult {
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
const data = nativeDb.getFileHashData();
|
|
66
|
-
if (!data.exists) {
|
|
67
|
-
return {
|
|
68
|
-
changed: allFiles.map((f) => ({ file: f })),
|
|
69
|
-
removed: [],
|
|
70
|
-
isFullBuild: true,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
const existing = new Map<string, FileHashRow>(data.rows.map((r) => [r.file, r]));
|
|
74
|
-
const removed = detectRemovedFiles(existing, allFiles, rootDir);
|
|
75
|
-
const journalResult = tryJournalTier(db, existing, rootDir, removed, data.maxMtime);
|
|
76
|
-
if (journalResult) return journalResult;
|
|
77
|
-
return mtimeAndHashTiers(existing, allFiles, rootDir, removed);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// WASM / fallback path
|
|
62
|
+
// NativeDatabase is not open during change detection (deferred to after
|
|
63
|
+
// early-exit check). All queries use better-sqlite3 here.
|
|
81
64
|
let hasTable = false;
|
|
82
65
|
try {
|
|
83
66
|
db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
|
|
@@ -294,14 +277,14 @@ async function runPendingAnalysis(ctx: PipelineContext): Promise<boolean> {
|
|
|
294
277
|
rootDir,
|
|
295
278
|
analysisOpts,
|
|
296
279
|
);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
280
|
+
const { runAnalyses } = await import('../../../../ast-analysis/engine.js');
|
|
281
|
+
await runAnalyses(
|
|
282
|
+
db,
|
|
283
|
+
analysisSymbols,
|
|
284
|
+
rootDir,
|
|
285
|
+
{ ast: false, complexity: false, cfg: needsCfg, dataflow: needsDataflow },
|
|
286
|
+
engineOpts,
|
|
287
|
+
);
|
|
305
288
|
return true;
|
|
306
289
|
}
|
|
307
290
|
|
|
@@ -487,12 +470,7 @@ export async function detectChanges(ctx: PipelineContext): Promise<void> {
|
|
|
487
470
|
}
|
|
488
471
|
const increResult =
|
|
489
472
|
incremental && !forceFullRebuild
|
|
490
|
-
? getChangedFiles(
|
|
491
|
-
db,
|
|
492
|
-
allFiles,
|
|
493
|
-
rootDir,
|
|
494
|
-
ctx.engineName === 'native' ? ctx.nativeDb : undefined,
|
|
495
|
-
)
|
|
473
|
+
? getChangedFiles(db, allFiles, rootDir)
|
|
496
474
|
: {
|
|
497
475
|
changed: allFiles.map((f): ChangedFile => ({ file: f })),
|
|
498
476
|
removed: [] as string[],
|
|
@@ -258,7 +258,7 @@ export async function finalize(ctx: PipelineContext): Promise<void> {
|
|
|
258
258
|
// immediately after build.
|
|
259
259
|
const pair = { db: ctx.db, nativeDb: ctx.nativeDb };
|
|
260
260
|
const isTempDir = path.resolve(rootDir).startsWith(path.resolve(tmpdir()));
|
|
261
|
-
if (!isFullBuild && allSymbols.size <=
|
|
261
|
+
if (!isFullBuild && allSymbols.size <= ctx.config.build.smallFilesThreshold && !isTempDir) {
|
|
262
262
|
closeDbPairDeferred(pair);
|
|
263
263
|
} else {
|
|
264
264
|
closeDbPair(pair);
|
|
@@ -159,23 +159,26 @@ function tryNativeInsert(ctx: PipelineContext): boolean {
|
|
|
159
159
|
}
|
|
160
160
|
const fileHashes = buildFileHashes(allSymbols, precomputedData, metadataUpdates, rootDir);
|
|
161
161
|
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// written by the other (#696, #709, #715, #717).
|
|
162
|
+
// In native-first mode (single rusqlite connection), no WAL dance is needed.
|
|
163
|
+
// In dual-connection mode, checkpoint JS side before native write, then
|
|
164
|
+
// checkpoint native side after (#696, #709, #715, #717).
|
|
166
165
|
let result: boolean;
|
|
167
|
-
|
|
168
|
-
if (ctx.db) {
|
|
169
|
-
ctx.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
170
|
-
}
|
|
166
|
+
if (ctx.nativeFirstProxy) {
|
|
171
167
|
result = ctx.nativeDb!.bulkInsertNodes(batches, fileHashes, removed);
|
|
172
|
-
}
|
|
168
|
+
} else {
|
|
173
169
|
try {
|
|
174
|
-
ctx.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
if (ctx.db) {
|
|
171
|
+
ctx.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
172
|
+
}
|
|
173
|
+
result = ctx.nativeDb!.bulkInsertNodes(batches, fileHashes, removed);
|
|
174
|
+
} finally {
|
|
175
|
+
try {
|
|
176
|
+
ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
177
|
+
} catch (e) {
|
|
178
|
+
debug(
|
|
179
|
+
`tryNativeInsert: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
return result;
|
|
@@ -34,15 +34,14 @@ function buildReexportMap(ctx: PipelineContext): void {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Find barrel files related to changed files for scoped re-parsing.
|
|
37
|
-
* For small incremental builds (<=
|
|
37
|
+
* For small incremental builds (<=smallFilesThreshold files), only barrels that re-export from
|
|
38
38
|
* or are imported by the changed files. For larger changes, all barrels.
|
|
39
39
|
*/
|
|
40
40
|
function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
|
|
41
41
|
const { db, fileSymbols, rootDir, aliases } = ctx;
|
|
42
42
|
const changedRelPaths = new Set<string>(fileSymbols.keys());
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
if (changedRelPaths.size <= SMALL_CHANGE_THRESHOLD) {
|
|
44
|
+
if (changedRelPaths.size <= ctx.config.build.smallFilesThreshold) {
|
|
46
45
|
const allBarrelFiles = new Set(
|
|
47
46
|
(
|
|
48
47
|
db
|
package/src/domain/parser.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Tree } from 'web-tree-sitter';
|
|
|
5
5
|
import { Language, Parser, Query } from 'web-tree-sitter';
|
|
6
6
|
import { debug, warn } from '../infrastructure/logger.js';
|
|
7
7
|
import { getNative, getNativePackageVersion, loadNative } from '../infrastructure/native.js';
|
|
8
|
-
import { toErrorMessage } from '../shared/errors.js';
|
|
8
|
+
import { ParseError, toErrorMessage } from '../shared/errors.js';
|
|
9
9
|
import type {
|
|
10
10
|
EngineMode,
|
|
11
11
|
ExtractorOutput,
|
|
@@ -188,7 +188,11 @@ async function doLoadLanguage(entry: LanguageRegistryEntry): Promise<void> {
|
|
|
188
188
|
_queryCache.set(entry.id, new Query(lang, patterns.join('\n')));
|
|
189
189
|
}
|
|
190
190
|
} catch (e: unknown) {
|
|
191
|
-
if (entry.required)
|
|
191
|
+
if (entry.required)
|
|
192
|
+
throw new ParseError(`Required parser ${entry.id} failed to initialize`, {
|
|
193
|
+
file: entry.grammarFile,
|
|
194
|
+
cause: e as Error,
|
|
195
|
+
});
|
|
192
196
|
warn(
|
|
193
197
|
`${entry.id} parser failed to initialize: ${(e as Error).message}. ${entry.id} files will be skipped.`,
|
|
194
198
|
);
|
package/src/domain/queries.ts
CHANGED
|
@@ -22,7 +22,7 @@ export {
|
|
|
22
22
|
VALID_ROLES,
|
|
23
23
|
} from '../shared/kinds.js';
|
|
24
24
|
// ── Shared utilities ─────────────────────────────────────────────────────
|
|
25
|
-
export { kindIcon, normalizeSymbol } from '../shared/normalize.js';
|
|
25
|
+
export { kindIcon, normalizeSymbol, toSymbolRef } from '../shared/normalize.js';
|
|
26
26
|
export { briefData } from './analysis/brief.js';
|
|
27
27
|
export { contextData, explainData } from './analysis/context.js';
|
|
28
28
|
export { fileDepsData, filePathData, fnDepsData, pathData } from './analysis/dependencies.js';
|