#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use crate::search::query::SearchMode;
use crate::search::readiness::SearchRefinementLevel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SemanticFallbackReason {
SemanticAbsent,
SemanticBackfilling,
SemanticPolicyDisabled,
SemanticChecksumMismatch,
SemanticContextRejected,
SemanticContextUnavailable,
HybridExecutionError,
PackEnrichmentUnavailable,
SemanticNotApplied,
}
impl SemanticFallbackReason {
pub(crate) fn code(self) -> &'static str {
match self {
Self::SemanticAbsent => "semantic_absent",
Self::SemanticBackfilling => "semantic_backfilling",
Self::SemanticPolicyDisabled => "semantic_policy_disabled",
Self::SemanticChecksumMismatch => "semantic_checksum_mismatch",
Self::SemanticContextRejected => "semantic_context_rejected",
Self::SemanticContextUnavailable => "semantic_context_unavailable",
Self::HybridExecutionError => "hybrid_execution_error",
Self::PackEnrichmentUnavailable => "pack_enrichment_unavailable",
Self::SemanticNotApplied => "semantic_not_applied",
}
}
}
pub(crate) fn classify_fallback_reason(reason: &str) -> SemanticFallbackReason {
let r = reason.to_ascii_lowercase();
if r.contains("policy") || r.contains("disabled") || r.contains("lexical-only") {
SemanticFallbackReason::SemanticPolicyDisabled
} else if r.contains("checksum") || r.contains("verification failed") {
SemanticFallbackReason::SemanticChecksumMismatch
} else if r.contains("backfill") || r.contains("catching up") {
SemanticFallbackReason::SemanticBackfilling
} else if r.contains("not acquired") || r.contains("no model") || r.contains("model absent") {
SemanticFallbackReason::SemanticAbsent
} else if r.contains("pack semantic enrichment") {
SemanticFallbackReason::PackEnrichmentUnavailable
} else if r.contains("context rejected") {
SemanticFallbackReason::SemanticContextRejected
} else if r.contains("context unavailable") {
SemanticFallbackReason::SemanticContextUnavailable
} else if r.contains("hybrid execution") {
SemanticFallbackReason::HybridExecutionError
} else {
SemanticFallbackReason::SemanticNotApplied
}
}
pub(crate) fn refinement_level(
realized: SearchMode,
quality_tier_refined: bool,
) -> SearchRefinementLevel {
match realized {
SearchMode::Lexical => SearchRefinementLevel::LexicalOnly,
SearchMode::Semantic | SearchMode::Hybrid => {
if quality_tier_refined {
SearchRefinementLevel::FullyHybridRefined
} else {
SearchRefinementLevel::FastTierRefined
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SemanticOnlyOutcome {
NotRequested,
Satisfied,
FailedUnavailable,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SearchModeSignals<'a> {
pub requested: SearchMode,
pub realized: SearchMode,
pub defaulted: bool,
pub fell_back_to_lexical: bool,
pub fallback_reason_text: Option<&'a str>,
pub quality_tier_refined: bool,
pub semantic_only_satisfied: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct SearchModeReport {
pub requested_mode: SearchMode,
pub realized_mode: SearchMode,
pub mode_defaulted: bool,
pub refinement_level: SearchRefinementLevel,
pub semantic_refinement: bool,
pub fallback_tier: Option<&'static str>,
pub fallback_reason: Option<SemanticFallbackReason>,
pub semantic_only_outcome: SemanticOnlyOutcome,
}
pub(crate) fn project(signals: SearchModeSignals<'_>) -> SearchModeReport {
let refinement_level = if signals.fell_back_to_lexical {
SearchRefinementLevel::LexicalOnly
} else {
refinement_level(signals.realized, signals.quality_tier_refined)
};
let semantic_refinement = matches!(
refinement_level,
SearchRefinementLevel::FastTierRefined | SearchRefinementLevel::FullyHybridRefined
);
let fallback_tier = if signals.fell_back_to_lexical {
Some("lexical")
} else {
None
};
let fallback_reason = if signals.fell_back_to_lexical {
Some(
signals
.fallback_reason_text
.map(classify_fallback_reason)
.unwrap_or(SemanticFallbackReason::SemanticNotApplied),
)
} else {
None
};
let semantic_only_outcome = match signals.requested {
SearchMode::Semantic => {
if signals.semantic_only_satisfied {
SemanticOnlyOutcome::Satisfied
} else {
SemanticOnlyOutcome::FailedUnavailable
}
}
_ => SemanticOnlyOutcome::NotRequested,
};
SearchModeReport {
requested_mode: signals.requested,
realized_mode: signals.realized,
mode_defaulted: signals.defaulted,
refinement_level,
semantic_refinement,
fallback_tier,
fallback_reason,
semantic_only_outcome,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> SearchModeSignals<'static> {
SearchModeSignals {
requested: SearchMode::Hybrid,
realized: SearchMode::Hybrid,
defaulted: true,
fell_back_to_lexical: false,
fallback_reason_text: None,
quality_tier_refined: true,
semantic_only_satisfied: false,
}
}
#[test]
fn scenario_full_hybrid() {
let r = project(base());
assert_eq!(r.realized_mode, SearchMode::Hybrid);
assert_eq!(
r.refinement_level,
SearchRefinementLevel::FullyHybridRefined
);
assert!(r.semantic_refinement);
assert!(r.fallback_tier.is_none());
assert!(r.fallback_reason.is_none());
assert_eq!(r.semantic_only_outcome, SemanticOnlyOutcome::NotRequested);
}
#[test]
fn scenario_hybrid_with_absent_model_falls_open_to_lexical() {
let mut s = base();
s.realized = SearchMode::Lexical;
s.fell_back_to_lexical = true;
s.fallback_reason_text = Some("semantic context unavailable: no model acquired");
let r = project(s);
assert_eq!(r.realized_mode, SearchMode::Lexical);
assert_eq!(r.refinement_level, SearchRefinementLevel::LexicalOnly);
assert!(!r.semantic_refinement);
assert_eq!(r.fallback_tier, Some("lexical"));
assert_eq!(
r.fallback_reason,
Some(SemanticFallbackReason::SemanticAbsent)
);
}
#[test]
fn scenario_lexical_only_policy() {
let mut s = base();
s.requested = SearchMode::Lexical;
s.realized = SearchMode::Lexical;
s.defaulted = false;
let r = project(s);
assert_eq!(r.refinement_level, SearchRefinementLevel::LexicalOnly);
assert!(!r.semantic_refinement);
assert!(r.fallback_tier.is_none());
assert!(r.fallback_reason.is_none());
assert_eq!(r.semantic_only_outcome, SemanticOnlyOutcome::NotRequested);
}
#[test]
fn scenario_fast_tier_refinement() {
let mut s = base();
s.quality_tier_refined = false; let r = project(s);
assert_eq!(r.refinement_level, SearchRefinementLevel::FastTierRefined);
assert!(r.semantic_refinement);
assert!(r.fallback_tier.is_none());
}
#[test]
fn scenario_semantic_only_unavailable_fails_visibly() {
let mut s = base();
s.requested = SearchMode::Semantic;
s.realized = SearchMode::Semantic;
s.semantic_only_satisfied = false;
let r = project(s);
assert_eq!(
r.semantic_only_outcome,
SemanticOnlyOutcome::FailedUnavailable
);
assert!(
r.fallback_tier.is_none(),
"semantic-only does not fail open"
);
}
#[test]
fn scenario_semantic_only_satisfied() {
let mut s = base();
s.requested = SearchMode::Semantic;
s.realized = SearchMode::Semantic;
s.semantic_only_satisfied = true;
let r = project(s);
assert_eq!(r.semantic_only_outcome, SemanticOnlyOutcome::Satisfied);
assert_eq!(
r.refinement_level,
SearchRefinementLevel::FullyHybridRefined
);
assert!(r.semantic_refinement);
}
#[test]
fn scenario_stale_lexical_but_searchable() {
let mut s = base();
s.requested = SearchMode::Lexical;
s.realized = SearchMode::Lexical;
let r = project(s);
assert_eq!(r.refinement_level, SearchRefinementLevel::LexicalOnly);
assert!(r.fallback_reason.is_none());
}
#[test]
fn scenario_hybrid_execution_error_falls_open() {
let mut s = base();
s.realized = SearchMode::Lexical;
s.fell_back_to_lexical = true;
s.fallback_reason_text = Some("hybrid execution unavailable: index io error");
let r = project(s);
assert_eq!(
r.fallback_reason,
Some(SemanticFallbackReason::HybridExecutionError)
);
assert_eq!(r.refinement_level, SearchRefinementLevel::LexicalOnly);
}
#[test]
fn classify_covers_live_fallback_strings() {
let cases = [
(
"semantic context rejected: bad dim",
SemanticFallbackReason::SemanticContextRejected,
),
(
"semantic context unavailable: missing vectors",
SemanticFallbackReason::SemanticContextUnavailable,
),
(
"hybrid execution unavailable: e",
SemanticFallbackReason::HybridExecutionError,
),
(
"pack semantic enrichment unavailable; using lexical evidence",
SemanticFallbackReason::PackEnrichmentUnavailable,
),
(
"semantic disabled by policy",
SemanticFallbackReason::SemanticPolicyDisabled,
),
(
"model checksum mismatch",
SemanticFallbackReason::SemanticChecksumMismatch,
),
(
"semantic backfill in progress",
SemanticFallbackReason::SemanticBackfilling,
),
("no model acquired", SemanticFallbackReason::SemanticAbsent),
(
"something else entirely",
SemanticFallbackReason::SemanticNotApplied,
),
];
for (text, want) in cases {
assert_eq!(classify_fallback_reason(text), want, "for {text:?}");
}
}
#[test]
fn refinement_level_maps_modes() {
assert_eq!(
refinement_level(SearchMode::Lexical, true),
SearchRefinementLevel::LexicalOnly
);
assert_eq!(
refinement_level(SearchMode::Hybrid, true),
SearchRefinementLevel::FullyHybridRefined
);
assert_eq!(
refinement_level(SearchMode::Hybrid, false),
SearchRefinementLevel::FastTierRefined
);
assert_eq!(
refinement_level(SearchMode::Semantic, true),
SearchRefinementLevel::FullyHybridRefined
);
}
#[test]
fn report_serializes_with_snake_case() {
let r = project(base());
let json = serde_json::to_string(&r).unwrap();
assert!(json.contains("\"refinement_level\":\"fully_hybrid_refined\""));
assert!(json.contains("\"semantic_only_outcome\":\"not_requested\""));
assert!(json.contains("\"semantic_refinement\":true"));
}
#[test]
fn fallback_reason_code_matches_serialization() {
for reason in [
SemanticFallbackReason::SemanticAbsent,
SemanticFallbackReason::HybridExecutionError,
SemanticFallbackReason::PackEnrichmentUnavailable,
] {
assert_eq!(
serde_json::to_string(&reason).unwrap(),
format!("\"{}\"", reason.code())
);
}
}
}