use serde::{Deserialize, Serialize};
use super::AuditHint;
use crate::scan::ScanReport;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryAudit {
pub antigen_type: String,
pub file: std::path::PathBuf,
pub line: usize,
pub hints: Vec<AuditHint>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CategoryAuditReport {
pub audits: Vec<CategoryAudit>,
pub explicit_count: usize,
pub defaulted_count: usize,
#[serde(default)]
pub mismatch_count: usize,
}
impl CategoryAuditReport {
#[must_use]
pub const fn all_explicit(&self) -> bool {
self.defaulted_count == 0
}
#[must_use]
pub const fn no_category_witness_mismatch(&self) -> bool {
self.mismatch_count == 0
}
#[must_use]
pub fn no_silence_witness_mismatch(&self) -> bool {
!self.audits.iter().any(|ca| {
ca.hints
.contains(&AuditHint::AntigenWitnessShapeMismatchForSilenceNoWitness)
|| ca
.hints
.contains(&AuditHint::AntigenWitnessShapeMismatchForSilenceWrongTier)
})
}
}
#[must_use]
pub fn audit_category(report: &ScanReport) -> CategoryAuditReport {
use crate::category::AntigenCategory;
let mut audits = Vec::new();
let mut explicit_count = 0usize;
let mut defaulted_count = 0usize;
let mut mismatch_count = 0usize;
for decl in &report.antigens {
if decl.category.is_empty() {
defaulted_count += 1;
audits.push(CategoryAudit {
antigen_type: decl.type_name.clone(),
file: decl.file.clone(),
line: decl.line,
hints: vec![AuditHint::AntigenCategoryDefaultedImplicitFunctional],
});
continue;
}
explicit_count += 1;
let mut has_substrate_witness = false;
let mut has_code_witness = false;
let mut has_any_immunity = false;
for imm in &report.immunities {
if imm.antigen_type != decl.type_name {
continue;
}
if !crate::scan::canonical_paths_match(
imm.canonical_path.as_deref(),
decl.canonical_path.as_deref(),
) {
continue;
}
has_any_immunity = true;
if imm.requires_predicate.is_some() {
has_substrate_witness = true;
}
if !imm.witness.is_empty() {
has_code_witness = true;
}
}
if report.defenses.iter().any(|d| {
d.antigen_type == decl.type_name
&& crate::scan::canonical_paths_match(
d.canonical_path.as_deref(),
decl.canonical_path.as_deref(),
)
}) {
has_any_immunity = true;
has_code_witness = true;
}
let wants_substrate = decl.category.contains(&AntigenCategory::SubstrateAlignment);
let wants_code = decl
.category
.contains(&AntigenCategory::FunctionalCorrectness);
let is_hybrid = wants_substrate && wants_code;
if !has_any_immunity {
if wants_substrate {
audits.push(CategoryAudit {
antigen_type: decl.type_name.clone(),
file: decl.file.clone(),
line: decl.line,
hints: vec![AuditHint::AntigenWitnessShapeMismatchForSilenceNoWitness],
});
}
continue;
}
let substrate_satisfied = !wants_substrate || has_substrate_witness;
let code_satisfied = !wants_code || has_code_witness;
if substrate_satisfied && code_satisfied {
continue;
}
let hybrid_one_axis_witnessed = is_hybrid && (has_substrate_witness ^ has_code_witness);
let hint = if hybrid_one_axis_witnessed {
AuditHint::AntigenCategoryHybridIncompleteEvidence
} else {
AuditHint::AntigenCategoryClaimInconsistentWithPredicateType
};
let mut hints = vec![hint];
if wants_substrate && has_code_witness && !has_substrate_witness {
hints.push(AuditHint::AntigenWitnessShapeMismatchForSilenceWrongTier);
}
mismatch_count += 1;
audits.push(CategoryAudit {
antigen_type: decl.type_name.clone(),
file: decl.file.clone(),
line: decl.line,
hints,
});
}
CategoryAuditReport {
audits,
explicit_count,
defaulted_count,
mismatch_count,
}
}