1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct Violation {
6 pub law_name: String,
7 pub law_index: usize,
8 pub first_violation: Option<f64>,
9 pub last_violation: Option<f64>,
10 pub violation_count: usize,
11 pub breaking_scale: Option<f64>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Report {
17 pub total_ticks: usize,
18 pub health: f64,
19 pub total_violations: usize,
20 pub violations: Vec<Violation>,
21}
22
23impl Report {
24 pub fn to_text(&self) -> String {
26 let mut out = String::new();
27 out.push_str("=== NoetherGuard Report ===\n");
28 out.push_str(&format!("Ticks: {}\n", self.total_ticks));
29 out.push_str(&format!("Health: {:.1}%\n", self.health * 100.0));
30 out.push_str(&format!("Violations: {}\n", self.total_violations));
31
32 if self.violations.is_empty() {
33 out.push_str("\nAll conservation laws satisfied. ✓\n");
34 } else {
35 out.push_str("\nViolations:\n");
36 for v in &self.violations {
37 out.push_str(&format!(
38 " [{}] {} — {} violations",
39 v.law_index, v.law_name, v.violation_count
40 ));
41 if let (Some(first), Some(last)) = (v.first_violation, v.last_violation) {
42 out.push_str(&format!(" (t={:.3}..{:.3})", first, last));
43 }
44 if let Some(scale) = v.breaking_scale {
45 out.push_str(&format!(" breaking_scale={:.4}", scale));
46 }
47 out.push('\n');
48 }
49 }
50 out
51 }
52
53 pub fn to_json(&self) -> String {
55 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
56 }
57}
58
59impl std::fmt::Display for Report {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 write!(f, "{}", self.to_text())
62 }
63}