use serde::{Deserialize, Serialize};
use crate::model::{Criticality, Issue, IssueType, Level};
use crate::projection::FailureModeView;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CriticalityBuckets {
pub high: usize,
pub medium: usize,
pub low: usize,
pub unscored: usize,
}
impl CriticalityBuckets {
fn record(&mut self, residual: Option<Criticality>) {
match residual {
Some(Level::High) => self.high += 1,
Some(Level::Medium) => self.medium += 1,
Some(Level::Low) => self.low += 1,
None => self.unscored += 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadinessReport {
pub ready: bool,
pub by_criticality: CriticalityBuckets,
pub blockers: Vec<String>,
}
pub(crate) fn assess(failure_modes: &[FailureModeView], issues: &[Issue]) -> ReadinessReport {
let mut by_criticality = CriticalityBuckets::default();
let mut blockers = Vec::new();
for fmv in failure_modes {
by_criticality.record(fmv.residual_criticality);
if !fmv.is_scored() {
blockers.push(format!(
"CLARIFY: failure mode '{}' is missing cause/effect/score",
fmv.failure_mode.id
));
}
}
for fmv in failure_modes {
match fmv.residual_criticality {
Some(Level::High) => blockers.push(format!(
"RESIDUAL: failure mode '{}' still has High residual criticality",
fmv.failure_mode.id
)),
Some(Level::Medium) => blockers.push(format!(
"RESIDUAL: failure mode '{}' still has Medium residual criticality",
fmv.failure_mode.id
)),
_ => {}
}
}
for issue in issues {
if issue.r#type == IssueType::WeakMitigationOrder {
blockers.push(format!(
"DISCIPLINE: failure mode '{}' relies only on fail_fast (weak mitigation order)",
issue.failure_mode_id
));
}
}
if failure_modes.is_empty() {
blockers.push("EMPTY: no failure modes have been analyzed".to_string());
}
ReadinessReport {
ready: blockers.is_empty(),
by_criticality,
blockers,
}
}
impl Default for ReadinessReport {
fn default() -> Self {
Self {
ready: false,
by_criticality: CriticalityBuckets::default(),
blockers: vec!["EMPTY: no events".to_string()],
}
}
}