From 1c37ec1941fe4a492b6f18e95d1af2087d72dce4 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Fri, 30 Oct 2026 16:22:35 -0500 Subject: [PATCH] fix(cli): sync warns about an index from an older engine, and still runs An index built by an older extraction version made `codegraph sync` print "Already up to date" while `codegraph status` said to re-index. Sync re-reads changed files only and never advances the extraction stamp, so that message was false however little had changed. Sync now says "No files changed" in that case and ends with a warning that names `codegraph index`, after syncing changed files as before. It exits 0. Refusing, as the PR first did, would have stopped the installed git hooks: they run sync in the background with its output discarded, so every index behind a hook would quietly stop following commits on the next upgrade. `--quiet` stays silent, as cli-sync.test.ts already pins. Validation: the new cli-sync-stale-index.test.ts has 6 cases for an old stamp and a missing one, unchanged and changed files, quiet and not, a current index, and the rebuild clearing the warning. 3 fail on main, 6 pass here. A real commit through the installed post-commit hook on a stale index put the new file in the index in about a second. Original PR #1842 by @bompus. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cli-sync-stale-index.test.ts | 107 +++++++++++++++++++++++++ src/bin/codegraph.ts | 11 ++- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 __tests__/cli-sync-stale-index.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 20cdf37c2..cc129fb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `codegraph upgrade` now upgrades a global npm install when run from your home folder, which holds npm's global folder on Windows and with tools like nvm: before, it ran `npm install` without `-g` there, which left the global copy on the old version and put a second copy in your home folder. A copy installed in a project is now upgraded in that project wherever you run the command, with a warning if npm left it on the old version, and a copy installed with pnpm, yarn or bun gets the command that upgrades it instead of npm. Thanks @timlincool for the report and @maxmilian for the first fix. (#2477) - The Windows installer, `irm …/install.ps1 | iex`, no longer stops with "(403) Forbidden" once GitHub's hourly limit for anonymous requests is used up, as it often is on shared networks and in CI: it now finds the latest release the way the macOS and Linux installer and `codegraph upgrade` do, and asks GitHub's API only when that fails. A version set without its `v`, like `CODEGRAPH_VERSION=1.6.2`, now installs too, where before its download failed. Thanks @hiyochi. - `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336) +- `codegraph sync` no longer says "Already up to date" on an index built by an older CodeGraph: it syncs your changed files as before, then warns that the files it didn't touch still need a full `codegraph index`. Thanks @bompus. - Running `codegraph status` while `codegraph index` rebuilds a project no longer makes the rebuild fail with "database is locked": status now only reads the index, and it says when a rebuild is running in another process, in its output and as `rebuildInProgress` in `status --json`. Thanks @sealad886 for the report, and @danusha2345 and @KissFeng for the fixes. (#2913) - Running `codegraph query`, `explore`, `node`, `callers`, `callees`, `impact`, `context`, `files` or `affected` while `codegraph index` rebuilds a project no longer makes the rebuild fail with "database is locked", and neither does the context hook Claude Code runs on its own when you ask about your code: like `codegraph status`, they now only read the index and say when a rebuild is running, and the hook stays quiet until the rebuild is done. They still answer from an index an older CodeGraph version built. Thanks @sealad886 for the report. (#2913) - Opening a project whose index needs a schema update no longer fails when several CodeGraph processes open it at the same moment, like agent sessions starting right after an upgrade, or the MCP server and a git hook: one of them updates the index while the others wait for it and then carry on. Before, the ones that lost the race applied the same update again, and failed with "UNIQUE constraint failed" or "database is locked". Thanks @inth3shadows for the report and @johnhldiver-beep for the first fix. (#2980) diff --git a/__tests__/cli-sync-stale-index.test.ts b/__tests__/cli-sync-stale-index.test.ts new file mode 100644 index 000000000..9fffdbab1 --- /dev/null +++ b/__tests__/cli-sync-stale-index.test.ts @@ -0,0 +1,107 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { EXTRACTION_VERSION } from '../src/extraction/extraction-version'; + +const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); +const { DatabaseSync } = require('node:sqlite'); +const WARNING = 'Run "codegraph index" for a full rebuild'; +let root: string; + +function run(...args: string[]) { + return spawnSync(process.execPath, [BIN, ...args], { + cwd: root, + encoding: 'utf8', + timeout: 30_000, + env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NODE_NO_WARNINGS: '1', NO_COLOR: '1' }, + }); +} + +function setStamp(stamp: number | null) { + const db = new DatabaseSync(path.join(root, '.codegraph', 'codegraph.db')); + try { + if (stamp === null) { + db.prepare('DELETE FROM project_metadata WHERE key = ?').run('indexed_with_extraction_version'); + } else { + db.prepare('UPDATE project_metadata SET value = ? WHERE key = ?') + .run(String(stamp), 'indexed_with_extraction_version'); + } + } finally { + db.close(); + } +} + +function indexState() { + const cg = CodeGraph.openSync(root); + try { + return { stale: cg.isIndexStale(), files: cg.getStats().fileCount }; + } finally { cg.close(); } +} + +beforeEach(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-stale-')); + fs.writeFileSync(path.join(root, 'original.ts'), 'export function original() { return 1; }\n'); + const cg = CodeGraph.initSync(root); + try { await cg.indexAll(); } finally { cg.close(); } +}); + +afterEach(() => fs.rmSync(root, { recursive: true, force: true })); + +describe('explicit CLI sync with stale extraction', () => { + for (const stamp of [EXTRACTION_VERSION - 1, null]) { + it(`warns for stamp ${stamp} and still syncs`, () => { + setStamp(stamp); + + const unchanged = run('sync'); + expect(unchanged.status).toBe(0); + expect(unchanged.stdout).toContain(WARNING); + expect(unchanged.stdout).toContain('No files changed'); + expect(unchanged.stdout).not.toContain('Already up to date'); + expect(unchanged.stdout).not.toContain('codegraph sync'); + + // The git hooks depend on this half: the changed file goes in. + fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n'); + const changed = run('sync'); + expect(changed.status).toBe(0); + expect(changed.stdout).toContain('Synced 1 changed files'); + expect(changed.stdout).toContain(WARNING); + // A sync can't stand in for the rebuild, so the warning keeps coming. + expect(indexState()).toEqual({ stale: true, files: 2 }); + }, 60_000); + + it(`stays silent and still syncs for stamp ${stamp} under --quiet`, () => { + setStamp(stamp); + fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n'); + const result = run('sync', '--quiet'); + expect(result.status).toBe(0); + expect(result.stdout + result.stderr).toBe(''); + expect(indexState()).toEqual({ stale: true, files: 2 }); + }, 60_000); + } + + it('does not warn about a current index', () => { + fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n'); + const changed = run('sync'); + expect(changed.status).toBe(0); + expect(changed.stdout).not.toContain(WARNING); + expect(indexState()).toEqual({ stale: false, files: 2 }); + + const unchanged = run('sync'); + expect(unchanged.status).toBe(0); + expect(unchanged.stdout).toContain('Already up to date'); + expect(unchanged.stdout).not.toContain(WARNING); + }, 60_000); + + it('stops warning after the full rebuild it names', () => { + setStamp(EXTRACTION_VERSION - 1); + expect(run('sync').stdout).toContain(WARNING); + expect(run('index').status).toBe(0); + const after = run('sync'); + expect(after.status).toBe(0); + expect(after.stdout).toContain('Already up to date'); + expect(after.stdout).not.toContain(WARNING); + }, 60_000); +}); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 7c53fa4b3..9f37d748b 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -1091,9 +1091,14 @@ program }).finally(() => progress.stop()); const totalChanges = result.filesAdded + result.filesModified + result.filesRemoved; + // Sync re-reads changed files only, so a file left alone keeps what an + // older engine extracted from it, and the index isn't up to date however + // little changed. It warns and carries on: the git hooks discard this + // output, so a refusal would quietly stop them after every upgrade. + const staleIndex = cg.isIndexStale(); if (totalChanges === 0 && !result.pendingRefsProcessed) { - clack.log.info('Already up to date'); + clack.log.info(staleIndex ? 'No files changed' : 'Already up to date'); } else if (totalChanges > 0) { clack.log.success(`Synced ${formatNumber(totalChanges)} changed files`); const details: string[] = []; @@ -1109,6 +1114,10 @@ program clack.log.info(`Resolved ${formatNumber(result.pendingRefsResolved ?? 0)} pending references${unresolved}`); } + if (staleIndex) { + clack.log.warn('This index was built by an older CodeGraph engine, and sync only re-reads changed files. Run "codegraph index" for a full rebuild.'); + } + clack.outro('Done'); } finally { cg.destroy();