corpora-rules 0.1.0

Validation rule pack (E3, held-decision, schema) for the corpora docs validator.
Documentation
//! Held-detection — makes "forgotten" machine-detectable (schema-v0 §6). An accepted
//! decision with no implementation and no `realized_by` is unscheduled debt.

use corpora_core::{Diagnostic, Facet, Graph, Impl, Status};

use crate::Rule;

pub struct HeldDecision;

impl Rule for HeldDecision {
    fn code(&self) -> &'static str {
        "HELD"
    }

    fn check(&self, g: &Graph, out: &mut Vec<Diagnostic>) {
        for r in g.records() {
            let Facet::Decision(d) = &r.facet else { continue };
            if d.status == Status::Accepted
                && d.implementation == Some(Impl::Absent)
                && d.realized_by.is_empty()
            {
                let id = r.id.as_ref().map(|i| i.0.as_str()).unwrap_or("<no-id>");
                out.push(Diagnostic::warn(
                    "HELD",
                    &r.path,
                    format!("{id} accepted but unrealized and unscheduled"),
                ));
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use corpora_core::*;
    use std::sync::Arc;

    fn decision(id: &str, status: Status, implementation: Option<Impl>, realized: Vec<Id>) -> Record {
        Record::minimal(
            Some(Id(id.into())),
            DocPath(format!("{id}.md")),
            Kind::Decision,
            Lifecycle::Current,
            Authority::Normative,
            Facet::Decision(DecisionFacet {
                status,
                date: Date("2026-06-21".into()),
                implementation,
                fork: None,
                realized_by: realized,
            }),
        )
    }

    #[test]
    fn flags_unrealized_unscheduled() {
        let held = decision("D1", Status::Accepted, Some(Impl::Absent), vec![]);
        let scheduled = decision("D2", Status::Accepted, Some(Impl::Absent), vec![Id("roadmap.v0".into())]);
        let done = decision("D3", Status::Accepted, Some(Impl::Implemented), vec![]);

        let (g, _) = Graph::build(vec![Arc::new(held), Arc::new(scheduled), Arc::new(done)]);
        let mut out = Vec::new();
        HeldDecision.check(&g, &mut out);

        assert_eq!(out.len(), 1);
        assert_eq!(out[0].code, "HELD");
    }
}