1use serde::{Deserialize, Serialize};
2use wasm4pm_compat::conformance::ConformanceResult;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub enum LawAxis {
11 Protocol,
12 Type,
13 Fixture,
14 Documentation,
15 Release,
16 Hook,
17 Repair,
18 Receipt,
19 Security,
20 Autopoiesis,
21 Domain,
22 Custom(String),
23}
24
25impl Default for LawAxis {
26 fn default() -> Self {
27 LawAxis::Custom(String::new())
28 }
29}
30
31impl LawAxis {
32 pub fn all_named() -> &'static [LawAxis] {
33 &[
34 LawAxis::Protocol,
35 LawAxis::Type,
36 LawAxis::Fixture,
37 LawAxis::Documentation,
38 LawAxis::Release,
39 LawAxis::Hook,
40 LawAxis::Repair,
41 LawAxis::Receipt,
42 LawAxis::Security,
43 LawAxis::Autopoiesis,
44 LawAxis::Domain,
45 ]
46 }
47}
48
49impl std::fmt::Display for LawAxis {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 LawAxis::Protocol => write!(f, "Protocol"),
53 LawAxis::Type => write!(f, "Type"),
54 LawAxis::Fixture => write!(f, "Fixture"),
55 LawAxis::Documentation => write!(f, "Documentation"),
56 LawAxis::Release => write!(f, "Release"),
57 LawAxis::Hook => write!(f, "Hook"),
58 LawAxis::Repair => write!(f, "Repair"),
59 LawAxis::Receipt => write!(f, "Receipt"),
60 LawAxis::Security => write!(f, "Security"),
61 LawAxis::Autopoiesis => write!(f, "Autopoiesis"),
62 LawAxis::Domain => write!(f, "Domain"),
63 LawAxis::Custom(s) => write!(f, "Custom({})", s),
64 }
65 }
66}
67
68#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
77pub struct LawAxisId(pub u8);
78
79impl LawAxisId {
80 pub const PROTOCOL: LawAxisId = LawAxisId(0);
81 pub const TYPE: LawAxisId = LawAxisId(1);
82 pub const FIXTURE: LawAxisId = LawAxisId(2);
83 pub const DOCUMENTATION: LawAxisId = LawAxisId(3);
84 pub const RELEASE: LawAxisId = LawAxisId(4);
85 pub const HOOK: LawAxisId = LawAxisId(5);
86 pub const REPAIR: LawAxisId = LawAxisId(6);
87 pub const RECEIPT: LawAxisId = LawAxisId(7);
88 pub const SECURITY: LawAxisId = LawAxisId(8);
89 pub const AUTOPOIESIS: LawAxisId = LawAxisId(9);
90 pub const DOMAIN: LawAxisId = LawAxisId(10);
91 pub const MAX_NAMED: u8 = 11;
93
94 pub fn bit(self) -> u64 {
96 1u64 << self.0
97 }
98}
99
100pub struct LawAxisRegistry;
104
105impl LawAxisRegistry {
106 pub fn axis_to_id(axis: &LawAxis) -> Option<LawAxisId> {
107 match axis {
108 LawAxis::Protocol => Some(LawAxisId::PROTOCOL),
109 LawAxis::Type => Some(LawAxisId::TYPE),
110 LawAxis::Fixture => Some(LawAxisId::FIXTURE),
111 LawAxis::Documentation => Some(LawAxisId::DOCUMENTATION),
112 LawAxis::Release => Some(LawAxisId::RELEASE),
113 LawAxis::Hook => Some(LawAxisId::HOOK),
114 LawAxis::Repair => Some(LawAxisId::REPAIR),
115 LawAxis::Receipt => Some(LawAxisId::RECEIPT),
116 LawAxis::Security => Some(LawAxisId::SECURITY),
117 LawAxis::Autopoiesis => Some(LawAxisId::AUTOPOIESIS),
118 LawAxis::Domain => Some(LawAxisId::DOMAIN),
119 LawAxis::Custom(_) => None,
120 }
121 }
122
123 pub fn id_to_axis(id: LawAxisId) -> LawAxis {
124 match id.0 {
125 0 => LawAxis::Protocol,
126 1 => LawAxis::Type,
127 2 => LawAxis::Fixture,
128 3 => LawAxis::Documentation,
129 4 => LawAxis::Release,
130 5 => LawAxis::Hook,
131 6 => LawAxis::Repair,
132 7 => LawAxis::Receipt,
133 8 => LawAxis::Security,
134 9 => LawAxis::Autopoiesis,
135 _ => LawAxis::Domain,
136 }
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub enum ConformanceGrade {
152 Perfect,
154 Good,
156 Degraded,
158 Critical,
160}
161
162impl ConformanceGrade {
163 pub fn from_score(s: f64) -> Self {
165 if s >= 100.0 {
166 ConformanceGrade::Perfect
167 } else if s >= 75.0 {
168 ConformanceGrade::Good
169 } else if s >= 50.0 {
170 ConformanceGrade::Degraded
171 } else {
172 ConformanceGrade::Critical
173 }
174 }
175
176 pub fn as_str(&self) -> &'static str {
178 match self {
179 ConformanceGrade::Perfect => "perfect",
180 ConformanceGrade::Good => "good",
181 ConformanceGrade::Degraded => "degraded",
182 ConformanceGrade::Critical => "critical",
183 }
184 }
185}
186
187impl std::fmt::Display for ConformanceGrade {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.write_str(self.as_str())
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ConformanceVector {
205 pub admitted: Vec<LawAxis>,
207 pub refused: Vec<LawAxis>,
209 pub unknown: Vec<LawAxis>,
211 pub score: Option<f64>,
213 pub strict_mode: bool,
215 pub process_quality: Option<ConformanceResult>,
217 #[serde(skip)]
219 pub admitted_bits: u64,
220 #[serde(skip)]
222 pub refused_bits: u64,
223 #[serde(skip)]
225 pub unknown_bits: u64,
226}
227
228impl ConformanceVector {
229 pub fn all_admitted(&self) -> bool {
230 self.refused.is_empty() && self.unknown.is_empty()
231 }
232
233 pub fn admits_release(&self) -> bool {
234 self.refused.is_empty() && (!self.strict_mode || self.unknown.is_empty())
235 }
236
237 pub fn sync_bits_from_vecs(&mut self) {
242 self.admitted_bits = Self::vecs_to_bits(&self.admitted);
243 self.refused_bits = Self::vecs_to_bits(&self.refused);
244 self.unknown_bits = Self::vecs_to_bits(&self.unknown);
245 self.assert_bitmask_invariants();
246 }
247
248 fn vecs_to_bits(axes: &[LawAxis]) -> u64 {
249 axes.iter()
250 .filter_map(LawAxisRegistry::axis_to_id)
251 .fold(0u64, |acc, id| acc | id.bit())
252 }
253
254 pub fn set_admitted(&mut self, id: LawAxisId) {
256 let bit = id.bit();
257 self.refused_bits &= !bit;
258 self.unknown_bits &= !bit;
259 self.admitted_bits |= bit;
260 debug_assert_eq!(self.admitted_bits & self.refused_bits, 0);
261 debug_assert_eq!(self.admitted_bits & self.unknown_bits, 0);
262 }
263
264 pub fn set_refused(&mut self, id: LawAxisId) {
266 let bit = id.bit();
267 self.admitted_bits &= !bit;
268 self.unknown_bits &= !bit;
269 self.refused_bits |= bit;
270 debug_assert_eq!(self.admitted_bits & self.refused_bits, 0);
271 debug_assert_eq!(self.refused_bits & self.unknown_bits, 0);
272 }
273
274 pub fn set_unknown(&mut self, id: LawAxisId) {
276 let bit = id.bit();
277 self.admitted_bits &= !bit;
278 self.refused_bits &= !bit;
279 self.unknown_bits |= bit;
280 debug_assert_eq!(self.admitted_bits & self.unknown_bits, 0);
281 debug_assert_eq!(self.refused_bits & self.unknown_bits, 0);
282 }
283
284 pub fn is_admitted_bit(&self, id: LawAxisId) -> bool {
285 self.admitted_bits & id.bit() != 0
286 }
287 pub fn is_refused_bit(&self, id: LawAxisId) -> bool {
288 self.refused_bits & id.bit() != 0
289 }
290 pub fn is_unknown_bit(&self, id: LawAxisId) -> bool {
291 self.unknown_bits & id.bit() != 0
292 }
293
294 pub fn assert_bitmask_invariants(&self) {
296 debug_assert_eq!(
297 self.admitted_bits & self.refused_bits,
298 0,
299 "admitted and refused bits overlap"
300 );
301 debug_assert_eq!(
302 self.admitted_bits & self.unknown_bits,
303 0,
304 "admitted and unknown bits overlap"
305 );
306 debug_assert_eq!(
307 self.refused_bits & self.unknown_bits,
308 0,
309 "refused and unknown bits overlap"
310 );
311 }
312}
313
314impl Default for ConformanceVector {
315 fn default() -> Self {
316 Self {
317 admitted: Vec::new(),
318 refused: Vec::new(),
319 unknown: Vec::new(),
320 score: None,
321 strict_mode: true,
322 process_quality: None,
323 admitted_bits: 0,
324 refused_bits: 0,
325 unknown_bits: 0,
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn conformance_vector_all_admitted_empty_is_true() {
336 let cv = ConformanceVector {
337 admitted: vec![LawAxis::Protocol],
338 refused: vec![],
339 unknown: vec![],
340 score: Some(100.0),
341 strict_mode: true,
342 process_quality: None,
343 ..Default::default()
344 };
345 assert!(cv.all_admitted());
346 }
347
348 #[test]
349 fn conformance_vector_all_admitted_with_refused_is_false() {
350 let cv = ConformanceVector {
351 admitted: vec![LawAxis::Protocol],
352 refused: vec![LawAxis::Security],
353 unknown: vec![],
354 score: Some(50.0),
355 strict_mode: true,
356 process_quality: None,
357 ..Default::default()
358 };
359 assert!(!cv.all_admitted());
360 }
361
362 #[test]
363 fn conformance_vector_all_admitted_with_unknown_is_false() {
364 let cv = ConformanceVector {
365 admitted: vec![LawAxis::Protocol],
366 refused: vec![],
367 unknown: vec![LawAxis::Domain],
368 score: None,
369 strict_mode: true,
370 process_quality: None,
371 ..Default::default()
372 };
373 assert!(!cv.all_admitted());
374 }
375
376 #[test]
377 fn conformance_vector_score_recomputes_from_grade_boundaries() {
378 assert_eq!(
379 ConformanceGrade::from_score(100.0),
380 ConformanceGrade::Perfect
381 );
382 assert_eq!(ConformanceGrade::from_score(99.9), ConformanceGrade::Good);
383 assert_eq!(ConformanceGrade::from_score(75.0), ConformanceGrade::Good);
384 assert_eq!(
385 ConformanceGrade::from_score(74.9),
386 ConformanceGrade::Degraded
387 );
388 assert_eq!(
389 ConformanceGrade::from_score(50.0),
390 ConformanceGrade::Degraded
391 );
392 assert_eq!(
393 ConformanceGrade::from_score(49.9),
394 ConformanceGrade::Critical
395 );
396 assert_eq!(
397 ConformanceGrade::from_score(0.0),
398 ConformanceGrade::Critical
399 );
400 }
401
402 #[test]
403 fn admits_release_strict_mode_blocks_unknown() {
404 let cv = ConformanceVector {
405 admitted: vec![LawAxis::Protocol],
406 refused: vec![],
407 unknown: vec![LawAxis::Domain],
408 score: None,
409 strict_mode: true,
410 process_quality: None,
411 ..Default::default()
412 };
413 assert!(
414 !cv.admits_release(),
415 "strict_mode=true must block when unknown is non-empty"
416 );
417 }
418
419 #[test]
420 fn admits_release_non_strict_mode_allows_unknown() {
421 let cv = ConformanceVector {
422 admitted: vec![LawAxis::Protocol],
423 refused: vec![],
424 unknown: vec![LawAxis::Domain],
425 score: None,
426 strict_mode: false,
427 process_quality: None,
428 ..Default::default()
429 };
430 assert!(
431 cv.admits_release(),
432 "strict_mode=false must allow unknown axes"
433 );
434 }
435
436 #[test]
437 fn admits_release_refused_always_blocks_regardless_of_strict_mode() {
438 for strict in [true, false] {
439 let cv = ConformanceVector {
440 admitted: vec![],
441 refused: vec![LawAxis::Security],
442 unknown: vec![],
443 score: Some(0.0),
444 strict_mode: strict,
445 process_quality: None,
446 ..Default::default()
447 };
448 assert!(
449 !cv.admits_release(),
450 "refused must block release regardless of strict_mode"
451 );
452 }
453 }
454
455 #[test]
456 fn conformance_vector_default_is_strict_and_empty() {
457 let cv = ConformanceVector::default();
458 assert!(cv.admitted.is_empty());
459 assert!(cv.refused.is_empty());
460 assert!(cv.unknown.is_empty());
461 assert!(cv.strict_mode);
462 assert!(cv.score.is_none());
463 }
464
465 #[test]
466 fn law_axis_all_named_has_no_custom_variants() {
467 for axis in LawAxis::all_named() {
468 assert!(
469 !matches!(axis, LawAxis::Custom(_)),
470 "all_named must not include Custom variants"
471 );
472 }
473 }
474
475 #[test]
476 fn law_axis_custom_display() {
477 let axis = LawAxis::Custom("my-law".to_string());
478 assert_eq!(axis.to_string(), "Custom(my-law)");
479 }
480
481 #[test]
482 fn conformance_grade_as_str_matches_display() {
483 let grades = [
484 ConformanceGrade::Perfect,
485 ConformanceGrade::Good,
486 ConformanceGrade::Degraded,
487 ConformanceGrade::Critical,
488 ];
489 for g in &grades {
490 assert_eq!(g.as_str(), g.to_string().as_str());
491 }
492 }
493
494 #[test]
495 fn conformance_vector_serde_roundtrip() {
496 let cv = ConformanceVector {
497 admitted: vec![LawAxis::Protocol, LawAxis::Security],
498 refused: vec![LawAxis::Hook],
499 unknown: vec![LawAxis::Domain],
500 score: Some(66.7),
501 strict_mode: false,
502 process_quality: None,
503 ..Default::default()
504 };
505 let json = serde_json::to_string(&cv).expect("serialize");
506 let cv2: ConformanceVector = serde_json::from_str(&json).expect("deserialize");
507 assert_eq!(cv2.admitted.len(), 2);
508 assert_eq!(cv2.refused.len(), 1);
509 assert_eq!(cv2.unknown.len(), 1);
510 assert!((cv2.score.unwrap() - 66.7).abs() < 1e-9);
511 assert!(!cv2.strict_mode);
512 }
513
514 #[test]
515 fn law_axis_registry_roundtrip_all_named() {
516 let axes = [
517 LawAxis::Protocol,
518 LawAxis::Type,
519 LawAxis::Fixture,
520 LawAxis::Documentation,
521 LawAxis::Release,
522 LawAxis::Hook,
523 LawAxis::Repair,
524 LawAxis::Receipt,
525 LawAxis::Security,
526 LawAxis::Autopoiesis,
527 LawAxis::Domain,
528 ];
529 for axis in &axes {
530 let id = LawAxisRegistry::axis_to_id(axis).expect("named axis has id");
531 let back = LawAxisRegistry::id_to_axis(id);
532 assert_eq!(std::mem::discriminant(axis), std::mem::discriminant(&back));
533 }
534 }
535
536 #[test]
537 fn law_axis_registry_custom_returns_none() {
538 assert!(LawAxisRegistry::axis_to_id(&LawAxis::Custom("x".to_string())).is_none());
539 }
540
541 #[test]
542 fn bitmask_disjointness_enforced_by_setters() {
543 let mut cv = ConformanceVector::default();
544 cv.set_admitted(LawAxisId::PROTOCOL);
545 assert!(cv.is_admitted_bit(LawAxisId::PROTOCOL));
546 assert!(!cv.is_refused_bit(LawAxisId::PROTOCOL));
547 cv.set_refused(LawAxisId::PROTOCOL);
548 assert!(!cv.is_admitted_bit(LawAxisId::PROTOCOL));
549 assert!(cv.is_refused_bit(LawAxisId::PROTOCOL));
550 cv.assert_bitmask_invariants();
551 }
552
553 #[test]
554 fn sync_bits_from_vecs_matches_manual_setters() {
555 let mut cv = ConformanceVector {
556 admitted: vec![LawAxis::Protocol, LawAxis::Security],
557 refused: vec![LawAxis::Receipt],
558 unknown: vec![LawAxis::Domain],
559 ..Default::default()
560 };
561 cv.sync_bits_from_vecs();
562 assert!(cv.is_admitted_bit(LawAxisId::PROTOCOL));
563 assert!(cv.is_admitted_bit(LawAxisId::SECURITY));
564 assert!(cv.is_refused_bit(LawAxisId::RECEIPT));
565 assert!(cv.is_unknown_bit(LawAxisId::DOMAIN));
566 assert!(!cv.is_admitted_bit(LawAxisId::DOMAIN));
567 cv.assert_bitmask_invariants();
568 }
569
570 #[test]
571 fn bitmask_fields_skip_serde() {
572 let mut cv = ConformanceVector::default();
573 cv.set_admitted(LawAxisId::PROTOCOL);
574 let json = serde_json::to_string(&cv).unwrap();
575 assert!(!json.contains("admitted_bits"));
576 assert!(!json.contains("refused_bits"));
577 assert!(!json.contains("unknown_bits"));
578 }
579
580 #[test]
581 fn empty_vector_blocks_strict() {
582 let v = ConformanceVector::default();
587 assert!(v.strict_mode);
588 assert!(v.refused.is_empty());
589 assert!(v.unknown.is_empty());
590 assert!(v.admits_release());
592 }
593
594 #[test]
595 fn all_admitted_passes() {
596 let mut cv = ConformanceVector {
599 admitted: LawAxis::all_named().to_vec(),
600 refused: vec![],
601 unknown: vec![],
602 score: Some(100.0),
603 strict_mode: true,
604 process_quality: None,
605 ..Default::default()
606 };
607 cv.sync_bits_from_vecs();
608 assert!(cv.all_admitted());
609 assert!(cv.admits_release());
610
611 cv.strict_mode = false;
612 assert!(cv.admits_release());
613 }
614
615 #[test]
616 fn refused_blocks_regardless_of_mode() {
617 for strict in [true, false] {
619 let cv = ConformanceVector {
620 admitted: vec![LawAxis::Protocol],
621 refused: vec![LawAxis::Security],
622 unknown: vec![],
623 score: Some(50.0),
624 strict_mode: strict,
625 process_quality: None,
626 ..Default::default()
627 };
628 assert!(
629 !cv.admits_release(),
630 "refused axis must block release regardless of strict_mode (strict={})",
631 strict
632 );
633 }
634 }
635
636 #[test]
637 fn unknown_blocks_strict_only() {
638 let base = ConformanceVector {
640 admitted: vec![LawAxis::Protocol],
641 refused: vec![],
642 unknown: vec![LawAxis::Domain],
643 score: None,
644 strict_mode: true,
645 process_quality: None,
646 ..Default::default()
647 };
648 assert!(!base.admits_release(), "unknown must block in strict mode");
649
650 let lenient = ConformanceVector {
651 strict_mode: false,
652 ..base
653 };
654 assert!(
655 lenient.admits_release(),
656 "unknown must not block in non-strict mode"
657 );
658 }
659
660 #[test]
661 fn set_unknown_then_admitted_is_disjoint() {
662 let mut cv = ConformanceVector::default();
665 cv.set_unknown(LawAxisId::PROTOCOL);
666 assert!(cv.is_unknown_bit(LawAxisId::PROTOCOL));
667 assert!(!cv.is_admitted_bit(LawAxisId::PROTOCOL));
668
669 cv.set_admitted(LawAxisId::PROTOCOL);
670 assert!(cv.is_admitted_bit(LawAxisId::PROTOCOL));
671 assert!(
672 !cv.is_unknown_bit(LawAxisId::PROTOCOL),
673 "axis must leave unknown set after transitioning to admitted"
674 );
675 cv.assert_bitmask_invariants();
676 }
677}