#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use crate::search::readiness::{
ArchiveRiskLevel, CanonicalDbAvailability, DerivedAssetTruthTable, LexicalReadinessState,
RecommendedAction, SafeNextAction, SearchRefinementLevel, SemanticReadinessState,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SurfaceKind {
Health,
Status,
Triage,
SearchMeta,
}
impl SurfaceKind {
fn defers_rich_probes(self) -> bool {
matches!(self, Self::Health)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ReadinessClass {
Ready,
StaleSearchable,
Repairing,
Missing,
CorruptQuarantined,
DbUnusable,
Unreachable,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ReadinessSummary {
pub surface: SurfaceKind,
pub class: ReadinessClass,
pub is_searchable: bool,
pub recommended_action: RecommendedAction,
pub safe_next_action: SafeNextAction,
pub lexical: LexicalReadinessState,
pub semantic: SemanticReadinessState,
pub archive_risk: ArchiveRiskLevel,
pub quarantine_incomplete: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub realized_refinement: Option<SearchRefinementLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lexical_fallback_reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deferred_fields: Vec<String>,
}
fn classify(table: &DerivedAssetTruthTable) -> ReadinessClass {
match table.db {
CanonicalDbAvailability::Unreachable => return ReadinessClass::Unreachable,
CanonicalDbAvailability::OpenFailed | CanonicalDbAvailability::Corrupt => {
return ReadinessClass::DbUnusable;
}
CanonicalDbAvailability::Missing => return ReadinessClass::Missing,
CanonicalDbAvailability::Available => {}
}
match table.readiness.lexical {
LexicalReadinessState::Ready => ReadinessClass::Ready,
LexicalReadinessState::StaleButSearchable => ReadinessClass::StaleSearchable,
LexicalReadinessState::Repairing => ReadinessClass::Repairing,
LexicalReadinessState::Missing => ReadinessClass::Missing,
LexicalReadinessState::CorruptQuarantined => ReadinessClass::CorruptQuarantined,
}
}
pub(crate) fn project(table: &DerivedAssetTruthTable, surface: SurfaceKind) -> ReadinessSummary {
let class = classify(table);
let recommended_action = table.readiness.recommended_action();
let safe_next_action = table.safe_next_command().action;
let (realized_refinement, lexical_fallback_reason) = if surface == SurfaceKind::SearchMeta {
let refinement = table.readiness.last_search_refinement;
let fallback = match refinement {
Some(SearchRefinementLevel::LexicalOnly) => Some(match table.readiness.semantic {
SemanticReadinessState::Absent => "semantic_absent".to_string(),
SemanticReadinessState::Backfilling => "semantic_backfilling".to_string(),
SemanticReadinessState::PolicyDisabled => "semantic_policy_disabled".to_string(),
_ => "semantic_not_applied".to_string(),
}),
_ => None,
};
(refinement, fallback)
} else {
(None, None)
};
let deferred_fields = if surface.defers_rich_probes() {
vec![
"quarantine_detail".to_string(),
"last_projection_ms".to_string(),
"scan_watermark_ms".to_string(),
]
} else {
Vec::new()
};
ReadinessSummary {
surface,
class,
is_searchable: table.is_searchable(),
recommended_action,
safe_next_action,
lexical: table.readiness.lexical,
semantic: table.readiness.semantic,
archive_risk: table.archive_risk,
quarantine_incomplete: table.quarantine.has_exclusions(),
realized_refinement,
lexical_fallback_reason,
deferred_fields,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::search::readiness::fleet_fixtures;
fn fixture(name: &str) -> DerivedAssetTruthTable {
fleet_fixtures()
.into_iter()
.find(|(n, _)| *n == name)
.unwrap_or_else(|| panic!("missing fixture {name}"))
.1
}
#[test]
fn enums_serialize_snake_case() {
assert_eq!(
serde_json::to_string(&ReadinessClass::StaleSearchable).unwrap(),
"\"stale_searchable\""
);
assert_eq!(
serde_json::to_string(&SurfaceKind::SearchMeta).unwrap(),
"\"search_meta\""
);
}
#[test]
fn health_and_status_never_contradict_for_any_fleet_state() {
for (name, table) in fleet_fixtures() {
let health = project(&table, SurfaceKind::Health);
let status = project(&table, SurfaceKind::Status);
let triage = project(&table, SurfaceKind::Triage);
assert_eq!(health.class, status.class, "{name} class health vs status");
assert_eq!(
health.recommended_action, status.recommended_action,
"{name} recommended_action health vs status"
);
assert_eq!(
health.is_searchable, status.is_searchable,
"{name} searchable"
);
assert_eq!(health.lexical, status.lexical, "{name} lexical");
assert_eq!(health.semantic, status.semantic, "{name} semantic");
assert_eq!(triage.class, status.class, "{name} triage vs status");
assert_eq!(triage.recommended_action, status.recommended_action);
}
}
#[test]
fn cheap_health_marks_deferred_fields_full_status_does_not() {
let t = fixture("local_stale_quarantine");
let health = project(&t, SurfaceKind::Health);
let status = project(&t, SurfaceKind::Status);
assert!(
!health.deferred_fields.is_empty(),
"health must mark deferrals"
);
assert!(status.deferred_fields.is_empty(), "status defers nothing");
}
#[test]
fn golden_classes_cover_stale_missing_repairing_corrupt() {
assert_eq!(
project(&fixture("css_stale_existing_index"), SurfaceKind::Status).class,
ReadinessClass::StaleSearchable
);
assert_eq!(
project(
&fixture("csd_missing_lexical_metadata"),
SurfaceKind::Status
)
.class,
ReadinessClass::Missing
);
assert_eq!(
project(&fixture("mac_mini_old_unreachable"), SurfaceKind::Status).class,
ReadinessClass::Unreachable
);
let mut repairing = fixture("css_stale_existing_index");
repairing.readiness = crate::search::readiness::ReadinessSnapshot::new(
LexicalReadinessState::Repairing,
SemanticReadinessState::Absent,
);
assert_eq!(
project(&repairing, SurfaceKind::Status).class,
ReadinessClass::Repairing
);
let mut corrupt = fixture("css_stale_existing_index");
corrupt.readiness = crate::search::readiness::ReadinessSnapshot::new(
LexicalReadinessState::CorruptQuarantined,
SemanticReadinessState::Absent,
);
let c = project(&corrupt, SurfaceKind::Status);
assert_eq!(c.class, ReadinessClass::CorruptQuarantined);
assert!(!c.is_searchable);
}
#[test]
fn search_meta_reports_realized_refinement_and_fallback_reason() {
let t = fixture("local_stale_quarantine");
let meta = project(&t, SurfaceKind::SearchMeta);
assert_eq!(
meta.realized_refinement,
Some(SearchRefinementLevel::LexicalOnly)
);
assert!(meta.lexical_fallback_reason.is_some());
assert!(
meta.quarantine_incomplete,
"quarantine incompleteness reported"
);
let status = project(&t, SurfaceKind::Status);
assert!(status.realized_refinement.is_none());
assert!(status.lexical_fallback_reason.is_none());
}
#[test]
fn one_recommended_action_with_component_facts_preserved() {
let t = fixture("ts1_high_archive_risk");
let s = project(&t, SurfaceKind::Status);
assert_eq!(s.recommended_action, RecommendedAction::NothingRequired);
assert_eq!(s.archive_risk, ArchiveRiskLevel::High);
assert_eq!(s.safe_next_action, SafeNextAction::BackupThenRepair);
}
#[test]
fn summary_round_trips_through_json() {
let t = fixture("local_stale_quarantine");
let meta = project(&t, SurfaceKind::SearchMeta);
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("\"surface\":\"search_meta\""));
assert!(json.contains("\"class\":\"stale_searchable\""));
let parsed: ReadinessSummary = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, meta);
}
}