1use corpora_core::{Authority, Diagnostic, Facet, Graph, Kind, Lifecycle, Status};
6
7use crate::Rule;
8
9pub struct Schema;
10
11impl Rule for Schema {
12 fn code(&self) -> &'static str {
13 "SCHEMA"
14 }
15
16 fn check(&self, g: &Graph, out: &mut Vec<Diagnostic>) {
17 for r in g.records() {
18 let allowed = allowed_authorities(r.kind);
19 if !allowed.contains(&r.authority) {
20 out.push(Diagnostic::error(
21 "SCHEMA",
22 &r.path,
23 format!(
24 "authority {:?} is not allowed for kind {:?} (expected one of {:?})",
25 r.authority, r.kind, allowed
26 ),
27 ));
28 }
29
30 if let Facet::Decision(d) = &r.facet {
31 let lifecycles = allowed_lifecycles(d.status);
32 if !lifecycles.contains(&r.lifecycle) {
33 out.push(Diagnostic::error(
34 "SCHEMA",
35 &r.path,
36 format!(
37 "status {:?} is inconsistent with lifecycle {:?} (expected one of {:?})",
38 d.status, r.lifecycle, lifecycles
39 ),
40 ));
41 }
42 }
43 }
44 }
45}
46
47fn allowed_authorities(kind: Kind) -> &'static [Authority] {
48 use Authority::*;
49 match kind {
50 Kind::Decision => &[Normative],
51 Kind::Axiom => &[Axiomatic, Normative],
52 Kind::Invariant => &[Normative],
53 Kind::Architecture => &[Normative],
54 Kind::Current => &[Descriptive],
55 Kind::Roadmap => &[Prospective],
56 Kind::Milestone => &[Operational, Prospective],
57 Kind::Evidence => &[Evidence],
58 Kind::ReviewLog => &[Historical],
59 Kind::Evolution => &[Historical],
60 Kind::Handoff => &[Operational],
61 Kind::Explainer => &[Explanatory],
62 Kind::Index => &[Navigational],
63 }
64}
65
66fn allowed_lifecycles(status: Status) -> &'static [Lifecycle] {
71 use Lifecycle::*;
72 match status {
73 Status::Open => &[Draft],
74 Status::Proposed => &[Draft],
75 Status::Accepted => &[Current],
76 Status::Superseded => &[Superseded],
77 Status::Deprecated => &[Historical],
78 Status::Rejected => &[Historical, Draft],
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use corpora_core::*;
86 use std::sync::Arc;
87
88 fn build(records: Vec<Record>) -> Graph {
89 Graph::build(records.into_iter().map(Arc::new).collect()).0
90 }
91
92 fn decision(status: Status, lifecycle: Lifecycle, authority: Authority) -> Record {
93 Record::minimal(
94 Some(Id("D1".into())),
95 DocPath("d1.md".into()),
96 Kind::Decision,
97 lifecycle,
98 authority,
99 Facet::Decision(DecisionFacet {
100 status,
101 date: Date("2026-06-21".into()),
102 implementation: None,
103 fork: None,
104 realized_by: vec![],
105 }),
106 )
107 }
108
109 #[test]
110 fn wrong_authority_for_kind_flagged() {
111 let g = build(vec![decision(Status::Accepted, Lifecycle::Current, Authority::Descriptive)]);
112 let mut out = Vec::new();
113 Schema.check(&g, &mut out);
114 assert_eq!(out.len(), 1);
115 assert_eq!(out[0].code, "SCHEMA");
116 }
117
118 #[test]
119 fn status_lifecycle_mismatch_flagged() {
120 let g = build(vec![decision(Status::Accepted, Lifecycle::Draft, Authority::Normative)]);
122 let mut out = Vec::new();
123 Schema.check(&g, &mut out);
124 assert!(out.iter().any(|d| d.message.contains("lifecycle")), "{out:?}");
125 }
126
127 #[test]
128 fn consistent_decision_passes() {
129 let g = build(vec![decision(Status::Accepted, Lifecycle::Current, Authority::Normative)]);
130 let mut out = Vec::new();
131 Schema.check(&g, &mut out);
132 assert!(out.is_empty(), "{out:?}");
133 }
134}