1use std::time::Duration;
2
3use chrono::{DateTime, Utc};
4
5use crate::tile::Tile;
6
7#[derive(Debug, Clone)]
9pub struct DecaySchedule {
10 pub half_life: Duration,
11 pub valence_weight: f64,
12 pub min_valence_for_permanent: f64,
13}
14
15impl Default for DecaySchedule {
16 fn default() -> Self {
17 Self {
18 half_life: Duration::from_secs(7 * 24 * 3600), valence_weight: 0.5,
20 min_valence_for_permanent: 0.9,
21 }
22 }
23}
24
25impl DecaySchedule {
26 pub fn retention(&self, tile: &Tile, now: DateTime<Utc>) -> f64 {
31 if tile.valence >= self.min_valence_for_permanent {
33 return 1.0;
34 }
35
36 let elapsed = now.signed_duration_since(tile.accessed_at);
37 let elapsed_secs = elapsed.num_seconds().max(0) as f64;
38 let half_life_secs = self.half_life.as_secs() as f64;
39
40 let valence_factor = 1.0 + self.valence_weight * tile.valence;
42 let effective_half_life = half_life_secs * valence_factor;
43
44 let access_factor = 1.0 + (tile.access_count as f64).ln().max(0.0) * 0.3;
46 let adjusted_half_life = effective_half_life * access_factor;
47
48 let decay_constant = 0.693 / adjusted_half_life; let retention = (-decay_constant * elapsed_secs).exp();
50
51 retention.clamp(0.0, 1.0)
52 }
53
54 pub fn should_forget(&self, tile: &Tile, now: DateTime<Utc>) -> bool {
56 self.retention(tile, now) < 0.1 }
58
59 pub fn reconsolidate(&self, tile: &mut Tile, new_context: &str) {
64 tile.accessed_at = Utc::now();
66 tile.access_count += 1;
67
68 if !new_context.is_empty() {
70 let additions: Vec<&str> = new_context
72 .split('.')
73 .map(|s| s.trim())
74 .filter(|s| !s.is_empty())
75 .take(3)
76 .collect();
77
78 if !additions.is_empty() {
79 tile.summary = format!("{} [updated: {}]", tile.summary, additions.join("; "));
80 }
81 }
82
83 tile.valence = (tile.valence + 0.05).min(1.0);
85 }
86
87 pub fn fast_decay() -> Self {
89 Self {
90 half_life: Duration::from_secs(60), valence_weight: 0.3,
92 min_valence_for_permanent: 0.95,
93 }
94 }
95
96 pub fn slow_decay() -> Self {
98 Self {
99 half_life: Duration::from_secs(365 * 24 * 3600), valence_weight: 1.0,
101 min_valence_for_permanent: 0.7,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use std::collections::HashMap;
110
111 fn test_tile(valence: f64) -> Tile {
112 Tile {
113 id: crate::tile::TileId::new(),
114 source_hash: String::new(),
115 constraints: HashMap::new(),
116 summary: "test tile".into(),
117 context_required: vec![],
118 valence,
119 created_at: Utc::now(),
120 accessed_at: Utc::now(),
121 access_count: 0,
122 generation: 0,
123 parent_id: None,
124 }
125 }
126
127 #[test]
128 fn fresh_tile_full_retention() {
129 let schedule = DecaySchedule::default();
130 let tile = test_tile(0.5);
131 let retention = schedule.retention(&tile, Utc::now());
132 assert!(retention > 0.99, "fresh tile should have ~1.0 retention, got {}", retention);
133 }
134
135 #[test]
136 fn permanent_never_decays() {
137 let schedule = DecaySchedule::default();
138 let mut tile = test_tile(0.95);
139 tile.accessed_at = Utc::now() - chrono::Duration::days(365);
140 let retention = schedule.retention(&tile, Utc::now());
141 assert_eq!(retention, 1.0);
142 }
143
144 #[test]
145 fn fast_decay_forgets_quickly() {
146 let schedule = DecaySchedule::fast_decay();
147 let mut tile = test_tile(0.1);
148 tile.accessed_at = Utc::now() - chrono::Duration::minutes(10);
149 assert!(schedule.should_forget(&tile, Utc::now()));
150 }
151
152 #[test]
153 fn reconsolidate_resets_decay() {
154 let schedule = DecaySchedule::default();
155 let mut tile = test_tile(0.5);
156 tile.accessed_at = Utc::now() - chrono::Duration::days(30);
158 let before_retention = schedule.retention(&tile, Utc::now());
159
160 schedule.reconsolidate(&mut tile, "New information about the project");
162
163 let after_retention = schedule.retention(&tile, Utc::now());
164 assert!(after_retention > before_retention);
165 assert_eq!(tile.access_count, 1);
166 assert!(tile.valence > 0.5);
167 }
168
169 #[test]
170 fn access_count_slows_decay() {
171 let schedule = DecaySchedule::default();
172 let mut tile1 = test_tile(0.5);
173 tile1.accessed_at = Utc::now() - chrono::Duration::days(7);
174 tile1.access_count = 1;
175
176 let mut tile2 = test_tile(0.5);
177 tile2.accessed_at = Utc::now() - chrono::Duration::days(7);
178 tile2.access_count = 100;
179
180 let r1 = schedule.retention(&tile1, Utc::now());
181 let r2 = schedule.retention(&tile2, Utc::now());
182 assert!(r2 > r1, "more accesses should retain better: {} vs {}", r2, r1);
183 }
184
185 #[test]
186 fn high_valence_decays_slower() {
187 let schedule = DecaySchedule::default();
188 let mut low = test_tile(0.1);
189 low.accessed_at = Utc::now() - chrono::Duration::days(7);
190 let mut high = test_tile(0.8);
191 high.accessed_at = Utc::now() - chrono::Duration::days(7);
192
193 let r_low = schedule.retention(&low, Utc::now());
194 let r_high = schedule.retention(&high, Utc::now());
195 assert!(r_high > r_low);
196 }
197}