use smos_domain::config::{HeatConfig, RetrievalConfig};
use smos_domain::enums::FactStatus;
use smos_domain::{Confidence, FactId, Heat, MemoryKey, Timestamp};
#[derive(Debug, Clone, PartialEq)]
pub struct RetrievalHit {
pub id: FactId,
pub document: String,
pub memory_key: MemoryKey,
pub status: FactStatus,
pub confidence: Confidence,
pub valid_until: Option<Timestamp>,
pub heat_base: Heat,
pub last_access_at: Timestamp,
}
impl RetrievalHit {
pub fn passes_prefilters(&self, cfg: &RetrievalConfig) -> bool {
if self.status != FactStatus::Accepted {
return false;
}
if self.valid_until.is_some() {
return false;
}
if self.confidence.value() < cfg.min_confidence {
return false;
}
true
}
pub fn heat_live(&self, decay_rate: f32, now: Timestamp) -> f32 {
Heat::decay(self.heat_base, self.last_access_at, now, decay_rate)
}
}
pub fn prefilter_and_heat(
hits: &[RetrievalHit],
cfg: &RetrievalConfig,
heat_cfg: &HeatConfig,
now: Timestamp,
) -> Vec<RetrievalHit> {
hits.iter()
.filter(|h| h.passes_prefilters(cfg))
.filter(|h| h.heat_live(heat_cfg.decay_rate, now) > heat_cfg.min_threshold)
.cloned()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn now() -> Timestamp {
Timestamp::from_unix_secs(1_700_000_000).unwrap()
}
fn key() -> MemoryKey {
MemoryKey::from_raw("origa").unwrap()
}
fn fid(tag: &str) -> FactId {
FactId::from_content(tag)
}
fn hit(status: FactStatus) -> RetrievalHit {
RetrievalHit {
id: fid("hit"),
document: "doc".to_string(),
memory_key: key(),
status,
confidence: Confidence::new(0.8).unwrap(),
valid_until: None,
heat_base: Heat::new(1.0).unwrap(),
last_access_at: now(),
}
}
fn rcfg() -> RetrievalConfig {
RetrievalConfig::default()
}
fn hcfg() -> HeatConfig {
HeatConfig::default()
}
#[test]
fn accepted_hit_with_high_confidence_passes() {
let hits = vec![hit(FactStatus::Accepted)];
let out = prefilter_and_heat(&hits, &rcfg(), &hcfg(), now());
assert_eq!(out.len(), 1);
}
#[test]
fn pending_hit_is_filtered_out() {
let hits = vec![hit(FactStatus::Pending)];
let out = prefilter_and_heat(&hits, &rcfg(), &hcfg(), now());
assert!(out.is_empty());
}
#[test]
fn rejected_hit_is_filtered_out() {
let hits = vec![hit(FactStatus::Rejected)];
let out = prefilter_and_heat(&hits, &rcfg(), &hcfg(), now());
assert!(out.is_empty());
}
#[test]
fn hit_with_valid_until_is_filtered_out() {
let mut h = hit(FactStatus::Accepted);
h.valid_until = Some(now());
let out = prefilter_and_heat(&[h], &rcfg(), &hcfg(), now());
assert!(out.is_empty());
}
#[test]
fn hit_below_min_confidence_is_filtered_out() {
let mut h = hit(FactStatus::Accepted);
h.confidence = Confidence::new(0.5).unwrap();
let out = prefilter_and_heat(&[h], &rcfg(), &hcfg(), now());
assert!(out.is_empty());
}
#[test]
fn hit_with_stale_heat_is_filtered_out() {
let mut h = hit(FactStatus::Accepted);
h.last_access_at = Timestamp::from_unix_secs(now().as_unix_secs() - 1000 * 3600).unwrap();
let out = prefilter_and_heat(&[h], &rcfg(), &hcfg(), now());
assert!(out.is_empty());
}
#[test]
fn preserves_order_of_survivors() {
let hits = vec![
RetrievalHit {
id: fid("a"),
document: "first".to_string(),
memory_key: key(),
status: FactStatus::Accepted,
confidence: Confidence::new(0.9).unwrap(),
valid_until: None,
heat_base: Heat::new(1.0).unwrap(),
last_access_at: now(),
},
RetrievalHit {
id: fid("b"),
document: "second".to_string(),
memory_key: key(),
status: FactStatus::Accepted,
confidence: Confidence::new(0.95).unwrap(),
valid_until: None,
heat_base: Heat::new(1.0).unwrap(),
last_access_at: now(),
},
];
let out = prefilter_and_heat(&hits, &rcfg(), &hcfg(), now());
assert_eq!(out.len(), 2);
assert_eq!(out[0].id, fid("a"));
assert_eq!(out[1].id, fid("b"));
}
}