#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum RetrievalQuality {
Full = 0x00,
Partial = 0x01,
LayerAOnly = 0x02,
DegenerateDetected = 0x03,
BruteForceBudgeted = 0x04,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum ResponseQuality {
Verified = 0x00,
Usable = 0x01,
Degraded = 0x02,
Unreliable = 0x03,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum QualityPreference {
Auto = 0x00,
PreferQuality = 0x01,
PreferLatency = 0x02,
AcceptDegraded = 0x03,
}
impl Default for QualityPreference {
fn default() -> Self {
Self::Auto
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IndexLayersUsed {
pub layer_a: bool,
pub layer_b: bool,
pub layer_c: bool,
pub hot_cache: bool,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SearchEvidenceSummary {
pub layers_used: IndexLayersUsed,
pub n_probe_effective: u32,
pub degenerate_detected: bool,
pub centroid_distance_cv: f32,
pub hnsw_candidate_count: u32,
pub safety_net_candidate_count: u32,
}
impl Default for SearchEvidenceSummary {
fn default() -> Self {
Self {
layers_used: IndexLayersUsed::default(),
n_probe_effective: 0,
degenerate_detected: false,
centroid_distance_cv: 0.0,
hnsw_candidate_count: 0,
safety_net_candidate_count: 0,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BudgetReport {
pub centroid_routing_us: u64,
pub hnsw_traversal_us: u64,
pub safety_net_scan_us: u64,
pub reranking_us: u64,
pub total_us: u64,
pub distance_ops: u64,
pub distance_ops_budget: u64,
pub bytes_read: u64,
pub linear_scan_count: u64,
pub linear_scan_budget: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum FallbackPath {
None = 0x00,
NProbeWidened = 0x01,
DegenerateWidened = 0x02,
SafetyNetSelective = 0x03,
SafetyNetBudgetExhausted = 0x04,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DegradationReason {
CentroidDrift { epoch_drift: u32, max_drift: u32 },
DegenerateDistribution { cv: f32, threshold: f32 },
BudgetExhausted {
scanned: u64,
total: u64,
budget_type: BudgetType,
},
IndexNotLoaded { available: IndexLayersUsed },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum BudgetType {
Time = 0x00,
Candidates = 0x01,
DistanceOps = 0x02,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DegradationReport {
pub fallback_path: FallbackPath,
pub reason: DegradationReason,
pub guarantee_lost: &'static str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SafetyNetBudget {
pub max_scan_time_us: u64,
pub max_scan_candidates: u64,
pub max_distance_ops: u64,
}
impl SafetyNetBudget {
pub const LAYER_A: Self = Self {
max_scan_time_us: 2_000, max_scan_candidates: 10_000,
max_distance_ops: 10_000,
};
pub const PARTIAL: Self = Self {
max_scan_time_us: 5_000, max_scan_candidates: 50_000,
max_distance_ops: 50_000,
};
pub const FULL: Self = Self {
max_scan_time_us: 10_000, max_scan_candidates: 100_000,
max_distance_ops: 100_000,
};
pub const DISABLED: Self = Self {
max_scan_time_us: 0,
max_scan_candidates: 0,
max_distance_ops: 0,
};
pub const fn extended_4x(&self) -> Self {
Self {
max_scan_time_us: self.max_scan_time_us.saturating_mul(4),
max_scan_candidates: self.max_scan_candidates.saturating_mul(4),
max_distance_ops: self.max_distance_ops.saturating_mul(4),
}
}
pub const fn is_disabled(&self) -> bool {
self.max_scan_time_us == 0 && self.max_scan_candidates == 0 && self.max_distance_ops == 0
}
}
impl Default for SafetyNetBudget {
fn default() -> Self {
Self::LAYER_A
}
}
pub fn derive_response_quality(retrieval_qualities: &[RetrievalQuality]) -> ResponseQuality {
if retrieval_qualities.is_empty() {
return ResponseQuality::Unreliable;
}
let worst = retrieval_qualities
.iter()
.copied()
.max_by_key(|q| *q as u8)
.unwrap_or(RetrievalQuality::Full);
match worst {
RetrievalQuality::Full => ResponseQuality::Verified,
RetrievalQuality::Partial => ResponseQuality::Usable,
RetrievalQuality::LayerAOnly => ResponseQuality::Usable,
RetrievalQuality::DegenerateDetected => ResponseQuality::Degraded,
RetrievalQuality::BruteForceBudgeted => ResponseQuality::Degraded,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retrieval_quality_ordering() {
assert!(RetrievalQuality::Full < RetrievalQuality::BruteForceBudgeted);
assert!(RetrievalQuality::Partial < RetrievalQuality::DegenerateDetected);
}
#[test]
fn response_quality_ordering() {
assert!(ResponseQuality::Verified < ResponseQuality::Unreliable);
assert!(ResponseQuality::Usable < ResponseQuality::Degraded);
}
#[test]
fn derive_quality_full() {
let q = derive_response_quality(&[RetrievalQuality::Full, RetrievalQuality::Full]);
assert_eq!(q, ResponseQuality::Verified);
}
#[test]
fn derive_quality_mixed() {
let q = derive_response_quality(&[
RetrievalQuality::Full,
RetrievalQuality::DegenerateDetected,
]);
assert_eq!(q, ResponseQuality::Degraded);
}
#[test]
fn derive_quality_empty_is_unreliable() {
let q = derive_response_quality(&[]);
assert_eq!(q, ResponseQuality::Unreliable);
}
#[test]
fn derive_quality_layer_a() {
let q = derive_response_quality(&[RetrievalQuality::LayerAOnly]);
assert_eq!(q, ResponseQuality::Usable);
}
#[test]
fn derive_quality_brute_force() {
let q = derive_response_quality(&[RetrievalQuality::BruteForceBudgeted]);
assert_eq!(q, ResponseQuality::Degraded);
}
#[test]
fn safety_net_budget_layer_a() {
let b = SafetyNetBudget::LAYER_A;
assert_eq!(b.max_scan_time_us, 2_000);
assert_eq!(b.max_scan_candidates, 10_000);
assert_eq!(b.max_distance_ops, 10_000);
assert!(!b.is_disabled());
}
#[test]
fn safety_net_budget_extended() {
let b = SafetyNetBudget::LAYER_A.extended_4x();
assert_eq!(b.max_scan_time_us, 8_000);
assert_eq!(b.max_scan_candidates, 40_000);
assert_eq!(b.max_distance_ops, 40_000);
}
#[test]
fn safety_net_budget_disabled() {
let b = SafetyNetBudget::DISABLED;
assert!(b.is_disabled());
assert_eq!(b.max_scan_time_us, 0);
}
#[test]
fn quality_preference_default_is_auto() {
assert_eq!(QualityPreference::default(), QualityPreference::Auto);
}
#[test]
fn quality_repr_values() {
assert_eq!(RetrievalQuality::Full as u8, 0x00);
assert_eq!(RetrievalQuality::BruteForceBudgeted as u8, 0x04);
assert_eq!(ResponseQuality::Verified as u8, 0x00);
assert_eq!(ResponseQuality::Unreliable as u8, 0x03);
assert_eq!(QualityPreference::Auto as u8, 0x00);
assert_eq!(QualityPreference::AcceptDegraded as u8, 0x03);
}
#[test]
fn fallback_path_repr() {
assert_eq!(FallbackPath::None as u8, 0x00);
assert_eq!(FallbackPath::SafetyNetBudgetExhausted as u8, 0x04);
}
#[test]
fn budget_report_default_is_zero() {
let r = BudgetReport::default();
assert_eq!(r.total_us, 0);
assert_eq!(r.distance_ops, 0);
}
#[test]
fn degradation_report_construction() {
let report = DegradationReport {
fallback_path: FallbackPath::SafetyNetBudgetExhausted,
reason: DegradationReason::BudgetExhausted {
scanned: 5000,
total: 10000,
budget_type: BudgetType::DistanceOps,
},
guarantee_lost: "recall may be below target",
};
assert_eq!(report.fallback_path, FallbackPath::SafetyNetBudgetExhausted);
}
#[test]
fn evidence_summary_default() {
let e = SearchEvidenceSummary::default();
assert!(!e.degenerate_detected);
assert_eq!(e.n_probe_effective, 0);
assert_eq!(e.centroid_distance_cv, 0.0);
}
#[test]
fn index_layers_default_all_false() {
let l = IndexLayersUsed::default();
assert!(!l.layer_a);
assert!(!l.layer_b);
assert!(!l.layer_c);
assert!(!l.hot_cache);
}
}