use anyhow::{anyhow, Result};
use chrono::Utc;
use std::path::PathBuf;
use crate::cli::{
HazAdqConcludeArgs, HazAdqCoverArgs, HazAdqPlanArgs, HazardAddArgs, HazardAdequacyCmd,
HazardAssessArgs, HazardCmd, HazardConfirmArgs, HazardListArgs, HazardShowArgs,
HazardUpdateArgs, SfAddArgs, SfCmd, SfListArgs, SfMitigateArgs, SfShowArgs, SfUpdateArgs,
SreqAddArgs, SreqCmd, SreqListArgs, SreqRealizeArgs, SreqShowArgs, SreqUpdateArgs,
SreqVerifyArgs, TraceArgs,
};
use crate::model::{
EvidenceKind, Hazard, HazardStatus, Link, LinkKind, Project, SafetyFunction,
SafetyFunctionStatus, SafetyRequirement, Sil, Status, TestOutcome, TestRecord,
};
use crate::storage::{self, load_for_mutation, load_resolved};
const ACHIEVED_INTEGRITY_STAMP: &str =
"target only — no PFD/PFH, architectural-constraint (HFT/SFF), diagnostic-coverage \
or systematic-capability evidence is recorded here; achieved integrity is out of scope \
(see `req help safety`).";
fn normalize(prefix: &str, raw: &str) -> String {
let trimmed = raw.trim();
let upper = trimmed.to_uppercase();
let want = format!("{}-", prefix);
let digits = if let Some(rest) = upper.strip_prefix(&want) {
rest.to_string()
} else if trimmed.chars().all(|c| c.is_ascii_digit()) && !trimmed.is_empty() {
trimmed.to_string()
} else {
return upper;
};
match digits.parse::<u32>() {
Ok(n) => format!("{}-{:04}", prefix, n),
Err(_) => upper,
}
}
fn resolve_haz(project: &Project, raw: &str) -> Result<String> {
let id = normalize("HAZ", raw);
if project.hazards.contains_key(&id) {
Ok(id)
} else {
Err(anyhow!("no such hazard: {}", raw))
}
}
fn resolve_sf(project: &Project, raw: &str) -> Result<String> {
let id = normalize("SF", raw);
if project.safety_functions.contains_key(&id) {
Ok(id)
} else {
Err(anyhow!("no such safety function: {}", raw))
}
}
fn resolve_sr(project: &Project, raw: &str) -> Result<String> {
let id = normalize("SR", raw);
if project.safety_requirements.contains_key(&id) {
Ok(id)
} else {
Err(anyhow!("no such safety requirement: {}", raw))
}
}
fn sil_str(s: Option<Sil>) -> String {
s.map(|s| s.as_str().to_string())
.unwrap_or_else(|| "—".to_string())
}
fn git_head() -> String {
std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default()
}
pub fn run_hazard(cmd: HazardCmd, file: &Option<PathBuf>) -> Result<()> {
match cmd {
HazardCmd::Add(a) => {
super::safety_gov::ensure_enabled(file)?;
hazard_add(a, file)
}
HazardCmd::Assess(a) => {
super::safety_gov::ensure_enabled(file)?;
hazard_assess(a, file)
}
HazardCmd::Update(a) => {
super::safety_gov::ensure_enabled(file)?;
hazard_update(a, file)
}
HazardCmd::Adequacy(a) => {
super::safety_gov::ensure_enabled(file)?;
run_hazard_adequacy(a, file)
}
HazardCmd::Confirm(a) => {
super::safety_gov::ensure_enabled(file)?;
hazard_confirm(a, file)
}
HazardCmd::List(a) => hazard_list(a, file),
HazardCmd::Show(a) => hazard_show(a, file),
}
}
fn run_hazard_adequacy(cmd: HazardAdequacyCmd, file: &Option<PathBuf>) -> Result<()> {
match cmd {
HazardAdequacyCmd::Plan(a) => hazard_adequacy_plan(a, file),
HazardAdequacyCmd::Cover(a) => hazard_adequacy_cover(a, file),
HazardAdequacyCmd::Conclude(a) => hazard_adequacy_conclude(a, file),
}
}
fn hazard_adequacy_gate(
project: &Project,
haz_id: &str,
coverage: &[crate::model::CoverageNote],
) -> Result<String> {
use std::collections::HashSet;
let sfs = project.mitigating_sfs(haz_id);
if sfs.is_empty() {
return Err(anyhow!(
"{} has no live mitigating safety function — nothing to argue adequacy over. Link one \
with `req sf mitigate SF-NNNN {}`.",
haz_id,
haz_id
));
}
let covered: HashSet<&str> = coverage.iter().map(|c| c.target.as_str()).collect();
let mut uncovered = Vec::new();
let mut unverified = Vec::new();
let mut tokens = Vec::new();
for sf in &sfs {
if !covered.contains(sf.id.as_str()) {
uncovered.push(sf.id.clone());
}
let verified = matches!(sf.status, SafetyFunctionStatus::Verified);
if !verified {
unverified.push(format!("{} ({})", sf.id, sf.status.as_str()));
}
let ch = sf
.verification
.as_ref()
.and_then(|v| v.content_hash.as_deref());
tokens.push(crate::model::chain_token(&sf.id, verified, ch));
}
if !uncovered.is_empty() {
return Err(anyhow!(
"{} cannot conclude adequacy — these mitigating safety functions have no walk-through \
note: {}. Record one with `req hazard adequacy cover {} --sf SF-NNNN --note \"...\"`.",
haz_id,
uncovered.join(", "),
haz_id
));
}
if !unverified.is_empty() {
return Err(anyhow!(
"{} cannot be argued adequately mitigated by VERIFIED safety functions — these are not \
Verified: {}. Verify them first (their dossier + human co-sign) so the chain is sound \
bottom-up.",
haz_id,
unverified.join(", ")
));
}
Ok(crate::model::chain_anchor(&tokens))
}
fn hazard_adequacy_plan(args: HazAdqPlanArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_haz(&project, &args.id)?;
if args.plan.trim().is_empty() {
return Err(anyhow!("--plan must not be empty"));
}
let now = Utc::now();
let commit = crate::commands::test_cmd::current_head_sha_opt().unwrap_or_default();
{
let h = project.hazards.get_mut(&id).unwrap();
if let Some(a) = &h.adequacy {
if a.verdict.is_some() && !args.reopen {
return Err(anyhow!(
"{} already has a concluded adequacy dossier — pass --reopen to re-argue it \
(clears the prior verdict and co-sign).",
id
));
}
}
h.adequacy = Some(crate::model::AdequacyArgument {
plan: args.plan.clone(),
coverage: Vec::new(),
statement: String::new(),
credited_external_measures: None,
verdict: None,
chain_anchor: None,
actor: super::current_actor(),
at: now,
commit,
human_confirmation: None,
});
h.updated = now;
h.history.push(super::history(
if args.reopen {
"adequacy dossier re-opened (plan recorded)"
} else {
"adequacy dossier opened (plan recorded)"
},
None,
));
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&project.hazards[&id])?);
} else {
println!("Opened adequacy dossier for {}.", id);
let sfs = project.mitigating_sfs(&id);
println!(
"Walk through each mitigating safety function ({}): `req hazard adequacy cover {} --sf SF-NNNN --note \"...\"`",
sfs.iter().map(|s| s.id.as_str()).collect::<Vec<_>>().join(", "),
id
);
}
Ok(())
}
fn hazard_adequacy_cover(args: HazAdqCoverArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_haz(&project, &args.id)?;
let sf_id = resolve_sf(&project, &args.sf)?;
if args.note.trim().is_empty() {
return Err(anyhow!("--note must not be empty"));
}
if !project.mitigating_sfs(&id).iter().any(|s| s.id == sf_id) {
return Err(anyhow!(
"{} does not live-mitigate {} — only a mitigating safety function can be covered here.",
sf_id,
id
));
}
let now = Utc::now();
let actor = super::current_actor();
{
let h = project.hazards.get_mut(&id).unwrap();
let adq = h.adequacy.as_mut().ok_or_else(|| {
anyhow!(
"{} has no open adequacy dossier — run `req hazard adequacy plan {} --plan \"...\"` first.",
id, id
)
})?;
if adq.verdict.is_some() {
return Err(anyhow!(
"{}'s adequacy dossier is already concluded — re-open it with `req hazard adequacy plan {} --reopen` to revise.",
id, id
));
}
adq.coverage.retain(|c| c.target != sf_id);
adq.coverage.push(crate::model::CoverageNote {
target: sf_id.clone(),
note: args.note.clone(),
at: now,
actor,
});
h.updated = now;
h.history.push(super::history(
format!("adequacy coverage recorded for {}", sf_id),
None,
));
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&project.hazards[&id])?);
} else {
println!("Recorded adequacy coverage of {} for {}.", sf_id, id);
}
Ok(())
}
fn hazard_adequacy_conclude(args: HazAdqConcludeArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_haz(&project, &args.id)?;
if args.statement.trim().is_empty() {
return Err(anyhow!(
"--statement (the residual-risk argument) must not be empty"
));
}
let coverage = project
.hazards
.get(&id)
.and_then(|h| h.adequacy.as_ref())
.map(|a| a.coverage.clone())
.ok_or_else(|| {
anyhow!(
"{} has no open adequacy dossier — run `req hazard adequacy plan {} --plan \"...\"` first.",
id, id
)
})?;
let anchor = hazard_adequacy_gate(&project, &id, &coverage)?;
let now = Utc::now();
let commit = crate::commands::test_cmd::current_head_sha_opt().unwrap_or_default();
{
let h = project.hazards.get_mut(&id).unwrap();
let adq = h.adequacy.as_mut().unwrap();
adq.statement = args.statement.clone();
adq.credited_external_measures = args.external.clone();
adq.verdict = Some(crate::model::AdequacyVerdict::Adequate);
adq.chain_anchor = Some(anchor);
adq.commit = commit;
adq.at = now;
adq.human_confirmation = None;
h.updated = now;
h.history.push(super::history(
"adequacy dossier concluded (adequate) — awaiting human co-sign",
None,
));
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&project.hazards[&id])?);
} else {
println!(
"Concluded adequacy for {} — verdict ADEQUATE (every mitigating SF covered and Verified).",
id
);
println!(
"Next: a human runs `req hazard confirm {}` to co-sign it and promote to Verified.",
id
);
}
Ok(())
}
fn hazard_confirm(args: HazardConfirmArgs, file: &Option<PathBuf>) -> Result<()> {
if matches!(super::current_actor_kind(), crate::model::ActorKind::Agent) {
return Err(anyhow!(
"co-signing a hazard's adequacy argument must be done by a human, but \
REQ_ACTOR_KIND=agent. A person must run `req hazard confirm`."
));
}
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_haz(&project, &args.id)?;
{
let adq = project
.hazards
.get(&id)
.and_then(|h| h.adequacy.as_ref())
.ok_or_else(|| {
anyhow!(
"{} has no adequacy dossier to co-sign — run `req hazard adequacy plan {} --plan \"...\"` first.",
id, id
)
})?;
if !matches!(adq.verdict, Some(crate::model::AdequacyVerdict::Adequate)) {
return Err(anyhow!(
"{} has no concluded ADEQUATE verdict to co-sign — run `req hazard adequacy conclude {} --statement \"...\"` first.",
id, id
));
}
let coverage = adq.coverage.clone();
hazard_adequacy_gate(&project, &id, &coverage)?;
}
let now = Utc::now();
{
let h = project.hazards.get_mut(&id).unwrap();
if !matches!(h.status, HazardStatus::Mitigated | HazardStatus::Verified) {
return Err(anyhow!(
"{} is {} — only a Mitigated hazard can be promoted to Verified.",
id,
h.status.as_str()
));
}
let adequacy = h.adequacy.as_mut().unwrap();
adequacy.human_confirmation = Some(crate::model::VerificationActivity {
summary: if args.note.is_empty() {
"human co-sign of the mitigation-adequacy dossier".to_string()
} else {
args.note.clone()
},
outcome: TestOutcome::Pass,
references: Vec::new(),
at: now,
actor: super::current_actor(),
});
h.status = HazardStatus::Verified;
h.updated = now;
h.history.push(super::history(
"adequacy dossier co-signed by human — promoted to Verified",
None,
));
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&project.hazards[&id])?);
} else {
println!(
"Co-signed adequacy dossier for {} — promoted to Verified.",
id
);
}
Ok(())
}
fn hazard_add(args: HazardAddArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let now = Utc::now();
let consequence = args.consequence.map(Into::into);
let frequency = args.frequency.map(Into::into);
let avoidance = args.avoidance.map(Into::into);
let probability = args.probability.map(Into::into);
let fully_assessed = consequence.is_some()
&& frequency.is_some()
&& avoidance.is_some()
&& probability.is_some();
let status = if fully_assessed {
HazardStatus::Assessed
} else {
HazardStatus::Identified
};
let id = project.allocate_haz_id();
let hazard = Hazard {
id: id.clone(),
title: args.title,
description: args.description,
operating_context: args.context,
harm: args.harm,
consequence,
frequency,
avoidance,
probability,
status,
tags: args.tag,
links: Vec::new(),
created: now,
updated: now,
history: vec![super::history("created", None)],
adequacy: None,
extra: Default::default(),
};
project.hazards.insert(id.clone(), hazard.clone());
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&hazard)?);
} else {
println!("Added {}", id);
match project.required_sil(&hazard) {
Some(s) => println!("Assessed: required {}", s.as_str()),
None => println!(
"Status: identified (run `req hazard assess {} -C .. -F .. -P .. -W ..` to derive a SIL)",
id
),
}
}
Ok(())
}
fn hazard_list(args: HazardListArgs, file: &Option<PathBuf>) -> Result<()> {
let (_path, project) = load_resolved(file)?;
let status_filter: Option<HazardStatus> = args.status.map(Into::into);
let sil_filter = args.sil.as_deref().map(|s| s.to_uppercase());
let mut rows: Vec<&Hazard> = project
.hazards
.values()
.filter(|h| status_filter.map(|s| h.status == s).unwrap_or(true))
.filter(|h| {
sil_filter
.as_ref()
.map(|want| {
project
.required_sil(h)
.map(|s| s.as_str().to_uppercase() == *want)
.unwrap_or(false)
})
.unwrap_or(true)
})
.filter(|h| {
if !args.unmitigated {
return true;
}
!project
.safety_functions
.values()
.any(|sf| mitigates(sf, &h.id))
})
.collect();
rows.sort_by(|a, b| a.id.cmp(&b.id));
if args.json {
println!("{}", serde_json::to_string_pretty(&rows)?);
return Ok(());
}
if rows.is_empty() {
println!("No hazards.");
return Ok(());
}
println!("{:<9} {:<6} {:<11} TITLE", "ID", "SIL", "STATUS");
for h in rows {
println!(
"{:<9} {:<6} {:<11} {}",
h.id,
sil_str(project.required_sil(h)),
h.status.as_str(),
h.title
);
}
Ok(())
}
fn hazard_show(args: HazardShowArgs, file: &Option<PathBuf>) -> Result<()> {
let (_path, project) = load_resolved(file)?;
let id = resolve_haz(&project, &args.id)?;
let h = &project.hazards[&id];
if args.json {
println!("{}", serde_json::to_string_pretty(h)?);
return Ok(());
}
println!("{} {}", h.id, h.title);
println!(" status: {}", h.status.as_str());
if !h.description.is_empty() {
println!(" description: {}", h.description);
}
if !h.operating_context.is_empty() {
println!(" context: {}", h.operating_context);
}
println!(" harm: {}", h.harm);
match (h.consequence, h.frequency, h.avoidance, h.probability) {
(Some(c), Some(f), Some(p), Some(w)) => {
println!(
" risk: {} · {} · {} · {} ──► required {}",
c.as_str(),
f.as_str(),
p.as_str(),
w.as_str(),
sil_str(project.required_sil(h))
);
}
_ => println!(" risk: not yet assessed"),
}
let sfs: Vec<&SafetyFunction> = project
.safety_functions
.values()
.filter(|sf| mitigates(sf, &h.id))
.collect();
if sfs.is_empty() {
println!(" mitigated by: (none)");
} else {
println!(" mitigated by:");
for sf in sfs {
println!(" {} — {} [{}]", sf.id, sf.title, sf.status.as_str());
}
}
if !h.tags.is_empty() {
println!(" tags: {}", h.tags.join(", "));
}
match &h.adequacy {
None => println!(
" adequacy: (none — `req hazard adequacy plan {} --plan \"...\"` to open the \
mitigation-adequacy dossier)",
h.id
),
Some(a) => {
let standing = match (a.verdict, a.human_confirmation.is_some()) {
(Some(v), true) => format!("{} (human co-signed)", v.as_str()),
(Some(v), false) => format!("{} (awaiting human co-sign)", v.as_str()),
(None, _) => "in progress (not concluded)".to_string(),
};
println!(" adequacy: {}", standing);
if !a.plan.is_empty() {
println!(" plan: {}", a.plan);
}
for c in &a.coverage {
println!(" covers {}: {}", c.target, c.note);
}
if !a.statement.is_empty() {
println!(" residual: {}", a.statement);
}
if let Some(ext) = &a.credited_external_measures {
println!(" ext.credit: {}", ext);
}
}
}
for line in hazard_signoff_lines(&project, h) {
println!("{}", line);
}
println!("\nRun `req trace {}` for the full safety case.", h.id);
Ok(())
}
fn hazard_assess(args: HazardAssessArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_haz(&project, &args.id)?;
let now = Utc::now();
{
let h = project.hazards.get_mut(&id).unwrap();
h.consequence = Some(args.consequence.into());
h.frequency = Some(args.frequency.into());
h.avoidance = Some(args.avoidance.into());
h.probability = Some(args.probability.into());
if matches!(h.status, HazardStatus::Identified) {
h.status = HazardStatus::Assessed;
}
h.updated = now;
h.history
.push(super::history("assessed", args.reason.clone()));
}
project.updated = now;
let derived = project.required_sil(&project.hazards[&id]);
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&project.hazards[&id])?);
} else {
println!("Assessed {} ──► required {}", id, sil_str(derived));
}
Ok(())
}
fn hazard_update(args: HazardUpdateArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_haz(&project, &args.id)?;
let now = Utc::now();
{
let h = project.hazards.get_mut(&id).unwrap();
if let Some(t) = args.title {
h.title = t;
}
if let Some(d) = args.description {
h.description = d;
}
if let Some(c) = args.context {
h.operating_context = c;
}
if let Some(harm) = args.harm {
h.harm = harm;
}
if let Some(s) = args.status {
let next: HazardStatus = s.into();
if matches!(next, HazardStatus::Verified) {
return Err(anyhow!(
"{} cannot be set to verified directly — record a mitigation-adequacy argument \
with `req hazard adequacy {} --statement \"...\"`, then a human runs \
`req hazard confirm {}` to co-sign it and promote the hazard to Verified.",
id,
id,
id
));
}
h.status = next;
}
for t in &args.add_tag {
if !h.tags.contains(t) {
h.tags.push(t.clone());
}
}
h.tags.retain(|t| !args.remove_tag.contains(t));
h.updated = now;
h.history
.push(super::history("updated", args.reason.clone()));
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&project.hazards[&id])?);
} else {
println!("Updated {}", id);
}
Ok(())
}
fn mitigates(sf: &SafetyFunction, haz_id: &str) -> bool {
sf.links
.iter()
.any(|l| l.kind == LinkKind::Mitigates && l.target == haz_id)
}
fn realizes(sr: &SafetyRequirement, sf_id: &str) -> bool {
sr.links
.iter()
.any(|l| l.kind == LinkKind::Realizes && l.target == sf_id)
}
pub fn sf_signoff_lines(project: &Project, sf: &SafetyFunction) -> Vec<String> {
let srs = project.realizing_srs(&sf.id);
let mut out = Vec::new();
if srs.is_empty() {
out.push(
" sign-off basis: no realizing safety requirement — nothing to implement the function"
.into(),
);
return out;
}
let total = srs.len();
let verified: Vec<&str> = srs
.iter()
.filter(|sr| matches!(sr.status, Status::Verified))
.map(|sr| sr.id.as_str())
.collect();
let pending: Vec<&str> = srs
.iter()
.filter(|sr| !matches!(sr.status, Status::Verified))
.map(|sr| sr.id.as_str())
.collect();
let ids: Vec<&str> = srs.iter().map(|sr| sr.id.as_str()).collect();
out.push(" sign-off basis:".into());
out.push(format!(
" implemented by {} safety requirement(s): {}",
total,
ids.join(", ")
));
out.push(format!(
" safety requirements Verified: {}/{}",
verified.len(),
total
));
let concluded = sf
.verification
.as_ref()
.map(|v| v.verdict.is_some())
.unwrap_or(false);
if !pending.is_empty() {
out.push(format!(
" \u{21d2} NOT yet signable — these safety requirements are not Verified: {}",
pending.join(", ")
));
} else if concluded {
out.push(
" \u{21d2} the realizing safety requirements adequately implement this function and are all"
.into(),
);
out.push(
" Verified; with the concluded dossier above, the function is ready for human co-sign."
.into(),
);
} else {
out.push(
" \u{21d2} the realizing safety requirements are all Verified — conclude the dossier".into(),
);
out.push(format!(
" (`req verification conclude {} --statement \"...\" --promote`), then co-sign.",
sf.id
));
}
out
}
pub fn hazard_signoff_lines(project: &Project, hz: &Hazard) -> Vec<String> {
let sfs = project.mitigating_sfs(&hz.id);
let mut out = Vec::new();
if sfs.is_empty() {
return out;
}
let sf_total = sfs.len();
let sf_verified = sfs
.iter()
.filter(|sf| matches!(sf.status, SafetyFunctionStatus::Verified))
.count();
let mut sr_total = 0usize;
let mut sr_verified = 0usize;
let mut pending: Vec<&str> = Vec::new();
for sf in &sfs {
let srs = project.realizing_srs(&sf.id);
sr_total += srs.len();
sr_verified += srs
.iter()
.filter(|sr| matches!(sr.status, Status::Verified))
.count();
if !matches!(sf.status, SafetyFunctionStatus::Verified) {
pending.push(sf.id.as_str());
}
}
let ids: Vec<&str> = sfs.iter().map(|sf| sf.id.as_str()).collect();
out.push(" sign-off basis:".into());
out.push(format!(
" mitigated by {} safety function(s): {}",
sf_total,
ids.join(", ")
));
out.push(format!(
" safety functions Verified: {}/{} (each implemented by its realizing SRs)",
sf_verified, sf_total
));
out.push(format!(
" realizing SRs Verified: {}/{}",
sr_verified, sr_total
));
let concluded = hz
.adequacy
.as_ref()
.map(|a| matches!(a.verdict, Some(crate::model::AdequacyVerdict::Adequate)))
.unwrap_or(false);
if !pending.is_empty() {
out.push(format!(
" \u{21d2} NOT yet signable — these mitigations are not Verified: {}",
pending.join(", ")
));
} else if concluded {
out.push(
" \u{21d2} every specified safety function is Verified and implemented by Verified safety"
.into(),
);
out.push(
" requirements; with the residual-risk argument above, the hazard is ready for human co-sign."
.into(),
);
} else {
out.push(
" \u{21d2} every mitigating safety function is Verified — conclude the adequacy dossier".into(),
);
out.push(format!(
" (`req hazard adequacy conclude {} --statement \"...\"`), then co-sign.",
hz.id
));
}
out
}
pub fn run_sf(cmd: SfCmd, file: &Option<PathBuf>) -> Result<()> {
match cmd {
SfCmd::Add(a) => {
super::safety_gov::ensure_enabled(file)?;
sf_add(a, file)
}
SfCmd::Update(a) => {
super::safety_gov::ensure_enabled(file)?;
sf_update(a, file)
}
SfCmd::Mitigate(a) => {
super::safety_gov::ensure_enabled(file)?;
sf_mitigate(a, file)
}
SfCmd::List(a) => sf_list(a, file),
SfCmd::Show(a) => sf_show(a, file),
}
}
fn sf_add(args: SfAddArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let now = Utc::now();
let mut links = Vec::new();
for raw in &args.mitigates {
let hid = resolve_haz(&project, raw)?;
links.push(Link {
kind: LinkKind::Mitigates,
target: hid,
});
}
let status = if links.is_empty() {
SafetyFunctionStatus::Proposed
} else {
SafetyFunctionStatus::Allocated
};
let id = project.allocate_sf_id();
let sf = SafetyFunction {
id: id.clone(),
title: args.title,
description: args.description,
safe_state: args.safe_state,
status,
tags: args.tag,
links: links.clone(),
created: now,
updated: now,
history: vec![super::history("created", None)],
verification: None,
extra: Default::default(),
};
project.safety_functions.insert(id.clone(), sf.clone());
for l in &links {
if let Some(h) = project.hazards.get_mut(&l.target) {
if matches!(h.status, HazardStatus::Identified | HazardStatus::Assessed) {
h.status = HazardStatus::Mitigated;
h.updated = now;
h.history
.push(super::history(format!("mitigated by {}", id), None));
}
}
}
project.updated = now;
let alloc = project.allocated_sil(&sf);
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&sf)?);
} else {
println!("Added {}", id);
println!(" allocated SIL: {}", sil_str(alloc));
}
Ok(())
}
fn sf_list(args: SfListArgs, file: &Option<PathBuf>) -> Result<()> {
let (_path, project) = load_resolved(file)?;
let status_filter: Option<SafetyFunctionStatus> = args.status.map(Into::into);
let sil_filter = args.sil.as_deref().map(|s| s.to_uppercase());
let mut rows: Vec<&SafetyFunction> = project
.safety_functions
.values()
.filter(|sf| status_filter.map(|s| sf.status == s).unwrap_or(true))
.filter(|sf| {
sil_filter
.as_ref()
.map(|want| {
project
.allocated_sil(sf)
.map(|s| s.as_str().to_uppercase() == *want)
.unwrap_or(false)
})
.unwrap_or(true)
})
.filter(|sf| {
if !args.unrealized {
return true;
}
!project
.safety_requirements
.values()
.any(|sr| realizes(sr, &sf.id))
})
.collect();
rows.sort_by(|a, b| a.id.cmp(&b.id));
if args.json {
println!("{}", serde_json::to_string_pretty(&rows)?);
return Ok(());
}
if rows.is_empty() {
println!("No safety functions.");
return Ok(());
}
println!("{:<8} {:<6} {:<12} TITLE", "ID", "SIL", "STATUS");
for sf in rows {
println!(
"{:<8} {:<6} {:<12} {}",
sf.id,
sil_str(project.allocated_sil(sf)),
sf.status.as_str(),
sf.title
);
}
Ok(())
}
fn sf_show(args: SfShowArgs, file: &Option<PathBuf>) -> Result<()> {
let (_path, project) = load_resolved(file)?;
let id = resolve_sf(&project, &args.id)?;
let sf = &project.safety_functions[&id];
if args.json {
println!("{}", serde_json::to_string_pretty(sf)?);
return Ok(());
}
println!("{} {}", sf.id, sf.title);
println!(" status: {}", sf.status.as_str());
if !sf.description.is_empty() {
println!(" description: {}", sf.description);
}
if !sf.safe_state.is_empty() {
println!(" safe state: {}", sf.safe_state);
}
println!(" allocated SIL: {}", sil_str(project.allocated_sil(sf)));
let hazards: Vec<&Link> = sf
.links
.iter()
.filter(|l| l.kind == LinkKind::Mitigates)
.collect();
if hazards.is_empty() {
println!(" mitigates: (no hazard)");
} else {
println!(" mitigates:");
for l in hazards {
let title = project
.hazards
.get(&l.target)
.map(|h| h.title.as_str())
.unwrap_or("<missing>");
println!(" {} — {}", l.target, title);
}
}
let srs: Vec<&SafetyRequirement> = project
.safety_requirements
.values()
.filter(|sr| realizes(sr, &sf.id))
.collect();
if srs.is_empty() {
println!(" realized by: (none)");
} else {
println!(" realized by:");
for sr in srs {
println!(" {} — {} [{}]", sr.id, sr.title, sr.status.as_str());
}
}
match &sf.verification {
None => println!(
" verification: (none — `req verification plan {}` to record how this function \
achieves its safe state)",
sf.id
),
Some(v) => {
let verdict = v
.verdict
.map(|o| o.as_str().to_uppercase())
.unwrap_or_else(|| "pending".to_string());
let cosign = if v.human_confirmation.is_some() {
"human co-signed"
} else {
"awaiting human co-sign"
};
println!(" verification: verdict {} ({})", verdict, cosign);
if !v.plan.is_empty() {
println!(" plan: {}", v.plan);
}
if let Some(a) = &v.analysis {
println!(" analysis: {} — {}", a.outcome.as_str(), a.summary);
}
if let Some(t) = &v.testing {
println!(" testing: {} — {}", t.outcome.as_str(), t.summary);
}
for c in &v.coverage {
let sb = project
.safety_requirements
.get(&c.target)
.map(|sr| sr.status.as_str())
.unwrap_or("?");
println!(" covers {} [{}]: {}", c.target, sb, c.note);
}
if let Some(s) = &v.statement {
println!(" statement: {}", s);
}
}
}
for line in sf_signoff_lines(&project, sf) {
println!("{}", line);
}
println!(" scope: {}", ACHIEVED_INTEGRITY_STAMP);
println!("\nRun `req trace {}` for the full safety case.", sf.id);
Ok(())
}
fn sf_update(args: SfUpdateArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_sf(&project, &args.id)?;
let now = Utc::now();
{
let sf = project.safety_functions.get_mut(&id).unwrap();
if let Some(t) = args.title {
sf.title = t;
}
if let Some(d) = args.description {
sf.description = d;
}
if let Some(s) = args.safe_state {
sf.safe_state = s;
}
if let Some(s) = args.status {
let next: SafetyFunctionStatus = s.into();
if matches!(
next,
SafetyFunctionStatus::Implemented | SafetyFunctionStatus::Verified
) {
return Err(anyhow!(
"{} cannot be set to {} directly — a safety function earns these through its \
verification dossier: `req verification plan {} ...` → analysis → test → \
conclude --promote (reaches Implemented), then a human runs \
`req verification confirm {}` to co-sign it to Verified.",
id,
next.as_str(),
id,
id
));
}
sf.status = next;
}
for t in &args.add_tag {
if !sf.tags.contains(t) {
sf.tags.push(t.clone());
}
}
sf.tags.retain(|t| !args.remove_tag.contains(t));
sf.updated = now;
sf.history
.push(super::history("updated", args.reason.clone()));
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&project.safety_functions[&id])?
);
} else {
println!("Updated {}", id);
}
Ok(())
}
fn sf_mitigate(args: SfMitigateArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let sf_id = resolve_sf(&project, &args.sf)?;
let haz_id = resolve_haz(&project, &args.hazard)?;
let now = Utc::now();
{
let sf = project.safety_functions.get_mut(&sf_id).unwrap();
if args.remove {
sf.links
.retain(|l| !(l.kind == LinkKind::Mitigates && l.target == haz_id));
sf.history.push(super::history(
format!("unlinked mitigates {}", haz_id),
None,
));
} else if mitigates(sf, &haz_id) {
return Err(anyhow!("{} already mitigates {}", sf_id, haz_id));
} else {
sf.links.push(Link {
kind: LinkKind::Mitigates,
target: haz_id.clone(),
});
if matches!(sf.status, SafetyFunctionStatus::Proposed) {
sf.status = SafetyFunctionStatus::Allocated;
}
sf.history
.push(super::history(format!("mitigates {}", haz_id), None));
}
sf.updated = now;
}
if !args.remove {
if let Some(h) = project.hazards.get_mut(&haz_id) {
if matches!(h.status, HazardStatus::Identified | HazardStatus::Assessed) {
h.status = HazardStatus::Mitigated;
h.updated = now;
h.history
.push(super::history(format!("mitigated by {}", sf_id), None));
}
}
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&project.safety_functions[&sf_id])?
);
} else if args.remove {
println!("{} no longer mitigates {}", sf_id, haz_id);
} else {
println!("{} mitigates {}", sf_id, haz_id);
}
Ok(())
}
pub fn run_sreq(cmd: SreqCmd, file: &Option<PathBuf>) -> Result<()> {
match cmd {
SreqCmd::Add(a) => {
super::safety_gov::ensure_enabled(file)?;
sreq_add(a, file)
}
SreqCmd::Update(a) => {
super::safety_gov::ensure_enabled(file)?;
sreq_update(a, file)
}
SreqCmd::Realize(a) => {
super::safety_gov::ensure_enabled(file)?;
sreq_realize(a, file)
}
SreqCmd::Verify(a) => {
super::safety_gov::ensure_enabled(file)?;
sreq_verify(a, file)
}
SreqCmd::List(a) => sreq_list(a, file),
SreqCmd::Show(a) => sreq_show(a, file),
}
}
fn sreq_add(args: SreqAddArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let now = Utc::now();
let mut links = Vec::new();
for raw in &args.realizes {
let sfid = resolve_sf(&project, raw)?;
links.push(Link {
kind: LinkKind::Realizes,
target: sfid,
});
}
let id = project.allocate_sr_id();
let sr = SafetyRequirement {
id: id.clone(),
title: args.title,
statement: args.statement,
rationale: args.rationale,
acceptance: args.acceptance,
priority: args.priority.into(),
status: Status::Draft,
tags: args.tag,
links,
created: now,
updated: now,
history: vec![super::history("created", None)],
tests: Vec::new(),
verification: None,
walkthrough: None,
extra: Default::default(),
};
project.safety_requirements.insert(id.clone(), sr.clone());
project.updated = now;
let sil = project.inherited_sil(&sr);
storage::save(&path, &project)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&sr)?);
} else {
println!("Added {}", id);
println!(" inherits SIL: {}", sil_str(sil));
println!(
"Next: add `// {}:` to the source that implements this, then \
`req sreq verify {} --by automated ...`.",
id, id
);
}
Ok(())
}
fn sreq_list(args: SreqListArgs, file: &Option<PathBuf>) -> Result<()> {
let (_path, project) = load_resolved(file)?;
let status_filter: Option<Status> = args.status.map(Into::into);
let sil_filter = args.sil.as_deref().map(|s| s.to_uppercase());
let mut rows: Vec<&SafetyRequirement> = project
.safety_requirements
.values()
.filter(|sr| status_filter.map(|s| sr.status == s).unwrap_or(true))
.filter(|sr| {
sil_filter
.as_ref()
.map(|want| {
project
.inherited_sil(sr)
.map(|s| s.as_str().to_uppercase() == *want)
.unwrap_or(false)
})
.unwrap_or(true)
})
.filter(|sr| !args.unverified || !matches!(sr.status, Status::Verified))
.collect();
rows.sort_by(|a, b| a.id.cmp(&b.id));
if args.json {
println!("{}", serde_json::to_string_pretty(&rows)?);
return Ok(());
}
if rows.is_empty() {
println!("No safety requirements.");
return Ok(());
}
println!("{:<8} {:<6} {:<14} TITLE", "ID", "SIL", "STATUS");
for sr in rows {
let status = if super::provenance::sr_awaiting_cosign(sr) {
"awaiting-cosign"
} else {
sr.status.as_str()
};
println!(
"{:<8} {:<6} {:<14} {}",
sr.id,
sil_str(project.inherited_sil(sr)),
status,
sr.title
);
}
Ok(())
}
fn sreq_show(args: SreqShowArgs, file: &Option<PathBuf>) -> Result<()> {
let (_path, project) = load_resolved(file)?;
let id = resolve_sr(&project, &args.id)?;
let sr = &project.safety_requirements[&id];
if args.json {
println!("{}", serde_json::to_string_pretty(sr)?);
return Ok(());
}
println!("{} {}", sr.id, sr.title);
if super::provenance::sr_awaiting_cosign(sr) {
println!(
" status: {} (awaiting human co-sign — `req verification confirm {}`)",
sr.status.as_str(),
sr.id
);
} else {
println!(" status: {}", sr.status.as_str());
}
println!(" priority: {}", sr.priority.as_str());
println!(" inherits SIL: {}", sil_str(project.inherited_sil(sr)));
println!(" statement: {}", sr.statement);
println!(" rationale: {}", sr.rationale);
if !sr.acceptance.is_empty() {
println!(" acceptance:");
for (i, a) in sr.acceptance.iter().enumerate() {
println!(" {}. {}", i + 1, a);
}
}
let sfs: Vec<&Link> = sr
.links
.iter()
.filter(|l| l.kind == LinkKind::Realizes)
.collect();
if sfs.is_empty() {
println!(" realizes: (no safety function)");
} else {
println!(" realizes:");
for l in sfs {
let title = project
.safety_functions
.get(&l.target)
.map(|sf| sf.title.as_str())
.unwrap_or("<missing>");
println!(" {} — {}", l.target, title);
}
}
match sr.tests.last() {
Some(t) => {
println!(
" evidence: {} · {} · {}",
t.kind.as_str(),
if t.commit.is_empty() {
"—"
} else {
&t.commit[..t.commit.len().min(8)]
},
t.outcome.as_str()
);
let current = project.inherited_sil(sr);
if let Some(at) = t.sil_at_verification {
let cur = current.map(|s| s.as_str()).unwrap_or("—");
let flag = match current {
Some(c) if c.rank() > at.rank() => " ⚠ inherited SIL rose since verification",
_ => "",
};
println!(
" evidence SIL: {} (verified at) · {} (current){}",
at.as_str(),
cur,
flag
);
}
}
None => println!(" evidence: none"),
}
println!(" scope: {}", ACHIEVED_INTEGRITY_STAMP);
println!("\nRun `req trace {}` for the full safety case.", sr.id);
Ok(())
}
fn sreq_update(args: SreqUpdateArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_sr(&project, &args.id)?;
let now = Utc::now();
let min_force_reason_len = project.min_force_reason_len();
{
let sr = project.safety_requirements.get_mut(&id).unwrap();
if let Some(t) = args.title {
sr.title = t;
}
if let Some(s) = args.statement {
sr.statement = s;
}
if let Some(r) = args.rationale {
sr.rationale = r;
}
if let Some(a) = args.acceptance {
sr.acceptance = a;
}
for a in &args.add_acceptance {
sr.acceptance.push(a.clone());
}
if let Some(p) = args.priority {
sr.priority = p.into();
}
if let Some(s) = args.status {
let to: crate::model::Status = s.into();
if sr.status != to {
if !crate::model::is_natural_transition(sr.status, to) && !args.force {
return Err(anyhow!(
"{} -> {} is an irregular transition for {}; pass --force \
--reason \"...\" to record an explicit override.",
sr.status.as_str(),
to.as_str(),
id
));
}
if !crate::model::is_natural_transition(sr.status, to) {
super::ensure_force_reason(&args.reason, min_force_reason_len)?;
}
sr.status = to;
}
}
for t in &args.add_tag {
if !sr.tags.contains(t) {
sr.tags.push(t.clone());
}
}
sr.tags.retain(|t| !args.remove_tag.contains(t));
sr.updated = now;
sr.history
.push(super::history("updated", args.reason.clone()));
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&project.safety_requirements[&id])?
);
} else {
println!("Updated {}", id);
}
Ok(())
}
fn sreq_realize(args: SreqRealizeArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let sr_id = resolve_sr(&project, &args.sreq)?;
let sf_id = resolve_sf(&project, &args.sf)?;
let now = Utc::now();
{
let sr = project.safety_requirements.get_mut(&sr_id).unwrap();
if args.remove {
sr.links
.retain(|l| !(l.kind == LinkKind::Realizes && l.target == sf_id));
sr.history
.push(super::history(format!("unlinked realizes {}", sf_id), None));
} else if realizes(sr, &sf_id) {
return Err(anyhow!("{} already realizes {}", sr_id, sf_id));
} else {
sr.links.push(Link {
kind: LinkKind::Realizes,
target: sf_id.clone(),
});
sr.history
.push(super::history(format!("realizes {}", sf_id), None));
}
sr.updated = now;
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&project.safety_requirements[&sr_id])?
);
} else if args.remove {
println!("{} no longer realizes {}", sr_id, sf_id);
} else {
println!("{} realizes {}", sr_id, sf_id);
}
Ok(())
}
fn sreq_verify(args: SreqVerifyArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
let id = resolve_sr(&project, &args.id)?;
let kind: EvidenceKind = args.by.into();
let inherited = project.inherited_sil(&project.safety_requirements[&id]);
let status = project.safety_requirements[&id].status;
let mut gate_exception = false;
if args.promote {
super::verification::gate_safety_requirement(&project.safety_requirements[&id])?;
let ladder_ok = matches!(status, Status::Implemented | Status::Verified);
if !ladder_ok && !args.force {
return Err(anyhow!(
"{} is {} — promoting straight to Verified is irregular. Advance it to \
Implemented first, or pass --force --reason \"...\" to record the override.",
id,
status.as_str()
));
}
if let Some(sil) = inherited {
if sil.rank() >= Sil::Sil3.rank() && matches!(kind, EvidenceKind::Inspection) {
if args.force {
gate_exception = true;
} else {
return Err(anyhow!(
"SIL-rigour gate: {} inherits {} — it cannot be verified on \
inspection-only evidence. Provide automated or composition \
evidence, or pass --force --reason \"...\" to record an audited \
exception.",
id,
sil.as_str()
));
}
}
}
}
let now = Utc::now();
let mut notes = args.notes.clone();
if !args.cites.is_empty() {
notes = format!("cites {} — {}", args.cites.join(", "), notes);
}
if let Some(reason) = args.reason.as_deref().filter(|_| args.force) {
notes = format!("[override: {}] {}", reason, notes);
}
let record = TestRecord {
at: now,
actor: super::current_actor(),
commit: git_head(),
outcome: TestOutcome::Pass,
notes,
kind,
content_hash: None,
linked_files: None,
sil_gate_exception: gate_exception,
sil_at_verification: inherited,
external: None,
};
{
let sr = project.safety_requirements.get_mut(&id).unwrap();
sr.tests.push(record);
if args.promote {
sr.status = Status::Verified;
}
sr.updated = now;
sr.history.push(super::history(
if args.promote {
"verified (promoted)"
} else {
"evidence recorded"
},
args.reason.clone(),
));
}
project.updated = now;
storage::save(&path, &project)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&project.safety_requirements[&id])?
);
} else {
println!(
"Recorded {} evidence for {}{}",
kind.as_str(),
id,
if args.promote { " → Verified" } else { "" }
);
}
Ok(())
}
pub fn run_trace(args: TraceArgs, file: &Option<PathBuf>) -> Result<()> {
let (_path, project) = load_resolved(file)?;
let raw = args.id.trim().to_uppercase();
if raw.starts_with("HAZ") {
let id = resolve_haz(&project, &args.id)?;
trace_hazard(&project, &id, args.json)
} else if raw.starts_with("SF") {
let id = resolve_sf(&project, &args.id)?;
trace_from_sf(&project, &id, args.json)
} else if raw.starts_with("SR") {
let id = resolve_sr(&project, &args.id)?;
trace_from_sr(&project, &id, args.json)
} else {
Err(anyhow!("trace expects a HAZ-/SF-/SR- id; got {}", args.id))
}
}
struct Verdict {
required: Option<Sil>,
allocated: Option<Sil>,
sr_total: usize,
sr_verified: usize,
adequate: bool,
complete: bool,
blocking: Vec<String>,
}
fn assess_hazard(project: &Project, haz_id: &str) -> Verdict {
let h = &project.hazards[haz_id];
let required = project.required_sil(h);
let sfs: Vec<&SafetyFunction> = project
.safety_functions
.values()
.filter(|sf| mitigates(sf, haz_id))
.collect();
let allocated = sfs
.iter()
.filter_map(|sf| project.allocated_sil(sf))
.max_by_key(|s| s.rank());
let adequate = match (required, allocated) {
(Some(r), Some(a)) => a.rank() >= r.rank(),
(Some(_), None) => false,
(None, _) => true, };
let mut sr_total = 0;
let mut sr_verified = 0;
let mut blocking = Vec::new();
use crate::commands::provenance::{classify, sr_awaiting_cosign, Provenance};
let root = std::path::Path::new(".");
for sf in &sfs {
for sr in project
.safety_requirements
.values()
.filter(|sr| realizes(sr, &sf.id))
{
sr_total += 1;
let standing = classify(sr.verification.as_ref(), Some(root), &sr.id);
let confirmed = sr
.verification
.as_ref()
.and_then(|v| v.human_confirmation.as_ref())
.is_some();
let clean = matches!(sr.status, Status::Verified)
&& confirmed
&& standing == Provenance::Genuine;
if clean {
sr_verified += 1;
} else {
let why = if sr_awaiting_cosign(sr)
|| (matches!(sr.status, Status::Verified) && !confirmed)
{
"awaiting human co-sign"
} else if standing == Provenance::Stale {
"stale"
} else if !matches!(sr.status, Status::Verified) {
"not verified"
} else {
"not genuinely verified"
};
blocking.push(format!("{} {}", sr.id, why));
}
}
}
if sfs.is_empty() {
blocking.push("no mitigating safety function".to_string());
} else if sr_total == 0 {
blocking.push("no realizing safety requirement".to_string());
}
let complete = adequate && blocking.is_empty();
Verdict {
required,
allocated,
sr_total,
sr_verified,
adequate,
complete,
blocking,
}
}
fn trace_hazard(project: &Project, haz_id: &str, json: bool) -> Result<()> {
let h = &project.hazards[haz_id];
let v = assess_hazard(project, haz_id);
if json {
let chain: Vec<_> = project
.safety_functions
.values()
.filter(|sf| mitigates(sf, haz_id))
.map(|sf| {
let srs: Vec<_> = project
.safety_requirements
.values()
.filter(|sr| realizes(sr, &sf.id))
.map(|sr| {
serde_json::json!({
"id": sr.id,
"title": sr.title,
"status": sr.status.as_str(),
"inherited_sil": project.inherited_sil(sr).map(|s| s.as_str()),
"verification": sr.verification,
"walkthrough": sr.walkthrough,
})
})
.collect();
serde_json::json!({
"id": sf.id,
"title": sf.title,
"status": sf.status.as_str(),
"allocated_sil": project.allocated_sil(sf).map(|s| s.as_str()),
"safety_requirements": srs,
})
})
.collect();
let out = serde_json::json!({
"hazard": h,
"required_sil": v.required.map(|s| s.as_str()),
"allocated_sil": v.allocated.map(|s| s.as_str()),
"adequate": v.adequate,
"complete": v.complete,
"chain": chain,
"safety_requirements": { "total": v.sr_total, "verified": v.sr_verified },
"blocking": v.blocking,
});
println!("{}", serde_json::to_string_pretty(&out)?);
return Ok(());
}
println!("{} {} [{}]", h.id, h.title, h.status.as_str());
println!(" harm: {}", h.harm);
if !h.operating_context.is_empty() {
println!(" context: {}", h.operating_context);
}
match (h.consequence, h.frequency, h.avoidance, h.probability) {
(Some(c), Some(f), Some(p), Some(w)) => println!(
" risk: {} · {} · {} · {} ──► required {}",
c.as_str(),
f.as_str(),
p.as_str(),
w.as_str(),
sil_str(v.required)
),
_ => println!(" risk: not yet assessed"),
}
if let Some(a) = &h.adequacy {
let standing = match (a.verdict, a.human_confirmation.is_some()) {
(Some(vd), true) => format!("{} (human co-signed)", vd.as_str()),
(Some(vd), false) => format!("{} (awaiting human co-sign)", vd.as_str()),
(None, _) => "in progress (not concluded)".to_string(),
};
println!(" adequacy: {}", standing);
for c in &a.coverage {
let sb = project
.safety_functions
.get(&c.target)
.map(|sf| sf.status.as_str())
.unwrap_or("?");
println!(" covers {} [{}]: {}", c.target, sb, c.note);
}
if !a.statement.is_empty() {
println!(" residual: {}", a.statement);
}
}
let sfs: Vec<&SafetyFunction> = project
.safety_functions
.values()
.filter(|sf| mitigates(sf, haz_id))
.collect();
if sfs.is_empty() {
println!(" │");
println!(" └─ mitigated by ── (none)");
}
for sf in &sfs {
let alloc = project.allocated_sil(sf);
let meets = match (v.required, alloc) {
(Some(r), Some(a)) => {
if a.rank() >= r.rank() {
"✓ meets required"
} else {
"✗ below required"
}
}
_ => "",
};
println!(" │");
println!(" └─ mitigated by ─────────────────────────────────");
println!(" {} {} [{}]", sf.id, sf.title, sf.status.as_str());
if !sf.safe_state.is_empty() {
println!(" safe state: {}", sf.safe_state);
}
println!(" allocated SIL: {} {}", sil_str(alloc), meets);
if let Some(sfv) = &sf.verification {
for c in &sfv.coverage {
let sb = project
.safety_requirements
.get(&c.target)
.map(|sr| sr.status.as_str())
.unwrap_or("?");
println!(" covers {} [{}]: {}", c.target, sb, c.note);
}
}
let srs: Vec<&SafetyRequirement> = project
.safety_requirements
.values()
.filter(|sr| realizes(sr, &sf.id))
.collect();
if srs.is_empty() {
println!(" └─ realized by ── (none)");
} else {
println!(" └─ realized by ───────────────────────");
}
for sr in srs {
let mark = if matches!(sr.status, Status::Verified) {
"✓"
} else {
"⚠"
};
println!(
" {} {} [{}] {}",
sr.id,
sr.title,
sr.status.as_str(),
mark
);
println!(
" inherits SIL {}",
sil_str(project.inherited_sil(sr))
);
match sr.tests.last() {
Some(t) => println!(
" evidence: {} · {}",
t.kind.as_str(),
if t.commit.is_empty() {
"—".to_string()
} else {
t.commit[..t.commit.len().min(8)].to_string()
}
),
None => println!(" evidence: none ✗ unverified"),
}
match &sr.verification {
Some(val) => {
let verdict = val.verdict.map(|o| o.as_str()).unwrap_or("open");
let a = val
.analysis
.as_ref()
.map(|x| x.outcome.as_str())
.unwrap_or("—");
let t = val
.testing
.as_ref()
.map(|x| x.outcome.as_str())
.unwrap_or("—");
println!(" dossier: verdict {verdict} (analysis {a}, testing {t})");
match &val.human_confirmation {
Some(hc) => println!(
" human-confirmed: {} @ {}",
hc.actor,
hc.at.format("%Y-%m-%d %H:%M UTC")
),
None => println!(
" human-confirmed: ⚠ awaiting human confirmation (REQ-V-0034)"
),
}
if let Some(st) = &val.statement {
println!(" statement: {st}");
}
}
None => println!(" dossier: (none recorded)"),
}
match &sr.walkthrough {
Some(a) if a.objected => {
println!(" walkthrough: ✗ objection by {}", a.reviewer)
}
Some(a) => println!(
" walkthrough: ✓ acknowledged by {} at {} (commit {})",
a.reviewer,
a.at.format("%Y-%m-%d %H:%M UTC"),
if a.commit.is_empty() {
"—".to_string()
} else {
a.commit[..a.commit.len().min(8)].to_string()
}
),
None => println!(" walkthrough: ▷ not yet acknowledged"),
}
}
}
println!();
let verdict = if v.complete {
"✓ chain linked and verified"
} else {
"⚠ chain incomplete"
};
println!(" TRACE STATUS: {}", verdict);
println!(" scope: traceability + verification only — NOT a residual-risk verification");
println!(
" SIL allocation: required {} — allocated {} {}",
sil_str(v.required),
sil_str(v.allocated),
if v.adequate {
"✓ allocation ≥ required"
} else {
"✗ allocation below required"
}
);
println!(
" safety requirements: {} verified of {}",
v.sr_verified, v.sr_total
);
if !v.blocking.is_empty() {
println!(" blocking: {}", v.blocking.join("; "));
}
if matches!(v.required, Some(Sil::B)) || matches!(v.allocated, Some(Sil::B)) {
println!(
" ⚠ SIL 'b': a single E/E/PE safety-related system is not sufficient for this \
risk — independent/additional risk-reduction measures are required (an \
architecture decision, beyond what req's verification gate checks)."
);
}
println!();
println!("{}", SAFETY_DISCLAIMER_LINE);
Ok(())
}
pub const SAFETY_DISCLAIMER_LINE: &str =
" ⚠ req computes a candidate SIL from your inputs and checks traceability only. \
It is not qualified per IEC 61508-3 §7.4.4 and does not assure risk reduction — \
the safety determination remains yours. See `req help safety`.";
fn trace_from_sf(project: &Project, sf_id: &str, json: bool) -> Result<()> {
let sf = &project.safety_functions[sf_id];
let hazards: Vec<String> = sf
.links
.iter()
.filter(|l| l.kind == LinkKind::Mitigates)
.map(|l| l.target.clone())
.filter(|t| project.hazards.contains_key(t))
.collect();
if hazards.is_empty() {
if json {
println!("{}", serde_json::to_string_pretty(sf)?);
} else {
println!(
"{} mitigates no hazard yet — nothing to trace upward.",
sf_id
);
println!(
"Run `req sf show {}` for its realizing requirements.",
sf_id
);
}
return Ok(());
}
for (i, hid) in hazards.iter().enumerate() {
if i > 0 {
println!();
}
trace_hazard(project, hid, json)?;
}
Ok(())
}
fn trace_from_sr(project: &Project, sr_id: &str, json: bool) -> Result<()> {
let sr = &project.safety_requirements[sr_id];
let sfs: Vec<String> = sr
.links
.iter()
.filter(|l| l.kind == LinkKind::Realizes)
.map(|l| l.target.clone())
.filter(|t| project.safety_functions.contains_key(t))
.collect();
if sfs.is_empty() {
if json {
println!("{}", serde_json::to_string_pretty(sr)?);
} else {
println!(
"{} realizes no safety function yet — nothing to trace upward.",
sr_id
);
}
return Ok(());
}
for (i, sfid) in sfs.iter().enumerate() {
if i > 0 {
println!();
}
trace_from_sf(project, sfid, json)?;
}
Ok(())
}