Skip to main content

spec_driven_docs/domain/
gate_id.rs

1//! Gate identifiers: one variant per delivered gate.
2//!
3//! A gate ID names an executable check a consumer wires as a pre-commit hook
4//! and invokes as `sdd gate <id>`. This enum is only the identity; what a
5//! gate checks, how it is wired, and its display name live in the gate
6//! registry, and canon-only checks are cargo tests rather than variants here.
7
8use std::fmt;
9
10use clap::ValueEnum;
11
12/// A delivered gate, addressable as `sdd gate <id>` and as a pre-commit hook id.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum)]
14#[value(rename_all = "kebab-case")]
15pub enum GateId {
16    /// Decision record citations resolve to a live rule.
17    AdrCitesALiveRule,
18    /// Decision record filenames are dated-free slugs.
19    AdrFilenameShape,
20    /// Decision record bodies stay within the word cap.
21    AdrWordCap,
22    /// Agent digests stay within their line budgets.
23    AgentsDigestSize,
24    /// Chapters and catalogs stay within their line caps.
25    ChapterSizeCap,
26    /// Comparison tables carry a verification date.
27    ComparisonDatedTables,
28    /// Comparison table pipes are escaped inside cells.
29    ComparisonEscapedPipes,
30    /// Comparison documents carry a legend.
31    ComparisonLegend,
32    /// Comparison cells carry at most one reference.
33    ComparisonOneReferencePerCell,
34    /// Comparison verdicts carry their word.
35    ComparisonVerdictWord,
36    /// Every rule ID a gate can print resolves to a local requirement.
37    GateMessageCitesARule,
38    /// The instance manifest is present and coherent.
39    InstanceManifest,
40    /// Bugzilla-bound report bodies fit the tracker's width.
41    KiBugzillaReportWidth,
42    /// Known-issue records date their last check.
43    KiCheckedDate,
44    /// Known-issue filenames are slugged case IDs.
45    KiFilenameShape,
46    /// Known-issue records carry one filing state.
47    KiFiling,
48    /// Known-issue records walk the mechanism.
49    KiMechanismWalkthrough,
50    /// Filed known-issue records carry their report body.
51    KiReportBody,
52    /// Known-issue records carry a retirement condition.
53    KiRetireWhen,
54    /// Known-issue records carry one state.
55    KiState,
56    /// Files carry no absolute path into a person's home directory.
57    NoPersonalPath,
58    /// Documents state the present rather than narrate their edits.
59    NoSelfNarration,
60    /// Paragraphs occupy one source line rather than hard-wrap.
61    ProseStaysUnwrapped,
62    /// Typed clauses in the plan zone carry a well-formed rule ID.
63    SpecChangeIsTyped,
64    /// Spec requirements carry all five parts.
65    SpecRequirementParts,
66    /// Rule IDs are unique across the local specs.
67    SpecRuleIdUnique,
68    /// Specs stay within their line cap and carry a TOC when long.
69    SpecSizeCap,
70    /// Spec verification lines name hooks that exist.
71    SpecVerifyHooksExist,
72    /// Suppression comments name a known-issue case.
73    SuppressionNamesItsCase,
74    /// Prose follows the objective `SimpleEnglish` checks.
75    SimpleEnglish,
76    /// The tracking registry is valid, current, and its dependents exist.
77    TrackingRegistry,
78}
79
80impl GateId {
81    /// Every delivered gate, in id order.
82    pub const ALL: &'static [Self] = &[
83        Self::AdrCitesALiveRule,
84        Self::AdrFilenameShape,
85        Self::AdrWordCap,
86        Self::AgentsDigestSize,
87        Self::ChapterSizeCap,
88        Self::ComparisonDatedTables,
89        Self::ComparisonEscapedPipes,
90        Self::ComparisonLegend,
91        Self::ComparisonOneReferencePerCell,
92        Self::ComparisonVerdictWord,
93        Self::GateMessageCitesARule,
94        Self::InstanceManifest,
95        Self::KiBugzillaReportWidth,
96        Self::KiCheckedDate,
97        Self::KiFilenameShape,
98        Self::KiFiling,
99        Self::KiMechanismWalkthrough,
100        Self::KiReportBody,
101        Self::KiRetireWhen,
102        Self::KiState,
103        Self::NoPersonalPath,
104        Self::NoSelfNarration,
105        Self::ProseStaysUnwrapped,
106        Self::SpecChangeIsTyped,
107        Self::SpecRequirementParts,
108        Self::SpecRuleIdUnique,
109        Self::SpecSizeCap,
110        Self::SpecVerifyHooksExist,
111        Self::SuppressionNamesItsCase,
112        Self::SimpleEnglish,
113        Self::TrackingRegistry,
114    ];
115
116    /// The kebab-case id used on the command line and as the hook id.
117    #[must_use]
118    pub const fn as_str(self) -> &'static str {
119        match self {
120            Self::AdrCitesALiveRule => "adr-cites-a-live-rule",
121            Self::AdrFilenameShape => "adr-filename-shape",
122            Self::AdrWordCap => "adr-word-cap",
123            Self::AgentsDigestSize => "agents-digest-size",
124            Self::ChapterSizeCap => "chapter-size-cap",
125            Self::ComparisonDatedTables => "comparison-dated-tables",
126            Self::ComparisonEscapedPipes => "comparison-escaped-pipes",
127            Self::ComparisonLegend => "comparison-legend",
128            Self::ComparisonOneReferencePerCell => "comparison-one-reference-per-cell",
129            Self::ComparisonVerdictWord => "comparison-verdict-word",
130            Self::GateMessageCitesARule => "gate-message-cites-a-rule",
131            Self::InstanceManifest => "instance-manifest",
132            Self::KiBugzillaReportWidth => "ki-bugzilla-report-width",
133            Self::KiCheckedDate => "ki-checked-date",
134            Self::KiFilenameShape => "ki-filename-shape",
135            Self::KiFiling => "ki-filing",
136            Self::KiMechanismWalkthrough => "ki-mechanism-walkthrough",
137            Self::KiReportBody => "ki-report-body",
138            Self::KiRetireWhen => "ki-retire-when",
139            Self::KiState => "ki-state",
140            Self::NoPersonalPath => "no-personal-path",
141            Self::NoSelfNarration => "no-self-narration",
142            Self::ProseStaysUnwrapped => "prose-stays-unwrapped",
143            Self::SpecChangeIsTyped => "spec-change-is-typed",
144            Self::SpecRequirementParts => "spec-requirement-parts",
145            Self::SpecRuleIdUnique => "spec-rule-id-unique",
146            Self::SpecSizeCap => "spec-size-cap",
147            Self::SpecVerifyHooksExist => "spec-verify-hooks-exist",
148            Self::SuppressionNamesItsCase => "suppression-names-its-case",
149            Self::SimpleEnglish => "simple-english",
150            Self::TrackingRegistry => "tracking-registry",
151        }
152    }
153}
154
155impl std::str::FromStr for GateId {
156    type Err = String;
157
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        Self::ALL
160            .iter()
161            .copied()
162            .find(|gate| gate.as_str() == s)
163            .ok_or_else(|| format!("unknown gate: {s}"))
164    }
165}
166
167impl fmt::Display for GateId {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        f.write_str(self.as_str())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn all_lists_every_variant_once() {
179        let mut seen = std::collections::BTreeSet::new();
180        for gate in GateId::ALL {
181            assert!(seen.insert(gate.as_str()), "{gate} is duplicated");
182        }
183        assert_eq!(GateId::ALL.len(), 31);
184    }
185
186    #[test]
187    fn clap_value_matches_as_str() {
188        for gate in GateId::ALL {
189            let value = gate.to_possible_value().unwrap();
190            assert_eq!(value.get_name(), gate.as_str());
191        }
192    }
193}