use serde::{Deserialize, Serialize};
use crate::model::{Criticality, Disposition, Issue, IssueType, Level, MitigationStatus};
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, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolutionCounts {
pub planned: usize,
pub implemented: usize,
pub verified: usize,
pub abandoned: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerifiedGate {
pub ready: bool,
pub blockers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadinessReport {
pub ready: bool,
pub by_criticality: CriticalityBuckets,
pub blockers: Vec<String>,
#[serde(default)]
pub resolution: ResolutionCounts,
#[serde(default)]
pub verified: VerifiedGate,
#[serde(default)]
pub accepted_risks: Vec<String>,
#[serde(default)]
pub excluded: Vec<String>,
#[serde(default)]
pub critical_items: Vec<String>,
}
struct Gate {
by_criticality: CriticalityBuckets,
blockers: Vec<String>,
}
fn gate(failure_modes: &[FailureModeView], issues: &[Issue]) -> Gate {
let mut by_criticality = CriticalityBuckets::default();
let mut blockers = Vec::new();
let considered: Vec<&FailureModeView> = failure_modes
.iter()
.filter(|fmv| fmv.disposition != Disposition::Excluded)
.collect();
let blocking = |fmv: &&&FailureModeView| fmv.disposition == Disposition::Open;
for fmv in &considered {
by_criticality.record(fmv.residual_criticality);
}
for fmv in considered.iter().filter(blocking) {
if !fmv.is_scored() {
blockers.push(format!(
"CLARIFY: failure mode '{}' is missing cause/effect/score",
fmv.failure_mode.id
));
}
}
for fmv in considered.iter().filter(blocking) {
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 {
let open = considered
.iter()
.filter(blocking)
.any(|fmv| fmv.failure_mode.id == issue.failure_mode_id);
if open && issue.r#type == IssueType::WeakMitigationOrder {
blockers.push(format!(
"DISCIPLINE: failure mode '{}' relies only on fail_fast (weak mitigation order)",
issue.failure_mode_id
));
}
}
if considered.is_empty() {
blockers.push("EMPTY: no failure modes have been analyzed".to_string());
}
Gate {
by_criticality,
blockers,
}
}
fn ids_with(failure_modes: &[FailureModeView], disposition: Disposition) -> Vec<String> {
failure_modes
.iter()
.filter(|fmv| fmv.disposition == disposition)
.map(|fmv| fmv.failure_mode.id.clone())
.collect()
}
fn resolution_counts(failure_modes: &[FailureModeView]) -> ResolutionCounts {
let mut counts = ResolutionCounts::default();
for m in failure_modes.iter().flat_map(|fmv| &fmv.mitigations) {
match m.status {
MitigationStatus::Planned => counts.planned += 1,
MitigationStatus::Implemented => counts.implemented += 1,
MitigationStatus::Verified => counts.verified += 1,
MitigationStatus::Abandoned => counts.abandoned += 1,
}
}
counts
}
pub(crate) fn assess(
failure_modes: &[FailureModeView],
issues: &[Issue],
verified_views: &[FailureModeView],
verified_issues: &[Issue],
) -> ReadinessReport {
let declared = gate(failure_modes, issues);
let verified = gate(verified_views, verified_issues);
let critical_items = failure_modes
.iter()
.filter(|fmv| {
fmv.failure_mode.single_point
&& fmv.criticality == Some(Level::High)
&& fmv.disposition != Disposition::Excluded
})
.map(|fmv| fmv.failure_mode.id.clone())
.collect();
ReadinessReport {
ready: declared.blockers.is_empty(),
by_criticality: declared.by_criticality,
blockers: declared.blockers,
resolution: resolution_counts(failure_modes),
verified: VerifiedGate {
ready: verified.blockers.is_empty(),
blockers: verified.blockers,
},
accepted_risks: ids_with(failure_modes, Disposition::Accepted),
excluded: ids_with(failure_modes, Disposition::Excluded),
critical_items,
}
}
impl Default for ReadinessReport {
fn default() -> Self {
Self {
ready: false,
by_criticality: CriticalityBuckets::default(),
blockers: vec!["EMPTY: no events".to_string()],
resolution: ResolutionCounts::default(),
verified: VerifiedGate {
ready: false,
blockers: vec!["EMPTY: no events".to_string()],
},
accepted_risks: Vec::new(),
excluded: Vec::new(),
critical_items: Vec::new(),
}
}
}