use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use super::AuditHint;
use crate::scan::ScanReport;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineageFidelityAudit {
pub child: String,
pub parent: String,
pub file: PathBuf,
pub line: usize,
pub hint: AuditHint,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LineageFidelityAuditReport {
pub divergences: Vec<LineageFidelityAudit>,
}
#[must_use]
pub fn audit_lineage_fidelity(report: &ScanReport) -> LineageFidelityAuditReport {
use std::collections::HashMap;
let fingerprints: HashMap<(&str, Option<&str>), antigen_fingerprint::Fingerprint> = report
.antigens
.iter()
.filter_map(|a| {
let raw = a.fingerprint.as_deref()?;
antigen_fingerprint::Fingerprint::parse(raw)
.ok()
.map(|fp| ((a.type_name.as_str(), a.canonical_path.as_deref()), fp))
})
.collect();
let mut divergences = Vec::new();
for edge in &report.lineage_edges {
let (Some(child_fp), Some(parent_fp)) = (
fingerprints.get(&(edge.child.as_str(), edge.child_canonical_path.as_deref())),
fingerprints.get(&(edge.parent.as_str(), edge.parent_canonical_path.as_deref())),
) else {
continue;
};
if let Some(detail) = fingerprint_nonrefinement_reason(child_fp, parent_fp) {
divergences.push(LineageFidelityAudit {
child: edge.child.clone(),
parent: edge.parent.clone(),
file: edge.file.clone(),
line: edge.line,
hint: AuditHint::DescendedFromFingerprintDivergence,
detail,
});
}
}
LineageFidelityAuditReport { divergences }
}
fn fingerprint_nonrefinement_reason(
child: &antigen_fingerprint::Fingerprint,
parent: &antigen_fingerprint::Fingerprint,
) -> Option<String> {
match (parent.node_kind(), child.node_kind()) {
(Some(pk), Some(ck)) if pk != ck => {
return Some(format!(
"child `item = {ck:?}` differs from parent `item = {pk:?}` \
— disjoint item kinds cannot be a refinement"
));
}
(Some(pk), None) => {
return Some(format!(
"parent constrains `item = {pk:?}` but child has no item-kind \
constraint — child matches all item kinds and is broader, not a refinement"
));
}
_ => {}
}
let child_docs = collect_doc_contains_allof_only(&child.constraints);
let parent_docs = collect_doc_contains_allof_only(&parent.constraints);
for parent_needle in &parent_docs {
let covered = child_docs.iter().any(|cd| cd.contains(parent_needle));
if !covered {
return Some(format!(
"parent requires `doc_contains({parent_needle:?})` but no child \
`doc_contains` includes it — child can match where parent does not"
));
}
}
None
}
fn collect_doc_contains_allof_only(constraints: &[antigen_fingerprint::Constraint]) -> Vec<&str> {
use antigen_fingerprint::Constraint;
let mut out = Vec::new();
for c in constraints {
match c {
Constraint::DocContains(s) => out.push(s.as_str()),
Constraint::AllOf(children) => {
out.extend(collect_doc_contains_allof_only(children));
}
_ => {} }
}
out
}