@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
|
@@ -119,6 +119,23 @@ function buildImportEdges(
|
|
|
119
119
|
: 'imports';
|
|
120
120
|
allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
|
|
121
121
|
|
|
122
|
+
// Type-only imports: create symbol-level edges so the target symbols
|
|
123
|
+
// get fan-in credit and aren't falsely classified as dead code.
|
|
124
|
+
if (imp.typeOnly && ctx.nodesByNameAndFile) {
|
|
125
|
+
for (const name of imp.names) {
|
|
126
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
127
|
+
let targetFile = resolvedPath;
|
|
128
|
+
if (isBarrelFile(ctx, resolvedPath)) {
|
|
129
|
+
const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
|
|
130
|
+
if (actual) targetFile = actual;
|
|
131
|
+
}
|
|
132
|
+
const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
|
|
133
|
+
if (candidates && candidates.length > 0) {
|
|
134
|
+
allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
122
139
|
if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
|
|
123
140
|
buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
|
|
124
141
|
}
|
|
@@ -280,7 +297,18 @@ function buildImportEdgesNative(
|
|
|
280
297
|
}
|
|
281
298
|
}
|
|
282
299
|
|
|
283
|
-
// 6.
|
|
300
|
+
// 6. Build symbol node entries for type-only import resolution
|
|
301
|
+
const symbolNodes: Array<{ name: string; file: string; nodeId: number }> = [];
|
|
302
|
+
if (ctx.nodesByNameAndFile) {
|
|
303
|
+
for (const [key, nodes] of ctx.nodesByNameAndFile) {
|
|
304
|
+
if (nodes.length > 0) {
|
|
305
|
+
const [name, file] = key.split('|');
|
|
306
|
+
symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 7. Call native
|
|
284
312
|
const nativeEdges = native.buildImportEdges!(
|
|
285
313
|
files,
|
|
286
314
|
resolvedImports,
|
|
@@ -288,6 +316,7 @@ function buildImportEdgesNative(
|
|
|
288
316
|
fileNodeIds,
|
|
289
317
|
barrelFiles,
|
|
290
318
|
rootDir,
|
|
319
|
+
symbolNodes,
|
|
291
320
|
) as NativeEdge[];
|
|
292
321
|
|
|
293
322
|
for (const e of nativeEdges) {
|
|
@@ -313,7 +342,7 @@ function buildCallEdgesNative(
|
|
|
313
342
|
if (!fileNodeRow) continue;
|
|
314
343
|
|
|
315
344
|
const importedNames = buildImportedNamesForNative(ctx, relPath, symbols, rootDir);
|
|
316
|
-
const
|
|
345
|
+
const typeMapRaw: Array<{ name: string; typeName: string; confidence: number }> =
|
|
317
346
|
symbols.typeMap instanceof Map
|
|
318
347
|
? [...symbols.typeMap.entries()].map(([name, entry]) => ({
|
|
319
348
|
name,
|
|
@@ -323,6 +352,19 @@ function buildCallEdgesNative(
|
|
|
323
352
|
: Array.isArray(symbols.typeMap)
|
|
324
353
|
? (symbols.typeMap as Array<{ name: string; typeName: string; confidence: number }>)
|
|
325
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()];
|
|
326
368
|
nativeFiles.push({
|
|
327
369
|
file: relPath,
|
|
328
370
|
fileNodeId: fileNodeRow.id,
|
|
@@ -670,7 +712,7 @@ function loadNodes(ctx: PipelineContext): { rows: QueryNodeRow[]; scoped: boolea
|
|
|
670
712
|
const nodeKindFilter = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`;
|
|
671
713
|
|
|
672
714
|
// Gate: only scope for small incremental on large codebases
|
|
673
|
-
if (!isFullBuild && fileSymbols.size <=
|
|
715
|
+
if (!isFullBuild && fileSymbols.size <= ctx.config.build.smallFilesThreshold) {
|
|
674
716
|
const existingFileCount = (
|
|
675
717
|
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get() as { c: number }
|
|
676
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
|
|
@@ -180,6 +179,13 @@ export function isBarrelFile(ctx: PipelineContext, relPath: string): boolean {
|
|
|
180
179
|
return reexports.length >= ownDefs;
|
|
181
180
|
}
|
|
182
181
|
|
|
182
|
+
/** Check if a re-export source directly defines the symbol. */
|
|
183
|
+
function sourceDefinesSymbol(ctx: PipelineContext, source: string, symbolName: string): boolean {
|
|
184
|
+
const targetSymbols = ctx.fileSymbols.get(source);
|
|
185
|
+
if (!targetSymbols) return false;
|
|
186
|
+
return targetSymbols.definitions.some((d) => d.name === symbolName);
|
|
187
|
+
}
|
|
188
|
+
|
|
183
189
|
export function resolveBarrelExport(
|
|
184
190
|
ctx: PipelineContext,
|
|
185
191
|
barrelPath: string,
|
|
@@ -188,31 +194,24 @@ export function resolveBarrelExport(
|
|
|
188
194
|
): string | null {
|
|
189
195
|
if (visited.has(barrelPath)) return null;
|
|
190
196
|
visited.add(barrelPath);
|
|
197
|
+
|
|
191
198
|
const reexports = ctx.reexportMap.get(barrelPath) as ReexportEntry[] | undefined;
|
|
192
199
|
if (!reexports) return null;
|
|
200
|
+
|
|
193
201
|
for (const re of reexports) {
|
|
202
|
+
// Named re-export: only follow if the symbol is in the export list
|
|
194
203
|
if (re.names.length > 0 && !re.wildcardReexport) {
|
|
195
|
-
if (re.names.includes(symbolName))
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (hasDef) return re.source;
|
|
200
|
-
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
201
|
-
if (deeper) return deeper;
|
|
202
|
-
}
|
|
203
|
-
return re.source;
|
|
204
|
-
}
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (re.wildcardReexport || re.names.length === 0) {
|
|
208
|
-
const targetSymbols = ctx.fileSymbols.get(re.source);
|
|
209
|
-
if (targetSymbols) {
|
|
210
|
-
const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
|
|
211
|
-
if (hasDef) return re.source;
|
|
212
|
-
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
213
|
-
if (deeper) return deeper;
|
|
214
|
-
}
|
|
204
|
+
if (!re.names.includes(symbolName)) continue;
|
|
205
|
+
if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
|
|
206
|
+
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
207
|
+
return deeper ?? re.source;
|
|
215
208
|
}
|
|
209
|
+
|
|
210
|
+
// Wildcard or namespace re-export: check if target defines the symbol
|
|
211
|
+
if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
|
|
212
|
+
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
213
|
+
if (deeper) return deeper;
|
|
216
214
|
}
|
|
215
|
+
|
|
217
216
|
return null;
|
|
218
217
|
}
|
|
@@ -2,20 +2,16 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
|
|
4
4
|
import { debug, info } from '../../infrastructure/logger.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isSupportedFile, normalizePath, shouldIgnore } from '../../shared/constants.js';
|
|
6
6
|
import { DbError } from '../../shared/errors.js';
|
|
7
7
|
import { createParseTreeCache, getActiveEngine } from '../parser.js';
|
|
8
8
|
import { type IncrementalStmts, rebuildFile } from './builder/incremental.js';
|
|
9
9
|
import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
|
|
10
10
|
import { appendJournalEntries } from './journal.js';
|
|
11
11
|
|
|
12
|
-
function
|
|
12
|
+
function shouldIgnorePath(filePath: string): boolean {
|
|
13
13
|
const parts = filePath.split(path.sep);
|
|
14
|
-
return parts.some((p) =>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isTrackedExt(filePath: string): boolean {
|
|
18
|
-
return EXTENSIONS.has(path.extname(filePath));
|
|
14
|
+
return parts.some((p) => shouldIgnore(p));
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
/** Prepare all SQL statements needed by the watcher's incremental rebuild. */
|
|
@@ -141,24 +137,35 @@ function collectTrackedFiles(dir: string, result: string[]): void {
|
|
|
141
137
|
let entries: fs.Dirent[];
|
|
142
138
|
try {
|
|
143
139
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
144
|
-
} catch {
|
|
140
|
+
} catch (e: unknown) {
|
|
141
|
+
debug(`collectTrackedFiles: cannot read ${dir}: ${(e as Error).message}`);
|
|
145
142
|
return;
|
|
146
143
|
}
|
|
147
144
|
for (const entry of entries) {
|
|
148
|
-
if (
|
|
145
|
+
if (shouldIgnore(entry.name)) continue;
|
|
149
146
|
const full = path.join(dir, entry.name);
|
|
150
147
|
if (entry.isDirectory()) {
|
|
151
148
|
collectTrackedFiles(full, result);
|
|
152
|
-
} else if (
|
|
149
|
+
} else if (isSupportedFile(entry.name)) {
|
|
153
150
|
result.push(full);
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
/** Shared watcher state passed between setup and watcher sub-functions. */
|
|
156
|
+
interface WatcherContext {
|
|
157
|
+
rootDir: string;
|
|
158
|
+
db: ReturnType<typeof openDb>;
|
|
159
|
+
stmts: IncrementalStmts;
|
|
160
|
+
engineOpts: import('../../types.js').EngineOpts;
|
|
161
|
+
cache: ReturnType<typeof createParseTreeCache>;
|
|
162
|
+
pending: Set<string>;
|
|
163
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
164
|
+
debounceMs: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Initialize DB, engine, cache, and statements for watch mode. */
|
|
168
|
+
function setupWatcher(rootDir: string, opts: { engine?: string }): WatcherContext {
|
|
162
169
|
const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
|
|
163
170
|
if (!fs.existsSync(dbPath)) {
|
|
164
171
|
throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
|
|
@@ -183,111 +190,124 @@ export async function watchProject(
|
|
|
183
190
|
|
|
184
191
|
const stmts = prepareWatcherStatements(db);
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
return {
|
|
194
|
+
rootDir,
|
|
195
|
+
db,
|
|
196
|
+
stmts,
|
|
197
|
+
engineOpts,
|
|
198
|
+
cache,
|
|
199
|
+
pending: new Set<string>(),
|
|
200
|
+
timer: null,
|
|
201
|
+
debounceMs: 300,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
/** Schedule debounced processing of pending files. */
|
|
206
|
+
function scheduleDebouncedProcess(ctx: WatcherContext): void {
|
|
207
|
+
if (ctx.timer) clearTimeout(ctx.timer);
|
|
208
|
+
ctx.timer = setTimeout(async () => {
|
|
209
|
+
const files = [...ctx.pending];
|
|
210
|
+
ctx.pending.clear();
|
|
211
|
+
await processPendingFiles(files, ctx.db, ctx.rootDir, ctx.stmts, ctx.engineOpts, ctx.cache);
|
|
212
|
+
}, ctx.debounceMs);
|
|
213
|
+
}
|
|
195
214
|
|
|
196
|
-
|
|
215
|
+
/** Start polling-based file watcher. Returns cleanup function. */
|
|
216
|
+
function startPollingWatcher(ctx: WatcherContext, pollIntervalMs: number): () => void {
|
|
217
|
+
const mtimeMap = new Map<string, number>();
|
|
218
|
+
|
|
219
|
+
const initial: string[] = [];
|
|
220
|
+
collectTrackedFiles(ctx.rootDir, initial);
|
|
221
|
+
for (const f of initial) {
|
|
222
|
+
try {
|
|
223
|
+
mtimeMap.set(f, fs.statSync(f).mtimeMs);
|
|
224
|
+
} catch {
|
|
225
|
+
/* deleted between collect and stat */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
info(`Polling ${initial.length} tracked files every ${pollIntervalMs}ms`);
|
|
197
229
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
230
|
+
const pollTimer = setInterval(() => {
|
|
231
|
+
const current: string[] = [];
|
|
232
|
+
collectTrackedFiles(ctx.rootDir, current);
|
|
233
|
+
const currentSet = new Set(current);
|
|
202
234
|
|
|
203
|
-
|
|
204
|
-
const initial: string[] = [];
|
|
205
|
-
collectTrackedFiles(rootDir, initial);
|
|
206
|
-
for (const f of initial) {
|
|
235
|
+
for (const f of current) {
|
|
207
236
|
try {
|
|
208
|
-
|
|
237
|
+
const mtime = fs.statSync(f).mtimeMs;
|
|
238
|
+
const prev = mtimeMap.get(f);
|
|
239
|
+
if (prev === undefined || mtime !== prev) {
|
|
240
|
+
mtimeMap.set(f, mtime);
|
|
241
|
+
ctx.pending.add(f);
|
|
242
|
+
}
|
|
209
243
|
} catch {
|
|
210
244
|
/* deleted between collect and stat */
|
|
211
245
|
}
|
|
212
246
|
}
|
|
213
|
-
info(`Polling ${initial.length} tracked files every ${POLL_INTERVAL_MS}ms`);
|
|
214
|
-
|
|
215
|
-
const pollTimer = setInterval(() => {
|
|
216
|
-
const current: string[] = [];
|
|
217
|
-
collectTrackedFiles(rootDir, current);
|
|
218
|
-
const currentSet = new Set(current);
|
|
219
|
-
|
|
220
|
-
// Detect modified or new files
|
|
221
|
-
for (const f of current) {
|
|
222
|
-
try {
|
|
223
|
-
const mtime = fs.statSync(f).mtimeMs;
|
|
224
|
-
const prev = mtimeMap.get(f);
|
|
225
|
-
if (prev === undefined || mtime !== prev) {
|
|
226
|
-
mtimeMap.set(f, mtime);
|
|
227
|
-
pending.add(f);
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
/* deleted between collect and stat */
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
247
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
pending.add(f);
|
|
239
|
-
}
|
|
248
|
+
for (const f of mtimeMap.keys()) {
|
|
249
|
+
if (!currentSet.has(f)) {
|
|
250
|
+
mtimeMap.delete(f);
|
|
251
|
+
ctx.pending.add(f);
|
|
240
252
|
}
|
|
253
|
+
}
|
|
241
254
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
257
|
-
if (!filename) return;
|
|
258
|
-
if (shouldIgnore(filename)) return;
|
|
259
|
-
if (!isTrackedExt(filename)) return;
|
|
260
|
-
|
|
261
|
-
const fullPath = path.join(rootDir, filename);
|
|
262
|
-
pending.add(fullPath);
|
|
263
|
-
|
|
264
|
-
if (timer) clearTimeout(timer);
|
|
265
|
-
timer = setTimeout(async () => {
|
|
266
|
-
const files = [...pending];
|
|
267
|
-
pending.clear();
|
|
268
|
-
await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
|
|
269
|
-
}, DEBOUNCE_MS);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
cleanup = () => watcher.close();
|
|
273
|
-
}
|
|
255
|
+
if (ctx.pending.size > 0) {
|
|
256
|
+
scheduleDebouncedProcess(ctx);
|
|
257
|
+
}
|
|
258
|
+
}, pollIntervalMs);
|
|
259
|
+
|
|
260
|
+
return () => clearInterval(pollTimer);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Start native OS file watcher. Returns cleanup function. */
|
|
264
|
+
function startNativeWatcher(ctx: WatcherContext): () => void {
|
|
265
|
+
const watcher = fs.watch(ctx.rootDir, { recursive: true }, (_eventType, filename) => {
|
|
266
|
+
if (!filename) return;
|
|
267
|
+
if (shouldIgnorePath(filename)) return;
|
|
268
|
+
if (!isSupportedFile(filename)) return;
|
|
274
269
|
|
|
275
|
-
|
|
270
|
+
ctx.pending.add(path.join(ctx.rootDir, filename));
|
|
271
|
+
scheduleDebouncedProcess(ctx);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return () => watcher.close();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Register SIGINT handler to flush journal and clean up. */
|
|
278
|
+
function setupShutdownHandler(ctx: WatcherContext, cleanup: () => void): void {
|
|
279
|
+
process.once('SIGINT', () => {
|
|
276
280
|
info('Stopping watcher...');
|
|
277
281
|
cleanup();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
file: normalizePath(path.relative(rootDir, filePath)),
|
|
282
|
+
if (ctx.pending.size > 0) {
|
|
283
|
+
const entries = [...ctx.pending].map((filePath) => ({
|
|
284
|
+
file: normalizePath(path.relative(ctx.rootDir, filePath)),
|
|
282
285
|
}));
|
|
283
286
|
try {
|
|
284
|
-
appendJournalEntries(rootDir, entries);
|
|
287
|
+
appendJournalEntries(ctx.rootDir, entries);
|
|
285
288
|
} catch (e: unknown) {
|
|
286
289
|
debug(`Journal flush on exit failed (non-fatal): ${(e as Error).message}`);
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
|
-
if (cache) cache.clear();
|
|
290
|
-
closeDb(db);
|
|
292
|
+
if (ctx.cache) ctx.cache.clear();
|
|
293
|
+
closeDb(ctx.db);
|
|
291
294
|
process.exit(0);
|
|
292
295
|
});
|
|
293
296
|
}
|
|
297
|
+
|
|
298
|
+
export async function watchProject(
|
|
299
|
+
rootDir: string,
|
|
300
|
+
opts: { engine?: string; poll?: boolean; pollInterval?: number } = {},
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
const ctx = setupWatcher(rootDir, opts);
|
|
303
|
+
|
|
304
|
+
const usePoll = opts.poll ?? process.platform === 'win32';
|
|
305
|
+
const pollIntervalMs = opts.pollInterval ?? 2000;
|
|
306
|
+
|
|
307
|
+
info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
|
|
308
|
+
info('Press Ctrl+C to stop.');
|
|
309
|
+
|
|
310
|
+
const cleanup = usePoll ? startPollingWatcher(ctx, pollIntervalMs) : startNativeWatcher(ctx);
|
|
311
|
+
|
|
312
|
+
setupShutdownHandler(ctx, cleanup);
|
|
313
|
+
}
|
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,
|
|
@@ -143,6 +143,8 @@ const COMMON_QUERY_PATTERNS: string[] = [
|
|
|
143
143
|
'(call_expression function: (identifier) @callfn_name) @callfn_node',
|
|
144
144
|
'(call_expression function: (member_expression) @callmem_fn) @callmem_node',
|
|
145
145
|
'(call_expression function: (subscript_expression) @callsub_fn) @callsub_node',
|
|
146
|
+
'(new_expression constructor: (identifier) @newfn_name) @newfn_node',
|
|
147
|
+
'(new_expression constructor: (member_expression) @newmem_fn) @newmem_node',
|
|
146
148
|
'(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
|
|
147
149
|
];
|
|
148
150
|
|
|
@@ -186,7 +188,11 @@ async function doLoadLanguage(entry: LanguageRegistryEntry): Promise<void> {
|
|
|
186
188
|
_queryCache.set(entry.id, new Query(lang, patterns.join('\n')));
|
|
187
189
|
}
|
|
188
190
|
} catch (e: unknown) {
|
|
189
|
-
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
|
+
});
|
|
190
196
|
warn(
|
|
191
197
|
`${entry.id} parser failed to initialize: ${(e as Error).message}. ${entry.id} files will be skipped.`,
|
|
192
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';
|