use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::event::Event;
use crate::matrix::MatrixStrategy;
use crate::model::{
Criticality, Domain, EntityRef, EvidenceRef, FailureMode, FailureModeStanding, Issue, Level,
Mitigation, MitigationKind,
};
use crate::projection::{self, FailureModeView};
use crate::response::{ResponseClass, Scope};
use crate::scoring::{self, Axis};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalyzeMitigation {
pub id: String,
pub kind: MitigationKind,
pub description: String,
#[serde(default)]
pub residual_severity_observations: Vec<String>,
#[serde(default)]
pub residual_probability_observations: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalyzeInput {
pub id: String,
pub component: EntityRef,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effect: Option<String>,
pub domain: Domain,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<Scope>,
#[serde(default)]
pub severity_observations: Vec<String>,
#[serde(default)]
pub probability_observations: Vec<String>,
#[serde(default)]
pub mitigations: Vec<AnalyzeMitigation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalyzeFailureMode {
pub id: String,
pub criticality: Option<Criticality>,
pub residual_criticality: Option<Criticality>,
pub standing: Option<FailureModeStanding>,
pub response_class: Option<ResponseClass>,
pub issues: Vec<Issue>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalyzeReport {
pub matrix_strategy: MatrixStrategy,
pub failure_modes: Vec<AnalyzeFailureMode>,
pub risk_ranking: Vec<String>,
pub issues: Vec<Issue>,
pub ready: bool,
pub blockers: Vec<String>,
}
const ANALYZE_SESSION_ID: &str = "__analyze__";
pub fn analyze(strategy: MatrixStrategy, failure_modes: &[AnalyzeInput]) -> Result<AnalyzeReport> {
for fm in failure_modes {
scoring::derive_ordinal(strategy, Axis::Severity, &fm.severity_observations)?;
scoring::derive_ordinal(strategy, Axis::Probability, &fm.probability_observations)?;
for mit in &fm.mitigations {
scoring::derive_ordinal(
strategy,
Axis::Severity,
&mit.residual_severity_observations,
)?;
scoring::derive_ordinal(
strategy,
Axis::Probability,
&mit.residual_probability_observations,
)?;
}
}
let events = to_events(strategy, failure_modes);
let state = projection::project(&events);
let report_fms: Vec<AnalyzeFailureMode> = state
.failure_modes
.iter()
.map(|fmv| AnalyzeFailureMode {
id: fmv.failure_mode.id.clone(),
criticality: fmv.criticality,
residual_criticality: fmv.residual_criticality,
standing: fmv.standing,
response_class: fmv.response_class,
issues: issues_for(&state.issues, &fmv.failure_mode.id),
})
.collect();
let risk_ranking = rank_by_risk(&state.failure_modes);
Ok(AnalyzeReport {
matrix_strategy: state.matrix_strategy,
failure_modes: report_fms,
risk_ranking,
issues: state.issues,
ready: state.readiness.ready,
blockers: state.readiness.blockers,
})
}
fn to_events(strategy: MatrixStrategy, failure_modes: &[AnalyzeInput]) -> Vec<Event> {
let mut events = Vec::with_capacity(1 + failure_modes.len() * 2);
events.push(Event::SessionOpened {
session_id: ANALYZE_SESSION_ID.to_string(),
matrix_strategy: strategy,
meta: crate::model::SessionMeta::default(),
});
for input in failure_modes {
events.push(Event::FailureModeAdded {
failure_mode: Box::new(to_failure_mode(input)),
});
for mit in &input.mitigations {
events.push(Event::MitigationAdded {
mitigation: Box::new(to_mitigation(&input.id, mit)),
});
}
}
events
}
fn to_failure_mode(input: &AnalyzeInput) -> FailureMode {
FailureMode {
id: input.id.clone(),
session_id: ANALYZE_SESSION_ID.to_string(),
component: input.component.clone(),
description: input.description.clone(),
cause: input.cause.clone(),
effect: input.effect.clone(),
severity_observations: input.severity_observations.clone(),
probability_observations: input.probability_observations.clone(),
domain: input.domain,
scope: input.scope,
source: EvidenceRef::new(input.id.clone()),
parent: None,
refs: Vec::new(),
local_effect: None,
next_effect: None,
single_point: false,
}
}
fn to_mitigation(fm_id: &str, mit: &AnalyzeMitigation) -> Mitigation {
Mitigation {
id: mit.id.clone(),
session_id: ANALYZE_SESSION_ID.to_string(),
failure_mode_id: fm_id.to_string(),
kind: mit.kind,
description: mit.description.clone(),
residual_severity_observations: mit.residual_severity_observations.clone(),
residual_probability_observations: mit.residual_probability_observations.clone(),
source: EvidenceRef::new(mit.id.clone()),
}
}
fn issues_for(issues: &[Issue], fm_id: &str) -> Vec<Issue> {
issues
.iter()
.filter(|i| i.failure_mode_id == fm_id)
.cloned()
.collect()
}
fn rank_by_risk(failure_modes: &[FailureModeView]) -> Vec<String> {
let mut ranked: Vec<&FailureModeView> = failure_modes.iter().collect();
ranked.sort_by(|a, b| {
risk_key(b.criticality)
.cmp(&risk_key(a.criticality))
.then_with(|| risk_key(b.residual_criticality).cmp(&risk_key(a.residual_criticality)))
});
ranked
.into_iter()
.map(|fmv| fmv.failure_mode.id.clone())
.collect()
}
fn risk_key(c: Option<Criticality>) -> u8 {
match c {
Some(Level::High) => 3,
Some(Level::Medium) => 2,
Some(Level::Low) => 1,
None => 0,
}
}