1use 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 #[must_use]
36 pub fn parse(id: &str) -> Option<Self> {
37 match id {
38 $($id => Some(Self::$variant),)+
39 _ => None,
40 }
41 }
42 }
43
44 impl serde::Serialize for RuleId {
45 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
46 serializer.serialize_str(self.as_str())
47 }
48 }
49
50 impl<'de> serde::Deserialize<'de> for RuleId {
51 fn deserialize<D: serde::Deserializer<'de>>(
52 deserializer: D,
53 ) -> Result<Self, D::Error> {
54 let id = <std::borrow::Cow<'_, str> as serde::Deserialize>::deserialize(
55 deserializer,
56 )?;
57 Self::parse(&id).ok_or_else(|| {
58 serde::de::Error::custom(format!("no spec defines the rule {id}"))
59 })
60 }
61 }
62 };
63}
64
65rule_ids! {
66 FragmentNamesTheVenueForm => "acquisition:a-fragment-names-the-venue-form",
67 CleanRemovesOnlyANamedLeftover => "acquisition:clean-removes-only-a-named-leftover",
68 EveryPairCarriesAVerdict => "acquisition:every-pair-carries-a-verdict",
69 OneTargetRunsOneMechanism => "acquisition:one-target-runs-one-mechanism",
70 StatusReportsAndNeverJudges => "acquisition:status-reports-and-never-judges",
71 TheOperatorCallerReportsEveryOutcome => "acquisition:the-operator-caller-reports-every-outcome",
72 ThePinMovesInOneTransaction => "acquisition:the-pin-moves-in-one-transaction",
73 TheSetupPathOffersTheWire => "acquisition:the-setup-path-offers-the-wire",
74 TheShellEntryCallerIsRateLimitedAndSilent => "acquisition:the-shell-entry-caller-is-rate-limited-and-silent",
75 TheSyncLeavesADiffNobodyCommitted => "acquisition:the-sync-leaves-a-diff-nobody-committed",
76 TheToolServesAndDoesNotEdit => "acquisition:the-tool-serves-and-does-not-edit",
77 RecordedDimensionOnlyShrinks => "budget-debt:a-recorded-dimension-only-shrinks",
78 DebtIsCreatedByAnExplicitAct => "budget-debt:debt-is-created-by-an-explicit-act",
79 CellCarriesOneReference => "comparison-docs:a-cell-carries-one-reference",
80 ComparisonCarriesALegend => "comparison-docs:a-comparison-carries-a-legend",
81 VerdictCarriesItsWord => "comparison-docs:a-verdict-carries-its-word",
82 EveryTableIsDated => "comparison-docs:every-table-is-dated",
83 TablePipesAreEscaped => "comparison-docs:table-pipes-are-escaped",
84 CitationResolvesToARule => "decision-records:a-citation-resolves-to-a-rule",
85 BodyStaysWithinWordCap => "decision-records:body-stays-within-350-words",
86 FilenameCarriesNoDigit => "decision-records:filename-carries-no-digit",
87 MergedRecordIsPermanent => "decision-records:merged-record-is-permanent",
88 RecordIsNotRevised => "decision-records:record-is-not-revised",
89 DeclaredLocationIsNamedByItsVariable => "distribution:a-declared-location-is-named-by-its-variable",
90 DeliveredConfigurationServesADeliveredRule => "distribution:a-delivered-configuration-serves-a-delivered-rule",
91 LandingClassifiesItsTargetFirst => "distribution:a-landing-classifies-its-target-first",
92 SeededRuleRunsNoCanonCommand => "distribution:a-seeded-rule-runs-no-canon-command",
93 SkillChecksItsHostBeforeItPlans => "distribution:a-skill-checks-its-host-before-it-plans",
94 SkillHasOneOwner => "distribution:a-skill-has-one-owner",
95 SkillInstallRestoresOnFailure => "distribution:a-skill-install-restores-on-failure",
96 SkillObeysThePortableFormat => "distribution:a-skill-obeys-the-portable-format",
97 SkillPlansBeforeItActs => "distribution:a-skill-plans-before-it-acts",
98 InstallSweepsWhatThePayloadDropped => "distribution:an-install-sweeps-what-the-payload-dropped",
99 InitializationPreservesProjectContent => "distribution:initialization-preserves-project-content",
100 InstancesOperateOffline => "distribution:instances-operate-offline",
101 ManifestIdentifiesEveryOwnedFile => "distribution:manifest-identifies-every-owned-file",
102 DeclarationIsSeededOnceAndThenOwned => "distribution:the-declaration-is-seeded-once-and-then-owned",
103 SkillPackageIsSelfContained => "distribution:a-skill-package-is-self-contained",
104 SkillInstallPreviewsBeforeWriting => "distribution:skill-install-previews-before-writing",
105 SkillUninstallRemovesOnlyWhatItWrote => "distribution:skill-uninstall-removes-only-what-it-wrote",
106 SkillsArePartOfThePayload => "distribution:skills-are-part-of-the-payload",
107 TheDoctorAnswersForTheInstalledSkills => "distribution:the-doctor-answers-for-the-installed-skills",
108 ThePayloadNamesNoOtherProject => "distribution:the-payload-names-no-other-project",
109 ThePayloadRootsAreDeclaredOnce => "distribution:the-payload-roots-are-declared-once",
110 UpgradeConflictsAreAtomic => "distribution:upgrade-conflicts-are-atomic",
111 UserScopeFilesStayUnrecorded => "distribution:user-scope-files-stay-unrecorded",
112 UserScopeReceiptIsRequiredState => "distribution:a-user-scope-receipt-is-required-state",
113 MachineScopeRecipeDropsASessionVariable => "release:a-machine-scope-recipe-drops-a-session-variable",
114 BinaryBuildsForEveryDeclaredTarget => "release:the-binary-builds-for-every-declared-target",
115 OneCatalogDescribesEveryServedDocument => "docs-discovery:one-catalog-describes-every-served-document",
116 InstanceIsRoutedToTheIndex => "docs-discovery:an-instance-is-routed-to-the-index",
117 AuthorInstructionsStayWithinBudget => "docs-format:author-instructions-stay-within-budget",
118 ChapterStaysWithinLineCap => "docs-format:chapter-stays-within-200-lines",
119 DocumentStatesThePresent => "docs-format:document-states-the-present",
120 DocumentUsesStructuralMarkdownOnly => "docs-format:document-uses-structural-markdown-only",
121 EveryBudgetCarriesAGate => "docs-format:every-budget-carries-a-gate",
122 FenceDeclaresALanguage => "docs-format:fence-declares-a-language",
123 ProseStaysUnwrapped => "docs-format:prose-stays-unwrapped",
124 DocumentCarriesNoPersonalPath => "docs-foundations:a-document-carries-no-personal-path",
125 DocumentDirectoryExplainsItself => "docs-foundations:a-document-directory-explains-itself",
126 DocumentOwnsWhatItGoverns => "docs-foundations:a-document-owns-what-it-governs",
127 KindPrefixCarriesASlug => "docs-foundations:a-kind-prefix-carries-a-slug",
128 ArtifactFilenamesCarryAKindPrefix => "docs-foundations:artifact-filenames-carry-a-kind-prefix",
129 CompanionArtifactsShareTheSpecName => "docs-foundations:companion-artifacts-share-the-spec-name",
130 SpecStatesThePresent => "docs-foundations:spec-states-the-present",
131 SpecWinsOverRecord => "docs-foundations:spec-wins-over-record",
132 SpecsAreCentralized => "docs-foundations:specs-are-centralized",
133 ProhibitionsAreCapped => "docs-specs:prohibitions-are-capped",
134 RequirementCarriesAVerification => "docs-specs:requirement-carries-a-verification",
135 RequirementCarriesFiveParts => "docs-specs:requirement-carries-five-parts",
136 RuleIdIsUniqueAndSlugged => "docs-specs:rule-id-is-unique-and-slugged",
137 RuleIdOutlivesItsSentence => "docs-specs:rule-id-outlives-its-sentence",
138 SpecStaysWithinLineCap => "docs-specs:spec-stays-within-300-lines",
139 StatementUsesAnEarsPattern => "docs-specs:statement-uses-an-ears-pattern",
140 UnenforcedRulesAreDeclared => "docs-specs:unenforced-rules-are-declared",
141 VerificationNamesALiveHook => "docs-specs:verification-names-a-live-hook",
142 DivergentResultNamesItsDestination => "guides:a-divergent-result-names-its-destination",
143 ManualStepEnumeratesItsInteraction => "guides:a-manual-step-enumerates-its-interaction",
144 StepFollowsItsProducers => "guides:a-step-follows-its-producers",
145 StepIsOneImperativeAction => "guides:a-step-is-one-imperative-action",
146 ExternalFactIsVerifiedUpstream => "guides:an-external-fact-is-verified-upstream",
147 CitationsLiveInTheReferenceZone => "guides:citations-live-in-the-reference-zone",
148 EveryStepCarriesItsCheck => "guides:every-step-carries-its-check",
149 PreconditionsOpenAndVerificationCloses => "guides:preconditions-open-and-verification-closes",
150 TheManifestStaysReadable => "instance:the-manifest-stays-readable",
151 AgentsBlockStaysManaged => "instance:the-agents-block-stays-managed",
152 TrackingRegistryStaysValid => "instance:the-tracking-registry-stays-valid",
153 ProjectDeclaresWhatItsGatesJudge => "instance:the-project-declares-what-its-gates-judge",
154 ManagedBlockAgreesWithTheDeclaration => "instance:the-managed-block-agrees-with-the-declaration",
155 BugzillaReportBodyFitsReportWidth => "known-issues:a-bugzilla-report-body-fits-in-79-columns",
156 FiledRecordCarriesItsReport => "known-issues:a-filed-record-carries-its-report",
157 RecordCarriesItsRetirementCondition => "known-issues:a-record-carries-its-retirement-condition",
158 RecordCarriesOneFilingState => "known-issues:a-record-carries-one-filing-state",
159 RecordCarriesOneState => "known-issues:a-record-carries-one-state",
160 RecordRecordsItsLastCheck => "known-issues:a-record-records-its-last-check",
161 RecordWalksTheMechanism => "known-issues:a-record-walks-the-mechanism",
162 CaseIdIsASlug => "known-issues:case-id-is-a-slug",
163 CanonGateIsNotDelivered => "release:a-canon-gate-is-not-delivered",
164 DeliveredGateReadsWhatTheConventionOwns => "release:a-delivered-gate-reads-what-the-convention-owns",
165 ReleasedVersionIsNotReAuthored => "release:a-released-version-is-not-re-authored",
166 TagDerivesFromTheVersionFile => "release:a-tag-derives-from-the-version-file",
167 EveryReleaseDeclaresWhatItAsks => "release:every-release-declares-what-it-asks",
168 LicenseDeclaresBothHalves => "release:license-declares-both-halves",
169 CanonRecordDescribesItsTree => "release:the-canon-record-describes-its-tree",
170 RkPinHasTwoFactsAndOneMover => "release:the-rk-pin-has-two-facts-and-one-mover",
171 DeliveredGateSetIsDeclaredOnce => "release:the-delivered-gate-set-is-declared-once",
172 ThirdPartyNoticesTravelWithThePayload => "release:third-party-notices-travel-with-the-payload",
173 VersionsAreSemanticAndAligned => "release:versions-are-semantic-and-aligned",
174 CommentCitesTheRule => "spec-to-code:a-comment-cites-the-rule",
175 CommentNamesNoRecord => "spec-to-code:a-comment-names-no-record",
176 GateMessageCitesTheRule => "spec-to-code:a-gate-message-cites-the-rule",
177 SpecMayLeadItsCode => "spec-to-code:a-spec-may-lead-its-code",
178 SuppressionNamesItsCase => "spec-to-code:a-suppression-names-its-case",
179 PartialFailureReportsWhatItCompleted => "staging:a-partial-failure-reports-what-it-completed",
180 StageCarriesTheWholeCandidate => "staging:a-stage-carries-the-whole-candidate",
181 StagePersistsUntilItIsCleaned => "staging:a-stage-persists-until-it-is-cleaned",
182 StageWritesOnlyTheStage => "staging:a-stage-writes-only-the-stage",
183 UserScopeReceiptSharesNoEngine => "staging:a-user-scope-receipt-shares-no-engine",
184 UnattributedCollisionRefusesFirst => "staging:an-unattributed-collision-refuses-first",
185 EveryPathStaysUnderTheTarget => "staging:every-path-stays-under-the-target",
186 OneDocumentationRootServesTheRun => "staging:one-documentation-root-serves-the-run",
187 OneWriterHoldsTheTarget => "staging:one-writer-holds-the-target",
188 OwnerOnlyStateIsCheckedOnLinux => "staging:owner-only-state-is-checked-on-linux",
189 ProductionReadsNoStagedByte => "staging:production-reads-no-staged-byte",
190 ManifestAttributesEveryRefresh => "staging:the-manifest-attributes-every-refresh",
191 ManifestIsWrittenLast => "staging:the-manifest-is-written-last",
192 OperatorOwnsAcquisition => "staging:the-operator-owns-acquisition",
193 DeclaredDependentExists => "tracking:a-declared-dependent-exists",
194 PerishableSourceIsRegistered => "tracking:a-perishable-source-is-registered",
195 EntryDeclaresHowToRevalidate => "tracking:an-entry-declares-how-to-revalidate",
196 OverdueEntryBlocks => "tracking:an-overdue-entry-blocks",
197 UpstreamCheckDoesNotEditTheTree => "tracking:an-upstream-check-does-not-edit-the-tree",
198 UpstreamDerivationPinsARevision => "tracking:an-upstream-derivation-pins-a-revision",
199 RegistryHasOneReadableShape => "tracking:the-registry-has-one-readable-shape",
200 ExistingDocumentConvertsWhenEdited => "writing-style:an-existing-document-converts-when-edited",
201 NoDeliveredGateJudgesProse => "writing-style:no-delivered-gate-judges-prose",
202 SourcesNameTheRevisionRead => "writing-style:sources-name-the-revision-read",
203 DocumentationBlockRoutesToTheStyle => "writing-style:the-documentation-block-routes-to-the-style",
204 StyleLivesInOneDocument => "writing-style:the-style-lives-in-one-document",
205 NoneImposesNoObligation => "writing-policy:none-imposes-no-obligation",
206 ProjectSelectsOneSource => "writing-policy:the-project-selects-one-source",
207}
208
209impl fmt::Display for RuleId {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 f.write_str(self.as_str())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn slugs_are_well_formed_and_unique() {
221 let mut seen = std::collections::BTreeSet::new();
222 for rule in RuleId::ALL {
223 let id = rule.as_str();
224 let (domain, name) = id.split_once(':').unwrap();
225 let assert_slug = |part: &str| {
226 assert!(!part.is_empty(), "{id} has an empty half");
227 assert!(
228 part.bytes()
229 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'),
230 "{id} is not a slug pair"
231 );
232 };
233 assert_slug(domain);
234 assert_slug(name);
235 assert!(seen.insert(id), "{id} is duplicated");
236 }
237 }
238
239 #[test]
240 fn display_renders_the_slug_pair() {
241 assert_eq!(
242 RuleId::ChapterStaysWithinLineCap.to_string(),
243 "docs-format:chapter-stays-within-200-lines"
244 );
245 }
246}