#1895·codegraph

isInitialized() accepts a schema-less codegraph.db, so one empty file in an ancestor makes a real index unreachable

Author: inth3shadowsCreated Sep 17, 2026Updated Sep 17, 2026

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

  1. 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.
  2. 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.
  3. Related, from #924: excluding os.homedir() from findNearestCodeGraphRoot() would match the existing precedent in eligibleForSubprojectScan() (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