use corpora_core::{Diagnostic, Graph, Lifecycle};
use crate::Rule;
pub struct SupersessionIntegrity;
impl Rule for SupersessionIntegrity {
fn code(&self) -> &'static str {
"SUPERSESSION"
}
fn check(&self, g: &Graph, out: &mut Vec<Diagnostic>) {
for r in g.records() {
let Some(id) = &r.id else { continue };
match (r.lifecycle, g.is_superseded(id)) {
(Lifecycle::Superseded, false) => out.push(Diagnostic::error(
"SUPERSESSION",
&r.path,
format!("{} has lifecycle=superseded but nothing supersedes it", id.0),
)),
(life, true) if life != Lifecycle::Superseded => {
let succ = g
.effective_successor(id)
.map(|s| s.0.as_str())
.unwrap_or("(unresolved)");
out.push(Diagnostic::error(
"SUPERSESSION",
&r.path,
format!("{} is superseded (by {succ}) but lifecycle is {life:?}", id.0),
));
}
_ => {}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use corpora_core::*;
use std::sync::Arc;
fn narrative(id: &str, lifecycle: Lifecycle, supersedes: &[&str]) -> Record {
let mut r = Record::minimal(
Some(Id(id.into())),
DocPath(format!("{id}.md")),
Kind::Decision,
lifecycle,
Authority::Normative,
Facet::Decision(DecisionFacet {
status: Status::Accepted,
date: Date("2026-06-21".into()),
implementation: None,
fork: None,
realized_by: vec![],
}),
);
r.edges.supersedes = supersedes.iter().map(|s| Id((*s).into())).collect();
r
}
fn run(records: Vec<Record>) -> Vec<Diagnostic> {
let g = Graph::build(records.into_iter().map(Arc::new).collect()).0;
let mut out = Vec::new();
SupersessionIntegrity.check(&g, &mut out);
out
}
#[test]
fn superseded_lifecycle_without_successor_flagged() {
let out = run(vec![narrative("D1", Lifecycle::Superseded, &[])]);
assert_eq!(out.len(), 1, "{out:?}");
}
#[test]
fn superseded_record_with_wrong_lifecycle_flagged() {
let out = run(vec![
narrative("D1", Lifecycle::Current, &[]),
narrative("D2", Lifecycle::Current, &["D1"]),
]);
assert!(out.iter().any(|d| d.message.contains("D1 is superseded")), "{out:?}");
}
#[test]
fn consistent_pair_passes() {
let out = run(vec![
narrative("D1", Lifecycle::Superseded, &[]),
narrative("D2", Lifecycle::Current, &["D1"]),
]);
assert!(out.is_empty(), "{out:?}");
}
}