@optave/codegraph 3.9.6 → 3.11.0
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 +26 -12
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +1 -1
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/rules/index.d.ts.map +1 -1
- package/dist/ast-analysis/rules/index.js +77 -0
- package/dist/ast-analysis/rules/index.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 +50 -8
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/cli/commands/audit.js +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +2 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/check.js +1 -1
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/children.js +1 -1
- package/dist/cli/commands/children.js.map +1 -1
- package/dist/cli/commands/diff-impact.js +1 -1
- package/dist/cli/commands/diff-impact.js.map +1 -1
- package/dist/cli/commands/roles.js +1 -1
- package/dist/cli/commands/roles.js.map +1 -1
- package/dist/cli/commands/structure.js +1 -1
- package/dist/cli/commands/structure.js.map +1 -1
- package/dist/cli/shared/options.js +1 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +8 -0
- package/dist/db/connection.js.map +1 -1
- package/dist/domain/graph/builder/context.d.ts +10 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +10 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +7 -2
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +7 -2
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +0 -6
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +6 -23
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +44 -0
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +348 -42
- 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 +8 -2
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.js +8 -0
- package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts +24 -0
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +117 -3
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +9 -6
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +30 -0
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +36 -13
- 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 +73 -22
- 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 +23 -18
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +14 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +104 -11
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/models.d.ts +16 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +36 -2
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +20 -13
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/c.js +25 -6
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/cpp.js +47 -6
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/cuda.js +90 -14
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +83 -3
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/erlang.js +56 -20
- package/dist/extractors/erlang.js.map +1 -1
- package/dist/extractors/fsharp.d.ts +7 -0
- package/dist/extractors/fsharp.d.ts.map +1 -1
- package/dist/extractors/fsharp.js +94 -0
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/gleam.js +6 -2
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/groovy.js +41 -1
- package/dist/extractors/groovy.js.map +1 -1
- package/dist/extractors/haskell.js +48 -4
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/julia.js +172 -41
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/kotlin.js +4 -0
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/objc.js +184 -47
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/python.js +7 -4
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/r.js +93 -52
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/scala.d.ts.map +1 -1
- package/dist/extractors/scala.js +18 -32
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.js +18 -9
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/verilog.js +80 -15
- package/dist/extractors/verilog.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/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +14 -8
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts +1 -1
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +23 -5
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/mcp/tools/semantic-search.d.ts +1 -0
- package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
- package/dist/mcp/tools/semantic-search.js +1 -0
- package/dist/mcp/tools/semantic-search.js.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-fsharp.wasm +0 -0
- package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +11 -10
- package/src/ast-analysis/engine.ts +3 -1
- package/src/ast-analysis/rules/index.ts +87 -0
- package/src/ast-analysis/visitors/ast-store-visitor.ts +45 -9
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.ts +2 -0
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/children.ts +1 -1
- package/src/cli/commands/diff-impact.ts +1 -1
- package/src/cli/commands/roles.ts +1 -1
- package/src/cli/commands/structure.ts +1 -1
- package/src/cli/shared/options.ts +1 -1
- package/src/db/connection.ts +8 -0
- package/src/domain/graph/builder/context.ts +10 -0
- package/src/domain/graph/builder/helpers.ts +8 -3
- package/src/domain/graph/builder/incremental.ts +6 -41
- package/src/domain/graph/builder/pipeline.ts +404 -41
- package/src/domain/graph/builder/stages/build-edges.ts +9 -2
- package/src/domain/graph/builder/stages/collect-files.ts +9 -0
- package/src/domain/graph/builder/stages/detect-changes.ts +130 -4
- package/src/domain/graph/builder/stages/finalize.ts +9 -6
- package/src/domain/graph/builder/stages/insert-nodes.ts +38 -14
- package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
- package/src/domain/graph/watcher.ts +21 -23
- package/src/domain/parser.ts +110 -10
- package/src/domain/search/models.ts +37 -2
- package/src/domain/wasm-worker-entry.ts +20 -13
- package/src/extractors/c.ts +27 -8
- package/src/extractors/cpp.ts +50 -8
- package/src/extractors/cuda.ts +90 -16
- package/src/extractors/elixir.ts +75 -3
- package/src/extractors/erlang.ts +63 -20
- package/src/extractors/fsharp.ts +104 -0
- package/src/extractors/gleam.ts +7 -2
- package/src/extractors/groovy.ts +45 -1
- package/src/extractors/haskell.ts +45 -4
- package/src/extractors/julia.ts +164 -43
- package/src/extractors/kotlin.ts +4 -0
- package/src/extractors/objc.ts +171 -47
- package/src/extractors/python.ts +5 -3
- package/src/extractors/r.ts +88 -48
- package/src/extractors/scala.ts +24 -36
- package/src/extractors/solidity.ts +17 -8
- package/src/extractors/verilog.ts +83 -15
- package/src/infrastructure/config.ts +1 -0
- package/src/mcp/server.ts +16 -9
- package/src/mcp/tool-registry.ts +28 -5
- package/src/mcp/tools/semantic-search.ts +2 -0
- package/src/types.ts +16 -0
|
@@ -27,7 +27,7 @@ interface FileHashRow {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
interface FileStat {
|
|
30
|
-
|
|
30
|
+
mtime: number;
|
|
31
31
|
size: number;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -182,7 +182,7 @@ function mtimeAndHashTiers(
|
|
|
182
182
|
if (!stat) continue;
|
|
183
183
|
const storedMtime = record.mtime || 0;
|
|
184
184
|
const storedSize = record.size || 0;
|
|
185
|
-
if (storedSize > 0 &&
|
|
185
|
+
if (storedSize > 0 && stat.mtime === storedMtime && stat.size === storedSize) {
|
|
186
186
|
skipped.push(relPath);
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
@@ -512,6 +512,132 @@ function handleIncrementalBuild(ctx: PipelineContext): void {
|
|
|
512
512
|
purgeAndAddReverseDeps(ctx, changePaths, reverseDeps);
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Read-only pre-flight check for the native orchestrator.
|
|
517
|
+
*
|
|
518
|
+
* Returns true iff every collected source file has matching mtime+size in
|
|
519
|
+
* `file_hashes` and no DB-tracked file has been removed. When true, the
|
|
520
|
+
* caller can short-circuit before invoking the native orchestrator —
|
|
521
|
+
* matching WASM's ~20 ms early-exit path and avoiding the ~2s flat
|
|
522
|
+
* per-call native rebuild overhead seen in CI (#1054).
|
|
523
|
+
*
|
|
524
|
+
* Intentionally Tier-0/Tier-1 only (journal + mtime/size). Tier-2 content
|
|
525
|
+
* hashing is left to the native side: when this returns false the caller
|
|
526
|
+
* falls through to the orchestrator, which performs its own complete
|
|
527
|
+
* detection and is the source of truth.
|
|
528
|
+
*
|
|
529
|
+
* Conservatively returns false when CFG or dataflow analysis is enabled
|
|
530
|
+
* but the corresponding tables are empty — otherwise the fast-skip would
|
|
531
|
+
* silently suppress the pending-analysis pass that the JS path runs via
|
|
532
|
+
* `runPendingAnalysis`, and CFG/dataflow data would never populate on
|
|
533
|
+
* repos where source files don't change between builds.
|
|
534
|
+
*
|
|
535
|
+
* Pure read of `db` and the filesystem — never mutates either.
|
|
536
|
+
*/
|
|
537
|
+
export function detectNoChanges(
|
|
538
|
+
db: BetterSqlite3Database,
|
|
539
|
+
allFiles: string[],
|
|
540
|
+
rootDir: string,
|
|
541
|
+
opts?: Record<string, unknown>,
|
|
542
|
+
): boolean {
|
|
543
|
+
// Diagnostic logging gated by env var — used by the bench gate to surface
|
|
544
|
+
// why the fast-skip is not firing on CI runners (#1066). Off by default to
|
|
545
|
+
// avoid noise on every regular incremental build.
|
|
546
|
+
const diag = process.env.CODEGRAPH_FAST_SKIP_DIAG === '1';
|
|
547
|
+
const log = (reason: string): void => {
|
|
548
|
+
if (diag) info(`[fast-skip] ${reason}`);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
let hasTable = false;
|
|
552
|
+
try {
|
|
553
|
+
db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
|
|
554
|
+
hasTable = true;
|
|
555
|
+
} catch {
|
|
556
|
+
/* table missing — first build */
|
|
557
|
+
}
|
|
558
|
+
if (!hasTable) {
|
|
559
|
+
log('false: file_hashes table missing');
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const rows = db.prepare('SELECT file, hash, mtime, size FROM file_hashes').all() as FileHashRow[];
|
|
564
|
+
if (rows.length === 0) {
|
|
565
|
+
log('false: file_hashes table empty');
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
const existing = new Map<string, FileHashRow>(rows.map((r) => [r.file, r]));
|
|
569
|
+
|
|
570
|
+
const currentFiles = new Set<string>();
|
|
571
|
+
for (const file of allFiles) {
|
|
572
|
+
currentFiles.add(normalizePath(path.relative(rootDir, file)));
|
|
573
|
+
}
|
|
574
|
+
for (const existingFile of existing.keys()) {
|
|
575
|
+
if (!currentFiles.has(existingFile)) {
|
|
576
|
+
log(`false: tracked file no longer collected: ${existingFile}`);
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
for (const file of allFiles) {
|
|
582
|
+
const relPath = normalizePath(path.relative(rootDir, file));
|
|
583
|
+
const record = existing.get(relPath);
|
|
584
|
+
if (!record) {
|
|
585
|
+
log(`false: collected file missing from file_hashes: ${relPath}`);
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
const stat = fileStat(file) as FileStat | undefined;
|
|
589
|
+
if (!stat) {
|
|
590
|
+
log(`false: stat failed for ${relPath}`);
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
const storedMtime = record.mtime || 0;
|
|
594
|
+
const storedSize = record.size || 0;
|
|
595
|
+
if (storedSize <= 0) {
|
|
596
|
+
log(`false: stored size <= 0 for ${relPath} (stored=${record.size})`);
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
if (stat.mtime !== storedMtime || stat.size !== storedSize) {
|
|
600
|
+
log(
|
|
601
|
+
`false: mtime/size diff for ${relPath}: stat=${stat.mtime}/${stat.size} stored=${storedMtime}/${storedSize}`,
|
|
602
|
+
);
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Pending-analysis guard: if CFG/dataflow is enabled but the corresponding
|
|
608
|
+
// table is empty (analysis newly enabled, or tables wiped between builds),
|
|
609
|
+
// fall through so the orchestrator / JS pipeline can run runPendingAnalysis.
|
|
610
|
+
// Mirrors the check at the top of runPendingAnalysis (see line ~244).
|
|
611
|
+
if (opts) {
|
|
612
|
+
if (opts.cfg !== false && hasEmptyAnalysisTable(db, 'cfg_blocks')) {
|
|
613
|
+
log('false: pending-analysis guard — cfg_blocks is empty');
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
if (opts.dataflow !== false && hasEmptyAnalysisTable(db, 'dataflow')) {
|
|
617
|
+
log('false: pending-analysis guard — dataflow is empty');
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
log(`true: all checks passed (${allFiles.length} files)`);
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Returns true if `table` exists and has zero rows, matching the empty-table
|
|
628
|
+
* semantics of `runPendingAnalysis`. A missing table is treated as empty
|
|
629
|
+
* (the conservative outcome), so the caller falls through to the orchestrator
|
|
630
|
+
* which will create the schema and populate it.
|
|
631
|
+
*/
|
|
632
|
+
function hasEmptyAnalysisTable(db: BetterSqlite3Database, table: string): boolean {
|
|
633
|
+
try {
|
|
634
|
+
const row = db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get() as { c: number } | undefined;
|
|
635
|
+
return (row?.c ?? 0) === 0;
|
|
636
|
+
} catch {
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
515
641
|
export async function detectChanges(ctx: PipelineContext): Promise<void> {
|
|
516
642
|
const start = performance.now();
|
|
517
643
|
try {
|
|
@@ -537,7 +663,7 @@ export async function detectChanges(ctx: PipelineContext): Promise<void> {
|
|
|
537
663
|
relPath: c.relPath,
|
|
538
664
|
content: c.content,
|
|
539
665
|
hash: c.hash,
|
|
540
|
-
stat: c.stat ? { mtime:
|
|
666
|
+
stat: c.stat ? { mtime: c.stat.mtime, size: c.stat.size } : undefined,
|
|
541
667
|
_reverseDepOnly: c._reverseDepOnly,
|
|
542
668
|
}));
|
|
543
669
|
ctx.metadataUpdates = increResult.changed
|
|
@@ -548,7 +674,7 @@ export async function detectChanges(ctx: PipelineContext): Promise<void> {
|
|
|
548
674
|
.map((c) => ({
|
|
549
675
|
relPath: c.relPath,
|
|
550
676
|
hash: c.hash,
|
|
551
|
-
stat: { mtime:
|
|
677
|
+
stat: { mtime: c.stat.mtime, size: c.stat.size },
|
|
552
678
|
}));
|
|
553
679
|
if (!ctx.isFullBuild && ctx.parseChanges.length === 0 && ctx.removed.length === 0) {
|
|
554
680
|
const ranAnalysis = await runPendingAnalysis(ctx);
|
|
@@ -82,13 +82,16 @@ function persistBuildMetadata(
|
|
|
82
82
|
): void {
|
|
83
83
|
const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
|
|
84
84
|
if (!ctx.isFullBuild && ctx.allSymbols.size <= 3) return;
|
|
85
|
-
// When the native engine is active, persist the
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
85
|
+
// When the native engine is active, persist the binary's CARGO_PKG_VERSION
|
|
86
|
+
// (ctx.nativeBinaryVersion). The Rust orchestrator's check_version_mismatch
|
|
87
|
+
// compares against that exact value, so writing the platform package.json
|
|
88
|
+
// version (ctx.engineVersion) — which can drift from the binary in CI
|
|
89
|
+
// hot-swap flows (#1066) — would force every subsequent native build to
|
|
90
|
+
// be a full rebuild.
|
|
90
91
|
const codeVersionToWrite =
|
|
91
|
-
ctx.engineName === 'native' && ctx.
|
|
92
|
+
ctx.engineName === 'native' && ctx.nativeBinaryVersion
|
|
93
|
+
? ctx.nativeBinaryVersion
|
|
94
|
+
: CODEGRAPH_VERSION;
|
|
92
95
|
// Persist the repo root so downstream commands (e.g. `codegraph embed`)
|
|
93
96
|
// can resolve relative file paths regardless of the invoking cwd.
|
|
94
97
|
// Use realpathSync (symlink-resolving) to match the Rust engine's
|
|
@@ -12,10 +12,12 @@ import path from 'node:path';
|
|
|
12
12
|
import { performance } from 'node:perf_hooks';
|
|
13
13
|
import { bulkNodeIdsByFile } from '../../../../db/index.js';
|
|
14
14
|
import { debug } from '../../../../infrastructure/logger.js';
|
|
15
|
+
import { normalizePath } from '../../../../shared/constants.js';
|
|
15
16
|
import { toErrorMessage } from '../../../../shared/errors.js';
|
|
16
17
|
import type {
|
|
17
18
|
BetterSqlite3Database,
|
|
18
19
|
ExtractorOutput,
|
|
20
|
+
FileToParse,
|
|
19
21
|
MetadataUpdate,
|
|
20
22
|
SqliteStatement,
|
|
21
23
|
} from '../../../../types.js';
|
|
@@ -90,16 +92,30 @@ function marshalSymbolBatches(allSymbols: Map<string, ExtractorOutput>): InsertN
|
|
|
90
92
|
return batches;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Build file hash entries for every collected file, including those that
|
|
97
|
+
* produced zero symbols (empty files, parsers that silently no-op'd, or
|
|
98
|
+
* optional-language extensions whose grammar wasn't installed). Iterating the
|
|
99
|
+
* symbol map instead would skip such files and leave them missing from
|
|
100
|
+
* `file_hashes`, which permanently breaks the JS-side fast-skip pre-flight on
|
|
101
|
+
* any subsequent no-op rebuild (#1068).
|
|
102
|
+
*
|
|
103
|
+
* Exported for unit testing.
|
|
104
|
+
*/
|
|
105
|
+
export function buildFileHashes(
|
|
106
|
+
filesToParse: FileToParse[],
|
|
96
107
|
precomputedData: Map<string, PrecomputedFileData>,
|
|
97
108
|
metadataUpdates: MetadataUpdate[],
|
|
98
109
|
rootDir: string,
|
|
99
110
|
): Array<{ file: string; hash: string; mtime: number; size: number }> {
|
|
100
111
|
const fileHashes: Array<{ file: string; hash: string; mtime: number; size: number }> = [];
|
|
112
|
+
const seen = new Set<string>();
|
|
113
|
+
|
|
114
|
+
for (const item of filesToParse) {
|
|
115
|
+
const relPath = item.relPath ?? normalizePath(path.relative(rootDir, item.file));
|
|
116
|
+
if (seen.has(relPath)) continue;
|
|
117
|
+
seen.add(relPath);
|
|
101
118
|
|
|
102
|
-
for (const [relPath] of allSymbols) {
|
|
103
119
|
const precomputed = precomputedData.get(relPath);
|
|
104
120
|
if (precomputed?._reverseDepOnly) {
|
|
105
121
|
continue; // file unchanged, hash already correct
|
|
@@ -112,7 +128,7 @@ function buildFileHashes(
|
|
|
112
128
|
size = precomputed.stat.size;
|
|
113
129
|
} else {
|
|
114
130
|
const rawStat = fileStat(path.join(rootDir, relPath));
|
|
115
|
-
mtime = rawStat ?
|
|
131
|
+
mtime = rawStat ? rawStat.mtime : 0;
|
|
116
132
|
size = rawStat ? rawStat.size : 0;
|
|
117
133
|
}
|
|
118
134
|
fileHashes.push({ file: relPath, hash: precomputed.hash, mtime, size });
|
|
@@ -127,7 +143,7 @@ function buildFileHashes(
|
|
|
127
143
|
}
|
|
128
144
|
if (code !== null) {
|
|
129
145
|
const stat = fileStat(absPath);
|
|
130
|
-
const mtime = stat ?
|
|
146
|
+
const mtime = stat ? stat.mtime : 0;
|
|
131
147
|
const size = stat ? stat.size : 0;
|
|
132
148
|
fileHashes.push({ file: relPath, hash: fileHash(code), mtime, size });
|
|
133
149
|
}
|
|
@@ -136,7 +152,7 @@ function buildFileHashes(
|
|
|
136
152
|
|
|
137
153
|
// Also include metadata-only updates (self-heal mtime/size without re-parse)
|
|
138
154
|
for (const item of metadataUpdates) {
|
|
139
|
-
const mtime = item.stat ?
|
|
155
|
+
const mtime = item.stat ? item.stat.mtime : 0;
|
|
140
156
|
const size = item.stat ? item.stat.size : 0;
|
|
141
157
|
fileHashes.push({ file: item.relPath, hash: item.hash, mtime, size });
|
|
142
158
|
}
|
|
@@ -157,7 +173,7 @@ function tryNativeInsert(ctx: PipelineContext): boolean {
|
|
|
157
173
|
for (const item of filesToParse) {
|
|
158
174
|
if (item.relPath) precomputedData.set(item.relPath, item as PrecomputedFileData);
|
|
159
175
|
}
|
|
160
|
-
const fileHashes = buildFileHashes(
|
|
176
|
+
const fileHashes = buildFileHashes(filesToParse, precomputedData, metadataUpdates, rootDir);
|
|
161
177
|
|
|
162
178
|
// In native-first mode (single rusqlite connection), no WAL dance is needed.
|
|
163
179
|
// In dual-connection mode, checkpoint JS side before native write, then
|
|
@@ -321,7 +337,7 @@ function insertChildrenAndEdges(
|
|
|
321
337
|
|
|
322
338
|
function updateFileHashes(
|
|
323
339
|
_db: BetterSqlite3Database,
|
|
324
|
-
|
|
340
|
+
filesToParse: FileToParse[],
|
|
325
341
|
precomputedData: Map<string, PrecomputedFileData>,
|
|
326
342
|
metadataUpdates: MetadataUpdate[],
|
|
327
343
|
rootDir: string,
|
|
@@ -329,7 +345,15 @@ function updateFileHashes(
|
|
|
329
345
|
): void {
|
|
330
346
|
if (!upsertHash) return;
|
|
331
347
|
|
|
332
|
-
|
|
348
|
+
// Iterate every collected file (#1068): files that produced zero symbols
|
|
349
|
+
// (empty, parser no-op, or grammar-missing optional language) still need a
|
|
350
|
+
// hash row, otherwise the next no-op rebuild's fast-skip pre-flight rejects.
|
|
351
|
+
const seen = new Set<string>();
|
|
352
|
+
for (const item of filesToParse) {
|
|
353
|
+
const relPath = item.relPath ?? normalizePath(path.relative(rootDir, item.file));
|
|
354
|
+
if (seen.has(relPath)) continue;
|
|
355
|
+
seen.add(relPath);
|
|
356
|
+
|
|
333
357
|
const precomputed = precomputedData.get(relPath);
|
|
334
358
|
if (precomputed?._reverseDepOnly) {
|
|
335
359
|
// no-op: file unchanged, hash already correct
|
|
@@ -341,7 +365,7 @@ function updateFileHashes(
|
|
|
341
365
|
size = precomputed.stat.size;
|
|
342
366
|
} else {
|
|
343
367
|
const rawStat = fileStat(path.join(rootDir, relPath));
|
|
344
|
-
mtime = rawStat ?
|
|
368
|
+
mtime = rawStat ? rawStat.mtime : 0;
|
|
345
369
|
size = rawStat ? rawStat.size : 0;
|
|
346
370
|
}
|
|
347
371
|
upsertHash.run(relPath, precomputed.hash, mtime, size);
|
|
@@ -356,7 +380,7 @@ function updateFileHashes(
|
|
|
356
380
|
}
|
|
357
381
|
if (code !== null) {
|
|
358
382
|
const stat = fileStat(absPath);
|
|
359
|
-
const mtime = stat ?
|
|
383
|
+
const mtime = stat ? stat.mtime : 0;
|
|
360
384
|
const size = stat ? stat.size : 0;
|
|
361
385
|
upsertHash.run(relPath, fileHash(code), mtime, size);
|
|
362
386
|
}
|
|
@@ -365,7 +389,7 @@ function updateFileHashes(
|
|
|
365
389
|
|
|
366
390
|
// Also update metadata-only entries (self-heal mtime/size without re-parse)
|
|
367
391
|
for (const item of metadataUpdates) {
|
|
368
|
-
const mtime = item.stat ?
|
|
392
|
+
const mtime = item.stat ? item.stat.mtime : 0;
|
|
369
393
|
const size = item.stat ? item.stat.size : 0;
|
|
370
394
|
upsertHash.run(item.relPath, item.hash, mtime, size);
|
|
371
395
|
}
|
|
@@ -415,7 +439,7 @@ export async function insertNodes(ctx: PipelineContext): Promise<void> {
|
|
|
415
439
|
const insertAll = ctx.db.transaction(() => {
|
|
416
440
|
insertDefinitionsAndExports(ctx.db, allSymbols);
|
|
417
441
|
insertChildrenAndEdges(ctx.db, allSymbols);
|
|
418
|
-
updateFileHashes(ctx.db,
|
|
442
|
+
updateFileHashes(ctx.db, filesToParse, precomputedData, metadataUpdates, rootDir, upsertHash);
|
|
419
443
|
});
|
|
420
444
|
|
|
421
445
|
insertAll();
|
|
@@ -33,15 +33,23 @@ function buildReexportMap(ctx: PipelineContext): void {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Find barrel files related to
|
|
37
|
-
* For small
|
|
38
|
-
* or are imported by
|
|
36
|
+
* Find barrel files related to `fromRelPaths` for scoped re-parsing.
|
|
37
|
+
* For small frontiers (<=smallFilesThreshold files), only barrels that re-export from
|
|
38
|
+
* or are imported by `fromRelPaths`. For larger frontiers, all barrels.
|
|
39
|
+
*
|
|
40
|
+
* `firstPass` gates the reexport-from DB scan: re-parsed barrels haven't
|
|
41
|
+
* changed content, so subsequent passes can't surface new reexport-from
|
|
42
|
+
* candidates and only need to follow imports of newly-merged barrels
|
|
43
|
+
* (mirrors the Rust orchestrator's seed-only `collect_reexport_from_barrels`).
|
|
39
44
|
*/
|
|
40
|
-
function findBarrelCandidates(
|
|
45
|
+
function findBarrelCandidates(
|
|
46
|
+
ctx: PipelineContext,
|
|
47
|
+
fromRelPaths: readonly string[],
|
|
48
|
+
firstPass: boolean,
|
|
49
|
+
): Array<{ file: string }> {
|
|
41
50
|
const { db, fileSymbols, rootDir, aliases } = ctx;
|
|
42
|
-
const changedRelPaths = new Set<string>(fileSymbols.keys());
|
|
43
51
|
|
|
44
|
-
if (
|
|
52
|
+
if (fromRelPaths.length <= ctx.config.build.smallFilesThreshold) {
|
|
45
53
|
const allBarrelFiles = new Set(
|
|
46
54
|
(
|
|
47
55
|
db
|
|
@@ -56,9 +64,9 @@ function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
|
|
|
56
64
|
|
|
57
65
|
const barrels = new Set<string>();
|
|
58
66
|
|
|
59
|
-
// Find barrels imported by
|
|
67
|
+
// Find barrels imported by `fromRelPaths` using parsed import data
|
|
60
68
|
// (can't query DB edges -- they were purged for the changed files).
|
|
61
|
-
for (const relPath of
|
|
69
|
+
for (const relPath of fromRelPaths) {
|
|
62
70
|
const symbols = fileSymbols.get(relPath);
|
|
63
71
|
if (!symbols) continue;
|
|
64
72
|
for (const imp of symbols.imports) {
|
|
@@ -71,16 +79,17 @@ function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
|
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
if (firstPass) {
|
|
83
|
+
const reexportSourceStmt = db.prepare(
|
|
84
|
+
`SELECT DISTINCT n1.file FROM edges e
|
|
85
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
86
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
87
|
+
WHERE e.kind = 'reexports' AND n1.kind = 'file' AND n2.file = ?`,
|
|
88
|
+
);
|
|
89
|
+
for (const relPath of fromRelPaths) {
|
|
90
|
+
for (const row of reexportSourceStmt.all(relPath) as Array<{ file: string }>) {
|
|
91
|
+
barrels.add(row.file);
|
|
92
|
+
}
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
return [...barrels].map((file) => ({ file }));
|
|
@@ -95,11 +104,22 @@ function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
|
|
|
95
104
|
.all() as Array<{ file: string }>;
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Re-parse barrel files and update fileSymbols/reexportMap with fresh data.
|
|
109
|
+
* Returns the relative paths of newly-merged files so the caller can scan
|
|
110
|
+
* them for the next level of barrel candidates.
|
|
111
|
+
*
|
|
112
|
+
* A re-parsed file is marked `barrel-only` only when it really is one (the
|
|
113
|
+
* `isBarrelFile` check — reexports >= ownDefs). The previous unconditional
|
|
114
|
+
* `.add(relPath)` caused hybrid barrels with many local defs (e.g. a file
|
|
115
|
+
* with one `export type ... from` and dozens of internal functions) to drop
|
|
116
|
+
* all their non-reexport imports in build-edges, since the barrel-only branch
|
|
117
|
+
* skips them (#1174).
|
|
118
|
+
*/
|
|
99
119
|
async function reparseBarrelFiles(
|
|
100
120
|
ctx: PipelineContext,
|
|
101
121
|
barrelCandidates: Array<{ file: string }>,
|
|
102
|
-
): Promise<
|
|
122
|
+
): Promise<string[]> {
|
|
103
123
|
const { db, fileSymbols, rootDir, engineOpts } = ctx;
|
|
104
124
|
|
|
105
125
|
const barrelPaths: string[] = [];
|
|
@@ -109,18 +129,27 @@ async function reparseBarrelFiles(
|
|
|
109
129
|
}
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
if (barrelPaths.length === 0) return;
|
|
132
|
+
if (barrelPaths.length === 0) return [];
|
|
113
133
|
|
|
134
|
+
// Preserve `contains` and `parameter_of` — those are emitted by insertNodes,
|
|
135
|
+
// which only runs on the original (changed + reverse-dep) fileSymbols. Barrel
|
|
136
|
+
// candidates are merged here *after* insertNodes, so wiping those kinds
|
|
137
|
+
// would permanently drop them (mirrors the Rust orchestrator's Stage 6b
|
|
138
|
+
// delete in build_pipeline.rs).
|
|
114
139
|
const deleteOutgoingEdges = db.prepare(
|
|
115
|
-
|
|
140
|
+
`DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)
|
|
141
|
+
AND kind NOT IN ('contains', 'parameter_of')`,
|
|
116
142
|
);
|
|
117
143
|
|
|
144
|
+
const added: string[] = [];
|
|
118
145
|
try {
|
|
119
146
|
const barrelSymbols = await parseFilesAuto(barrelPaths, rootDir, engineOpts);
|
|
120
147
|
for (const [relPath, fileSym] of barrelSymbols) {
|
|
121
148
|
deleteOutgoingEdges.run(relPath);
|
|
122
149
|
fileSymbols.set(relPath, fileSym);
|
|
123
|
-
ctx
|
|
150
|
+
if (isBarrelFile(ctx, relPath)) {
|
|
151
|
+
ctx.barrelOnlyFiles.add(relPath);
|
|
152
|
+
}
|
|
124
153
|
const reexports = fileSym.imports.filter((imp: Import) => imp.reexport);
|
|
125
154
|
if (reexports.length > 0) {
|
|
126
155
|
ctx.reexportMap.set(
|
|
@@ -132,10 +161,12 @@ async function reparseBarrelFiles(
|
|
|
132
161
|
})),
|
|
133
162
|
);
|
|
134
163
|
}
|
|
164
|
+
added.push(relPath);
|
|
135
165
|
}
|
|
136
166
|
} catch (e: unknown) {
|
|
137
167
|
debug(`Barrel re-parse failed (non-fatal): ${(e as Error).message}`);
|
|
138
168
|
}
|
|
169
|
+
return added;
|
|
139
170
|
}
|
|
140
171
|
|
|
141
172
|
export async function resolveImports(ctx: PipelineContext): Promise<void> {
|
|
@@ -156,8 +187,31 @@ export async function resolveImports(ctx: PipelineContext): Promise<void> {
|
|
|
156
187
|
|
|
157
188
|
ctx.barrelOnlyFiles = new Set<string>();
|
|
158
189
|
if (!isFullBuild) {
|
|
159
|
-
|
|
160
|
-
|
|
190
|
+
// Iteratively discover and re-parse barrel chains. A barrel that imports
|
|
191
|
+
// another barrel (e.g. `parser.ts → extractors/index.ts → extractors/<lang>.ts`)
|
|
192
|
+
// needs both loaded so build-edges can emit the barrel-through edges from
|
|
193
|
+
// the first barrel to the leaf targets. Without iteration, only the first
|
|
194
|
+
// level of barrels gets merged into fileSymbols; the deeper chain has no
|
|
195
|
+
// entry in reexportMap and the resolver silently drops the affected edges
|
|
196
|
+
// on every incremental rebuild (#1174).
|
|
197
|
+
//
|
|
198
|
+
// Convergence is guaranteed because fileSymbols grows monotonically and
|
|
199
|
+
// is bounded by the set of barrel files in the project — each iteration
|
|
200
|
+
// either adds a previously-unseen barrel or terminates.
|
|
201
|
+
//
|
|
202
|
+
// Subsequent passes only walk newly-merged barrels' imports (`frontier`
|
|
203
|
+
// = paths returned by reparseBarrelFiles), matching the Rust
|
|
204
|
+
// orchestrator's `&newly_added` slice. Without this, every pass would
|
|
205
|
+
// re-query the DB for every key in `fileSymbols`.
|
|
206
|
+
let frontier: readonly string[] = [...fileSymbols.keys()];
|
|
207
|
+
let firstPass = true;
|
|
208
|
+
while (frontier.length > 0) {
|
|
209
|
+
const barrelCandidates = findBarrelCandidates(ctx, frontier, firstPass);
|
|
210
|
+
const added = await reparseBarrelFiles(ctx, barrelCandidates);
|
|
211
|
+
if (added.length === 0) break;
|
|
212
|
+
frontier = added;
|
|
213
|
+
firstPass = false;
|
|
214
|
+
}
|
|
161
215
|
}
|
|
162
216
|
}
|
|
163
217
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import { debug, info } from '../../infrastructure/logger.js';
|
|
4
|
+
import { debug, info, warn } from '../../infrastructure/logger.js';
|
|
5
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';
|
|
@@ -16,12 +16,13 @@ function shouldIgnorePath(filePath: string): boolean {
|
|
|
16
16
|
|
|
17
17
|
/** Prepare all SQL statements needed by the watcher's incremental rebuild. */
|
|
18
18
|
function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStmts {
|
|
19
|
-
|
|
19
|
+
return {
|
|
20
20
|
insertNode: db.prepare(
|
|
21
21
|
'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
|
|
22
22
|
),
|
|
23
23
|
getNodeId: {
|
|
24
|
-
get: (
|
|
24
|
+
get: (...params: unknown[]) => {
|
|
25
|
+
const [name, kind, file, line] = params as [string, string, string, number];
|
|
25
26
|
const id = getNodeIdQuery(db, name, kind, file, line);
|
|
26
27
|
return id != null ? { id } : undefined;
|
|
27
28
|
},
|
|
@@ -29,10 +30,7 @@ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStm
|
|
|
29
30
|
insertEdge: db.prepare(
|
|
30
31
|
'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
|
|
31
32
|
),
|
|
32
|
-
deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
|
|
33
|
-
deleteEdgesForFile: null as { run: (f: string) => void } | null,
|
|
34
33
|
countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
|
|
35
|
-
countEdgesForFile: null as { get: (f: string) => { c: number } | undefined } | null,
|
|
36
34
|
findNodeInFile: db.prepare(
|
|
37
35
|
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
|
|
38
36
|
),
|
|
@@ -41,19 +39,6 @@ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStm
|
|
|
41
39
|
),
|
|
42
40
|
listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
|
|
43
41
|
};
|
|
44
|
-
|
|
45
|
-
const origDeleteEdges = db.prepare(
|
|
46
|
-
`DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
|
|
47
|
-
);
|
|
48
|
-
const origCountEdges = db.prepare(
|
|
49
|
-
`SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
|
|
50
|
-
);
|
|
51
|
-
stmts.deleteEdgesForFile = { run: (f: string) => origDeleteEdges.run({ f }) };
|
|
52
|
-
stmts.countEdgesForFile = {
|
|
53
|
-
get: (f: string) => origCountEdges.get({ f }) as { c: number } | undefined,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
return stmts as IncrementalStmts;
|
|
57
42
|
}
|
|
58
43
|
|
|
59
44
|
/** Rebuild result shape from rebuildFile. */
|
|
@@ -80,10 +65,23 @@ async function processPendingFiles(
|
|
|
80
65
|
): Promise<void> {
|
|
81
66
|
const results: RebuildResult[] = [];
|
|
82
67
|
for (const filePath of files) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
// Per-file try/catch so one bad rebuild doesn't crash the watcher loop.
|
|
69
|
+
// The watcher is a long-running session — any SQLite error, parse failure,
|
|
70
|
+
// or filesystem race must be reported and skipped, not propagated. Issue #1176.
|
|
71
|
+
try {
|
|
72
|
+
const result = (await rebuildFile(db, rootDir, filePath, stmts, engineOpts, cache, {
|
|
73
|
+
diffSymbols: diffSymbols as (old: unknown[], new_: unknown[]) => unknown,
|
|
74
|
+
})) as RebuildResult | null;
|
|
75
|
+
if (result) results.push(result);
|
|
76
|
+
} catch (err: unknown) {
|
|
77
|
+
const relPath = normalizePath(path.relative(rootDir, filePath));
|
|
78
|
+
// Narrow with `instanceof` instead of casting: a non-Error throw (a plain
|
|
79
|
+
// string, `null`, or any value a third-party dependency throws) would log
|
|
80
|
+
// `(err as Error).message` as `undefined`. See Greptile review on #1182.
|
|
81
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
82
|
+
warn(`Failed to rebuild ${relPath}: ${message} — skipping`);
|
|
83
|
+
debug(err instanceof Error ? (err.stack ?? message) : String(err));
|
|
84
|
+
}
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
if (results.length > 0) {
|