Skip to main content

forge_reasoning/impact/
preview.rs

1//! Cascade preview with pagination
2
3use chrono::{DateTime, Utc};
4use uuid::Uuid;
5
6use super::propagation::{ConfidenceChange, PropagationResult};
7use crate::hypothesis::{Confidence, HypothesisBoard, HypothesisId};
8use crate::belief::BeliefGraph;
9use super::propagation::PropagationConfig;
10use crate::errors::Result;
11
12/// Unique identifier for a preview
13#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14pub struct PreviewId(Uuid);
15
16impl PreviewId {
17    pub fn new() -> Self {
18        Self(Uuid::new_v4())
19    }
20}
21
22impl Default for PreviewId {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl std::fmt::Display for PreviewId {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        write!(f, "{}", self.0)
31    }
32}
33
34/// Pagination state for cascade preview
35#[derive(Clone, Debug)]
36pub struct PaginationState {
37    pub total_items: usize,
38    pub page_size: usize,
39    pub current_page: usize,
40    pub total_pages: usize,
41}
42
43impl PaginationState {
44    pub fn new(total_items: usize, page_size: usize) -> Self {
45        let total_pages = if total_items == 0 {
46            0
47        } else {
48            ((total_items - 1) / page_size) + 1
49        };
50
51        Self {
52            total_items,
53            page_size,
54            current_page: 0,
55            total_pages,
56        }
57    }
58
59    pub fn offset(&self) -> usize {
60        self.current_page * self.page_size
61    }
62
63    pub fn has_next(&self) -> bool {
64        self.current_page < self.total_pages.saturating_sub(1)
65    }
66
67    pub fn has_prev(&self) -> bool {
68        self.current_page > 0
69    }
70}
71
72/// Cascade preview result
73#[derive(Clone, Debug)]
74pub struct CascadePreview {
75    pub preview_id: PreviewId,
76    pub start_hypothesis: HypothesisId,
77    pub new_confidence: Confidence,
78    pub result: PropagationResult,
79    pub created_at: DateTime<Utc>,
80    pub pagination: PaginationState,
81}
82
83/// A page of cascade changes
84#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
85pub struct PreviewPage {
86    pub preview_id: PreviewId,
87    pub page_number: usize,
88    pub total_pages: usize,
89    pub changes: Vec<ConfidenceChange>,
90    pub has_more: bool,
91}
92
93/// Warning about cycles in the cascade
94#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
95pub struct CycleWarning {
96    pub scc_members: Vec<HypothesisId>,
97    pub avg_confidence: f64,
98    pub description: String,
99}
100
101/// Create a cascade preview
102pub async fn create_preview(
103    start: HypothesisId,
104    new_confidence: Confidence,
105    board: &HypothesisBoard,
106    graph: &BeliefGraph,
107    config: &PropagationConfig,
108    page_size: usize,
109) -> Result<CascadePreview> {
110    use crate::errors::ReasoningError;
111
112    // Compute cascade using propagation module
113    let result = super::propagation::compute_cascade(start, new_confidence, board, graph, config)
114        .await
115        .map_err(|e| ReasoningError::InvalidState(e.to_string()))?;
116
117    // Create pagination state
118    let pagination = PaginationState::new(result.changes.len(), page_size);
119
120    Ok(CascadePreview {
121        preview_id: PreviewId::new(),
122        start_hypothesis: start,
123        new_confidence,
124        result,
125        created_at: Utc::now(),
126        pagination,
127    })
128}
129
130/// Get a page from a preview
131pub fn get_page(
132    preview: &CascadePreview,
133    page_number: usize,
134) -> PreviewPage {
135    // Validate page number
136    if page_number >= preview.pagination.total_pages {
137        // Return empty page for out-of-bounds request
138        return PreviewPage {
139            preview_id: preview.preview_id.clone(),
140            page_number,
141            total_pages: preview.pagination.total_pages,
142            changes: vec![],
143            has_more: false,
144        };
145    }
146
147    let start = page_number * preview.pagination.page_size;
148    let end = (start + preview.pagination.page_size).min(preview.result.changes.len());
149    let changes = preview.result.changes[start..end].to_vec();
150
151    PreviewPage {
152        preview_id: preview.preview_id.clone(),
153        page_number,
154        total_pages: preview.pagination.total_pages,
155        changes,
156        has_more: page_number < preview.pagination.total_pages.saturating_sub(1),
157    }
158}
159
160/// List cycle warnings from preview
161pub fn list_cycle_warnings(preview: &CascadePreview) -> Vec<CycleWarning> {
162    if !preview.result.cycles_detected {
163        return vec![];
164    }
165
166    // Group changes by depth to identify potential cycles
167    // (In a full implementation, we'd use the graph's SCC detection here)
168    let mut warnings = Vec::new();
169
170    // Find cycles in the changes by looking for repeated hypothesis IDs in paths
171    let mut cycle_members: std::collections::HashSet<HypothesisId> = std::collections::HashSet::new();
172
173    for change in &preview.result.changes {
174        // Check if the hypothesis appears multiple times in its own propagation path
175        let mut seen = std::collections::HashSet::new();
176        for id in &change.propagation_path {
177            if !seen.insert(*id) {
178                // Duplicate found - this is part of a cycle
179                cycle_members.insert(*id);
180            }
181        }
182    }
183
184    if !cycle_members.is_empty() {
185        let members: Vec<_> = cycle_members.iter().cloned().collect();
186
187        // Compute average confidence for cycle members
188        let cycle_changes: Vec<_> = preview.result.changes
189            .iter()
190            .filter(|c| cycle_members.contains(&c.hypothesis_id))
191            .collect();
192
193        if !cycle_changes.is_empty() {
194            let avg_confidence: f64 = cycle_changes.iter()
195                .map(|c| c.new_confidence.get())
196                .sum::<f64>() / cycle_changes.len() as f64;
197
198            warnings.push(CycleWarning {
199                scc_members: members,
200                avg_confidence,
201                description: format!(
202                    "Cycle detected with {} members. Normalized to {:.2} confidence.",
203                    cycle_members.len(),
204                    avg_confidence
205                ),
206            });
207        }
208    }
209
210    warnings
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::belief::BeliefGraph;
217
218    #[test]
219    fn test_preview_id_new() {
220        let id = PreviewId::new();
221        assert_ne!(id.0, Uuid::nil());
222    }
223
224    #[test]
225    fn test_preview_id_default() {
226        let id = PreviewId::default();
227        assert_ne!(id.0, Uuid::nil());
228    }
229
230    #[test]
231    fn test_pagination_state_new() {
232        let state = PaginationState::new(100, 20);
233        assert_eq!(state.total_items, 100);
234        assert_eq!(state.page_size, 20);
235        assert_eq!(state.total_pages, 5); // (100 - 1) / 20 + 1 = 5
236        assert_eq!(state.current_page, 0);
237    }
238
239    #[test]
240    fn test_pagination_state_empty() {
241        let state = PaginationState::new(0, 50);
242        assert_eq!(state.total_items, 0);
243        assert_eq!(state.total_pages, 0);
244    }
245
246    #[test]
247    fn test_pagination_offset() {
248        let state = PaginationState::new(100, 20);
249        assert_eq!(state.offset(), 0); // Page 0
250
251        let mut state2 = state;
252        state2.current_page = 2;
253        assert_eq!(state2.offset(), 40); // Page 2
254    }
255
256    #[test]
257    fn test_pagination_has_next() {
258        let mut state = PaginationState::new(100, 20);
259        assert!(state.has_next()); // Page 0 of 5
260
261        state.current_page = 4;
262        assert!(!state.has_next()); // Last page
263    }
264
265    #[test]
266    fn test_pagination_has_prev() {
267        let mut state = PaginationState::new(100, 20);
268        assert!(!state.has_prev()); // Page 0
269
270        state.current_page = 1;
271        assert!(state.has_prev()); // Page 1
272    }
273
274    #[tokio::test]
275    async fn test_create_preview() {
276        let board = HypothesisBoard::in_memory();
277        let graph = BeliefGraph::new();
278
279        // Create a hypothesis
280        let h_id = board.propose("Test", Confidence::new(0.5).unwrap()).await.unwrap();
281
282        // Create preview
283        let preview = create_preview(
284            h_id,
285            Confidence::new(0.8).unwrap(),
286            &board,
287            &graph,
288            &PropagationConfig::default(),
289            50,
290        ).await.unwrap();
291
292        assert_eq!(preview.start_hypothesis, h_id);
293        assert_eq!(preview.pagination.total_items, 1);
294        assert_eq!(preview.pagination.total_pages, 1);
295    }
296
297    #[tokio::test]
298    async fn test_get_page_first() {
299        let board = HypothesisBoard::in_memory();
300        let graph = BeliefGraph::new();
301
302        let h_id = board.propose("Test", Confidence::new(0.5).unwrap()).await.unwrap();
303
304        let preview = create_preview(
305            h_id,
306            Confidence::new(0.8).unwrap(),
307            &board,
308            &graph,
309            &PropagationConfig::default(),
310            10,
311        ).await.unwrap();
312
313        let page = get_page(&preview, 0);
314        assert_eq!(page.page_number, 0);
315        assert_eq!(page.total_pages, 1);
316        assert_eq!(page.changes.len(), 1);
317        assert!(!page.has_more);
318    }
319
320    #[tokio::test]
321    async fn test_get_page_out_of_bounds() {
322        let board = HypothesisBoard::in_memory();
323        let graph = BeliefGraph::new();
324
325        let h_id = board.propose("Test", Confidence::new(0.5).unwrap()).await.unwrap();
326
327        let preview = create_preview(
328            h_id,
329            Confidence::new(0.8).unwrap(),
330            &board,
331            &graph,
332            &PropagationConfig::default(),
333            10,
334        ).await.unwrap();
335
336        let page = get_page(&preview, 99); // Out of bounds
337        assert_eq!(page.page_number, 99);
338        assert_eq!(page.changes.len(), 0);
339        assert!(!page.has_more);
340    }
341
342    #[tokio::test]
343    async fn test_list_cycle_warnings_empty() {
344        let board = HypothesisBoard::in_memory();
345        let graph = BeliefGraph::new();
346
347        let h_id = board.propose("Test", Confidence::new(0.5).unwrap()).await.unwrap();
348
349        let preview = create_preview(
350            h_id,
351            Confidence::new(0.8).unwrap(),
352            &board,
353            &graph,
354            &PropagationConfig::default(),
355            10,
356        ).await.unwrap();
357
358        let warnings = list_cycle_warnings(&preview);
359        assert_eq!(warnings.len(), 0);
360    }
361
362    #[test]
363    fn test_preview_id_display() {
364        let id = PreviewId::new();
365        let s = format!("{}", id);
366        assert!(!s.is_empty());
367    }
368}