pub(crate) const MIN_SCAN_COVERAGE: f64 = 0.9;
pub(crate) const REASON_BLOB_READ: &str = "blob read failed";
pub(crate) const REASON_PARSE_ERROR: &str = "parse error";
pub(crate) enum ScanOutcome<T> {
NotCounted,
Lost(&'static str),
Scored(T),
SkippedOversize,
}
pub(crate) struct ScanCoverage {
eligible: usize,
scored: usize,
skipped_oversize: usize,
by_reason: Vec<(&'static str, usize)>,
}
impl ScanCoverage {
pub(crate) fn tally<T>(outcomes: &[ScanOutcome<T>]) -> Self {
let mut scored = 0usize;
let mut skipped_oversize = 0usize;
let mut counts: std::collections::BTreeMap<&'static str, usize> =
std::collections::BTreeMap::new();
for o in outcomes {
match o {
ScanOutcome::Scored(..) => scored += 1,
ScanOutcome::Lost(reason) => *counts.entry(reason).or_default() += 1,
ScanOutcome::SkippedOversize => skipped_oversize += 1,
ScanOutcome::NotCounted => {}
}
}
let lost: usize = counts.values().sum();
let mut by_reason: Vec<(&'static str, usize)> = counts.into_iter().collect();
by_reason.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
Self {
eligible: scored + lost,
scored,
skipped_oversize,
by_reason,
}
}
#[allow(clippy::cast_precision_loss)]
pub(crate) fn ratio(&self) -> f64 {
if self.eligible == 0 {
1.0
} else {
self.scored as f64 / self.eligible as f64
}
}
pub(crate) fn warn_if_degraded(&self, scan: &str, table: &str) {
if self.eligible == 0 || self.ratio() >= MIN_SCAN_COVERAGE {
return;
}
let detail = self
.by_reason
.iter()
.map(|(reason, n)| format!("{n} {reason}"))
.collect::<Vec<_>>()
.join(", ");
tracing::warn!(
"{scan} scan covered {scored}/{eligible} eligible source files \
({pct:.0}%); {detail}. Analyses and quality gates that read \
`{table}` are drawing on a minority of this repository. A blobless \
partial clone (`git clone --filter=blob:none`, or \
`actions/checkout` with a filter) is the usual cause and is not \
detected by the shallow-clone check, because such a clone has \
complete commit history.",
scored = self.scored,
eligible = self.eligible,
pct = self.ratio() * 100.0,
);
}
pub(crate) fn oversize_majority(&self) -> bool {
self.skipped_oversize > self.scored
}
pub(crate) fn warn_if_mostly_oversize(&self, scan: &str, table: &str) {
if !self.oversize_majority() {
return;
}
tracing::warn!(
"{scan} scan skipped {skipped} file(s) past the {cap}-byte AST \
cap — more than the {scored} it scanned. `{table}` describes a \
minority of what looks like source in this repository: the cap \
exists to skip generated/minified bundles, but at this share the \
skipped set IS the repository. Exclude bundle directories via \
`.codeloreignore` so the census reflects maintained code.",
skipped = self.skipped_oversize,
cap = crate::constants::DEFAULT_MAX_AST_FILE_BYTES,
scored = self.scored,
);
}
}
#[cfg(test)]
mod tests {
use super::{
MIN_SCAN_COVERAGE, REASON_BLOB_READ, REASON_PARSE_ERROR, ScanCoverage, ScanOutcome,
};
type Outcome = ScanOutcome<String>;
fn scored(path: &str) -> Outcome {
ScanOutcome::Scored(path.to_string())
}
#[test]
fn oversize_skips_stay_out_of_the_loss_ratio() {
let outcomes: Vec<Outcome> = vec![
scored("a.rs"),
ScanOutcome::SkippedOversize,
ScanOutcome::SkippedOversize,
];
let cov = ScanCoverage::tally(&outcomes);
assert!(
(cov.ratio() - 1.0).abs() < 1e-12,
"an oversize skip is deliberately not a loss — the cap exists to \
skip bundles, and bundle-carrying repositories skip routinely"
);
}
#[test]
fn the_oversize_disclosure_fires_on_a_strict_majority() {
let majority = ScanCoverage::tally::<String>(&[
scored("a.rs"),
ScanOutcome::SkippedOversize,
ScanOutcome::SkippedOversize,
]);
assert!(
majority.oversize_majority(),
"two skipped vs one scanned is a majority-blind table"
);
let tie = ScanCoverage::tally::<String>(&[scored("a.rs"), ScanOutcome::SkippedOversize]);
assert!(!tie.oversize_majority(), "a tie is not a majority");
let none = ScanCoverage::tally::<String>(&[scored("a.rs")]);
assert!(!none.oversize_majority());
}
#[test]
fn ineligible_files_are_not_a_coverage_loss() {
let outcomes = vec![
ScanOutcome::NotCounted,
ScanOutcome::NotCounted,
scored("src/lib.rs"),
];
let cov = ScanCoverage::tally(&outcomes);
assert_eq!(cov.eligible, 1, "only the Tier-1 file is eligible");
assert_eq!(cov.scored, 1);
assert!((cov.ratio() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn routine_skips_do_not_lower_coverage() {
let mut outcomes: Vec<Outcome> = (0..349).map(|i| scored(&format!("f{i}.rs"))).collect();
for _ in 0..55 {
outcomes.push(ScanOutcome::NotCounted);
}
let cov = ScanCoverage::tally(&outcomes);
assert_eq!(
cov.eligible, 349,
"paths history carries but HEAD does not track are not files the scan owed"
);
assert!(
(cov.ratio() - 1.0).abs() < f64::EPSILON,
"a scan that lost nothing must read as complete, got {}",
cov.ratio()
);
assert!(
cov.by_reason.is_empty(),
"nothing was lost, so nothing to attribute"
);
}
#[test]
fn a_source_less_tree_is_vacuously_complete() {
let cov = ScanCoverage::tally(&[Outcome::NotCounted]);
assert_eq!(cov.eligible, 0);
assert!(
(cov.ratio() - 1.0).abs() < f64::EPSILON,
"no eligible files must not read as 0% coverage"
);
}
#[test]
fn skips_lower_the_ratio_and_are_attributed_by_reason() {
let mut outcomes = vec![scored("a.rs")];
for _ in 0..9 {
outcomes.push(ScanOutcome::Lost(REASON_BLOB_READ));
}
let cov = ScanCoverage::tally(&outcomes);
assert_eq!(cov.eligible, 10);
assert_eq!(cov.scored, 1);
assert!(
(cov.ratio() - 0.1).abs() < 1e-9,
"1 of 10 eligible files is 10% coverage, got {}",
cov.ratio()
);
assert!(
cov.ratio() < MIN_SCAN_COVERAGE,
"10% coverage must fall below the floor that triggers disclosure"
);
assert_eq!(cov.by_reason, vec![(REASON_BLOB_READ, 9)]);
}
#[test]
fn reasons_are_ranked_most_frequent_first() {
let outcomes: Vec<Outcome> = vec![
ScanOutcome::Lost(REASON_PARSE_ERROR),
ScanOutcome::Lost(REASON_BLOB_READ),
ScanOutcome::Lost(REASON_BLOB_READ),
];
let cov = ScanCoverage::tally(&outcomes);
assert_eq!(
cov.by_reason,
vec![(REASON_BLOB_READ, 2), (REASON_PARSE_ERROR, 1)],
"the dominant failure mode must be named first so the message leads with it"
);
}
#[test]
fn a_healthy_scan_stays_above_the_floor() {
let outcomes = vec![
scored("a.rs"),
scored("b.rs"),
scored("c.rs"),
scored("d.rs"),
scored("e.rs"),
scored("f.rs"),
scored("g.rs"),
scored("h.rs"),
scored("i.rs"),
scored("j.rs"),
];
let cov = ScanCoverage::tally(&outcomes);
assert!(cov.ratio() >= MIN_SCAN_COVERAGE);
}
#[test]
fn a_successful_scan_with_no_output_still_counts_as_covered() {
let outcomes: Vec<ScanOutcome<Vec<u32>>> = vec![
ScanOutcome::Scored(Vec::new()),
ScanOutcome::Scored(vec![1]),
ScanOutcome::Lost(REASON_BLOB_READ),
];
let cov = ScanCoverage::tally(&outcomes);
assert_eq!(cov.eligible, 3, "both scored files are eligible");
assert_eq!(cov.scored, 2, "an empty payload is still a covered file");
assert!(
(cov.ratio() - 2.0 / 3.0).abs() < 1e-12,
"ratio should be 2/3, got {}",
cov.ratio()
);
}
#[test]
fn the_tally_is_independent_of_the_payload_type() {
let strings: Vec<ScanOutcome<String>> = vec![
ScanOutcome::Scored("a".into()),
ScanOutcome::Lost(REASON_PARSE_ERROR),
];
let pairs: Vec<ScanOutcome<(String, Vec<u8>)>> = vec![
ScanOutcome::Scored(("a".into(), Vec::new())),
ScanOutcome::Lost(REASON_PARSE_ERROR),
];
let a = ScanCoverage::tally(&strings);
let b = ScanCoverage::tally(&pairs);
assert_eq!((a.eligible, a.scored), (b.eligible, b.scored));
assert!((a.ratio() - b.ratio()).abs() < 1e-12);
}
}