1use crate::{GrammarState, ScenarioResult, CONTRACT_VERSION, CRATE_VERSION};
7
8pub fn generate_report(result: &ScenarioResult) -> String {
10 let mut report = String::with_capacity(4096);
11 push_report_header(&mut report, result);
12 push_detection_summary(&mut report, result);
13 let grammar_counts = grammar_state_counts(result);
14 push_grammar_distribution(&mut report, result, grammar_counts);
15 push_detection_point_state(&mut report, result);
16 push_residual_trajectory(&mut report, result);
17 push_non_interference_contract(&mut report);
18 push_report_metadata(&mut report);
19 report
20}
21
22fn push_report_header(report: &mut String, result: &ScenarioResult) {
23 report.push_str("╔══════════════════════════════════════════════════════════════╗\n");
24 report.push_str("║ DSFB Gray Failure Detection Report ║\n");
25 report.push_str("║ Deterministic Structural Semiotics Engine ║\n");
26 report.push_str("╚══════════════════════════════════════════════════════════════╝\n\n");
27 report.push_str(&format!("Scenario: {}\n", result.scenario_name));
28 report.push_str(&format!("Total Steps: {}\n", result.total_steps));
29 report.push_str(&format!(
30 "Injection Start: step {}\n",
31 result.injection_start
32 ));
33 report.push_str(&format!(
34 "Expected Reason Code: {:?}\n",
35 result.expected_reason_code
36 ));
37 match result.detected_reason_code {
38 Some(reason) => report.push_str(&format!("Detected Reason Code: {:?}\n\n", reason)),
39 None => report.push_str("Detected Reason Code: None\n\n"),
40 }
41}
42
43fn push_detection_summary(report: &mut String, result: &ScenarioResult) {
44 report.push_str("── Detection Summary ──────────────────────────────────────────\n\n");
45 if result.detected() {
46 push_detected_summary(report, result);
47 } else {
48 push_undetected_summary(report);
49 }
50 report.push_str(&format!(
51 "\n Total Boundary Steps: {}\n",
52 result.total_boundary_steps
53 ));
54 report.push_str(&format!(
55 " Total Violation Steps: {}\n",
56 result.total_violation_steps
57 ));
58}
59
60fn push_detected_summary(report: &mut String, result: &ScenarioResult) {
61 report.push_str(" ✓ FAULT DETECTED\n");
62 if let Some(step) = result.first_anomaly_step {
63 report.push_str(&format!(" First Anomaly: step {}\n", step));
64 }
65 if let Some(step) = result.first_boundary_step {
66 report.push_str(&format!(" First Boundary: step {}\n", step));
67 }
68 if let Some(step) = result.first_violation_step {
69 report.push_str(&format!(" First Violation: step {}\n", step));
70 }
71 match result.detection_delay_from_injection() {
72 Some(delay) => {
73 report.push_str(&format!(
74 " Detection Delay: {} steps after injection\n",
75 delay
76 ));
77 }
78 None => report.push_str(" Detection Delay: pre-injection false alarm\n"),
79 }
80 let detection_lead = result
81 .detection_lead_time()
82 .map_or_else(|| "unknown".to_string(), |value| value.to_string());
83 report.push_str(&format!(
84 " Detection Lead: {detection_lead} steps before scenario end\n"
85 ));
86 report.push_str(&format!(
87 " False Alarms: {} (before injection)\n",
88 result.false_alarms_before_injection
89 ));
90}
91
92fn push_undetected_summary(report: &mut String) {
93 report.push_str(" ✗ FAULT NOT DETECTED\n");
94 report.push_str(" The DSFB observer did not transition to Boundary or Violation.\n");
95 report.push_str(" Consider: envelope too wide, persistence window too large,\n");
96 report.push_str(" or drift rate too low for the configured sensitivity.\n");
97}
98
99fn grammar_state_counts(result: &ScenarioResult) -> (usize, usize, usize) {
100 let admissible = result
101 .samples
102 .iter()
103 .filter(|sample| sample.grammar_state == GrammarState::Admissible)
104 .count();
105 let boundary = result
106 .samples
107 .iter()
108 .filter(|sample| sample.grammar_state == GrammarState::Boundary)
109 .count();
110 let violation = result
111 .samples
112 .iter()
113 .filter(|sample| sample.grammar_state == GrammarState::Violation)
114 .count();
115 (admissible, boundary, violation)
116}
117
118fn push_grammar_distribution(
119 report: &mut String,
120 result: &ScenarioResult,
121 (admissible, boundary, violation): (usize, usize, usize),
122) {
123 report.push_str("\n── Grammar State Distribution ─────────────────────────────────\n\n");
124 report.push_str(&format!(
125 " Admissible: {} ({:.1}%)\n",
126 admissible,
127 admissible as f64 / result.samples.len() as f64 * 100.0
128 ));
129 report.push_str(&format!(
130 " Boundary: {} ({:.1}%)\n",
131 boundary,
132 boundary as f64 / result.samples.len() as f64 * 100.0
133 ));
134 report.push_str(&format!(
135 " Violation: {} ({:.1}%)\n",
136 violation,
137 violation as f64 / result.samples.len() as f64 * 100.0
138 ));
139}
140
141fn push_detection_point_state(report: &mut String, result: &ScenarioResult) {
142 let Some(det_step) = result.first_anomaly_step else {
143 return;
144 };
145 let Some(sample) = result.samples.iter().find(|sample| sample.step == det_step) else {
146 return;
147 };
148 report.push_str("\n── State at Detection Point ───────────────────────────────────\n\n");
149 report.push_str(&format!(" Residual: {:.4}\n", sample.residual));
150 report.push_str(&format!(" Drift: {:.6}\n", sample.drift));
151 report.push_str(&format!(" Slew: {:.6}\n", sample.slew));
152 report.push_str(&format!(
153 " Value: {:.4} (baseline: {:.4})\n",
154 sample.value, sample.baseline
155 ));
156}
157
158fn push_residual_trajectory(report: &mut String, result: &ScenarioResult) {
159 report.push_str("\n── Residual Trajectory (ASCII) ────────────────────────────────\n\n");
160 let step_size = (result.samples.len() / 60).max(1);
161 let max_r = result
162 .samples
163 .iter()
164 .map(|sample| sample.residual.abs())
165 .fold(0.0f64, f64::max)
166 .max(0.001);
167
168 for chunk in result.samples.chunks(step_size).take(60) {
169 report.push_str(&render_residual_chunk(chunk, max_r));
170 }
171}
172
173fn render_residual_chunk(chunk: &[crate::SampleRecord], max_r: f64) -> String {
174 let avg_r: f64 = chunk.iter().map(|sample| sample.residual).sum::<f64>() / chunk.len() as f64;
175 let bar_len = ((avg_r.abs() / max_r) * 40.0) as usize;
176 let state_char = match chunk.last().map(|sample| sample.grammar_state) {
177 Some(GrammarState::Admissible) => '·',
178 Some(GrammarState::Boundary) => '▸',
179 Some(GrammarState::Violation) => '█',
180 None => ' ',
181 };
182 let bar: String = std::iter::repeat_n(state_char, bar_len).collect();
183 format!(" {:>4} │{}\n", chunk[0].step, bar)
184}
185
186fn push_non_interference_contract(report: &mut String) {
187 report.push_str("\n── Non-Interference Contract ──────────────────────────────────\n\n");
188 report.push_str(" Contract Version: 1.0\n");
189 report.push_str(" All inputs accepted as immutable references (&ResidualSample).\n");
190 report.push_str(" No mutable reference to upstream system created.\n");
191 report.push_str(" Observer removal produces zero behavioral change.\n");
192}
193
194fn push_report_metadata(report: &mut String) {
195 report.push_str("\n── Report Metadata ────────────────────────────────────────────\n\n");
196 report.push_str(&format!(" DSFB Version: {}\n", CRATE_VERSION));
197 report.push_str(&format!(" Contract Version: {}\n", CONTRACT_VERSION));
198 report.push_str(" Invariant Forge LLC — riaan@invariantforge.net\n");
199}
200
201pub fn generate_csv(result: &ScenarioResult) -> String {
203 let mut csv = String::with_capacity(result.samples.len() * 80);
204 csv.push_str("step,value,baseline,residual,drift,slew,grammar_state\n");
205 for s in &result.samples {
206 let state_str = match s.grammar_state {
207 GrammarState::Admissible => "Admissible",
208 GrammarState::Boundary => "Boundary",
209 GrammarState::Violation => "Violation",
210 };
211 csv.push_str(&format!(
212 "{},{:.6},{:.6},{:.6},{:.8},{:.8},{}\n",
213 s.step, s.value, s.baseline, s.residual, s.drift, s.slew, state_str
214 ));
215 }
216 csv
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::{
223 run_scenario, AdmissibilityEnvelope, ClockDriftScenario, ObserverConfig, WorkloadPhase,
224 };
225
226 #[test]
227 fn test_report_generation() {
228 let mut scenario = ClockDriftScenario::default_scenario();
229 let config = ObserverConfig {
230 persistence_window: 20,
231 hysteresis_count: 3,
232 default_envelope: AdmissibilityEnvelope::symmetric(
233 2.0,
234 0.1,
235 0.05,
236 WorkloadPhase::SteadyState,
237 ),
238 ..ObserverConfig::fast_response()
239 };
240 let result = run_scenario(&mut scenario, &config);
241 let report = generate_report(&result);
242 assert!(report.contains("DSFB Gray Failure Detection Report"));
243 assert!(report.contains("FAULT DETECTED"));
244 }
245
246 #[test]
247 fn test_csv_generation() {
248 let mut scenario = ClockDriftScenario::default_scenario();
249 let config = ObserverConfig {
250 persistence_window: 20,
251 hysteresis_count: 3,
252 default_envelope: AdmissibilityEnvelope::symmetric(
253 2.0,
254 0.1,
255 0.05,
256 WorkloadPhase::SteadyState,
257 ),
258 ..ObserverConfig::fast_response()
259 };
260 let result = run_scenario(&mut scenario, &config);
261 let csv = generate_csv(&result);
262 assert!(csv.starts_with("step,value,baseline,residual,drift,slew,grammar_state\n"));
263 let lines: Vec<&str> = csv.lines().collect();
264 assert_eq!(lines.len(), 201); }
266}