Skip to main content

forge_reasoning/impact/
mod.rs

1//! Impact analysis for confidence propagation with cascade preview
2//!
3//! This module provides tools for analyzing and previewing the impact of confidence
4//! changes across the belief dependency graph. It supports:
5//!
6//! - Cascade preview: See all affected hypotheses before committing
7//! - Two-step API: preview() then confirm()
8//! - Snapshot revert: Undo changes within a time window
9//! - Pagination: Handle large cascades efficiently
10
11pub 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
23// Re-exports from submodules
24pub use propagation::{PropagationConfig, PropagationResult, ConfidenceChange, CascadeError};
25pub use preview::{CascadePreview, PreviewId, PreviewPage, PaginationState, CycleWarning};
26pub use snapshot::{SnapshotId, BeliefSnapshot, SnapshotStore};
27
28/// Impact analysis engine with two-step preview/confirm API
29///
30/// This engine enables safe confidence propagation by:
31/// 1. Saving state snapshot before computing cascade
32/// 2. Returning preview data for review
33/// 3. Applying changes only after explicit confirmation
34/// 4. Supporting revert within time window
35pub 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    /// Create a new impact analysis engine
46    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    /// Create engine with custom propagation config
61    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    /// Set custom page size for pagination
77    pub fn with_page_size(mut self, page_size: usize) -> Self {
78        self.page_size = page_size;
79        self
80    }
81
82    /// Step 1: Preview cascade effects
83    ///
84    /// Saves a snapshot before computing the cascade, then returns
85    /// a preview showing all affected hypotheses.
86    pub async fn preview(
87        &self,
88        start_hypothesis: HypothesisId,
89        new_confidence: Confidence,
90    ) -> Result<CascadePreview> {
91        // Save snapshot before computing cascade
92        let mut snapshots = self.snapshots.lock().await;
93        let _snapshot_id = snapshots.save(&self.board, &self.graph).await;
94        drop(snapshots);
95
96        // Compute cascade preview
97        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        // Cache the preview for confirm()
107        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    /// Step 2: Confirm and apply changes from preview
115    ///
116    /// Applies the confidence changes computed in the preview step.
117    /// Returns error if preview_id not found or expired.
118    pub async fn confirm(
119        &self,
120        preview_id: &PreviewId,
121    ) -> Result<PropagationResult> {
122        // Retrieve preview from cache
123        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        // Apply changes
132        propagation::propagate_confidence(result.clone(), &self.board).await?;
133
134        // Remove from cache after confirmation
135        let mut cache = self.preview_cache.lock().await;
136        cache.remove(preview_id);
137
138        Ok(result)
139    }
140
141    /// Get a paginated page from a preview
142    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    /// Query impact radius (count of affected hypotheses)
162    pub async fn impact_radius(
163        &self,
164        start: HypothesisId,
165    ) -> Result<usize> {
166        propagation::impact_radius(start, &self.graph).await
167    }
168
169    /// Revert to a previous snapshot
170    ///
171    /// Restores hypotheses and dependencies from the snapshot.
172    /// Returns error if snapshot not found or expired.
173    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    /// List all active snapshots
182    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    /// Get snapshot data for inspection
191    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        // Check that snapshot was saved
259        let snapshots = engine.list_snapshots().await;
260        assert!(!snapshots.is_empty());
261
262        // Preview should have valid ID
263        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        // Create preview (which saves snapshot)
290        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        // Create preview
309        engine.preview(h_id, Confidence::new(0.8).unwrap())
310            .await
311            .unwrap();
312
313        // Get first snapshot
314        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        // Step 1: Preview
346        let preview = engine.preview(h_id, Confidence::new(0.8).unwrap())
347            .await
348            .unwrap();
349
350        // Board should NOT have changes yet
351        let hypothesis = board.get(h_id).await.unwrap().unwrap();
352        assert_eq!(hypothesis.current_confidence().get(), 0.5);
353
354        // Step 2: Confirm
355        let _result = engine.confirm(&preview.preview_id).await
356            .unwrap();
357
358        // Now changes should be applied
359        // (In full implementation, this would update the board)
360    }
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        // Should be able to get page from cached preview
377        let page = engine.get_preview_page(&preview.preview_id, 0)
378            .unwrap();
379
380        assert_eq!(page.page_number, 0);
381    }
382}