Repository: colbymchenry/codegraph (2026-06-15T05:44:52Z)

Issue:
Bug: `codegraph index` produces 0 nodes / 0 edges while `codegraph init` works correctly
**Version:** 1.0.1  
**OS:** Windows 11 (PowerShell 7.6.1)
**Installed via:** `irm https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.ps1 | iex`

**Description**  
After running `codegraph init`, the index is populated correctly (82 nodes, 111 edges).  

However, running `codegraph index` immediately after overwrites the previous index with an empty one (0 nodes, 0 edges), even though the tool successfully finds all 6 files and reports that parsing completed.  

<img width="825" height="762" alt="Image" src="https://github.com/user-attachments/assets/ee7ecc39-5900-4c3e-a7c0-7ac3ca2a8911" />

**Steps to reproduce**  
```bash
# 1. Fresh initialization in a Python project
codegraph init
# Output:
#   Scanning files - 6 found
#   Indexed 6 files
#   82 nodes, 111 edges in 222ms

# 2. Run index
codegraph index
# Output:
#   Scanning files - 6 found
#   Indexed 6 files
#   0 nodes, 0 edges in 216ms
```

**Expected behavior**  
`codegraph index` should produce the same result as `codegraph init` (82 nodes, 111 edges).

**Actual behavior**  
`codegraph index` finds and parses all 6 files, but extracts 0 nodes and 0 edges. After running `index`, the search index becomes empty, although `init` had correctly populated it moments earlier.

**Additional context**  
- Running `codegraph index --verbose` shows that all 6 files (`app.py`, `agent.py`, `config.py`, `main.py`, `memory_manager.py`, `prompts.py`) are parsed, but the extracted node count remains 0.  

<img width="975" height="540" alt="Image" src="https://github.com/user-attachments/assets/dcfef81e-767c-4c31-a3d0-0f62bc2e81b9" />

