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 filenames are dated-free slugs.
17    AdrFilenameShape,
18    /// Decision record bodies stay within the word cap.
19    AdrWordCap,
20    /// Agent digests stay within their line budgets.
21    AgentsDigestSize,
22    /// Chapters and catalogs stay within their line caps.
23    ChapterSizeCap,
24    /// Comparison tables carry a verification date.
25    ComparisonDatedTables,
26    /// Comparison table pipes are escaped inside cells.
27    ComparisonEscapedPipes,
28    /// Comparison documents carry a legend.
29    ComparisonLegend,
30    /// Comparison cells carry at most one reference.
31    ComparisonOneReferencePerCell,
32    /// Comparison verdicts carry their word.
33    ComparisonVerdictWord,
34    /// Every rule ID a gate can print resolves to a local requirement.
35    GateMessageCitesARule,
36    /// The instance manifest is present and coherent.
37    InstanceManifest,
38    /// Bugzilla-bound report bodies fit the tracker's width.
39    KiBugzillaReportWidth,
40    /// Known-issue filenames are slugged case IDs.
41    KiFilenameShape,
42    /// Known-issue records walk the mechanism.
43    KiMechanismWalkthrough,
44    /// Filed known-issue records carry their report body.
45    KiReportBody,
46    /// Known-issue records carry a retirement condition.
47    KiRetireWhen,
48    /// Documents state the present rather than narrate their edits.
49    NoSelfNarration,
50    /// Paragraphs occupy one source line rather than hard-wrap.
51    ProseStaysUnwrapped,
52    /// Spec requirements carry all five parts.
53    SpecRequirementParts,
54    /// Rule IDs are unique across the local specs.
55    SpecRuleIdUnique,
56    /// Specs stay within their line cap and carry a TOC when long.
57    SpecSizeCap,
58    /// Spec verification lines name hooks that exist.
59    SpecVerifyHooksExist,
60    /// Suppression comments name a known-issue case.
61    SuppressionNamesItsCase,
62}
63
64impl GateId {
65    /// Every delivered gate, in id order.
66    pub const ALL: &'static [Self] = &[
67        Self::AdrFilenameShape,
68        Self::AdrWordCap,
69        Self::AgentsDigestSize,
70        Self::ChapterSizeCap,
71        Self::ComparisonDatedTables,
72        Self::ComparisonEscapedPipes,
73        Self::ComparisonLegend,
74        Self::ComparisonOneReferencePerCell,
75        Self::ComparisonVerdictWord,
76        Self::GateMessageCitesARule,
77        Self::InstanceManifest,
78        Self::KiBugzillaReportWidth,
79        Self::KiFilenameShape,
80        Self::KiMechanismWalkthrough,
81        Self::KiReportBody,
82        Self::KiRetireWhen,
83        Self::NoSelfNarration,
84        Self::ProseStaysUnwrapped,
85        Self::SpecRequirementParts,
86        Self::SpecRuleIdUnique,
87        Self::SpecSizeCap,
88        Self::SpecVerifyHooksExist,
89        Self::SuppressionNamesItsCase,
90    ];
91
92    /// The kebab-case id used on the command line and as the hook id.
93    #[must_use]
94    pub const fn as_str(self) -> &'static str {
95        match self {
96            Self::AdrFilenameShape => "adr-filename-shape",
97            Self::AdrWordCap => "adr-word-cap",
98            Self::AgentsDigestSize => "agents-digest-size",
99            Self::ChapterSizeCap => "chapter-size-cap",
100            Self::ComparisonDatedTables => "comparison-dated-tables",
101            Self::ComparisonEscapedPipes => "comparison-escaped-pipes",
102            Self::ComparisonLegend => "comparison-legend",
103            Self::ComparisonOneReferencePerCell => "comparison-one-reference-per-cell",
104            Self::ComparisonVerdictWord => "comparison-verdict-word",
105            Self::GateMessageCitesARule => "gate-message-cites-a-rule",
106            Self::InstanceManifest => "instance-manifest",
107            Self::KiBugzillaReportWidth => "ki-bugzilla-report-width",
108            Self::KiFilenameShape => "ki-filename-shape",
109            Self::KiMechanismWalkthrough => "ki-mechanism-walkthrough",
110            Self::KiReportBody => "ki-report-body",
111            Self::KiRetireWhen => "ki-retire-when",
112            Self::NoSelfNarration => "no-self-narration",
113            Self::ProseStaysUnwrapped => "prose-stays-unwrapped",
114            Self::SpecRequirementParts => "spec-requirement-parts",
115            Self::SpecRuleIdUnique => "spec-rule-id-unique",
116            Self::SpecSizeCap => "spec-size-cap",
117            Self::SpecVerifyHooksExist => "spec-verify-hooks-exist",
118            Self::SuppressionNamesItsCase => "suppression-names-its-case",
119        }
120    }
121}
122
123impl std::str::FromStr for GateId {
124    type Err = String;
125
126    fn from_str(s: &str) -> Result<Self, Self::Err> {
127        Self::ALL
128            .iter()
129            .copied()
130            .find(|gate| gate.as_str() == s)
131            .ok_or_else(|| format!("unknown gate: {s}"))
132    }
133}
134
135impl fmt::Display for GateId {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(self.as_str())
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn all_lists_every_variant_once() {
147        let mut seen = std::collections::BTreeSet::new();
148        for gate in GateId::ALL {
149            assert!(seen.insert(gate.as_str()), "{gate} is duplicated");
150        }
151        assert_eq!(GateId::ALL.len(), 23);
152    }
153
154    #[test]
155    fn clap_value_matches_as_str() {
156        for gate in GateId::ALL {
157            let value = gate.to_possible_value().unwrap();
158            assert_eq!(value.get_name(), gate.as_str());
159        }
160    }
161}