isInitialized() accepts a schema-less codegraph.db, so one empty file in an ancestor makes a real index unreachable
Summary
isInitialized() accepts a .codegraph/ as an initialized project when codegraph.db merely exists, without checking that it has a schema. Because resolveServerRoot() returns the up-walk hit unconditionally and never reaches the sub-project down-scan, one zero-table codegraph.db anywhere in an ancestor directory makes a real, fully-indexed sub-project unreachable.
Reproduces on v1.6.0 and on current main (142 commits past the tag) — isInitialized is byte-identical between them.
Reproduction
A workspace container ws/ (has .git, no index of its own) with a properly indexed sub-project ws/sub/, which the #1606 down-scan is supposed to adopt:
B=$(mktemp -d); mkdir -p $B/ws/sub/src $B/ws/.git
printf 'def alpha(x):\n return beta(x) + 1\ndef beta(x):\n return x * 2\n' > $B/ws/sub/src/a.py
printf '[project]\nname = "sub"\nversion = "0.1"\n' > $B/ws/sub/pyproject.toml
( cd $B/ws/sub && codegraph init ) # real index: 163840 bytes, 14 tables
cd $B/ws && codegraph serve --mcp # then call codegraph_explore
Control — no poisoned ancestor. The down-scan adopts ws/sub and the tool works:
**Exploration: what does alpha call**
Found 2 symbols across 1 file.
Now add a schema-less db to an ancestor of ws — not to ws, not to ws/sub:
mkdir -p $B/.codegraph
python3 -c "import sqlite3,sys;c=sqlite3.connect(sys.argv[1]);c.execute('create table t(x)');c.execute('drop table t');c.commit()" \
$B/.codegraph/codegraph.db # exists, 0 tables
cd $B/ws && codegraph serve --mcp
Result — the real index is now invisible:
No CodeGraph project is loaded for this session.
Searched for a .codegraph/ directory starting from: /tmp/...
The 163840-byte, 14-table index at ws/sub did not change. An 8192-byte file with zero tables, two directories above, disabled it.
Root cause
isInitialized() tests existence only (src/directory.ts:93-101):
export function isInitialized(projectRoot: string): boolean {
const codegraphDir = getCodeGraphDir(projectRoot);
if (!fs.existsSync(codegraphDir) || !fs.statSync(codegraphDir).isDirectory()) return false;
// Must have codegraph.db, not just .codegraph folder
const dbPath = path.join(codegraphDir, 'codegraph.db');
return fs.existsSync(dbPath);
}
findNearestCodeGraphRoot() returns the first ancestor satisfying it (src/directory.ts:158-177), and resolveServerRoot() short-circuits on that result (src/directory.ts:287-288):
const up = findNearestCodeGraphRoot(searchFrom);
if (up) return { root: up, viaSubScan: false, candidates: [] };
// down-scan below is never reached
So the schema-less db does two things: it captures the search root, and it suppresses the down-scan that would otherwise have found the genuine sub-project. The candidates: [] also means the #1607 "here is what IS reachable" hint comes back empty, so the error message cannot point at the index that exists.
Why this is easy to hit
A codegraph.db with no tables is what a failed, interrupted, or mistyped init leaves behind. It is silent — nothing warns that a directory is now capturing resolution for everything beneath it.
It is worst in $HOME, which is an ancestor of every project. On this machine $HOME/.codegraph/codegraph.db was 4096 bytes with zero tables, created 2026-07-15 and never populated, and $HOME/.codegraph/ cannot simply be deleted because it is also where app state lives (daemons/, telemetry.json, update-check.json — recreated within minutes). Discussed further on #924.
Suggested fix
- Make
isInitialized()validate the schema, not just the file's existence — e.g. confirm a known table is present. A db with zero tables is not an initialized project. - Optionally, let
resolveServerRoot()fall through to the down-scan when the up-walk hit turns out to be unusable, rather than returning it unconditionally. That would also improve the #1607 candidate list in this case. - Related, from #924: excluding
os.homedir()fromfindNearestCodeGraphRoot()would match the existing precedent ineligibleForSubprojectScan()(src/directory.ts:262-269, citing #1454 — "The user's home directory and the filesystem root are never eligible"). That guard exists for the down-scan but not the up-walk.
(1) alone fixes the reproduction above.
Environment
- codegraph v1.6.0, also verified against current
main - Node 22.23.2
- Linux 6.6.87.2 (WSL2), Ubuntu 24.04
Source: colbymchenry/codegraph