use corpora_core::{Authority, Diagnostic, Facet, Graph, Kind, Lifecycle, Status};
use crate::Rule;
pub struct Schema;
impl Rule for Schema {
fn code(&self) -> &'static str {
"SCHEMA"
}
fn check(&self, g: &Graph, out: &mut Vec<Diagnostic>) {
for r in g.records() {
let allowed = allowed_authorities(r.kind);
if !allowed.contains(&r.authority) {
out.push(Diagnostic::error(
"SCHEMA",
&r.path,
format!(
"authority {:?} is not allowed for kind {:?} (expected one of {:?})",
r.authority, r.kind, allowed
),
));
}
if let Facet::Decision(d) = &r.facet {
let lifecycles = allowed_lifecycles(d.status);
if !lifecycles.contains(&r.lifecycle) {
out.push(Diagnostic::error(
"SCHEMA",
&r.path,
format!(
"status {:?} is inconsistent with lifecycle {:?} (expected one of {:?})",
d.status, r.lifecycle, lifecycles
),
));
}
}
}
}
}
fn allowed_authorities(kind: Kind) -> &'static [Authority] {
use Authority::*;
match kind {
Kind::Decision => &[Normative],
Kind::Axiom => &[Axiomatic, Normative],
Kind::Invariant => &[Normative],
Kind::Architecture => &[Normative],
Kind::Current => &[Descriptive],
Kind::Roadmap => &[Prospective],
Kind::Milestone => &[Operational, Prospective],
Kind::Evidence => &[Evidence],
Kind::ReviewLog => &[Historical],
Kind::Evolution => &[Historical],
Kind::Handoff => &[Operational],
Kind::Explainer => &[Explanatory],
Kind::Index => &[Navigational],
}
}
fn allowed_lifecycles(status: Status) -> &'static [Lifecycle] {
use Lifecycle::*;
match status {
Status::Open => &[Draft],
Status::Proposed => &[Draft],
Status::Accepted => &[Current],
Status::Superseded => &[Superseded],
Status::Deprecated => &[Historical],
Status::Rejected => &[Historical, Draft],
}
}
#[cfg(test)]
mod tests {
use super::*;
use corpora_core::*;
use std::sync::Arc;
fn build(records: Vec<Record>) -> Graph {
Graph::build(records.into_iter().map(Arc::new).collect()).0
}
fn decision(status: Status, lifecycle: Lifecycle, authority: Authority) -> Record {
Record::minimal(
Some(Id("D1".into())),
DocPath("d1.md".into()),
Kind::Decision,
lifecycle,
authority,
Facet::Decision(DecisionFacet {
status,
date: Date("2026-06-21".into()),
implementation: None,
fork: None,
realized_by: vec![],
}),
)
}
#[test]
fn wrong_authority_for_kind_flagged() {
let g = build(vec![decision(Status::Accepted, Lifecycle::Current, Authority::Descriptive)]);
let mut out = Vec::new();
Schema.check(&g, &mut out);
assert_eq!(out.len(), 1);
assert_eq!(out[0].code, "SCHEMA");
}
#[test]
fn status_lifecycle_mismatch_flagged() {
let g = build(vec![decision(Status::Accepted, Lifecycle::Draft, Authority::Normative)]);
let mut out = Vec::new();
Schema.check(&g, &mut out);
assert!(out.iter().any(|d| d.message.contains("lifecycle")), "{out:?}");
}
#[test]
fn consistent_decision_passes() {
let g = build(vec![decision(Status::Accepted, Lifecycle::Current, Authority::Normative)]);
let mut out = Vec::new();
Schema.check(&g, &mut out);
assert!(out.is_empty(), "{out:?}");
}
}