- Running `uninit`, and then re-running `init` restores the correct index.
- This bug is not isolated to one project: reproduced on [PeekDesktop](https://github.com/shanselman/PeekDesktop) (C# project) — `index` also overwrites the index with 0 nodes and an abnormal edge count:  

<img width="1226" height="762" alt="Image" src="https://github.com/user-attachments/assets/5aec4582-af66-4940-aa15-9a0de26cb0db" />

- Also reproduced on [ppt-master](https://github.com/hugohe3/ppt-master) (Python project):  

<img width="944" height="768" alt="Image" src="https://github.com/user-attachments/assets/2502f214-cfbf-4049-bc9d-94a5620871fe" />


Discussion:
Fixed in #894 (merged to `main`, will ship in the next release). Thank you for the unusually thorough report — the side-by-side `init` vs `index` screenshots across three repos made the root cause obvious.

**What was happening:** `index` and `init` build the graph with the same routine, but `init` runs it on a brand-new empty database while `index` was running it on the *already-populated* one without clearing first. Since nothing had changed between your `init` and `index`, every file's content hash still matched and got skipped, so the run printed its **delta** (`82 − 82 = 0`) as "0 nodes, 0 edges." Good news: your graph was never actually emptied — the index stayed intact underneath — but that `0` summary understandably looked like `index` had wiped it.

**The fix:** `codegraph index` now does a clean full rebuild every time (clear, then re-index), so it produces the same complete result as a fresh `init` — `82 nodes, 111 edges` in your case, not `0`. If you want fast incremental updates instead of a full rebuild, `codegraph sync` is the command for that.

Once it's in a published release you can `codegraph upgrade` and `codegraph index` will behave as expected. Thanks again! 🙏



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 201dfc6f6..7b0c5e11c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### Fixes
 
+- `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)
 - 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)
 - 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)
 
diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts
index 12facd5aa..b0c2f4b48 100644
--- a/src/bin/codegraph.ts
+++ b/src/bin/codegraph.ts
@@ -602,8 +602,8 @@ program
  */
 program
   .command('index [path]')
-  .description('Index all files in the project')
-  .option('-f, --force', 'Force full re-index even if already indexed')
+  .description('Rebuild the full index from scratch (same result as a fresh init)')
+  .option('-f, --force', 'Index even if the path looks like your home directory or a filesystem root')
   .option('-q, --quiet', 'Suppress progress output')
   .option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
   .action(async (pathArg: string | undefined, options: { force?: boolean; quiet?: boolean; verbose?: boolean }) => {
@@ -611,7 +611,7 @@ program
 
     try {
       // Don't (re)index your home directory / a filesystem root (#845). --force
-      // (already "force full re-index") doubles as the override.
+      // doubles as the override.
       const unsafe = unsafeIndexRootReason(projectPath);
       if (unsafe && !options.force) {
         error(`Refusing to index ${projectPath} — it looks like ${unsafe}. Pass --force to override.`);
@@ -628,8 +628,9 @@ program
       const cg = await CodeGraph.open(projectPath);
 
       if (options.quiet) {
-        // Quiet mode: no UI, just run
-        if (options.force) cg.clear();
+        // Quiet mode: no UI, just run. `index` is a full re-index, so clear the
+        // existing graph and rebuild from scratch (see the note below — #874).
+        cg.clear();
         const result = await cg.indexAll();
         if (!result.success) process.exit(1);
         cg.destroy();
@@ -639,10 +640,12 @@ program
       const clack = await importESM('@clack/prompts');
       clack.intro('Indexing project');
 
-      if (options.force) {
-        cg.clear();
-        clack.log.info('Cleared existing index');
-      }
+      // `index` is a FULL re-index: clear the existing graph and rebuild it from
+      // scratch so the result is identical to a fresh `init`. Without the clear,
+      // indexAll() skips every unchanged file by its content hash and reports
+      // "0 nodes, 0 edges" against the already-populated graph — which reads as
+      // "index wiped my index" (#874). For fast incremental updates use `sync`.
+      cg.clear();
 
       let result: IndexResult;
 
@@ -890,7 +893,7 @@ program
       if (reindexRecommended) {
         const builtWith = buildInfo.version ? `v${buildInfo.version.replace(/^v/, '')}` : 'an earlier version';
         warn(`Index was built by ${builtWith}; re-index to pick up this engine's improvements.`);
-        info('Run "codegraph index -f" (full rebuild) or "codegraph sync"');
+        info('Run "codegraph index" (full rebuild) or "codegraph sync"');
         console.log();
       }
 


Test patch:
diff --git a/__tests__/index-command.test.ts b/__tests__/index-command.test.ts
new file mode 100644
index 000000000..9e0ce7953
--- /dev/null
+++ b/__tests__/index-command.test.ts
@@ -0,0 +1,107 @@
+/**
+ * Regression coverage for issue #874: `codegraph index` produced 0 nodes / 0
+ * edges while `codegraph init` worked, and appeared to wipe the graph.
+ *
+ * Root cause: `index` ran a full extraction against the already-populated DB
+ * without clearing it first. 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". The fix makes `index` a true full
+ * rebuild — clear, then re-index — so it produces the same complete result as a
+ * fresh `init`.
+ *
+ * Exercised end-to-end against the built binary so the CLI wiring (not just the
+ * library) is covered.
+ */
+
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { execFileSync } from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+import { CodeGraph } from '../src';
+
+const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
+
+function runCodegraph(args: string[], cwd: string): string {
+  return execFileSync(process.execPath, [BIN, ...args], {
+    cwd,
+    encoding: 'utf-8',
+    env: { ...process.env, CODEGRAPH_NO_DAEMON: '1' },
+    stdio: ['ignore', 'pipe', 'pipe'],
+  });
+}
+
+function graphCounts(dir: string): { nodes: number; edges: number } {
+  const cg = CodeGraph.openSync(dir);
+  try {
+    const stats = cg.getStats();
+    return { nodes: stats.nodeCount, edges: stats.edgeCount };
+  } finally {
+    cg.close();
+  }
+}
+
+describe('codegraph index — full re-index keeps the graph populated (#874)', () => {
+  let tempDir: string;
+
+  beforeEach(() => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-index-cmd-'));
+    // A couple of files with a call edge so there is a non-trivial graph to
+    // (fail to) reproduce.
+    fs.writeFileSync(
+      path.join(tempDir, 'a.ts'),
+      `export function greet(name: string) { return hello(name); }\n` +
+        `export function hello(n: string) { return 'hi ' + n; }\n`,
+    );
+    fs.writeFileSync(
+      path.join(tempDir, 'b.ts'),
+      `import { greet } from './a';\nexport function main() { return greet('world'); }\n`,
+    );
+  });
+
+  afterEach(() => {
+    fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('reproduces init\'s node/edge counts instead of emptying the index', () => {
+    runCodegraph(['init'], tempDir);
+    const afterInit = graphCounts(tempDir);
+    expect(afterInit.nodes).toBeGreaterThan(0);
+    expect(afterInit.edges).toBeGreaterThan(0);
+
+    const out = runCodegraph(['index'], tempDir);
+    const afterIndex = graphCounts(tempDir);
+
+    // The graph is still fully populated — `index` rebuilt it, it did not wipe it.
+    expect(afterIndex.nodes).toBe(afterInit.nodes);
+    expect(afterIndex.edges).toBe(afterInit.edges);
+
+    // ...and the CLI reported the real counts, never the misleading "0 nodes".
+    expect(out).not.toMatch(/\b0 nodes, 0 edges\b/);
+    expect(out).toMatch(new RegExp(`\\b${afterInit.nodes} nodes\\b`));
+  });
+
+  it('is idempotent: a second index does not grow the graph', () => {
+    runCodegraph(['init'], tempDir);
+    runCodegraph(['index'], tempDir);
+    const first = graphCounts(tempDir);
+    runCodegraph(['index'], tempDir);
+    const second = graphCounts(tempDir);
+
+    // A clean rebuild each time — no duplicate (re-resolved) edges accumulating
+    // across runs (the C# "+18 edges" symptom in the report).
+    expect(second.nodes).toBe(first.nodes);
+    expect(second.edges).toBe(first.edges);
+  });
+
+  it('--quiet path also rebuilds a populated graph', () => {
+    runCodegraph(['init'], tempDir);
+    const afterInit = graphCounts(tempDir);
+
+    runCodegraph(['index', '--quiet'], tempDir);
+    const afterIndex = graphCounts(tempDir);
+
+    expect(afterIndex.nodes).toBe(afterInit.nodes);
+    expect(afterIndex.edges).toBe(afterInit.edges);
+  });
+});