graph-stats-census-vs-warm-file-count-mismatch
The diagnosis is complete and the fix has been verified against the real upstream repo (gethilo/hilo, DF-WARPFS-43, commit 047b2b9). The full solution — including the complete 434-line patch, tests, and before/after evidence — is written to /workspace/solution.md.
Root cause. The two numbers use different definitions:
- hilo graph stats Total files = COUNT(DISTINCT "from") over the edges table → only files with outgoing edges.
- hilo graph warm Coverage: = contributes + facades + no_imports + unreadable + unsupported over every discovered file.
Zero-edge files (constants, __init__.py facades) never enter the census, and because edges.jsonl is append-only and warm does a delta DuckDB insert, sources of deleted/renamed files linger in the census. Hence census > contributes (run-12 705/666 and live 726/687).
Additive fix (does not change either definition):
1. graph warm persists its end-of-warm accounting to .vfs/graph/coverage.json (best-effort), on both return paths — the fresh-write path and the full-cache-hit fast path.
2. graph stats reads that ledger and prints a File census: line giving both numbers plus the drift direction:
- census > contributes → "stale entries no longer on disk (edges.jsonl is append-only)"
- census < contributes → "the census is missing contributors … run hilo graph warm"
3. graph clean purges coverage.json; missing/corrupt ledger degrades to the pre-ledger output.
Key code (in hilo-cli/src/commands/graph.rs):
const COVERAGE_LEDGER: &str = "coverage.json";
fn write_coverage_ledger(cwd: &Path, report: &CoverageReport, discovered: usize, sources: usize) {
let path = cwd.join(".vfs").join("graph").join(COVERAGE_LEDGER);
let doc = serde_json::json!({
"schema": 1,
"discovered_files": discovered,
"contributes": report.contributes,
"package_facades": report.facades,
"no_imports": report.no_imports,
"unreadable": report.unreadable.len(),
"unsupported_extension": report.unsupported.len(),
"unique_edge_sources": sources,
});
if let Some(parent) = path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!("warning: failed to create {}: {e}", parent.display());
return;
}
}
match serde_json::to_string_pretty(&doc) {
Ok(body) => {
if let Err(e) = std::fs::write(&path, body + "\n") {
eprintln!("warning: failed to write {}: {e}", path.display());
}
}
Err(e) => eprintln!("warning: failed to serialize coverage ledger: {e}"),
}
}
Called on both paths: write_coverage_ledger(cwd, &coverage, total_files, unique_sources.len()); in the fast-path branch and after the normal coverage.print().
cargo test -p hilo_graph --lib stats_file_census → 2 passedcargo test -p hilo-cli --test cli graph_stats_ → 5 passed, including the new reconcile/ledger test (fresh write, fast-path refresh, stale branch after deletion) and the degraded-mode test.git apply --check on 9465968 = OK) and reverse-applies to the fix commit.Coverage: 3 files while stats says Total files: 2 with no explanation; after, stats prints:
File census: 2 files with edges (distinct edge sources) — warm last counted 3 discovered files = 2 contribute + 1 zero-edge (1 no imports + 0 package facades).helper.py and re-warming, the stale branch fires: … some sources may be stale entries no longer on disk (edges.jsonl is append-only).Confirmed gotchas: census counts only outgoing-edge sources; CLI tests need hilo init before warm; the stale branch requires a re-warm after deletion (the ledger refreshes on warm, not stats).
# Evidence - Problem class: graph-stats-census-vs-warm-file-count-mismatch - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-26T02:45:05.411Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Two headline numbers on one graph disagreed: `hilo graph stats` Total files = COUNT(DISTINCT from) over the edges table (edge-derived census), while `hilo graph warm` prints Coverage: N files = contributes + facades + no-imports (every discovered file). Run-12 signature (705 vs 666) and live repro (726 vs 687) both decompose as contributes (census may additionally carry stale append-only inventory sources of deleted files). Additive fix that reconciles instead of changing either definition: warm persists its coverage accounting to .vfs/graph/coverage.json (best-effort write on BOTH return paths incl. the full-cache-hit fast path, purged by graph clean), and stats prints a File census reconcile line stating both numbers and naming the drift direction (stale inventory when census > contributors; missing contributors when census < contributors); missing/corrupt ledger degrades to the pre-ledger output. Rust live-AC gotchas hit while testing: the census counts only files with outgoing edges (a to-target with no from-edges is invisible), so unit tests must construct sources explicitly; CLI integration tests need `hilo init` before warm (manifest precondition) and a re-warm after deleting a source file before the stale branch fires (the ledger refreshes on warm, not stats).", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "graph-stats-census-vs-warm-file-count-mismatch", "provider": "openrouter", "solved_at": "2026-09-26T02:45:05.416Z", "version": ""}