use std::path::Path;
use serde::{Deserialize, Serialize};
use super::{FilesystemAuditContext, FrameState, SidecarLoad, WorkVerdict, load_sidecar};
use crate::scan::ScanReport;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StepState {
Attested,
Unattested,
Unevaluable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OutOfFrameCause {
UnknownWhoRef,
MissingWorkStep,
UnparseableFrame,
UnresolvableRef,
}
impl OutOfFrameCause {
#[must_use]
pub const fn remedy(self) -> &'static str {
match self {
Self::UnknownWhoRef => {
"scaffold + sign the .attest/<item>.json sidecar so the named \
who-ref's attestation is readable"
},
Self::MissingWorkStep => {
"declare the missing who-step (filled_by / ordered_by / triaged_by \
/ closure) — an empty work-need has nothing to attest"
},
Self::UnparseableFrame => {
"fix the malformed frame date (due / until / runs_until / \
re_triage_due must be ISO-8601 YYYY-MM-DD)"
},
Self::UnresolvableRef => {
"fix the dangling priority_order code-site reference (or, for a \
cross-crate target, await multi-crate Layer-2 resolution)"
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepDetail {
pub role: String,
pub reference: String,
pub state: StepState,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrescriptiveVerdict {
pub declaration: crate::scan::PrescriptiveDeclaration,
pub verdict: WorkVerdict,
pub steps: Vec<StepDetail>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocking: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub out_of_frame_cause: Option<OutOfFrameCause>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PrescriptiveAuditReport {
pub verdicts: Vec<PrescriptiveVerdict>,
}
impl PrescriptiveAuditReport {
#[must_use]
pub fn is_clean(&self) -> bool {
!self.verdicts.iter().any(|v| v.verdict.is_loud())
}
#[must_use]
pub fn overdue_count(&self) -> usize {
self.verdicts
.iter()
.filter(|v| v.verdict == WorkVerdict::Overdue)
.count()
}
#[must_use]
pub fn count_by_verdict(&self, verdict: WorkVerdict) -> usize {
self.verdicts
.iter()
.filter(|v| v.verdict == verdict)
.count()
}
#[must_use]
pub fn board_ordered(&self) -> Vec<&PrescriptiveVerdict> {
let mut ordered: Vec<&PrescriptiveVerdict> = self.verdicts.iter().collect();
ordered.sort_by_key(|v| match v.verdict {
WorkVerdict::Overdue => 0u8,
WorkVerdict::OutOfFrame => 1,
WorkVerdict::Pending => 2,
WorkVerdict::Fulfilled => 3,
});
ordered
}
}
fn resolve_who_step(decl: &crate::scan::PrescriptiveDeclaration, who_ref: &str) -> StepState {
use antigen_attestation::AuditHint as AH;
use antigen_attestation::evaluate::evaluate_predicate_with_kind;
use antigen_attestation::predicate::{Leaf, Predicate, SignerCurrency};
let item_label = decl.item_target.label();
let sidecar = match load_sidecar(&decl.file, &item_label) {
SidecarLoad::Ok(r) => r,
SidecarLoad::Missing | SidecarLoad::SchemaInvalid => return StepState::Unevaluable,
};
let Some(item) = sidecar.items.iter().find(|i| i.item_path == item_label) else {
return StepState::Unevaluable;
};
let current_fingerprint: &str = if decl.structural_fingerprint.is_empty() {
&item.current_fingerprint
} else {
&decl.structural_fingerprint
};
let predicate = Predicate::leaf(Leaf::Signers {
required: vec![who_ref.to_string()],
roles: std::collections::BTreeMap::new(),
against: SignerCurrency::Current,
signature_allow: Vec::new(),
signature_prefer: None,
});
let ctx = FilesystemAuditContext;
let evaluated = evaluate_predicate_with_kind(
&predicate,
item,
current_fingerprint,
&decl.file,
sidecar.kind,
&ctx,
)
.unwrap_or_else(|_| antigen_attestation::EvaluatedPredicate::sidecar_schema_invalid());
match evaluated.audit_hint {
AH::DisciplineSidecarMissing | AH::DisciplineSidecarSchemaInvalid => StepState::Unevaluable,
_ if evaluated.witness_tier != antigen_attestation::WitnessTier::None => {
StepState::Attested
},
_ => StepState::Unattested,
}
}
fn priority_order_ref_resolves(report: &ScanReport, ref_text: &str) -> bool {
let needle = ref_text.trim();
if needle.is_empty() {
return false;
}
let needle_is_qualified = needle.contains("::");
let needle_leaf = needle.rsplit("::").next().unwrap_or(needle).trim();
let labels = report
.presentations
.iter()
.map(|p| p.item_target.label())
.chain(report.immunities.iter().map(|i| i.item_target.label()))
.chain(
report
.prescriptive_declarations
.iter()
.map(|d| d.item_target.label()),
);
for label in labels {
let label = label.trim().to_owned();
if needle_is_qualified {
if label == needle || label.ends_with(&format!("::{needle}")) {
return true;
}
} else {
let label_leaf = label.rsplit("::").next().unwrap_or(&label).trim();
if label == needle || label_leaf == needle_leaf {
return true;
}
}
}
false
}
#[must_use]
pub fn audit_prescriptive(report: &ScanReport, _workspace_root: &Path) -> PrescriptiveAuditReport {
use crate::scan::WorkShape;
let today = chrono::Local::now().date_naive();
let mut verdicts = Vec::with_capacity(report.prescriptive_declarations.len());
for decl in &report.prescriptive_declarations {
let frame = FrameState::classify(decl.frame.as_deref(), today);
let mut steps: Vec<StepDetail> = Vec::new();
let (satisfied, evaluable, blocking, shape_cause) = match decl.kind.shape() {
WorkShape::RoleWorkflow => eval_role_workflow(decl, &mut steps),
WorkShape::Elimination => eval_elimination(decl, &mut steps),
WorkShape::Ordering => eval_ordering(report, decl, frame, &mut steps),
WorkShape::FrameOnly => eval_frame_only(decl, frame, &mut steps),
};
let frame_unparseable = matches!(frame, FrameState::Unparseable);
let evaluable = evaluable && !frame_unparseable;
let verdict = WorkVerdict::project(satisfied, evaluable, frame);
let blocking = if verdict == WorkVerdict::Fulfilled {
None
} else {
Some(blocking)
};
let out_of_frame_cause = if verdict == WorkVerdict::OutOfFrame {
if frame_unparseable {
Some(OutOfFrameCause::UnparseableFrame)
} else {
shape_cause
}
} else {
None
};
verdicts.push(PrescriptiveVerdict {
declaration: decl.clone(),
verdict,
steps,
blocking,
out_of_frame_cause,
});
}
PrescriptiveAuditReport { verdicts }
}
fn eval_role_workflow(
decl: &crate::scan::PrescriptiveDeclaration,
steps: &mut Vec<StepDetail>,
) -> (bool, bool, String, Option<OutOfFrameCause>) {
let mut chain: Vec<(&str, &str)> = Vec::new();
if let Some(orderer) = decl.ordered_by.as_deref() {
chain.push(("ordered_by", orderer));
}
for f in &decl.filled_by {
chain.push(("filled_by", f));
}
for (role, who) in &chain {
let state = resolve_who_step(decl, who);
steps.push(StepDetail {
role: (*role).to_string(),
reference: (*who).to_string(),
state,
});
}
let no_fillers = steps.is_empty();
let any_unevaluable = steps.iter().any(|s| s.state == StepState::Unevaluable);
let has_closing_step = steps.iter().any(|s| s.role != "ordered_by");
let all_fillers_attested =
has_closing_step && steps.iter().all(|s| s.state == StepState::Attested);
let mut reviewers_attested = true;
let mut any_reviewer = false;
let mut reviewer_unevaluable = false;
for r in &decl.reviewed_by {
any_reviewer = true;
let state = resolve_who_step(decl, r);
if state == StepState::Unevaluable {
reviewer_unevaluable = true;
}
if !all_fillers_attested || state != StepState::Attested {
reviewers_attested = false;
}
steps.push(StepDetail {
role: "reviewed_by".to_string(),
reference: r.clone(),
state,
});
}
let evaluable = !any_unevaluable && !reviewer_unevaluable && !no_fillers;
let satisfied = all_fillers_attested && (!any_reviewer || reviewers_attested);
let cause = if no_fillers {
Some(OutOfFrameCause::MissingWorkStep)
} else if any_unevaluable || reviewer_unevaluable {
Some(OutOfFrameCause::UnknownWhoRef)
} else {
None
};
let blocking = if no_fillers {
"no who-step declared — nothing to attest (declare filled_by/ordered_by)".to_string()
} else if any_unevaluable || reviewer_unevaluable {
"a who-step is un-evaluable (no sidecar / unknown who-ref) — out of frame".to_string()
} else if !has_closing_step {
"awaiting fill: ordered but no filled_by step — an opener never alone fulfills".to_string()
} else if !all_fillers_attested {
"awaiting fill: not every filled_by step is attested".to_string()
} else {
"awaiting review: reviewed_by not yet attested (all fillers done)".to_string()
};
(satisfied, evaluable, blocking, cause)
}
fn eval_elimination(
decl: &crate::scan::PrescriptiveDeclaration,
steps: &mut Vec<StepDetail>,
) -> (bool, bool, String, Option<OutOfFrameCause>) {
for alt in &decl.items {
steps.push(StepDetail {
role: "rule_out".to_string(),
reference: alt.clone(),
state: StepState::Unattested,
});
}
let mut closure_refs: Vec<(&str, &str)> = Vec::new();
for f in &decl.filled_by {
closure_refs.push(("investigator", f));
}
for r in &decl.reviewed_by {
closure_refs.push(("reviewer", r));
}
let mut any_unevaluable = false;
let mut all_attested = !closure_refs.is_empty();
for (role, who) in &closure_refs {
let state = resolve_who_step(decl, who);
if state == StepState::Unevaluable {
any_unevaluable = true;
}
if state != StepState::Attested {
all_attested = false;
}
steps.push(StepDetail {
role: (*role).to_string(),
reference: (*who).to_string(),
state,
});
}
let evaluable = !any_unevaluable && !closure_refs.is_empty();
let satisfied = all_attested;
let cause = if closure_refs.is_empty() {
Some(OutOfFrameCause::MissingWorkStep)
} else if any_unevaluable {
Some(OutOfFrameCause::UnknownWhoRef)
} else {
None
};
let blocking = if closure_refs.is_empty() {
"no investigator/reviewer declared — the differential cannot be closed".to_string()
} else if any_unevaluable {
"a closure who-step is un-evaluable (no sidecar / unknown who-ref)".to_string()
} else {
"awaiting elimination: investigator/reviewer not yet attested".to_string()
};
(satisfied, evaluable, blocking, cause)
}
fn eval_ordering(
report: &ScanReport,
decl: &crate::scan::PrescriptiveDeclaration,
frame: FrameState,
steps: &mut Vec<StepDetail>,
) -> (bool, bool, String, Option<OutOfFrameCause>) {
let mut all_refs_resolve = !decl.items.is_empty();
let mut unresolved: Vec<&str> = Vec::new();
for ref_text in &decl.items {
let resolves = priority_order_ref_resolves(report, ref_text);
if !resolves {
all_refs_resolve = false;
unresolved.push(ref_text);
}
steps.push(StepDetail {
role: "priority_order".to_string(),
reference: ref_text.clone(),
state: if resolves {
StepState::Attested
} else {
StepState::Unevaluable
},
});
}
let mut triaged_attested = !decl.filled_by.is_empty();
let mut triager_unevaluable = false;
for who in &decl.filled_by {
let state = resolve_who_step(decl, who);
if state == StepState::Unevaluable {
triager_unevaluable = true;
}
if state != StepState::Attested {
triaged_attested = false;
}
steps.push(StepDetail {
role: "triaged_by".to_string(),
reference: who.clone(),
state,
});
}
let evaluable = all_refs_resolve && !triager_unevaluable && !decl.items.is_empty();
let satisfied = triaged_attested && !matches!(frame, FrameState::Past);
let cause = if decl.items.is_empty() {
Some(OutOfFrameCause::MissingWorkStep)
} else if !all_refs_resolve {
Some(OutOfFrameCause::UnresolvableRef)
} else if triager_unevaluable {
Some(OutOfFrameCause::UnknownWhoRef)
} else {
None
};
let blocking = if decl.items.is_empty() {
"no priority_order declared — nothing to order".to_string()
} else if !all_refs_resolve {
format!(
"priority_order ref(s) do not resolve to a scanned code site: {unresolved:?} — out of frame (ADR-017-Amd1)"
)
} else if triager_unevaluable {
"triaged_by is un-evaluable (no sidecar / unknown who-ref)".to_string()
} else if !triaged_attested {
"awaiting triage: triaged_by not yet attested".to_string()
} else {
"re-triage owed: triaged_by attested but re_triage_due elapsed (the ordering is stale)"
.to_string()
};
(satisfied, evaluable, blocking, cause)
}
fn eval_frame_only(
decl: &crate::scan::PrescriptiveDeclaration,
_frame: FrameState,
steps: &mut Vec<StepDetail>,
) -> (bool, bool, String, Option<OutOfFrameCause>) {
let mut closure_attested = !decl.filled_by.is_empty();
let mut any_unevaluable = false;
for who in &decl.filled_by {
let state = resolve_who_step(decl, who);
if state == StepState::Unevaluable {
any_unevaluable = true;
}
if state != StepState::Attested {
closure_attested = false;
}
steps.push(StepDetail {
role: "closure".to_string(),
reference: who.clone(),
state,
});
}
let evaluable = !any_unevaluable;
let satisfied = closure_attested;
let cause = if any_unevaluable {
Some(OutOfFrameCause::UnknownWhoRef)
} else {
None
};
let blocking = if decl.filled_by.is_empty() {
"no closure attestation declared — frame-expiry alone never fulfills (positive-closure guard, ATK-PRES-13)".to_string()
} else if any_unevaluable {
"closure who-step is un-evaluable (no sidecar / unknown who-ref)".to_string()
} else {
"awaiting closure: the named closure attestation is not yet recorded".to_string()
};
(satisfied, evaluable, blocking, cause)
}