use std::time::Duration;
use chrono::{DateTime, Utc};
use crate::tile::Tile;
#[derive(Debug, Clone)]
pub struct DecaySchedule {
pub half_life: Duration,
pub valence_weight: f64,
pub min_valence_for_permanent: f64,
}
impl Default for DecaySchedule {
fn default() -> Self {
Self {
half_life: Duration::from_secs(7 * 24 * 3600), valence_weight: 0.5,
min_valence_for_permanent: 0.9,
}
}
}
impl DecaySchedule {
pub fn retention(&self, tile: &Tile, now: DateTime<Utc>) -> f64 {
if tile.valence >= self.min_valence_for_permanent {
return 1.0;
}
let elapsed = now.signed_duration_since(tile.accessed_at);
let elapsed_secs = elapsed.num_seconds().max(0) as f64;
let half_life_secs = self.half_life.as_secs() as f64;
let valence_factor = 1.0 + self.valence_weight * tile.valence;
let effective_half_life = half_life_secs * valence_factor;
let access_factor = 1.0 + (tile.access_count as f64).ln().max(0.0) * 0.3;
let adjusted_half_life = effective_half_life * access_factor;
let decay_constant = 0.693 / adjusted_half_life; let retention = (-decay_constant * elapsed_secs).exp();
retention.clamp(0.0, 1.0)
}
pub fn should_forget(&self, tile: &Tile, now: DateTime<Utc>) -> bool {
self.retention(tile, now) < 0.1 }
pub fn reconsolidate(&self, tile: &mut Tile, new_context: &str) {
tile.accessed_at = Utc::now();
tile.access_count += 1;
if !new_context.is_empty() {
let additions: Vec<&str> = new_context
.split('.')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.take(3)
.collect();
if !additions.is_empty() {
tile.summary = format!("{} [updated: {}]", tile.summary, additions.join("; "));
}
}
tile.valence = (tile.valence + 0.05).min(1.0);
}
pub fn fast_decay() -> Self {
Self {
half_life: Duration::from_secs(60), valence_weight: 0.3,
min_valence_for_permanent: 0.95,
}
}
pub fn slow_decay() -> Self {
Self {
half_life: Duration::from_secs(365 * 24 * 3600), valence_weight: 1.0,
min_valence_for_permanent: 0.7,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn test_tile(valence: f64) -> Tile {
Tile {
id: crate::tile::TileId::new(),
source_hash: String::new(),
constraints: HashMap::new(),
summary: "test tile".into(),
context_required: vec![],
valence,
created_at: Utc::now(),
accessed_at: Utc::now(),
access_count: 0,
generation: 0,
parent_id: None,
}
}
#[test]
fn fresh_tile_full_retention() {
let schedule = DecaySchedule::default();
let tile = test_tile(0.5);
let retention = schedule.retention(&tile, Utc::now());
assert!(retention > 0.99, "fresh tile should have ~1.0 retention, got {}", retention);
}
#[test]
fn permanent_never_decays() {
let schedule = DecaySchedule::default();
let mut tile = test_tile(0.95);
tile.accessed_at = Utc::now() - chrono::Duration::days(365);
let retention = schedule.retention(&tile, Utc::now());
assert_eq!(retention, 1.0);
}
#[test]
fn fast_decay_forgets_quickly() {
let schedule = DecaySchedule::fast_decay();
let mut tile = test_tile(0.1);
tile.accessed_at = Utc::now() - chrono::Duration::minutes(10);
assert!(schedule.should_forget(&tile, Utc::now()));
}
#[test]
fn reconsolidate_resets_decay() {
let schedule = DecaySchedule::default();
let mut tile = test_tile(0.5);
tile.accessed_at = Utc::now() - chrono::Duration::days(30);
let before_retention = schedule.retention(&tile, Utc::now());
schedule.reconsolidate(&mut tile, "New information about the project");
let after_retention = schedule.retention(&tile, Utc::now());
assert!(after_retention > before_retention);
assert_eq!(tile.access_count, 1);
assert!(tile.valence > 0.5);
}
#[test]
fn access_count_slows_decay() {
let schedule = DecaySchedule::default();
let mut tile1 = test_tile(0.5);
tile1.accessed_at = Utc::now() - chrono::Duration::days(7);
tile1.access_count = 1;
let mut tile2 = test_tile(0.5);
tile2.accessed_at = Utc::now() - chrono::Duration::days(7);
tile2.access_count = 100;
let r1 = schedule.retention(&tile1, Utc::now());
let r2 = schedule.retention(&tile2, Utc::now());
assert!(r2 > r1, "more accesses should retain better: {} vs {}", r2, r1);
}
#[test]
fn high_valence_decays_slower() {
let schedule = DecaySchedule::default();
let mut low = test_tile(0.1);
low.accessed_at = Utc::now() - chrono::Duration::days(7);
let mut high = test_tile(0.8);
high.accessed_at = Utc::now() - chrono::Duration::days(7);
let r_low = schedule.retention(&low, Utc::now());
let r_high = schedule.retention(&high, Utc::now());
assert!(r_high > r_low);
}
}