1#[derive(Clone, Debug, PartialEq, Eq)]
2pub enum ReviewClass {
3 GuardedAndWitnessed,
4 GuardedUnwitnessed,
5 ContractMissing,
6 GuardMissing,
7 ReachableUnwitnessed,
8 UnsafeUnreached,
9 WitnessMismatch,
10 RequiresLoom,
11 RequiresSanitizer,
12 RequiresKaniOrCrux,
13 MiriUnsupported,
14 StaticUnknown,
15 BaselineKnown,
16 Suppressed,
17}
18
19impl ReviewClass {
20 pub fn as_str(&self) -> &'static str {
21 match self {
22 Self::GuardedAndWitnessed => "guarded_and_witnessed",
23 Self::GuardedUnwitnessed => "guarded_unwitnessed",
24 Self::ContractMissing => "contract_missing",
25 Self::GuardMissing => "guard_missing",
26 Self::ReachableUnwitnessed => "reachable_unwitnessed",
27 Self::UnsafeUnreached => "unsafe_unreached",
28 Self::WitnessMismatch => "witness_mismatch",
29 Self::RequiresLoom => "requires_loom",
30 Self::RequiresSanitizer => "requires_sanitizer",
31 Self::RequiresKaniOrCrux => "requires_kani_or_crux",
32 Self::MiriUnsupported => "miri_unsupported",
33 Self::StaticUnknown => "static_unknown",
34 Self::BaselineKnown => "baseline_known",
35 Self::Suppressed => "suppressed",
36 }
37 }
38
39 pub fn is_actionable(&self) -> bool {
40 matches!(
41 self,
42 Self::GuardedUnwitnessed
43 | Self::ContractMissing
44 | Self::GuardMissing
45 | Self::ReachableUnwitnessed
46 | Self::UnsafeUnreached
47 | Self::WitnessMismatch
48 | Self::RequiresLoom
49 | Self::RequiresSanitizer
50 | Self::RequiresKaniOrCrux
51 | Self::MiriUnsupported
52 | Self::StaticUnknown
53 )
54 }
55
56 pub fn sarif_level(&self) -> &'static str {
66 match self {
67 Self::GuardedAndWitnessed | Self::BaselineKnown | Self::Suppressed => "none",
69 Self::GuardedUnwitnessed | Self::UnsafeUnreached | Self::StaticUnknown => "note",
71 Self::ContractMissing
73 | Self::GuardMissing
74 | Self::ReachableUnwitnessed
75 | Self::WitnessMismatch
76 | Self::RequiresLoom
77 | Self::RequiresSanitizer
78 | Self::RequiresKaniOrCrux
79 | Self::MiriUnsupported => "warning",
80 }
81 }
82
83 pub fn lsp_severity(&self) -> usize {
98 match self.sarif_level() {
99 "warning" => 2,
100 "note" => 3,
101 _ => 4,
103 }
104 }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub enum ProofPath {
109 ObservableRedGreen,
110 MutationMiriModel,
111 SourceRouteOnly,
112 HelperGated,
113 HumanReviewOnly,
114}
115
116impl ProofPath {
117 pub fn as_str(&self) -> &'static str {
118 match self {
119 Self::ObservableRedGreen => "observable_red_green",
120 Self::MutationMiriModel => "mutation_miri_model",
121 Self::SourceRouteOnly => "source_route_only",
122 Self::HelperGated => "helper_gated",
123 Self::HumanReviewOnly => "human_review_only",
124 }
125 }
126}
127
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum Priority {
130 High,
131 Medium,
132 Low,
133}
134
135impl Priority {
136 pub fn as_str(&self) -> &'static str {
137 match self {
138 Self::High => "high",
139 Self::Medium => "medium",
140 Self::Low => "low",
141 }
142 }
143}
144
145#[derive(Clone, Debug, PartialEq, Eq)]
146pub enum Confidence {
147 High,
148 Medium,
149 Low,
150 Unknown,
151}
152
153impl Confidence {
154 pub fn as_str(&self) -> &'static str {
155 match self {
156 Self::High => "high",
157 Self::Medium => "medium",
158 Self::Low => "low",
159 Self::Unknown => "unknown",
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn review_class_strings_cover_every_variant() {
170 let cases = [
171 (ReviewClass::GuardedAndWitnessed, "guarded_and_witnessed"),
172 (ReviewClass::GuardedUnwitnessed, "guarded_unwitnessed"),
173 (ReviewClass::ContractMissing, "contract_missing"),
174 (ReviewClass::GuardMissing, "guard_missing"),
175 (ReviewClass::ReachableUnwitnessed, "reachable_unwitnessed"),
176 (ReviewClass::UnsafeUnreached, "unsafe_unreached"),
177 (ReviewClass::WitnessMismatch, "witness_mismatch"),
178 (ReviewClass::RequiresLoom, "requires_loom"),
179 (ReviewClass::RequiresSanitizer, "requires_sanitizer"),
180 (ReviewClass::RequiresKaniOrCrux, "requires_kani_or_crux"),
181 (ReviewClass::MiriUnsupported, "miri_unsupported"),
182 (ReviewClass::StaticUnknown, "static_unknown"),
183 (ReviewClass::BaselineKnown, "baseline_known"),
184 (ReviewClass::Suppressed, "suppressed"),
185 ];
186
187 for (class, expected) in cases {
188 assert_eq!(class.as_str(), expected);
189 }
190 }
191
192 #[test]
193 fn review_class_actionability_keeps_closed_outcomes_non_actionable() {
194 let actionable = [
201 ReviewClass::GuardedUnwitnessed,
202 ReviewClass::ContractMissing,
203 ReviewClass::GuardMissing,
204 ReviewClass::ReachableUnwitnessed,
205 ReviewClass::UnsafeUnreached,
206 ReviewClass::WitnessMismatch,
207 ReviewClass::RequiresLoom,
208 ReviewClass::RequiresSanitizer,
209 ReviewClass::RequiresKaniOrCrux,
210 ReviewClass::MiriUnsupported,
211 ReviewClass::StaticUnknown,
212 ];
213 let non_actionable = [
214 ReviewClass::GuardedAndWitnessed,
215 ReviewClass::BaselineKnown,
216 ReviewClass::Suppressed,
217 ];
218
219 for class in actionable {
220 assert!(
221 class.is_actionable(),
222 "{} should be actionable",
223 class.as_str()
224 );
225 }
226 for class in non_actionable {
227 assert!(
228 !class.is_actionable(),
229 "{} should not be actionable",
230 class.as_str()
231 );
232 }
233 }
234
235 #[test]
236 fn priority_and_confidence_strings_cover_every_variant() {
237 assert_eq!(Priority::High.as_str(), "high");
238 assert_eq!(Priority::Medium.as_str(), "medium");
239 assert_eq!(Priority::Low.as_str(), "low");
240
241 assert_eq!(Confidence::High.as_str(), "high");
242 assert_eq!(Confidence::Medium.as_str(), "medium");
243 assert_eq!(Confidence::Low.as_str(), "low");
244 assert_eq!(Confidence::Unknown.as_str(), "unknown");
245 }
246
247 #[test]
248 fn proof_path_strings_cover_every_variant() {
249 let cases = [
250 (ProofPath::ObservableRedGreen, "observable_red_green"),
251 (ProofPath::MutationMiriModel, "mutation_miri_model"),
252 (ProofPath::SourceRouteOnly, "source_route_only"),
253 (ProofPath::HelperGated, "helper_gated"),
254 (ProofPath::HumanReviewOnly, "human_review_only"),
255 ];
256
257 for (path, expected) in cases {
258 assert_eq!(path.as_str(), expected);
259 }
260 }
261
262 #[test]
271 fn severity_table_is_pinned_for_every_class() {
272 let cases: &[(ReviewClass, &str, usize)] = &[
275 (ReviewClass::GuardedAndWitnessed, "none", 4),
277 (ReviewClass::BaselineKnown, "none", 4),
278 (ReviewClass::Suppressed, "none", 4),
279 (ReviewClass::GuardedUnwitnessed, "note", 3),
281 (ReviewClass::UnsafeUnreached, "note", 3),
282 (ReviewClass::StaticUnknown, "note", 3),
283 (ReviewClass::ContractMissing, "warning", 2),
285 (ReviewClass::GuardMissing, "warning", 2),
286 (ReviewClass::ReachableUnwitnessed, "warning", 2),
287 (ReviewClass::WitnessMismatch, "warning", 2),
288 (ReviewClass::RequiresLoom, "warning", 2),
289 (ReviewClass::RequiresSanitizer, "warning", 2),
290 (ReviewClass::RequiresKaniOrCrux, "warning", 2),
291 (ReviewClass::MiriUnsupported, "warning", 2),
292 ];
293
294 for (class, expected_sarif, expected_lsp) in cases {
295 assert_eq!(
296 class.sarif_level(),
297 *expected_sarif,
298 "sarif_level mismatch for {}",
299 class.as_str()
300 );
301 assert_eq!(
302 class.lsp_severity(),
303 *expected_lsp,
304 "lsp_severity mismatch for {}",
305 class.as_str()
306 );
307 }
308 }
309
310 #[test]
319 fn sarif_level_and_lsp_severity_are_consistent_for_every_class() {
320 let all_classes = [
321 ReviewClass::GuardedAndWitnessed,
322 ReviewClass::GuardedUnwitnessed,
323 ReviewClass::ContractMissing,
324 ReviewClass::GuardMissing,
325 ReviewClass::ReachableUnwitnessed,
326 ReviewClass::UnsafeUnreached,
327 ReviewClass::WitnessMismatch,
328 ReviewClass::RequiresLoom,
329 ReviewClass::RequiresSanitizer,
330 ReviewClass::RequiresKaniOrCrux,
331 ReviewClass::MiriUnsupported,
332 ReviewClass::StaticUnknown,
333 ReviewClass::BaselineKnown,
334 ReviewClass::Suppressed,
335 ];
336
337 for class in &all_classes {
338 let sarif = class.sarif_level();
339 let lsp = class.lsp_severity();
340 assert!(
342 lsp >= 2,
343 "class {} produced lsp_severity {lsp} — unsafe-review must never emit Error (1)",
344 class.as_str()
345 );
346 let expected_lsp = match sarif {
348 "warning" => 2,
349 "note" => 3,
350 _ => 4,
351 };
352 assert_eq!(
353 lsp,
354 expected_lsp,
355 "class {} has sarif_level={sarif} but lsp_severity={lsp} (expected {expected_lsp})",
356 class.as_str()
357 );
358 }
359 }
360}