Skip to content
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2b6a00d
fix: scope codegraph batch complexity targets to file paths
carlos-alm Jul 5, 2026
b28051a
fix: exclude parameters and interface/type members from dead-role cla…
carlos-alm Jul 5, 2026
17fbcba
fix: credit import-type usages as exports consumers
carlos-alm Jul 5, 2026
26918bb
fix: prevent fn-impact/query crash when -f/--file is passed
carlos-alm Jul 5, 2026
be6432c
fix: correct exported-symbol detection for literal and object-literal…
carlos-alm Jul 5, 2026
4ead840
fix: exclude primitive type keywords from ast --kind string matching
carlos-alm Jul 5, 2026
f6326bf
fix: resolve call edges through renamed import specifiers
carlos-alm Jul 5, 2026
590f560
fix: couple file_hashes updates with edge regeneration in incremental…
carlos-alm Jul 5, 2026
d3ea4a4
fix: compare signature-change diffs using new-file line ranges
carlos-alm Jul 5, 2026
c306812
feat: add environment doctor check for stale native binary and missin…
carlos-alm Jul 5, 2026
8bb5893
fix: eliminate non-deterministic ordering in community detection
carlos-alm Jul 6, 2026
46037b1
fix: sync update-graph.sh hook extension allowlist with EXTENSIONS
carlos-alm Jul 6, 2026
6a84cf9
fix: recompute directory structure metrics for affected directories o…
carlos-alm Jul 6, 2026
8d936d1
refactor: register hardcoded execFileSync/execSync maxBuffer values i…
carlos-alm Jul 6, 2026
11c84be
fix: gate blast-radius check on newly introduced risk, not pre-existi…
carlos-alm Jul 6, 2026
963301a
fix: gate identifier-argument dynamic call edges on callback-acceptin…
carlos-alm Jul 6, 2026
3e8035d
fix: gate Array.from's callback arg by position, not just callee name
carlos-alm Jul 6, 2026
879635e
fix: scope reexportedSymbols to actually-named re-export specifiers
carlos-alm Jul 6, 2026
0fe9fc3
fix: stop CFG block/edge count from overriding AST-derived cyclomatic…
carlos-alm Jul 6, 2026
ccf3c19
fix: backfill edges.technique for incrementally-inserted calls edges
carlos-alm Jul 6, 2026
d5b1162
fix: wrap remote embedding JSON parse failure in EngineError
carlos-alm Jul 6, 2026
8971017
refactor: extract shared platform-default-path helper in config.ts
carlos-alm Jul 6, 2026
056c5e4
refactor: decompose loadConfig and related high-effort functions in c…
carlos-alm Jul 6, 2026
0738171
refactor: decompose findDbPath and openRepo in db/connection.ts (docs…
carlos-alm Jul 6, 2026
fc36128
fix: resolve merge conflicts with main
carlos-alm Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 116 additions & 68 deletions src/db/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,68 +263,90 @@ export function closeDbPairDeferred(pair: LockedDatabasePair): void {
closeDbDeferred(pair.db);
}

export function findDbPath(customPath?: string): string {
if (customPath) {
const resolved = path.resolve(customPath);
// When a directory is passed (e.g. --db /path/to/repo), locate the DB
// inside it — matching the layout that `build` creates.
try {
if (fs.statSync(resolved).isDirectory()) {
// If the caller passed the .codegraph directory itself (e.g. --db /repo/.codegraph),
// use it directly to avoid double-appending .codegraph/.codegraph/graph.db.
if (path.basename(resolved) === '.codegraph') {
return path.join(resolved, 'graph.db');
}
return path.join(resolved, '.codegraph', 'graph.db');
/**
* Resolve an explicit `--db` path: when it points at a directory, locate the
* DB inside it — matching the layout that `build` creates.
*/
function resolveCustomDbPath(customPath: string): string {
const resolved = path.resolve(customPath);
try {
if (fs.statSync(resolved).isDirectory()) {
// If the caller passed the .codegraph directory itself (e.g. --db /repo/.codegraph),
// use it directly to avoid double-appending .codegraph/.codegraph/graph.db.
if (path.basename(resolved) === '.codegraph') {
return path.join(resolved, 'graph.db');
}
} catch {
// Path doesn't exist yet — return as-is (e.g. a future custom DB path).
return path.join(resolved, '.codegraph', 'graph.db');
}
return resolved;
} catch {
// Path doesn't exist yet — return as-is (e.g. a future custom DB path).
}
const rawCeiling = findRepoRoot();
// Normalize ceiling with realpathSync to resolve 8.3 short names (Windows
// RUNNER~1 → runneradmin) and symlinks (macOS /var → /private/var).
// findRepoRoot already applies realpathSync internally, but the git output
// may still contain short names on some Windows CI environments.
let ceiling: string | null;
if (rawCeiling) {
try {
ceiling = fs.realpathSync(rawCeiling);
} catch (e) {
debug(`realpathSync failed for ceiling "${rawCeiling}": ${toErrorMessage(e)}`);
ceiling = rawCeiling;
}
} else {
ceiling = null;
return resolved;
}

/**
* Normalize the git ceiling with realpathSync to resolve 8.3 short names
* (Windows RUNNER~1 → runneradmin) and symlinks (macOS /var → /private/var).
* findRepoRoot already applies realpathSync internally, but the git output
* may still contain short names on some Windows CI environments.
*/
function resolveDbSearchCeiling(rawCeiling: string | null): string | null {
if (!rawCeiling) return null;
try {
return fs.realpathSync(rawCeiling);
} catch (e) {
debug(`realpathSync failed for ceiling "${rawCeiling}": ${toErrorMessage(e)}`);
return rawCeiling;
}
// Resolve symlinks (e.g. macOS /var → /private/var) so dir matches ceiling from git
let dir: string;
}

/** Resolve symlinks in cwd (e.g. macOS /var → /private/var) so dir matches ceiling from git. */
function resolveDbSearchStartDir(): string {
try {
dir = fs.realpathSync(process.cwd());
return fs.realpathSync(process.cwd());
} catch (e) {
debug(`realpathSync failed for cwd: ${toErrorMessage(e)}`);
dir = process.cwd();
return process.cwd();
}
}

/**
* Walk up from `startDir` toward `ceiling` looking for an existing
* .codegraph/graph.db. Returns the found path, or null if the walk reaches
* the ceiling (or, when there's no ceiling, after checking only `startDir`)
* without finding one.
*/
function walkUpForDbPath(startDir: string, ceiling: string | null): string | null {
let dir = startDir;
while (true) {
const candidate = path.join(dir, '.codegraph', 'graph.db');
if (fs.existsSync(candidate)) return candidate;
if (ceiling && isSameDirectory(dir, ceiling)) {
debug(`findDbPath: stopped at git ceiling ${ceiling}`);
break;
return null;
}
// Outside a git repo, cwd is the first (and only) directory we'll check.
// Walking past it risks attaching to a stale .codegraph/ in an unrelated
// parent — e.g. /private/tmp/.codegraph/ leaking into every /tmp/foo/ run,
// or $HOME/.codegraph/ leaking into every scratch dir under $HOME.
if (!ceiling) {
debug(`findDbPath: no git ceiling, stopping at ${dir}`);
break;
return null;
}
const parent = path.dirname(dir);
if (parent === dir) break;
if (parent === dir) return null;
dir = parent;
}
}

export function findDbPath(customPath?: string): string {
if (customPath) {
return resolveCustomDbPath(customPath);
}
const ceiling = resolveDbSearchCeiling(findRepoRoot());
const startDir = resolveDbSearchStartDir();
const found = walkUpForDbPath(startDir, ceiling);
if (found) return found;
const base = ceiling || process.cwd();
return path.join(base, '.codegraph', 'graph.db');
}
Expand Down Expand Up @@ -416,6 +438,58 @@ function openRepoNative(customDbPath?: string): { repo: Repository; close(): voi
}
}

/** Validate and wrap an injected `opts.repo` Repository instance (no DB opened). */
function wrapInjectedRepo(repo: Repository): { repo: Repository; close(): void } {
if (!(repo instanceof Repository)) {
throw new TypeError(
`openRepo: opts.repo must be a Repository instance, got ${Object.prototype.toString.call(repo)}`,
);
}
return { repo, close() {} };
}

/**
* Attempt the native rusqlite path (Phase 6.14) when the resolved engine
* allows it. Re-throws user-visible errors (DB not found, busy/locked) since
* falling back to better-sqlite3 wouldn't help; returns undefined for other
* native failures so the caller falls back to better-sqlite3.
*/
function tryOpenRepoNative(
customDbPath: string | undefined,
engine: 'native' | 'wasm' | 'auto',
): { repo: Repository; close(): void } | undefined {
if (engine === 'wasm' || !isNativeAvailable()) return undefined;
try {
return openRepoNative(customDbPath);
} catch (e) {
// Re-throw user-visible errors (e.g. DB not found) — only silently
// fall back for native-engine failures (e.g. incompatible native binary).
if (e instanceof DbError) throw e;
// Re-throw locking/busy errors — falling back to better-sqlite3 would
// hit the same contention (and potentially hang without busy_timeout).
const msg = toErrorMessage(e);
if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
throw new DbError(`Database is busy (another process may be writing): ${msg}`, {});
}
debug(`openRepo: native path failed, falling back to better-sqlite3: ${msg}`);
return undefined;
}
}

/** Open the better-sqlite3 fallback repo used when the native path is unavailable or opted out. */
function openRepoSqliteFallback(
customDbPath: string | undefined,
busyTimeoutMs: number,
): { repo: Repository; close(): void } {
const db = openReadonlyOrFail(customDbPath, busyTimeoutMs);
return {
repo: new SqliteRepository(db),
close() {
db.close();
},
};
}

/**
* Open a Repository from either an injected instance or a DB path.
*
Expand All @@ -429,43 +503,17 @@ export function openRepo(
opts: { repo?: Repository; engine?: 'native' | 'wasm' | 'auto' } = {},
): { repo: Repository; close(): void } {
if (opts.repo != null) {
if (!(opts.repo instanceof Repository)) {
throw new TypeError(
`openRepo: opts.repo must be a Repository instance, got ${Object.prototype.toString.call(opts.repo)}`,
);
}
return { repo: opts.repo, close() {} };
return wrapInjectedRepo(opts.repo);
}

// Respect explicit engine selection: opts.engine > config.build.engine > auto.
// This ensures --engine wasm and benchmark workers bypass the native path.
const { engine, busyTimeoutMs } = resolveDbSettings(customDbPath, opts.engine);

// Try native rusqlite path first (Phase 6.14)
if (engine !== 'wasm' && isNativeAvailable()) {
try {
return openRepoNative(customDbPath);
} catch (e) {
// Re-throw user-visible errors (e.g. DB not found) — only silently
// fall back for native-engine failures (e.g. incompatible native binary).
if (e instanceof DbError) throw e;
// Re-throw locking/busy errors — falling back to better-sqlite3 would
// hit the same contention (and potentially hang without busy_timeout).
const msg = toErrorMessage(e);
if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
throw new DbError(`Database is busy (another process may be writing): ${msg}`, {});
}
debug(`openRepo: native path failed, falling back to better-sqlite3: ${msg}`);
}
}
const native = tryOpenRepoNative(customDbPath, engine);
if (native) return native;

const db = openReadonlyOrFail(customDbPath, busyTimeoutMs);
return {
repo: new SqliteRepository(db),
close() {
db.close();
},
};
return openRepoSqliteFallback(customDbPath, busyTimeoutMs);
}

/**
Expand Down
Loading