1use 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#[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#[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#[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#[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#[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
101pub 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 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 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
130pub fn get_page(
132 preview: &CascadePreview,
133 page_number: usize,
134) -> PreviewPage {
135 if page_number >= preview.pagination.total_pages {
137 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
160pub fn list_cycle_warnings(preview: &CascadePreview) -> Vec<CycleWarning> {
162 if !preview.result.cycles_detected {
163 return vec![];
164 }
165
166 let mut warnings = Vec::new();
169
170 let mut cycle_members: std::collections::HashSet<HypothesisId> = std::collections::HashSet::new();
172
173 for change in &preview.result.changes {
174 let mut seen = std::collections::HashSet::new();
176 for id in &change.propagation_path {
177 if !seen.insert(*id) {
178 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 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); 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); let mut state2 = state;
252 state2.current_page = 2;
253 assert_eq!(state2.offset(), 40); }
255
256 #[test]
257 fn test_pagination_has_next() {
258 let mut state = PaginationState::new(100, 20);
259 assert!(state.has_next()); state.current_page = 4;
262 assert!(!state.has_next()); }
264
265 #[test]
266 fn test_pagination_has_prev() {
267 let mut state = PaginationState::new(100, 20);
268 assert!(!state.has_prev()); state.current_page = 1;
271 assert!(state.has_prev()); }
273
274 #[tokio::test]
275 async fn test_create_preview() {
276 let board = HypothesisBoard::in_memory();
277 let graph = BeliefGraph::new();
278
279 let h_id = board.propose("Test", Confidence::new(0.5).unwrap()).await.unwrap();
281
282 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); 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}