diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs index fc9c8db791..ce9c278e80 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs @@ -1121,10 +1121,8 @@ pub(super) fn dashboard_code_graph_serving( /// /// Refused graph activation remains terminal for text serving, preserving the /// existing status behavior; strict dogfood can distinguish it from Ready via -/// the separate typed projection. Lane readiness is not seat identity: a -/// publication installs the replacement text owner before the serving swap -/// (`registry/mount.rs`), and search still answers the incumbent seat. -/// [`dashboard_terminal_status`] is what freshness reads use. +/// the separate typed projection. [`dashboard_terminal_status`] is what +/// freshness reads use. fn dashboard_generation_is_ready( latest: Option<&LatestCompleteCodeIndexV1>, text_ready: bool, @@ -1140,30 +1138,25 @@ fn dashboard_generation_is_ready( /// Whether the generation status advertises is the one search will serve. /// -/// `advertised_text_generation_id` is `None` when no text owner is installed; -/// freshness then names the serving seat, so there is no split to refuse. +/// Status identity is taken from the text owner, but a publication installs +/// the replacement text owner before the serving swap (`registry/mount.rs`), +/// and a graph-on search answers from the decoded seat. Until the seat catches +/// up, search serves the predecessor and the split is not terminal. A graph-off +/// mount serves the text owner directly and deliberately never seats the +/// decoded generation, so there is no seat to compare. pub(super) fn serving_seat_matches_advertised_generation( + graph_activation_enabled: bool, serving_generation_id: Option<&str>, advertised_text_generation_id: Option<&str>, ) -> bool { match advertised_text_generation_id { - None => true, - Some(advertised) => serving_generation_id == Some(advertised), + Some(advertised) if graph_activation_enabled => serving_generation_id == Some(advertised), + _ => true, } } -pub(super) fn serving_seat_matches_advertised_text_owner( - serving: Option<&LatestCompleteCodeIndexV1>, - text: Option<&LatestCodeTextGenerationV1>, -) -> bool { - serving_seat_matches_advertised_generation( - serving.map(|serving| serving.generation().manifest().generation_id.as_str()), - text.map(|text| text.metadata().manifest().generation_id.as_str()), - ) -} - -/// Terminal freshness: lane owners are ready and the serving seat is the -/// generation status will advertise. +/// Terminal freshness: lane owners are ready and search serves the generation +/// status advertises. pub(super) fn dashboard_terminal_status( latest: Option<&LatestCompleteCodeIndexV1>, text: Option<&LatestCodeTextGenerationV1>, @@ -1171,28 +1164,18 @@ pub(super) fn dashboard_terminal_status( graph_activation_enabled: bool, code_graph_serving: &Option, ) -> bool { - terminal_status_from_lanes( - dashboard_generation_is_ready( - latest, - text_ready, - graph_activation_enabled, - code_graph_serving, - ), - serving_seat_matches_advertised_text_owner(latest, text), + dashboard_generation_is_ready( + latest, + text_ready, + graph_activation_enabled, + code_graph_serving, + ) && serving_seat_matches_advertised_generation( + graph_activation_enabled, + latest.map(|latest| latest.generation().manifest().generation_id.as_str()), + text.map(|text| text.metadata().manifest().generation_id.as_str()), ) } -/// Lane readiness and seat identity are one freshness verdict. -/// -/// The graph-rebuild receipt had ready lanes and a serving seat that still -/// held the predecessor. That combination is not terminal. -pub(super) fn terminal_status_from_lanes( - lanes_ready: bool, - serving_matches_advertised: bool, -) -> bool { - lanes_ready && serving_matches_advertised -} - fn dashboard_text_freshness_identity( latest: Option<&LatestCodeTextGenerationV1>, ) -> tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1 { diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs index d47bf06e5f..9b2094741c 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs @@ -13,7 +13,7 @@ use tracedecay_domain::ProjectId; use super::super::graph_activation::install_injected_activation_gate; use super::{ CodeIndexCadenceOutcomeV1, CodeIndexSchedulerRegistryV1, dashboard_generation_is_ready, - serving_seat_matches_advertised_generation, terminal_status_from_lanes, + serving_seat_matches_advertised_generation, }; /// Failure bound on an owner pass finishing once the worker is parked. Nothing @@ -55,41 +55,52 @@ fn dashboard_ready_requires_text_and_graph_lane_owners() { /// The graph-rebuild receipt (transport acceptance, both ~90s attempts): lane /// owners were ready on the replacement text generation while search still -/// served the predecessor. Status must not call that split terminal. +/// served the predecessor seat. Status must not call that split terminal. #[test] fn graph_rebuild_split_is_not_terminal_freshness() { - let lanes_ready = - dashboard_generation_is_ready(None, true, true, &Some(CodeGraphServingReadinessV1::Ready)); assert!( - lanes_ready, + dashboard_generation_is_ready(None, true, true, &Some(CodeGraphServingReadinessV1::Ready)), "the receipt's text owner and graph activation were ready" ); assert!( !serving_seat_matches_advertised_generation( + true, Some("generation.predecessor"), Some("generation.head"), ), "search served the predecessor while status advertised the head" ); assert!( - !serving_seat_matches_advertised_generation(None, Some("generation.head")), + !serving_seat_matches_advertised_generation(true, None, Some("generation.head")), "a text owner installed before the serving swap is not yet the served generation" ); assert!(serving_seat_matches_advertised_generation( + true, Some("generation.head"), Some("generation.head"), )); assert!(serving_seat_matches_advertised_generation( + true, Some("generation.head"), None, )); - assert!(serving_seat_matches_advertised_generation(None, None)); - assert!( - !terminal_status_from_lanes(lanes_ready, false), - "ready lanes must not be terminal while search serves the predecessor" - ); - assert!(terminal_status_from_lanes(lanes_ready, true)); - assert!(!terminal_status_from_lanes(false, true)); + assert!(serving_seat_matches_advertised_generation(true, None, None)); +} + +/// A graph-off mount serves its text owner directly and never seats the +/// decoded generation; the empty seat is not a lagging seat. +#[test] +fn graph_off_text_owner_is_terminal_without_a_seat() { + assert!(serving_seat_matches_advertised_generation( + false, + None, + Some("generation.head"), + )); + assert!(serving_seat_matches_advertised_generation( + false, + Some("generation.predecessor"), + Some("generation.head"), + )); } /// Park the background worker and wait out whatever pass is already in flight.