#![cfg_attr(
not(test),
expect(
dead_code,
reason = "coverage substrate (SL-042 P2) is a leaf built ahead of its \
P3/P4 reconcile-reader consumer — every item is dead in the \
bins/lib build until that consumer is wired"
)
)]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::requirement::{CoverageStatus, ReqStatus};
const MODES: &[&str] = &["VT", "VA", "VH"];
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct CoverageKey {
pub(crate) slice: String,
pub(crate) requirement: String,
pub(crate) contributing_change: String,
pub(crate) mode: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct CoverageEntry {
#[serde(flatten)]
pub(crate) key: CoverageKey,
pub(crate) status: CoverageStatus,
pub(crate) git_anchor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) attested_date: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) touched_paths: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub(crate) struct CoverageFile {
#[serde(default)]
pub(crate) entry: Vec<CoverageEntry>,
}
pub(crate) fn parse(s: &str) -> Result<CoverageFile> {
Ok(toml::from_str(s)?)
}
pub(crate) fn render(f: &CoverageFile) -> Result<String> {
Ok(toml::to_string(f)?)
}
pub(crate) fn mode_is_valid(mode: &str) -> bool {
MODES.contains(&mode)
}
pub(crate) fn upsert(file: &mut CoverageFile, entry: CoverageEntry) {
if let Some(existing) = file.entry.iter_mut().find(|e| e.key == entry.key) {
*existing = entry;
} else {
file.entry.push(entry);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IsStale {
Fresh,
Stale,
Unknown,
}
impl From<Option<u32>> for IsStale {
fn from(count: Option<u32>) -> Self {
match count {
Some(0) => IsStale::Fresh,
Some(_) => IsStale::Stale,
None => IsStale::Unknown,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Composite {
cells: Vec<(CoverageEntry, IsStale)>,
}
fn key_order(k: &CoverageKey) -> (&str, &str, &str, &str) {
(
k.slice.as_str(),
k.requirement.as_str(),
k.contributing_change.as_str(),
k.mode.as_str(),
)
}
pub(crate) fn composite(entries: &[(CoverageEntry, IsStale)]) -> Composite {
let mut cells = entries.to_vec();
cells.sort_by(|a, b| key_order(&a.0.key).cmp(&key_order(&b.0.key)));
Composite { cells }
}
impl Composite {
pub(crate) fn is_empty(&self) -> bool {
self.cells.is_empty()
}
pub(crate) fn any_fresh_verified(&self) -> bool {
self.cells
.iter()
.any(|(e, s)| e.status == CoverageStatus::Verified && *s == IsStale::Fresh)
}
pub(crate) fn any_failed_or_blocked(&self) -> bool {
self.cells
.iter()
.any(|(e, _)| matches!(e.status, CoverageStatus::Failed | CoverageStatus::Blocked))
}
pub(crate) fn only_forward(&self) -> bool {
self.cells.iter().all(|(e, _)| {
matches!(
e.status,
CoverageStatus::Planned | CoverageStatus::InProgress
)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Verdict {
Coherent,
Divergent(DivergentReason),
Indeterminate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DivergentReason {
ObservedContradiction,
EvidenceOutrunsAuthored,
}
pub(crate) fn drift(authored: ReqStatus, composite: &Composite) -> Verdict {
use ReqStatus::{Active, Deprecated, InProgress, Pending, Retired, Superseded};
if matches!(authored, Retired | Superseded) {
return Verdict::Coherent;
}
if composite.any_failed_or_blocked() {
return Verdict::Divergent(DivergentReason::ObservedContradiction);
}
match authored {
Pending | InProgress => {
if composite.any_fresh_verified() {
Verdict::Divergent(DivergentReason::EvidenceOutrunsAuthored)
} else if composite.is_empty() || composite.only_forward() {
Verdict::Coherent
} else {
Verdict::Indeterminate
}
}
Active | Deprecated => {
if composite.is_empty() {
Verdict::Indeterminate
} else if composite.any_fresh_verified() {
Verdict::Coherent
} else {
Verdict::Indeterminate
}
}
Retired | Superseded => Verdict::Coherent,
}
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "tests: fail-fast unwrap on round-trip/parse is idiomatic"
)]
mod tests {
use super::*;
fn key(slice: &str, req: &str, change: &str, mode: &str) -> CoverageKey {
CoverageKey {
slice: slice.to_owned(),
requirement: req.to_owned(),
contributing_change: change.to_owned(),
mode: mode.to_owned(),
}
}
fn entry(k: CoverageKey, status: CoverageStatus, attested: Option<&str>) -> CoverageEntry {
CoverageEntry {
key: k,
status,
git_anchor: "anchor-abc123".to_owned(),
attested_date: attested.map(str::to_owned),
touched_paths: Vec::new(),
}
}
fn cell(
slice: &str,
req: &str,
change: &str,
status: CoverageStatus,
stale: IsStale,
) -> (CoverageEntry, IsStale) {
(entry(key(slice, req, change, "VT"), status, None), stale)
}
#[test]
fn round_trip_preserves_attested_present_and_absent() {
let file = CoverageFile {
entry: vec![
entry(
key("SL-042", "REQ-109", "SL-042", "VH"),
CoverageStatus::Verified,
Some("2026-06-12"),
),
entry(
key("SL-042", "REQ-108", "SL-041", "VT"),
CoverageStatus::Failed,
None,
),
],
};
let back = parse(&render(&file).unwrap()).unwrap();
assert_eq!(
back, file,
"mode + status + git_anchor + attested_date preserved"
);
let first = back.entry.first().unwrap();
assert_eq!(first.key.mode, "VH");
assert_eq!(first.status, CoverageStatus::Verified);
assert_eq!(first.git_anchor, "anchor-abc123");
assert_eq!(first.attested_date.as_deref(), Some("2026-06-12"));
assert!(back.entry.get(1).unwrap().attested_date.is_none());
}
#[test]
fn empty_file_round_trips() {
let empty = CoverageFile::default();
assert_eq!(parse(&render(&empty).unwrap()).unwrap(), empty);
}
#[test]
fn upsert_distinct_keys_appends() {
let mut file = CoverageFile::default();
upsert(
&mut file,
entry(
key("SL-042", "REQ-109", "SL-042", "VT"),
CoverageStatus::Planned,
None,
),
);
upsert(
&mut file,
entry(
key("SL-042", "REQ-108", "SL-042", "VT"),
CoverageStatus::Verified,
None,
),
);
assert_eq!(file.entry.len(), 2, "two distinct keys both surface");
}
#[test]
fn upsert_identical_key_replaces_with_latest_payload() {
let k = key("SL-042", "REQ-109", "SL-042", "VT");
let mut file = CoverageFile::default();
upsert(&mut file, entry(k.clone(), CoverageStatus::Planned, None));
upsert(
&mut file,
entry(k.clone(), CoverageStatus::Verified, Some("2026-06-12")),
);
assert_eq!(file.entry.len(), 1, "same key replaces, never duplicates");
let only = file.entry.first().unwrap();
assert_eq!(only.status, CoverageStatus::Verified, "latest payload wins");
assert_eq!(only.attested_date.as_deref(), Some("2026-06-12"));
}
#[test]
fn entries_differing_only_in_slice_coexist() {
let mut file = CoverageFile::default();
upsert(
&mut file,
entry(
key("SL-042", "REQ-109", "SL-042", "VT"),
CoverageStatus::Verified,
None,
),
);
upsert(
&mut file,
entry(
key("SL-099", "REQ-109", "SL-099", "VT"),
CoverageStatus::Planned,
None,
),
);
assert_eq!(
file.entry.len(),
2,
"same requirement across two slices coexists"
);
}
#[test]
fn mode_membership_is_vt_va_vh_only() {
assert!(mode_is_valid("VT"));
assert!(mode_is_valid("VA"));
assert!(mode_is_valid("VH"));
assert!(!mode_is_valid("VX"));
assert!(!mode_is_valid("vt"));
assert!(!mode_is_valid(""));
}
#[test]
fn coverage_entry_carries_observed_status_not_authored_reqstatus() {
let observed: CoverageStatus = entry(
key("SL-042", "REQ-109", "SL-042", "VT"),
CoverageStatus::Verified,
None,
)
.status;
assert_eq!(observed, CoverageStatus::Verified);
}
#[test]
fn coverage_and_requirement_status_live_in_distinct_stores() {
let coverage_path = ".doctrine/slice/042/coverage.toml";
let requirement_path = ".doctrine/requirement/109/requirement-109.toml";
assert_ne!(coverage_path, requirement_path);
}
#[test]
fn p2_entry_without_touched_paths_parses_and_defaults_empty() {
let body = r#"
[[entry]]
slice = "SL-042"
requirement = "REQ-109"
contributing_change = "SL-042"
mode = "VT"
status = "verified"
git_anchor = "anchor-abc123"
"#;
let file = parse(body).unwrap();
let only = file.entry.first().unwrap();
assert!(only.touched_paths.is_empty(), "absent field defaults empty");
}
#[test]
fn touched_paths_round_trips_when_present() {
let mut e = entry(
key("SL-042", "REQ-110", "SL-042", "VT"),
CoverageStatus::Verified,
None,
);
e.touched_paths = vec!["src/coverage.rs".to_owned(), "src/git.rs".to_owned()];
let file = CoverageFile { entry: vec![e] };
let back = parse(&render(&file).unwrap()).unwrap();
assert_eq!(back, file, "touched_paths survives the round-trip");
}
#[test]
fn is_stale_from_seam_count() {
assert_eq!(IsStale::from(Some(0)), IsStale::Fresh);
assert_eq!(IsStale::from(Some(1)), IsStale::Stale);
assert_eq!(IsStale::from(Some(42)), IsStale::Stale);
assert_eq!(IsStale::from(None), IsStale::Unknown);
}
#[test]
fn composite_is_order_independent() {
let ordered = vec![
cell(
"SL-040",
"REQ-110",
"SL-040",
CoverageStatus::Verified,
IsStale::Fresh,
),
cell(
"SL-042",
"REQ-110",
"SL-041",
CoverageStatus::Planned,
IsStale::Unknown,
),
cell(
"SL-041",
"REQ-110",
"SL-042",
CoverageStatus::Failed,
IsStale::Stale,
),
];
let shuffled = vec![
ordered.get(2).unwrap().clone(),
ordered.first().unwrap().clone(),
ordered.get(1).unwrap().clone(),
];
assert_eq!(
composite(&ordered),
composite(&shuffled),
"the fold is pure over in-memory input — order cannot change the value"
);
}
fn composites() -> Vec<(&'static str, Composite)> {
vec![
("empty", composite(&[])),
(
"fresh-verified",
composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Verified,
IsStale::Fresh,
)]),
),
(
"stale-verified",
composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Verified,
IsStale::Stale,
)]),
),
(
"failed-or-blocked",
composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Failed,
IsStale::Fresh,
)]),
),
(
"forward-only",
composite(&[
cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Planned,
IsStale::Unknown,
),
cell(
"SL-043",
"REQ-111",
"SL-043",
CoverageStatus::InProgress,
IsStale::Stale,
),
]),
),
]
}
#[test]
fn verdict_matrix_matches_the_decision_tree() {
use DivergentReason::{EvidenceOutrunsAuthored, ObservedContradiction};
use ReqStatus::{Active, Deprecated, InProgress, Pending, Retired, Superseded};
use Verdict::{Coherent, Divergent, Indeterminate};
let expect: Vec<(ReqStatus, [Verdict; 5])> = vec![
(
Pending,
[
Coherent, Divergent(EvidenceOutrunsAuthored), Indeterminate, Divergent(ObservedContradiction), Coherent, ],
),
(
InProgress,
[
Coherent,
Divergent(EvidenceOutrunsAuthored),
Indeterminate,
Divergent(ObservedContradiction),
Coherent,
],
),
(
Active,
[
Indeterminate, Coherent, Indeterminate, Divergent(ObservedContradiction), Indeterminate, ],
),
(
Deprecated,
[
Indeterminate,
Coherent,
Indeterminate,
Divergent(ObservedContradiction),
Indeterminate,
],
),
(Retired, [Coherent, Coherent, Coherent, Coherent, Coherent]),
(
Superseded,
[Coherent, Coherent, Coherent, Coherent, Coherent],
),
];
let states = composites();
for (authored, row) in &expect {
for (idx, (label, comp)) in states.iter().enumerate() {
let got = drift(*authored, comp);
let want = *row.get(idx).unwrap();
assert_eq!(
got, want,
"drift({:?}, {label}) expected {want:?}, got {got:?}",
authored
);
}
}
}
#[test]
fn drift_returns_verdict_not_reqstatus() {
let v: Verdict = drift(ReqStatus::Active, &composite(&[]));
assert_eq!(v, Verdict::Indeterminate);
}
#[test]
fn composite_predicates_read_the_cells() {
let c = composite(&[
cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Verified,
IsStale::Fresh,
),
cell(
"SL-043",
"REQ-111",
"SL-043",
CoverageStatus::Planned,
IsStale::Unknown,
),
]);
assert!(!c.is_empty());
assert!(c.any_fresh_verified());
assert!(!c.any_failed_or_blocked());
assert!(!c.only_forward(), "a Verified cell is not forward-only");
let stale_verified = composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Verified,
IsStale::Stale,
)]);
assert!(
!stale_verified.any_fresh_verified(),
"stale Verified is not fresh-verified"
);
}
}