use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExperienceTypeTag {
Difficulty,
Solution,
ErrorPattern,
SuccessPattern,
UserPreference,
ArchitecturalDecision,
TechInsight,
Fact,
Generic,
}
impl ExperienceTypeTag {
pub fn all() -> &'static [Self] {
&[
Self::Difficulty,
Self::Solution,
Self::ErrorPattern,
Self::SuccessPattern,
Self::UserPreference,
Self::ArchitecturalDecision,
Self::TechInsight,
Self::Fact,
Self::Generic,
]
}
pub fn from_experience_type(et: &pulsedb::ExperienceType) -> Self {
match et {
pulsedb::ExperienceType::Difficulty { .. } => Self::Difficulty,
pulsedb::ExperienceType::Solution { .. } => Self::Solution,
pulsedb::ExperienceType::ErrorPattern { .. } => Self::ErrorPattern,
pulsedb::ExperienceType::SuccessPattern { .. } => Self::SuccessPattern,
pulsedb::ExperienceType::UserPreference { .. } => Self::UserPreference,
pulsedb::ExperienceType::ArchitecturalDecision { .. } => Self::ArchitecturalDecision,
pulsedb::ExperienceType::TechInsight { .. } => Self::TechInsight,
pulsedb::ExperienceType::Fact { .. } => Self::Fact,
pulsedb::ExperienceType::Generic { .. } => Self::Generic,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecencyCurve {
Exponential {
half_life_hours: f32,
},
Uniform,
}
impl Default for RecencyCurve {
fn default() -> Self {
RecencyCurve::Exponential {
half_life_hours: 72.0,
}
}
}
#[derive(Debug, Clone)]
pub struct Lens {
pub domain_focus: Vec<String>,
pub type_weights: HashMap<ExperienceTypeTag, f32>,
pub recency_curve: RecencyCurve,
pub purpose_embedding: Vec<f32>,
pub attention_budget: usize,
}
impl Lens {
pub fn new(domains: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
domain_focus: domains.into_iter().map(Into::into).collect(),
type_weights: HashMap::new(),
recency_curve: RecencyCurve::default(),
purpose_embedding: Vec::new(),
attention_budget: 50,
}
}
}
impl Default for Lens {
fn default() -> Self {
Self::new(Vec::<String>::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lens_new_with_str_slices() {
let lens = Lens::new(["safety", "clinical"]);
assert_eq!(lens.domain_focus, vec!["safety", "clinical"]);
assert_eq!(lens.attention_budget, 50);
assert!(lens.type_weights.is_empty());
assert!(lens.purpose_embedding.is_empty());
assert!(matches!(
lens.recency_curve,
RecencyCurve::Exponential {
half_life_hours
} if (half_life_hours - 72.0).abs() < f32::EPSILON
));
}
#[test]
fn test_lens_new_with_owned_strings() {
let domains = vec!["code".to_string(), "architecture".to_string()];
let lens = Lens::new(domains);
assert_eq!(lens.domain_focus, vec!["code", "architecture"]);
}
#[test]
fn test_lens_default() {
let lens = Lens::default();
assert!(lens.domain_focus.is_empty());
assert_eq!(lens.attention_budget, 50);
assert!(matches!(
lens.recency_curve,
RecencyCurve::Exponential { half_life_hours } if (half_life_hours - 72.0).abs() < f32::EPSILON
));
}
#[test]
fn test_lens_clone() {
let mut lens = Lens::new(["test"]);
lens.type_weights
.insert(ExperienceTypeTag::ErrorPattern, 2.0);
let cloned = lens.clone();
assert_eq!(cloned.domain_focus, lens.domain_focus);
assert_eq!(
cloned.type_weights.get(&ExperienceTypeTag::ErrorPattern),
Some(&2.0)
);
}
#[test]
fn test_recency_curve_default() {
let curve = RecencyCurve::default();
assert!(matches!(
curve,
RecencyCurve::Exponential { half_life_hours } if (half_life_hours - 72.0).abs() < f32::EPSILON
));
}
#[test]
fn test_experience_type_tag_all_nine_variants() {
let all = ExperienceTypeTag::all();
assert_eq!(all.len(), 9);
let set: std::collections::HashSet<_> = all.iter().collect();
assert_eq!(set.len(), 9);
}
#[test]
fn test_experience_type_tag_as_hashmap_key() {
let mut weights = HashMap::new();
weights.insert(ExperienceTypeTag::Difficulty, 1.5);
weights.insert(ExperienceTypeTag::Solution, 2.0);
weights.insert(ExperienceTypeTag::Generic, 0.5);
assert_eq!(weights.get(&ExperienceTypeTag::Difficulty), Some(&1.5));
assert_eq!(weights.get(&ExperienceTypeTag::Solution), Some(&2.0));
assert_eq!(weights.get(&ExperienceTypeTag::Fact), None);
}
#[test]
fn test_recency_curve_serialization() {
let curve = RecencyCurve::Exponential {
half_life_hours: 48.0,
};
let json = serde_json::to_string(&curve).unwrap();
let deserialized: RecencyCurve = serde_json::from_str(&json).unwrap();
assert!(matches!(
deserialized,
RecencyCurve::Exponential { half_life_hours } if (half_life_hours - 48.0).abs() < f32::EPSILON
));
let uniform = RecencyCurve::Uniform;
let json = serde_json::to_string(&uniform).unwrap();
let deserialized: RecencyCurve = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, RecencyCurve::Uniform));
}
}