use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::storage::schema::ExperienceTypeTag;
use crate::types::{AgentId, CollectiveId, ExperienceId, InstanceId, TaskId, Timestamp};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ExperienceType {
Difficulty {
description: String,
severity: Severity,
},
Solution {
problem_ref: Option<ExperienceId>,
approach: String,
worked: bool,
},
ErrorPattern {
signature: String,
fix: String,
prevention: String,
},
SuccessPattern {
task_type: String,
approach: String,
quality: f32,
},
UserPreference {
category: String,
preference: String,
strength: f32,
},
ArchitecturalDecision {
decision: String,
rationale: String,
},
TechInsight {
technology: String,
insight: String,
},
Fact {
statement: String,
source: String,
},
Generic {
category: Option<String>,
},
}
impl ExperienceType {
pub fn type_tag(&self) -> ExperienceTypeTag {
match self {
Self::Difficulty { .. } => ExperienceTypeTag::Difficulty,
Self::Solution { .. } => ExperienceTypeTag::Solution,
Self::ErrorPattern { .. } => ExperienceTypeTag::ErrorPattern,
Self::SuccessPattern { .. } => ExperienceTypeTag::SuccessPattern,
Self::UserPreference { .. } => ExperienceTypeTag::UserPreference,
Self::ArchitecturalDecision { .. } => ExperienceTypeTag::ArchitecturalDecision,
Self::TechInsight { .. } => ExperienceTypeTag::TechInsight,
Self::Fact { .. } => ExperienceTypeTag::Fact,
Self::Generic { .. } => ExperienceTypeTag::Generic,
}
}
}
impl Default for ExperienceType {
fn default() -> Self {
Self::Generic { category: None }
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Experience {
pub id: ExperienceId,
pub collective_id: CollectiveId,
pub content: String,
#[serde(skip)]
pub embedding: Vec<f32>,
pub experience_type: ExperienceType,
pub importance: f32,
pub confidence: f32,
pub applications: BTreeMap<InstanceId, u32>,
pub domain: Vec<String>,
pub related_files: Vec<String>,
pub source_agent: AgentId,
pub source_task: Option<TaskId>,
pub timestamp: Timestamp,
pub last_reinforced: Timestamp,
pub archived: bool,
}
impl Experience {
pub fn applications(&self) -> u32 {
self.applications
.values()
.copied()
.fold(0u32, u32::saturating_add)
}
}
#[derive(Clone, Debug)]
pub struct NewExperience {
pub collective_id: CollectiveId,
pub content: String,
pub experience_type: ExperienceType,
pub embedding: Option<Vec<f32>>,
pub importance: f32,
pub confidence: f32,
pub domain: Vec<String>,
pub related_files: Vec<String>,
pub source_agent: AgentId,
pub source_task: Option<TaskId>,
}
impl Default for NewExperience {
fn default() -> Self {
Self {
collective_id: CollectiveId::nil(),
content: String::new(),
experience_type: ExperienceType::default(),
embedding: None,
importance: 0.5,
confidence: 0.5,
domain: Vec::new(),
related_files: Vec::new(),
source_agent: AgentId::new("anonymous"),
source_task: None,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ExperienceUpdate {
pub importance: Option<f32>,
pub confidence: Option<f32>,
pub domain: Option<Vec<String>>,
pub related_files: Option<Vec<String>>,
pub archived: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_postcard_roundtrip() {
for severity in [
Severity::Low,
Severity::Medium,
Severity::High,
Severity::Critical,
] {
let bytes = postcard::to_stdvec(&severity).unwrap();
let restored: Severity = postcard::from_bytes(&bytes).unwrap();
assert_eq!(severity, restored);
}
}
#[test]
fn test_experience_type_default() {
let et = ExperienceType::default();
assert!(matches!(et, ExperienceType::Generic { category: None }));
}
#[test]
fn test_experience_type_tag_mapping() {
let cases: Vec<(ExperienceType, ExperienceTypeTag)> = vec![
(
ExperienceType::Difficulty {
description: "test".into(),
severity: Severity::High,
},
ExperienceTypeTag::Difficulty,
),
(
ExperienceType::Solution {
problem_ref: None,
approach: "test".into(),
worked: true,
},
ExperienceTypeTag::Solution,
),
(
ExperienceType::ErrorPattern {
signature: "test".into(),
fix: "test".into(),
prevention: "test".into(),
},
ExperienceTypeTag::ErrorPattern,
),
(
ExperienceType::SuccessPattern {
task_type: "test".into(),
approach: "test".into(),
quality: 0.9,
},
ExperienceTypeTag::SuccessPattern,
),
(
ExperienceType::UserPreference {
category: "test".into(),
preference: "test".into(),
strength: 0.8,
},
ExperienceTypeTag::UserPreference,
),
(
ExperienceType::ArchitecturalDecision {
decision: "test".into(),
rationale: "test".into(),
},
ExperienceTypeTag::ArchitecturalDecision,
),
(
ExperienceType::TechInsight {
technology: "test".into(),
insight: "test".into(),
},
ExperienceTypeTag::TechInsight,
),
(
ExperienceType::Fact {
statement: "test".into(),
source: "test".into(),
},
ExperienceTypeTag::Fact,
),
(
ExperienceType::Generic {
category: Some("test".into()),
},
ExperienceTypeTag::Generic,
),
];
for (experience_type, expected_tag) in cases {
assert_eq!(
experience_type.type_tag(),
expected_tag,
"Tag mismatch for {:?}",
experience_type,
);
}
}
#[test]
fn test_experience_type_postcard_roundtrip_all_variants() {
let variants = vec![
ExperienceType::Difficulty {
description: "compile error".into(),
severity: Severity::High,
},
ExperienceType::Solution {
problem_ref: Some(ExperienceId::new()),
approach: "added lifetime annotation".into(),
worked: true,
},
ExperienceType::ErrorPattern {
signature: "E0308 mismatched types".into(),
fix: "check return type".into(),
prevention: "use clippy".into(),
},
ExperienceType::SuccessPattern {
task_type: "refactoring".into(),
approach: "extract method".into(),
quality: 0.95,
},
ExperienceType::UserPreference {
category: "style".into(),
preference: "snake_case".into(),
strength: 0.9,
},
ExperienceType::ArchitecturalDecision {
decision: "use redb over SQLite".into(),
rationale: "pure Rust, ACID, no FFI".into(),
},
ExperienceType::TechInsight {
technology: "tokio".into(),
insight: "spawn_blocking for CPU-bound work".into(),
},
ExperienceType::Fact {
statement: "redb uses shadow paging".into(),
source: "redb docs".into(),
},
ExperienceType::Generic { category: None },
];
for variant in variants {
let bytes = postcard::to_stdvec(&variant).unwrap();
let restored: ExperienceType = postcard::from_bytes(&bytes).unwrap();
assert_eq!(variant.type_tag(), restored.type_tag());
}
}
#[test]
fn test_experience_postcard_roundtrip() {
let timestamp = Timestamp::now();
let exp = Experience {
id: ExperienceId::new(),
collective_id: CollectiveId::new(),
content: "Test experience content".into(),
embedding: vec![0.1, 0.2, 0.3], experience_type: ExperienceType::Fact {
statement: "Rust is memory-safe".into(),
source: "docs".into(),
},
importance: 0.8,
confidence: 0.9,
applications: BTreeMap::from([(InstanceId::new(), 5)]),
domain: vec!["rust".into(), "safety".into()],
related_files: vec!["src/main.rs".into()],
source_agent: AgentId::new("agent-1"),
source_task: Some(TaskId::new("task-42")),
timestamp,
last_reinforced: timestamp,
archived: false,
};
let bytes = postcard::to_stdvec(&exp).unwrap();
let restored: Experience = postcard::from_bytes(&bytes).unwrap();
assert_eq!(exp.id, restored.id);
assert_eq!(exp.collective_id, restored.collective_id);
assert_eq!(exp.content, restored.content);
assert!(restored.embedding.is_empty());
assert_eq!(
exp.experience_type.type_tag(),
restored.experience_type.type_tag()
);
assert_eq!(exp.importance, restored.importance);
assert_eq!(exp.confidence, restored.confidence);
assert_eq!(exp.applications, restored.applications);
assert_eq!(exp.applications(), restored.applications());
assert_eq!(exp.domain, restored.domain);
assert_eq!(exp.related_files, restored.related_files);
assert_eq!(exp.source_agent, restored.source_agent);
assert_eq!(exp.source_task, restored.source_task);
assert_eq!(exp.timestamp, restored.timestamp);
assert_eq!(exp.archived, restored.archived);
}
#[test]
fn test_experience_embedding_skipped_in_serialization() {
let timestamp = Timestamp::now();
let exp = Experience {
id: ExperienceId::new(),
collective_id: CollectiveId::new(),
content: "test".into(),
embedding: vec![1.0; 384], experience_type: ExperienceType::default(),
importance: 0.5,
confidence: 0.5,
applications: BTreeMap::new(),
domain: vec![],
related_files: vec![],
source_agent: AgentId::new("a"),
source_task: None,
timestamp,
last_reinforced: timestamp,
archived: false,
};
let bytes = postcard::to_stdvec(&exp).unwrap();
assert!(
bytes.len() < 500,
"Serialized size {} suggests embedding was not skipped",
bytes.len()
);
}
#[test]
fn test_new_experience_default() {
let ne = NewExperience::default();
assert_eq!(ne.collective_id, CollectiveId::nil());
assert!(ne.content.is_empty());
assert!(matches!(
ne.experience_type,
ExperienceType::Generic { category: None }
));
assert!(ne.embedding.is_none());
assert_eq!(ne.importance, 0.5);
assert_eq!(ne.confidence, 0.5);
assert!(ne.domain.is_empty());
assert!(ne.related_files.is_empty());
assert_eq!(ne.source_agent.as_str(), "anonymous");
assert!(ne.source_task.is_none());
}
#[test]
fn test_experience_update_default() {
let update = ExperienceUpdate::default();
assert!(update.importance.is_none());
assert!(update.confidence.is_none());
assert!(update.domain.is_none());
assert!(update.related_files.is_none());
assert!(update.archived.is_none());
}
}