Skip to content

Commit b49147e

Browse files
colbymchenryclaude
andauthored
fix(cli): make codegraph index a full rebuild so it stops reporting 0 nodes (colbymchenry#874) (colbymchenry#894)
`codegraph index` ran extraction against the already-populated DB without clearing it first. On an unchanged tree every file's content hash still matched, so the orchestrator skipped re-inserting all of them and the run reported its delta (after - before = 0) as "0 nodes, 0 edges" — which read as if `index` had wiped the graph. `init` only ever differed because it runs on a freshly created, empty DB. Clear the existing graph before re-indexing so `index` rebuilds from scratch and reports the same complete result as a fresh `init`. `--force` keeps its role as the home-dir/root-path override; `sync` stays the incremental path. Adds an end-to-end regression test driving the built binary (init -> index), asserting the graph stays populated and the summary is never "0 nodes, 0 edges". Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ab107b3 commit b49147e

3 files changed

Lines changed: 121 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1111

1212
### Fixes
1313

14+
- `codegraph index` now rebuilds the full graph from scratch, so it produces the same result as a fresh `codegraph init` instead of reporting "0 nodes, 0 edges" and looking like it wiped your index. Previously, re-running `index` on an unchanged project skipped every file (their contents hadn't changed) and showed an empty-looking summary; it now clears and re-indexes for an honest, complete rebuild every time. Use `codegraph sync` for fast incremental updates between full rebuilds. Thanks @Arc-univer. (#874)
1415
- The file watcher that auto-syncs the graph now fails cleanly when live watching can no longer be trusted, instead of looking healthy while the index quietly goes stale. If the operating system runs out of file-watch resources, or another process holds the write lock far longer than a normal save, CodeGraph now disables auto-sync once — with a single clear message telling you to run `codegraph sync` (or rely on the git sync hooks) to refresh — rather than retrying forever or repeating the same error on a loop. And while auto-sync is disabled, CodeGraph's tool responses (and `codegraph status`) now say so plainly, so your AI agent knows to read files directly instead of trusting a frozen index. This mostly matters for long-running MCP/daemon sessions, which could otherwise keep serving stale results while appearing to work. Thanks @thismilktea. (#876)
1516
- On Linux, hitting the kernel's inotify watch limit on a large project no longer silently leaves half the tree unwatched. CodeGraph now tells you once — naming the exact setting to raise (`fs.inotify.max_user_watches`, e.g. `sudo sysctl fs.inotify.max_user_watches=1048576`) — and keeps live-watching the directories it could register while `codegraph sync` (or the git sync hooks) covers the rest. (#876)
1617

__tests__/index-command.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* Regression coverage for issue #874: `codegraph index` produced 0 nodes / 0
3+
* edges while `codegraph init` worked, and appeared to wipe the graph.
4+
*
5+
* Root cause: `index` ran a full extraction against the already-populated DB
6+
* without clearing it first. Every file's content hash still matched, so the
7+
* orchestrator skipped re-inserting all of them, and the run reported its delta
8+
* (after - before = 0) as "0 nodes, 0 edges". The fix makes `index` a true full
9+
* rebuild — clear, then re-index — so it produces the same complete result as a
10+
* fresh `init`.
11+
*
12+
* Exercised end-to-end against the built binary so the CLI wiring (not just the
13+
* library) is covered.
14+
*/
15+
16+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
17+
import { execFileSync } from 'child_process';
18+
import * as fs from 'fs';
19+
import * as path from 'path';
20+
import * as os from 'os';
21+
import { CodeGraph } from '../src';
22+
23+
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
24+
25+
function runCodegraph(args: string[], cwd: string): string {
26+
return execFileSync(process.execPath, [BIN, ...args], {
27+
cwd,
28+
encoding: 'utf-8',
29+
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1' },
30+
stdio: ['ignore', 'pipe', 'pipe'],
31+
});
32+
}
33+
34+
function graphCounts(dir: string): { nodes: number; edges: number } {
35+
const cg = CodeGraph.openSync(dir);
36+
try {
37+
const stats = cg.getStats();
38+
return { nodes: stats.nodeCount, edges: stats.edgeCount };
39+
} finally {
40+
cg.close();
41+
}
42+
}
43+
44+
describe('codegraph index — full re-index keeps the graph populated (#874)', () => {
45+
let tempDir: string;
46+
47+
beforeEach(() => {
48+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-index-cmd-'));
49+
// A couple of files with a call edge so there is a non-trivial graph to
50+
// (fail to) reproduce.
51+
fs.writeFileSync(
52+
path.join(tempDir, 'a.ts'),
53+
`export function greet(name: string) { return hello(name); }\n` +
54+
`export function hello(n: string) { return 'hi ' + n; }\n`,
55+
);
56+
fs.writeFileSync(
57+
path.join(tempDir, 'b.ts'),
58+
`import { greet } from './a';\nexport function main() { return greet('world'); }\n`,
59+
);
60+
});
61+
62+
afterEach(() => {
63+
fs.rmSync(tempDir, { recursive: true, force: true });
64+
});
65+
66+
it('reproduces init\'s node/edge counts instead of emptying the index', () => {
67+
runCodegraph(['init'], tempDir);
68+
const afterInit = graphCounts(tempDir);
69+
expect(afterInit.nodes).toBeGreaterThan(0);
70+
expect(afterInit.edges).toBeGreaterThan(0);
71+
72+
const out = runCodegraph(['index'], tempDir);
73+
const afterIndex = graphCounts(tempDir);
74+
75+
// The graph is still fully populated — `index` rebuilt it, it did not wipe it.
76+
expect(afterIndex.nodes).toBe(afterInit.nodes);
77+
expect(afterIndex.edges).toBe(afterInit.edges);
78+
79+
// ...and the CLI reported the real counts, never the misleading "0 nodes".
80+
expect(out).not.toMatch(/\b0 nodes, 0 edges\b/);
81+
expect(out).toMatch(new RegExp(`\\b${afterInit.nodes} nodes\\b`));
82+
});
83+
84+
it('is idempotent: a second index does not grow the graph', () => {
85+
runCodegraph(['init'], tempDir);
86+
runCodegraph(['index'], tempDir);
87+
const first = graphCounts(tempDir);
88+
runCodegraph(['index'], tempDir);
89+
const second = graphCounts(tempDir);
90+
91+
// A clean rebuild each time — no duplicate (re-resolved) edges accumulating
92+
// across runs (the C# "+18 edges" symptom in the report).
93+
expect(second.nodes).toBe(first.nodes);
94+
expect(second.edges).toBe(first.edges);
95+
});
96+
97+
it('--quiet path also rebuilds a populated graph', () => {
98+
runCodegraph(['init'], tempDir);
99+
const afterInit = graphCounts(tempDir);
100+
101+
runCodegraph(['index', '--quiet'], tempDir);
102+
const afterIndex = graphCounts(tempDir);
103+
104+
expect(afterIndex.nodes).toBe(afterInit.nodes);
105+
expect(afterIndex.edges).toBe(afterInit.edges);
106+
});
107+
});

src/bin/codegraph.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -602,16 +602,16 @@ program
602602
*/
603603
program
604604
.command('index [path]')
605-
.description('Index all files in the project')
606-
.option('-f, --force', 'Force full re-index even if already indexed')
605+
.description('Rebuild the full index from scratch (same result as a fresh init)')
606+
.option('-f, --force', 'Index even if the path looks like your home directory or a filesystem root')
607607
.option('-q, --quiet', 'Suppress progress output')
608608
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
609609
.action(async (pathArg: string | undefined, options: { force?: boolean; quiet?: boolean; verbose?: boolean }) => {
610610
const projectPath = resolveProjectPath(pathArg);
611611

612612
try {
613613
// Don't (re)index your home directory / a filesystem root (#845). --force
614-
// (already "force full re-index") doubles as the override.
614+
// doubles as the override.
615615
const unsafe = unsafeIndexRootReason(projectPath);
616616
if (unsafe && !options.force) {
617617
error(`Refusing to index ${projectPath} — it looks like ${unsafe}. Pass --force to override.`);
@@ -628,8 +628,9 @@ program
628628
const cg = await CodeGraph.open(projectPath);
629629

630630
if (options.quiet) {
631-
// Quiet mode: no UI, just run
632-
if (options.force) cg.clear();
631+
// Quiet mode: no UI, just run. `index` is a full re-index, so clear the
632+
// existing graph and rebuild from scratch (see the note below — #874).
633+
cg.clear();
633634
const result = await cg.indexAll();
634635
if (!result.success) process.exit(1);
635636
cg.destroy();
@@ -639,10 +640,12 @@ program
639640
const clack = await importESM('@clack/prompts');
640641
clack.intro('Indexing project');
641642

642-
if (options.force) {
643-
cg.clear();
644-
clack.log.info('Cleared existing index');
645-
}
643+
// `index` is a FULL re-index: clear the existing graph and rebuild it from
644+
// scratch so the result is identical to a fresh `init`. Without the clear,
645+
// indexAll() skips every unchanged file by its content hash and reports
646+
// "0 nodes, 0 edges" against the already-populated graph — which reads as
647+
// "index wiped my index" (#874). For fast incremental updates use `sync`.
648+
cg.clear();
646649

647650
let result: IndexResult;
648651

@@ -890,7 +893,7 @@ program
890893
if (reindexRecommended) {
891894
const builtWith = buildInfo.version ? `v${buildInfo.version.replace(/^v/, '')}` : 'an earlier version';
892895
warn(`Index was built by ${builtWith}; re-index to pick up this engine's improvements.`);
893-
info('Run "codegraph index -f" (full rebuild) or "codegraph sync"');
896+
info('Run "codegraph index" (full rebuild) or "codegraph sync"');
894897
console.log();
895898
}
896899

0 commit comments

Comments
 (0)