use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use crate::coverage::{self, CoverageEntry, IsStale};
use crate::git;
const SLICE_DIR: &str = ".doctrine/slice";
pub(crate) fn scan_coverage(root: &Path, req: &str) -> Vec<(CoverageEntry, IsStale)> {
scan_coverage_batch(root, &BTreeSet::from([req.to_owned()]))
.remove(req)
.unwrap_or_default()
}
pub(crate) fn scan_coverage_batch(
root: &Path,
wanted: &BTreeSet<String>,
) -> BTreeMap<String, Vec<(CoverageEntry, IsStale)>> {
let buckets = collect_matching_entries_batch(root, wanted);
let head = git::head_sha(root);
buckets
.into_iter()
.map(|(req, entries)| (req, stale_each(root, head.as_deref(), entries)))
.collect()
}
fn stale_each(
root: &Path,
head: Option<&str>,
entries: Vec<CoverageEntry>,
) -> Vec<(CoverageEntry, IsStale)> {
entries
.into_iter()
.map(|entry| {
let stale = match head {
Some(head) => IsStale::from(git::commits_touching(
root,
&entry.touched_paths,
&entry.git_anchor,
head,
)),
None => IsStale::Unknown,
};
(entry, stale)
})
.collect()
}
pub(crate) fn slice_local_covered_reqs(
root: &Path,
slice_id: u32,
canonical: &str,
) -> anyhow::Result<Vec<String>> {
let path = root
.join(SLICE_DIR)
.join(format!("{slice_id:03}"))
.join("coverage.toml");
let body = match fs::read_to_string(&path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => {
return Err(e).map_err(|e| anyhow::anyhow!("failed to read {}: {e}", path.display()));
}
};
let file = coverage::parse(&body)
.map_err(|e| anyhow::anyhow!("failed to parse {}: {e}", path.display()))?;
let mut seen = std::collections::BTreeSet::new();
let mut out = Vec::new();
for entry in file.entry {
anyhow::ensure!(
entry.key.slice == canonical,
"integrity error: {} carries a foreign coverage entry slice = \"{}\" \
(expected \"{canonical}\") for requirement {} — a slice's own \
coverage.toml must cite only itself",
path.display(),
entry.key.slice,
entry.key.requirement,
);
if seen.insert(entry.key.requirement.clone()) {
out.push(entry.key.requirement);
}
}
Ok(out)
}
fn collect_matching_entries_batch(
root: &Path,
wanted: &BTreeSet<String>,
) -> BTreeMap<String, Vec<CoverageEntry>> {
let mut out: BTreeMap<String, Vec<CoverageEntry>> =
wanted.iter().map(|r| (r.clone(), Vec::new())).collect();
let slice_root = root.join(SLICE_DIR);
let Ok(slices) = fs::read_dir(&slice_root) else {
return out; };
for slice in slices.flatten() {
if slice.file_type().is_ok_and(|t| t.is_symlink()) {
continue;
}
let coverage_path = slice.path().join("coverage.toml");
let Ok(body) = fs::read_to_string(&coverage_path) else {
continue; };
let Ok(file) = coverage::parse(&body) else {
continue; };
for entry in file.entry {
if let Some(bucket) = out.get_mut(&entry.key.requirement) {
bucket.push(entry);
}
}
}
out
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "tests: fail-fast unwrap on disk/git setup is idiomatic"
)]
mod tests {
use super::*;
use std::process::Command;
use std::time::Instant;
fn write_coverage(root: &Path, slice_num: u32, body: &str) {
let dir = root.join(SLICE_DIR).join(format!("{slice_num:03}"));
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("coverage.toml"), body).unwrap();
}
fn one_entry_body(slice: &str, req: &str, status: &str) -> String {
format!(
"[[entry]]\n\
slice = \"{slice}\"\n\
requirement = \"{req}\"\n\
contributing_change = \"{slice}\"\n\
mode = \"VT\"\n\
status = \"{status}\"\n\
git_anchor = \"deadbeef\"\n"
)
}
#[test]
fn missing_slice_tree_yields_empty() {
let dir = tempfile::tempdir().unwrap();
assert!(scan_coverage(dir.path(), "REQ-110").is_empty());
}
#[test]
fn filters_to_the_requested_requirement_across_slices() {
let dir = tempfile::tempdir().unwrap();
write_coverage(
dir.path(),
40,
&one_entry_body("SL-040", "REQ-110", "verified"),
);
write_coverage(
dir.path(),
41,
&one_entry_body("SL-041", "REQ-999", "planned"),
);
write_coverage(
dir.path(),
42,
&one_entry_body("SL-042", "REQ-110", "planned"),
);
let cells = scan_coverage(dir.path(), "REQ-110");
assert_eq!(
cells.len(),
2,
"only the two REQ-110 entries survive the filter"
);
assert!(cells.iter().all(|(e, _)| e.key.requirement == "REQ-110"));
}
#[test]
fn slug_alias_symlink_does_not_double_count() {
let dir = tempfile::tempdir().unwrap();
write_coverage(
dir.path(),
42,
&one_entry_body("SL-042", "REQ-110", "planned"),
);
let slice_root = dir.path().join(SLICE_DIR);
std::os::unix::fs::symlink(
slice_root.join("042"),
slice_root.join("042-reconciliation-observe-substrate"),
)
.unwrap();
let cells = scan_coverage(dir.path(), "REQ-110");
assert_eq!(
cells.len(),
1,
"the slug-alias symlink must not re-yield the same coverage entry (ISS-006)"
);
}
#[test]
fn malformed_coverage_file_is_skipped_not_fatal() {
let dir = tempfile::tempdir().unwrap();
write_coverage(
dir.path(),
40,
&one_entry_body("SL-040", "REQ-110", "verified"),
);
write_coverage(dir.path(), 41, "this is not valid toml = = =");
let cells = scan_coverage(dir.path(), "REQ-110");
assert_eq!(
cells.len(),
1,
"the good file survives; the bad one is skipped"
);
}
#[test]
fn no_head_makes_every_cell_unknown() {
let dir = tempfile::tempdir().unwrap();
write_coverage(
dir.path(),
40,
&one_entry_body("SL-040", "REQ-110", "verified"),
);
let cells = scan_coverage(dir.path(), "REQ-110");
assert_eq!(cells.len(), 1);
assert_eq!(cells.first().unwrap().1, IsStale::Unknown);
}
fn measure_scan_fanin(n: u32) -> std::time::Duration {
let dir = tempfile::tempdir().unwrap();
for i in 0..n {
let req = if i % 2 == 0 { "REQ-110" } else { "REQ-999" };
write_coverage(dir.path(), i, &one_entry_body("SL-000", req, "planned"));
}
let start = Instant::now();
let cells = scan_coverage(dir.path(), "REQ-110");
let elapsed = start.elapsed();
let expected = n.div_euclid(2) + n.rem_euclid(2);
assert_eq!(cells.len() as u32, expected, "filter kept the REQ-110 half");
elapsed
}
#[test]
fn vt4a_scan_fanin_small_tiers() {
for n in [50_u32, 500] {
let d = measure_scan_fanin(n);
println!("VT-4(a) scan fan-in N={n}: {d:?}");
assert!(
d.as_secs() < 10,
"scan fan-in N={n} took {d:?} — investigate (debug budget ~10x)"
);
}
}
#[test]
#[ignore = "heavy 2000-file tier — run explicitly to confirm the scan cliff; \
numbers recorded in the worker report"]
fn vt4a_scan_fanin_heavy_tier() {
let d = measure_scan_fanin(2000);
println!("VT-4(a) scan fan-in N=2000: {d:?}");
assert!(d.as_secs() < 30, "scan fan-in N=2000 took {d:?}");
}
fn measure_staleness_per_call(n: u32) -> Option<(std::time::Duration, std::time::Duration)> {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let head = git::head_sha(root)?;
let out = Command::new("git")
.arg("-C")
.arg(root)
.args(["rev-list", "--max-parents=0", "HEAD"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let roots = String::from_utf8(out.stdout).ok()?;
let base = roots.lines().next()?.trim().to_owned();
if base.is_empty() {
return None;
}
let paths = vec!["src/coverage.rs".to_owned()];
let start = Instant::now();
for _ in 0..n {
let _ = git::commits_touching(root, &paths, &base, &head);
}
let total = start.elapsed();
let per = total.checked_div(n).unwrap_or(total);
Some((total, per))
}
#[test]
fn vt4b_staleness_per_call_cost() {
let Some((total, per)) = measure_staleness_per_call(20) else {
println!("VT-4(b) staleness: fork not a usable git repo — skipped");
return;
};
println!("VT-4(b) staleness N=20: total {total:?}, per-call {per:?}");
assert!(
per.as_millis() < 2000,
"per-call staleness {per:?} — investigate subprocess cost"
);
}
fn git_at(root: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(root)
.args([
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"-c",
"commit.gpgsign=false",
])
.args(args)
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
);
String::from_utf8(out.stdout).unwrap().trim().to_owned()
}
fn write_commit(root: &Path, rel: &str, contents: &str, msg: &str) -> String {
let full = root.join(rel);
fs::create_dir_all(full.parent().unwrap()).unwrap();
fs::write(&full, contents).unwrap();
git_at(root, &["add", rel]);
git_at(root, &["commit", "-q", "-m", msg]);
git_at(root, &["rev-parse", "HEAD"])
}
#[test]
fn seam_fits_coverage_entry_granularity_stale_and_fresh() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
git_at(root, &["init", "-q", "-b", "main"]);
let anchor = write_commit(root, "src/foo.rs", "fn a() {}\n", "add foo");
let _ = write_commit(root, "src/bar.rs", "fn b() {}\n", "add bar");
let head_fresh = git_at(root, &["rev-parse", "HEAD"]);
let entry = entry_with(&anchor, &["src/foo.rs"]);
let fresh = IsStale::from(git::commits_touching(
root,
&entry.touched_paths,
&entry.git_anchor,
&head_fresh,
));
assert_eq!(
fresh,
IsStale::Fresh,
"anchor..HEAD over an untouched path resolves Fresh through the seam"
);
let head_stale = write_commit(root, "src/foo.rs", "fn a() { 1; }\n", "edit foo");
let stale = IsStale::from(git::commits_touching(
root,
&entry.touched_paths,
&entry.git_anchor,
&head_stale,
));
assert_eq!(
stale,
IsStale::Stale,
"a commit touching the path since the anchor resolves Stale through the seam"
);
}
fn entry_with(anchor: &str, paths: &[&str]) -> CoverageEntry {
use crate::requirement::CoverageStatus;
CoverageEntry {
key: coverage::CoverageKey {
slice: "SL-042".to_owned(),
requirement: "REQ-115".to_owned(),
contributing_change: "SL-042".to_owned(),
mode: "VH".to_owned(),
},
status: CoverageStatus::Verified,
git_anchor: anchor.to_owned(),
attested_date: Some("2026-06-12".to_owned()),
touched_paths: paths.iter().map(|p| (*p).to_owned()).collect(),
check: None,
}
}
fn vh_va_coverage_body(anchor: &str, path: &str) -> String {
let entry = |mode: &str| {
format!(
"[[entry]]\n\
slice = \"SL-042\"\n\
requirement = \"REQ-115\"\n\
contributing_change = \"SL-042\"\n\
mode = \"{mode}\"\n\
status = \"verified\"\n\
git_anchor = \"{anchor}\"\n\
attested_date = \"2026-06-12\"\n\
touched_paths = [\"{path}\"]\n"
)
};
format!("{}{}", entry("VH"), entry("VA"))
}
#[test]
fn vh_va_verified_evidence_is_flagged_stale_never_demoted() {
use crate::requirement::CoverageStatus;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
git_at(root, &["init", "-q", "-b", "main"]);
let anchor = write_commit(root, "src/foo.rs", "fn a() {}\n", "add foo");
let cov_rel = ".doctrine/slice/042/coverage.toml";
write_commit(
root,
cov_rel,
&vh_va_coverage_body(&anchor, "src/foo.rs"),
"coverage",
);
write_commit(root, "src/foo.rs", "fn a() { 1; }\n", "edit foo");
let cells = scan_coverage(root, "REQ-115");
assert_eq!(
cells.len(),
2,
"the VH and VA entries both survive the filter"
);
for (entry, stale) in &cells {
assert_eq!(
*stale,
IsStale::Stale,
"{} evidence over an edited path is flagged stale",
entry.key.mode
);
assert_eq!(
entry.status,
CoverageStatus::Verified,
"{} status stays Verified — staleness NEVER auto-demotes (NF-002)",
entry.key.mode
);
}
assert!(cells.iter().any(|(e, _)| e.key.mode == "VH"));
assert!(cells.iter().any(|(e, _)| e.key.mode == "VA"));
}
#[test]
fn vh_va_verified_evidence_untouched_since_anchor_is_fresh() {
use crate::requirement::CoverageStatus;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
git_at(root, &["init", "-q", "-b", "main"]);
let anchor = write_commit(root, "src/foo.rs", "fn a() {}\n", "add foo");
write_commit(
root,
".doctrine/slice/042/coverage.toml",
&vh_va_coverage_body(&anchor, "src/foo.rs"),
"coverage",
);
write_commit(root, "src/bar.rs", "fn b() {}\n", "add bar");
let cells = scan_coverage(root, "REQ-115");
assert_eq!(cells.len(), 2);
for (entry, stale) in &cells {
assert_eq!(
*stale,
IsStale::Fresh,
"{} evidence over an untouched path is Fresh — the contrast case",
entry.key.mode
);
assert_eq!(entry.status, CoverageStatus::Verified, "status unchanged");
}
}
fn rival_staleness_path() -> String {
format!("{}::", "contentset")
}
#[test]
fn coverage_staleness_flows_only_through_commits_touching() {
let scan_src = include_str!("coverage_scan.rs");
let cov_src = include_str!("coverage.rs");
let rival = rival_staleness_path();
assert!(
scan_src.contains("commits_touching"),
"the scan shell resolves staleness through git::commits_touching"
);
for (name, src) in [("coverage_scan.rs", scan_src), ("coverage.rs", cov_src)] {
assert!(
!src.contains(&rival),
"{name} must not path into the memory-side staleness leaf — coverage \
staleness has its own single seam (git::commits_touching), no \
parallel impl"
);
}
}
use std::collections::BTreeSet;
fn wanted(reqs: &[&str]) -> BTreeSet<String> {
reqs.iter().map(|r| (*r).to_owned()).collect()
}
#[test]
fn batch_partitions_each_req_and_matches_single_scan_at_composite_seam() {
let dir = tempfile::tempdir().unwrap();
write_coverage(
dir.path(),
40,
&one_entry_body("SL-040", "REQ-110", "verified"),
);
write_coverage(
dir.path(),
41,
&one_entry_body("SL-041", "REQ-200", "planned"),
);
write_coverage(
dir.path(),
42,
&one_entry_body("SL-042", "REQ-110", "planned"),
);
write_coverage(
dir.path(),
43,
&one_entry_body("SL-043", "REQ-999", "planned"),
);
let batch = scan_coverage_batch(dir.path(), &wanted(&["REQ-110", "REQ-200", "REQ-300"]));
assert_eq!(
batch.keys().cloned().collect::<Vec<_>>(),
vec![
"REQ-110".to_owned(),
"REQ-200".to_owned(),
"REQ-300".to_owned()
],
"dense: exactly the wanted reqs are keyed"
);
assert!(
batch["REQ-300"].is_empty(),
"uncovered wanted req → empty bucket"
);
assert_eq!(batch["REQ-110"].len(), 2, "REQ-110 covered in two slices");
assert_eq!(batch["REQ-200"].len(), 1);
for (req, bucket) in &batch {
assert!(
bucket.iter().all(|(e, _)| &e.key.requirement == req),
"bucket {req} holds only its own req's entries"
);
}
for req in ["REQ-110", "REQ-200"] {
assert_eq!(
coverage::composite(&batch[req]),
coverage::composite(&scan_coverage(dir.path(), req)),
"composite(batch[{req}]) equals composite(scan_coverage({req}))"
);
}
}
#[test]
fn batch_slug_alias_symlink_does_not_double_count() {
let dir = tempfile::tempdir().unwrap();
write_coverage(
dir.path(),
42,
&one_entry_body("SL-042", "REQ-110", "planned"),
);
let slice_root = dir.path().join(SLICE_DIR);
std::os::unix::fs::symlink(
slice_root.join("042"),
slice_root.join("042-reconciliation-observe-substrate"),
)
.unwrap();
let batch = scan_coverage_batch(dir.path(), &wanted(&["REQ-110"]));
assert_eq!(
batch["REQ-110"].len(),
1,
"the slug-alias symlink must not re-yield the entry through the batch (ISS-006)"
);
}
#[test]
fn batch_is_deterministic_and_degradation_tolerant() {
let dir = tempfile::tempdir().unwrap();
write_coverage(
dir.path(),
40,
&one_entry_body("SL-040", "REQ-110", "verified"),
);
write_coverage(dir.path(), 41, "this is not valid toml = = =");
let w = wanted(&["REQ-110", "REQ-200"]);
let a = scan_coverage_batch(dir.path(), &w);
let b = scan_coverage_batch(dir.path(), &w);
for req in ["REQ-110", "REQ-200"] {
assert_eq!(
coverage::composite(&a[req]),
coverage::composite(&b[req]),
"composite(batch[{req}]) is stable across runs"
);
}
assert_eq!(
a["REQ-110"].len(),
1,
"malformed file skipped, good one kept"
);
assert!(a["REQ-200"].is_empty());
let empty_dir = tempfile::tempdir().unwrap();
let m = scan_coverage_batch(empty_dir.path(), &w);
assert_eq!(
m.keys().cloned().collect::<Vec<_>>(),
vec!["REQ-110".to_owned(), "REQ-200".to_owned()],
"missing tree still yields a dense map over the wanted set"
);
assert!(
m.values().all(Vec::is_empty),
"every bucket empty on a missing tree"
);
}
}