Summary
codegraph build on a brand-new project (no prior .codegraph/graph.db) always takes the incremental code path internally, never the full-build path — even though there is zero prior state to diff against. This means applyReachabilityDowngrade's whole-graph BFS reachability pass (#2032, PR #2258) never runs on a project's first build; it only runs on a later --no-incremental rebuild. Discovered while validating #2033's end-to-end dead-code scenario.
Root cause
getChangedFiles (src/domain/graph/builder/stages/detect-changes.ts):
let hasTable = false;
try {
db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
hasTable = true;
} catch (e) { ... }
if (!hasTable) {
return { changed: allFiles.map((f) => ({ file: f })), removed: [], isFullBuild: true };
}
file_hashes is created via CREATE TABLE IF NOT EXISTS in src/db/migrations.ts, applied unconditionally on every DB open — so hasTable is true even for a freshly-created, zero-row DB. The !hasTable branch (the only place that sets isFullBuild: true in this function) is therefore dead in practice; it only fires for a DB predating the file_hashes migration. A first build instead falls through to mtimeAndHashTiers, which always returns isFullBuild: false (every file appears in tierMtimeSize's needsHash list since existing is an empty Map, and tierHash/mtimeAndHashTiers hard-code isFullBuild: false regardless).
Confirmed via repro:
rm -rf .codegraph && codegraph build # logs "[codegraph] Incremental: N changed, 0 removed"
ctx.isFullBuild ends up false for the very first build of any project, of any size.
Impact
classifyRoles (src/domain/graph/builder/stages/build-structure.ts) and classifyNodeRoles (src/features/structure.ts) only run the #2032 transitive-unreachable-dead-code downgrade on the full-build path — classifyNodeRolesIncremental deliberately omits it (a partial, changed-files-scoped edge set can't safely answer a whole-graph reachability question). Since a first build is misclassified as incremental, roles --role dead never benefits from #2032's fix until a user explicitly runs codegraph build --no-incremental — a significant, silent gap in the dead-code detector's real-world effectiveness for every fresh clone/first build.
Suggested fix
getChangedFiles should treat an empty file_hashes table the same as a missing one — check SELECT COUNT(*) FROM file_hashes (or reuse the existing row-count probe pattern already used elsewhere, e.g. hasEmptyAnalysisTable in the same file) rather than just table existence, and return isFullBuild: true in that case too.
Related
Summary
codegraph buildon a brand-new project (no prior.codegraph/graph.db) always takes the incremental code path internally, never the full-build path — even though there is zero prior state to diff against. This meansapplyReachabilityDowngrade's whole-graph BFS reachability pass (#2032, PR #2258) never runs on a project's first build; it only runs on a later--no-incrementalrebuild. Discovered while validating #2033's end-to-end dead-code scenario.Root cause
getChangedFiles(src/domain/graph/builder/stages/detect-changes.ts):file_hashesis created viaCREATE TABLE IF NOT EXISTSinsrc/db/migrations.ts, applied unconditionally on every DB open — sohasTableistrueeven for a freshly-created, zero-row DB. The!hasTablebranch (the only place that setsisFullBuild: truein this function) is therefore dead in practice; it only fires for a DB predating thefile_hashesmigration. A first build instead falls through tomtimeAndHashTiers, which always returnsisFullBuild: false(every file appears intierMtimeSize'sneedsHashlist sinceexistingis an empty Map, andtierHash/mtimeAndHashTiershard-codeisFullBuild: falseregardless).Confirmed via repro:
ctx.isFullBuildends upfalsefor the very first build of any project, of any size.Impact
classifyRoles(src/domain/graph/builder/stages/build-structure.ts) andclassifyNodeRoles(src/features/structure.ts) only run the #2032 transitive-unreachable-dead-code downgrade on the full-build path —classifyNodeRolesIncrementaldeliberately omits it (a partial, changed-files-scoped edge set can't safely answer a whole-graph reachability question). Since a first build is misclassified as incremental,roles --role deadnever benefits from #2032's fix until a user explicitly runscodegraph build --no-incremental— a significant, silent gap in the dead-code detector's real-world effectiveness for every fresh clone/first build.Suggested fix
getChangedFilesshould treat an emptyfile_hashestable the same as a missing one — checkSELECT COUNT(*) FROM file_hashes(or reuse the existing row-count probe pattern already used elsewhere, e.g.hasEmptyAnalysisTablein the same file) rather than just table existence, and returnisFullBuild: truein that case too.Related