use std::path::Path;
use crate::model::{Project, Status, TestOutcome, Verification};
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Provenance {
Ungated,
ExemptBackfilled,
ExemptNoDossier,
Stale,
Unconfirmed,
Genuine,
}
impl Provenance {
pub fn as_str(self) -> &'static str {
match self {
Provenance::Ungated => "ungated",
Provenance::ExemptBackfilled => "exempt:backfilled",
Provenance::ExemptNoDossier => "exempt:no-dossier",
Provenance::Stale => "stale",
Provenance::Unconfirmed => "unconfirmed",
Provenance::Genuine => "genuine",
}
}
pub fn is_genuine(self) -> bool {
matches!(self, Provenance::Genuine)
}
}
pub fn classify(v: Option<&Verification>, source_root: Option<&Path>, id: &str) -> Provenance {
let v = match v {
None => return Provenance::Ungated,
Some(v) => v,
};
if v.exempt {
return match v.exemption_kind {
Some(crate::model::ExemptionKind::NoDossier) => Provenance::ExemptNoDossier,
Some(crate::model::ExemptionKind::Backfilled) => Provenance::ExemptBackfilled,
None => Provenance::ExemptBackfilled,
};
}
let genuine = matches!(v.verdict, Some(TestOutcome::Pass))
&& v.analysis.is_some()
&& v.testing.is_some()
&& v.statement.is_some();
if !genuine {
return Provenance::Ungated;
}
if let (Some(root), Some(hash)) = (source_root, v.content_hash.as_deref()) {
let s = crate::commands::test_cmd::staleness_by_content(
hash,
v.linked_files.as_ref(),
id,
root,
);
if matches!(s, crate::commands::test_cmd::Staleness::Stale { .. }) {
return Provenance::Stale;
}
}
Provenance::Genuine
}
pub fn sr_awaiting_cosign(sr: &crate::model::SafetyRequirement) -> bool {
matches!(sr.status, Status::Implemented)
&& classify(sr.verification.as_ref(), None, &sr.id) == Provenance::Genuine
&& sr
.verification
.as_ref()
.and_then(|v| v.human_confirmation.as_ref())
.is_none()
}
pub fn sr_standing(
sr: &crate::model::SafetyRequirement,
source_root: Option<&std::path::Path>,
) -> &'static str {
if matches!(sr.status, Status::Obsolete) {
return "obsolete";
}
if sr_awaiting_cosign(sr) {
return "awaiting-cosign";
}
if matches!(sr.status, Status::Verified) {
return match classify(sr.verification.as_ref(), source_root, &sr.id) {
Provenance::Genuine => {
if sr
.verification
.as_ref()
.and_then(|v| v.human_confirmation.as_ref())
.is_some()
{
"verified"
} else {
"unconfirmed"
}
}
Provenance::Stale => "stale",
_ => "ungated",
};
}
match sr.verification.as_ref() {
None => "no-dossier",
Some(v) if v.analysis.is_none() => "plan-only",
Some(v) if v.testing.is_none() => "analysed-untested",
Some(_) => "pending-conclusion",
}
}
pub struct ProvenanceRow {
pub id: String,
pub family: &'static str,
pub provenance: Provenance,
pub sil: Option<String>,
}
pub fn provenance_report(project: &Project, source_root: Option<&Path>) -> Vec<ProvenanceRow> {
let mut rows = Vec::new();
let mut reqs: Vec<_> = project
.requirements
.values()
.filter(|r| matches!(r.status, Status::Verified))
.collect();
reqs.sort_by(|a, b| a.id.cmp(&b.id));
for r in reqs {
rows.push(ProvenanceRow {
id: r.id.clone(),
family: "requirement",
provenance: classify(r.verification.as_ref(), source_root, &r.id),
sil: None,
});
}
let mut srs: Vec<_> = project
.safety_requirements
.values()
.filter(|sr| matches!(sr.status, Status::Verified))
.collect();
srs.sort_by(|a, b| a.id.cmp(&b.id));
for sr in srs {
let mut provenance = classify(sr.verification.as_ref(), source_root, &sr.id);
if provenance == Provenance::Genuine
&& sr
.verification
.as_ref()
.map(|v| v.human_confirmation.is_none())
.unwrap_or(false)
{
provenance = Provenance::Unconfirmed;
}
rows.push(ProvenanceRow {
id: sr.id.clone(),
family: "safety-requirement",
provenance,
sil: project.inherited_sil(sr).map(|s| s.as_str().to_string()),
});
}
rows
}