From 77e732550f8ae63fe15301e6bce439a6fca2ccc6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 01:48:55 +0000 Subject: [PATCH 1/3] refactor(code-index): type the freshness observation ladder One projector owns staleness, coverage, and rebuild-in-flight so the two serving reads cannot diverge, and a terminal publication park is the shared slot rather than a worker-local bool. Co-authored-by: Zack Jackson --- crates/tracedecay-cli/src/display.rs | 4 +- .../branch_publication.rs | 10 + .../code_index_scheduler/publication_store.rs | 18 +- .../src/code_index_scheduler/registry.rs | 18 + .../registry/convergence_park_tests.rs | 29 +- .../code_index_scheduler/registry/mount.rs | 21 +- .../reconcile_failure_isolation_tests.rs | 39 ++ .../registry/serving_reads.rs | 108 ++---- .../src/code_index_scheduler/tests/mod.rs | 4 +- .../code_index_scheduler/tests/reconcile.rs | 33 +- .../src/code_index_scheduler/tests/serving.rs | 19 +- .../src/code_index_freshness.rs | 358 +++++++++++++++++- .../src/retrieval/primitive_surface.rs | 6 +- .../src/code_index_freshness_api.rs | 42 +- .../src/delivery_api.rs | 8 +- .../src/handlers/graph/search_freshness.rs | 22 +- .../src/handlers/info/status.rs | 43 ++- .../ignored_dependency_admission_tests.rs | 5 +- .../src/mcp/tools/handlers/dispatch_tests.rs | 18 +- .../handlers/graph_search_dispatch_tests.rs | 10 +- .../tests/dashboard_api_test/delivery.rs | 6 +- .../code/IndexFreshness.dom.test.tsx | 8 +- 22 files changed, 638 insertions(+), 191 deletions(-) diff --git a/crates/tracedecay-cli/src/display.rs b/crates/tracedecay-cli/src/display.rs index 04d862c177..80e873e568 100644 --- a/crates/tracedecay-cli/src/display.rs +++ b/crates/tracedecay-cli/src/display.rs @@ -304,8 +304,8 @@ fn print_freshness_row(freshness: Option<&CodeIndexWorktreeFreshnessV1>, inner_w format_relative_time(unix_seconds_from_micros(sealed)) )); } - if let Some(state) = freshness.staleness_state.as_deref() { - parts.push(state.to_owned()); + if let Some(state) = freshness.staleness_state { + parts.push(state.as_str().to_owned()); } if parts.is_empty() { "no sealed generation yet".to_owned() diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_publication.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_publication.rs index 0fd136d005..97c63170d4 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_publication.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_publication.rs @@ -456,6 +456,16 @@ impl BranchPublicationContextV1 { format!("{}; {}", parked.reason, parked.remediation), )); } + CodeIndexDemandAdmissionV1::NotApplicable => { + return Err(TraceDecayError::project_route( + CODE_INDEX_IDENTITY_MISMATCH, + false, + format!( + "code indexing does not apply to branch worktree '{}'; the route has no repository identity", + canonical_worktree_root.display() + ), + )); + } CodeIndexDemandAdmissionV1::RefusedByPolicy | CodeIndexDemandAdmissionV1::Unavailable(_) => { return Err(TraceDecayError::project_route( diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/publication_store.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/publication_store.rs index 79a860d5cd..fc0159a287 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/publication_store.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/publication_store.rs @@ -2634,20 +2634,10 @@ impl CodeChunkProjectionSink for DaemonProjectionSinkV1 { output_digest: None, }), ); - decisions.extend( - request - .changes - .reused - .iter() - .map(|change| ChunkProjectionDecisionV1 { - chunk_id: change.chunk_id.clone(), - prior_chunk_digest: change.prior_digest.clone(), - current_chunk_digest: change.current_digest.clone(), - operation: ProjectionOperationV1::Reused, - outcome: ProjectionOutcomeV1::Reused, - output_digest: None, - }), - ); + // Unchanged chunks are a digest-authenticated complement + // (`reused_count`), not a list. The receipt builder records that count; + // inventing per-chunk reused rows would be extra receipts the request + // does not name. decisions.sort_by(|left, right| left.chunk_id.cmp(&right.chunk_id)); receipt_builder .build(&decisions) 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 b828cb9835..1bcd3d3ac0 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 @@ -813,6 +813,13 @@ fn is_terminal_publication_authority_park(parked: &CodeIndexConvergenceParkedV1) parked.blocked_reason == Some(CodeIndexBuildBlockedReasonV1::PublicationAuthorityCorrupt) } +fn publication_authority_is_terminal(slot: &RwLock>) -> bool { + slot.read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .as_ref() + .is_some_and(is_terminal_publication_authority_park) +} + /// Record one observation of a deterministic contract violation on a mounted /// worktree's park slot. /// @@ -922,6 +929,17 @@ mod terminal_publication_park_tests { } } + #[test] + fn publication_authority_terminal_is_the_shared_slot_not_task_local() { + let slot = RwLock::new(None); + assert!(!publication_authority_is_terminal(&slot)); + *slot + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) = + Some(terminal_park("planted by another actor")); + assert!(publication_authority_is_terminal(&slot)); + } + #[test] fn clear_convergence_park_preserves_terminal_publication_corruption() { let slot = RwLock::new(Some(terminal_park("corrupt-authority"))); diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/convergence_park_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/convergence_park_tests.rs index 20f586e339..f5798baaac 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/convergence_park_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/convergence_park_tests.rs @@ -166,13 +166,18 @@ async fn a_legacy_permissive_text_artifacts_root_self_heals_and_serves() { .await; let observed = fixture - .wait_for_freshness(|freshness| freshness.staleness_state.as_deref() == Some("fresh")) + .wait_for_freshness(|freshness| { + freshness.staleness_state + == Some( + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh, + ) + }) .await .expect("freshness projection for the mounted worktree"); assert_eq!( - observed.staleness_state.as_deref(), - Some("fresh"), + observed.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh), "the healed store must converge to serving instead of warming forever: {observed:?}" ); assert!( @@ -221,8 +226,8 @@ async fn an_unhealable_text_artifacts_root_parks_typed_and_recovers_when_fixed() ) }); assert_eq!( - parked.staleness_state.as_deref(), - Some("parked"), + parked.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Parked), "status must report parked, not indexing/warming: {parked:?}" ); assert!( @@ -249,7 +254,7 @@ async fn an_unhealable_text_artifacts_root_parks_typed_and_recovers_when_fixed() let recovered = fixture .wait_for_freshness(|freshness| { - freshness.parked.is_none() && freshness.staleness_state.as_deref() == Some("fresh") + freshness.parked.is_none() && freshness.staleness_state == Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh) }) .await .expect("freshness projection for the mounted worktree"); @@ -259,8 +264,8 @@ async fn an_unhealable_text_artifacts_root_parks_typed_and_recovers_when_fixed() "the park must clear once the violation is removed: {recovered:?}" ); assert_eq!( - recovered.staleness_state.as_deref(), - Some("fresh"), + recovered.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh), "convergence must resume on the ordinary wake cadence after the fix: {recovered:?}" ); let mode = fs::metadata(&fixture.artifacts_root) @@ -301,8 +306,8 @@ async fn fresh_graph_activation_waits_while_the_published_text_owner_is_parked() .await .expect("freshness projection for the mounted worktree"); assert_eq!( - parked.staleness_state.as_deref(), - Some("parked"), + parked.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Parked), "the text owner must park on the unhealable root: {parked:?}" ); @@ -318,8 +323,8 @@ async fn fresh_graph_activation_waits_while_the_published_text_owner_is_parked() .await .expect("freshness projection for the mounted worktree"); assert_eq!( - observed.staleness_state.as_deref(), - Some("parked"), + observed.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Parked), "waiting graph activation must not unpark an owner that has not finished: {observed:?}" ); assert!( 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 89d1e07df1..fbee84842f 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 @@ -35,7 +35,7 @@ use super::{ PendingWakeV1, PublishedTextProjectionOutcomeV1, ServingSwapOutcomeV1, TEXT_PROJECTION_DOCUMENTS_PER_PASS_V1, clear_convergence_park, convergence_park_retries_on_wake, is_repeated_conflict_verdict, park_convergence, - retained_noop_requires_follow_up_wake, + publication_authority_is_terminal, retained_noop_requires_follow_up_wake, }; impl CodeIndexSchedulerRegistryV1 { @@ -385,7 +385,6 @@ impl CodeIndexSchedulerRegistryV1 { // artifact build. Releasing that capacity emits no wake, so this // worker must schedule its own. let mut capacity_retry = ReconcileCapacityRetryV1::new(); - let mut publication_authority_terminal = false; // The last arrival this worker restored for a nothing-seated // warming outcome. A quiet remount's seat pass restores its // arrival exactly once so the next pass can restore and warm the @@ -428,7 +427,11 @@ impl CodeIndexSchedulerRegistryV1 { .await; return; } - if publication_authority_terminal { + // The shared park slot is the authority admission already + // consults. A task-local bool would ignore a park planted by + // another actor and would reset if this loop state were lost + // while the slot remained. + if publication_authority_is_terminal(&worker_convergence_park) { let _ = Self::take_pending_arrival( &worker_pending_wake, CodeIndexCadenceTriggerV1::Mount, @@ -2097,7 +2100,6 @@ impl CodeIndexSchedulerRegistryV1 { // Mid-wait branch publication rechecks the park on // serving-generation notifications. worker_serving_generation_changed.send_replace(()); - publication_authority_terminal = true; } else if transient_capacity { // Shared process capacity was held by another // holder when this pass asked for it. Releasing @@ -2169,7 +2171,7 @@ impl CodeIndexSchedulerRegistryV1 { ), Ok((Ok(_), _, _)) => {} } - if !publication_authority_terminal { + if !publication_authority_is_terminal(&worker_convergence_park) { // Restore arrival so the next pass measures this wake's full queue wait. Self::restore_pending_arrival(&worker_pending_wake, arrival, trigger); } @@ -2266,12 +2268,11 @@ impl CodeIndexSchedulerRegistryV1 { .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .as_ref() - .is_some_and(LatestCodeTextGenerationV1::text_projection_needs_work); - if installed_owner_still_needs_work { - Self::note_worker_continuation( - &worker_pending_wake, - &worker_wake, + .is_some_and( + LatestCodeTextGenerationV1::text_projection_needs_work, ); + if installed_owner_still_needs_work { + Self::note_worker_continuation(&worker_pending_wake, &worker_wake); } } PublishedTextProjectionOutcomeV1::Shutdown => { diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/reconcile_failure_isolation_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/reconcile_failure_isolation_tests.rs index 177fb6b3a5..93126d740d 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/reconcile_failure_isolation_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/reconcile_failure_isolation_tests.rs @@ -713,6 +713,45 @@ async fn retire_and_remount_clears_terminal_publication_park_for_new_admission() fixture.registry.shutdown().await; } +/// A park written by an actor other than this worker must still suppress +/// the loop. The old worker-local bool stayed false here, so later wakes +/// reconciled against a terminal publication authority. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn planted_terminal_publication_park_suppresses_later_wakes() { + let fixture = Fixture::mount("project.reconcile-planted-publication-park").await; + let fault = fixture + .install_fault(ReconcileFaultKindV1::Permanent, usize::MAX) + .await; + fixture + .plant_terminal_publication_park("planted by an actor other than the worker") + .await; + + fixture.wake_with_pending_arrival().await; + fixture.drive_external_wakes().await; + fixture.settle_for(TERMINATION_QUIET_WINDOW).await; + + assert_eq!( + fault.attempts(), + 0, + "a shared terminal park must stop the worker without a task-local latch" + ); + assert_eq!( + fixture.pending_wake_micros().await, + 0, + "the suppressed wake must drain the pending arrival" + ); + let freshness = fixture + .registry + .dashboard_freshness(&fixture.project) + .await + .expect("mounted freshness"); + assert!( + !freshness.rebuild_in_flight, + "a terminal park is not a rebuild: {freshness:?}" + ); + fixture.registry.shutdown().await; +} + fn run_git_in(root: &Path, args: &[&str]) { let output = Command::new("git") .args(args) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs index 33c7f9120a..734080706f 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs @@ -360,12 +360,23 @@ impl CodeIndexSchedulerRegistryV1 { graph_activation_enabled, &code_graph_serving, ); - let verifying = ready && refresh_in_flight && !source_change_pending; - let refreshing = refresh_in_flight && !verifying; - let rebuild_in_flight = refreshing; - let stale = hook_hint_count != Some(0); + let observation = tracedecay_contracts::code_index_freshness::CodeIndexFreshnessLadderV1::project( + tracedecay_contracts::code_index_freshness::CodeIndexFreshnessLadderInputsV1 { + ready, + refresh_in_flight, + source_change_pending, + parked: parked.as_ref(), + // The scheduler lock is held by the pass this read + // could not join, so verification is unknown here. + source_verified: None, + hook_hint_count, + }, + ); let clone_index = text.as_ref().map_or_else(Default::default, |text| { - text.clone_index_status(stale || refreshing, clone_update) + text.clone_index_status( + hook_hint_count != Some(0) || observation.rebuild_in_flight, + clone_update, + ) }); let last_reconcile_micros = match last_reconciled_at_micros .load(Ordering::Acquire) @@ -378,38 +389,10 @@ impl CodeIndexSchedulerRegistryV1 { code_graph_serving, clone_index: Some(clone_index), last_reconcile_micros, - rebuild_in_flight, - staleness_state: Some( - if parked.is_some() && !ready { - "parked" - } else if verifying { - "verifying" - } else if refreshing { - if ready { - "refreshing" - } else { - "indexing" - } - } else if stale && ready { - "stale" - } else if ready { - "fresh" - } else { - "indexing" - } - .to_owned(), - ), + rebuild_in_flight: observation.rebuild_in_flight, + staleness_state: Some(observation.staleness_state), hook_hint_count, - coverage: if refreshing { - "partial_refresh_in_progress" - } else if verifying { - "partial_source_verification" - } else if hook_hint_count.is_some() { - "complete" - } else { - "partial_hook_hint_overflow" - } - .to_owned(), + coverage: observation.coverage, progress, parked, generation_recovery, @@ -451,33 +434,19 @@ impl CodeIndexSchedulerRegistryV1 { graph_activation_enabled, &code_graph_serving, ); - let verifying = ready && refresh_in_flight && !source_change_pending; - let refreshing = refresh_in_flight && !verifying; - let rebuild_in_flight = refreshing; + let observation = tracedecay_contracts::code_index_freshness::CodeIndexFreshnessLadderV1::project( + tracedecay_contracts::code_index_freshness::CodeIndexFreshnessLadderInputsV1 { + ready, + refresh_in_flight, + source_change_pending, + parked: parked.as_ref(), + source_verified: Some(verified), + hook_hint_count, + }, + ); let clone_index = text.as_ref().map_or_else(Default::default, |text| { - text.clone_index_status(stale || refreshing, clone_update) + text.clone_index_status(stale || observation.rebuild_in_flight, clone_update) }); - let staleness_state = if parked.is_some() && !ready { - "parked" - } else if verifying { - "verifying" - } else if refreshing { - if ready { - "refreshing" - } else { - "indexing" - } - } else if stale || hook_hint_count != Some(0) { - if ready { - "stale" - } else { - "indexing" - } - } else if ready { - "fresh" - } else { - "indexing" - }; let identity = if text.is_some() { dashboard_text_freshness_identity(text.as_ref()) } else { @@ -488,21 +457,10 @@ impl CodeIndexSchedulerRegistryV1 { code_graph_serving, clone_index: Some(clone_index), last_reconcile_micros: scheduler.last_reconciled_at_micros(), - rebuild_in_flight, - staleness_state: Some(staleness_state.to_owned()), + rebuild_in_flight: observation.rebuild_in_flight, + staleness_state: Some(observation.staleness_state), hook_hint_count, - coverage: if refreshing { - "partial_refresh_in_progress" - } else if verifying { - "partial_source_verification" - } else if !verified { - "partial_unverified_restore" - } else if hook_hint_count.is_some() { - "complete" - } else { - "partial_hook_hint_overflow" - } - .to_owned(), + coverage: observation.coverage, progress, parked, generation_recovery, diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs index e087defa7d..845dfa9f10 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs @@ -1505,8 +1505,8 @@ async fn wait_for_dashboard_ready(registry: &CodeIndexSchedulerRegistryV1, path: .dashboard_freshness(path) .await .is_some_and(|freshness| { - freshness.staleness_state.as_deref() == Some("fresh") - && freshness.coverage == "complete" + freshness.staleness_state == Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh) + && freshness.coverage == tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete }); if ready { break; 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 3049faba74..5676787235 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 @@ -649,7 +649,10 @@ async fn registry_feeds_publications_and_bounded_freshness_reads() { Some(initial.generation_id.as_str()) ); assert!(freshness.last_reconcile_micros.is_some()); - assert_eq!(freshness.staleness_state.as_deref(), Some("fresh")); + assert_eq!( + freshness.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh) + ); assert_eq!(freshness.hook_hint_count, Some(0)); fixture.edit("src/lib.rs", "pub fn alpha() -> u32 { 2 }\n"); @@ -3656,11 +3659,14 @@ async fn dashboard_progress_does_not_wait_for_the_scheduler_mutex() { assert_eq!(projected_progress.generation_id, expected.generation_id); assert!(projected_progress.progress_epoch >= expected.progress_epoch); assert_eq!( - projected.staleness_state.as_deref(), - Some("fresh"), + projected.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh), "an unrelated scheduler-mutex holder is not a source refresh" ); - assert_eq!(projected.coverage, "complete"); + assert_eq!( + projected.coverage, + tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete + ); assert_eq!(projected.hook_hint_count, Some(0)); let _ = release_tx.send(()); scheduler_holder @@ -3935,8 +3941,14 @@ async fn elapsed_freshness_window_alone_does_not_make_dashboard_state_stale() { .dashboard_freshness(fixture.path()) .await .expect("dashboard freshness"); - assert_eq!(projected.staleness_state.as_deref(), Some("fresh")); - assert_eq!(projected.coverage, "complete"); + assert_eq!( + projected.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh) + ); + assert_eq!( + projected.coverage, + tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete + ); registry.shutdown().await; } @@ -4025,8 +4037,11 @@ async fn a_fresh_seat_declines_query_admission_during_source_verification() { .dashboard_freshness(fixture.path()) .await .expect("dashboard freshness"); - assert_eq!(projected.staleness_state.as_deref(), Some("verifying")); - assert_eq!(projected.coverage, "partial_source_verification"); + assert_eq!( + projected.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Verifying) + ); + assert_eq!(projected.coverage, tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::PartialSourceVerification); assert!(!projected.rebuild_in_flight); drop(pass); @@ -9714,7 +9729,7 @@ async fn blocked_observability_store_does_not_hold_reconcile_readiness() { .dashboard_freshness(fixture.path()) .await .expect("dashboard freshness"); - if freshness.staleness_state.as_deref() == Some("fresh") && freshness.coverage == "complete" + if freshness.staleness_state == Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh) && freshness.coverage == tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete { break; } diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs index cdc2027b5f..3f755259dd 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs @@ -1054,7 +1054,9 @@ async fn expired_source_proof_reschedules_pending_clone_backfill() { settled.is_ok(), "the source refresh stranded clone backfill: pending_wake={:?} reconcile_in_progress={} source_age={:?} receipts={:?}", registry.pending_wake_micros_for_scope(&scope).await, - registry.reconcile_in_progress_for_test(fixture.path()).await, + registry + .reconcile_in_progress_for_test(fixture.path()) + .await, source_freshness .state .lock() @@ -6315,7 +6317,10 @@ async fn graph_off_overflow_preserves_text_owner_progress_without_full_decode() .reconcile_in_progress_for_test(fixture.path()) .await && let Some(freshness) = registry.dashboard_freshness(fixture.path()).await - && freshness.staleness_state.as_deref() == Some("fresh") + && freshness.staleness_state + == Some( + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh, + ) { // A completed owner pass can have another queued wake. Capture the // settled public snapshot instead of racing a later status read. @@ -6423,8 +6428,14 @@ async fn graph_off_overflow_preserves_text_owner_progress_without_full_decode() Some(&PublicRetrieverStatus::Unavailable) ); - assert_eq!(dashboard.staleness_state.as_deref(), Some("fresh")); - assert_eq!(dashboard.coverage, "complete"); + assert_eq!( + dashboard.staleness_state, + Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh) + ); + assert_eq!( + dashboard.coverage, + tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete + ); assert_eq!( dashboard .progress diff --git a/crates/tracedecay-contracts/src/code_index_freshness.rs b/crates/tracedecay-contracts/src/code_index_freshness.rs index 1f4a26d3ee..f2fbcfd6d2 100644 --- a/crates/tracedecay-contracts/src/code_index_freshness.rs +++ b/crates/tracedecay-contracts/src/code_index_freshness.rs @@ -297,6 +297,181 @@ impl Default for CodeCloneIndexStatusV1 { } } +/// Closed staleness ladder for one mounted worktree. +/// +/// The scheduler publishes one of these tokens. MCP, the dashboard, and the +/// CLI must match the variant — not a hand-copied string — so a new ladder +/// state cannot appear at one caller and be missed at the others. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum CodeIndexStalenessStateV1 { + Fresh, + Stale, + Indexing, + Refreshing, + Verifying, + Parked, +} + +impl CodeIndexStalenessStateV1 { + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Fresh => "fresh", + Self::Stale => "stale", + Self::Indexing => "indexing", + Self::Refreshing => "refreshing", + Self::Verifying => "verifying", + Self::Parked => "parked", + } + } + + #[must_use] + pub fn from_wire(token: &str) -> Option { + match token { + "fresh" => Some(Self::Fresh), + "stale" => Some(Self::Stale), + "indexing" => Some(Self::Indexing), + "refreshing" => Some(Self::Refreshing), + "verifying" => Some(Self::Verifying), + "parked" => Some(Self::Parked), + _ => None, + } + } +} + +impl std::fmt::Display for CodeIndexStalenessStateV1 { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// Coverage of one freshness read. Distinct from [`CodeIndexStalenessStateV1`]: +/// a generation can be fresh and still omit hook-hint counts, or unverified +/// while the ladder would otherwise say ready. +/// +/// `Unobserved` is only the constructed default. A projected read never emits +/// it, so an absent observation cannot be mistaken for `complete`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum CodeIndexFreshnessCoverageV1 { + Unobserved, + Complete, + PartialRefreshInProgress, + PartialSourceVerification, + PartialUnverifiedRestore, + PartialHookHintOverflow, +} + +impl Default for CodeIndexFreshnessCoverageV1 { + fn default() -> Self { + Self::Unobserved + } +} + +impl CodeIndexFreshnessCoverageV1 { + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Unobserved => "unobserved", + Self::Complete => "complete", + Self::PartialRefreshInProgress => "partial_refresh_in_progress", + Self::PartialSourceVerification => "partial_source_verification", + Self::PartialUnverifiedRestore => "partial_unverified_restore", + Self::PartialHookHintOverflow => "partial_hook_hint_overflow", + } + } + + #[must_use] + pub fn from_wire(token: &str) -> Option { + match token { + "unobserved" => Some(Self::Unobserved), + "complete" => Some(Self::Complete), + "partial_refresh_in_progress" => Some(Self::PartialRefreshInProgress), + "partial_source_verification" => Some(Self::PartialSourceVerification), + "partial_unverified_restore" => Some(Self::PartialUnverifiedRestore), + "partial_hook_hint_overflow" => Some(Self::PartialHookHintOverflow), + _ => None, + } + } +} + +impl std::fmt::Display for CodeIndexFreshnessCoverageV1 { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// Inputs to the single freshness ladder. +/// +/// `source_verified` is `None` when the scheduler lock was not held, so that +/// read must not claim an unverified restore. A park with `retries_on_wake` +/// false is terminal for this observation: a racing wake must not make the +/// worktree look like a rebuild. +#[derive(Clone, Copy, Debug)] +pub struct CodeIndexFreshnessLadderInputsV1<'a> { + pub ready: bool, + pub refresh_in_flight: bool, + pub source_change_pending: bool, + pub parked: Option<&'a CodeIndexConvergenceParkedV1>, + pub source_verified: Option, + pub hook_hint_count: Option, +} + +/// The ladder plus the two fields that must stay consistent with it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CodeIndexFreshnessLadderV1 { + pub staleness_state: CodeIndexStalenessStateV1, + pub rebuild_in_flight: bool, + pub coverage: CodeIndexFreshnessCoverageV1, +} + +impl CodeIndexFreshnessLadderV1 { + #[must_use] + pub fn project(inputs: CodeIndexFreshnessLadderInputsV1<'_>) -> Self { + let terminal_park = inputs.parked.is_some_and(|parked| !parked.retries_on_wake); + let refresh_in_flight = inputs.refresh_in_flight && !terminal_park; + let verifying = inputs.ready && refresh_in_flight && !inputs.source_change_pending; + let refreshing = refresh_in_flight && !verifying; + let hints_outstanding = inputs.hook_hint_count != Some(0); + let source_unverified = inputs.source_verified == Some(false); + let admitted_stale = source_unverified || hints_outstanding; + let staleness_state = if inputs.parked.is_some() && !inputs.ready { + CodeIndexStalenessStateV1::Parked + } else if verifying { + CodeIndexStalenessStateV1::Verifying + } else if refreshing { + if inputs.ready { + CodeIndexStalenessStateV1::Refreshing + } else { + CodeIndexStalenessStateV1::Indexing + } + } else if admitted_stale && inputs.ready { + CodeIndexStalenessStateV1::Stale + } else if admitted_stale || !inputs.ready { + CodeIndexStalenessStateV1::Indexing + } else { + CodeIndexStalenessStateV1::Fresh + }; + let coverage = if refreshing { + CodeIndexFreshnessCoverageV1::PartialRefreshInProgress + } else if verifying { + CodeIndexFreshnessCoverageV1::PartialSourceVerification + } else if source_unverified { + CodeIndexFreshnessCoverageV1::PartialUnverifiedRestore + } else if inputs.hook_hint_count.is_some() { + CodeIndexFreshnessCoverageV1::Complete + } else { + CodeIndexFreshnessCoverageV1::PartialHookHintOverflow + }; + Self { + staleness_state, + rebuild_in_flight: refreshing, + coverage, + } + } +} + /// Freshness/generation state for one mounted worktree. /// /// `Deserialize` is part of the wire contract: the CLI status command decodes @@ -333,7 +508,7 @@ pub struct CodeIndexWorktreeFreshnessV1 { /// Staleness-ladder state from the last scheduler execution. `fresh` means /// the scheduler most recently observed a fresh source; the status read /// does not probe the worktree to revalidate that observation. - pub staleness_state: Option, + pub staleness_state: Option, /// Whether the exact scheduler route owns a reconcile pass or has a /// pending wake. A stale seated generation with this false is stalled, /// not in a routine rebuild window. @@ -342,7 +517,7 @@ pub struct CodeIndexWorktreeFreshnessV1 { /// Pending hook-hint count, when cheaply available. pub hook_hint_count: Option, /// Whether this read covers the complete mounted scheduler state. - pub coverage: String, + pub coverage: CodeIndexFreshnessCoverageV1, /// Latest committed progress for the active generation, if one is mounted. pub progress: Option, /// Deterministic contract violation currently parking background @@ -480,4 +655,183 @@ mod tests { }) ); } + + fn ladder(inputs: CodeIndexFreshnessLadderInputsV1<'_>) -> CodeIndexFreshnessLadderV1 { + CodeIndexFreshnessLadderV1::project(inputs) + } + + fn settled() -> CodeIndexFreshnessLadderInputsV1<'static> { + CodeIndexFreshnessLadderInputsV1 { + ready: true, + refresh_in_flight: false, + source_change_pending: false, + parked: None, + source_verified: Some(true), + hook_hint_count: Some(0), + } + } + + #[test] + fn freshness_ladder_tokens_round_trip_the_wire_spelling() { + for state in [ + CodeIndexStalenessStateV1::Fresh, + CodeIndexStalenessStateV1::Stale, + CodeIndexStalenessStateV1::Indexing, + CodeIndexStalenessStateV1::Refreshing, + CodeIndexStalenessStateV1::Verifying, + CodeIndexStalenessStateV1::Parked, + ] { + assert_eq!( + CodeIndexStalenessStateV1::from_wire(state.as_str()), + Some(state) + ); + assert_eq!( + serde_json::to_value(state).expect("staleness serializes"), + serde_json::json!(state.as_str()) + ); + } + for coverage in [ + CodeIndexFreshnessCoverageV1::Complete, + CodeIndexFreshnessCoverageV1::PartialRefreshInProgress, + CodeIndexFreshnessCoverageV1::PartialSourceVerification, + CodeIndexFreshnessCoverageV1::PartialUnverifiedRestore, + CodeIndexFreshnessCoverageV1::PartialHookHintOverflow, + ] { + assert_eq!( + CodeIndexFreshnessCoverageV1::from_wire(coverage.as_str()), + Some(coverage) + ); + } + assert_eq!( + CodeIndexFreshnessCoverageV1::default(), + CodeIndexFreshnessCoverageV1::Unobserved + ); + } + + #[test] + fn settled_ready_generation_is_fresh_and_complete() { + let observed = ladder(settled()); + assert_eq!(observed.staleness_state, CodeIndexStalenessStateV1::Fresh); + assert!(!observed.rebuild_in_flight); + assert_eq!(observed.coverage, CodeIndexFreshnessCoverageV1::Complete); + } + + #[test] + fn verifying_is_not_a_rebuild() { + let observed = ladder(CodeIndexFreshnessLadderInputsV1 { + ready: true, + refresh_in_flight: true, + source_change_pending: false, + ..settled() + }); + assert_eq!( + observed.staleness_state, + CodeIndexStalenessStateV1::Verifying + ); + assert!(!observed.rebuild_in_flight); + assert_eq!( + observed.coverage, + CodeIndexFreshnessCoverageV1::PartialSourceVerification + ); + } + + #[test] + fn source_change_while_ready_is_a_refresh() { + let observed = ladder(CodeIndexFreshnessLadderInputsV1 { + ready: true, + refresh_in_flight: true, + source_change_pending: true, + ..settled() + }); + assert_eq!( + observed.staleness_state, + CodeIndexStalenessStateV1::Refreshing + ); + assert!(observed.rebuild_in_flight); + assert_eq!( + observed.coverage, + CodeIndexFreshnessCoverageV1::PartialRefreshInProgress + ); + } + + #[test] + fn locked_read_reports_unverified_restore_separately_from_a_busy_read() { + let locked = ladder(CodeIndexFreshnessLadderInputsV1 { + source_verified: Some(false), + ..settled() + }); + assert_eq!(locked.staleness_state, CodeIndexStalenessStateV1::Stale); + assert_eq!( + locked.coverage, + CodeIndexFreshnessCoverageV1::PartialUnverifiedRestore + ); + let busy = ladder(CodeIndexFreshnessLadderInputsV1 { + source_verified: None, + ..settled() + }); + assert_eq!(busy.staleness_state, CodeIndexStalenessStateV1::Fresh); + assert_eq!(busy.coverage, CodeIndexFreshnessCoverageV1::Complete); + } + + #[test] + fn unknown_hint_count_is_stale_when_ready_and_overflow_coverage() { + let observed = ladder(CodeIndexFreshnessLadderInputsV1 { + hook_hint_count: None, + ..settled() + }); + assert_eq!(observed.staleness_state, CodeIndexStalenessStateV1::Stale); + assert_eq!( + observed.coverage, + CodeIndexFreshnessCoverageV1::PartialHookHintOverflow + ); + } + + #[test] + fn terminal_park_is_not_a_racing_rebuild() { + let parked = CodeIndexConvergenceParkedV1 { + reason: "corrupt".to_owned(), + blocked_reason: Some(CodeIndexBuildBlockedReasonV1::PublicationAuthorityCorrupt), + remediation: "reset".to_owned(), + parked_at_micros: 1, + observed_passes: 1, + retries_on_wake: false, + }; + let observed = ladder(CodeIndexFreshnessLadderInputsV1 { + ready: false, + refresh_in_flight: true, + parked: Some(&parked), + source_verified: Some(true), + hook_hint_count: Some(0), + source_change_pending: false, + }); + assert_eq!(observed.staleness_state, CodeIndexStalenessStateV1::Parked); + assert!(!observed.rebuild_in_flight); + assert_eq!(observed.coverage, CodeIndexFreshnessCoverageV1::Complete); + } + + #[test] + fn retryable_park_keeps_a_racing_rebuild_visible() { + let parked = CodeIndexConvergenceParkedV1 { + reason: "mode".to_owned(), + blocked_reason: None, + remediation: "chmod".to_owned(), + parked_at_micros: 1, + observed_passes: 1, + retries_on_wake: true, + }; + let observed = ladder(CodeIndexFreshnessLadderInputsV1 { + ready: false, + refresh_in_flight: true, + parked: Some(&parked), + source_verified: Some(true), + hook_hint_count: Some(0), + source_change_pending: false, + }); + assert_eq!(observed.staleness_state, CodeIndexStalenessStateV1::Parked); + assert!(observed.rebuild_in_flight); + assert_eq!( + observed.coverage, + CodeIndexFreshnessCoverageV1::PartialRefreshInProgress + ); + } } diff --git a/crates/tracedecay-contracts/src/retrieval/primitive_surface.rs b/crates/tracedecay-contracts/src/retrieval/primitive_surface.rs index 0acfd5c1e5..2809e11d49 100644 --- a/crates/tracedecay-contracts/src/retrieval/primitive_surface.rs +++ b/crates/tracedecay-contracts/src/retrieval/primitive_surface.rs @@ -14,6 +14,7 @@ use tracedecay_domain::{ RepositoryId, SourceSpan, SymbolOccurrenceId, WorktreeId, }; +use crate::code_index_freshness::CodeIndexStalenessStateV1; use crate::memory::{FactSearchGraphCoverageV1, FactSearchHitV1}; pub const MAX_REDUNDANCY_FAMILIES_V1: u32 = 100; @@ -88,7 +89,7 @@ pub struct PrimitiveIndexingStateV1 { #[serde(default, skip_serializing_if = "Option::is_none")] pub latest_generation: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub staleness_state: Option, + pub staleness_state: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub rebuild_in_flight: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -766,6 +767,7 @@ mod tests { PrimitiveRecallV1, PrimitiveSearchCoverageV1, PrimitiveSearchFreshnessV1, RedundancySurfaceRequestWireV1, SimilarSurfaceRequestWireV1, }; + use crate::code_index_freshness::CodeIndexStalenessStateV1; use crate::memory::{FactSearchGraphCoverageV1, FactSearchGraphDegradationV1}; fn context_result() -> ContextResultV1 { @@ -873,7 +875,7 @@ mod tests { summary: "state=refreshing".to_owned(), served_generation: Some("generation.old".to_owned()), latest_generation: Some("generation.new".to_owned()), - staleness_state: Some("refreshing".to_owned()), + staleness_state: Some(CodeIndexStalenessStateV1::Refreshing), rebuild_in_flight: Some(true), stale_lanes: vec!["lexical".to_owned()], reason: None, diff --git a/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs b/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs index 437fc8e7d4..91866de018 100644 --- a/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs +++ b/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs @@ -12,7 +12,9 @@ use axum::Json; use axum::extract::State; -use tracedecay_contracts::code_index_freshness::CodeIndexFreshnessPayloadV1; +use tracedecay_contracts::code_index_freshness::{ + CodeIndexFreshnessCoverageV1, CodeIndexFreshnessPayloadV1, CodeIndexStalenessStateV1, +}; use super::DashboardState; use super::read_model::{ @@ -52,8 +54,8 @@ async fn project_code_index_freshness( match live { Some(worktree) if worktree.latest_generation_id.is_some() - && worktree.coverage == "complete" - && worktree.staleness_state.as_deref() == Some("fresh") => + && worktree.coverage == CodeIndexFreshnessCoverageV1::Complete + && worktree.staleness_state == Some(CodeIndexStalenessStateV1::Fresh) => { DashboardEnvelopeV1::ready( scope_from_state(state), @@ -85,7 +87,7 @@ async fn project_code_index_freshness( } Some(worktree) if worktree.latest_generation_id.is_none() => DashboardEnvelopeV1::new( scope_from_state(state), - if worktree.staleness_state.as_deref() == Some("indexing") { + if worktree.staleness_state == Some(CodeIndexStalenessStateV1::Indexing) { DashboardDomainStateV1::Loading } else { DashboardDomainStateV1::Unknown @@ -95,7 +97,7 @@ async fn project_code_index_freshness( payload, ), Some(worktree) => { - let omission = if worktree.staleness_state.as_deref() == Some("fresh") { + let omission = if worktree.staleness_state == Some(CodeIndexStalenessStateV1::Fresh) { format!( "scheduler freshness is fresh but coverage is {}; complete coverage is required", worktree.coverage @@ -103,7 +105,10 @@ async fn project_code_index_freshness( } else { format!( "scheduler freshness state is {}; only fresh serves as current", - worktree.staleness_state.as_deref().unwrap_or("unreported") + worktree + .staleness_state + .map(|state| state.as_str()) + .unwrap_or("unreported") ) }; DashboardEnvelopeV1::new( @@ -141,7 +146,8 @@ mod tests { use super::*; use crate::read_model::DashboardDomainStateV1; use tracedecay_contracts::code_index_freshness::{ - CodeGraphServingReadinessV1, CodeIndexWorktreeFreshnessV1, + CodeGraphServingReadinessV1, CodeIndexFreshnessCoverageV1, CodeIndexStalenessStateV1, + CodeIndexWorktreeFreshnessV1, }; async fn state_for_test() -> (tempfile::TempDir, DashboardState) { @@ -180,10 +186,10 @@ mod tests { snapshot_content_identity: None, sealed_at_micros: None, last_reconcile_micros: Some(42), - staleness_state: Some("indexing".to_owned()), + staleness_state: Some(CodeIndexStalenessStateV1::Indexing), rebuild_in_flight: false, hook_hint_count: Some(0), - coverage: "complete".to_owned(), + coverage: CodeIndexFreshnessCoverageV1::Complete, progress: None, parked: None, generation_recovery: None, @@ -212,15 +218,18 @@ mod tests { #[tokio::test] async fn complete_generation_reports_its_noncurrent_freshness_state() { let _pin = tracedecay_runtime_core::config::PinnedUserDataDir::new(); - for staleness in ["stale", "verifying"] { + for staleness in [ + CodeIndexStalenessStateV1::Stale, + CodeIndexStalenessStateV1::Verifying, + ] { let (_project, mut state) = state_for_test().await; state.code_index_freshness_reader = Some(Arc::new(move |root| { Box::pin(async move { Some(CodeIndexWorktreeFreshnessV1 { worktree_root: root.display().to_string(), latest_generation_id: Some("generation.fixture".to_owned()), - staleness_state: Some(staleness.to_owned()), - coverage: "complete".to_owned(), + staleness_state: Some(staleness), + coverage: CodeIndexFreshnessCoverageV1::Complete, hook_hint_count: Some(1), ..Default::default() }) @@ -230,7 +239,10 @@ mod tests { let Json(envelope) = freshness(State(state)).await; assert_eq!(envelope.domain_state, DashboardDomainStateV1::Partial); - assert_eq!(envelope.payload.worktrees[0].coverage, "complete"); + assert_eq!( + envelope.payload.worktrees[0].coverage, + CodeIndexFreshnessCoverageV1::Complete + ); assert_eq!( envelope.coverage.omission_reasons, vec![format!( @@ -249,8 +261,8 @@ mod tests { Some(CodeIndexWorktreeFreshnessV1 { worktree_root: root.display().to_string(), latest_generation_id: Some("generation.fixture".to_owned()), - staleness_state: Some("fresh".to_owned()), - coverage: "partial_hook_hint_overflow".to_owned(), + staleness_state: Some(CodeIndexStalenessStateV1::Fresh), + coverage: CodeIndexFreshnessCoverageV1::PartialHookHintOverflow, ..Default::default() }) }) diff --git a/crates/tracedecay-dashboard-api/src/delivery_api.rs b/crates/tracedecay-dashboard-api/src/delivery_api.rs index 25e8d6d108..2d86d19566 100644 --- a/crates/tracedecay-dashboard-api/src/delivery_api.rs +++ b/crates/tracedecay-dashboard-api/src/delivery_api.rs @@ -1136,9 +1136,13 @@ fn indexed_delivery_head( branch_ref: freshness.source_reference?, head_commit_id: CommitId::new(freshness.source_revision?).ok()?, generation: CodeGenerationId::new(freshness.latest_generation_id?).ok()?, - coverage: if freshness.coverage != "complete" { + coverage: if freshness.coverage + != tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete + { ProjectDeliveryInboxCoverageV1::Partial - } else if freshness.staleness_state.as_deref() == Some("fresh") { + } else if freshness.staleness_state + == Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh) + { ProjectDeliveryInboxCoverageV1::Complete } else { ProjectDeliveryInboxCoverageV1::Stale diff --git a/crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs b/crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs index 012a6e8ec2..3383cb9ba5 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs @@ -13,7 +13,8 @@ use std::fmt::Write as _; use tracedecay_contracts::code_index_freshness::{ - CodeIndexBuildPhaseV1, CodeIndexFreshnessPayloadV1, CodeIndexWorktreeFreshnessV1, + CodeIndexBuildPhaseV1, CodeIndexFreshnessPayloadV1, CodeIndexStalenessStateV1, + CodeIndexWorktreeFreshnessV1, }; use tracedecay_contracts::retrieval::{ PrimitiveFreshnessStateV1, PrimitiveIndexingStateV1, PrimitiveSearchFreshnessV1, @@ -88,7 +89,7 @@ pub(super) fn search_freshness( }; let scheduler_says_stale = match worktree { WorktreeFreshnessSourceV1::Worktree(state) => { - state.staleness_state.as_deref() != Some("fresh") + state.staleness_state != Some(CodeIndexStalenessStateV1::Fresh) || state.rebuild_in_flight || (state.latest_generation_id.is_some() && served_generation.is_some() @@ -109,7 +110,10 @@ pub(super) fn search_freshness( let _ = write!( summary, "state={} rebuild_in_flight={}", - state.staleness_state.as_deref().unwrap_or("unknown"), + state + .staleness_state + .map(|state| state.as_str()) + .unwrap_or("unknown"), state.rebuild_in_flight ); ( @@ -181,7 +185,9 @@ pub(super) fn freshness_lines(freshness: &PrimitiveSearchFreshnessV1) -> String #[cfg(test)] mod tests { - use tracedecay_contracts::code_index_freshness::CodeIndexBuildProgressV1; + use tracedecay_contracts::code_index_freshness::{ + CodeIndexBuildProgressV1, CodeIndexFreshnessCoverageV1, CodeIndexStalenessStateV1, + }; use super::*; @@ -193,10 +199,10 @@ mod tests { WorktreeFreshnessSourceV1::Worktree(Box::new(CodeIndexWorktreeFreshnessV1 { worktree_root: "/fixture".to_owned(), latest_generation_id: latest_generation_id.map(str::to_owned), - staleness_state: Some(staleness_state.to_owned()), + staleness_state: CodeIndexStalenessStateV1::from_wire(staleness_state), rebuild_in_flight, hook_hint_count: Some(0), - coverage: "complete".to_owned(), + coverage: CodeIndexFreshnessCoverageV1::Complete, ..CodeIndexWorktreeFreshnessV1::default() })) } @@ -283,10 +289,10 @@ mod tests { fn unavailable_search_reports_the_indexing_state_with_progress() { let mut state = CodeIndexWorktreeFreshnessV1 { worktree_root: "/fixture".to_owned(), - staleness_state: Some("indexing".to_owned()), + staleness_state: Some(CodeIndexStalenessStateV1::Indexing), rebuild_in_flight: true, hook_hint_count: Some(3), - coverage: "complete".to_owned(), + coverage: CodeIndexFreshnessCoverageV1::Complete, ..CodeIndexWorktreeFreshnessV1::default() }; state.progress = Some(CodeIndexBuildProgressV1 { diff --git a/crates/tracedecay-mcp/src/handlers/info/status.rs b/crates/tracedecay-mcp/src/handlers/info/status.rs index 9183d0d249..7742749299 100644 --- a/crates/tracedecay-mcp/src/handlers/info/status.rs +++ b/crates/tracedecay-mcp/src/handlers/info/status.rs @@ -4,6 +4,9 @@ use std::path::Path; use serde_json::{Value, json}; use tracedecay_application::tracedecay::BranchDiagnostics; +use tracedecay_contracts::code_index_freshness::{ + CodeIndexFreshnessCoverageV1, CodeIndexStalenessStateV1, +}; use tracedecay_contracts::storage::{SchemaConvergenceFindingV1, SchemaConvergenceStateV1}; use tracedecay_domain::errors::Result; use tracedecay_global_db::{RegisteredGlobalDb, SessionIngestHealth}; @@ -138,8 +141,8 @@ fn ready_serving_source( Some(ReadyServingSourceV1 { reference: freshness.source_reference.as_deref()?, revision: freshness.source_revision.as_deref(), - current_source_verified: freshness.coverage == "complete" - && freshness.staleness_state.as_deref() == Some("fresh"), + current_source_verified: freshness.coverage == CodeIndexFreshnessCoverageV1::Complete + && freshness.staleness_state == Some(CodeIndexStalenessStateV1::Fresh), }) } @@ -272,10 +275,11 @@ pub async fn handle_status( // exists for the worktree; until the first seal every // retrieval lane refuses `generation_rebuilding`. let retrieval_serving = if freshness.latest_generation_id.is_some() { - let (serving_freshness, condition) = match freshness.staleness_state.as_deref() - { - Some("fresh") => ("current", None), - Some("verifying") => ("last_complete_stale", Some("source_verification")), + let (serving_freshness, condition) = match freshness.staleness_state { + Some(CodeIndexStalenessStateV1::Fresh) => ("current", None), + Some(CodeIndexStalenessStateV1::Verifying) => { + ("last_complete_stale", Some("source_verification")) + } Some(_) if freshness.rebuild_in_flight => { ("last_complete_stale", Some("rebuilding")) } @@ -444,14 +448,14 @@ fn code_index_freshness_projection( freshness: &tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1, ) -> (&'static str, Option) { let authoritative = freshness.latest_generation_id.is_some() - && freshness.coverage == "complete" - && freshness.staleness_state.as_deref() == Some("fresh"); + && freshness.coverage == CodeIndexFreshnessCoverageV1::Complete + && freshness.staleness_state == Some(CodeIndexStalenessStateV1::Fresh); if let Some(parked) = freshness.parked.as_ref() { let warning = format!( "code-index background convergence is parked: {}; {}", parked.reason, parked.remediation ); - let status = if freshness.staleness_state.as_deref() == Some("parked") { + let status = if freshness.staleness_state == Some(CodeIndexStalenessStateV1::Parked) { "parked" } else if authoritative { "current" @@ -462,7 +466,7 @@ fn code_index_freshness_projection( } if authoritative { ("current", None) - } else if freshness.staleness_state.as_deref() == Some("verifying") { + } else if freshness.staleness_state == Some(CodeIndexStalenessStateV1::Verifying) { ( "stale", Some( @@ -724,6 +728,9 @@ mod tests { code_index_freshness_projection, graph_statistics_value, historical_session_catch_up_state, render_status_md, schema_convergence_status, }; + use tracedecay_contracts::code_index_freshness::{ + CodeIndexFreshnessCoverageV1, CodeIndexStalenessStateV1, + }; use tracedecay_contracts::storage::{ SchemaConvergenceFindingV1, SchemaConvergenceProgressV1, SchemaConvergenceStageV1, SchemaConvergenceStateV1, @@ -820,8 +827,8 @@ mod tests { fn a_parked_deterministic_violation_reports_parked_not_warming() { let freshness = tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1 { worktree_root: "/project".to_owned(), - staleness_state: Some("parked".to_owned()), - coverage: "complete".to_owned(), + staleness_state: Some(CodeIndexStalenessStateV1::Parked), + coverage: CodeIndexFreshnessCoverageV1::Complete, parked: Some( tracedecay_contracts::code_index_freshness::CodeIndexConvergenceParkedV1 { reason: "code text artifacts root is not owner-private (mode 775, need 700)" @@ -876,8 +883,8 @@ mod tests { let freshness = tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1 { worktree_root: "/project".to_owned(), latest_generation_id: Some("generation.fixture".to_owned()), - staleness_state: Some("fresh".to_owned()), - coverage: "complete".to_owned(), + staleness_state: Some(CodeIndexStalenessStateV1::Fresh), + coverage: CodeIndexFreshnessCoverageV1::Complete, parked: Some( tracedecay_contracts::code_index_freshness::CodeIndexConvergenceParkedV1 { reason: "code text artifacts root is not owner-private".to_owned(), @@ -901,8 +908,8 @@ mod tests { fn an_unparked_incomplete_read_stays_warming() { let freshness = tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1 { worktree_root: "/project".to_owned(), - staleness_state: Some("indexing".to_owned()), - coverage: "complete".to_owned(), + staleness_state: Some(CodeIndexStalenessStateV1::Indexing), + coverage: CodeIndexFreshnessCoverageV1::Complete, ..Default::default() }; @@ -921,8 +928,8 @@ mod tests { let freshness = tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1 { worktree_root: "/project".to_owned(), latest_generation_id: Some("generation.fixture".to_owned()), - staleness_state: Some("verifying".to_owned()), - coverage: "partial_source_verification".to_owned(), + staleness_state: Some(CodeIndexStalenessStateV1::Verifying), + coverage: CodeIndexFreshnessCoverageV1::PartialSourceVerification, ..Default::default() }; diff --git a/crates/tracedecay/src/daemon/project_open_owners/code_index_reads/ignored_dependency_admission_tests.rs b/crates/tracedecay/src/daemon/project_open_owners/code_index_reads/ignored_dependency_admission_tests.rs index 29c061005c..8c3a2499c6 100644 --- a/crates/tracedecay/src/daemon/project_open_owners/code_index_reads/ignored_dependency_admission_tests.rs +++ b/crates/tracedecay/src/daemon/project_open_owners/code_index_reads/ignored_dependency_admission_tests.rs @@ -394,8 +394,9 @@ async fn latest( .dashboard_freshness(project_root) .await .and_then(|freshness| freshness.staleness_state) - .as_deref() - == Some("fresh") + == Some( + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh, + ) && let Some(latest) = registry.latest_complete_fresh(project_root).await { return latest; diff --git a/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs index 81ceaa6e84..0019c5012f 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs @@ -656,7 +656,9 @@ async fn status_serving_branch_reports_the_lane_serving_truth() { staleness_state: Option<&str>, rebuild_in_flight: bool| { let latest_generation_id = latest_generation_id.map(str::to_owned); - let staleness_state = staleness_state.map(str::to_owned); + let staleness_state = staleness_state.and_then( + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::from_wire, + ); let reader: tracedecay_contracts::code_index_freshness::CodeIndexFreshnessReader = std::sync::Arc::new(move |worktree_root: std::path::PathBuf| { let freshness = @@ -759,11 +761,11 @@ async fn status_serving_branch_reports_the_lane_serving_truth() { tracedecay_contracts::code_index_freshness::CodeGraphServingReadinessV1::Ready, ), coverage: if staleness == "fresh" { - "complete".to_owned() + tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete } else { - "partial_source_verification".to_owned() + tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::PartialSourceVerification }, - staleness_state: Some(staleness.to_owned()), + staleness_state: tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::from_wire(staleness), ..Default::default() }; Box::pin(async move { Some(freshness) }) @@ -898,7 +900,9 @@ async fn status_serving_branch_reports_the_lane_serving_truth() { code_graph_serving: Some( tracedecay_contracts::code_index_freshness::CodeGraphServingReadinessV1::Ready, ), - staleness_state: Some("fresh".to_owned()), + staleness_state: Some( + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh, + ), ..Default::default() }; Box::pin(async move { Some(freshness) }) @@ -1008,7 +1012,9 @@ async fn status_serving_branch_reports_the_lane_serving_truth() { worktree_root: worktree_root.display().to_string(), latest_generation_id: Some("generation.status-serving-truth.1".to_owned()), sealed_at_micros: Some(sealed_at_micros), - staleness_state: Some("stale".to_owned()), + staleness_state: Some( + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Stale, + ), ..Default::default() }; Box::pin(async move { Some(freshness) }) diff --git a/crates/tracedecay/src/mcp/tools/handlers/graph_search_dispatch_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/graph_search_dispatch_tests.rs index ef5866d49d..a57314bd0c 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/graph_search_dispatch_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/graph_search_dispatch_tests.rs @@ -269,15 +269,19 @@ fn freshness_reader( rebuild_in_flight: bool, ) -> CodeIndexFreshnessReader { let latest_generation_id = latest_generation_id.map(str::to_owned); - let staleness_state = staleness_state.to_owned(); + let staleness_state = + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::from_wire( + staleness_state, + ); std::sync::Arc::new(move |worktree_root: std::path::PathBuf| { let freshness = tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1 { worktree_root: worktree_root.display().to_string(), latest_generation_id: latest_generation_id.clone(), - staleness_state: Some(staleness_state.clone()), + staleness_state, rebuild_in_flight, hook_hint_count: Some(0), - coverage: "complete".to_owned(), + coverage: + tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete, ..Default::default() }; Box::pin(async move { Some(freshness) }) diff --git a/crates/tracedecay/tests/dashboard_api_test/delivery.rs b/crates/tracedecay/tests/dashboard_api_test/delivery.rs index df110a7724..722cf3be75 100644 --- a/crates/tracedecay/tests/dashboard_api_test/delivery.rs +++ b/crates/tracedecay/tests/dashboard_api_test/delivery.rs @@ -313,8 +313,10 @@ fn delivery_http_admission_freshness_reader() -> CodeIndexFreshnessReader { source_reference: Some("refs/heads/feature".to_owned()), source_revision: Some(DELIVERY_HTTP_ADMISSION_INDEXED_HEAD.to_owned()), latest_generation_id: Some("generation.delivery-http-admission.1".to_owned()), - coverage: "complete".to_owned(), - staleness_state: Some("fresh".to_owned()), + coverage: tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete, + staleness_state: Some( + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh, + ), sealed_at_micros: Some(30), ..CodeIndexWorktreeFreshnessV1::default() }) diff --git a/dashboard/src/workspaces/code/IndexFreshness.dom.test.tsx b/dashboard/src/workspaces/code/IndexFreshness.dom.test.tsx index bada7d04aa..2d112ab812 100644 --- a/dashboard/src/workspaces/code/IndexFreshness.dom.test.tsx +++ b/dashboard/src/workspaces/code/IndexFreshness.dom.test.tsx @@ -537,7 +537,7 @@ describe('Code index freshness', () => { ...worktree(), latest_generation_id: readyProgress.generation_id, staleness_state: 'stale', - coverage: 'partial', + coverage: 'partial_unverified_restore', rebuild_in_flight: false, progress: readyProgress, }, @@ -759,7 +759,9 @@ describe('Code index freshness', () => { it('reports a stale generation as stale against the reference it was sealed on', async () => { renderFreshness('partial', { - worktrees: [{ ...worktree(), staleness_state: 'stale', coverage: 'partial' }], + worktrees: [ + { ...worktree(), staleness_state: 'stale', coverage: 'partial_unverified_restore' }, + ], note: 'live daemon scheduler state; generation and scope come from the durable sealed generation', }); @@ -767,7 +769,7 @@ describe('Code index freshness', () => { const mount = document.querySelector('[data-worktree-staleness="stale"]'); expect(mount).toBeTruthy(); expect(mount?.textContent).toContain('refs/heads/codex/tracedecay-total-redesign-plan'); - expect(mount?.textContent).toContain('partial'); + expect(mount?.textContent).toContain('partial_unverified_restore'); }); it('reports an unreachable daemon as offline', async () => { From 516cdf3b7431d5f6fb7a3ebe0cbcab2c2db80d4e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 02:11:33 +0000 Subject: [PATCH 2/3] chore(contracts): regenerate typed freshness ladder schemas Co-authored-by: Zack Jackson --- .../schemas/dashboard-contracts.schema.json | 42 +++++++++++++++---- dashboard/src/contracts/generated.ts | 21 +++++++++- sdks/typescript/src/operations.ts | 8 ++-- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/dashboard/codegen/schemas/dashboard-contracts.schema.json b/dashboard/codegen/schemas/dashboard-contracts.schema.json index 2a5350dfe6..b4c22551b4 100644 --- a/dashboard/codegen/schemas/dashboard-contracts.schema.json +++ b/dashboard/codegen/schemas/dashboard-contracts.schema.json @@ -2961,6 +2961,18 @@ ], "type": "object" }, + "CodeIndexFreshnessCoverageV1": { + "description": "Coverage of one freshness read. Distinct from [`CodeIndexStalenessStateV1`]:\na generation can be fresh and still omit hook-hint counts, or unverified\nwhile the ladder would otherwise say ready.\n\n`Unobserved` is only the constructed default. A projected read never emits\nit, so an absent observation cannot be mistaken for `complete`.", + "enum": [ + "unobserved", + "complete", + "partial_refresh_in_progress", + "partial_source_verification", + "partial_unverified_restore", + "partial_hook_hint_overflow" + ], + "type": "string" + }, "CodeIndexFreshnessPayloadV1": { "properties": { "note": { @@ -3013,6 +3025,18 @@ ], "type": "object" }, + "CodeIndexStalenessStateV1": { + "description": "Closed staleness ladder for one mounted worktree.\n\nThe scheduler publishes one of these tokens. MCP, the dashboard, and the\nCLI must match the variant — not a hand-copied string — so a new ladder\nstate cannot appear at one caller and be missed at the others.", + "enum": [ + "fresh", + "stale", + "indexing", + "refreshing", + "verifying", + "parked" + ], + "type": "string" + }, "CodeIndexWorkerLimitingReasonV1": { "enum": [ "automatic_all_cores", @@ -3155,8 +3179,8 @@ "description": "Whether that generation's verified graph projection can serve reads." }, "coverage": { - "description": "Whether this read covers the complete mounted scheduler state.", - "type": "string" + "$ref": "#/$defs/CodeIndexFreshnessCoverageV1", + "description": "Whether this read covers the complete mounted scheduler state." }, "generation_recovery": { "anyOf": [ @@ -3257,11 +3281,15 @@ ] }, "staleness_state": { - "description": "Staleness-ladder state from the last scheduler execution. `fresh` means\nthe scheduler most recently observed a fresh source; the status read\ndoes not probe the worktree to revalidate that observation.", - "type": [ - "string", - "null" - ] + "anyOf": [ + { + "$ref": "#/$defs/CodeIndexStalenessStateV1" + }, + { + "type": "null" + } + ], + "description": "Staleness-ladder state from the last scheduler execution. `fresh` means\nthe scheduler most recently observed a fresh source; the status read\ndoes not probe the worktree to revalidate that observation." }, "worktree_id": { "description": "Stable worktree identity resolved by the scheduler.", diff --git a/dashboard/src/contracts/generated.ts b/dashboard/src/contracts/generated.ts index 20d9bed37d..9713ad5358 100644 --- a/dashboard/src/contracts/generated.ts +++ b/dashboard/src/contracts/generated.ts @@ -821,6 +821,15 @@ export const CodeIndexConvergenceParkedV1Schema = z.object({ }); export type CodeIndexConvergenceParkedV1 = z.infer; +/** Coverage of one freshness read. Distinct from [`CodeIndexStalenessStateV1`]: +a generation can be fresh and still omit hook-hint counts, or unverified +while the ladder would otherwise say ready. + +`Unobserved` is only the constructed default. A projected read never emits +it, so an absent observation cannot be mistaken for `complete`. */ +export const CodeIndexFreshnessCoverageV1Schema = z.enum(["complete", "partial_hook_hint_overflow", "partial_refresh_in_progress", "partial_source_verification", "partial_unverified_restore", "unobserved"]); +export type CodeIndexFreshnessCoverageV1 = z.infer; + export const CodeIndexFreshnessPayloadV1Schema = z.object({ note: z.string(), worktrees: z.array(z.lazy(() => CodeIndexWorktreeFreshnessV1Schema)), @@ -840,6 +849,14 @@ export const CodeIndexGenerationRecoveryV1Schema = z.object({ }); export type CodeIndexGenerationRecoveryV1 = z.infer; +/** Closed staleness ladder for one mounted worktree. + +The scheduler publishes one of these tokens. MCP, the dashboard, and the +CLI must match the variant — not a hand-copied string — so a new ladder +state cannot appear at one caller and be missed at the others. */ +export const CodeIndexStalenessStateV1Schema = z.enum(["fresh", "indexing", "parked", "refreshing", "stale", "verifying"]); +export type CodeIndexStalenessStateV1 = z.infer; + export const CodeIndexWorkerLimitingReasonV1Schema = z.enum(["automatic_all_cores", "automatic_half_cores", "configured_exact", "environment_override", "resident_memory"]); export type CodeIndexWorkerLimitingReasonV1 = z.infer; @@ -881,7 +898,7 @@ keeping one authority for the freshness shape. */ export const CodeIndexWorktreeFreshnessV1Schema = z.object({ clone_index: z.union([z.lazy(() => CodeCloneIndexStatusV1Schema), z.null()]).optional(), code_graph_serving: z.union([z.lazy(() => CodeGraphServingReadinessV1Schema), z.null()]).optional(), - coverage: z.string(), + coverage: z.lazy(() => CodeIndexFreshnessCoverageV1Schema), generation_recovery: z.union([z.lazy(() => CodeIndexGenerationRecoveryV1Schema), z.null()]).optional(), hook_hint_count: z.number().int().safe().min(0).nullable(), last_reconcile_micros: z.number().int().safe().nullable(), @@ -894,7 +911,7 @@ export const CodeIndexWorktreeFreshnessV1Schema = z.object({ snapshot_content_identity: z.string().nullable(), source_reference: z.string().nullable(), source_revision: z.string().nullable(), - staleness_state: z.string().nullable(), + staleness_state: z.union([z.lazy(() => CodeIndexStalenessStateV1Schema), z.null()]), worktree_id: z.string().nullable(), worktree_root: z.string(), }); diff --git a/sdks/typescript/src/operations.ts b/sdks/typescript/src/operations.ts index 3666ae04bb..64431ccdb9 100644 --- a/sdks/typescript/src/operations.ts +++ b/sdks/typescript/src/operations.ts @@ -97,6 +97,7 @@ export type CodeFacetDimension = "kind" | "language" | "path"; export type CodeFacetRecord = { readonly count: number; readonly dimension: CodeFacetDimension; readonly value: string }; export type CodeGenerationId = string; export type CodeGraphReadFreshnessV1 = { readonly state: "current"; readonly [key: string]: unknown } | { readonly rebuild_in_flight: boolean; readonly sealed_at: UtcMicros; readonly state: "last_complete_stale"; readonly [key: string]: unknown }; +export type CodeIndexStalenessStateV1 = "fresh" | "stale" | "indexing" | "refreshing" | "verifying" | "parked"; export type CodeIndexWorkerSelectionV1 = { readonly mode: "automatic" } | { readonly mode: "exact"; readonly workers: number }; export type CodeLexicalField = "symbol_name" | "qualified_name" | "path" | "signature" | "documentation" | "body_text" | "preamble_text" | "exact_term" | "subtoken"; export type CodeLexicalFieldFilter = { readonly field: CodeLexicalField; readonly include: boolean }; @@ -495,7 +496,7 @@ export type PortOrderSymbolV1 = { readonly depends_on?: readonly string[] | null export type PortTargetOnlySymbolV1 = { readonly file: string; readonly kind: string; readonly line: number; readonly name: string }; export type PortUnmatchedSymbolV1 = { readonly kind: string; readonly line: number; readonly name: string }; export type PrimitiveFreshnessStateV1 = "fresh" | "possibly_stale"; -export type PrimitiveIndexingStateV1 = { readonly latest_generation?: string | null; readonly reason?: string | null; readonly rebuild_in_flight?: boolean | null; readonly served_generation?: string | null; readonly stale_lanes?: readonly string[]; readonly staleness_state?: string | null; readonly summary: string }; +export type PrimitiveIndexingStateV1 = { readonly latest_generation?: string | null; readonly reason?: string | null; readonly rebuild_in_flight?: boolean | null; readonly served_generation?: string | null; readonly stale_lanes?: readonly string[]; readonly staleness_state?: CodeIndexStalenessStateV1 | null; readonly summary: string }; export type PrimitiveLaneCompleteV1 = "complete"; export type PrimitiveLaneStateV1 = "stale" | "partial" | "unavailable"; export type PrimitiveLaneStatusV1 = PrimitiveLaneCompleteV1 | { readonly generation?: string | null; readonly reason?: string | null; readonly status: PrimitiveLaneStateV1; readonly [key: string]: unknown }; @@ -1368,6 +1369,7 @@ const DEFINITIONS = { CodeFacetRecord: {"additionalProperties":false,"properties":{"count":{"format":"uint64","minimum":0,"type":"integer"},"dimension":{"$ref":"#/$defs/CodeFacetDimension"},"value":{"type":"string"}},"required":["dimension","value","count"],"type":"object"}, CodeGenerationId: {"description":"Strongly typed canonical identity: `CodeGenerationId`.","type":"string"}, CodeGraphReadFreshnessV1: {"description":"Freshness of the generation a verified graph read serves.","oneOf":[{"properties":{"state":{"const":"current","type":"string"}},"required":["state"],"type":"object"},{"properties":{"rebuild_in_flight":{"type":"boolean"},"sealed_at":{"$ref":"#/$defs/UtcMicros"},"state":{"const":"last_complete_stale","type":"string"}},"required":["state","sealed_at","rebuild_in_flight"],"type":"object"}]}, + CodeIndexStalenessStateV1: {"description":"Closed staleness ladder for one mounted worktree.\n\nThe scheduler publishes one of these tokens. MCP, the dashboard, and the\nCLI must match the variant — not a hand-copied string — so a new ladder\nstate cannot appear at one caller and be missed at the others.","enum":["fresh","stale","indexing","refreshing","verifying","parked"],"type":"string"}, CodeIndexWorkerSelectionV1: {"description":"Profile-level worker-count intent for the process-wide code-index pool.\n`Automatic` delegates the concrete count to runtime resource admission;\n`Exact` persists an operator-requested positive worker count.","oneOf":[{"additionalProperties":false,"properties":{"mode":{"const":"automatic","type":"string"}},"required":["mode"],"type":"object"},{"additionalProperties":false,"properties":{"mode":{"const":"exact","type":"string"},"workers":{"format":"uint16","maximum":65535,"minimum":0,"type":"integer"}},"required":["mode","workers"],"type":"object"}]}, CodeLexicalField: {"description":"Typed code fields accepted by the generation-owned lexical authority.","enum":["symbol_name","qualified_name","path","signature","documentation","body_text","preamble_text","exact_term","subtoken"],"type":"string"}, CodeLexicalFieldFilter: {"additionalProperties":false,"properties":{"field":{"$ref":"#/$defs/CodeLexicalField"},"include":{"type":"boolean"}},"required":["field","include"],"type":"object"}, @@ -1766,7 +1768,7 @@ const DEFINITIONS = { PortTargetOnlySymbolV1: {"additionalProperties":false,"properties":{"file":{"type":"string"},"kind":{"type":"string"},"line":{"format":"uint32","minimum":0,"type":"integer"},"name":{"type":"string"}},"required":["name","kind","file","line"],"type":"object"}, PortUnmatchedSymbolV1: {"additionalProperties":false,"properties":{"kind":{"type":"string"},"line":{"format":"uint32","minimum":0,"type":"integer"},"name":{"type":"string"}},"required":["name","kind","line"],"type":"object"}, PrimitiveFreshnessStateV1: {"description":"Whether the served code generation is known current at serve time.","enum":["fresh","possibly_stale"],"type":"string"}, - PrimitiveIndexingStateV1: {"additionalProperties":false,"description":"The indexing state behind a `possibly_stale` verdict: the served\ngeneration, the scheduler's latest sealed generation, its staleness-ladder\nstate, and the lanes that answered from an older generation. `summary` is\nthe one-line rendering agents read.","properties":{"latest_generation":{"type":["string","null"]},"reason":{"type":["string","null"]},"rebuild_in_flight":{"type":["boolean","null"]},"served_generation":{"type":["string","null"]},"stale_lanes":{"items":{"type":"string"},"type":"array"},"staleness_state":{"type":["string","null"]},"summary":{"type":"string"}},"required":["summary"],"type":"object"}, + PrimitiveIndexingStateV1: {"additionalProperties":false,"description":"The indexing state behind a `possibly_stale` verdict: the served\ngeneration, the scheduler's latest sealed generation, its staleness-ladder\nstate, and the lanes that answered from an older generation. `summary` is\nthe one-line rendering agents read.","properties":{"latest_generation":{"type":["string","null"]},"reason":{"type":["string","null"]},"rebuild_in_flight":{"type":["boolean","null"]},"served_generation":{"type":["string","null"]},"stale_lanes":{"items":{"type":"string"},"type":"array"},"staleness_state":{"anyOf":[{"$ref":"#/$defs/CodeIndexStalenessStateV1"},{"type":"null"}]},"summary":{"type":"string"}},"required":["summary"],"type":"object"}, PrimitiveLaneCompleteV1: {"enum":["complete"],"type":"string"}, PrimitiveLaneStateV1: {"enum":["stale","partial","unavailable"],"type":"string"}, PrimitiveLaneStatusV1: {"anyOf":[{"$ref":"#/$defs/PrimitiveLaneCompleteV1"},{"properties":{"generation":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"$ref":"#/$defs/PrimitiveLaneStateV1"}},"required":["status"],"type":"object"}]}, @@ -2260,7 +2262,7 @@ const SCHEMAS: readonly CanonicalJsonSchema[] = [ {"$defs":{"AccessRuleId":DEFINITIONS.AccessRuleId,"ActorId":DEFINITIONS.ActorId,"AnalyzerEnvironmentVariable":DEFINITIONS.AnalyzerEnvironmentVariable,"AnalyzerExecutableId":DEFINITIONS.AnalyzerExecutableId,"AnalyzerExecutableReferenceV1":DEFINITIONS.AnalyzerExecutableReferenceV1,"AnalyzerLanguageId":DEFINITIONS.AnalyzerLanguageId,"AnalyzerLanguageSelectionV1":DEFINITIONS.AnalyzerLanguageSelectionV1,"AnalyzerPrivacyClassV1":DEFINITIONS.AnalyzerPrivacyClassV1,"AnalyzerResourceLimitsV1":DEFINITIONS.AnalyzerResourceLimitsV1,"AnalyzerRestartPolicyV1":DEFINITIONS.AnalyzerRestartPolicyV1,"AnalyzerSettingsV1":DEFINITIONS.AnalyzerSettingsV1,"AnalyzerStructuredValueV1":DEFINITIONS.AnalyzerStructuredValueV1,"AuthorityRef":DEFINITIONS.AuthorityRef,"AutomaticWorktreeGcV1":DEFINITIONS.AutomaticWorktreeGcV1,"AutomationBackendV1":DEFINITIONS.AutomationBackendV1,"AutomationHostModeV1":DEFINITIONS.AutomationHostModeV1,"AutomationSettingsV1":DEFINITIONS.AutomationSettingsV1,"AutomationTaskSetV1":DEFINITIONS.AutomationTaskSetV1,"AutomationTaskSettingsV1":DEFINITIONS.AutomationTaskSettingsV1,"BranchCollisionPolicyV1":DEFINITIONS.BranchCollisionPolicyV1,"BranchNameComponentV1":DEFINITIONS.BranchNameComponentV1,"BranchNameSeparatorV1":DEFINITIONS.BranchNameSeparatorV1,"BranchNamingPolicyV1":DEFINITIONS.BranchNamingPolicyV1,"BranchTopologyKindV1":DEFINITIONS.BranchTopologyKindV1,"BranchTopologyPolicyV1":DEFINITIONS.BranchTopologyPolicyV1,"CanonicalGitRefNameV1":DEFINITIONS.CanonicalGitRefNameV1,"CanonicalGitRefPrefix":DEFINITIONS.CanonicalGitRefPrefix,"CapabilityId":DEFINITIONS.CapabilityId,"CodeIndexWorkerSelectionV1":DEFINITIONS.CodeIndexWorkerSelectionV1,"ConfigurationIdempotencyKey":DEFINITIONS.ConfigurationIdempotencyKey,"ConfigurationLayerIdV1":DEFINITIONS.ConfigurationLayerIdV1,"ConfigurationRevisionId":DEFINITIONS.ConfigurationRevisionId,"ConfigurationValueV1":DEFINITIONS.ConfigurationValueV1,"ContextScoutConfigurationLimitsV1":DEFINITIONS.ContextScoutConfigurationLimitsV1,"ContextScoutConfigurationModeV1":DEFINITIONS.ContextScoutConfigurationModeV1,"ContextScoutConfigurationStateV1":DEFINITIONS.ContextScoutConfigurationStateV1,"ContextScoutConfiguredModelPathV1":DEFINITIONS.ContextScoutConfiguredModelPathV1,"ContextScoutSettingsV1":DEFINITIONS.ContextScoutSettingsV1,"CredentialReferenceId":DEFINITIONS.CredentialReferenceId,"CrossMergeModeV1":DEFINITIONS.CrossMergeModeV1,"CrossMergePolicyV1":DEFINITIONS.CrossMergePolicyV1,"GitHubStackedPullRequestPolicyV1":DEFINITIONS.GitHubStackedPullRequestPolicyV1,"HistoryRewritePolicyV1":DEFINITIONS.HistoryRewritePolicyV1,"LocatorDigest":DEFINITIONS.LocatorDigest,"ManifestDigest":DEFINITIONS.ManifestDigest,"ProjectId":DEFINITIONS.ProjectId,"ProtectedRefDispositionV1":DEFINITIONS.ProtectedRefDispositionV1,"ProtectedRefRuleV1":DEFINITIONS.ProtectedRefRuleV1,"ProtectedRefSelectorV1":DEFINITIONS.ProtectedRefSelectorV1,"ProviderId":DEFINITIONS.ProviderId,"QueryCollectionId":DEFINITIONS.QueryCollectionId,"RepositoryId":DEFINITIONS.RepositoryId,"RepositoryPlacementScopeV1":DEFINITIONS.RepositoryPlacementScopeV1,"RequiredCheckExpectationV1":DEFINITIONS.RequiredCheckExpectationV1,"RequiredCheckV1":DEFINITIONS.RequiredCheckV1,"ReviewRequirementV1":DEFINITIONS.ReviewRequirementV1,"ReviewTopologyKindV1":DEFINITIONS.ReviewTopologyKindV1,"ReviewTopologyPolicyV1":DEFINITIONS.ReviewTopologyPolicyV1,"RuleEffect":DEFINITIONS.RuleEffect,"ScopeAccessRule":DEFINITIONS.ScopeAccessRule,"ScopeAccessSubjectV1":DEFINITIONS.ScopeAccessSubjectV1,"ScopeControlOperationV1":DEFINITIONS.ScopeControlOperationV1,"ScopeSourceBinding":DEFINITIONS.ScopeSourceBinding,"SensitiveFilesystemLocatorV1":DEFINITIONS.SensitiveFilesystemLocatorV1,"SettingKey":DEFINITIONS.SettingKey,"SourceBindingId":DEFINITIONS.SourceBindingId,"SourceKindV1":DEFINITIONS.SourceKindV1,"TopologyConcurrencyPolicyV1":DEFINITIONS.TopologyConcurrencyPolicyV1,"TopologyEscalationPolicyV1":DEFINITIONS.TopologyEscalationPolicyV1,"TopologyGatePolicyV1":DEFINITIONS.TopologyGatePolicyV1,"TopologyNotificationLevelV1":DEFINITIONS.TopologyNotificationLevelV1,"UserProfileId":DEFINITIONS.UserProfileId,"UtcMicros":DEFINITIONS.UtcMicros,"WorkApprovalPolicy":DEFINITIONS.WorkApprovalPolicy,"WorkContentLocationClassV1":DEFINITIONS.WorkContentLocationClassV1,"WorkEffortClassV1":DEFINITIONS.WorkEffortClassV1,"WorkEgressPolicy":DEFINITIONS.WorkEgressPolicy,"WorkExecutableBindingV1":DEFINITIONS.WorkExecutableBindingV1,"WorkExecutableCapabilityV1":DEFINITIONS.WorkExecutableCapabilityV1,"WorkExecutableReference":DEFINITIONS.WorkExecutableReference,"WorkExecutionLimits":DEFINITIONS.WorkExecutionLimits,"WorkExpertiseCategoryV1":DEFINITIONS.WorkExpertiseCategoryV1,"WorkExpertiseConsentV1":DEFINITIONS.WorkExpertiseConsentV1,"WorkFallbackTopology":DEFINITIONS.WorkFallbackTopology,"WorkFilesystemPolicy":DEFINITIONS.WorkFilesystemPolicy,"WorkOrdinalBandV1":DEFINITIONS.WorkOrdinalBandV1,"WorkProviderRouteId":DEFINITIONS.WorkProviderRouteId,"WorkProviderRouteV1":DEFINITIONS.WorkProviderRouteV1,"WorkRouteCandidateV1":DEFINITIONS.WorkRouteCandidateV1,"WorkRouteExecutionProfileV1":DEFINITIONS.WorkRouteExecutionProfileV1,"WorkSandboxPolicy":DEFINITIONS.WorkSandboxPolicy,"WorkTopologyPolicyV1":DEFINITIONS.WorkTopologyPolicyV1,"WorktreeCleanlinessRequirementV1":DEFINITIONS.WorktreeCleanlinessRequirementV1,"WorktreePlacementModeV1":DEFINITIONS.WorktreePlacementModeV1,"WorktreeRetentionPolicyV1":DEFINITIONS.WorktreeRetentionPolicyV1,"WorktreeRootPolicyV1":DEFINITIONS.WorktreeRootPolicyV1},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"description":"Typed revision-CAS input for a configuration write through the daemon\ninvocation boundary.","properties":{"expected_revision":{"$ref":"#/$defs/ConfigurationRevisionId"},"idempotency_key":{"$ref":"#/$defs/ConfigurationIdempotencyKey"},"key":{"$ref":"#/$defs/SettingKey"},"layer":{"$ref":"#/$defs/ConfigurationLayerIdV1"},"value":{"$ref":"#/$defs/ConfigurationValueV1"}},"required":["layer","key","value","expected_revision","idempotency_key"],"title":"ConfigurationSetRequestV1","type":"object"}, {"$defs":{"ConfigurationIdempotencyKey":DEFINITIONS.ConfigurationIdempotencyKey,"ConfigurationLayerIdV1":DEFINITIONS.ConfigurationLayerIdV1,"ConfigurationRevisionId":DEFINITIONS.ConfigurationRevisionId,"ProjectId":DEFINITIONS.ProjectId,"QueryCollectionId":DEFINITIONS.QueryCollectionId,"SettingKey":DEFINITIONS.SettingKey,"UserProfileId":DEFINITIONS.UserProfileId},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"expected_revision":{"$ref":"#/$defs/ConfigurationRevisionId"},"idempotency_key":{"$ref":"#/$defs/ConfigurationIdempotencyKey"},"key":{"$ref":"#/$defs/SettingKey"},"layer":{"$ref":"#/$defs/ConfigurationLayerIdV1"}},"required":["layer","key","expected_revision","idempotency_key"],"title":"ConfigurationUnsetRequestV1","type":"object"}, {"$defs":{"ContextModeV1":DEFINITIONS.ContextModeV1},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"include_code":{"type":["boolean","null"]},"include_memory":{"type":["boolean","null"]},"lexical_anchors":{"description":"Exact identifiers or technical terms ranked through the lexical lane as\nadditional routes fused with the task text. Bounded and validated by\nthe retrieval kernel; a violation is a typed request rejection.","items":{"type":"string"},"type":["array","null"]},"max_code_blocks":{"format":"uint32","minimum":0,"type":["integer","null"]},"max_nodes":{"format":"uint32","minimum":0,"type":["integer","null"]},"memory_limit":{"format":"uint32","minimum":0,"type":["integer","null"]},"memory_min_trust":{"format":"double","type":["number","null"]},"mode":{"anyOf":[{"$ref":"#/$defs/ContextModeV1"},{"type":"null"}]},"prefer_symbol":{"description":"Add a symbol-name lexical route for the identifier-shaped words of the\ntask text.","type":["boolean","null"]},"task":{"type":"string"}},"required":["task"],"title":"ContextSurfaceRequestV1","type":"object"}, - {"$defs":{"ContextCodeBlockV1":DEFINITIONS.ContextCodeBlockV1,"ContextModeV1":DEFINITIONS.ContextModeV1,"ContextSearchMatchV1":DEFINITIONS.ContextSearchMatchV1,"FactAssertionId":DEFINITIONS.FactAssertionId,"FactCategoryV1":DEFINITIONS.FactCategoryV1,"FactCommitOwnerV1":DEFINITIONS.FactCommitOwnerV1,"FactEventId":DEFINITIONS.FactEventId,"FactId":DEFINITIONS.FactId,"FactIdentitySourceResultV1":DEFINITIONS.FactIdentitySourceResultV1,"FactSearchGraphCoverageV1":DEFINITIONS.FactSearchGraphCoverageV1,"FactSearchGraphDegradationV1":DEFINITIONS.FactSearchGraphDegradationV1,"FactSearchHitV1":DEFINITIONS.FactSearchHitV1,"FactSearchScoresV1":DEFINITIONS.FactSearchScoresV1,"FactTelemetryV1":DEFINITIONS.FactTelemetryV1,"FactV1":DEFINITIONS.FactV1,"LocatorDigest":DEFINITIONS.LocatorDigest,"PrimitiveFreshnessStateV1":DEFINITIONS.PrimitiveFreshnessStateV1,"PrimitiveIndexingStateV1":DEFINITIONS.PrimitiveIndexingStateV1,"PrimitiveLaneCompleteV1":DEFINITIONS.PrimitiveLaneCompleteV1,"PrimitiveLaneStateV1":DEFINITIONS.PrimitiveLaneStateV1,"PrimitiveLaneStatusV1":DEFINITIONS.PrimitiveLaneStatusV1,"PrimitiveRecallV1":DEFINITIONS.PrimitiveRecallV1,"PrimitiveSearchCoverageV1":DEFINITIONS.PrimitiveSearchCoverageV1,"PrimitiveSearchFreshnessV1":DEFINITIONS.PrimitiveSearchFreshnessV1,"PrimitiveSymbolLocationV1":DEFINITIONS.PrimitiveSymbolLocationV1,"PrimitiveUnavailableEvidenceV1":DEFINITIONS.PrimitiveUnavailableEvidenceV1,"PrimitiveUnavailableStatusV1":DEFINITIONS.PrimitiveUnavailableStatusV1,"ProjectId":DEFINITIONS.ProjectId,"ProvenanceId":DEFINITIONS.ProvenanceId,"RetrievalAnchorId":DEFINITIONS.RetrievalAnchorId,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"code":{"items":{"$ref":"#/$defs/ContextCodeBlockV1"},"type":"array"},"code_generation":{"type":["string","null"]},"coverage":{"$ref":"#/$defs/PrimitiveSearchCoverageV1"},"freshness":{"$ref":"#/$defs/PrimitiveSearchFreshnessV1","description":"Freshness of the served code generation, derived from the typed lane\ncoverage and the daemon scheduler's worktree state."},"memory_graph_coverage":{"anyOf":[{"$ref":"#/$defs/FactSearchGraphCoverageV1"},{"type":"null"}]},"memory_matches":{"items":{"$ref":"#/$defs/FactSearchHitV1"},"type":"array"},"memory_matches_error":{"type":["string","null"]},"mode":{"$ref":"#/$defs/ContextModeV1"},"related_symbols":{"items":{"$ref":"#/$defs/PrimitiveSymbolLocationV1"},"type":"array"},"search_matches":{"items":{"$ref":"#/$defs/ContextSearchMatchV1"},"type":"array"},"symbols":{"items":{"$ref":"#/$defs/PrimitiveSymbolLocationV1"},"type":"array"},"task":{"type":"string"},"verified_graph_evidence":{"anyOf":[{"$ref":"#/$defs/PrimitiveUnavailableEvidenceV1"},{"type":"null"}]}},"required":["task","mode","freshness","symbols","related_symbols","code","coverage","memory_matches"],"title":"ContextResultV1","type":"object"}, + {"$defs":{"CodeIndexStalenessStateV1":DEFINITIONS.CodeIndexStalenessStateV1,"ContextCodeBlockV1":DEFINITIONS.ContextCodeBlockV1,"ContextModeV1":DEFINITIONS.ContextModeV1,"ContextSearchMatchV1":DEFINITIONS.ContextSearchMatchV1,"FactAssertionId":DEFINITIONS.FactAssertionId,"FactCategoryV1":DEFINITIONS.FactCategoryV1,"FactCommitOwnerV1":DEFINITIONS.FactCommitOwnerV1,"FactEventId":DEFINITIONS.FactEventId,"FactId":DEFINITIONS.FactId,"FactIdentitySourceResultV1":DEFINITIONS.FactIdentitySourceResultV1,"FactSearchGraphCoverageV1":DEFINITIONS.FactSearchGraphCoverageV1,"FactSearchGraphDegradationV1":DEFINITIONS.FactSearchGraphDegradationV1,"FactSearchHitV1":DEFINITIONS.FactSearchHitV1,"FactSearchScoresV1":DEFINITIONS.FactSearchScoresV1,"FactTelemetryV1":DEFINITIONS.FactTelemetryV1,"FactV1":DEFINITIONS.FactV1,"LocatorDigest":DEFINITIONS.LocatorDigest,"PrimitiveFreshnessStateV1":DEFINITIONS.PrimitiveFreshnessStateV1,"PrimitiveIndexingStateV1":DEFINITIONS.PrimitiveIndexingStateV1,"PrimitiveLaneCompleteV1":DEFINITIONS.PrimitiveLaneCompleteV1,"PrimitiveLaneStateV1":DEFINITIONS.PrimitiveLaneStateV1,"PrimitiveLaneStatusV1":DEFINITIONS.PrimitiveLaneStatusV1,"PrimitiveRecallV1":DEFINITIONS.PrimitiveRecallV1,"PrimitiveSearchCoverageV1":DEFINITIONS.PrimitiveSearchCoverageV1,"PrimitiveSearchFreshnessV1":DEFINITIONS.PrimitiveSearchFreshnessV1,"PrimitiveSymbolLocationV1":DEFINITIONS.PrimitiveSymbolLocationV1,"PrimitiveUnavailableEvidenceV1":DEFINITIONS.PrimitiveUnavailableEvidenceV1,"PrimitiveUnavailableStatusV1":DEFINITIONS.PrimitiveUnavailableStatusV1,"ProjectId":DEFINITIONS.ProjectId,"ProvenanceId":DEFINITIONS.ProvenanceId,"RetrievalAnchorId":DEFINITIONS.RetrievalAnchorId,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"code":{"items":{"$ref":"#/$defs/ContextCodeBlockV1"},"type":"array"},"code_generation":{"type":["string","null"]},"coverage":{"$ref":"#/$defs/PrimitiveSearchCoverageV1"},"freshness":{"$ref":"#/$defs/PrimitiveSearchFreshnessV1","description":"Freshness of the served code generation, derived from the typed lane\ncoverage and the daemon scheduler's worktree state."},"memory_graph_coverage":{"anyOf":[{"$ref":"#/$defs/FactSearchGraphCoverageV1"},{"type":"null"}]},"memory_matches":{"items":{"$ref":"#/$defs/FactSearchHitV1"},"type":"array"},"memory_matches_error":{"type":["string","null"]},"mode":{"$ref":"#/$defs/ContextModeV1"},"related_symbols":{"items":{"$ref":"#/$defs/PrimitiveSymbolLocationV1"},"type":"array"},"search_matches":{"items":{"$ref":"#/$defs/ContextSearchMatchV1"},"type":"array"},"symbols":{"items":{"$ref":"#/$defs/PrimitiveSymbolLocationV1"},"type":"array"},"task":{"type":"string"},"verified_graph_evidence":{"anyOf":[{"$ref":"#/$defs/PrimitiveUnavailableEvidenceV1"},{"type":"null"}]}},"required":["task","mode","freshness","symbols","related_symbols","code","coverage","memory_matches"],"title":"ContextResultV1","type":"object"}, {"$defs":{"ContextScoutAddressV1":DEFINITIONS.ContextScoutAddressV1},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"address":{"$ref":"#/$defs/ContextScoutAddressV1"}},"required":["address"],"title":"ContextScoutExactAddressRequestV1","type":"object"}, {"$defs":{"ContextScoutLimitsV1":DEFINITIONS.ContextScoutLimitsV1,"ContextScoutModelOutcomeV1":DEFINITIONS.ContextScoutModelOutcomeV1},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"exhausted":{"type":"boolean"},"last_estimated_cost_microusd":{"format":"uint64","minimum":0,"type":["integer","null"]},"last_input_tokens":{"format":"uint64","minimum":0,"type":["integer","null"]},"last_model_outcome":{"anyOf":[{"$ref":"#/$defs/ContextScoutModelOutcomeV1"},{"type":"null"}]},"last_output_tokens":{"format":"uint64","minimum":0,"type":["integer","null"]},"limits":{"$ref":"#/$defs/ContextScoutLimitsV1"}},"required":["limits","exhausted"],"title":"ContextScoutBudgetResultV1","type":"object"}, {"$defs":{"ContextScoutAddressV1":DEFINITIONS.ContextScoutAddressV1,"ContextScoutWorkV1":DEFINITIONS.ContextScoutWorkV1},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"address":{"$ref":"#/$defs/ContextScoutAddressV1"},"idempotency_key":{"type":"string"},"work":{"$ref":"#/$defs/ContextScoutWorkV1"}},"required":["address","work","idempotency_key"],"title":"ContextScoutCancelRequestV1","type":"object"}, From 102d163a6d65d4f68aeb269d6b1ff5f29f172533 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 02:22:31 +0000 Subject: [PATCH 3/3] fix(code-index): satisfy clippy on typed freshness reads Co-authored-by: Zack Jackson --- crates/tracedecay-contracts/src/code_index_freshness.rs | 9 ++------- .../src/code_index_freshness_api.rs | 8 ++++---- .../src/handlers/graph/search_freshness.rs | 5 ++--- .../tracedecay/src/mcp/tools/handlers/dispatch_tests.rs | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/tracedecay-contracts/src/code_index_freshness.rs b/crates/tracedecay-contracts/src/code_index_freshness.rs index f2fbcfd6d2..ff669c1242 100644 --- a/crates/tracedecay-contracts/src/code_index_freshness.rs +++ b/crates/tracedecay-contracts/src/code_index_freshness.rs @@ -352,9 +352,10 @@ impl std::fmt::Display for CodeIndexStalenessStateV1 { /// /// `Unobserved` is only the constructed default. A projected read never emits /// it, so an absent observation cannot be mistaken for `complete`. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum CodeIndexFreshnessCoverageV1 { + #[default] Unobserved, Complete, PartialRefreshInProgress, @@ -363,12 +364,6 @@ pub enum CodeIndexFreshnessCoverageV1 { PartialHookHintOverflow, } -impl Default for CodeIndexFreshnessCoverageV1 { - fn default() -> Self { - Self::Unobserved - } -} - impl CodeIndexFreshnessCoverageV1 { #[must_use] pub const fn as_str(self) -> &'static str { diff --git a/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs b/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs index 91866de018..60bb3308f3 100644 --- a/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs +++ b/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs @@ -105,10 +105,10 @@ async fn project_code_index_freshness( } else { format!( "scheduler freshness state is {}; only fresh serves as current", - worktree - .staleness_state - .map(|state| state.as_str()) - .unwrap_or("unreported") + worktree.staleness_state.map_or( + "unreported", + CodeIndexStalenessStateV1::as_str, + ) ) }; DashboardEnvelopeV1::new( diff --git a/crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs b/crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs index 3383cb9ba5..ce273cc068 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs @@ -112,13 +112,12 @@ pub(super) fn search_freshness( "state={} rebuild_in_flight={}", state .staleness_state - .map(|state| state.as_str()) - .unwrap_or("unknown"), + .map_or("unknown", CodeIndexStalenessStateV1::as_str), state.rebuild_in_flight ); ( state.latest_generation_id.clone(), - state.staleness_state.clone(), + state.staleness_state, Some(state.rebuild_in_flight), ) } diff --git a/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs index 0019c5012f..9fa3c93bff 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs @@ -665,7 +665,7 @@ async fn status_serving_branch_reports_the_lane_serving_truth() { tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1 { worktree_root: worktree_root.display().to_string(), latest_generation_id: latest_generation_id.clone(), - staleness_state: staleness_state.clone(), + staleness_state, rebuild_in_flight, ..Default::default() };