Skip to main content

lsp_max_protocol/
conformance.rs

1use serde::{Deserialize, Serialize};
2
3// ---------------------------------------------------------------------------
4// LawAxis — replaces ad-hoc string law_ids
5// ---------------------------------------------------------------------------
6
7#[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// ---------------------------------------------------------------------------
68// ConformanceGrade — DfLSS CTQ compiler-enforced grade levels
69// ---------------------------------------------------------------------------
70
71/// Typed grade derived from a raw conformance score.
72///
73/// DfLSS CTQ requires grade-level branching to be compiler-enforced rather
74/// than stringly typed.  Use [`ConformanceGrade::from_score`] to convert the
75/// raw `f64` produced by `LspInstance::conformance_score()`.
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub enum ConformanceGrade {
79    /// Score ≥ 100.0 — zero defects, fully conformant.
80    Perfect,
81    /// Score ≥ 75.0 — within acceptable operating bounds.
82    Good,
83    /// Score ≥ 50.0 — degraded; corrective action recommended.
84    Degraded,
85    /// Score < 50.0 — critical; immediate intervention required.
86    Critical,
87}
88
89impl ConformanceGrade {
90    /// Map a raw conformance score to its grade level.
91    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    /// Return the canonical string label used in JSON responses.
104    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// ---------------------------------------------------------------------------
121// ConformanceVector — doctrine-correct: Admitted/Refused/Unknown are distinct
122// ---------------------------------------------------------------------------
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ConformanceVector {
126    /// Law axes that have been admitted (evidence present and valid)
127    pub admitted: Vec<LawAxis>,
128    /// Law axes that have been explicitly refused (evidence present, violation confirmed)
129    pub refused: Vec<LawAxis>,
130    /// Law axes where admissibility cannot be determined (NEVER collapsed into admitted or refused)
131    pub unknown: Vec<LawAxis>,
132    /// Derived score: 100 * admitted / (admitted + refused + unknown), None if all unknown
133    pub score: Option<f64>,
134    /// Whether unknown axes block release actuation
135    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}