1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub enum LawAxis {
10 Protocol,
11 Type,
12 Fixture,
13 Documentation,
14 Release,
15 Hook,
16 Repair,
17 Receipt,
18 Security,
19 Autopoiesis,
20 Domain,
21 Custom(String),
22}
23
24impl Default for LawAxis {
25 fn default() -> Self {
26 LawAxis::Custom(String::new())
27 }
28}
29
30impl LawAxis {
31 pub fn all_named() -> &'static [LawAxis] {
32 &[
33 LawAxis::Protocol,
34 LawAxis::Type,
35 LawAxis::Fixture,
36 LawAxis::Documentation,
37 LawAxis::Release,
38 LawAxis::Hook,
39 LawAxis::Repair,
40 LawAxis::Receipt,
41 LawAxis::Security,
42 LawAxis::Autopoiesis,
43 LawAxis::Domain,
44 ]
45 }
46}
47
48impl std::fmt::Display for LawAxis {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 LawAxis::Protocol => write!(f, "Protocol"),
52 LawAxis::Type => write!(f, "Type"),
53 LawAxis::Fixture => write!(f, "Fixture"),
54 LawAxis::Documentation => write!(f, "Documentation"),
55 LawAxis::Release => write!(f, "Release"),
56 LawAxis::Hook => write!(f, "Hook"),
57 LawAxis::Repair => write!(f, "Repair"),
58 LawAxis::Receipt => write!(f, "Receipt"),
59 LawAxis::Security => write!(f, "Security"),
60 LawAxis::Autopoiesis => write!(f, "Autopoiesis"),
61 LawAxis::Domain => write!(f, "Domain"),
62 LawAxis::Custom(s) => write!(f, "Custom({})", s),
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub enum ConformanceGrade {
79 Perfect,
81 Good,
83 Degraded,
85 Critical,
87}
88
89impl ConformanceGrade {
90 pub fn from_score(s: f64) -> Self {
92 if s >= 100.0 {
93 ConformanceGrade::Perfect
94 } else if s >= 75.0 {
95 ConformanceGrade::Good
96 } else if s >= 50.0 {
97 ConformanceGrade::Degraded
98 } else {
99 ConformanceGrade::Critical
100 }
101 }
102
103 pub fn as_str(&self) -> &'static str {
105 match self {
106 ConformanceGrade::Perfect => "perfect",
107 ConformanceGrade::Good => "good",
108 ConformanceGrade::Degraded => "degraded",
109 ConformanceGrade::Critical => "critical",
110 }
111 }
112}
113
114impl std::fmt::Display for ConformanceGrade {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.write_str(self.as_str())
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ConformanceVector {
126 pub admitted: Vec<LawAxis>,
128 pub refused: Vec<LawAxis>,
130 pub unknown: Vec<LawAxis>,
132 pub score: Option<f64>,
134 pub strict_mode: bool,
136}
137
138impl ConformanceVector {
139 pub fn all_admitted(&self) -> bool {
140 self.refused.is_empty() && self.unknown.is_empty()
141 }
142
143 pub fn admits_release(&self) -> bool {
144 self.refused.is_empty() && (!self.strict_mode || self.unknown.is_empty())
145 }
146}
147
148impl Default for ConformanceVector {
149 fn default() -> Self {
150 Self {
151 admitted: Vec::new(),
152 refused: Vec::new(),
153 unknown: Vec::new(),
154 score: None,
155 strict_mode: true,
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn conformance_vector_all_admitted_empty_is_true() {
166 let cv = ConformanceVector {
167 admitted: vec![LawAxis::Protocol],
168 refused: vec![],
169 unknown: vec![],
170 score: Some(100.0),
171 strict_mode: true,
172 };
173 assert!(cv.all_admitted());
174 }
175
176 #[test]
177 fn conformance_vector_all_admitted_with_refused_is_false() {
178 let cv = ConformanceVector {
179 admitted: vec![LawAxis::Protocol],
180 refused: vec![LawAxis::Security],
181 unknown: vec![],
182 score: Some(50.0),
183 strict_mode: true,
184 };
185 assert!(!cv.all_admitted());
186 }
187
188 #[test]
189 fn conformance_vector_all_admitted_with_unknown_is_false() {
190 let cv = ConformanceVector {
191 admitted: vec![LawAxis::Protocol],
192 refused: vec![],
193 unknown: vec![LawAxis::Domain],
194 score: None,
195 strict_mode: true,
196 };
197 assert!(!cv.all_admitted());
198 }
199
200 #[test]
201 fn conformance_vector_score_recomputes_from_grade_boundaries() {
202 assert_eq!(
203 ConformanceGrade::from_score(100.0),
204 ConformanceGrade::Perfect
205 );
206 assert_eq!(ConformanceGrade::from_score(99.9), ConformanceGrade::Good);
207 assert_eq!(ConformanceGrade::from_score(75.0), ConformanceGrade::Good);
208 assert_eq!(
209 ConformanceGrade::from_score(74.9),
210 ConformanceGrade::Degraded
211 );
212 assert_eq!(
213 ConformanceGrade::from_score(50.0),
214 ConformanceGrade::Degraded
215 );
216 assert_eq!(
217 ConformanceGrade::from_score(49.9),
218 ConformanceGrade::Critical
219 );
220 assert_eq!(
221 ConformanceGrade::from_score(0.0),
222 ConformanceGrade::Critical
223 );
224 }
225
226 #[test]
227 fn admits_release_strict_mode_blocks_unknown() {
228 let cv = ConformanceVector {
229 admitted: vec![LawAxis::Protocol],
230 refused: vec![],
231 unknown: vec![LawAxis::Domain],
232 score: None,
233 strict_mode: true,
234 };
235 assert!(
236 !cv.admits_release(),
237 "strict_mode=true must block when unknown is non-empty"
238 );
239 }
240
241 #[test]
242 fn admits_release_non_strict_mode_allows_unknown() {
243 let cv = ConformanceVector {
244 admitted: vec![LawAxis::Protocol],
245 refused: vec![],
246 unknown: vec![LawAxis::Domain],
247 score: None,
248 strict_mode: false,
249 };
250 assert!(
251 cv.admits_release(),
252 "strict_mode=false must allow unknown axes"
253 );
254 }
255
256 #[test]
257 fn admits_release_refused_always_blocks_regardless_of_strict_mode() {
258 for strict in [true, false] {
259 let cv = ConformanceVector {
260 admitted: vec![],
261 refused: vec![LawAxis::Security],
262 unknown: vec![],
263 score: Some(0.0),
264 strict_mode: strict,
265 };
266 assert!(
267 !cv.admits_release(),
268 "refused must block release regardless of strict_mode"
269 );
270 }
271 }
272
273 #[test]
274 fn conformance_vector_default_is_strict_and_empty() {
275 let cv = ConformanceVector::default();
276 assert!(cv.admitted.is_empty());
277 assert!(cv.refused.is_empty());
278 assert!(cv.unknown.is_empty());
279 assert!(cv.strict_mode);
280 assert!(cv.score.is_none());
281 }
282
283 #[test]
284 fn law_axis_all_named_has_no_custom_variants() {
285 for axis in LawAxis::all_named() {
286 assert!(
287 !matches!(axis, LawAxis::Custom(_)),
288 "all_named must not include Custom variants"
289 );
290 }
291 }
292
293 #[test]
294 fn law_axis_custom_display() {
295 let axis = LawAxis::Custom("my-law".to_string());
296 assert_eq!(axis.to_string(), "Custom(my-law)");
297 }
298
299 #[test]
300 fn conformance_grade_as_str_matches_display() {
301 let grades = [
302 ConformanceGrade::Perfect,
303 ConformanceGrade::Good,
304 ConformanceGrade::Degraded,
305 ConformanceGrade::Critical,
306 ];
307 for g in &grades {
308 assert_eq!(g.as_str(), g.to_string().as_str());
309 }
310 }
311
312 #[test]
313 fn conformance_vector_serde_roundtrip() {
314 let cv = ConformanceVector {
315 admitted: vec![LawAxis::Protocol, LawAxis::Security],
316 refused: vec![LawAxis::Hook],
317 unknown: vec![LawAxis::Domain],
318 score: Some(66.7),
319 strict_mode: false,
320 };
321 let json = serde_json::to_string(&cv).expect("serialize");
322 let cv2: ConformanceVector = serde_json::from_str(&json).expect("deserialize");
323 assert_eq!(cv2.admitted.len(), 2);
324 assert_eq!(cv2.refused.len(), 1);
325 assert_eq!(cv2.unknown.len(), 1);
326 assert!((cv2.score.unwrap() - 66.7).abs() < 1e-9);
327 assert!(!cv2.strict_mode);
328 }
329}