use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::cli::{
AvoidanceArg, ConsequenceArg, EvidenceArg, FrequencyArg, HazardStatusArg, KindArg, LinkKindArg,
PriorityArg, ProbabilityArg, SafetyFunctionStatusArg, StatusArg,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
pub name: String,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub next_id: u32,
pub requirements: BTreeMap<String, Requirement>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub hazards: BTreeMap<String, Hazard>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub safety_functions: BTreeMap<String, SafetyFunction>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub safety_requirements: BTreeMap<String, SafetyRequirement>,
#[serde(default = "one", skip_serializing_if = "is_one")]
pub next_haz_id: u32,
#[serde(default = "one", skip_serializing_if = "is_one")]
pub next_sf_id: u32,
#[serde(default = "one", skip_serializing_if = "is_one")]
pub next_sr_id: u32,
#[serde(default, rename = "_purpose", skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
#[serde(default, rename = "_config", skip_serializing_if = "Option::is_none")]
pub config: Option<ProjectConfig>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProjectConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coverage: Option<CoverageConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gate: Option<GateConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lint: Option<LintConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub safety: Option<SafetyConfig>,
#[serde(
rename = "verification",
alias = "validation",
default,
skip_serializing_if = "Option::is_none"
)]
pub verification: Option<VerificationConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub test_integration: Option<TestIntegrationConfig>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TestIntegrationConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verdict_map: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VerificationConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exempt_tags: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_force_reason_len: Option<usize>,
}
pub const DEFAULT_VERIFICATION_EXEMPT_TAG: &str = "verification-exempt";
pub const DEFAULT_MIN_FORCE_REASON_LEN: usize = 12;
impl Project {
pub fn verification_exempt_tags(&self) -> Vec<String> {
self.config
.as_ref()
.and_then(|c| c.verification.as_ref())
.and_then(|v| v.exempt_tags.clone())
.unwrap_or_else(|| vec![DEFAULT_VERIFICATION_EXEMPT_TAG.to_string()])
}
pub fn req_is_verification_exempt(&self, r: &Requirement) -> bool {
let tags = self.verification_exempt_tags();
r.tags.iter().any(|t| tags.iter().any(|e| e == t))
}
pub fn sil_escalated_srs(&self) -> Vec<(String, Sil, Sil)> {
let mut out = Vec::new();
for (id, sr) in &self.safety_requirements {
if !matches!(sr.status, Status::Verified) {
continue;
}
let Some(current) = self.inherited_sil(sr) else {
continue;
};
let evidence = sr
.tests
.iter()
.rev()
.find(|t| matches!(t.outcome, TestOutcome::Pass))
.and_then(|t| t.sil_at_verification);
if let Some(ev) = evidence {
if current.rank() > ev.rank() {
out.push((id.clone(), ev, current));
}
}
}
out
}
pub fn min_force_reason_len(&self) -> usize {
self.config
.as_ref()
.and_then(|c| c.verification.as_ref())
.and_then(|v| v.min_force_reason_len)
.unwrap_or(DEFAULT_MIN_FORCE_REASON_LEN)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SafetyConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub calibration_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub calibration: Option<BTreeMap<String, CalibrationRow>>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct CalibrationRow {
pub w1: Sil,
pub w2: Sil,
pub w3: Sil,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisclaimerAcceptance {
#[serde(rename = "_notice", default, skip_serializing_if = "String::is_empty")]
pub notice: String,
pub accepted_by: String,
pub at: DateTime<Utc>,
pub tool_version: String,
pub disclaimer_version: String,
}
pub const SAFETY_DISCLAIMER_VERSION: &str = "2";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CoverageConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GateConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub marker_near_hunks: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LintConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub short_rationale_words: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inspection_only_tags: Option<Vec<String>>,
}
pub const PURPOSE_MAX_CHARS: usize = 500;
impl Project {
pub fn new(name: String) -> Self {
let now = Utc::now();
Self {
name,
created: now,
updated: now,
next_id: 1,
requirements: BTreeMap::new(),
hazards: BTreeMap::new(),
safety_functions: BTreeMap::new(),
safety_requirements: BTreeMap::new(),
next_haz_id: 1,
next_sf_id: 1,
next_sr_id: 1,
purpose: None,
config: None,
extra: BTreeMap::new(),
}
}
pub fn allocate_id(&mut self) -> String {
let id = format!("REQ-{:04}", self.next_id);
self.next_id += 1;
id
}
pub fn allocate_haz_id(&mut self) -> String {
let id = format!("HAZ-{:04}", self.next_haz_id);
self.next_haz_id += 1;
id
}
pub fn allocate_sf_id(&mut self) -> String {
let id = format!("SF-{:04}", self.next_sf_id);
self.next_sf_id += 1;
id
}
pub fn allocate_sr_id(&mut self) -> String {
let id = format!("SR-{:04}", self.next_sr_id);
self.next_sr_id += 1;
id
}
pub fn calibration(&self) -> Option<&BTreeMap<String, CalibrationRow>> {
self.config
.as_ref()
.and_then(|c| c.safety.as_ref())
.and_then(|s| s.calibration.as_ref())
}
pub fn required_sil(&self, h: &Hazard) -> Option<Sil> {
match (h.consequence, h.frequency, h.avoidance, h.probability) {
(Some(c), Some(f), Some(p), Some(w)) => {
Some(determine_sil_calibrated(c, f, p, w, self.calibration()))
}
_ => None,
}
}
pub fn allocated_sil(&self, sf: &SafetyFunction) -> Option<Sil> {
sf.links
.iter()
.filter(|l| l.kind == LinkKind::Mitigates)
.filter_map(|l| self.hazards.get(&l.target))
.filter(|h| !matches!(h.status, HazardStatus::Obsolete))
.filter_map(|h| self.required_sil(h))
.max_by_key(|s| s.rank())
}
pub fn inherited_sil(&self, sr: &SafetyRequirement) -> Option<Sil> {
sr.links
.iter()
.filter(|l| l.kind == LinkKind::Realizes)
.filter_map(|l| self.safety_functions.get(&l.target))
.filter(|sf| !matches!(sf.status, SafetyFunctionStatus::Obsolete))
.filter_map(|sf| self.allocated_sil(sf))
.max_by_key(|s| s.rank())
}
}
fn one() -> u32 {
1
}
fn is_one(n: &u32) -> bool {
*n == 1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Requirement {
pub id: String,
pub title: String,
pub statement: String,
pub rationale: String,
pub acceptance: Vec<String>,
pub kind: Kind,
pub priority: Priority,
pub status: Status,
pub tags: Vec<String>,
pub links: Vec<Link>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub history: Vec<HistoryEntry>,
#[serde(default)]
pub tests: Vec<TestRecord>,
#[serde(
rename = "verification",
alias = "validation",
default,
skip_serializing_if = "Option::is_none"
)]
pub verification: Option<Verification>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestRecord {
pub at: DateTime<Utc>,
pub actor: String,
pub commit: String,
pub outcome: TestOutcome,
pub notes: String,
#[serde(default = "EvidenceKind::automated")]
pub kind: EvidenceKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub linked_files: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "is_false")]
pub sil_gate_exception: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sil_at_verification: Option<Sil>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external: Option<ExternalSource>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalSource {
pub system: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_verdict: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mapping_version: Option<String>,
}
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TestOutcome {
Pass,
Fail,
}
impl TestOutcome {
pub fn as_str(&self) -> &'static str {
match self {
TestOutcome::Pass => "pass",
TestOutcome::Fail => "fail",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationActivity {
pub summary: String,
pub outcome: TestOutcome,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub references: Vec<String>,
pub at: DateTime<Utc>,
pub actor: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Verification {
pub plan: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub analysis: Option<VerificationActivity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub testing: Option<VerificationActivity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub statement: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verdict: Option<TestOutcome>,
#[serde(default, skip_serializing_if = "is_false")]
pub exempt: bool,
pub opened: DateTime<Utc>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub opened_commit: String,
pub actor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub concluded: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub concluded_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub linked_files: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub human_confirmation: Option<VerificationActivity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exemption_kind: Option<ExemptionKind>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExemptionKind {
#[serde(rename = "no-dossier")]
NoDossier,
#[serde(rename = "backfilled")]
Backfilled,
}
impl Verification {
pub fn opened(plan: String, actor: String, commit: String, at: DateTime<Utc>) -> Self {
Self {
plan,
analysis: None,
testing: None,
statement: None,
verdict: None,
exempt: false,
opened: at,
opened_commit: commit,
actor,
concluded: None,
concluded_commit: None,
content_hash: None,
linked_files: None,
human_confirmation: None,
exemption_kind: None,
}
}
pub fn is_concluded(&self) -> bool {
self.verdict.is_some()
}
pub fn passed(&self) -> bool {
self.exempt || matches!(self.verdict, Some(TestOutcome::Pass))
}
pub fn derive_verdict(&self) -> Option<TestOutcome> {
match (&self.analysis, &self.testing) {
(Some(a), Some(t)) => {
if matches!(a.outcome, TestOutcome::Pass) && matches!(t.outcome, TestOutcome::Pass)
{
Some(TestOutcome::Pass)
} else {
Some(TestOutcome::Fail)
}
}
_ => None,
}
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EvidenceKind {
Automated,
Composition,
Inspection,
}
impl EvidenceKind {
pub fn automated() -> Self {
EvidenceKind::Automated
}
pub fn as_str(&self) -> &'static str {
match self {
EvidenceKind::Automated => "automated",
EvidenceKind::Composition => "composition",
EvidenceKind::Inspection => "inspection",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Link {
pub kind: LinkKind,
pub target: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
pub at: DateTime<Utc>,
pub actor: String,
#[serde(default = "ActorKind::unknown")]
pub actor_kind: ActorKind,
pub action: String,
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub on_behalf_of: Option<String>,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ActorKind {
Human,
Agent,
Unknown,
}
impl ActorKind {
pub fn unknown() -> Self {
ActorKind::Unknown
}
pub fn as_str(&self) -> &'static str {
match self {
ActorKind::Human => "human",
ActorKind::Agent => "agent",
ActorKind::Unknown => "unknown",
}
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Kind {
Functional,
NonFunctional,
Constraint,
Interface,
Business,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Priority {
Must,
Should,
Could,
Wont,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Status {
Draft,
Proposed,
Approved,
Implemented,
Verified,
Obsolete,
}
pub fn is_natural_transition(from: Status, to: Status) -> bool {
use Status::*;
if from == to {
return true;
}
if to == Obsolete && from != Obsolete {
return true;
}
matches!(
(from, to),
(Draft, Proposed)
| (Draft, Approved) | (Proposed, Approved)
| (Approved, Implemented)
| (Implemented, Verified)
)
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum LinkKind {
Parent,
DependsOn,
Conflicts,
Refines,
Verifies,
Mitigates,
Realizes,
}
impl From<KindArg> for Kind {
fn from(k: KindArg) -> Self {
match k {
KindArg::Functional => Kind::Functional,
KindArg::NonFunctional => Kind::NonFunctional,
KindArg::Constraint => Kind::Constraint,
KindArg::Interface => Kind::Interface,
KindArg::Business => Kind::Business,
}
}
}
impl From<PriorityArg> for Priority {
fn from(p: PriorityArg) -> Self {
match p {
PriorityArg::Must => Priority::Must,
PriorityArg::Should => Priority::Should,
PriorityArg::Could => Priority::Could,
PriorityArg::Wont => Priority::Wont,
}
}
}
impl From<StatusArg> for Status {
fn from(s: StatusArg) -> Self {
match s {
StatusArg::Draft => Status::Draft,
StatusArg::Proposed => Status::Proposed,
StatusArg::Approved => Status::Approved,
StatusArg::Implemented => Status::Implemented,
StatusArg::Verified => Status::Verified,
StatusArg::Obsolete => Status::Obsolete,
}
}
}
impl From<LinkKindArg> for LinkKind {
fn from(l: LinkKindArg) -> Self {
match l {
LinkKindArg::Parent => LinkKind::Parent,
LinkKindArg::DependsOn => LinkKind::DependsOn,
LinkKindArg::Conflicts => LinkKind::Conflicts,
LinkKindArg::Refines => LinkKind::Refines,
LinkKindArg::Verifies => LinkKind::Verifies,
}
}
}
impl From<ConsequenceArg> for Consequence {
fn from(c: ConsequenceArg) -> Self {
match c {
ConsequenceArg::Ca => Consequence::Ca,
ConsequenceArg::Cb => Consequence::Cb,
ConsequenceArg::Cc => Consequence::Cc,
ConsequenceArg::Cd => Consequence::Cd,
}
}
}
impl From<FrequencyArg> for Frequency {
fn from(f: FrequencyArg) -> Self {
match f {
FrequencyArg::Fa => Frequency::Fa,
FrequencyArg::Fb => Frequency::Fb,
}
}
}
impl From<AvoidanceArg> for Avoidance {
fn from(a: AvoidanceArg) -> Self {
match a {
AvoidanceArg::Pa => Avoidance::Pa,
AvoidanceArg::Pb => Avoidance::Pb,
}
}
}
impl From<ProbabilityArg> for Probability {
fn from(p: ProbabilityArg) -> Self {
match p {
ProbabilityArg::W1 => Probability::W1,
ProbabilityArg::W2 => Probability::W2,
ProbabilityArg::W3 => Probability::W3,
}
}
}
impl From<HazardStatusArg> for HazardStatus {
fn from(s: HazardStatusArg) -> Self {
match s {
HazardStatusArg::Identified => HazardStatus::Identified,
HazardStatusArg::Assessed => HazardStatus::Assessed,
HazardStatusArg::Mitigated => HazardStatus::Mitigated,
HazardStatusArg::Verified => HazardStatus::Verified,
HazardStatusArg::Obsolete => HazardStatus::Obsolete,
}
}
}
impl From<SafetyFunctionStatusArg> for SafetyFunctionStatus {
fn from(s: SafetyFunctionStatusArg) -> Self {
match s {
SafetyFunctionStatusArg::Proposed => SafetyFunctionStatus::Proposed,
SafetyFunctionStatusArg::Allocated => SafetyFunctionStatus::Allocated,
SafetyFunctionStatusArg::Implemented => SafetyFunctionStatus::Implemented,
SafetyFunctionStatusArg::Verified => SafetyFunctionStatus::Verified,
SafetyFunctionStatusArg::Obsolete => SafetyFunctionStatus::Obsolete,
}
}
}
impl From<EvidenceArg> for EvidenceKind {
fn from(e: EvidenceArg) -> Self {
match e {
EvidenceArg::Automated => EvidenceKind::Automated,
EvidenceArg::Composition => EvidenceKind::Composition,
EvidenceArg::Inspection => EvidenceKind::Inspection,
}
}
}
impl Kind {
pub fn as_str(&self) -> &'static str {
match self {
Kind::Functional => "functional",
Kind::NonFunctional => "non-functional",
Kind::Constraint => "constraint",
Kind::Interface => "interface",
Kind::Business => "business",
}
}
}
impl Priority {
pub fn as_str(&self) -> &'static str {
match self {
Priority::Must => "must",
Priority::Should => "should",
Priority::Could => "could",
Priority::Wont => "wont",
}
}
}
impl Status {
pub fn as_str(&self) -> &'static str {
match self {
Status::Draft => "draft",
Status::Proposed => "proposed",
Status::Approved => "approved",
Status::Implemented => "implemented",
Status::Verified => "verified",
Status::Obsolete => "obsolete",
}
}
}
impl LinkKind {
pub fn as_str(&self) -> &'static str {
match self {
LinkKind::Parent => "parent",
LinkKind::DependsOn => "depends-on",
LinkKind::Conflicts => "conflicts",
LinkKind::Refines => "refines",
LinkKind::Verifies => "verifies",
LinkKind::Mitigates => "mitigates",
LinkKind::Realizes => "realizes",
}
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Consequence {
#[serde(rename = "C_A")]
Ca,
#[serde(rename = "C_B")]
Cb,
#[serde(rename = "C_C")]
Cc,
#[serde(rename = "C_D")]
Cd,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Frequency {
#[serde(rename = "F_A")]
Fa,
#[serde(rename = "F_B")]
Fb,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Avoidance {
#[serde(rename = "P_A")]
Pa,
#[serde(rename = "P_B")]
Pb,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Probability {
W1,
W2,
W3,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Sil {
#[serde(rename = "none")]
NoneRequired,
#[serde(rename = "a")]
A,
#[serde(rename = "SIL1")]
Sil1,
#[serde(rename = "SIL2")]
Sil2,
#[serde(rename = "SIL3")]
Sil3,
#[serde(rename = "SIL4")]
Sil4,
#[serde(rename = "b")]
B,
}
impl Consequence {
pub fn as_str(&self) -> &'static str {
match self {
Consequence::Ca => "C_A",
Consequence::Cb => "C_B",
Consequence::Cc => "C_C",
Consequence::Cd => "C_D",
}
}
}
impl Frequency {
pub fn as_str(&self) -> &'static str {
match self {
Frequency::Fa => "F_A",
Frequency::Fb => "F_B",
}
}
}
impl Avoidance {
pub fn as_str(&self) -> &'static str {
match self {
Avoidance::Pa => "P_A",
Avoidance::Pb => "P_B",
}
}
}
impl Probability {
pub fn as_str(&self) -> &'static str {
match self {
Probability::W1 => "W1",
Probability::W2 => "W2",
Probability::W3 => "W3",
}
}
}
impl Sil {
pub fn rank(&self) -> u8 {
match self {
Sil::NoneRequired => 0,
Sil::A => 1,
Sil::Sil1 => 2,
Sil::Sil2 => 3,
Sil::Sil3 => 4,
Sil::Sil4 => 5,
Sil::B => 6,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Sil::NoneRequired => "—",
Sil::A => "a",
Sil::Sil1 => "SIL1",
Sil::Sil2 => "SIL2",
Sil::Sil3 => "SIL3",
Sil::Sil4 => "SIL4",
Sil::B => "b",
}
}
pub fn parse(s: &str) -> Option<Sil> {
match s.trim().to_lowercase().as_str() {
"none" | "-" | "—" | "" => Some(Sil::NoneRequired),
"a" => Some(Sil::A),
"1" | "sil1" => Some(Sil::Sil1),
"2" | "sil2" => Some(Sil::Sil2),
"3" | "sil3" => Some(Sil::Sil3),
"4" | "sil4" => Some(Sil::Sil4),
"b" => Some(Sil::B),
_ => None,
}
}
}
pub fn determine_sil(c: Consequence, f: Frequency, p: Avoidance, w: Probability) -> Sil {
use Avoidance::*;
use Consequence::*;
use Frequency::*;
use Probability::*;
use Sil::{NoneRequired as N, Sil1, Sil2, Sil3, A, B};
if let Ca = c {
return N;
}
let row: [Sil; 3] = match (c, f, p) {
(Cb, Fa, Pa) => [A, N, N],
(Cb, Fa, Pb) => [Sil1, A, N],
(Cb, Fb, Pa) => [Sil1, A, N],
(Cb, Fb, Pb) => [Sil2, Sil1, A],
(Cc, Fa, Pa) => [Sil1, A, N],
(Cc, Fa, Pb) => [Sil2, Sil1, A],
(Cc, Fb, Pa) => [Sil2, Sil1, A],
(Cc, Fb, Pb) => [Sil3, Sil2, Sil1],
(Cd, Fa, Pa) => [Sil2, Sil1, A],
(Cd, Fa, Pb) => [Sil3, Sil2, Sil1],
(Cd, Fb, Pa) => [Sil3, Sil2, Sil1],
(Cd, Fb, Pb) => [B, Sil3, Sil2],
(Ca, _, _) => [N, N, N],
};
match w {
W3 => row[0],
W2 => row[1],
W1 => row[2],
}
}
pub fn calibration_leaf(c: Consequence, f: Frequency, p: Avoidance) -> String {
format!("{}/{}/{}", c.as_str(), f.as_str(), p.as_str())
}
pub fn determine_sil_calibrated(
c: Consequence,
f: Frequency,
p: Avoidance,
w: Probability,
calibration: Option<&BTreeMap<String, CalibrationRow>>,
) -> Sil {
if let Some(table) = calibration {
if let Some(row) = table.get(&calibration_leaf(c, f, p)) {
return match w {
Probability::W1 => row.w1,
Probability::W2 => row.w2,
Probability::W3 => row.w3,
};
}
}
determine_sil(c, f, p, w)
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum HazardStatus {
Identified,
Assessed,
Mitigated,
Verified,
Obsolete,
}
impl HazardStatus {
pub fn as_str(&self) -> &'static str {
match self {
HazardStatus::Identified => "identified",
HazardStatus::Assessed => "assessed",
HazardStatus::Mitigated => "mitigated",
HazardStatus::Verified => "verified",
HazardStatus::Obsolete => "obsolete",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hazard {
pub id: String,
pub title: String,
pub description: String,
pub operating_context: String,
pub harm: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub consequence: Option<Consequence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frequency: Option<Frequency>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub avoidance: Option<Avoidance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub probability: Option<Probability>,
pub status: HazardStatus,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub links: Vec<Link>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub history: Vec<HistoryEntry>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
impl Hazard {
pub fn is_assessed(&self) -> bool {
self.consequence.is_some()
&& self.frequency.is_some()
&& self.avoidance.is_some()
&& self.probability.is_some()
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SafetyFunctionStatus {
Proposed,
Allocated,
Implemented,
Verified,
Obsolete,
}
impl SafetyFunctionStatus {
pub fn as_str(&self) -> &'static str {
match self {
SafetyFunctionStatus::Proposed => "proposed",
SafetyFunctionStatus::Allocated => "allocated",
SafetyFunctionStatus::Implemented => "implemented",
SafetyFunctionStatus::Verified => "verified",
SafetyFunctionStatus::Obsolete => "obsolete",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SafetyFunction {
pub id: String,
pub title: String,
pub description: String,
pub safe_state: String,
pub status: SafetyFunctionStatus,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub links: Vec<Link>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub history: Vec<HistoryEntry>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SafetyRequirement {
pub id: String,
pub title: String,
pub statement: String,
pub rationale: String,
#[serde(default)]
pub acceptance: Vec<String>,
pub priority: Priority,
pub status: Status,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub links: Vec<Link>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub history: Vec<HistoryEntry>,
#[serde(default)]
pub tests: Vec<TestRecord>,
#[serde(
rename = "verification",
alias = "validation",
default,
skip_serializing_if = "Option::is_none"
)]
pub verification: Option<Verification>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub walkthrough: Option<WalkthroughAck>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalkthroughAck {
pub reviewer: String,
pub at: DateTime<Utc>,
pub commit: String,
#[serde(default, skip_serializing_if = "is_false")]
pub objected: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[cfg(test)]
mod sil_tests {
use super::*;
#[test]
#[allow(clippy::type_complexity)]
fn risk_graph_every_leaf() {
use Avoidance::*;
use Consequence::*;
use Frequency::*;
use Probability::*;
use Sil::{NoneRequired as N, Sil1, Sil2, Sil3, A, B};
let table: &[((Consequence, Frequency, Avoidance), [Sil; 3])] = &[
((Cb, Fa, Pa), [A, N, N]),
((Cb, Fa, Pb), [Sil1, A, N]),
((Cb, Fb, Pa), [Sil1, A, N]),
((Cb, Fb, Pb), [Sil2, Sil1, A]),
((Cc, Fa, Pa), [Sil1, A, N]),
((Cc, Fa, Pb), [Sil2, Sil1, A]),
((Cc, Fb, Pa), [Sil2, Sil1, A]),
((Cc, Fb, Pb), [Sil3, Sil2, Sil1]),
((Cd, Fa, Pa), [Sil2, Sil1, A]),
((Cd, Fa, Pb), [Sil3, Sil2, Sil1]),
((Cd, Fb, Pa), [Sil3, Sil2, Sil1]),
((Cd, Fb, Pb), [B, Sil3, Sil2]),
];
for ((c, f, p), [e3, e2, e1]) in table {
assert_eq!(
determine_sil(*c, *f, *p, W3),
*e3,
"{:?}/{:?}/{:?}/W3",
c,
f,
p
);
assert_eq!(
determine_sil(*c, *f, *p, W2),
*e2,
"{:?}/{:?}/{:?}/W2",
c,
f,
p
);
assert_eq!(
determine_sil(*c, *f, *p, W1),
*e1,
"{:?}/{:?}/{:?}/W1",
c,
f,
p
);
}
}
#[test]
fn consequence_ca_never_needs_safety() {
use Avoidance::*;
use Frequency::*;
use Probability::*;
for f in [Fa, Fb] {
for p in [Pa, Pb] {
for w in [W1, W2, W3] {
assert_eq!(determine_sil(Consequence::Ca, f, p, w), Sil::NoneRequired);
}
}
}
}
#[test]
fn sil_rank_is_monotonic() {
assert!(Sil::B.rank() > Sil::Sil4.rank());
assert!(Sil::Sil4.rank() > Sil::Sil1.rank());
assert!(Sil::Sil1.rank() > Sil::A.rank());
assert!(Sil::A.rank() > Sil::NoneRequired.rank());
}
}