1pub mod propagation;
12pub mod preview;
13pub mod snapshot;
14
15use std::sync::Arc;
16use std::collections::HashMap;
17use tokio::sync::Mutex;
18
19use crate::belief::BeliefGraph;
20use crate::hypothesis::{Confidence, HypothesisBoard, HypothesisId};
21use crate::errors::Result;
22
23pub use propagation::{PropagationConfig, PropagationResult, ConfidenceChange, CascadeError};
25pub use preview::{CascadePreview, PreviewId, PreviewPage, PaginationState, CycleWarning};
26pub use snapshot::{SnapshotId, BeliefSnapshot, SnapshotStore};
27
28pub struct ImpactAnalysisEngine {
36 board: Arc<HypothesisBoard>,
37 graph: Arc<BeliefGraph>,
38 snapshots: Arc<Mutex<SnapshotStore>>,
39 propagation_config: PropagationConfig,
40 page_size: usize,
41 preview_cache: Arc<Mutex<HashMap<PreviewId, CascadePreview>>>,
42}
43
44impl ImpactAnalysisEngine {
45 pub fn new(
47 board: Arc<HypothesisBoard>,
48 graph: Arc<BeliefGraph>,
49 ) -> Self {
50 Self {
51 board,
52 graph,
53 snapshots: Arc::new(Mutex::new(SnapshotStore::new())),
54 propagation_config: PropagationConfig::default(),
55 page_size: 50,
56 preview_cache: Arc::new(Mutex::new(HashMap::new())),
57 }
58 }
59
60 pub fn with_config(
62 board: Arc<HypothesisBoard>,
63 graph: Arc<BeliefGraph>,
64 config: PropagationConfig,
65 ) -> Self {
66 Self {
67 board,
68 graph,
69 snapshots: Arc::new(Mutex::new(SnapshotStore::new())),
70 propagation_config: config,
71 page_size: 50,
72 preview_cache: Arc::new(Mutex::new(HashMap::new())),
73 }
74 }
75
76 pub fn with_page_size(mut self, page_size: usize) -> Self {
78 self.page_size = page_size;
79 self
80 }
81
82 pub async fn preview(
87 &self,
88 start_hypothesis: HypothesisId,
89 new_confidence: Confidence,
90 ) -> Result<CascadePreview> {
91 let mut snapshots = self.snapshots.lock().await;
93 let _snapshot_id = snapshots.save(&self.board, &self.graph).await;
94 drop(snapshots);
95
96 let cascade_preview = preview::create_preview(
98 start_hypothesis,
99 new_confidence,
100 &self.board,
101 &self.graph,
102 &self.propagation_config,
103 self.page_size,
104 ).await?;
105
106 let mut cache = self.preview_cache.lock().await;
108 cache.insert(cascade_preview.preview_id.clone(), cascade_preview.clone());
109 drop(cache);
110
111 Ok(cascade_preview)
112 }
113
114 pub async fn confirm(
119 &self,
120 preview_id: &PreviewId,
121 ) -> Result<PropagationResult> {
122 let cache = self.preview_cache.lock().await;
124 let preview = cache.get(preview_id)
125 .ok_or_else(|| crate::errors::ReasoningError::NotFound(
126 format!("Preview {} not found or expired", preview_id)
127 ))?;
128 let result = preview.result.clone();
129 drop(cache);
130
131 propagation::propagate_confidence(result.clone(), &self.board).await?;
133
134 let mut cache = self.preview_cache.lock().await;
136 cache.remove(preview_id);
137
138 Ok(result)
139 }
140
141 pub fn get_preview_page(
143 &self,
144 preview_id: &PreviewId,
145 page_number: usize,
146 ) -> Result<PreviewPage> {
147 let cache = self.preview_cache
148 .try_lock()
149 .map_err(|_| crate::errors::ReasoningError::InvalidState(
150 "Failed to acquire preview cache lock".to_string()
151 ))?;
152
153 let preview = cache.get(preview_id)
154 .ok_or_else(|| crate::errors::ReasoningError::NotFound(
155 format!("Preview {} not found", preview_id)
156 ))?;
157
158 Ok(preview::get_page(preview, page_number))
159 }
160
161 pub async fn impact_radius(
163 &self,
164 start: HypothesisId,
165 ) -> Result<usize> {
166 propagation::impact_radius(start, &self.graph).await
167 }
168
169 pub async fn revert(
174 &self,
175 snapshot_id: &SnapshotId,
176 ) -> Result<()> {
177 let snapshots: tokio::sync::MutexGuard<'_, SnapshotStore> = self.snapshots.lock().await;
178 snapshots.restore(snapshot_id, &self.board, &self.graph).await
179 }
180
181 pub async fn list_snapshots(&self) -> Vec<BeliefSnapshot> {
183 let snapshots: tokio::sync::MutexGuard<'_, SnapshotStore> = self.snapshots.lock().await;
184 snapshots.list_snapshots()
185 .into_iter()
186 .cloned()
187 .collect()
188 }
189
190 pub async fn get_snapshot(&self, id: &SnapshotId) -> Option<BeliefSnapshot> {
192 let snapshots: tokio::sync::MutexGuard<'_, SnapshotStore> = self.snapshots.lock().await;
193 snapshots.get(id).cloned()
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::hypothesis::confidence::Confidence;
201
202 #[tokio::test]
203 async fn test_engine_creation() {
204 let board = Arc::new(HypothesisBoard::in_memory());
205 let graph = Arc::new(BeliefGraph::new());
206
207 let engine = ImpactAnalysisEngine::new(board.clone(), graph.clone());
208 assert_eq!(engine.page_size, 50);
209 }
210
211 #[tokio::test]
212 async fn test_engine_with_custom_config() {
213 let board = Arc::new(HypothesisBoard::in_memory());
214 let graph = Arc::new(BeliefGraph::new());
215
216 let config = PropagationConfig {
217 decay_factor: 0.9,
218 min_confidence: 0.2,
219 max_cascade_size: 5000,
220 };
221
222 let engine = ImpactAnalysisEngine::with_config(
223 board,
224 graph,
225 config,
226 );
227
228 assert_eq!(engine.propagation_config.decay_factor, 0.9);
229 assert_eq!(engine.propagation_config.min_confidence, 0.2);
230 assert_eq!(engine.propagation_config.max_cascade_size, 5000);
231 }
232
233 #[tokio::test]
234 async fn test_engine_with_custom_page_size() {
235 let board = Arc::new(HypothesisBoard::in_memory());
236 let graph = Arc::new(BeliefGraph::new());
237
238 let engine = ImpactAnalysisEngine::new(board, graph)
239 .with_page_size(100);
240
241 assert_eq!(engine.page_size, 100);
242 }
243
244 #[tokio::test]
245 async fn test_preview_saves_snapshot() {
246 let board = Arc::new(HypothesisBoard::in_memory());
247 let graph = Arc::new(BeliefGraph::new());
248
249 let engine = ImpactAnalysisEngine::new(board.clone(), graph.clone());
250
251 let prior = Confidence::new(0.5).unwrap();
252 let h_id = board.propose("Test", prior).await.unwrap();
253
254 let preview = engine.preview(h_id, Confidence::new(0.8).unwrap())
255 .await
256 .unwrap();
257
258 let snapshots = engine.list_snapshots().await;
260 assert!(!snapshots.is_empty());
261
262 assert_ne!(preview.preview_id.to_string(), "");
264 }
265
266 #[tokio::test]
267 async fn test_confirm_fails_for_invalid_preview_id() {
268 let board = Arc::new(HypothesisBoard::in_memory());
269 let graph = Arc::new(BeliefGraph::new());
270
271 let engine = ImpactAnalysisEngine::new(board, graph);
272
273 let fake_id = PreviewId::new();
274 let result = engine.confirm(&fake_id).await;
275
276 assert!(result.is_err());
277 }
278
279 #[tokio::test]
280 async fn test_list_snapshots_returns_active() {
281 let board = Arc::new(HypothesisBoard::in_memory());
282 let graph = Arc::new(BeliefGraph::new());
283
284 let engine = ImpactAnalysisEngine::new(board.clone(), graph.clone());
285
286 let prior = Confidence::new(0.5).unwrap();
287 let h_id = board.propose("Test", prior).await.unwrap();
288
289 engine.preview(h_id, Confidence::new(0.8).unwrap())
291 .await
292 .unwrap();
293
294 let snapshots = engine.list_snapshots().await;
295 assert!(!snapshots.is_empty());
296 }
297
298 #[tokio::test]
299 async fn test_get_snapshot_returns_some() {
300 let board = Arc::new(HypothesisBoard::in_memory());
301 let graph = Arc::new(BeliefGraph::new());
302
303 let engine = ImpactAnalysisEngine::new(board.clone(), graph.clone());
304
305 let prior = Confidence::new(0.5).unwrap();
306 let h_id = board.propose("Test", prior).await.unwrap();
307
308 engine.preview(h_id, Confidence::new(0.8).unwrap())
310 .await
311 .unwrap();
312
313 let snapshots = engine.list_snapshots().await;
315 let snapshot_id = &snapshots[0].id;
316
317 let retrieved = engine.get_snapshot(snapshot_id).await;
318 assert!(retrieved.is_some());
319 assert_eq!(&retrieved.as_ref().unwrap().id, snapshot_id);
320 }
321
322 #[tokio::test]
323 async fn test_get_snapshot_returns_none_for_nonexistent() {
324 let board = Arc::new(HypothesisBoard::in_memory());
325 let graph = Arc::new(BeliefGraph::new());
326
327 let engine = ImpactAnalysisEngine::new(board.clone(), graph.clone());
328
329 let fake_id = SnapshotId::new();
330 let retrieved = engine.get_snapshot(&fake_id).await;
331
332 assert!(retrieved.is_none());
333 }
334
335 #[tokio::test]
336 async fn test_two_step_api_prevents_accidental_changes() {
337 let board = Arc::new(HypothesisBoard::in_memory());
338 let graph = Arc::new(BeliefGraph::new());
339
340 let engine = ImpactAnalysisEngine::new(board.clone(), graph.clone());
341
342 let prior = Confidence::new(0.5).unwrap();
343 let h_id = board.propose("Test", prior).await.unwrap();
344
345 let preview = engine.preview(h_id, Confidence::new(0.8).unwrap())
347 .await
348 .unwrap();
349
350 let hypothesis = board.get(h_id).await.unwrap().unwrap();
352 assert_eq!(hypothesis.current_confidence().get(), 0.5);
353
354 let _result = engine.confirm(&preview.preview_id).await
356 .unwrap();
357
358 }
361
362 #[tokio::test]
363 async fn test_preview_gets_cached() {
364 let board = Arc::new(HypothesisBoard::in_memory());
365 let graph = Arc::new(BeliefGraph::new());
366
367 let engine = ImpactAnalysisEngine::new(board.clone(), graph.clone());
368
369 let prior = Confidence::new(0.5).unwrap();
370 let h_id = board.propose("Test", prior).await.unwrap();
371
372 let preview = engine.preview(h_id, Confidence::new(0.8).unwrap())
373 .await
374 .unwrap();
375
376 let page = engine.get_preview_page(&preview.preview_id, 0)
378 .unwrap();
379
380 assert_eq!(page.page_number, 0);
381 }
382}