use serde::{Deserialize, Serialize};
use super::{AuditHint, CLONAL_ITERATIONS_DEFAULT_FLOOR, IGG_HISTORICAL_SPAN_DEFAULT_FLOOR};
use crate::scan::ScanReport;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConvergentEvidenceAudit {
pub declaration: crate::scan::ConvergentEvidence,
pub hints: Vec<AuditHint>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ConvergentEvidenceAuditReport {
pub audits: Vec<ConvergentEvidenceAudit>,
pub clean_count: usize,
pub concern_count: usize,
}
impl ConvergentEvidenceAuditReport {
#[must_use]
pub const fn all_clean(&self) -> bool {
self.concern_count == 0
}
}
#[must_use]
pub fn audit_convergent_evidence(report: &ScanReport) -> ConvergentEvidenceAuditReport {
let known_antigen_names: std::collections::HashSet<&str> = report
.antigens
.iter()
.map(|a| a.type_name.as_str())
.collect();
let mut audits: Vec<ConvergentEvidenceAudit> = Vec::new();
for decl in &report.convergent_evidences {
let hints = evaluate_convergent_evidence_hints(decl, &known_antigen_names);
audits.push(ConvergentEvidenceAudit {
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;
}
}
ConvergentEvidenceAuditReport {
audits,
clean_count,
concern_count,
}
}
fn evaluate_convergent_evidence_hints(
decl: &crate::scan::ConvergentEvidence,
known_antigen_names: &std::collections::HashSet<&str>,
) -> Vec<AuditHint> {
use crate::scan::ConvergentEvidenceKind;
let mut hints = Vec::new();
match decl.kind {
ConvergentEvidenceKind::Diagnostic => {
if decl.modality_classes.is_empty() {
hints.push(AuditHint::DiagnosticModalitiesEmpty);
return hints;
}
let distinct: std::collections::HashSet<&str> =
decl.modality_classes.iter().map(String::as_str).collect();
if distinct.len() == 1 && decl.modality_classes.len() > 1 {
hints.push(AuditHint::DiagnosticModalitiesClassCollapsed);
}
if let Some(min) = decl.min_independent {
if min == 0 {
hints.push(AuditHint::DiagnosticMinIndependentZero);
} else if u64::try_from(distinct.len()).unwrap_or(u64::MAX) < min {
hints.push(AuditHint::DiagnosticModalityInsufficient);
}
}
}
ConvergentEvidenceKind::Clonal => {
if matches!(decl.seed_kind.as_deref(), Some("Fixed")) {
hints.push(AuditHint::ClonalFixedSeedDetected);
}
if let Some(iters) = decl.iterations {
if iters < CLONAL_ITERATIONS_DEFAULT_FLOOR {
hints.push(AuditHint::ClonalIterationsBelowThreshold);
}
}
}
ConvergentEvidenceKind::Igg => {
if let Some(span) = decl.historical_span {
if span < IGG_HISTORICAL_SPAN_DEFAULT_FLOOR {
hints.push(AuditHint::IggSpanTooShort);
}
}
let unique_count: std::collections::HashSet<&str> =
decl.witnesses.iter().map(String::as_str).collect();
if let Some(min_re) = decl.min_reattestations {
if min_re == 0 {
hints.push(AuditHint::IggMinReattestationsZero);
} else if u64::try_from(unique_count.len()).unwrap_or(u64::MAX) < min_re {
hints.push(AuditHint::IggReattestationsInsufficient);
}
}
if decl.witnesses.len() > 1 && unique_count.len() == 1 {
hints.push(AuditHint::IggIdentityCollapseWarning);
}
}
ConvergentEvidenceKind::Crossreactive => {
for fp in &decl.fingerprints {
if !known_antigen_names.contains(fp.as_str()) {
hints.push(AuditHint::CrossreactiveFingerprintUnresolved);
break;
}
}
}
ConvergentEvidenceKind::Polyclonal
| ConvergentEvidenceKind::Monoclonal
| ConvergentEvidenceKind::Adcc => {
}
}
hints
}