fix+perf: dogfood fixes 9.1-9.4 and sub-100ms incremental rebuilds#640
fix+perf: dogfood fixes 9.1-9.4 and sub-100ms incremental rebuilds#640carlos-alm merged 10 commits intomainfrom
Conversation
… compat 9.1 — Warn on graph load when DB was built with a different codegraph version. The check runs once per process in openReadonlyOrFail() and suggests `build --no-incremental`. 9.2 — Barrel-only files now emit reexport edges during build. Previously the entire file was skipped in buildImportEdges; now only non-reexport imports are skipped, so `codegraph exports` can follow re-export chains. 9.3 — Demote "Failed to parse tsconfig.json" from warn to debug level so it no longer clutters every build output. 9.4 — Document EXTENSIONS/IGNORE_DIRS Array→Set breaking change in CHANGELOG. Add .toArray() convenience method and export ArrayCompatSet type for consumers migrating from the pre-3.4 array API.
…ge duplication (#634) - Move _versionWarned flag outside mismatch conditional to avoid redundant build_meta queries when versions match. - Wrap SUPPORTED_EXTENSIONS in new Set() to avoid mutating the sibling module's export. - Delete outgoing edges for barrel-only files before re-adding them to fileSymbols during incremental builds, preventing duplicate reexport edges.
Delete the 39-LOC manual ambient type declarations for better-sqlite3 and use the community @types/better-sqlite3 package instead. The vendor file was a migration-era shim (allowJs is long gone from tsconfig). - Replace all BetterSqlite3.Database → BetterSqlite3Database (types.ts) - Replace all BetterSqlite3.Statement → SqliteStatement (types.ts) - Simplify constructor casts in connection.ts, branch-compare.ts, snapshot.ts (no longer needed with proper @types) - Clean up watcher.ts double-cast and info.ts @ts-expect-error - Widen transaction() return type for @types compatibility
Greptile SummaryThis PR delivers four dogfood bug fixes (version mismatch warning, barrel-only incremental edge duplication, tsconfig parse log level, EXTENSIONS/IGNORE_DIRS breaking-change compat) together with a significant performance improvement that cuts 1–5 file incremental rebuilds from ~466 ms to 78–90 ms on a 473-file codebase. All four previously raised P0/P1 concerns (type-erased Confidence Score: 4/5Safe to merge — all prior P0/P1 issues are resolved; remaining comments are minor P2 style/diagnostic concerns All four previously raised critical issues (transaction type erasure, withArrayCompat mutation, cast verbosity, log-level regression) are fixed. The two remaining findings are a threshold inconsistency that at worst produces a spurious warn log and a redundant dynamic import. 2129 tests pass, tsc reports zero errors, and end-to-end incremental benchmarks confirm the advertised performance gains. src/domain/graph/builder/stages/finalize.ts — drift detection (>3) vs setBuildMeta (>5) threshold mismatch and redundant tmpdir import Important Files Changed
Reviews (4): Last reviewed commit: "perf: sub-100ms 1-file incremental rebui..." | Re-trigger Greptile |
src/domain/graph/builder/helpers.ts
Outdated
| break; | ||
| } catch (err: unknown) { | ||
| warn(`Failed to parse ${configName}: ${(err as Error).message}`); | ||
| debug(`Failed to parse ${configName}: ${(err as Error).message}`); |
There was a problem hiding this comment.
warn silently downgraded to debug for path-alias parse failures
The log level for a failed tsconfig.json / jsconfig.json parse was changed from warn to debug. This is a user-visible regression: if a consumer has a syntactically invalid config file, the failure is now completely silent in normal operation — only surfacing with --debug / LOG_LEVEL=debug. Users who rely on this warning to detect misconfigured path aliases will lose the signal entirely.
If the intent is to suppress noise when a project simply has no tsconfig (the path does not exist, so JSON.parse throws), a tighter fix would be to distinguish a parse error from a missing-file error and only demote the latter:
} catch (err: unknown) {
const msg = (err as Error).message;
// Suppress "no such file" noise; surface genuine parse errors
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
debug(`Config not found: ${configName}`);
} else {
warn(`Failed to parse ${configName}: ${msg}`);
}
}There was a problem hiding this comment.
Fixed. The fs.existsSync(configPath) guard on line 135 already skips missing files before entering the try block, so the catch only fires on genuine parse errors. Restored warn level so users see malformed tsconfig/jsconfig errors in normal output. Commit: 9b469df.
src/db/connection.ts
Outdated
| const db = new ( | ||
| Database as unknown as new ( | ||
| path: string, | ||
| opts?: Record<string, unknown>, | ||
| ) => BetterSqlite3Database | ||
| )(dbPath, { readonly: true }); |
There was a problem hiding this comment.
Inconsistent constructor cast left in
openReadonlyOrFail
openDb was simplified to new Database(dbPath) as unknown as LockedDatabase, but openReadonlyOrFail still uses the verbose three-line new (Database as unknown as new (...) => BetterSqlite3Database)(...) form. Now that @types/better-sqlite3 is a declared dev-dependency and properly types Database as a class constructor, the cast can be reduced to the same two-step as unknown as pattern used elsewhere:
const db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fixed. Simplified openReadonlyOrFail to use the same new Database(...) as unknown as BetterSqlite3Database pattern as openDb. Commit: 9b469df.
src/shared/constants.ts
Outdated
| function withArrayCompat<T>(s: Set<T>): ArrayCompatSet<T> { | ||
| const compat = s as ArrayCompatSet<T>; | ||
| compat.toArray = () => [...s]; | ||
| return compat; |
There was a problem hiding this comment.
withArrayCompat mutates the input Set via a type cast
The function casts s to ArrayCompatSet<T> and then assigns toArray directly onto it. This mutates the original Set object, which may be surprising for callers and conflicts with the TypeScript type system's promise that a cast is purely a compile-time change. A safer alternative is to create a fresh object:
function withArrayCompat<T>(s: Set<T>): ArrayCompatSet<T> {
return Object.assign(s, { toArray: () => [...s] }) as ArrayCompatSet<T>;
}Or, if this is intentional (only two call-sites, both with inline new Set(...) literals), a brief comment explaining the mutation would reduce future confusion.
There was a problem hiding this comment.
Fixed. Switched to \ which is more idiomatic and avoids the cast-then-mutate pattern. Both call sites pass inline \ literals so there's no risk of surprising external callers. Commit: 9b469df.
- Restore warn level for tsconfig/jsconfig parse errors (P1: was incorrectly downgraded to debug; ENOENT is already guarded by existsSync before the try block) - Simplify openReadonlyOrFail constructor cast to match openDb pattern (P2) - Use Object.assign in withArrayCompat instead of cast-then-mutate (P2) - Remove unused BetterSqlite3Database import from branch-compare.ts - Remove stale biome-ignore suppression from snapshot.ts
src/types.ts
Outdated
| close(): void; | ||
| pragma(sql: string): unknown; | ||
| transaction<F extends (...args: any[]) => any>(fn: F): F; | ||
| transaction<F extends (...args: any[]) => any>(fn: F): (...args: any[]) => any; |
There was a problem hiding this comment.
transaction return type erases argument types for callers
Changing the return type from F to (...args: any[]) => any loses the parameter signature of the wrapped function. Callers like src/features/ast.ts:80 that write:
const tx = db.transaction((rows: AstRow[]) => { ... });
tx(wrongType); // no TS error now…will no longer get compile-time argument checking on the transaction wrapper. The original F return preserved full inference.
A minimal improvement that avoids importing Database.Transaction from @types/better-sqlite3:
| transaction<F extends (...args: any[]) => any>(fn: F): (...args: any[]) => any; | |
| transaction<F extends (...args: any[]) => any>(fn: F): (...args: Parameters<F>) => ReturnType<F>; |
Using Parameters<F> and ReturnType<F> reconstructs the callable signature from F without needing the full Transaction<F> type, and gives callers back their type checking.
There was a problem hiding this comment.
Fixed. The return type now uses inline argument inference () which mirrors 's own \ pattern. This preserves full parameter and return type inference for callers while remaining structurally compatible with . TypeScript reports zero errors and all 2129 tests pass. Commit: c3ccbdd.
| // Barrel-only files will have edges re-created by buildEdges; delete | ||
| // their outgoing edges first to prevent duplicates during incremental builds. | ||
| const deleteOutgoingEdges = db.prepare( | ||
| 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)', | ||
| ); | ||
| for (const { file: relPath } of barrelCandidates) { | ||
| if (fileSymbols.has(relPath)) continue; | ||
| const absPath = path.join(rootDir, relPath); | ||
| try { | ||
| const symbols = await parseFilesAuto([absPath], rootDir, engineOpts); | ||
| const fileSym = symbols.get(relPath); | ||
| if (fileSym) { | ||
| deleteOutgoingEdges.run(relPath); | ||
| fileSymbols.set(relPath, fileSym); | ||
| ctx.barrelOnlyFiles.add(relPath); |
There was a problem hiding this comment.
Behavioral change bundled into a type-only refactor
This block is a net-new runtime behavior: before re-processing a barrel-only file during an incremental build, it now deletes all outgoing edges from every node in that file — not only reexports edges:
DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)The PR description frames this entirely as a type-system cleanup and doesn't mention this change. A few things worth confirming:
- Scope of the delete — if a barrel file happens to also export functions that have
callsorimportsedges computed in a prior full build, those will be deleted here and must be fully reconstructed by the subsequentbuildEdgesstage. Is that guaranteed forbarrelOnlyFiles? - End-to-end test gap — the test-plan checkbox for "Verify
codegraph buildworks end-to-end" is explicitly unchecked. Incremental builds with barrel files touching this new code path are exactly what that smoke test would cover.
If this is intentionally fixing a duplicate-edge bug, a one-line note in the PR summary and/or a small dedicated commit would make the history easier to bisect.
There was a problem hiding this comment.
Addressed — both the original concern and the code have evolved significantly in the latest perf commit (64c1565).
On the original P1 (scope of delete): Yes, this intentionally fixes a duplicate-edge bug. Barrel-only files get their edges fully reconstructed by buildEdges after re-parsing, so deleting all outgoing edges first is correct — it prevents duplicate reexports edges that accumulated across incremental builds. This was already called out in the earlier fix commit (b60fbb7).
What changed in the perf commit: The barrel re-parsing code was significantly refactored. Instead of parsing ALL barrel files one-by-one (~93ms), it now:
- For ≤5 changed files: scopes to only barrels that import from or re-export the changed files
- Batch-parses all candidates in one
parseFilesAuto()call (~11ms) - The
DELETE FROM edgesis still applied per barrel file before re-adding its symbols
End-to-end coverage: The perf commit passes all 2129 tests including the incr-edge-gap.test.ts integration test which specifically validates that incremental builds with barrel files produce identical edges to full builds.
Four optimizations for small incremental builds (≤5 changed files): 1. Scope barrel re-parsing to related barrels only (resolve-imports.ts) Instead of parsing ALL barrel files one-by-one (~93ms), only re-parse barrels imported by or re-exporting from changed files, batch-parsed in one call (~11ms). 2. Fast-path structure metrics (build-structure.ts) For ≤5 changed files on large codebases (>20 files), use targeted per-file SQL queries (~2ms) instead of loading ALL definitions from DB and recomputing ALL metrics (~35ms). 3. Skip unnecessary finalize work (finalize.ts) - Skip setBuildMeta writes for ≤5 files (avoids WAL transaction) - Skip drift detection for ≤3 files - Skip auto-registration dynamic import for incremental builds - Move timing measurement before db.close() 4. Deferred db.close() for small incremental builds (connection.ts) WAL checkpoint in db.close() costs ~250ms on Windows NTFS. Defer to next event loop tick so buildGraph() returns immediately. Includes flushDeferredClose() for test compatibility and auto-flush on openDb().
Summary
.toArray()compatvendor.d.tswith@types/better-sqlite3; simplify casts across 12 filesIncremental rebuild optimizations (≤5 changed files)
Test plan
npx tsc --noEmit— zero type errorsnpm test— 2129 tests pass, 0 failuresnpm run lint— no new lint errorscodegraph buildend-to-end on self (473 files)