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 std::sync::LazyLock;
18
19use crate::domain::projection::SentinelDeclaration;
20use crate::domain::rule_id::RuleId;
21
22/// One sentinel: the rule, and the adopted specification that owns it.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Sentinel {
25    /// The rule an instance's specifications must define.
26    pub rule: RuleId,
27    /// The payload specification that carries it.
28    pub source: String,
29    /// Where an instance holds that specification, with `{docs_root}`
30    /// templated.
31    pub destination: String,
32    /// The declaration it authorizes, for the note that names it.
33    pub declares: String,
34}
35
36/// Every sentinel this binary's own release declares.
37///
38/// A declared rule this engine does not know is dropped rather than
39/// refused: a release may authorize a feature a later engine renamed, and
40/// a sentinel nothing can look up is a note nobody can act on.
41pub static SENTINELS: LazyLock<Vec<Sentinel>> =
42    LazyLock::new(|| from_declaration(&crate::domain::profile::DECLARATION.sentinels));
43
44/// Read a declaration's sentinels, keeping the ones this engine knows.
45#[must_use]
46pub fn from_declaration(declared: &[SentinelDeclaration]) -> Vec<Sentinel> {
47    declared
48        .iter()
49        .filter_map(|entry| {
50            let rule = RuleId::ALL
51                .iter()
52                .copied()
53                .find(|known| known.as_str() == entry.rule)?;
54            Some(Sentinel {
55                rule,
56                source: entry.source.clone(),
57                destination: entry.destination.clone(),
58                declares: entry.declares.clone(),
59            })
60        })
61        .collect()
62}
63
64/// The sentinel a rule is, if it is one.
65#[must_use]
66pub fn sentinel(rule: RuleId) -> Option<&'static Sentinel> {
67    SENTINELS.iter().find(|held| held.rule == rule)
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn every_sentinel_is_owned_by_an_embedded_adopted_specification() {
76        for s in SENTINELS.iter() {
77            assert!(
78                crate::embedded::asset(&s.source).is_some(),
79                "{} is not embedded",
80                s.source
81            );
82            let adopted = crate::domain::profile::ProfileId::KnowledgeBase
83                .profile()
84                .adopted
85                .iter()
86                .any(|p| p.source == s.source && p.destination == s.destination);
87            assert!(adopted, "{} is not an adopted projection", s.source);
88            let text = crate::embedded::asset(&s.source)
89                .and_then(|bytes| std::str::from_utf8(bytes).ok())
90                .unwrap_or_default();
91            assert!(
92                crate::embedded::rule_ids_in(text).any(|id| id == s.rule.as_str()),
93                "{} does not define {}",
94                s.source,
95                s.rule
96            );
97        }
98    }
99
100    #[test]
101    fn no_delivered_gate_cites_a_sentinel() {
102        for row in crate::gates::GATES {
103            for rule in row.cites {
104                assert!(
105                    sentinel(*rule).is_none(),
106                    "{} cites the sentinel {rule}; an instance upgraded after the binary would fail every commit",
107                    row.id
108                );
109            }
110        }
111    }
112}