#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use crate::search::readiness::SearchRefinementLevel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SemanticReadinessReason {
PolicyDisabled,
BaselineNoSemantic,
ModelNotAcquired,
ModelFilesMissing,
ChecksumMismatch,
VectorIndexMissing,
DbFingerprintMismatch,
BackfillInProgress,
FastTierReady,
QualityTierReady,
}
impl SemanticReadinessReason {
pub(crate) fn is_available(self) -> bool {
matches!(self, Self::FastTierReady | Self::QualityTierReady)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum FallbackMode {
None,
Lexical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SemanticNextStep {
None,
EnableSemanticPolicy,
InstallModel,
RepairModelFiles,
ReacquireModelChecksumFailed,
BuildVectorIndex,
RebuildForCurrentDb,
WaitForBackfill,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SemanticSignals {
pub policy_enabled: bool,
pub baseline_only: bool,
pub model_present: bool,
pub model_files_complete: bool,
pub checksum_ok: bool,
pub vector_index_present: bool,
pub db_fingerprint_matches: Option<bool>,
pub backfill_in_progress: bool,
pub fast_tier_ready: bool,
pub quality_tier_ready: bool,
}
impl SemanticSignals {
pub(crate) fn reason(&self) -> SemanticReadinessReason {
use SemanticReadinessReason as R;
if !self.policy_enabled {
return R::PolicyDisabled;
}
if self.baseline_only {
return R::BaselineNoSemantic;
}
if !self.model_present {
return R::ModelNotAcquired;
}
if !self.model_files_complete {
return R::ModelFilesMissing;
}
if !self.checksum_ok {
return R::ChecksumMismatch;
}
if !self.vector_index_present {
return R::VectorIndexMissing;
}
if self.db_fingerprint_matches == Some(false) {
return R::DbFingerprintMismatch;
}
if self.quality_tier_ready {
return R::QualityTierReady;
}
if self.fast_tier_ready {
return R::FastTierReady;
}
if self.backfill_in_progress {
return R::BackfillInProgress;
}
R::BackfillInProgress
}
pub(crate) fn report(&self) -> SemanticReadinessReport {
use SemanticReadinessReason as R;
let reason = self.reason();
let available = reason.is_available();
let quality_tier_ready = matches!(reason, R::QualityTierReady);
let realized_refinement = if quality_tier_ready {
SearchRefinementLevel::FullyHybridRefined
} else if matches!(reason, R::FastTierReady) {
SearchRefinementLevel::FastTierRefined
} else {
SearchRefinementLevel::LexicalOnly
};
let fallback_mode = if available {
FallbackMode::None
} else {
FallbackMode::Lexical
};
let next_step = match reason {
R::QualityTierReady if self.backfill_in_progress => {
SemanticNextStep::WaitForBackfill
}
R::QualityTierReady => SemanticNextStep::None,
R::PolicyDisabled => SemanticNextStep::EnableSemanticPolicy,
R::BaselineNoSemantic | R::ModelNotAcquired => SemanticNextStep::InstallModel,
R::ModelFilesMissing => SemanticNextStep::RepairModelFiles,
R::ChecksumMismatch => SemanticNextStep::ReacquireModelChecksumFailed,
R::VectorIndexMissing => SemanticNextStep::BuildVectorIndex,
R::DbFingerprintMismatch => SemanticNextStep::RebuildForCurrentDb,
R::BackfillInProgress => SemanticNextStep::WaitForBackfill,
R::FastTierReady if self.backfill_in_progress => {
SemanticNextStep::WaitForBackfill
}
R::FastTierReady => SemanticNextStep::None,
};
let state_detail = match (reason, self.backfill_in_progress) {
(R::QualityTierReady, true) => {
"quality semantic tier ready; residual semantic backfill is still finishing"
}
(R::FastTierReady, false) => {
"fast semantic tier ready; quality tier is not published and no backfill is active"
}
_ => reason.state_detail(),
};
SemanticReadinessReport {
reason,
available,
semantic_only_search_available: available,
fallback_mode,
fast_tier_ready: available && self.fast_tier_ready,
quality_tier_ready,
state_detail: state_detail.to_string(),
next_step,
realized_refinement,
}
}
}
impl SemanticReadinessReason {
fn state_detail(self) -> &'static str {
match self {
Self::PolicyDisabled => "semantic search disabled by policy; lexical search only",
Self::BaselineNoSemantic => {
"baseline build without semantic configured; install a model to enable hybrid refinement"
}
Self::ModelNotAcquired => {
"no embedding model acquired; lexical search works, hybrid refinement is opt-in"
}
Self::ModelFilesMissing => "embedding model files are incomplete; re-acquire the model",
Self::ChecksumMismatch => "embedding model failed checksum verification; re-acquire it",
Self::VectorIndexMissing => "model ready but no vector index built yet",
Self::DbFingerprintMismatch => {
"vector index is stale against the current database; rebuild embeddings"
}
Self::BackfillInProgress => {
"semantic backfill in progress; hybrid refinement improves as it completes"
}
Self::FastTierReady => "fast semantic tier ready; quality tier still backfilling",
Self::QualityTierReady => {
"quality semantic tier ready; full hybrid refinement available"
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SemanticReadinessReport {
pub reason: SemanticReadinessReason,
pub available: bool,
pub semantic_only_search_available: bool,
pub fallback_mode: FallbackMode,
pub fast_tier_ready: bool,
pub quality_tier_ready: bool,
pub state_detail: String,
pub next_step: SemanticNextStep,
pub realized_refinement: SearchRefinementLevel,
}
#[cfg(test)]
mod tests {
use super::*;
fn ready() -> SemanticSignals {
SemanticSignals {
policy_enabled: true,
baseline_only: false,
model_present: true,
model_files_complete: true,
checksum_ok: true,
vector_index_present: true,
db_fingerprint_matches: Some(true),
backfill_in_progress: false,
fast_tier_ready: true,
quality_tier_ready: true,
}
}
#[test]
fn enums_serialize_as_snake_case() {
assert_eq!(
serde_json::to_string(&SemanticReadinessReason::DbFingerprintMismatch).unwrap(),
"\"db_fingerprint_mismatch\""
);
assert_eq!(
serde_json::to_string(&FallbackMode::Lexical).unwrap(),
"\"lexical\""
);
assert_eq!(
serde_json::to_string(&SemanticNextStep::ReacquireModelChecksumFailed).unwrap(),
"\"reacquire_model_checksum_failed\""
);
}
#[test]
fn quality_tier_ready_is_fully_hybrid_with_no_fallback() {
let r = ready().report();
assert_eq!(r.reason, SemanticReadinessReason::QualityTierReady);
assert!(r.available);
assert!(r.quality_tier_ready);
assert_eq!(r.fallback_mode, FallbackMode::None);
assert_eq!(
r.realized_refinement,
SearchRefinementLevel::FullyHybridRefined
);
assert_eq!(r.next_step, SemanticNextStep::None);
assert!(r.semantic_only_search_available);
}
#[test]
fn policy_disabled_dominates_every_other_signal() {
let mut s = ready();
s.policy_enabled = false;
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::PolicyDisabled);
assert!(!r.available);
assert!(!r.semantic_only_search_available);
assert!(!r.fast_tier_ready);
assert!(!r.quality_tier_ready);
assert_eq!(r.fallback_mode, FallbackMode::Lexical);
assert_eq!(r.next_step, SemanticNextStep::EnableSemanticPolicy);
}
#[test]
fn baseline_without_semantic_suggests_install() {
let mut s = ready();
s.baseline_only = true;
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::BaselineNoSemantic);
assert_eq!(r.next_step, SemanticNextStep::InstallModel);
assert_eq!(r.realized_refinement, SearchRefinementLevel::LexicalOnly);
}
#[test]
fn acquisition_chain_reasons_are_reported_in_priority_order() {
let mut s = ready();
s.model_present = false;
assert_eq!(s.reason(), SemanticReadinessReason::ModelNotAcquired);
assert_eq!(s.report().next_step, SemanticNextStep::InstallModel);
let mut s = ready();
s.model_files_complete = false;
assert_eq!(s.reason(), SemanticReadinessReason::ModelFilesMissing);
assert_eq!(s.report().next_step, SemanticNextStep::RepairModelFiles);
let mut s = ready();
s.checksum_ok = false;
assert_eq!(s.reason(), SemanticReadinessReason::ChecksumMismatch);
assert_eq!(
s.report().next_step,
SemanticNextStep::ReacquireModelChecksumFailed
);
}
#[test]
fn vector_index_missing_when_model_ok_but_no_index() {
let mut s = ready();
s.quality_tier_ready = false;
s.fast_tier_ready = false;
s.vector_index_present = false;
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::VectorIndexMissing);
assert_eq!(r.next_step, SemanticNextStep::BuildVectorIndex);
assert!(!r.available);
assert!(!r.semantic_only_search_available);
}
#[test]
fn stale_fingerprint_is_flagged_and_must_rebuild() {
let mut s = ready();
s.quality_tier_ready = false;
s.fast_tier_ready = false;
s.db_fingerprint_matches = Some(false);
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::DbFingerprintMismatch);
assert_eq!(r.next_step, SemanticNextStep::RebuildForCurrentDb);
}
#[test]
fn explicit_staleness_dominates_claimed_ready_tiers() {
let mut s = ready();
s.db_fingerprint_matches = Some(false);
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::DbFingerprintMismatch);
assert!(!r.available);
assert!(!r.semantic_only_search_available);
assert!(!r.fast_tier_ready);
assert!(!r.quality_tier_ready);
assert_eq!(r.fallback_mode, FallbackMode::Lexical);
assert_eq!(r.realized_refinement, SearchRefinementLevel::LexicalOnly);
}
#[test]
fn missing_vector_index_dominates_claimed_quality_tier() {
let mut s = ready();
s.vector_index_present = false;
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::VectorIndexMissing);
assert!(!r.available);
assert!(!r.semantic_only_search_available);
assert!(!r.quality_tier_ready);
}
#[test]
fn backfill_in_progress_waits() {
let mut s = ready();
s.quality_tier_ready = false;
s.fast_tier_ready = false;
s.backfill_in_progress = true;
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::BackfillInProgress);
assert_eq!(r.next_step, SemanticNextStep::WaitForBackfill);
}
#[test]
fn fast_tier_ready_serves_search_while_quality_backfills() {
let mut s = ready();
s.quality_tier_ready = false;
s.backfill_in_progress = true;
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::FastTierReady);
assert!(r.available);
assert!(r.semantic_only_search_available);
assert_eq!(r.fallback_mode, FallbackMode::None);
assert_eq!(
r.realized_refinement,
SearchRefinementLevel::FastTierRefined
);
assert_eq!(r.next_step, SemanticNextStep::WaitForBackfill);
assert!(r.state_detail.contains("backfilling"));
}
#[test]
fn fast_tier_without_active_backfill_has_no_wait_or_build_action() {
let mut s = ready();
s.quality_tier_ready = false;
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::FastTierReady);
assert!(r.available);
assert!(r.semantic_only_search_available);
assert!(r.fast_tier_ready);
assert!(!r.quality_tier_ready);
assert_eq!(r.fallback_mode, FallbackMode::None);
assert_eq!(
r.realized_refinement,
SearchRefinementLevel::FastTierRefined
);
assert_eq!(r.next_step, SemanticNextStep::None);
assert!(r.state_detail.contains("no backfill is active"));
}
#[test]
fn quality_tier_remains_queryable_while_residual_backfill_finishes() {
let mut s = ready();
s.backfill_in_progress = true;
let r = s.report();
assert_eq!(r.reason, SemanticReadinessReason::QualityTierReady);
assert!(r.available);
assert!(r.semantic_only_search_available);
assert!(r.quality_tier_ready);
assert_eq!(r.fallback_mode, FallbackMode::None);
assert_eq!(
r.realized_refinement,
SearchRefinementLevel::FullyHybridRefined
);
assert_eq!(r.next_step, SemanticNextStep::WaitForBackfill);
assert!(r.state_detail.contains("residual semantic backfill"));
}
#[test]
fn report_round_trips_through_json_with_expected_fields() {
let r = ready().report();
let json = serde_json::to_string(&r).unwrap();
for needle in [
"\"reason\":\"quality_tier_ready\"",
"\"available\":true",
"\"semantic_only_search_available\":true",
"\"fallback_mode\":\"none\"",
"\"next_step\":\"none\"",
"\"realized_refinement\":\"fully_hybrid_refined\"",
] {
assert!(json.contains(needle), "missing {needle} in {json}");
}
let parsed: SemanticReadinessReport = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, r);
}
#[test]
fn every_reason_is_reachable_from_some_signal_set() {
use SemanticReadinessReason as R;
let reached: std::collections::BTreeSet<R> = [
{
let mut s = ready();
s.policy_enabled = false;
s.reason()
},
{
let mut s = ready();
s.baseline_only = true;
s.reason()
},
{
let mut s = ready();
s.model_present = false;
s.reason()
},
{
let mut s = ready();
s.model_files_complete = false;
s.reason()
},
{
let mut s = ready();
s.checksum_ok = false;
s.reason()
},
{
let mut s = ready();
s.quality_tier_ready = false;
s.fast_tier_ready = false;
s.vector_index_present = false;
s.reason()
},
{
let mut s = ready();
s.quality_tier_ready = false;
s.fast_tier_ready = false;
s.db_fingerprint_matches = Some(false);
s.reason()
},
{
let mut s = ready();
s.quality_tier_ready = false;
s.fast_tier_ready = false;
s.backfill_in_progress = true;
s.reason()
},
{
let mut s = ready();
s.quality_tier_ready = false;
s.reason()
},
ready().reason(),
]
.into_iter()
.collect();
assert_eq!(reached.len(), 10, "all ten reasons must be reachable");
}
}