spec_driven_docs/domain/
rule_id.rs1use std::fmt;
10
11macro_rules! rule_ids {
12 ($($variant:ident => $id:literal,)+) => {
13 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15 pub enum RuleId {
16 $(
17 #[doc = $id]
18 $variant,
19 )+
20 }
21
22 impl RuleId {
23 pub const ALL: &'static [Self] = &[$(Self::$variant),+];
25
26 #[must_use]
28 pub const fn as_str(self) -> &'static str {
29 match self {
30 $(Self::$variant => $id),+
31 }
32 }
33 }
34 };
35}
36
37rule_ids! {
38 CellCarriesOneReference => "comparison-docs:a-cell-carries-one-reference",
39 ComparisonCarriesALegend => "comparison-docs:a-comparison-carries-a-legend",
40 VerdictCarriesItsWord => "comparison-docs:a-verdict-carries-its-word",
41 EveryTableIsDated => "comparison-docs:every-table-is-dated",
42 TablePipesAreEscaped => "comparison-docs:table-pipes-are-escaped",
43 BodyStaysWithinWordCap => "decision-records:body-stays-within-350-words",
44 FilenameCarriesNoDigit => "decision-records:filename-carries-no-digit",
45 MergedRecordIsPermanent => "decision-records:merged-record-is-permanent",
46 RecordIsNotRevised => "decision-records:record-is-not-revised",
47 SkillObeysThePortableFormat => "distribution:a-skill-obeys-the-portable-format",
48 InitializationPreservesProjectContent => "distribution:initialization-preserves-project-content",
49 InstancesOperateOffline => "distribution:instances-operate-offline",
50 ManifestIdentifiesEveryOwnedFile => "distribution:manifest-identifies-every-owned-file",
51 SkillInstallPreviewsBeforeWriting => "distribution:skill-install-previews-before-writing",
52 SkillUninstallRemovesOnlyPayloadFiles => "distribution:skill-uninstall-removes-only-payload-files",
53 SkillsArePartOfThePayload => "distribution:skills-are-part-of-the-payload",
54 UpgradeConflictsAreAtomic => "distribution:upgrade-conflicts-are-atomic",
55 UserScopeFilesStayUnrecorded => "distribution:user-scope-files-stay-unrecorded",
56 AuthorInstructionsStayWithinBudget => "docs-format:author-instructions-stay-within-budget",
57 ChapterStaysWithinLineCap => "docs-format:chapter-stays-within-200-lines",
58 DocumentStatesThePresent => "docs-format:document-states-the-present",
59 DocumentUsesStructuralMarkdownOnly => "docs-format:document-uses-structural-markdown-only",
60 EveryBudgetCarriesAGate => "docs-format:every-budget-carries-a-gate",
61 FenceDeclaresALanguage => "docs-format:fence-declares-a-language",
62 ProseStaysUnwrapped => "docs-format:prose-stays-unwrapped",
63 KindPrefixCarriesASlug => "docs-foundations:a-kind-prefix-carries-a-slug",
64 ArtifactFilenamesCarryAKindPrefix => "docs-foundations:artifact-filenames-carry-a-kind-prefix",
65 CompanionArtifactsShareTheSpecName => "docs-foundations:companion-artifacts-share-the-spec-name",
66 SpecStatesThePresent => "docs-foundations:spec-states-the-present",
67 SpecWinsOverRecord => "docs-foundations:spec-wins-over-record",
68 SpecsAreCentralized => "docs-foundations:specs-are-centralized",
69 ProhibitionsAreCapped => "docs-specs:prohibitions-are-capped",
70 RequirementCarriesAVerification => "docs-specs:requirement-carries-a-verification",
71 RequirementCarriesFiveParts => "docs-specs:requirement-carries-five-parts",
72 RuleIdIsUniqueAndSlugged => "docs-specs:rule-id-is-unique-and-slugged",
73 RuleIdOutlivesItsSentence => "docs-specs:rule-id-outlives-its-sentence",
74 SpecStaysWithinLineCap => "docs-specs:spec-stays-within-300-lines",
75 StatementUsesAnEarsPattern => "docs-specs:statement-uses-an-ears-pattern",
76 UnenforcedRulesAreDeclared => "docs-specs:unenforced-rules-are-declared",
77 VerificationNamesALiveHook => "docs-specs:verification-names-a-live-hook",
78 BugzillaReportBodyFitsReportWidth => "known-issues:a-bugzilla-report-body-fits-in-79-columns",
79 FiledRecordCarriesItsReport => "known-issues:a-filed-record-carries-its-report",
80 RecordCarriesItsRetirementCondition => "known-issues:a-record-carries-its-retirement-condition",
81 RecordWalksTheMechanism => "known-issues:a-record-walks-the-mechanism",
82 CaseIdIsASlug => "known-issues:case-id-is-a-slug",
83 CanonGateIsNotDelivered => "release:a-canon-gate-is-not-delivered",
84 ReleasedVersionIsNotReAuthored => "release:a-released-version-is-not-re-authored",
85 TagDerivesFromTheVersionFile => "release:a-tag-derives-from-the-version-file",
86 LicenseDeclaresBothHalves => "release:license-declares-both-halves",
87 DeliveredGateSetIsDeclaredOnce => "release:the-delivered-gate-set-is-declared-once",
88 VersionsAreSemanticAndAligned => "release:versions-are-semantic-and-aligned",
89 CommentCitesTheRule => "spec-to-code:a-comment-cites-the-rule",
90 CommentNamesNoRecord => "spec-to-code:a-comment-names-no-record",
91 GateMessageCitesTheRule => "spec-to-code:a-gate-message-cites-the-rule",
92 SpecChangeIsTyped => "spec-to-code:a-spec-change-is-typed",
93 SpecMayLeadItsCode => "spec-to-code:a-spec-may-lead-its-code",
94 SuppressionNamesItsCase => "spec-to-code:a-suppression-names-its-case",
95 EntryDocumentCitesRuleIds => "spec-to-code:an-entry-document-cites-rule-ids",
96 UnenactedRulesAreTheBacklog => "spec-to-code:unenacted-rules-are-the-backlog",
97}
98
99impl fmt::Display for RuleId {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str(self.as_str())
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn slugs_are_well_formed_and_unique() {
111 let mut seen = std::collections::BTreeSet::new();
112 for rule in RuleId::ALL {
113 let id = rule.as_str();
114 let (domain, name) = id.split_once(':').unwrap();
115 let assert_slug = |part: &str| {
116 assert!(!part.is_empty(), "{id} has an empty half");
117 assert!(
118 part.bytes()
119 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'),
120 "{id} is not a slug pair"
121 );
122 };
123 assert_slug(domain);
124 assert_slug(name);
125 assert!(seen.insert(id), "{id} is duplicated");
126 }
127 }
128
129 #[test]
130 fn display_renders_the_slug_pair() {
131 assert_eq!(
132 RuleId::ChapterStaysWithinLineCap.to_string(),
133 "docs-format:chapter-stays-within-200-lines"
134 );
135 }
136}