use std::collections::HashSet;
use std::path::PathBuf;
use crate::{BoundaryKind, Outcome, Report, RuntimeBoundary, Severity, Violation};
mod scan;
use scan::{Probe, collect_probes};
#[cfg(test)]
use scan::scan_source;
pub fn audit_probe_coverage(declared: &[RuntimeBoundary], src_dirs: &[PathBuf]) -> Outcome {
let mut probes = Vec::new();
for dir in src_dirs {
if let Err(message) = collect_probes(dir, &mut probes) {
return Outcome::ConstitutionError(message);
}
}
let probed_set: HashSet<&str> = probes
.iter()
.filter_map(|p| match p {
Probe::Literal(seam) => Some(seam.as_str()),
Probe::Unauditable { .. } => None,
})
.collect();
let declared_set: HashSet<&str> = declared.iter().map(RuntimeBoundary::seam).collect();
let mut violations = Vec::new();
let mut seen_decl = HashSet::new();
let mut dup_reported = HashSet::new();
for boundary in declared {
let seam = boundary.seam();
if !seen_decl.insert(seam) && dup_reported.insert(seam) {
violations.push(
Violation::new(
BoundaryKind::Runtime,
seam.to_string(),
"each runtime seam must be declared exactly once".to_string(),
format!("seam '{seam}' is declared more than once"),
"a duplicate declaration would silently shadow the earlier boundary at install"
.to_string(),
Severity::Enforce,
)
.with_anchor(boundary.anchor().map(String::from)),
);
}
}
let mut seen = HashSet::new();
for boundary in declared {
let seam = boundary.seam();
if !probed_set.contains(seam) && seen.insert(seam) {
violations.push(
Violation::new(
BoundaryKind::Runtime,
seam.to_string(),
"every declared runtime seam must be probed".to_string(),
format!("declared seam '{seam}' has no assert_boundary! probe"),
"a RuntimeBoundary with no probe is never enforced at runtime".to_string(),
boundary.severity(),
)
.with_anchor(boundary.anchor().map(String::from)),
);
}
}
let mut seen_probe = HashSet::new();
for probe in &probes {
if let Probe::Literal(seam) = probe {
if !declared_set.contains(seam.as_str()) && seen_probe.insert(seam.as_str()) {
violations.push(Violation::new(
BoundaryKind::Runtime,
seam.clone(),
"every probe must reference a declared seam".to_string(),
format!("probe references undeclared seam '{seam}'"),
"an undeclared seam panics at runtime — declare the RuntimeBoundary or fix the probe's seam name".to_string(),
Severity::Enforce,
));
}
}
}
let mut unauditable_files: Vec<&str> = probes
.iter()
.filter_map(|p| match p {
Probe::Unauditable { file } => Some(file.as_str()),
Probe::Literal(_) => None,
})
.collect();
unauditable_files.sort_unstable();
unauditable_files.dedup();
for file in unauditable_files {
violations.push(
Violation::new(
BoundaryKind::Runtime,
"<un-auditable probe>".to_string(),
"an assert_boundary! seam must be a string literal to be auditable".to_string(),
format!(
"{file} has an assert_boundary! probe with a non-literal seam (const or \
expression), which the CI face cannot trace to a declared seam"
),
"spell the seam as a string literal so probe coverage can be verified".to_string(),
Severity::Enforce,
)
.with_file(Some(file.to_string())),
);
}
if violations.is_empty() {
Outcome::Clean
} else {
Outcome::Violations(Report::new(violations))
}
}
#[cfg(test)]
mod tests;