Skip to main content

spec_driven_docs/domain/
policy.rs

1//! The sentinel rules: one per feature a project can declare against its
2//! own adopted specifications.
3//!
4//! A feature is available the moment the binary carries it. Its normative
5//! text arrives as a new adopted specification, seeded on the next upgrade,
6//! and each such specification carries one rule that authorizes the
7//! declaration. That rule ID is the sentinel. Whether an instance's
8//! specifications authorize what its configuration declares is read by
9//! presence of the sentinel and never by matching prose, because a rule ID
10//! survives rewording and a sentence does not.
11//!
12//! A sentinel never joins a delivered gate's `cites` list. That gate check is
13//! always-run, so a cited ID an instance's specifications lack would fail
14//! every commit in the window between upgrading the binary and running
15//! `sdd upgrade`. The canon suite holds that boundary.
16
17use crate::domain::rule_id::RuleId;
18
19/// One sentinel: the rule, and the adopted specification that owns it.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Sentinel {
22    /// The rule an instance's specifications must define.
23    pub rule: RuleId,
24    /// The embedded specification that carries it.
25    pub source: &'static str,
26    /// Where an instance holds that specification, with `{docs_root}`
27    /// templated.
28    pub destination: &'static str,
29    /// The declaration it authorizes, for the note that names it.
30    pub declares: &'static str,
31}
32
33/// Every sentinel this binary knows.
34pub const SENTINELS: &[Sentinel] = &[
35    Sentinel {
36        rule: RuleId::RecordedDimensionOnlyShrinks,
37        source: "_docs/specs/SPEC-budget-debt.md",
38        destination: "{docs_root}/specs/SPEC-budget-debt.md",
39        declares: ".spec-driven-docs/debt.yaml records budget debt",
40    },
41    Sentinel {
42        rule: RuleId::ProjectSelectsOneSource,
43        source: "_docs/specs/SPEC-writing-policy.md",
44        destination: "{docs_root}/specs/SPEC-writing-policy.md",
45        declares: ".spec-driven-docs/config.yaml selects a writing style other than builtin",
46    },
47];
48
49/// The sentinel a rule is, if it is one.
50#[must_use]
51pub fn sentinel(rule: RuleId) -> Option<&'static Sentinel> {
52    SENTINELS.iter().find(|s| s.rule == rule)
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn every_sentinel_is_owned_by_an_embedded_adopted_specification() {
61        for s in SENTINELS {
62            assert!(
63                crate::embedded::asset(s.source).is_some(),
64                "{} is not embedded",
65                s.source
66            );
67            let adopted = crate::domain::profile::ProfileId::KnowledgeBase
68                .profile()
69                .adopted
70                .iter()
71                .any(|p| p.source == s.source && p.destination == s.destination);
72            assert!(adopted, "{} is not an adopted projection", s.source);
73            let text = crate::embedded::asset(s.source)
74                .and_then(|bytes| std::str::from_utf8(bytes).ok())
75                .unwrap_or_default();
76            assert!(
77                crate::embedded::rule_ids_in(text).any(|id| id == s.rule.as_str()),
78                "{} does not define {}",
79                s.source,
80                s.rule
81            );
82        }
83    }
84
85    #[test]
86    fn no_delivered_gate_cites_a_sentinel() {
87        for row in crate::gates::GATES {
88            for rule in row.cites {
89                assert!(
90                    sentinel(*rule).is_none(),
91                    "{} cites the sentinel {rule}; an instance upgraded after the binary would fail every commit",
92                    row.id
93                );
94            }
95        }
96    }
97}