organism_simulation/
types.rs1use converge_pack::FactId;
7use serde::{Deserialize, Serialize};
8
9use crate::SimulationDimension;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum RiskLikelihood {
18 VeryLikely,
19 Likely,
20 Possible,
21 Unlikely,
22 Rare,
23}
24
25impl RiskLikelihood {
26 #[must_use]
28 pub fn probability(&self) -> f64 {
29 match self {
30 Self::VeryLikely => 0.9,
31 Self::Likely => 0.7,
32 Self::Possible => 0.4,
33 Self::Unlikely => 0.15,
34 Self::Rare => 0.05,
35 }
36 }
37
38 #[must_use]
40 pub fn from_str_lossy(s: &str) -> Option<Self> {
41 match s {
42 "very_likely" | "VeryLikely" => Some(Self::VeryLikely),
43 "likely" | "Likely" => Some(Self::Likely),
44 "possible" | "Possible" => Some(Self::Possible),
45 "unlikely" | "Unlikely" => Some(Self::Unlikely),
46 "rare" | "Rare" => Some(Self::Rare),
47 _ => None,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SimulationVerdict {
58 pub strategy_id: FactId,
59 pub dimension: SimulationDimension,
60 pub passed: bool,
61 pub confidence: f64,
62 pub findings: Vec<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub recommendation: Option<SimulationRecommendation>,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum SimulationRecommendation {
71 Proceed,
72 ProceedWithCaution,
73 DoNotProceed,
74}
75
76impl SimulationVerdict {
77 pub fn to_json(&self) -> String {
78 serde_json::to_string(self).expect("SimulationVerdict is always serializable")
79 }
80
81 pub fn fact_id(&self) -> String {
83 let dim = match self.dimension {
84 SimulationDimension::Outcome => "sim",
85 SimulationDimension::Cost => "cost",
86 SimulationDimension::Policy => "policy",
87 SimulationDimension::Causal => "causal",
88 SimulationDimension::Operational => "ops",
89 };
90 let result = if self.passed { "pass" } else { "fail" };
91 format!("{dim}-{result}-{}", self.strategy_id)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn risk_likelihood_serde() {
101 let json = serde_json::to_string(&RiskLikelihood::VeryLikely).unwrap();
102 assert_eq!(json, "\"very_likely\"");
103
104 let back: RiskLikelihood = serde_json::from_str("\"unlikely\"").unwrap();
105 assert_eq!(back, RiskLikelihood::Unlikely);
106 }
107
108 #[test]
109 fn risk_likelihood_probabilities() {
110 assert!((RiskLikelihood::VeryLikely.probability() - 0.9).abs() < f64::EPSILON);
111 assert!((RiskLikelihood::Rare.probability() - 0.05).abs() < f64::EPSILON);
112 }
113
114 #[test]
115 fn risk_likelihood_from_str_lossy() {
116 assert_eq!(
117 RiskLikelihood::from_str_lossy("very_likely"),
118 Some(RiskLikelihood::VeryLikely)
119 );
120 assert_eq!(
121 RiskLikelihood::from_str_lossy("Likely"),
122 Some(RiskLikelihood::Likely)
123 );
124 assert_eq!(RiskLikelihood::from_str_lossy("unknown"), None);
125 }
126
127 #[test]
128 fn simulation_verdict_serde_roundtrip() {
129 let verdict = SimulationVerdict {
130 strategy_id: "strat-1".into(),
131 dimension: SimulationDimension::Cost,
132 passed: true,
133 confidence: 0.85,
134 findings: vec!["within budget".into()],
135 recommendation: None,
136 };
137
138 let json = verdict.to_json();
139 let back: SimulationVerdict = serde_json::from_str(&json).unwrap();
140 assert_eq!(back.dimension, SimulationDimension::Cost);
141 assert!(back.passed);
142 assert!((back.confidence - 0.85).abs() < f64::EPSILON);
143 }
144
145 #[test]
146 fn simulation_verdict_with_recommendation() {
147 let verdict = SimulationVerdict {
148 strategy_id: "s1".into(),
149 dimension: SimulationDimension::Outcome,
150 passed: false,
151 confidence: 0.3,
152 findings: vec![],
153 recommendation: Some(SimulationRecommendation::DoNotProceed),
154 };
155
156 let json = verdict.to_json();
157 assert!(json.contains("do_not_proceed"));
158 }
159
160 #[test]
161 fn simulation_verdict_fact_id() {
162 let verdict = SimulationVerdict {
163 strategy_id: "abc".into(),
164 dimension: SimulationDimension::Cost,
165 passed: true,
166 confidence: 0.9,
167 findings: vec![],
168 recommendation: None,
169 };
170 assert_eq!(verdict.fact_id(), "cost-pass-abc");
171
172 let fail_verdict = SimulationVerdict {
173 strategy_id: "xyz".into(),
174 dimension: SimulationDimension::Operational,
175 passed: false,
176 confidence: 0.2,
177 findings: vec![],
178 recommendation: Some(SimulationRecommendation::DoNotProceed),
179 };
180 assert_eq!(fail_verdict.fact_id(), "ops-fail-xyz");
181 }
182}