diff --git a/gitnexus/src/core/run-analyze.ts b/gitnexus/src/core/run-analyze.ts index a03122c2e..b144065a8 100644 --- a/gitnexus/src/core/run-analyze.ts +++ b/gitnexus/src/core/run-analyze.ts @@ -4614,9 +4614,11 @@ async function runFullAnalysisInner( // re-read the on-disk meta immediately before writing (the shape the // /api/embed checkpoint writer in server/api.ts already uses, which also // keeps a concurrent writer's update from being reverted by a stale - // snapshot) and replace ONLY `embeddingCheckpoint`. In-place writers - // can also publish a measured live count; staged writers keep it in - // memory until the replacement is published. + // snapshot) and replace ONLY `embeddingCheckpoint` — plus + // `stats.embeddings` when the caller actually MEASURED the published + // index (the post-window `onCheckpoint` on an in-place build). A staging + // build's count is not published until the atomic swap succeeds. The + // window-start callback passes nothing, preserving the latest count. const saveEmbeddingCheckpoint = async ( checkpoint: { nodesProcessed: number; @@ -4888,9 +4890,8 @@ async function runFullAnalysisInner( // already written to disk: prior meta says 0, a clean run inserts // embeddings and checkpoints the real count, the final probe is // unavailable, and finalization carries the stale 0 forward while reporting - // success. `loadMeta` never throws (it returns null), and the checkpoint - // writer already re-reads the same way, so this is the same freshness - // discipline applied to the same field. + // success. For a staged build, use its last measured count only in the + // final meta, written after the swap; never publish it at a checkpoint. const latestMetaForCount = embeddingCount === undefined ? ((await loadMeta(metaDir)) ?? existingMeta) : undefined; const persistedEmbeddingCount = resolvePersistedEmbeddingCount( diff --git a/gitnexus/test/unit/run-analyze-fts-repair.test.ts b/gitnexus/test/unit/run-analyze-fts-repair.test.ts index 6ebfd3c6f..5d616cf30 100644 --- a/gitnexus/test/unit/run-analyze-fts-repair.test.ts +++ b/gitnexus/test/unit/run-analyze-fts-repair.test.ts @@ -2321,15 +2321,14 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => { nodeIds: ['node-1', 'node-2'], }); snapshots.windowStart = await loadMeta(storagePath); - // Post-window checkpoint — this one MEASURED the live count. + // Post-window checkpoint measures staging, not the published DB. await pipelineOptions.onCheckpoint?.({ nodesProcessed: 2, totalNodes: 4, chunksProcessed: 4, }); snapshots.postWindow = await loadMeta(storagePath); - // Window 2 — the old code restated the PREVIOUS run's count here and - // clobbered the live figure the post-window save had just written. + // Window 2 must retain the published count too. await pipelineOptions.onCheckpointWindowStart?.({ nodesProcessed: 2, totalNodes: 4, @@ -2381,7 +2380,7 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => { }); expect(snapshots.windowStart?.lastCommit).not.toBe(currentCommit); - // The measured count belongs to the unpublished staging generation. + // ── Post-window: the staged count is not published yet ───────────── expect(snapshots.postWindow).toMatchObject({ lastCommit: STALE_COMMIT, fileHashes: STALE_HASHES, @@ -2394,7 +2393,7 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => { unsafeNodeIds: [], }); - // ── Window 2: no stale restatement over the measured figure ──────── + // ── Window 2: the published count remains unchanged ──────────────── expect(snapshots.secondWindow).toMatchObject({ lastCommit: STALE_COMMIT, stats: { embeddings: 7 }, @@ -2409,7 +2408,7 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => { const finalMeta = JSON.parse( await fs.readFile(`${storagePath}/meta.json`, 'utf-8'), ) as RepoMeta; - expect(finalMeta).toMatchObject({ lastCommit: currentCommit }); + expect(finalMeta).toMatchObject({ lastCommit: currentCommit, stats: { embeddings: 42 } }); expect(finalMeta.embeddingCheckpoint).toBeUndefined(); expect(finalMeta.incrementalInProgress).toBeUndefined(); } finally {