1use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10use std::sync::Arc;
11use uuid::Uuid;
12
13use crate::belief::BeliefGraph;
14use crate::errors::{ReasoningError, Result};
15use crate::hypothesis::{Hypothesis, HypothesisBoard, HypothesisId};
16
17#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct SnapshotId(Uuid);
20
21impl SnapshotId {
22 pub fn new() -> Self {
24 Self(Uuid::new_v4())
25 }
26}
27
28impl Default for SnapshotId {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl std::fmt::Display for SnapshotId {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{}", self.0)
37 }
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
42pub struct BeliefSnapshot {
43 pub id: SnapshotId,
45 pub hypotheses: Vec<Hypothesis>,
47 pub dependencies: Vec<(HypothesisId, HypothesisId)>,
49 pub created_at: DateTime<Utc>,
51 pub expires_at: DateTime<Utc>,
53}
54
55impl BeliefSnapshot {
56 pub fn is_expired(&self) -> bool {
58 Utc::now() > self.expires_at
59 }
60
61 pub fn remaining_time(&self) -> Option<Duration> {
63 if self.is_expired() {
64 None
65 } else {
66 Some(self.expires_at - Utc::now())
67 }
68 }
69}
70
71pub struct SnapshotStore {
73 snapshots: BTreeMap<DateTime<Utc>, BeliefSnapshot>,
75 window_duration: Duration,
77}
78
79impl SnapshotStore {
80 pub fn new() -> Self {
82 Self {
83 snapshots: BTreeMap::new(),
84 window_duration: Duration::minutes(5),
85 }
86 }
87
88 pub fn with_window(window_duration: Duration) -> Self {
90 Self {
91 snapshots: BTreeMap::new(),
92 window_duration,
93 }
94 }
95
96 pub async fn save(
101 &mut self,
102 board: &HypothesisBoard,
103 graph: &BeliefGraph,
104 ) -> SnapshotId {
105 let hypotheses = board.list().await.unwrap_or_default();
107 let dependencies = self.capture_dependencies(graph);
108
109 let created_at = Utc::now();
111 let expires_at = created_at + self.window_duration;
112 let id = SnapshotId::new();
113
114 let snapshot = BeliefSnapshot {
115 id: id.clone(),
116 hypotheses,
117 dependencies,
118 created_at,
119 expires_at,
120 };
121
122 self.snapshots.insert(created_at, snapshot);
124
125 self.cleanup_expired();
127
128 id
129 }
130
131 pub fn get(&self, id: &SnapshotId) -> Option<&BeliefSnapshot> {
133 self.snapshots
134 .values()
135 .find(|s| &s.id == id)
136 }
137
138 pub async fn restore(
143 &self,
144 id: &SnapshotId,
145 _board: &Arc<HypothesisBoard>,
146 _graph: &Arc<BeliefGraph>,
147 ) -> Result<()> {
148 let snapshot = self
149 .get(id)
150 .ok_or_else(|| ReasoningError::NotFound(format!("Snapshot {} not found", id)))?;
151
152 if snapshot.is_expired() {
153 return Err(ReasoningError::InvalidState(format!(
154 "Snapshot {} expired at {}",
155 id, snapshot.expires_at
156 )));
157 }
158
159 tracing::info!(
165 "Snapshot restore requested for {}: {} hypotheses, {} dependencies",
166 id,
167 snapshot.hypotheses.len(),
168 snapshot.dependencies.len()
169 );
170
171 Ok(())
172 }
173
174 pub fn get_snapshot_data(&self, id: &SnapshotId) -> Result<BeliefSnapshot> {
176 self.get(id)
177 .cloned()
178 .ok_or_else(|| ReasoningError::NotFound(format!("Snapshot {} not found", id)))
179 }
180
181 fn cleanup_expired(&mut self) {
183 let now = Utc::now();
184 self.snapshots.retain(|&created, _| created + self.window_duration > now);
185 }
186
187 pub fn list_snapshots(&self) -> Vec<&BeliefSnapshot> {
189 let now = Utc::now();
190 self.snapshots
191 .values()
192 .filter(|s| s.expires_at > now)
193 .collect()
194 }
195
196 pub fn is_expired(&self, id: &SnapshotId) -> bool {
198 self.get(id)
199 .map(|s| s.is_expired())
200 .unwrap_or(true)
201 }
202
203 pub fn active_count(&self) -> usize {
205 let now = Utc::now();
206 self.snapshots
207 .values()
208 .filter(|s| s.expires_at > now)
209 .count()
210 }
211
212 fn capture_dependencies(&self, graph: &BeliefGraph) -> Vec<(HypothesisId, HypothesisId)> {
214 graph
216 .nodes()
217 .iter()
218 .flat_map(|&node_id| {
219 graph
220 .dependees(node_id)
221 .unwrap_or_default()
222 .into_iter()
223 .map(move |dep_id| (node_id, dep_id))
224 })
225 .collect()
226 }
227
228 pub fn remove(&mut self, id: &SnapshotId) -> bool {
230 if let Some(created_at) = self.snapshots.values().find(|s| &s.id == id).map(|s| s.created_at) {
232 self.snapshots.remove(&created_at);
233 true
234 } else {
235 false
236 }
237 }
238}
239
240impl Default for SnapshotStore {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::hypothesis::confidence::Confidence;
250
251 #[test]
252 fn test_snapshot_id_unique() {
253 let id1 = SnapshotId::new();
254 let id2 = SnapshotId::new();
255 assert_ne!(id1, id2);
256 }
257
258 #[test]
259 fn test_snapshot_default_id() {
260 let id = SnapshotId::default();
261 assert_ne!(id.0, Uuid::nil());
263 }
264
265 #[tokio::test]
266 async fn test_save_creates_snapshot() {
267 let mut store = SnapshotStore::new();
268 let board = HypothesisBoard::in_memory();
269 let graph = BeliefGraph::new();
270
271 let id = store.save(&board, &graph).await;
272
273 let snapshot = store.get(&id);
274 assert!(snapshot.is_some());
275 assert_eq!(&snapshot.unwrap().id, &id);
276 }
277
278 #[tokio::test]
279 async fn test_get_returns_none_for_nonexistent() {
280 let store = SnapshotStore::new();
281 let fake_id = SnapshotId::new();
282 assert!(store.get(&fake_id).is_none());
283 }
284
285 #[tokio::test]
286 async fn test_cleanup_removes_expired() {
287 let mut store = SnapshotStore::with_window(Duration::seconds(1)); let board = HypothesisBoard::in_memory();
290 let graph = BeliefGraph::new();
291
292 let id = store.save(&board, &graph).await;
294 assert!(store.get(&id).is_some());
295
296 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
298
299 store.cleanup_expired();
301
302 assert!(store.get(&id).is_none());
304 }
305
306 #[test]
307 fn test_is_expired_for_old_snapshot() {
308 let _store = SnapshotStore::with_window(Duration::milliseconds(100));
309
310 let created_at = Utc::now() - Duration::seconds(1);
311 let expires_at = created_at + Duration::milliseconds(100);
312 let snapshot = BeliefSnapshot {
313 id: SnapshotId::new(),
314 hypotheses: vec![],
315 dependencies: vec![],
316 created_at,
317 expires_at,
318 };
319
320 assert!(snapshot.is_expired());
322 }
323
324 #[test]
325 fn test_is_expired_for_fresh_snapshot() {
326 let created_at = Utc::now();
327 let expires_at = created_at + Duration::minutes(5);
328 let snapshot = BeliefSnapshot {
329 id: SnapshotId::new(),
330 hypotheses: vec![],
331 dependencies: vec![],
332 created_at,
333 expires_at,
334 };
335
336 assert!(!snapshot.is_expired());
337 }
338
339 #[tokio::test]
340 async fn test_list_snapshots_returns_only_active() {
341 let mut store = SnapshotStore::with_window(Duration::milliseconds(100));
342
343 let board = HypothesisBoard::in_memory();
344 let graph = BeliefGraph::new();
345
346 let _id1 = store.save(&board, &graph).await;
348
349 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
351
352 let id2 = store.save(&board, &graph).await;
354
355 store.cleanup_expired();
357
358 let active = store.list_snapshots();
360 assert_eq!(active.len(), 1);
361 assert_eq!(&active[0].id, &id2);
362 }
363
364 #[tokio::test]
365 async fn test_window_duration_respected() {
366 let store = SnapshotStore::new();
368 assert_eq!(store.window_duration, Duration::minutes(5));
369
370 let custom_store = SnapshotStore::with_window(Duration::hours(1));
372 assert_eq!(custom_store.window_duration, Duration::hours(1));
373 }
374
375 #[tokio::test]
376 async fn test_remove_snapshot() {
377 let mut store = SnapshotStore::new();
378
379 let board = HypothesisBoard::in_memory();
380 let graph = BeliefGraph::new();
381
382 let id = store.save(&board, &graph).await;
383 assert!(store.get(&id).is_some());
384
385 assert!(store.remove(&id));
387 assert!(store.get(&id).is_none());
388
389 assert!(!store.remove(&id));
391 }
392
393 #[tokio::test]
394 async fn test_active_count() {
395 let mut store = SnapshotStore::with_window(Duration::seconds(1));
396
397 let board = HypothesisBoard::in_memory();
398 let graph = BeliefGraph::new();
399
400 assert_eq!(store.active_count(), 0);
401
402 store.save(&board, &graph).await;
403 assert_eq!(store.active_count(), 1);
404
405 store.save(&board, &graph).await;
406 assert_eq!(store.active_count(), 2);
407
408 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
410 store.cleanup_expired();
411
412 assert_eq!(store.active_count(), 0);
413 }
414
415 #[tokio::test]
416 async fn test_remaining_time() {
417 let created_at = Utc::now();
418 let expires_at = created_at + Duration::minutes(5);
419 let snapshot = BeliefSnapshot {
420 id: SnapshotId::new(),
421 hypotheses: vec![],
422 dependencies: vec![],
423 created_at,
424 expires_at,
425 };
426
427 let remaining = snapshot.remaining_time();
428 assert!(remaining.is_some());
429 assert!(remaining.unwrap().num_seconds() > 0);
430 assert!(remaining.unwrap().num_seconds() <= 300); }
432
433 #[tokio::test]
434 async fn test_remaining_time_for_expired() {
435 let created_at = Utc::now() - Duration::minutes(10);
436 let expires_at = created_at + Duration::minutes(5);
437 let snapshot = BeliefSnapshot {
438 id: SnapshotId::new(),
439 hypotheses: vec![],
440 dependencies: vec![],
441 created_at,
442 expires_at,
443 };
444
445 assert!(snapshot.remaining_time().is_none());
446 }
447
448 #[tokio::test]
449 async fn test_snapshot_with_hypotheses() {
450 let mut store = SnapshotStore::new();
451
452 let board = HypothesisBoard::in_memory();
453 let prior = Confidence::new(0.5).unwrap();
454 let _h1 = board
455 .propose("Test hypothesis 1", prior)
456 .await
457 .unwrap();
458 let _h2 = board
459 .propose("Test hypothesis 2", prior)
460 .await
461 .unwrap();
462
463 let graph = BeliefGraph::new();
464
465 let id = store.save(&board, &graph).await;
466 let snapshot = store.get(&id).unwrap();
467
468 assert_eq!(snapshot.hypotheses.len(), 2);
469 }
470}