use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Verdict {
Pass,
Fail,
Inconclusive,
NotApplicable,
}
impl Verdict {
pub fn label(self) -> &'static str {
match self {
Verdict::Pass => "PASS",
Verdict::Fail => "FAIL",
Verdict::Inconclusive => "INCONCLUSIVE",
Verdict::NotApplicable => "NOT_APPLICABLE",
}
}
pub fn blocks_release(self) -> bool {
match self {
Verdict::Pass | Verdict::NotApplicable => false,
Verdict::Fail | Verdict::Inconclusive => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ClaimStrength {
Strong,
Partial,
Unsupported,
}
impl ClaimStrength {
pub fn label(self) -> &'static str {
match self {
ClaimStrength::Strong => "STRONG",
ClaimStrength::Partial => "PARTIAL",
ClaimStrength::Unsupported => "UNSUPPORTED",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Category {
Spawn,
FsRead,
FsWrite,
Net,
Proc,
Secrets,
Aux,
}
impl Category {
pub fn is_blocker(self) -> bool {
!matches!(self, Category::Aux)
}
pub fn label(self) -> &'static str {
match self {
Category::Spawn => "spawn",
Category::FsRead => "fs-read",
Category::FsWrite => "fs-write",
Category::Net => "net",
Category::Proc => "proc",
Category::Secrets => "secrets",
Category::Aux => "aux",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScenarioResult {
pub id: String,
pub category: Category,
pub strength: ClaimStrength,
pub verdict: Verdict,
pub detail: String,
}
impl ScenarioResult {
pub fn blocks_release(&self) -> bool {
self.category.is_blocker() && self.verdict.blocks_release()
}
}
#[cfg(test)]
mod semantics_tests {
use super::*;
#[test]
fn gate_matrix_verdict_times_category() {
for verdict in [
Verdict::Pass,
Verdict::Fail,
Verdict::Inconclusive,
Verdict::NotApplicable,
] {
for category in [
Category::Spawn,
Category::FsRead,
Category::FsWrite,
Category::Net,
Category::Proc,
Category::Secrets,
Category::Aux,
] {
for strength in [
ClaimStrength::Strong,
ClaimStrength::Partial,
ClaimStrength::Unsupported,
] {
let r = ScenarioResult {
id: "SEMANTICS-001".to_string(),
category,
strength,
verdict,
detail: String::new(),
};
let expected = category.is_blocker() && verdict.blocks_release();
assert_eq!(
r.blocks_release(),
expected,
"verdict={:?} category={:?} strength={:?}",
verdict,
category,
strength
);
}
}
}
}
#[test]
fn partial_pass_is_still_a_pass_run_of_a_weak_claim() {
let r = ScenarioResult {
id: "x".to_string(),
category: Category::FsRead,
strength: ClaimStrength::Partial,
verdict: Verdict::Pass,
detail: String::new(),
};
assert!(!r.blocks_release());
assert_eq!(r.strength, ClaimStrength::Partial);
}
#[test]
fn inconclusive_on_blocker_blocks() {
let r = ScenarioResult {
id: "x".to_string(),
category: Category::Net,
strength: ClaimStrength::Strong,
verdict: Verdict::Inconclusive,
detail: String::new(),
};
assert!(r.blocks_release());
}
}