use antigen_macros::{antigen_tolerance, presents};
use serde::{Deserialize, Serialize};
use super::AuditHint;
use crate::scan::ScanReport;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MucosalAudit {
pub declaration: crate::scan::MucosalDeclaration,
pub hints: Vec<AuditHint>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MucosalAuditReport {
pub audits: Vec<MucosalAudit>,
pub clean_count: usize,
pub concern_count: usize,
}
impl MucosalAuditReport {
#[must_use]
pub const fn all_clean(&self) -> bool {
self.concern_count == 0
}
}
const MUCOSAL_RATIONALE_FLOOR: usize = 20;
const MUCOSAL_TOLERANT_RATIONALE_FLOOR: usize = 40;
#[must_use]
#[presents(DelegateCrossCrateResolutionGap)]
#[antigen_tolerance(
DelegateCrossCrateResolutionGap,
rationale = "Accepted v0.2 limitation: handler_kinds is built from intra-crate #[mucosal] \
declarations only, so a #[mucosal_delegate] pointing at a cross-crate handler \
false-positives as MucosalDisciplineDelegateTargetMissing. The structural fix is a \
multi-crate scan pass (v0.3+ scope, same boundary as --include-deps cross-crate \
addressing). Until then the false-positive is the conservative failure (flags rather \
than silently trusts an unresolvable delegate).",
until = "v0.3"
)]
pub fn audit_mucosal(report: &ScanReport) -> MucosalAuditReport {
use crate::scan::{ItemTarget, MucosalKindTag};
let mut handler_kinds: std::collections::HashMap<&str, std::collections::HashSet<&str>> =
std::collections::HashMap::new();
let mut handler_files: std::collections::HashMap<
&str,
std::collections::HashSet<&std::path::Path>,
> = std::collections::HashMap::new();
for decl in &report.mucosal_declarations {
if decl.tag == MucosalKindTag::Mucosal {
if let ItemTarget::Fn(fn_name) = &decl.item_target {
if let Some(kind) = decl.boundary_kind.as_deref() {
handler_kinds
.entry(fn_name.as_str())
.or_default()
.insert(kind);
}
handler_files
.entry(fn_name.as_str())
.or_default()
.insert(decl.file.as_path());
}
}
}
let ambiguous_names: std::collections::HashSet<&str> = handler_files
.iter()
.filter(|(_, files)| files.len() > 1)
.map(|(name, _)| *name)
.collect();
let mut audits: Vec<MucosalAudit> = Vec::new();
for decl in &report.mucosal_declarations {
let hints = evaluate_mucosal_hints(decl, &handler_kinds, &ambiguous_names);
audits.push(MucosalAudit {
declaration: decl.clone(),
hints,
});
}
let mut clean_count = 0usize;
let mut concern_count = 0usize;
for a in &audits {
if a.hints.is_empty() {
clean_count += 1;
} else {
concern_count += 1;
}
}
MucosalAuditReport {
audits,
clean_count,
concern_count,
}
}
fn evaluate_mucosal_hints(
decl: &crate::scan::MucosalDeclaration,
handler_kinds: &std::collections::HashMap<&str, std::collections::HashSet<&str>>,
ambiguous_names: &std::collections::HashSet<&str>,
) -> Vec<AuditHint> {
use crate::scan::MucosalKindTag;
let mut hints = Vec::new();
match decl.tag {
MucosalKindTag::Mucosal => {
if decl.boundary_kind.is_none() {
hints.push(AuditHint::MucosalKindMismatch);
}
if decl
.rationale
.as_deref()
.is_none_or(|r| r.len() < MUCOSAL_RATIONALE_FLOOR)
{
hints.push(AuditHint::MucosalRationaleInsufficient);
}
},
MucosalKindTag::MucosalDelegate => {
if decl.boundary_kind.is_none() {
hints.push(AuditHint::MucosalKindMismatch);
}
match decl.handled_by.as_deref() {
None => hints.push(AuditHint::MucosalDisciplineDelegateTargetMissing),
Some(handler) => {
if ambiguous_names.contains(handler) {
hints.push(AuditHint::MucosalDisciplineDelegateTargetAmbiguous);
} else {
match handler_kinds.get(handler) {
None => hints.push(AuditHint::MucosalDisciplineDelegateTargetMissing),
Some(kinds) if kinds.is_empty() => {
hints.push(AuditHint::MucosalDisciplineDelegateTargetNotMucosal);
},
Some(kinds) => {
let matches = decl
.boundary_kind
.as_deref()
.is_some_and(|b| kinds.contains(b));
if !matches {
hints.push(
AuditHint::MucosalDisciplineDelegateTargetKindMismatch,
);
}
},
}
}
},
}
},
MucosalKindTag::MucosalTolerant => {
if decl.boundary_kind.is_none() {
hints.push(AuditHint::MucosalKindMismatch);
}
if decl
.rationale
.as_deref()
.is_none_or(|r| r.len() < MUCOSAL_TOLERANT_RATIONALE_FLOOR)
{
hints.push(AuditHint::MucosalTolerantRationaleInsufficient);
}
if decl.accepts.as_deref().is_none_or(|a| a.trim().is_empty()) {
hints.push(AuditHint::MucosalTolerantAcceptsEmpty);
}
if decl.reviewed_by.is_none() {
hints.push(AuditHint::MucosalTolerantWithoutReviewer);
}
if let Some(until) = decl.until.as_deref() {
if let Ok(until_date) = chrono::NaiveDate::parse_from_str(until, "%Y-%m-%d") {
if chrono::Utc::now().date_naive() > until_date {
hints.push(AuditHint::MucosalTolerantPastReviewDate);
}
}
}
},
}
hints
}