use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use wm_core::{Coordinate5D, Galaxy, HolographicCoords};
pub type MemoryId = Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemoryType {
ShortTerm,
#[default]
LongTerm,
Emotional,
Narrative,
Symbolic,
Pattern,
Procedural,
Citta,
Hypothesis,
}
impl MemoryType {
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::ShortTerm,
Self::LongTerm,
Self::Emotional,
Self::Narrative,
Self::Symbolic,
Self::Pattern,
Self::Procedural,
Self::Citta,
Self::Hypothesis,
]
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ShortTerm => "short_term",
Self::LongTerm => "long_term",
Self::Emotional => "emotional",
Self::Narrative => "narrative",
Self::Symbolic => "symbolic",
Self::Pattern => "pattern",
Self::Procedural => "procedural",
Self::Citta => "citta",
Self::Hypothesis => "hypothesis",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Tier {
Working,
#[default]
Episodic,
Semantic,
Archival,
}
impl Tier {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Working => "working",
Self::Episodic => "episodic",
Self::Semantic => "semantic",
Self::Archival => "archival",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryMetadata {
pub id: MemoryId,
pub galaxy: Galaxy,
pub content_hash: String,
pub tags: Vec<String>,
pub importance: f32,
pub created_at: DateTime<Utc>,
pub accessed_at: DateTime<Utc>,
pub access_count: u64,
pub coords: HolographicCoords,
#[serde(default = "default_coord5d")]
pub coord5d: Coordinate5D,
#[serde(default)]
pub memory_type: MemoryType,
#[serde(default = "default_neuro_score")]
pub neuro_score: f32,
#[serde(default = "default_novelty_score")]
pub novelty_score: f32,
#[serde(default)]
pub emotional_valence: f32,
#[serde(default)]
pub emotional_weight: f32,
#[serde(default)]
pub is_protected: bool,
#[serde(default)]
pub is_private: bool,
#[serde(default)]
pub model_exclude: bool,
#[serde(default = "default_source")]
pub source: String,
#[serde(default = "default_source_trust")]
pub source_trust: f32,
#[serde(default = "default_half_life_days")]
pub half_life_days: f32,
#[serde(default)]
pub recall_count: u64,
#[serde(default = "default_version")]
pub version: u64,
#[serde(default = "default_agent_id")]
pub agent_id: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub topic: Option<String>,
#[serde(default)]
pub tier: Tier,
#[serde(default)]
pub class: Option<crate::typology::MemoryClass>,
#[serde(default)]
pub dup_count: u64,
#[serde(default)]
pub validity: wm_core::episodic::ValidityState,
#[serde(default)]
pub corroborated_by: Vec<uuid::Uuid>,
#[serde(default)]
pub revision_count: u32,
}
const fn default_coord5d() -> Coordinate5D {
Coordinate5D::new(0.5, 0.5, 0.5, 0.5, 0.5)
}
const fn default_neuro_score() -> f32 {
0.5
}
const fn default_novelty_score() -> f32 {
1.0
}
fn default_source() -> String {
"unattributed".to_string()
}
const fn default_source_trust() -> f32 {
0.5
}
#[must_use]
pub fn validity_enforced() -> bool {
matches!(std::env::var("WM_VALIDITY_ENFORCE"), Ok(v) if v == "1")
}
#[must_use]
pub fn corroboration_weight() -> f32 {
match std::env::var("WM_CORROBORATION_WEIGHT") {
Ok(v) => v.parse::<f32>().map_or(0.0, |w| {
if w.is_finite() && w >= 0.0 {
w.min(1.0)
} else {
0.0
}
}),
Err(_) => 0.0,
}
}
#[allow(clippy::suboptimal_flops)]
#[must_use]
pub fn corroboration_boost(score: f32, distinct_sessions: usize, weight: f32) -> f32 {
if weight <= 0.0 || distinct_sessions == 0 {
return score;
}
let n = distinct_sessions as f32;
score * (1.0 + weight * (n / (n + 2.0)))
}
#[allow(clippy::suboptimal_flops)]
#[must_use]
pub fn trust_weighted_score(score: f32, source_trust: f32, weight: f32) -> f32 {
let factor = 1.0 + weight * (source_trust.clamp(0.0, 1.0) - 0.7);
score * factor.max(0.0)
}
const fn default_half_life_days() -> f32 {
30.0
}
const fn default_version() -> u64 {
1
}
fn default_agent_id() -> String {
"system".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub metadata: MemoryMetadata,
pub content: String,
pub embedding: Option<Vec<f32>>,
}
impl Memory {
#[must_use]
pub fn new(galaxy: Galaxy, content: String) -> Self {
let now = Utc::now();
let id = Uuid::new_v4();
let content_hash = content_hash(&content);
let coord5d = Coordinate5D::encode_with_context(&content, 0.5, 0.5);
Self {
metadata: MemoryMetadata {
id,
galaxy,
content_hash,
tags: vec![],
importance: 0.5,
created_at: now,
accessed_at: now,
access_count: 0,
coords: HolographicCoords::new(galaxy, now.timestamp() as u64),
coord5d,
memory_type: MemoryType::default(),
neuro_score: default_neuro_score(),
novelty_score: default_novelty_score(),
emotional_valence: 0.0,
emotional_weight: 0.0,
is_protected: false,
is_private: false,
model_exclude: false,
source: default_source(),
source_trust: default_source_trust(),
half_life_days: default_half_life_days(),
recall_count: 0,
version: default_version(),
agent_id: default_agent_id(),
title: None,
topic: None,
tier: Tier::Working,
class: crate::typology::detect_class(&content, &[]),
dup_count: 0,
validity: wm_core::episodic::ValidityState::default(),
corroborated_by: Vec::new(),
revision_count: 0,
},
content,
embedding: None,
}
}
#[must_use]
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.metadata.tags = tags;
self
}
#[must_use]
pub const fn with_importance(mut self, importance: f32) -> Self {
self.metadata.importance = importance.clamp(0.0, 1.0);
self.metadata.coord5d.v = self.metadata.importance;
self
}
#[must_use]
pub const fn with_memory_type(mut self, memory_type: MemoryType) -> Self {
self.metadata.memory_type = memory_type;
self
}
#[must_use]
pub const fn with_emotional_valence(mut self, valence: f32, weight: f32) -> Self {
self.metadata.emotional_valence = valence.clamp(-1.0, 1.0);
self.metadata.emotional_weight = weight.clamp(0.0, 1.0);
self
}
#[must_use]
pub const fn with_protection(mut self, protected: bool) -> Self {
self.metadata.is_protected = protected;
self
}
#[must_use]
pub fn with_source(mut self, source: String, trust: f32) -> Self {
self.metadata.source = source;
self.metadata.source_trust = trust.clamp(0.0, 1.0);
self
}
#[must_use]
pub const fn with_half_life_days(mut self, days: f32) -> Self {
self.metadata.half_life_days = days.max(1.0);
self
}
#[must_use]
pub const fn with_neuro_score(mut self, score: f32) -> Self {
self.metadata.neuro_score = score.clamp(0.0, 1.0);
self
}
#[must_use]
pub const fn with_novelty_score(mut self, score: f32) -> Self {
self.metadata.novelty_score = score.clamp(0.0, 1.0);
self
}
#[must_use]
pub const fn with_privacy(mut self, is_private: bool, model_exclude: bool) -> Self {
self.metadata.is_private = is_private;
self.metadata.model_exclude = model_exclude;
self
}
pub fn transition_tier(&mut self, to: Tier) -> Result<(), wm_core::CoreError> {
let legal = matches!(
(self.metadata.tier, to),
(Tier::Working, Tier::Episodic | Tier::Archival)
| (Tier::Episodic, Tier::Semantic | Tier::Archival)
| (Tier::Semantic, Tier::Archival)
| (Tier::Archival, Tier::Episodic)
);
if !legal {
return Err(wm_core::CoreError::InvalidArgs(format!(
"illegal tier transition {} -> {} (dream-cycle ladder: one step forward, \
decay-out to archival, or archival promotion on read)",
self.metadata.tier.as_str(),
to.as_str()
)));
}
self.metadata.tier = to;
Ok(())
}
pub fn transition_validity(
&mut self,
transition: wm_core::episodic::MemoryTransition,
) -> Result<(), wm_core::episodic::ValidityTransitionError> {
let id = self.metadata.id;
self.metadata.validity.transition(id, transition)
}
#[must_use]
pub fn with_agent(mut self, agent_id: String, version: u64) -> Self {
self.metadata.agent_id = agent_id;
self.metadata.version = version;
self
}
#[must_use]
pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
self.embedding = Some(embedding);
self
}
pub fn record_access(&mut self) {
self.metadata.accessed_at = Utc::now();
self.metadata.access_count += 1;
}
pub fn recall(&mut self) {
let now = Utc::now();
self.metadata.accessed_at = now;
self.metadata.access_count += 1;
self.metadata.recall_count += 1;
let boost = 0.05 * (1.0 - self.metadata.neuro_score);
self.metadata.neuro_score = (self.metadata.neuro_score + boost).clamp(0.0, 1.0);
self.metadata.novelty_score = (self.metadata.novelty_score * 0.9).clamp(0.0, 1.0);
}
pub fn decay(&mut self, now: DateTime<Utc>) {
if self.metadata.is_protected {
return;
}
let days_since = ((now - self.metadata.accessed_at).num_seconds() as f32) / 86_400.0;
if days_since <= 0.0 {
return;
}
let half_life = self.metadata.half_life_days.max(1.0);
let factor = 0.5_f32.powf(days_since / half_life);
self.metadata.neuro_score = (self.metadata.neuro_score * factor).clamp(0.0, 1.0);
}
pub fn decay_importance(&mut self, factor: f32) {
if self.metadata.is_protected {
return;
}
self.metadata.importance = (self.metadata.importance * factor).clamp(0.0, 1.0);
}
#[must_use]
pub fn should_forget(&self, threshold: f32) -> bool {
!self.metadata.is_protected && self.metadata.importance < threshold
}
#[must_use]
pub fn is_telemetry_or_noise(&self) -> bool {
if self.metadata.is_protected {
return false;
}
for tag in &self.metadata.tags {
let t = tag.to_lowercase();
if t.contains("decision")
|| t.contains("breakthrough")
|| t.contains("architecture")
|| t.contains("policy")
|| t.contains("canon")
|| t.contains("aria")
|| t.contains("insight")
|| t.contains("lineage")
{
return false;
}
}
for tag in &self.metadata.tags {
let t = tag.to_lowercase();
if t.contains("telemetry")
|| t.contains("friction")
|| t.contains("turn_type")
|| t.contains("raw-archive")
|| t.contains("error_dump")
|| t.contains("benchmark")
|| t.contains("probe")
|| t.contains("rsi:telemetry")
{
return true;
}
}
let content = &self.content;
if (content.starts_with('{') || content.starts_with('['))
&& (content.contains("\"turn_type\"")
|| content.contains("\"friction\"")
|| content.contains("\"latency_ms\"")
|| content.contains("\"raw_archive\""))
{
return true;
}
if self.metadata.importance < 0.25 && content.len() < 50 {
return true;
}
false
}
}
#[must_use]
pub fn content_hash(content: &str) -> String {
use sha2::{Digest, Sha256};
let hasher = Sha256::digest(content.as_bytes());
format!("{hasher:x}")
}
#[must_use]
pub fn encode_embedding(embedding: &[f32]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(embedding.len() * 4);
for &v in embedding {
bytes.extend_from_slice(&v.to_le_bytes());
}
bytes
}
#[must_use]
pub fn decode_embedding(bytes: &[u8]) -> Vec<f32> {
bytes
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemoryStore;
use chrono::Duration;
use wm_core::Galaxy;
#[test]
fn trust_weighted_score_semantics() {
assert!((trust_weighted_score(2.0, 1.0, 0.0) - 2.0).abs() < 1e-5);
assert!((trust_weighted_score(2.0, 0.0, 0.0) - 2.0).abs() < 1e-5);
for w in [0.0f32, 0.15, 0.5, 1.0] {
assert!((trust_weighted_score(3.0, 0.7, w) - 3.0).abs() < 1e-5);
}
assert!(trust_weighted_score(1.0, 1.0, 0.5) > 1.0);
assert!(trust_weighted_score(1.0, 0.2, 0.5) < 1.0);
assert!((trust_weighted_score(1.0, 1.0, 0.5) - 1.15).abs() < 1e-5);
assert!((trust_weighted_score(1.0, 0.0, 0.5) - 0.65).abs() < 1e-5);
assert!((trust_weighted_score(2.0, 0.0, 1.0) - 0.6).abs() < 1e-5);
assert!((trust_weighted_score(2.0, 5.0, 1.0) - 2.6).abs() < 1e-5);
assert!((trust_weighted_score(2.0, -1.0, 1.0) - 0.6).abs() < 1e-5);
}
#[test]
fn memory_type_default_is_long_term() {
assert_eq!(MemoryType::default(), MemoryType::LongTerm);
}
#[test]
fn memory_type_all_has_9_variants() {
assert_eq!(MemoryType::all().len(), 9);
}
#[test]
fn memory_type_as_str_roundtrip() {
for &mt in MemoryType::all() {
let s = mt.as_str();
let json = serde_json::to_string(&mt).unwrap();
let expected = format!("\"{s}\"");
assert_eq!(json, expected);
}
}
#[test]
fn memory_type_serde_roundtrip() {
for &mt in MemoryType::all() {
let json = serde_json::to_string(&mt).unwrap();
let back: MemoryType = serde_json::from_str(&json).unwrap();
assert_eq!(mt, back);
}
}
#[test]
fn new_memory_has_enriched_defaults() {
let mem = Memory::new(Galaxy::Codex, "test".into());
let m = &mem.metadata;
assert_eq!(m.memory_type, MemoryType::LongTerm);
assert!((m.neuro_score - 0.5).abs() < f32::EPSILON);
assert!((m.novelty_score - 1.0).abs() < f32::EPSILON);
assert!((m.emotional_valence).abs() < f32::EPSILON);
assert!((m.emotional_weight).abs() < f32::EPSILON);
assert!(!m.is_protected);
assert!(!m.is_private);
assert!(!m.model_exclude);
assert_eq!(m.source, "unattributed");
assert!((m.source_trust - 0.5).abs() < f32::EPSILON);
assert!((m.half_life_days - 30.0).abs() < f32::EPSILON);
assert_eq!(m.recall_count, 0);
assert_eq!(m.version, 1);
assert_eq!(m.agent_id, "system");
}
#[test]
fn new_memory_validity_defaults_active() {
let mem = Memory::new(Galaxy::Codex, "test".into());
assert_eq!(
mem.metadata.validity,
wm_core::episodic::ValidityState::Active
);
assert!(mem.metadata.validity.is_current());
}
#[test]
fn legacy_metadata_without_validity_deserializes_active() {
let mem = Memory::new(Galaxy::Codex, "test".into());
let mut json = serde_json::to_value(&mem.metadata).unwrap();
json.as_object_mut().unwrap().remove("validity");
let back: MemoryMetadata = serde_json::from_value(json).unwrap();
assert_eq!(back.validity, wm_core::episodic::ValidityState::Active);
}
#[test]
fn transition_validity_supersede_roundtrip() {
let mut mem = Memory::new(Galaxy::Codex, "old claim".into());
let replacement = uuid::Uuid::new_v4();
mem.transition_validity(wm_core::episodic::MemoryTransition::Supersede { replacement })
.unwrap();
assert_eq!(
mem.metadata.validity,
wm_core::episodic::ValidityState::Superseded { by: replacement }
);
assert!(!mem.metadata.validity.is_current());
assert!(mem.metadata.validity.is_historical());
}
#[test]
fn transition_validity_refuses_self_supersession() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
let own = mem.metadata.id;
let err = mem
.transition_validity(wm_core::episodic::MemoryTransition::Supersede {
replacement: own,
})
.unwrap_err();
assert_eq!(
err,
wm_core::episodic::ValidityTransitionError::SelfSupersession
);
assert!(mem.metadata.validity.is_current());
}
#[test]
fn transition_validity_erased_is_terminal() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
mem.transition_validity(wm_core::episodic::MemoryTransition::Erase)
.unwrap();
assert_eq!(
mem.metadata.validity,
wm_core::episodic::ValidityState::Erased
);
let err = mem
.transition_validity(wm_core::episodic::MemoryTransition::Archive)
.unwrap_err();
assert_eq!(err, wm_core::episodic::ValidityTransitionError::Erased);
}
#[test]
fn new_memory_has_no_corroborators() {
let mem = Memory::new(Galaxy::Codex, "test".into());
assert!(mem.metadata.corroborated_by.is_empty());
}
#[test]
fn legacy_metadata_without_corroboration_deserializes_empty() {
let mem = Memory::new(Galaxy::Codex, "test".into());
let mut json = serde_json::to_value(&mem.metadata).unwrap();
json.as_object_mut().unwrap().remove("corroborated_by");
let back: MemoryMetadata = serde_json::from_value(json).unwrap();
assert!(back.corroborated_by.is_empty());
}
#[test]
fn corroboration_boost_semantics() {
assert!((corroboration_boost(2.0, 5, 0.0) - 2.0).abs() < 1e-5);
assert!((corroboration_boost(2.0, 0, 1.0) - 2.0).abs() < 1e-5);
let first = corroboration_boost(3.0, 1, 0.6);
assert!((first - 3.6).abs() < 1e-5);
let b1 = corroboration_boost(1.0, 1, 1.0);
let b2 = corroboration_boost(1.0, 2, 1.0);
let b100 = corroboration_boost(1.0, 100, 1.0);
assert!(b2 > b1 && b100 > b2 && b100 < 2.0);
assert!((b2 - 1.5).abs() < 1e-5);
}
#[test]
fn corroboration_weight_defaults_off() {
assert!((corroboration_weight() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn validity_enforced_defaults_off() {
assert!(!validity_enforced());
}
#[test]
fn recall_boosts_neuro_score() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
let initial = mem.metadata.neuro_score;
mem.recall();
assert!(mem.metadata.neuro_score > initial);
assert_eq!(mem.metadata.recall_count, 1);
assert_eq!(mem.metadata.access_count, 1);
}
#[test]
fn recall_has_diminishing_returns() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
mem.metadata.neuro_score = 0.9;
mem.recall();
let boost_high = 0.05 * (1.0 - 0.9); assert!((mem.metadata.neuro_score - (0.9 + boost_high)).abs() < 1e-5);
let mut mem2 = Memory::new(Galaxy::Codex, "test".into());
mem2.metadata.neuro_score = 0.1;
mem2.recall();
let boost_low = 0.05 * (1.0 - 0.1); assert!((mem2.metadata.neuro_score - (0.1 + boost_low)).abs() < 1e-5);
}
#[test]
fn recall_decays_novelty() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
let initial_novelty = mem.metadata.novelty_score;
mem.recall();
assert!(mem.metadata.novelty_score < initial_novelty);
assert!((initial_novelty.mul_add(-0.9, mem.metadata.novelty_score)).abs() < 1e-5);
}
#[test]
fn recall_neuro_score_caps_at_1() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
mem.metadata.neuro_score = 0.99;
for _ in 0..100 {
mem.recall();
}
assert!(mem.metadata.neuro_score > 0.999);
assert!(mem.metadata.neuro_score <= 1.0);
}
#[test]
fn decay_reduces_neuro_score_over_time() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
mem.metadata.neuro_score = 1.0;
mem.metadata.half_life_days = 30.0;
mem.metadata.accessed_at = Utc::now() - Duration::days(30);
mem.decay(Utc::now());
assert!((mem.metadata.neuro_score - 0.5).abs() < 0.01);
}
#[test]
fn decay_zero_time_is_noop() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
let score = mem.metadata.neuro_score;
mem.decay(Utc::now());
assert!((mem.metadata.neuro_score - score).abs() < f32::EPSILON);
}
#[test]
fn decay_respects_is_protected() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
mem.metadata.neuro_score = 1.0;
mem.metadata.is_protected = true;
mem.metadata.accessed_at = Utc::now() - Duration::days(365);
mem.decay(Utc::now());
assert!((mem.metadata.neuro_score - 1.0).abs() < f32::EPSILON);
}
#[test]
fn decay_uses_per_memory_half_life() {
let mut mem_short = Memory::new(Galaxy::Codex, "short".into());
mem_short.metadata.neuro_score = 1.0;
mem_short.metadata.half_life_days = 7.0;
mem_short.metadata.accessed_at = Utc::now() - Duration::days(7);
let mut mem_long = Memory::new(Galaxy::Codex, "long".into());
mem_long.metadata.neuro_score = 1.0;
mem_long.metadata.half_life_days = 90.0;
mem_long.metadata.accessed_at = Utc::now() - Duration::days(7);
mem_short.decay(Utc::now());
mem_long.decay(Utc::now());
assert!(mem_short.metadata.neuro_score < mem_long.metadata.neuro_score);
}
#[test]
fn should_forget_respects_protection() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
mem.metadata.importance = 0.01;
mem.metadata.is_protected = true;
assert!(!mem.should_forget(0.1));
}
#[test]
fn decay_importance_respects_protection() {
let mut mem = Memory::new(Galaxy::Codex, "test".into());
mem.metadata.importance = 0.5;
mem.metadata.is_protected = true;
mem.decay_importance(0.5);
assert!((mem.metadata.importance - 0.5).abs() < f32::EPSILON);
}
#[test]
fn with_memory_type_builder() {
let mem = Memory::new(Galaxy::Codex, "test".into()).with_memory_type(MemoryType::Emotional);
assert_eq!(mem.metadata.memory_type, MemoryType::Emotional);
}
#[test]
fn with_emotional_valence_clamps() {
let mem = Memory::new(Galaxy::Codex, "test".into()).with_emotional_valence(2.0, 2.0);
assert!((mem.metadata.emotional_valence - 1.0).abs() < f32::EPSILON);
assert!((mem.metadata.emotional_weight - 1.0).abs() < f32::EPSILON);
let mem2 = Memory::new(Galaxy::Codex, "test".into()).with_emotional_valence(-2.0, -1.0);
assert!((mem2.metadata.emotional_valence - (-1.0)).abs() < f32::EPSILON);
assert!((mem2.metadata.emotional_weight).abs() < f32::EPSILON);
}
#[test]
fn with_protection_builder() {
let mem = Memory::new(Galaxy::Codex, "test".into()).with_protection(true);
assert!(mem.metadata.is_protected);
}
#[test]
fn with_source_builder() {
let mem = Memory::new(Galaxy::Codex, "test".into()).with_source("web".into(), 0.5);
assert_eq!(mem.metadata.source, "web");
assert!((mem.metadata.source_trust - 0.5).abs() < f32::EPSILON);
}
#[test]
fn with_half_life_days_clamps_to_min_1() {
let mem = Memory::new(Galaxy::Codex, "test".into()).with_half_life_days(0.1);
assert!((mem.metadata.half_life_days - 1.0).abs() < f32::EPSILON);
}
#[test]
fn with_neuro_score_clamps() {
let mem = Memory::new(Galaxy::Codex, "test".into()).with_neuro_score(1.5);
assert!((mem.metadata.neuro_score - 1.0).abs() < f32::EPSILON);
let mem2 = Memory::new(Galaxy::Codex, "test".into()).with_neuro_score(-0.5);
assert!((mem2.metadata.neuro_score).abs() < f32::EPSILON);
}
#[test]
fn with_novelty_score_clamps() {
let mem = Memory::new(Galaxy::Codex, "test".into()).with_novelty_score(2.0);
assert!((mem.metadata.novelty_score - 1.0).abs() < f32::EPSILON);
}
#[test]
fn with_privacy_builder() {
let mem = Memory::new(Galaxy::Codex, "secret".into()).with_privacy(true, true);
assert!(mem.metadata.is_private);
assert!(mem.metadata.model_exclude);
}
#[test]
fn with_agent_builder() {
let mem = Memory::new(Galaxy::Codex, "test".into()).with_agent("agent-007".into(), 42);
assert_eq!(mem.metadata.agent_id, "agent-007");
assert_eq!(mem.metadata.version, 42);
}
#[test]
fn serde_backward_compat_missing_enriched_fields() {
let old_json = serde_json::json!({
"metadata": {
"id": uuid::Uuid::new_v4().to_string(),
"galaxy": "Codex",
"content_hash": "abc123",
"tags": [],
"importance": 0.5,
"created_at": "2025-01-01T00:00:00Z",
"accessed_at": "2025-01-01T00:00:00Z",
"access_count": 0,
"coords": {
"galaxy": 2,
"sector": 0,
"radial": 0.5,
"angular": 0.0,
"temporal": 0,
"consciousness": 0.5
}
},
"content": "old memory",
"embedding": null
});
let mem: Memory = serde_json::from_value(old_json).unwrap();
assert_eq!(mem.metadata.memory_type, MemoryType::LongTerm);
assert!((mem.metadata.neuro_score - 0.5).abs() < f32::EPSILON);
assert!((mem.metadata.novelty_score - 1.0).abs() < f32::EPSILON);
assert!(!mem.metadata.is_protected);
assert_eq!(mem.metadata.source, "unattributed");
assert!((mem.metadata.source_trust - 0.5).abs() < f32::EPSILON);
assert!((mem.metadata.half_life_days - 30.0).abs() < f32::EPSILON);
assert_eq!(mem.metadata.recall_count, 0);
assert_eq!(mem.metadata.version, 1);
assert_eq!(mem.metadata.agent_id, "system");
assert_eq!(mem.metadata.title, None);
assert_eq!(mem.metadata.topic, None);
assert_eq!(mem.metadata.tier, Tier::Episodic);
assert_eq!(mem.metadata.class, None);
assert_eq!(mem.metadata.dup_count, 0);
}
#[test]
fn fresh_memory_stamps_working_tier_and_detected_class() {
let friction = Memory::new(
Galaxy::Codex,
"## Auto-logged Friction: Tool dispatch error\n\nbody".into(),
);
assert_eq!(friction.metadata.tier, Tier::Working);
assert_eq!(
friction.metadata.class,
Some(crate::typology::MemoryClass::Telemetry)
);
let plain = Memory::new(Galaxy::Codex, "a normal thought about kumquats".into());
assert_eq!(plain.metadata.tier, Tier::Working);
assert_eq!(plain.metadata.class, None);
}
#[test]
fn tier_transition_ladder_is_enforced() {
let mut m = Memory::new(Galaxy::Codex, "tier ladder".into());
assert_eq!(m.metadata.tier, Tier::Working);
m.transition_tier(Tier::Episodic).unwrap();
assert_eq!(m.metadata.tier, Tier::Episodic);
m.transition_tier(Tier::Semantic).unwrap();
assert_eq!(m.metadata.tier, Tier::Semantic);
m.transition_tier(Tier::Archival).unwrap();
assert_eq!(m.metadata.tier, Tier::Archival);
m.transition_tier(Tier::Episodic).unwrap();
assert_eq!(m.metadata.tier, Tier::Episodic);
m.transition_tier(Tier::Archival).unwrap();
for (from, to) in [
(Tier::Semantic, Tier::Episodic),
(Tier::Semantic, Tier::Working),
(Tier::Archival, Tier::Working),
(Tier::Archival, Tier::Semantic),
(Tier::Working, Tier::Semantic),
] {
let mut mem = Memory::new(Galaxy::Codex, "illegal move probe".into());
mem.metadata.tier = from;
let err = mem.transition_tier(to).unwrap_err();
assert!(
err.to_string().contains("illegal tier transition"),
"{from:?} -> {to:?} must be refused, got: {err}"
);
assert_eq!(mem.metadata.tier, from, "refused move must not mutate");
}
}
#[test]
fn msgpack_roundtrip_preserves_enriched_fields() {
let mem = Memory::new(Galaxy::Codex, "test".into())
.with_memory_type(MemoryType::Emotional)
.with_emotional_valence(0.8, 0.6)
.with_protection(true)
.with_source("tool".into(), 0.7)
.with_half_life_days(14.0)
.with_neuro_score(0.75)
.with_novelty_score(0.3)
.with_privacy(true, false)
.with_agent("agent-x".into(), 5);
let bytes = rmp_serde::to_vec(&mem).unwrap();
let back: Memory = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(back.metadata.memory_type, MemoryType::Emotional);
assert!((back.metadata.neuro_score - 0.75).abs() < 1e-5);
assert!((back.metadata.novelty_score - 0.3).abs() < 1e-5);
assert!((back.metadata.emotional_valence - 0.8).abs() < 1e-5);
assert!((back.metadata.emotional_weight - 0.6).abs() < 1e-5);
assert!(back.metadata.is_protected);
assert!(back.metadata.is_private);
assert!(!back.metadata.model_exclude);
assert_eq!(back.metadata.source, "tool");
assert!((back.metadata.source_trust - 0.7).abs() < 1e-5);
assert!((back.metadata.half_life_days - 14.0).abs() < 1e-5);
assert_eq!(back.metadata.agent_id, "agent-x");
assert_eq!(back.metadata.version, 5);
}
#[test]
fn lmdb_roundtrip_preserves_enriched_fields() {
let tmp = tempfile::tempdir().unwrap();
let store = MemoryStore::open_default(tmp.path()).unwrap();
let mem = Memory::new(Galaxy::Codex, "enriched".into())
.with_memory_type(MemoryType::Pattern)
.with_emotional_valence(-0.5, 0.8)
.with_protection(true)
.with_source("inferred".into(), 0.3)
.with_half_life_days(7.0)
.with_neuro_score(0.9)
.with_novelty_score(0.2)
.with_agent("test-agent".into(), 3);
let id = mem.metadata.id;
store.put(Galaxy::Codex, &mem).unwrap();
let back = store.get(Galaxy::Codex, id).unwrap().unwrap();
assert_eq!(back.metadata.memory_type, MemoryType::Pattern);
assert!((back.metadata.neuro_score - 0.9).abs() < 1e-5);
assert!((back.metadata.emotional_valence - (-0.5)).abs() < 1e-5);
assert!(back.metadata.is_protected);
assert_eq!(back.metadata.source, "inferred");
assert!((back.metadata.source_trust - 0.3).abs() < 1e-5);
assert!((back.metadata.half_life_days - 7.0).abs() < 1e-5);
assert_eq!(back.metadata.agent_id, "test-agent");
assert_eq!(back.metadata.version, 3);
}
}