Skip to main content

lsp_max_protocol/
conformance.rs

1use serde::{Deserialize, Serialize};
2use wasm4pm_compat::conformance::ConformanceResult;
3
4// ---------------------------------------------------------------------------
5// LawAxis — replaces ad-hoc string law_ids
6// ---------------------------------------------------------------------------
7
8#[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// ---------------------------------------------------------------------------
69// LawAxisId — stable numeric index for named LawAxis variants
70// ---------------------------------------------------------------------------
71
72/// Stable numeric index for a named `LawAxis` variant.
73///
74/// All 11 named variants fit in bits 0–10 of a `u64`. `Custom` axes have no
75/// stable numeric identity and are excluded from bitmask arithmetic.
76#[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    /// Total number of named (non-Custom) axes.
92    pub const MAX_NAMED: u8 = 11;
93
94    /// Returns the bitmask for this axis — `1 << self.0`.
95    pub fn bit(self) -> u64 {
96        1u64 << self.0
97    }
98}
99
100/// Bidirectional mapping between `LawAxis` and `LawAxisId`.
101///
102/// `Custom` axes return `None` from `axis_to_id` — they contribute no bit.
103pub 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// ---------------------------------------------------------------------------
141// ConformanceGrade — DfLSS CTQ compiler-enforced grade levels
142// ---------------------------------------------------------------------------
143
144/// Typed grade derived from a raw conformance score.
145///
146/// DfLSS CTQ requires grade-level branching to be compiler-enforced rather
147/// than stringly typed.  Use [`ConformanceGrade::from_score`] to convert the
148/// raw `f64` produced by `LspInstance::conformance_score()`.
149#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub enum ConformanceGrade {
152    /// Score ≥ 100.0 — zero defects, fully conformant.
153    Perfect,
154    /// Score ≥ 75.0 — within acceptable operating bounds.
155    Good,
156    /// Score ≥ 50.0 — degraded; corrective action recommended.
157    Degraded,
158    /// Score < 50.0 — critical; immediate intervention required.
159    Critical,
160}
161
162impl ConformanceGrade {
163    /// Map a raw conformance score to its grade level.
164    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    /// Return the canonical string label used in JSON responses.
177    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// ---------------------------------------------------------------------------
194// ConformanceVector — doctrine-correct: Admitted/Refused/Unknown are distinct
195// ---------------------------------------------------------------------------
196
197/// Three-valued conformance state: Admitted / Refused / Unknown are distinct
198/// sets, and Unknown never collapses into either (doing so is a defect). See the
199/// runnable explanation and contract witness in
200/// `examples/conformance_vector_explained.rs`, which asserts this law and panics
201/// if it regresses. For the gate composing with receipt verification, see
202/// `examples/admission_pipeline.rs`.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ConformanceVector {
205    /// Law axes that have been admitted (evidence present and valid)
206    pub admitted: Vec<LawAxis>,
207    /// Law axes that have been explicitly refused (evidence present, violation confirmed)
208    pub refused: Vec<LawAxis>,
209    /// Law axes where admissibility cannot be determined (NEVER collapsed into admitted or refused)
210    pub unknown: Vec<LawAxis>,
211    /// Derived score: 100 * admitted / (admitted + refused + unknown), None if all unknown
212    pub score: Option<f64>,
213    /// Whether unknown axes block release actuation
214    pub strict_mode: bool,
215    /// Process quality from POWL conformance check. None until wasm4pm graduation.
216    pub process_quality: Option<ConformanceResult>,
217    /// Bitmask for admitted axes (bits 0–10 = Protocol..Domain). Not serialized.
218    #[serde(skip)]
219    pub admitted_bits: u64,
220    /// Bitmask for refused axes. Not serialized.
221    #[serde(skip)]
222    pub refused_bits: u64,
223    /// Bitmask for unknown axes. Not serialized.
224    #[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    /// Recompute all three bitmasks from the Vec fields.
238    ///
239    /// Call this after constructing from a struct literal or deserializing
240    /// from JSON, so the internal index stays consistent.
241    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    /// Mark an axis as admitted, removing it from refused and unknown sets.
255    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    /// Mark an axis as refused, removing it from admitted and unknown sets.
265    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    /// Mark an axis as unknown, removing it from admitted and refused sets.
275    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    /// Assert all three bitmasks are mutually disjoint.
295    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        // Default vector: strict_mode=true, all sets empty.
583        // No refused axes and no unknown axes → admits_release passes.
584        // Confirm that strict_mode alone (without any unknown axes) does not
585        // block release — the block only fires when unknown is non-empty.
586        let v = ConformanceVector::default();
587        assert!(v.strict_mode);
588        assert!(v.refused.is_empty());
589        assert!(v.unknown.is_empty());
590        // empty + strict: passes because nothing is refused and nothing unknown
591        assert!(v.admits_release());
592    }
593
594    #[test]
595    fn all_admitted_passes() {
596        // Build a vector where every named axis is admitted; assert release
597        // is CANDIDATE in both strict and non-strict mode.
598        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        // One refused axis must block admits_release in both strict and non-strict.
618        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        // Unknown axes block admits_release when strict_mode=true but not when false.
639        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        // Transition an axis from unknown → admitted via bitmask setters.
663        // After the transition the axis must be absent from the unknown set.
664        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}