1pub mod causal;
12pub mod cost;
13pub mod operational;
14pub mod outcome;
15pub mod policy;
16pub mod types;
17
18use serde::{Deserialize, Serialize};
19use uuid::Uuid;
20
21pub use causal::{CausalSimulationAgent, CausalSimulator, CausalSimulatorConfig};
22pub use cost::{CostSimulationAgent, CostSimulator, CostSimulatorConfig};
23pub use operational::{
24 OperationalSimulationAgent, OperationalSimulator, OperationalSimulatorConfig,
25};
26pub use outcome::{OutcomeSimulationAgent, OutcomeSimulator, OutcomeSimulatorConfig};
27pub use policy::{PolicySimulationAgent, PolicySimulator, PolicySimulatorConfig};
28pub use types::{RiskLikelihood, SimulationVerdict};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SimulationResult {
34 pub plan_id: Uuid,
35 pub runs: u32,
36 pub dimensions: Vec<DimensionResult>,
37 pub overall_confidence: f64,
38 pub recommendation: SimulationRecommendation,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct DimensionResult {
43 pub dimension: SimulationDimension,
44 pub passed: bool,
45 pub confidence: f64,
46 pub findings: Vec<String>,
47 pub samples: Vec<Sample>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum SimulationDimension {
53 Outcome,
54 Cost,
55 Policy,
56 Causal,
57 Operational,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum SimulationRecommendation {
63 Proceed,
64 ProceedWithCaution,
65 DoNotProceed,
66}
67
68#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
71pub struct Sample {
72 pub value: f64,
73 pub probability: f64,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct SimulationReport {
80 pub results: Vec<SimulationResult>,
81}
82
83#[cfg(test)]
87mod tests {
88 use super::*;
89 use proptest::prelude::*;
90
91 fn plan_id() -> Uuid {
92 Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap()
93 }
94
95 fn sample_dimension_result(dim: SimulationDimension, passed: bool) -> DimensionResult {
96 DimensionResult {
97 dimension: dim,
98 passed,
99 confidence: 0.85,
100 findings: vec!["ok".into()],
101 samples: vec![Sample {
102 value: 1.0,
103 probability: 0.9,
104 }],
105 }
106 }
107
108 #[test]
109 fn simulation_dimension_all_variants_distinct() {
110 let variants = [
111 SimulationDimension::Outcome,
112 SimulationDimension::Cost,
113 SimulationDimension::Policy,
114 SimulationDimension::Causal,
115 SimulationDimension::Operational,
116 ];
117 for (i, a) in variants.iter().enumerate() {
118 for (j, b) in variants.iter().enumerate() {
119 assert_eq!(i == j, a == b);
120 }
121 }
122 }
123
124 #[test]
125 fn recommendation_all_variants_distinct() {
126 let variants = [
127 SimulationRecommendation::Proceed,
128 SimulationRecommendation::ProceedWithCaution,
129 SimulationRecommendation::DoNotProceed,
130 ];
131 for (i, a) in variants.iter().enumerate() {
132 for (j, b) in variants.iter().enumerate() {
133 assert_eq!(i == j, a == b);
134 }
135 }
136 }
137
138 #[test]
139 fn simulation_dimension_serde_snake_case() {
140 let json = serde_json::to_string(&SimulationDimension::Outcome).unwrap();
141 assert_eq!(json, "\"outcome\"");
142 let json = serde_json::to_string(&SimulationDimension::Cost).unwrap();
143 assert_eq!(json, "\"cost\"");
144 let json = serde_json::to_string(&SimulationDimension::Operational).unwrap();
145 assert_eq!(json, "\"operational\"");
146 }
147
148 #[test]
149 fn recommendation_serde_snake_case() {
150 let json = serde_json::to_string(&SimulationRecommendation::ProceedWithCaution).unwrap();
151 assert_eq!(json, "\"proceed_with_caution\"");
152 let json = serde_json::to_string(&SimulationRecommendation::DoNotProceed).unwrap();
153 assert_eq!(json, "\"do_not_proceed\"");
154 }
155
156 #[test]
157 fn sample_serde_roundtrip() {
158 let s = Sample {
159 value: 42.5,
160 probability: 0.73,
161 };
162 let json = serde_json::to_string(&s).unwrap();
163 let back: Sample = serde_json::from_str(&json).unwrap();
164 assert!((back.value - 42.5).abs() < f64::EPSILON);
165 assert!((back.probability - 0.73).abs() < f64::EPSILON);
166 }
167
168 #[test]
169 fn dimension_result_serde_roundtrip() {
170 let dr = sample_dimension_result(SimulationDimension::Cost, true);
171 let json = serde_json::to_string(&dr).unwrap();
172 let back: DimensionResult = serde_json::from_str(&json).unwrap();
173 assert_eq!(back.dimension, SimulationDimension::Cost);
174 assert!(back.passed);
175 assert_eq!(back.findings.len(), 1);
176 assert_eq!(back.samples.len(), 1);
177 }
178
179 #[test]
180 fn dimension_result_empty_findings_and_samples() {
181 let dr = DimensionResult {
182 dimension: SimulationDimension::Policy,
183 passed: false,
184 confidence: 0.0,
185 findings: vec![],
186 samples: vec![],
187 };
188 let json = serde_json::to_string(&dr).unwrap();
189 let back: DimensionResult = serde_json::from_str(&json).unwrap();
190 assert!(back.findings.is_empty());
191 assert!(back.samples.is_empty());
192 }
193
194 #[test]
195 fn simulation_result_serde_roundtrip() {
196 let sr = SimulationResult {
197 plan_id: plan_id(),
198 runs: 1000,
199 dimensions: vec![
200 sample_dimension_result(SimulationDimension::Outcome, true),
201 sample_dimension_result(SimulationDimension::Cost, false),
202 ],
203 overall_confidence: 0.72,
204 recommendation: SimulationRecommendation::ProceedWithCaution,
205 };
206 let json = serde_json::to_string(&sr).unwrap();
207 let back: SimulationResult = serde_json::from_str(&json).unwrap();
208 assert_eq!(back.plan_id, plan_id());
209 assert_eq!(back.runs, 1000);
210 assert_eq!(back.dimensions.len(), 2);
211 assert_eq!(
212 back.recommendation,
213 SimulationRecommendation::ProceedWithCaution
214 );
215 }
216
217 #[test]
218 fn simulation_result_zero_runs() {
219 let sr = SimulationResult {
220 plan_id: plan_id(),
221 runs: 0,
222 dimensions: vec![],
223 overall_confidence: 0.0,
224 recommendation: SimulationRecommendation::DoNotProceed,
225 };
226 let json = serde_json::to_string(&sr).unwrap();
227 let back: SimulationResult = serde_json::from_str(&json).unwrap();
228 assert_eq!(back.runs, 0);
229 assert!(back.dimensions.is_empty());
230 }
231
232 #[test]
233 fn simulation_report_default_is_empty() {
234 let report = SimulationReport::default();
235 assert!(report.results.is_empty());
236 }
237
238 #[test]
239 fn simulation_report_serde_roundtrip() {
240 let report = SimulationReport {
241 results: vec![SimulationResult {
242 plan_id: plan_id(),
243 runs: 50,
244 dimensions: vec![],
245 overall_confidence: 0.5,
246 recommendation: SimulationRecommendation::Proceed,
247 }],
248 };
249 let json = serde_json::to_string(&report).unwrap();
250 let back: SimulationReport = serde_json::from_str(&json).unwrap();
251 assert_eq!(back.results.len(), 1);
252 assert_eq!(back.results[0].runs, 50);
253 }
254
255 proptest! {
256 #[test]
257 fn sample_roundtrips_reasonable_values(
258 value in -1e15f64..1e15f64,
259 probability in 0.0f64..=1.0,
260 ) {
261 let s = Sample { value, probability };
262 let json = serde_json::to_string(&s).unwrap();
263 let back: Sample = serde_json::from_str(&json).unwrap();
264 prop_assert!((back.value - value).abs() < 1e-6 * value.abs().max(1.0));
265 prop_assert!((back.probability - probability).abs() < f64::EPSILON);
266 }
267
268 #[test]
269 fn confidence_survives_roundtrip(conf in 0.0f64..=1.0) {
270 let sr = SimulationResult {
271 plan_id: plan_id(),
272 runs: 1,
273 dimensions: vec![],
274 overall_confidence: conf,
275 recommendation: SimulationRecommendation::Proceed,
276 };
277 let json = serde_json::to_string(&sr).unwrap();
278 let back: SimulationResult = serde_json::from_str(&json).unwrap();
279 prop_assert!((back.overall_confidence - conf).abs() < f64::EPSILON);
280 }
281 }
282}