Skip to main content

spec_driven_docs/domain/
rule_id.rs

1//! Rule identifiers: one variant per requirement the specs define.
2//!
3//! A rule ID is an address — `domain:rule` — that a failure message cites and
4//! a reader follows to the sentence that binds. This enum is the compile-time
5//! form of every `` ### `domain:rule` `` heading under the canon's specs; a
6//! parity test in `embedded` holds the two sets equal. Rule prose lives in
7//! the specs, never here.
8
9use std::fmt;
10
11macro_rules! rule_ids {
12    ($($variant:ident => $id:literal,)+) => {
13        /// A `domain:rule` requirement address defined by a spec.
14        #[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            /// Every rule the specs define, in slug order.
24            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
25
26            /// The `domain:rule` slug pair this variant addresses.
27            #[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    CitationResolvesToARule => "decision-records:a-citation-resolves-to-a-rule",
44    BodyStaysWithinWordCap => "decision-records:body-stays-within-350-words",
45    FilenameCarriesNoDigit => "decision-records:filename-carries-no-digit",
46    MergedRecordIsPermanent => "decision-records:merged-record-is-permanent",
47    RecordIsNotRevised => "decision-records:record-is-not-revised",
48    DeclaredLocationIsNamedByItsVariable => "distribution:a-declared-location-is-named-by-its-variable",
49    LandingClassifiesItsTargetFirst => "distribution:a-landing-classifies-its-target-first",
50    SeededRuleRunsNoCanonCommand => "distribution:a-seeded-rule-runs-no-canon-command",
51    SkillChecksItsHostBeforeItPlans => "distribution:a-skill-checks-its-host-before-it-plans",
52    SkillHasOneOwner => "distribution:a-skill-has-one-owner",
53    SkillInstallRestoresOnFailure => "distribution:a-skill-install-restores-on-failure",
54    SkillObeysThePortableFormat => "distribution:a-skill-obeys-the-portable-format",
55    SkillPlansBeforeItActs => "distribution:a-skill-plans-before-it-acts",
56    StaleSkillIsNotAConflict => "distribution:a-stale-skill-is-not-a-conflict",
57    InstallSweepsWhatThePayloadDropped => "distribution:an-install-sweeps-what-the-payload-dropped",
58    InitializationPreservesProjectContent => "distribution:initialization-preserves-project-content",
59    InstancesOperateOffline => "distribution:instances-operate-offline",
60    ManifestIdentifiesEveryOwnedFile => "distribution:manifest-identifies-every-owned-file",
61    SharedSkillArtifactsHaveOneHome => "distribution:shared-skill-artifacts-have-one-home",
62    SkillInstallPreviewsBeforeWriting => "distribution:skill-install-previews-before-writing",
63    SkillUninstallRemovesOnlyWhatItWrote => "distribution:skill-uninstall-removes-only-what-it-wrote",
64    SkillsArePartOfThePayload => "distribution:skills-are-part-of-the-payload",
65    TheDoctorAnswersForTheInstalledSkills => "distribution:the-doctor-answers-for-the-installed-skills",
66    ThePayloadNamesNoOtherProject => "distribution:the-payload-names-no-other-project",
67    ThePayloadNamesNoPlanningTool => "distribution:the-payload-names-no-planning-tool",
68    ThePayloadRootsAreDeclaredOnce => "distribution:the-payload-roots-are-declared-once",
69    UpgradeConflictsAreAtomic => "distribution:upgrade-conflicts-are-atomic",
70    UserScopeFilesStayUnrecorded => "distribution:user-scope-files-stay-unrecorded",
71    AuthorInstructionsStayWithinBudget => "docs-format:author-instructions-stay-within-budget",
72    ChapterStaysWithinLineCap => "docs-format:chapter-stays-within-200-lines",
73    DocumentStatesThePresent => "docs-format:document-states-the-present",
74    DocumentUsesStructuralMarkdownOnly => "docs-format:document-uses-structural-markdown-only",
75    EveryBudgetCarriesAGate => "docs-format:every-budget-carries-a-gate",
76    FenceDeclaresALanguage => "docs-format:fence-declares-a-language",
77    ProseStaysUnwrapped => "docs-format:prose-stays-unwrapped",
78    DocumentCarriesNoPersonalPath => "docs-foundations:a-document-carries-no-personal-path",
79    DocumentOwnsWhatItGoverns => "docs-foundations:a-document-owns-what-it-governs",
80    KindPrefixCarriesASlug => "docs-foundations:a-kind-prefix-carries-a-slug",
81    ArtifactFilenamesCarryAKindPrefix => "docs-foundations:artifact-filenames-carry-a-kind-prefix",
82    CompanionArtifactsShareTheSpecName => "docs-foundations:companion-artifacts-share-the-spec-name",
83    SpecStatesThePresent => "docs-foundations:spec-states-the-present",
84    SpecWinsOverRecord => "docs-foundations:spec-wins-over-record",
85    SpecsAreCentralized => "docs-foundations:specs-are-centralized",
86    ProhibitionsAreCapped => "docs-specs:prohibitions-are-capped",
87    RequirementCarriesAVerification => "docs-specs:requirement-carries-a-verification",
88    RequirementCarriesFiveParts => "docs-specs:requirement-carries-five-parts",
89    RuleIdIsUniqueAndSlugged => "docs-specs:rule-id-is-unique-and-slugged",
90    RuleIdOutlivesItsSentence => "docs-specs:rule-id-outlives-its-sentence",
91    SpecStaysWithinLineCap => "docs-specs:spec-stays-within-300-lines",
92    StatementUsesAnEarsPattern => "docs-specs:statement-uses-an-ears-pattern",
93    UnenforcedRulesAreDeclared => "docs-specs:unenforced-rules-are-declared",
94    VerificationNamesALiveHook => "docs-specs:verification-names-a-live-hook",
95    DivergentResultNamesItsDestination => "guides:a-divergent-result-names-its-destination",
96    ManualStepEnumeratesItsInteraction => "guides:a-manual-step-enumerates-its-interaction",
97    StepFollowsItsProducers => "guides:a-step-follows-its-producers",
98    StepIsOneImperativeAction => "guides:a-step-is-one-imperative-action",
99    ExternalFactIsVerifiedUpstream => "guides:an-external-fact-is-verified-upstream",
100    CitationsLiveInTheReferenceZone => "guides:citations-live-in-the-reference-zone",
101    EveryStepCarriesItsCheck => "guides:every-step-carries-its-check",
102    PreconditionsOpenAndVerificationCloses => "guides:preconditions-open-and-verification-closes",
103    TheManifestStaysReadable => "instance:the-manifest-stays-readable",
104    AgentsBlockStaysManaged => "instance:the-agents-block-stays-managed",
105    TrackingRegistryStaysValid => "instance:the-tracking-registry-stays-valid",
106    BugzillaReportBodyFitsReportWidth => "known-issues:a-bugzilla-report-body-fits-in-79-columns",
107    FiledRecordCarriesItsReport => "known-issues:a-filed-record-carries-its-report",
108    RecordCarriesItsRetirementCondition => "known-issues:a-record-carries-its-retirement-condition",
109    RecordCarriesOneFilingState => "known-issues:a-record-carries-one-filing-state",
110    RecordCarriesOneState => "known-issues:a-record-carries-one-state",
111    RecordRecordsItsLastCheck => "known-issues:a-record-records-its-last-check",
112    RecordWalksTheMechanism => "known-issues:a-record-walks-the-mechanism",
113    CaseIdIsASlug => "known-issues:case-id-is-a-slug",
114    CanonGateIsNotDelivered => "release:a-canon-gate-is-not-delivered",
115    ReleasedVersionIsNotReAuthored => "release:a-released-version-is-not-re-authored",
116    TagDerivesFromTheVersionFile => "release:a-tag-derives-from-the-version-file",
117    LicenseDeclaresBothHalves => "release:license-declares-both-halves",
118    CanonRecordDescribesItsTree => "release:the-canon-record-describes-its-tree",
119    RkPinHasTwoFactsAndOneMover => "release:the-rk-pin-has-two-facts-and-one-mover",
120    DeliveredGateSetIsDeclaredOnce => "release:the-delivered-gate-set-is-declared-once",
121    ThirdPartyNoticesTravelWithThePayload => "release:third-party-notices-travel-with-the-payload",
122    VersionsAreSemanticAndAligned => "release:versions-are-semantic-and-aligned",
123    CompatibilityAdaptersMatchUpstream => "simple-english:compatibility-adapters-match-upstream",
124    ObjectiveCheckMatchesItsUpstreamRule => "simple-english:an-objective-check-matches-its-upstream-rule",
125    ExceptionNamesItsReason => "simple-english:an-exception-names-its-reason",
126    MarketingCopyStaysOutOfScope => "simple-english:marketing-copy-stays-out-of-scope",
127    PlainIsTheDefault => "simple-english:plain-is-the-default",
128    ProtectedContentStaysExact => "simple-english:protected-content-stays-exact",
129    StrictIsExplicit => "simple-english:strict-is-explicit",
130    StructureSuppliesThePassageMode => "simple-english:structure-supplies-the-passage-mode",
131    SimpleEnglishDependencyIsAvailableOffline => "simple-english:the-dependency-is-available-offline",
132    UpstreamPatternIsBinding => "simple-english:the-upstream-pattern-is-binding",
133    UpstreamTermsKeepTheirNames => "simple-english:upstream-terms-keep-their-names",
134    CommentCitesTheRule => "spec-to-code:a-comment-cites-the-rule",
135    CommentNamesNoRecord => "spec-to-code:a-comment-names-no-record",
136    GateMessageCitesTheRule => "spec-to-code:a-gate-message-cites-the-rule",
137    PermanentExceptionStatesItsReason => "spec-to-code:a-permanent-exception-states-its-reason",
138    SpecChangeIsTyped => "spec-to-code:a-spec-change-is-typed",
139    SpecMayLeadItsCode => "spec-to-code:a-spec-may-lead-its-code",
140    SuppressionNamesItsCase => "spec-to-code:a-suppression-names-its-case",
141    EntryDocumentCitesRuleIds => "spec-to-code:an-entry-document-cites-rule-ids",
142    UnenactedRulesAreTheBacklog => "spec-to-code:unenacted-rules-are-the-backlog",
143    DeclaredDependentExists => "tracking:a-declared-dependent-exists",
144    PerishableSourceIsRegistered => "tracking:a-perishable-source-is-registered",
145    EntryDeclaresHowToRevalidate => "tracking:an-entry-declares-how-to-revalidate",
146    OverdueEntryBlocks => "tracking:an-overdue-entry-blocks",
147    UpstreamCheckDoesNotEditTheTree => "tracking:an-upstream-check-does-not-edit-the-tree",
148    UpstreamDerivationPinsARevision => "tracking:an-upstream-derivation-pins-a-revision",
149    RegistryHasOneReadableShape => "tracking:the-registry-has-one-readable-shape",
150}
151
152impl fmt::Display for RuleId {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        f.write_str(self.as_str())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn slugs_are_well_formed_and_unique() {
164        let mut seen = std::collections::BTreeSet::new();
165        for rule in RuleId::ALL {
166            let id = rule.as_str();
167            let (domain, name) = id.split_once(':').unwrap();
168            let assert_slug = |part: &str| {
169                assert!(!part.is_empty(), "{id} has an empty half");
170                assert!(
171                    part.bytes()
172                        .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'),
173                    "{id} is not a slug pair"
174                );
175            };
176            assert_slug(domain);
177            assert_slug(name);
178            assert!(seen.insert(id), "{id} is duplicated");
179        }
180    }
181
182    #[test]
183    fn display_renders_the_slug_pair() {
184        assert_eq!(
185            RuleId::ChapterStaysWithinLineCap.to_string(),
186            "docs-format:chapter-stays-within-200-lines"
187        );
188    }
189}