@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
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { collectFile } from '../../db/query-builder.js';
|
|
3
3
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
4
|
-
import { BATCH_COMMANDS,
|
|
5
|
-
import {
|
|
4
|
+
import { BATCH_COMMANDS, splitTargets } from '../../features/batch.js';
|
|
5
|
+
import { batchQuery } from '../../presentation/batch.js';
|
|
6
6
|
import { ConfigError, toErrorMessage } from '../../shared/errors.js';
|
|
7
7
|
import type { CommandDefinition } from '../types.js';
|
|
8
8
|
|
|
9
|
-
interface MultiBatchItem {
|
|
10
|
-
command: string;
|
|
11
|
-
target: string;
|
|
12
|
-
opts?: Record<string, unknown>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isMultiBatch(targets: unknown[]): targets is MultiBatchItem[] {
|
|
16
|
-
return (
|
|
17
|
-
targets.length > 0 &&
|
|
18
|
-
typeof targets[0] === 'object' &&
|
|
19
|
-
targets[0] !== null &&
|
|
20
|
-
'command' in targets[0]
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
9
|
export const command: CommandDefinition = {
|
|
25
10
|
name: 'batch <command> [targets...]',
|
|
26
11
|
description: `Run a query against multiple targets in one call. Output is always JSON.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
|
|
@@ -69,18 +54,12 @@ export const command: CommandDefinition = {
|
|
|
69
54
|
);
|
|
70
55
|
}
|
|
71
56
|
|
|
72
|
-
|
|
57
|
+
batchQuery(targets as Array<string | { command: string; target: string }>, opts.db, {
|
|
58
|
+
command,
|
|
73
59
|
depth: opts.depth ? parseInt(opts.depth as string, 10) : undefined,
|
|
74
60
|
file: opts.file,
|
|
75
61
|
kind: opts.kind,
|
|
76
62
|
noTests: ctx.resolveNoTests(opts),
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
if (isMultiBatch(targets)) {
|
|
80
|
-
const data = multiBatchData(targets as MultiBatchItem[], opts.db, batchOpts);
|
|
81
|
-
console.log(JSON.stringify(data, null, 2));
|
|
82
|
-
} else {
|
|
83
|
-
batch(command!, targets as string[], opts.db, batchOpts);
|
|
84
|
-
}
|
|
63
|
+
});
|
|
85
64
|
},
|
|
86
65
|
};
|
|
@@ -15,9 +15,29 @@ export const command: CommandDefinition = {
|
|
|
15
15
|
['--limit <number>', 'Max results to return'],
|
|
16
16
|
['--offset <number>', 'Skip N results (default: 0)'],
|
|
17
17
|
['--ndjson', 'Newline-delimited JSON output'],
|
|
18
|
+
['--modules', 'Show module boundaries (directories with high cohesion)'],
|
|
19
|
+
['--threshold <number>', 'Cohesion threshold for --modules (default: 0.3)'],
|
|
18
20
|
],
|
|
19
21
|
async execute([dir], opts, ctx) {
|
|
20
|
-
const { structureData, formatStructure } =
|
|
22
|
+
const { structureData, formatStructure, moduleBoundariesData, formatModuleBoundaries } =
|
|
23
|
+
await import('../../presentation/structure.js');
|
|
24
|
+
|
|
25
|
+
if (opts.modules) {
|
|
26
|
+
const parsed = opts.threshold ? parseFloat(opts.threshold as string) : undefined;
|
|
27
|
+
if (parsed !== undefined && Number.isNaN(parsed)) {
|
|
28
|
+
console.error('Error: --threshold must be a number');
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const data = moduleBoundariesData(opts.db, {
|
|
33
|
+
threshold: parsed,
|
|
34
|
+
});
|
|
35
|
+
if (!ctx.outputResult(data, 'modules', opts)) {
|
|
36
|
+
console.log(formatModuleBoundaries(data));
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
const qOpts = ctx.resolveQueryOpts(opts);
|
|
22
42
|
const data = structureData(opts.db, {
|
|
23
43
|
directory: dir,
|
package/src/db/connection.ts
CHANGED
|
@@ -109,7 +109,7 @@ function isProcessAlive(pid: number): boolean {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function acquireAdvisoryLock(dbPath: string): void {
|
|
112
|
+
export function acquireAdvisoryLock(dbPath: string): void {
|
|
113
113
|
const lockPath = `${dbPath}.lock`;
|
|
114
114
|
try {
|
|
115
115
|
if (fs.existsSync(lockPath)) {
|
|
@@ -129,7 +129,7 @@ function acquireAdvisoryLock(dbPath: string): void {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function releaseAdvisoryLock(lockPath: string): void {
|
|
132
|
+
export function releaseAdvisoryLock(lockPath: string): void {
|
|
133
133
|
try {
|
|
134
134
|
const content = fs.readFileSync(lockPath, 'utf-8').trim();
|
|
135
135
|
if (Number(content) === process.pid) {
|
|
@@ -311,6 +311,7 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database {
|
|
|
311
311
|
}
|
|
312
312
|
const Database = getDatabase();
|
|
313
313
|
const db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;
|
|
314
|
+
db.pragma('busy_timeout = 5000');
|
|
314
315
|
|
|
315
316
|
warnOnVersionMismatch(() => {
|
|
316
317
|
const row = db
|
|
@@ -359,7 +360,7 @@ function openRepoNative(customDbPath?: string): { repo: Repository; close(): voi
|
|
|
359
360
|
*/
|
|
360
361
|
export function openRepo(
|
|
361
362
|
customDbPath?: string,
|
|
362
|
-
opts: { repo?: Repository } = {},
|
|
363
|
+
opts: { repo?: Repository; engine?: 'native' | 'wasm' | 'auto' } = {},
|
|
363
364
|
): { repo: Repository; close(): void } {
|
|
364
365
|
if (opts.repo != null) {
|
|
365
366
|
if (!(opts.repo instanceof Repository)) {
|
|
@@ -370,15 +371,25 @@ export function openRepo(
|
|
|
370
371
|
return { repo: opts.repo, close() {} };
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
// Respect explicit engine selection: opts.engine > CODEGRAPH_ENGINE env > auto.
|
|
375
|
+
// This ensures --engine wasm and benchmark workers bypass the native path.
|
|
376
|
+
const engine = opts.engine || process.env.CODEGRAPH_ENGINE || 'auto';
|
|
377
|
+
|
|
373
378
|
// Try native rusqlite path first (Phase 6.14)
|
|
374
|
-
if (isNativeAvailable()) {
|
|
379
|
+
if (engine !== 'wasm' && isNativeAvailable()) {
|
|
375
380
|
try {
|
|
376
381
|
return openRepoNative(customDbPath);
|
|
377
382
|
} catch (e) {
|
|
378
383
|
// Re-throw user-visible errors (e.g. DB not found) — only silently
|
|
379
384
|
// fall back for native-engine failures (e.g. incompatible native binary).
|
|
380
385
|
if (e instanceof DbError) throw e;
|
|
381
|
-
|
|
386
|
+
// Re-throw locking/busy errors — falling back to better-sqlite3 would
|
|
387
|
+
// hit the same contention (and potentially hang without busy_timeout).
|
|
388
|
+
const msg = toErrorMessage(e);
|
|
389
|
+
if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
|
|
390
|
+
throw new DbError(`Database is busy (another process may be writing): ${msg}`, {});
|
|
391
|
+
}
|
|
392
|
+
debug(`openRepo: native path failed, falling back to better-sqlite3: ${msg}`);
|
|
382
393
|
}
|
|
383
394
|
}
|
|
384
395
|
|
|
@@ -405,14 +416,22 @@ export function openReadonlyWithNative(customPath?: string): {
|
|
|
405
416
|
} {
|
|
406
417
|
const db = openReadonlyOrFail(customPath);
|
|
407
418
|
|
|
419
|
+
// Respect explicit engine selection, consistent with openRepo().
|
|
420
|
+
const engine = process.env.CODEGRAPH_ENGINE || 'auto';
|
|
421
|
+
|
|
408
422
|
let nativeDb: NativeDatabase | undefined;
|
|
409
|
-
if (isNativeAvailable()) {
|
|
423
|
+
if (engine !== 'wasm' && isNativeAvailable()) {
|
|
410
424
|
try {
|
|
411
425
|
const dbPath = findDbPath(customPath);
|
|
412
426
|
const native = getNative();
|
|
413
427
|
nativeDb = native.NativeDatabase.openReadonly(dbPath);
|
|
414
428
|
} catch (e) {
|
|
415
|
-
|
|
429
|
+
const msg = toErrorMessage(e);
|
|
430
|
+
if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
|
|
431
|
+
debug(`openReadonlyWithNative: native path busy, skipping native DB: ${msg}`);
|
|
432
|
+
} else {
|
|
433
|
+
debug(`openReadonlyWithNative: native path failed: ${msg}`);
|
|
434
|
+
}
|
|
416
435
|
}
|
|
417
436
|
}
|
|
418
437
|
|
package/src/db/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
export type { LockedDatabase, LockedDatabasePair } from './connection.js';
|
|
4
4
|
export {
|
|
5
|
+
acquireAdvisoryLock,
|
|
5
6
|
closeDb,
|
|
6
7
|
closeDbDeferred,
|
|
7
8
|
closeDbPair,
|
|
@@ -13,6 +14,7 @@ export {
|
|
|
13
14
|
openReadonlyOrFail,
|
|
14
15
|
openReadonlyWithNative,
|
|
15
16
|
openRepo,
|
|
17
|
+
releaseAdvisoryLock,
|
|
16
18
|
} from './connection.js';
|
|
17
19
|
export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './migrations.js';
|
|
18
20
|
export {
|
|
@@ -223,4 +223,47 @@ export class Repository implements IRepository {
|
|
|
223
223
|
hasCoChangesTable(): boolean {
|
|
224
224
|
return false;
|
|
225
225
|
}
|
|
226
|
+
|
|
227
|
+
// ── Composite queries ──────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Complete fnDeps query in a single call. Returns null when not natively
|
|
230
|
+
* supported — callers should fall back to the JS-orchestrated path.
|
|
231
|
+
*/
|
|
232
|
+
fnDeps(
|
|
233
|
+
_name: string,
|
|
234
|
+
_opts?: { depth?: number; noTests?: boolean; file?: string; kind?: string },
|
|
235
|
+
): FnDepsResult | null {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Composite query result types ────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
export interface FnDepsNode {
|
|
243
|
+
name: string;
|
|
244
|
+
kind: string;
|
|
245
|
+
file: string;
|
|
246
|
+
line: number | null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface FnDepsCallerNode extends FnDepsNode {
|
|
250
|
+
viaHierarchy?: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface FnDepsEntry {
|
|
254
|
+
name: string;
|
|
255
|
+
kind: string;
|
|
256
|
+
file: string;
|
|
257
|
+
line: number | null;
|
|
258
|
+
endLine: number | null;
|
|
259
|
+
role: string | null;
|
|
260
|
+
fileHash: string | null;
|
|
261
|
+
callees: FnDepsNode[];
|
|
262
|
+
callers: FnDepsCallerNode[];
|
|
263
|
+
transitiveCallers: Record<number, FnDepsNode[]>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface FnDepsResult {
|
|
267
|
+
name: string;
|
|
268
|
+
results: FnDepsEntry[];
|
|
226
269
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Barrel re-export for repository/ modules.
|
|
2
2
|
|
|
3
|
+
export type { FnDepsCallerNode, FnDepsEntry, FnDepsNode, FnDepsResult } from './base.js';
|
|
3
4
|
export { Repository } from './base.js';
|
|
4
5
|
export { purgeFileData, purgeFilesData } from './build-stmts.js';
|
|
5
6
|
export { cachedStmt } from './cached-stmt.js';
|
|
@@ -45,7 +45,13 @@ import type {
|
|
|
45
45
|
TriageNodeRow,
|
|
46
46
|
TriageQueryOpts,
|
|
47
47
|
} from '../../types.js';
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
type FnDepsCallerNode,
|
|
50
|
+
type FnDepsEntry,
|
|
51
|
+
type FnDepsNode,
|
|
52
|
+
type FnDepsResult,
|
|
53
|
+
Repository,
|
|
54
|
+
} from './base.js';
|
|
49
55
|
|
|
50
56
|
// ── Row converters (napi camelCase → Repository snake_case) ─────────────
|
|
51
57
|
|
|
@@ -461,4 +467,64 @@ export class NativeRepository extends Repository {
|
|
|
461
467
|
}
|
|
462
468
|
return false;
|
|
463
469
|
}
|
|
470
|
+
|
|
471
|
+
// ── Composite queries ──────────────────────────────────────────────
|
|
472
|
+
fnDeps(
|
|
473
|
+
name: string,
|
|
474
|
+
opts?: { depth?: number; noTests?: boolean; file?: string; kind?: string },
|
|
475
|
+
): FnDepsResult | null {
|
|
476
|
+
if (typeof this.#ndb.fnDeps !== 'function') return null;
|
|
477
|
+
const raw = this.#ndb.fnDeps(
|
|
478
|
+
name,
|
|
479
|
+
opts?.depth ?? undefined,
|
|
480
|
+
opts?.noTests ?? undefined,
|
|
481
|
+
opts?.file ?? undefined,
|
|
482
|
+
opts?.kind ?? undefined,
|
|
483
|
+
);
|
|
484
|
+
// Convert from native format (transitive_callers as array of groups)
|
|
485
|
+
// to JS format (transitiveCallers as Record<number, Array>)
|
|
486
|
+
return {
|
|
487
|
+
name: raw.name,
|
|
488
|
+
results: raw.results.map((entry: any): FnDepsEntry => {
|
|
489
|
+
const transitiveCallers: Record<number, FnDepsNode[]> = {};
|
|
490
|
+
for (const group of entry.transitiveCallers ?? []) {
|
|
491
|
+
transitiveCallers[group.depth] = (group.callers ?? []).map(
|
|
492
|
+
(c: any): FnDepsNode => ({
|
|
493
|
+
name: c.name,
|
|
494
|
+
kind: c.kind,
|
|
495
|
+
file: c.file,
|
|
496
|
+
line: c.line ?? null,
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
name: entry.name,
|
|
502
|
+
kind: entry.kind,
|
|
503
|
+
file: entry.file,
|
|
504
|
+
line: entry.line ?? null,
|
|
505
|
+
endLine: entry.endLine ?? entry.end_line ?? null,
|
|
506
|
+
role: entry.role ?? null,
|
|
507
|
+
fileHash: entry.fileHash ?? entry.file_hash ?? null,
|
|
508
|
+
callees: (entry.callees ?? []).map(
|
|
509
|
+
(c: any): FnDepsNode => ({
|
|
510
|
+
name: c.name,
|
|
511
|
+
kind: c.kind,
|
|
512
|
+
file: c.file,
|
|
513
|
+
line: c.line ?? null,
|
|
514
|
+
}),
|
|
515
|
+
),
|
|
516
|
+
callers: (entry.callers ?? []).map(
|
|
517
|
+
(c: any): FnDepsCallerNode => ({
|
|
518
|
+
name: c.name,
|
|
519
|
+
kind: c.kind,
|
|
520
|
+
file: c.file,
|
|
521
|
+
line: c.line ?? null,
|
|
522
|
+
viaHierarchy: c.viaHierarchy ?? c.via_hierarchy ?? undefined,
|
|
523
|
+
}),
|
|
524
|
+
),
|
|
525
|
+
transitiveCallers,
|
|
526
|
+
};
|
|
527
|
+
}),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
464
530
|
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
readSourceRange,
|
|
28
28
|
} from '../../shared/file-utils.js';
|
|
29
29
|
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
|
|
30
|
-
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
30
|
+
import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
|
|
31
31
|
import { paginateResult } from '../../shared/paginate.js';
|
|
32
32
|
import type {
|
|
33
33
|
BetterSqlite3Database,
|
|
@@ -177,7 +177,7 @@ function buildImplementationInfo(db: BetterSqlite3Database, node: NodeRow, noTes
|
|
|
177
177
|
let impls = findImplementors(db, node.id) as RelatedNodeRow[];
|
|
178
178
|
if (noTests) impls = impls.filter((n) => !isTestFile(n.file));
|
|
179
179
|
return {
|
|
180
|
-
implementors: impls.map(
|
|
180
|
+
implementors: impls.map(toSymbolRef),
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
// For classes/structs: show what they implement
|
|
@@ -186,7 +186,7 @@ function buildImplementationInfo(db: BetterSqlite3Database, node: NodeRow, noTes
|
|
|
186
186
|
if (noTests) ifaces = ifaces.filter((n) => !isTestFile(n.file));
|
|
187
187
|
if (ifaces.length > 0) {
|
|
188
188
|
return {
|
|
189
|
-
implements: ifaces.map(
|
|
189
|
+
implements: ifaces.map(toSymbolRef),
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
192
|
}
|
|
@@ -359,21 +359,11 @@ function explainFunctionImpl(
|
|
|
359
359
|
const summary = fileLines ? extractSummary(fileLines, node.line, displayOpts) : null;
|
|
360
360
|
const signature = fileLines ? extractSignature(fileLines, node.line, displayOpts) : null;
|
|
361
361
|
|
|
362
|
-
const callees = (findCallees(db, node.id) as RelatedNodeRow[]).map(
|
|
363
|
-
name: c.name,
|
|
364
|
-
kind: c.kind,
|
|
365
|
-
file: c.file,
|
|
366
|
-
line: c.line,
|
|
367
|
-
}));
|
|
362
|
+
const callees = (findCallees(db, node.id) as RelatedNodeRow[]).map(toSymbolRef);
|
|
368
363
|
|
|
369
364
|
const allCallerRows = findCallers(db, node.id) as RelatedNodeRow[];
|
|
370
365
|
|
|
371
|
-
let callers = allCallerRows.map(
|
|
372
|
-
name: c.name,
|
|
373
|
-
kind: c.kind,
|
|
374
|
-
file: c.file,
|
|
375
|
-
line: c.line,
|
|
376
|
-
}));
|
|
366
|
+
let callers = allCallerRows.map(toSymbolRef);
|
|
377
367
|
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
378
368
|
|
|
379
369
|
const seenFiles = new Set<string>();
|
|
@@ -2,7 +2,7 @@ import { findFileNodes, type Repository } from '../../db/index.js';
|
|
|
2
2
|
import { cachedStmt } from '../../db/repository/cached-stmt.js';
|
|
3
3
|
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
4
4
|
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
|
|
5
|
-
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
5
|
+
import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
|
|
6
6
|
import { paginateResult } from '../../shared/paginate.js';
|
|
7
7
|
import type {
|
|
8
8
|
BetterSqlite3Database,
|
|
@@ -143,12 +143,8 @@ function buildNodeDepsResult(
|
|
|
143
143
|
|
|
144
144
|
return {
|
|
145
145
|
...normalizeSymbol(node, repo, hc),
|
|
146
|
-
callees: filteredCallees.map(
|
|
147
|
-
|
|
148
|
-
kind: c.kind,
|
|
149
|
-
file: c.file,
|
|
150
|
-
line: c.line,
|
|
151
|
-
})),
|
|
146
|
+
callees: filteredCallees.map(toSymbolRef),
|
|
147
|
+
// Not using toSymbolRef — callers include the extra viaHierarchy field
|
|
152
148
|
callers: callers.map((c) => ({
|
|
153
149
|
name: c.name,
|
|
154
150
|
kind: c.kind,
|
|
@@ -173,6 +169,19 @@ export function fnDepsData(
|
|
|
173
169
|
} = {},
|
|
174
170
|
) {
|
|
175
171
|
return withRepo(customDbPath, (repo) => {
|
|
172
|
+
// Try native composite path — single NAPI call for the entire query.
|
|
173
|
+
const nativeResult = repo.fnDeps(name, {
|
|
174
|
+
depth: opts.depth,
|
|
175
|
+
noTests: opts.noTests,
|
|
176
|
+
file: opts.file,
|
|
177
|
+
kind: opts.kind,
|
|
178
|
+
});
|
|
179
|
+
if (nativeResult) {
|
|
180
|
+
const base = { name: nativeResult.name, results: nativeResult.results };
|
|
181
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fallback: JS-orchestrated path (used when native engine is unavailable)
|
|
176
185
|
const depth = opts.depth || 3;
|
|
177
186
|
const noTests = opts.noTests || false;
|
|
178
187
|
const hc = new Map();
|
|
@@ -232,20 +241,14 @@ function resolveEndpoints(
|
|
|
232
241
|
to,
|
|
233
242
|
found: false,
|
|
234
243
|
error: `No symbol matching "${to}"`,
|
|
235
|
-
fromCandidates: fromNodes
|
|
236
|
-
.slice(0, 5)
|
|
237
|
-
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
|
|
244
|
+
fromCandidates: fromNodes.slice(0, 5).map(toSymbolRef),
|
|
238
245
|
toCandidates: [],
|
|
239
246
|
},
|
|
240
247
|
};
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
const fromCandidates = fromNodes
|
|
244
|
-
|
|
245
|
-
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
246
|
-
const toCandidates = toNodes
|
|
247
|
-
.slice(0, 5)
|
|
248
|
-
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
250
|
+
const fromCandidates = fromNodes.slice(0, 5).map(toSymbolRef);
|
|
251
|
+
const toCandidates = toNodes.slice(0, 5).map(toSymbolRef);
|
|
249
252
|
|
|
250
253
|
return {
|
|
251
254
|
sourceNode: fromNodes[0],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Repository, SqliteRepository } from '../../db/index.js';
|
|
2
2
|
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
3
|
-
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
3
|
+
import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
|
|
4
4
|
import { paginateResult } from '../../shared/paginate.js';
|
|
5
5
|
import type { BetterSqlite3Database, NodeRow, RelatedNodeRow } from '../../types.js';
|
|
6
6
|
import { resolveAnalysisOpts, withRepo } from './query-helpers.js';
|
|
@@ -125,7 +125,7 @@ export function bfsTransitiveCallers(
|
|
|
125
125
|
visited.add(c.id);
|
|
126
126
|
nextFrontier.push(c.id);
|
|
127
127
|
if (!levels[d]) levels[d] = [];
|
|
128
|
-
levels[d]!.push(
|
|
128
|
+
levels[d]!.push(toSymbolRef(c));
|
|
129
129
|
if (onVisit) onVisit(c, fid, d);
|
|
130
130
|
}
|
|
131
131
|
if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
2
2
|
import { CORE_SYMBOL_KINDS } from '../../shared/kinds.js';
|
|
3
|
-
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
3
|
+
import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
|
|
4
4
|
import { paginateResult } from '../../shared/paginate.js';
|
|
5
5
|
import type { RelatedNodeRow } from '../../types.js';
|
|
6
6
|
import { withRepo } from './query-helpers.js';
|
|
@@ -34,12 +34,7 @@ export function implementationsData(
|
|
|
34
34
|
|
|
35
35
|
return {
|
|
36
36
|
...normalizeSymbol(node, repo, hc),
|
|
37
|
-
implementors: implementors.map(
|
|
38
|
-
name: impl.name,
|
|
39
|
-
kind: impl.kind,
|
|
40
|
-
file: impl.file,
|
|
41
|
-
line: impl.line,
|
|
42
|
-
})),
|
|
37
|
+
implementors: implementors.map(toSymbolRef),
|
|
43
38
|
};
|
|
44
39
|
});
|
|
45
40
|
|
|
@@ -76,12 +71,7 @@ export function interfacesData(
|
|
|
76
71
|
|
|
77
72
|
return {
|
|
78
73
|
...normalizeSymbol(node, repo, hc),
|
|
79
|
-
interfaces: interfaces.map(
|
|
80
|
-
name: iface.name,
|
|
81
|
-
kind: iface.kind,
|
|
82
|
-
file: iface.file,
|
|
83
|
-
line: iface.line,
|
|
84
|
-
})),
|
|
74
|
+
interfaces: interfaces.map(toSymbolRef),
|
|
85
75
|
};
|
|
86
76
|
});
|
|
87
77
|
|
|
@@ -33,6 +33,10 @@ export class PipelineContext {
|
|
|
33
33
|
forceFullRebuild: boolean = false;
|
|
34
34
|
schemaVersion!: number;
|
|
35
35
|
nativeDb?: NativeDatabase;
|
|
36
|
+
/** Whether native engine is available (deferred — DB opened only when needed). */
|
|
37
|
+
nativeAvailable: boolean = false;
|
|
38
|
+
/** True when ctx.db is a NativeDbProxy — single rusqlite connection for the entire pipeline. */
|
|
39
|
+
nativeFirstProxy: boolean = false;
|
|
36
40
|
|
|
37
41
|
// ── File collection (set by collectFiles stage) ────────────────────
|
|
38
42
|
allFiles!: string[];
|
|
@@ -366,6 +366,27 @@ function buildImportEdges(
|
|
|
366
366
|
stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
|
|
367
367
|
edgesAdded++;
|
|
368
368
|
|
|
369
|
+
// Type-only imports: create symbol-level edges so the target symbols
|
|
370
|
+
// get fan-in credit and aren't falsely classified as dead code.
|
|
371
|
+
if (imp.typeOnly) {
|
|
372
|
+
for (const name of imp.names) {
|
|
373
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
374
|
+
let targetFile = resolvedPath;
|
|
375
|
+
if (db && isBarrelFile(db, resolvedPath)) {
|
|
376
|
+
const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
|
|
377
|
+
if (actual) targetFile = actual;
|
|
378
|
+
}
|
|
379
|
+
const candidates = stmts.findNodeInFile.all(cleanName, targetFile) as Array<{
|
|
380
|
+
id: number;
|
|
381
|
+
file: string;
|
|
382
|
+
}>;
|
|
383
|
+
if (candidates.length > 0) {
|
|
384
|
+
stmts.insertEdge.run(fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0);
|
|
385
|
+
edgesAdded++;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
369
390
|
// Barrel resolution: create edges through re-export chains
|
|
370
391
|
if (!imp.reexport && db) {
|
|
371
392
|
edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeId, resolvedPath, imp);
|
|
@@ -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
|
+
}
|