use ruvector_domain_expansion::DomainId;
use crate::{
AnticipationHint, ConsolidationConfig, ConsolidationResult, TemporalConfig, TemporalMemory,
};
use exo_core::{Metadata, Pattern, PatternId, SubstrateTime};
const DIM: usize = 64;
fn domain_hash(id: &str) -> f32 {
let mut h: u32 = 0x811c_9dc5;
for b in id.bytes() {
h ^= b as u32;
h = h.wrapping_mul(0x0100_0193);
}
h as f32 / u32::MAX as f32
}
fn build_embedding(src: &DomainId, dst: &DomainId, cycle: u64, delta_reward: f32) -> Vec<f32> {
let mut emb = vec![0.0f32; DIM];
let sh = domain_hash(&src.0);
let dh = domain_hash(&dst.0);
emb[0] = sh;
emb[1] = dh;
emb[2] = (cycle as f32).ln_1p() / (1_000.0_f32).ln_1p();
emb[3] = delta_reward.clamp(0.0, 1.0);
for i in 4..DIM {
let phase = (sh + dh) * i as f32 * std::f32::consts::PI / DIM as f32;
emb[i] = phase.sin() * 0.5 + 0.5;
}
emb
}
pub struct TransferTimeline {
memory: TemporalMemory,
last_transfer_id: Option<PatternId>,
count: usize,
}
impl TransferTimeline {
pub fn new() -> Self {
let config = TemporalConfig {
consolidation: ConsolidationConfig {
salience_threshold: 0.1,
..Default::default()
},
..Default::default()
};
Self {
memory: TemporalMemory::new(config),
last_transfer_id: None,
count: 0,
}
}
pub fn record_transfer(
&mut self,
src: &DomainId,
dst: &DomainId,
cycle: u64,
delta_reward: f32,
) -> crate::Result<PatternId> {
let embedding = build_embedding(src, dst, cycle, delta_reward);
let salience = delta_reward.abs().clamp(0.1, 1.0);
let antecedents: Vec<PatternId> = self.last_transfer_id.iter().copied().collect();
let pattern = Pattern {
id: PatternId::new(),
embedding,
metadata: Metadata::default(),
timestamp: SubstrateTime::now(),
antecedents: antecedents.clone(),
salience,
};
let id = self.memory.store(pattern, &antecedents)?;
self.last_transfer_id = Some(id);
self.count += 1;
Ok(id)
}
pub fn consolidate(&self) -> ConsolidationResult {
self.memory.consolidate()
}
pub fn anticipate_next(&self) -> Vec<AnticipationHint> {
match self.last_transfer_id {
Some(id) => vec![
AnticipationHint::CausalChain { context: id },
AnticipationHint::SequentialPattern { recent: vec![id] },
],
None => vec![],
}
}
pub fn count(&self) -> usize {
self.count
}
pub fn causal_graph(&self) -> &crate::CausalGraph {
self.memory.causal_graph()
}
}
impl Default for TransferTimeline {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_record_and_count() {
let mut tl = TransferTimeline::new();
let src = DomainId("retrieval".to_string());
let dst = DomainId("graph".to_string());
tl.record_transfer(&src, &dst, 1, 0.3).unwrap();
tl.record_transfer(&src, &dst, 2, 0.5).unwrap();
assert_eq!(tl.count(), 2);
}
#[test]
fn test_consolidate() {
let mut tl = TransferTimeline::new();
let src = DomainId("a".to_string());
let dst = DomainId("b".to_string());
for i in 0..5 {
tl.record_transfer(&src, &dst, i, 0.4).unwrap();
}
let result = tl.consolidate();
assert!(result.num_consolidated >= 1);
}
#[test]
fn test_anticipate_empty() {
let tl = TransferTimeline::new();
assert!(tl.anticipate_next().is_empty());
}
#[test]
fn test_anticipate_after_record() {
let mut tl = TransferTimeline::new();
let src = DomainId("x".to_string());
let dst = DomainId("y".to_string());
tl.record_transfer(&src, &dst, 1, 0.4).unwrap();
let hints = tl.anticipate_next();
assert!(!hints.is_empty());
}
#[test]
fn test_embedding_values() {
let src = DomainId("retrieval".to_string());
let dst = DomainId("graph".to_string());
let emb = build_embedding(&src, &dst, 42, 0.7);
assert_eq!(emb.len(), DIM);
assert!((emb[3] - 0.7).abs() < 1e-6);
}
}