From c6aadcb57790482fb3e76a19c73265359e8fd197 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 06:30:28 +0000 Subject: [PATCH 1/3] fix(code-index): keep sealed generation when proof expires A seal or clone backfill outlives the 30s freshness window. Expiry and a predecessor witness used to clear the newer generation and reseal it. Unchanged sealed bytes now rebind that proof instead. Co-authored-by: Zack Jackson --- .../src/code_index_scheduler/reconcile.rs | 183 +++++++++++++++--- .../code_index_scheduler/registry/mount.rs | 29 +-- .../code_index_scheduler/tests/reconcile.rs | 133 +++++++++++++ 3 files changed, 299 insertions(+), 46 deletions(-) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs index c2c7eecdde..878c9b53e9 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs @@ -608,6 +608,37 @@ impl SourceFreshnessFenceV1 { }) && self.snapshot_is_recently_verified(&state, project_root, shutting_down) } + + /// Whether the last completed proof was sealed from exactly this snapshot. + /// + /// Clock age is not part of the answer. A seal or clone backfill can + /// outlive the admission window without the snapshot changing identity. + pub(super) fn proof_describes_snapshot( + &self, + snapshot_content_identity: &ContentDigest, + ) -> bool { + let state = self.snapshot(); + state.verified_against_source + && state.source_witness.as_ref().is_some_and(|witness| { + witness + .content_manifest + .describes_snapshot(snapshot_content_identity) + }) + } + + /// Refresh the admission clock and the git-metadata sample after the + /// sealed digests still matched. The content witness and reconciled + /// epoch stay put: this is the same proof, not a new generation. + fn rebind_admission_clock(&self, git_metadata: identity::GitMetadataFingerprintV1) { + let micros = now_micros().0; + let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner); + state.git_metadata = git_metadata; + state.last_reconciled_at = Instant::now(); + state.verified_against_source = true; + state.freshness_unknown = false; + self.last_reconciled_at_micros + .store(micros, Ordering::Release); + } } /// What the cheap Git/stat freshness ladder concluded about the retained @@ -1737,6 +1768,45 @@ impl CodeIndexWorktreeSchedulerV1 { Ok(Some(outcome)) } + /// Record that `metadata` is the generation the live worktree still seals. + /// + /// The in-memory fence takes this snapshot. The disk witness, when one + /// exists, is rewritten to this generation id so the next open does not + /// treat the predecessor's proof as a reason to drop it and reseal. + fn accept_unchanged_sealed_snapshot( + &mut self, + metadata: &VerifiedSealedTextGenerationMetadataV1, + git_metadata: identity::GitMetadataFingerprintV1, + stat_signature: String, + source_manifest: SourceContentManifestV1, + prior_witness: Option<&RestoreFreshnessWitnessV1>, + ) -> CodeIndexReconcileOutcomeV1 { + let snapshot_content_identity = metadata.snapshot().content_identity.clone(); + self.latest_content_identity = Some(snapshot_content_identity.clone()); + self.mark_reconciled_retained_generation_state( + git_metadata.clone(), + Some(ReconciledSourceWitnessV1 { + stat_signature: stat_signature.clone(), + content_manifest: source_manifest, + }), + ); + if let Some(prior) = prior_witness { + RestoreFreshnessWitnessV1 { + generation_id: metadata.manifest().generation_id.as_str().to_owned(), + git_metadata_signature: git_metadata.stable_signature(), + stat_signature, + repository_parse_identity_digest: prior.repository_parse_identity_digest.clone(), + ignored_source_admissions_digest: prior.ignored_source_admissions_digest.clone(), + ignored_source_paths: Vec::new(), + } + .persist(&self.store_root); + } + CodeIndexReconcileOutcomeV1::Noop(CodeIndexNoopEvidenceV1 { + snapshot_content_identity, + overflow_reconciled: false, + }) + } + pub(super) fn reconcile_retained_text_generation_with( &mut self, metadata: &VerifiedSealedTextGenerationMetadataV1, @@ -1758,10 +1828,14 @@ impl CodeIndexWorktreeSchedulerV1 { .observe_retained_text_compatibility(metadata) .is_reusable(); let witness = RestoreFreshnessWitnessV1::load(&self.store_root); - if witness.as_ref().is_some_and(|witness| { - witness.generation_id != metadata.manifest().generation_id.as_str() - || !witness.ignored_source_paths.is_empty() - }) || !self.ignored_source_admissions.is_empty() + // A predecessor freshness witness is not a reason to drop this + // generation. It names the proof that sealed an earlier snapshot. + // Ignored-source rosters still require the complete capture: their + // digest is not the ordinary file manifest this path compares. + if witness + .as_ref() + .is_some_and(|witness| !witness.ignored_source_paths.is_empty()) + || !self.ignored_source_admissions.is_empty() { return Ok(None); } @@ -1788,37 +1862,36 @@ impl CodeIndexWorktreeSchedulerV1 { // generation's sealed file digests; its matching stat signature is // the negative cache that lets a moved tree skip the byte comparison. let source_manifest = SourceContentManifestV1::for_snapshot(metadata.snapshot()); - if retained_is_reusable + let sealed_bytes_match = retained_is_reusable && !has_hints - && let Some(witness) = witness.as_ref() - && witness.git_metadata_signature == sampled_metadata.stable_signature() - && witness.stat_signature == sampled_sweep.signature && sampled_sweep.content_matches( &self.project_root, &source_manifest, &self.shutting_down, - ) - { - let snapshot_content_identity = metadata.snapshot().content_identity.clone(); - self.latest_content_identity = Some(snapshot_content_identity.clone()); - self.mark_reconciled_retained_generation_state( - sampled_metadata, - Some(ReconciledSourceWitnessV1 { - stat_signature: sampled_sweep.signature, - content_manifest: source_manifest, - }), ); - return Ok(Some(CodeIndexReconcileOutcomeV1::Noop( - CodeIndexNoopEvidenceV1 { - snapshot_content_identity, - overflow_reconciled: false, - }, + let quiet_witness = sealed_bytes_match + && witness.as_ref().is_some_and(|witness| { + witness.git_metadata_signature == sampled_metadata.stable_signature() + && witness.stat_signature == sampled_sweep.signature + }); + // Graph-on refuses to decode the sealed generation just because the + // predecessor witness, or a git-index mtime this seal itself moved, + // does not name this generation. The sealed digests are the proof. + // Graph-off still captures so a metadata-only drift is verified + // without a full decode when the quiet witness is absent. + if sealed_bytes_match && (quiet_witness || !rebuild_changed_source_without_decode) { + return Ok(Some(self.accept_unchanged_sealed_snapshot( + metadata, + sampled_metadata, + sampled_sweep.signature, + source_manifest, + witness.as_ref(), ))); } - // A compatible generation whose witness did not prove a quiet tree - // falls through to the full graph-on reconcile. An incompatible - // lightweight owner rebuilds here without decoding the retained graph. + // A compatible generation whose bytes moved falls through to the full + // graph-on reconcile. An incompatible lightweight owner rebuilds here + // without decoding the retained graph. if retained_is_reusable && !rebuild_changed_source_without_decode { return Ok(None); } @@ -2813,6 +2886,51 @@ impl CodeIndexWorktreeSchedulerV1 { .source_currency_witness_for(generation_id, snapshot_content_identity) } + /// Bind a sealed snapshot to the source proof, renewing an expired clock + /// when the sealed digests still match. + /// + /// The admission window is 30s. A graph seal and the clone-fingerprint + /// backfill both outlive it under load. Treating that expiry as "this + /// generation is not the proof" cleared the serving witness and the next + /// pass resealed the same snapshot. A hook epoch or a digest mismatch + /// still refuses; only an unchanged sealed snapshot keeps its generation. + pub(super) fn currency_witness_for_sealed_snapshot( + &self, + generation_id: &CodeGenerationId, + snapshot_content_identity: &ContentDigest, + ) -> Option { + if self.shutting_down.load(Ordering::Acquire) { + return None; + } + if self.freshness_fence.serves_recently_verified_source( + snapshot_content_identity, + &self.project_root, + &self.shutting_down, + ) { + return self + .freshness_fence + .source_currency_witness_for(generation_id, snapshot_content_identity); + } + if !self + .freshness_fence + .proof_describes_snapshot(snapshot_content_identity) + || self.freshness_fence.source_change_pending() + { + return None; + } + let freshness = self.freshness_fence.snapshot(); + if !self.source_witness_matches_worktree(&freshness) { + return None; + } + // Sample after the walk. `gix::open` inside the digest comparison can + // move index metadata; storing the post-walk sample is what keeps the + // next probe from calling that side effect a new generation. + let git_metadata = identity::GitMetadataFingerprintV1::capture(&self.project_root); + self.freshness_fence.rebind_admission_clock(git_metadata); + self.freshness_fence + .source_currency_witness_for(generation_id, snapshot_content_identity) + } + /// A cheap stat-level (path, mtime, size) signature of the present source /// candidates. It opens gix and runs stat-based status (no byte reads, no /// content hashing). A changed signature skips straight to reconcile; an @@ -3252,6 +3370,19 @@ impl CodeIndexWorktreeSchedulerV1 { self.publication.sealed_decode_count() } + /// Age the admission clock past its own threshold without touching source. + #[cfg(test)] + pub(super) fn expire_source_proof_for_test(&self) { + let mut state = self + .freshness_fence + .state + .lock() + .unwrap_or_else(PoisonError::into_inner); + state.last_reconciled_at = Instant::now() + .checked_sub(state.staleness_threshold + Duration::from_secs(1)) + .unwrap_or_else(Instant::now); + } + #[cfg(any(test, feature = "test-helpers"))] pub fn poison_decoded_publication_cache_for_test(&self) { self.publication.poison_decoded_cache_for_test(); diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs index ac239ce49c..0a726b2573 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs @@ -1801,8 +1801,6 @@ impl CodeIndexSchedulerRegistryV1 { let text_generation = Arc::clone(&worker_text_generation); let serving_seats = Arc::clone(&worker_serving_seats); let serving_generation_changed = worker_serving_generation_changed.clone(); - let source_freshness = worker_source_freshness.clone(); - let project_root = worker_project_root.clone(); let text_latest = latest.clone(); let latest = latest.clone(); let shutting_down = Arc::clone(&worker_shutting_down); @@ -1836,13 +1834,14 @@ impl CodeIndexSchedulerRegistryV1 { // proofs to the seat. Asking the fence whether it // has verified *this* sealed snapshot is what makes // the binding truthful for a seat this pass did not - // publish. - let pass_proves_latest = source_freshness - .serves_recently_verified_source( - &latest.generation().snapshot().content_identity, - &project_root, - &shutting_down, - ); + // publish. An expired clock, or a git-index sample + // this seal moved, is not a different snapshot: + // dropping the witness here is how a newer + // generation stayed unserved through clone backfill. + let sealed_currency = scheduler.currency_witness_for_sealed_snapshot( + &latest.generation().manifest().generation_id, + &latest.generation().snapshot().content_identity, + ); let mut serving = serving_generation .write() .unwrap_or_else(std::sync::PoisonError::into_inner); @@ -1883,17 +1882,7 @@ impl CodeIndexSchedulerRegistryV1 { *serving_source_witness .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = - pass_proves_latest - .then(|| { - source_freshness.source_currency_witness_for( - &latest.generation().manifest().generation_id, - &latest - .generation() - .snapshot() - .content_identity, - ) - }) - .flatten(); + sealed_currency; } // The durable pointer names a successor, so no // proof of this seat's currency exists to bind. diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs index 188d869742..5a3e412f32 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs @@ -8173,6 +8173,139 @@ fn graph_off_stale_witness_reconciles_unchanged_source_without_full_decode() { ); } +/// The disk freshness witness names whichever generation last persisted it. +/// A later seal of the same bytes used to return `None` the moment that id +/// disagreed, and the graph-on caller then decoded and resealed. Under load +/// that reseal outlived the admission window, the swap cleared the witness, +/// and the newer generation never became current. Unchanged sealed bytes +/// keep the generation and rewrite the witness onto it. Moved bytes still +/// refuse, without publishing a substitute. +#[test] +fn predecessor_freshness_witness_keeps_the_sealed_generation() { + let fixture = GitFixture::new(ALPHA_LIB_V1); + let store = TempDir::new().expect("store root"); + let mut scheduler = scheduler( + &fixture, + store.path().to_path_buf(), + Arc::new(SharedCodeIndexBytePoolV1::default()), + ); + let seeded = published(scheduler.reconcile_now().expect("seed retained generation")); + let metadata = scheduler + .servable_retained_text_generation() + .expect("publication store") + .expect("authenticated retained text generation") + .metadata() + .clone(); + let generation_id = metadata.manifest().generation_id.clone(); + let mut witness = + RestoreFreshnessWitnessV1::load(store.path()).expect("the seal persisted a proof"); + assert_eq!(witness.generation_id, generation_id.as_str()); + witness.generation_id = "generation.predecessor".to_owned(); + witness.persist(store.path()); + let index_path = fixture.path().join(".git/index"); + let index_mtime = std::fs::metadata(&index_path) + .expect("git index metadata") + .modified() + .expect("git index mtime"); + filetime::set_file_mtime( + &index_path, + filetime::FileTime::from_system_time(index_mtime + Duration::from_secs(2)), + ) + .expect("advance only the git index mtime"); + + let decodes_before = scheduler.sealed_decode_count(); + let outcome = scheduler + .reconcile_retained_text_generation_with(&metadata, false) + .expect("graph-on retained reconcile") + .expect("unchanged sealed bytes must not be dropped"); + let CodeIndexReconcileOutcomeV1::Noop(evidence) = outcome else { + panic!("predecessor proof must not reseal the same snapshot: {outcome:?}"); + }; + assert_eq!( + evidence.snapshot_content_identity, seeded.snapshot_content_identity, + "the noop names the generation that was already sealed" + ); + assert_eq!( + scheduler.sealed_decode_count(), + decodes_before, + "keeping the sealed generation must not decode it again" + ); + assert_eq!( + RestoreFreshnessWitnessV1::load(store.path()) + .expect("rebound proof") + .generation_id, + generation_id.as_str(), + "the disk proof must name the sealed generation, not the predecessor" + ); + assert_eq!( + scheduler + .source_currency_witness_for(&generation_id, &metadata.snapshot().content_identity,) + .map(|witness| witness.generation_id), + Some(generation_id.clone()), + "the in-memory proof must admit the sealed generation" + ); + + fixture.edit( + "src/lib.rs", + "pub fn changed_after_predecessor_proof() -> u32 { 2 }\n", + ); + let refused = scheduler + .reconcile_retained_text_generation_with(&metadata, false) + .expect("changed source is a typed refusal, not an error"); + assert!( + refused.is_none(), + "moved bytes must not keep the sealed generation: {refused:?}" + ); + assert_eq!( + scheduler + .publication + .read_publication_pointer() + .expect("read pointer") + .expect("active pointer") + .generation_id, + generation_id.as_str(), + "refusing the moved bytes must not publish a substitute generation" + ); +} + +/// Clone backfill and the seal itself outlive the 30s admission window. Expiry +/// is a request to re-check the sealed digests, not a reason to drop the +/// generation those digests already name. A byte change after expiry still drops it. +#[test] +fn expired_proof_keeps_the_sealed_generation_until_bytes_move() { + let fixture = GitFixture::new(ALPHA_LIB_V1); + let store = TempDir::new().expect("store root"); + let mut scheduler = scheduler( + &fixture, + store.path().to_path_buf(), + Arc::new(SharedCodeIndexBytePoolV1::default()), + ); + let seeded = published(scheduler.reconcile_now().expect("seed retained generation")); + scheduler.expire_source_proof_for_test(); + assert_eq!( + scheduler + .currency_witness_for_sealed_snapshot( + &seeded.generation_id, + &seeded.snapshot_content_identity, + ) + .map(|witness| witness.generation_id), + Some(seeded.generation_id.clone()), + "an expired proof must keep the generation whose sealed bytes still match" + ); + + fixture.edit("src/lib.rs", "pub fn alpha() -> u32 { 9 }\n"); + scheduler.expire_source_proof_for_test(); + assert!( + scheduler + .currency_witness_for_sealed_snapshot( + &seeded.generation_id, + &seeded.snapshot_content_identity, + ) + .is_none(), + "an expired proof must drop the generation once its sealed bytes moved" + ); +} + /// A query freshness probe against a restored owner that no pass has verified /// yet must report "not current", the restart's first pass is still the /// remedy, without minting an observed source change: no overflow hint and no From 08c3f3c24dc7199cbdaaa67bbab99238221c820c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 06:46:30 +0000 Subject: [PATCH 2/3] docs(code-index): leave clone copy out of witness rebind The seat-swap witness does not own the lexical full-copy. That copy stays on the retained successor driver. Co-authored-by: Zack Jackson --- .../src/code_index_scheduler/reconcile.rs | 9 ++++----- .../src/code_index_scheduler/registry/mount.rs | 7 ++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs index 878c9b53e9..2ba6cbacf4 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs @@ -2889,11 +2889,10 @@ impl CodeIndexWorktreeSchedulerV1 { /// Bind a sealed snapshot to the source proof, renewing an expired clock /// when the sealed digests still match. /// - /// The admission window is 30s. A graph seal and the clone-fingerprint - /// backfill both outlive it under load. Treating that expiry as "this - /// generation is not the proof" cleared the serving witness and the next - /// pass resealed the same snapshot. A hook epoch or a digest mismatch - /// still refuses; only an unchanged sealed snapshot keeps its generation. + /// The admission window is 30s. This does not move the clone-successor + /// copy off the publication advance. It only stops an expired clock, or a + /// predecessor disk witness, from clearing the generation those digests + /// already name. A hook epoch or a digest mismatch still refuses. pub(super) fn currency_witness_for_sealed_snapshot( &self, generation_id: &CodeGenerationId, diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs index 0a726b2573..3d91e3a619 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs @@ -1835,9 +1835,10 @@ impl CodeIndexSchedulerRegistryV1 { // has verified *this* sealed snapshot is what makes // the binding truthful for a seat this pass did not // publish. An expired clock, or a git-index sample - // this seal moved, is not a different snapshot: - // dropping the witness here is how a newer - // generation stayed unserved through clone backfill. + // this seal moved, is not a different snapshot. + // Dropping the witness here cleared the newer + // generation. The lexical full-copy is not decided + // on this swap. let sealed_currency = scheduler.currency_witness_for_sealed_snapshot( &latest.generation().manifest().generation_id, &latest.generation().snapshot().content_identity, From c1633dd96bb0f70d89d5a33cc64c3d5c35d71dfa Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 19 Sep 2026 07:54:37 +0000 Subject: [PATCH 3/3] fix(code-index): refuse a sealed generation the checkout moved past Dropping the witness generation-id guard let the graph-on retained reconcile return `Noop` whenever the sealed file digests still matched, including after a commit or branch switch that touches no indexed byte (an empty or docs-only commit). `accept_unchanged_sealed_snapshot` then persisted the new git-metadata sample onto the retained generation's witness, so the stale `reference`/`source_revision` attribution stayed masked until code bytes moved. `finish_retained_reconcile` rebuilds on exactly that drift, and `branch_generations` resolves generations by the commit they sealed, so the retained generation must not outlive it. Gate the accept on the attribution a fresh capture would seal: HEAD's ref must still match the snapshot's, and a snapshot that sealed a revision must still name HEAD's commit. `self.identity` is re-resolved a few lines above, so this adds no walk. A snapshot sealed from a dirty tree carries no revision and keeps the fast path. Verified by `a_moved_commit_refuses_the_sealed_generation_despite_identical_bytes`, which fails on the parent commit with `Some(Noop(..))`. Co-Authored-By: Claude Fable 5.1 --- .../src/code_index_scheduler/reconcile.rs | 20 +++++- .../code_index_scheduler/tests/reconcile.rs | 63 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs index 2ba6cbacf4..b686c00dc6 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs @@ -1874,12 +1874,30 @@ impl CodeIndexWorktreeSchedulerV1 { witness.git_metadata_signature == sampled_metadata.stable_signature() && witness.stat_signature == sampled_sweep.signature }); + // Identical source bytes do not make a moved commit or branch the same + // generation. `finish_retained_reconcile` rebuilds on exactly this + // drift, and branch-scoped reads resolve generations by their sealed + // `source_revision`, so accepting here would leave the retained + // generation attributed to a commit the checkout has left for as long + // as the bytes hold still. `self.identity` was re-resolved above, so + // this costs no extra walk. A snapshot sealed without a revision + // (a dirty capture) has no commit attribution to invalidate. + let sealed_attribution_is_current = metadata.snapshot().reference.as_ref() + == self.identity.head_ref() + && metadata + .snapshot() + .source_revision + .as_ref() + .is_none_or(|sealed| self.identity.head_commit() == Some(sealed)); // Graph-on refuses to decode the sealed generation just because the // predecessor witness, or a git-index mtime this seal itself moved, // does not name this generation. The sealed digests are the proof. // Graph-off still captures so a metadata-only drift is verified // without a full decode when the quiet witness is absent. - if sealed_bytes_match && (quiet_witness || !rebuild_changed_source_without_decode) { + if sealed_bytes_match + && sealed_attribution_is_current + && (quiet_witness || !rebuild_changed_source_without_decode) + { return Ok(Some(self.accept_unchanged_sealed_snapshot( metadata, sampled_metadata, diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs index 5a3e412f32..da03d8b338 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs @@ -10510,3 +10510,66 @@ fn serving_swap_seats_a_generation_whose_publication_moved_while_it_activated() "neither refusing arm writes the serving slot" ); } + +/// Unchanged source bytes are not a reason to keep a generation the checkout +/// has committed past. An empty (or docs-only) commit moves HEAD without +/// touching one indexed byte, and `finish_retained_reconcile` rebuilds on +/// exactly that `source_revision` drift because branch-scoped reads resolve +/// generations by the commit they sealed. Accepting the sealed snapshot here +/// would pin the stale attribution for as long as the bytes hold still. +#[test] +fn a_moved_commit_refuses_the_sealed_generation_despite_identical_bytes() { + let fixture = GitFixture::new(ALPHA_LIB_V1); + let store = TempDir::new().expect("store root"); + let mut scheduler = scheduler( + &fixture, + store.path().to_path_buf(), + Arc::new(SharedCodeIndexBytePoolV1::default()), + ); + published(scheduler.reconcile_now().expect("seed retained generation")); + let metadata = scheduler + .servable_retained_text_generation() + .expect("publication store") + .expect("authenticated retained text generation") + .metadata() + .clone(); + let sealed_revision = metadata + .snapshot() + .source_revision + .clone() + .expect("a clean seed seals its commit"); + git( + fixture.path(), + &["commit", "-qm", "docs only", "--allow-empty"], + ); + let moved_head = + CommitId::new(git_stdout(fixture.path(), &["rev-parse", "HEAD"])).expect("moved HEAD"); + assert_ne!(sealed_revision, moved_head, "the fixture must move HEAD"); + + let refused = scheduler + .reconcile_retained_text_generation_with(&metadata, false) + .expect("graph-on retained reconcile"); + assert!( + refused.is_none(), + "a moved commit must not keep the generation sealed at {sealed_revision:?}: {refused:?}" + ); + + // The refusal is what hands the pass to the authoritative capture, and + // that capture is what re-attributes the generation to the new commit. + published( + scheduler + .reconcile_now() + .expect("rebuild at the moved commit"), + ); + assert_eq!( + scheduler + .servable_retained_text_generation() + .expect("publication store") + .expect("authenticated retained text generation") + .metadata() + .snapshot() + .source_revision, + Some(moved_head), + "the rebuilt generation must name the commit the checkout is on" + ); +}