diff --git a/Cargo.lock b/Cargo.lock index 9feb0b91..610492c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,7 +66,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "codegraph-core" -version = "3.9.1" +version = "3.9.3" dependencies = [ "ignore", "napi", diff --git a/crates/codegraph-core/src/build_pipeline.rs b/crates/codegraph-core/src/build_pipeline.rs index 53a58f14..1512560f 100644 --- a/crates/codegraph-core/src/build_pipeline.rs +++ b/crates/codegraph-core/src/build_pipeline.rs @@ -723,7 +723,11 @@ fn check_version_mismatch(conn: &Connection) -> bool { return true; } } - if let Some(prev_version) = get_meta("codegraph_version") { + // Compare against engine_version (the addon's own version), not + // codegraph_version (the npm package version). The JS post-processing + // overwrites codegraph_version with the npm version, which may differ + // from CARGO_PKG_VERSION — causing a perpetual full-rebuild loop (#928). + if let Some(prev_version) = get_meta("engine_version") { if prev_version != current_version { return true; } diff --git a/src/domain/graph/builder/pipeline.ts b/src/domain/graph/builder/pipeline.ts index 0d85751b..96eee63c 100644 --- a/src/domain/graph/builder/pipeline.ts +++ b/src/domain/graph/builder/pipeline.ts @@ -88,12 +88,20 @@ function checkEngineSchemaMismatch(ctx: PipelineContext): void { ); ctx.forceFullRebuild = true; } - const prevVersion = meta('codegraph_version'); - if (prevVersion && prevVersion !== CODEGRAPH_VERSION) { - info( - `Codegraph version changed (${prevVersion} → ${CODEGRAPH_VERSION}), promoting to full rebuild.`, - ); - ctx.forceFullRebuild = true; + // When the native orchestrator is available, it handles its own version + // check (engine_version vs CARGO_PKG_VERSION). The JS-side codegraph_version + // may differ from the Rust addon version (npm 3.9.3 vs addon 3.9.2), so + // checking it here would force unnecessary full rebuilds and prevent the + // native orchestrator from ever running its fast incremental path (#928). + // Only check codegraph_version when falling through to the JS pipeline. + if (!ctx.nativeAvailable) { + const prevVersion = meta('codegraph_version'); + if (prevVersion && prevVersion !== CODEGRAPH_VERSION) { + info( + `Codegraph version changed (${prevVersion} → ${CODEGRAPH_VERSION}), promoting to full rebuild.`, + ); + ctx.forceFullRebuild = true; + } } } @@ -630,15 +638,16 @@ async function tryNativeOrchestrator( const p = result.phases; - // Sync build_meta so JS-side version/engine checks work on next build. + // The Rust orchestrator already persists engine, engine_version, + // codegraph_version, node_count, edge_count, and last_build. + // Only write fields Rust doesn't set, plus built_at for JS callers. + // IMPORTANT: Do NOT overwrite codegraph_version here — Rust writes + // CARGO_PKG_VERSION and check_version_mismatch compares against it. + // Overwriting with CODEGRAPH_VERSION (npm version) when they differ + // causes a perpetual full-rebuild loop (#928). setBuildMeta(ctx.db, { - engine: ctx.engineName, - engine_version: ctx.engineVersion || '', - codegraph_version: CODEGRAPH_VERSION, schema_version: String(ctx.schemaVersion), built_at: new Date().toISOString(), - node_count: String(result.nodeCount ?? 0), - edge_count: String(result.edgeCount ?? 0), }); info( @@ -816,7 +825,19 @@ export async function buildGraph( if (nativeResult) return nativeResult; } catch (err) { warn(`Native build orchestrator failed, falling back to JS pipeline: ${toErrorMessage(err)}`); - // Fall through to JS pipeline + // The version gate in checkEngineSchemaMismatch was skipped because + // nativeAvailable was true. Now that we're falling back to the JS + // pipeline, perform the codegraph_version check here so a version + // bump still promotes to a full rebuild (#928). + if (ctx.incremental && !ctx.forceFullRebuild) { + const prevVersion = getBuildMeta(ctx.db, 'codegraph_version'); + if (prevVersion && prevVersion !== CODEGRAPH_VERSION) { + info( + `Codegraph version changed (${prevVersion} → ${CODEGRAPH_VERSION}), promoting to full rebuild.`, + ); + ctx.forceFullRebuild = true; + } + } } await runPipelineStages(ctx); diff --git a/src/domain/graph/builder/stages/finalize.ts b/src/domain/graph/builder/stages/finalize.ts index 857bbf40..73e4e811 100644 --- a/src/domain/graph/builder/stages/finalize.ts +++ b/src/domain/graph/builder/stages/finalize.ts @@ -82,11 +82,16 @@ function persistBuildMetadata( const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb; if (!ctx.isFullBuild && ctx.allSymbols.size <= 3) return; try { + // engine_version must be the actual engine version (addon version for native, + // npm version for WASM) — not always CODEGRAPH_VERSION. The Rust orchestrator's + // check_version_mismatch compares engine_version against CARGO_PKG_VERSION, + // so writing the npm version here would cause perpetual full rebuilds (#928). + const engineVer = ctx.engineVersion || CODEGRAPH_VERSION; if (useNativeDb) { ctx.nativeDb!.setBuildMeta( Object.entries({ engine: ctx.engineName, - engine_version: CODEGRAPH_VERSION, + engine_version: engineVer, codegraph_version: CODEGRAPH_VERSION, schema_version: String(ctx.schemaVersion), built_at: buildNow.toISOString(), @@ -97,7 +102,7 @@ function persistBuildMetadata( } else { setBuildMeta(ctx.db, { engine: ctx.engineName, - engine_version: CODEGRAPH_VERSION, + engine_version: engineVer, codegraph_version: CODEGRAPH_VERSION, schema_version: String(ctx.schemaVersion), built_at: buildNow.toISOString(), diff --git a/tests/integration/build.test.ts b/tests/integration/build.test.ts index 218d90c8..e75ae447 100644 --- a/tests/integration/build.test.ts +++ b/tests/integration/build.test.ts @@ -490,17 +490,18 @@ describe('version/engine mismatch auto-promotes to full rebuild', () => { ); db2.close(); - // codegraph_version must match the npm package version - const pkg = JSON.parse( - fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8'), - ); - expect(meta.codegraph_version).toBe(pkg.version); - // engine must be either 'native' or 'wasm' (not empty, not stale) expect(['native', 'wasm']).toContain(meta.engine); - // engine_version must equal the npm package version (#751: was using Rust crate version) - expect(meta.engine_version).toBe(pkg.version); + // engine_version must be a valid semver string. For native engine, this is + // the addon version (CARGO_PKG_VERSION); for WASM, the npm package version. + // They may differ when the addon hasn't been republished yet (#928). + expect(meta.engine_version).toMatch(/^\d+\.\d+\.\d+$/); + + // codegraph_version tracks the npm package version when the JS pipeline + // finalizes, or the addon version when the Rust orchestrator runs. + // Either is valid — the key invariant is it's a valid semver string. + expect(meta.codegraph_version).toMatch(/^\d+\.\d+\.\d+$/); // built_at must be a valid ISO timestamp from the current build expect(new Date(meta.built_at).getTime()).toBeGreaterThan(0);