Skip to main content

boxferry_engine/
rule.rs

1//! Stable `BoxFerry` diagnostic rule catalogue.
2
3use crate::{DiagnosticCode, InvalidDiagnosticCode, Severity};
4
5/// Stable identifier for one `BoxFerry` diagnostic rule.
6#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7#[repr(u32)]
8#[non_exhaustive]
9#[allow(
10    missing_docs,
11    reason = "each variant is documented by its public catalogue definition"
12)]
13pub enum RuleId {
14    ComposeModelInvalid = 1_000_001,
15    ComposeProfileRequired = 1_000_002,
16    ComposeProfileMismatch = 1_000_003,
17    ComposeIntentUnsupported = 1_000_004,
18    ComposeValueInvalid = 1_000_005,
19    ComposeTargetInvalid = 1_000_006,
20    ComposeOutputUnsupported = 1_000_007,
21    ComposeGenerationFailed = 1_000_008,
22    ComposeCompatibilityConstraint = 1_000_009,
23    ComposeUnsetVariable = 1_000_101,
24    ComposeRequiredVariable = 1_000_102,
25    ComposeInterpolationInvalid = 1_000_103,
26    ComposeInterpolationNestingLimit = 1_000_104,
27    ComposeUnresolvedVariable = 1_000_105,
28    ComposeNativeError = 1_000_197,
29    ComposeNativeWarning = 1_000_198,
30    ComposeNativeNote = 1_000_199,
31    OrchestrationFailed = 3_001_000,
32    InputReadFailed = 3_001_001,
33    ComposeProjectRootInvalid = 3_001_002,
34    PodmanTargetSelectionInvalid = 3_001_003,
35    InterpolationInputInvalid = 3_001_004,
36    ComposeLoadFailed = 3_001_005,
37    ConversionFailed = 3_001_006,
38    QuadletParseFailed = 3_001_007,
39    OutputDirectoryNotEmpty = 3_002_001,
40    OutputPathInvalid = 3_002_002,
41    OutputWriteFailed = 3_002_003,
42    ReportWriteFailed = 3_003_001,
43    SupportBundleWriteFailed = 3_003_002,
44    PodmanSourceInvalid = 4_000_001,
45    PodmanInputUnmodelled = 4_000_002,
46    PodmanPromotionRequired = 4_000_003,
47    PodmanIdentityConflict = 4_000_004,
48    PodmanSecretGrantIncomplete = 4_000_005,
49    PodmanTargetInvalid = 4_000_006,
50    PodmanOutputUnsupported = 4_000_007,
51    PodmanPlanningFailed = 4_000_008,
52    QuadletTargetInvalid = 5_000_001,
53    QuadletOpenEndedTarget = 5_000_002,
54    QuadletOutputUnsupported = 5_000_003,
55    QuadletValueInvalid = 5_000_004,
56    QuadletGenerationFailed = 5_000_005,
57    QuadletCapabilityUnavailable = 5_000_006,
58    QuadletGroupingApproximation = 5_000_007,
59    QuadletDependencyUnsupported = 5_000_008,
60    QuadletRestartApproximation = 5_000_009,
61    QuadletEnvironmentFileApproximation = 5_000_010,
62    QuadletGroupingInvalid = 5_000_011,
63    QuadletDependencyInvalid = 5_000_012,
64    QuadletCapabilityDeprecated = 5_000_013,
65    QuadletUnresolvedVariable = 5_000_014,
66    QuadletSourceInvalid = 5_001_001,
67    QuadletModelInvalid = 5_001_002,
68    QuadletInputUnsupported = 5_001_003,
69    QuadletInputApproximation = 5_001_004,
70    QuadletNativeSyntax = 5_001_101,
71    QuadletNativeModel = 5_001_102,
72    QuadletNativeDocumentSet = 5_001_103,
73    QuadletNativeFailure = 5_001_104,
74}
75
76/// Immutable metadata shared by human, JSON, and support-bundle presentation.
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub struct DiagnosticRule {
79    id: RuleId,
80    code: &'static str,
81    name: &'static str,
82    default_severity: Severity,
83    description: &'static str,
84    help: &'static str,
85}
86
87impl DiagnosticRule {
88    /// Returns the typed rule identifier.
89    #[must_use]
90    pub const fn id(self) -> RuleId {
91        self.id
92    }
93
94    /// Returns the stable machine-readable rule code.
95    #[must_use]
96    pub const fn code(self) -> &'static str {
97        self.code
98    }
99
100    /// Returns the stable human-readable rule name.
101    #[must_use]
102    pub const fn name(self) -> &'static str {
103        self.name
104    }
105
106    /// Returns the rule's normal severity.
107    #[must_use]
108    pub const fn default_severity(self) -> Severity {
109        self.default_severity
110    }
111
112    /// Returns the subsystem that owns this rule namespace.
113    #[must_use]
114    pub fn owner(self) -> &'static str {
115        match self.code.as_bytes().get(..3) {
116            Some(b"BFC") => "Compose adapter",
117            Some(b"BFO") => "BoxFerry orchestration",
118            Some(b"BFP") => "Podman adapter",
119            Some(b"BFQ") => "Quadlet adapter",
120            _ => "BoxFerry engine",
121        }
122    }
123
124    /// Returns the value-free explanation of the condition.
125    #[must_use]
126    pub const fn description(self) -> &'static str {
127        self.description
128    }
129
130    /// Returns static remediation guidance that never contains user data.
131    #[must_use]
132    pub const fn help(self) -> &'static str {
133        self.help
134    }
135
136    /// Constructs the validated code used by structured engine diagnostics.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`InvalidDiagnosticCode`] if the repository-owned catalogue is malformed.
141    pub fn diagnostic_code(self) -> Result<DiagnosticCode, InvalidDiagnosticCode> {
142        DiagnosticCode::new(self.code)
143    }
144}
145
146macro_rules! rule {
147    ($id:ident, $code:literal, $name:literal, $severity:ident, $description:literal, $help:literal) => {
148        DiagnosticRule {
149            id: RuleId::$id,
150            code: $code,
151            name: $name,
152            default_severity: Severity::$severity,
153            description: $description,
154            help: $help,
155        }
156    };
157}
158
159/// Complete, code-sorted catalogue for this `BoxFerry` build.
160pub const RULES: &[DiagnosticRule] = &[
161    rule!(
162        ComposeModelInvalid,
163        "BFC0001",
164        "compose-model-invalid",
165        Error,
166        "Compose intent could not be represented by the neutral application model.",
167        "Correct the reported Compose value or remove the conflicting declaration."
168    ),
169    rule!(
170        ComposeProfileRequired,
171        "BFC0002",
172        "compose-profile-selection-required",
173        Error,
174        "Profiled Compose services require an explicit profile selection.",
175        "Select profiles explicitly with --profile, or use --all-profiles only after reviewing mutually exclusive services."
176    ),
177    rule!(
178        ComposeProfileMismatch,
179        "BFC0003",
180        "compose-profile-selection-mismatch",
181        Error,
182        "The Compose profile selection is invalid or belongs to another merged project.",
183        "Recreate the profile selection from the same merged Compose project."
184    ),
185    rule!(
186        ComposeIntentUnsupported,
187        "BFC0004",
188        "compose-intent-unsupported",
189        Warning,
190        "Compose intent is not represented by the current neutral-model subset.",
191        "Review the named feature; use --loss-policy partial only when omitting that intent is acceptable."
192    ),
193    rule!(
194        ComposeValueInvalid,
195        "BFC0005",
196        "compose-value-invalid",
197        Error,
198        "A Compose value cannot be represented safely in the neutral model.",
199        "Correct the reported value before converting again."
200    ),
201    rule!(
202        ComposeTargetInvalid,
203        "BFC0006",
204        "compose-target-invalid",
205        Error,
206        "The requested Compose output target is invalid.",
207        "Select a supported Compose target profile and compatible version inputs."
208    ),
209    rule!(
210        ComposeOutputUnsupported,
211        "BFC0007",
212        "compose-output-unsupported",
213        Warning,
214        "Neutral application intent is not represented in generated Compose output.",
215        "Review the named subject; use --loss-policy partial only when the omitted intent is acceptable."
216    ),
217    rule!(
218        ComposeGenerationFailed,
219        "BFC0008",
220        "compose-generation-failed",
221        Error,
222        "ComposeLens rejected generated Compose output.",
223        "Correct the reported neutral value or target choice and retry."
224    ),
225    rule!(
226        ComposeCompatibilityConstraint,
227        "BFC0009",
228        "compose-compatibility-constraint",
229        Warning,
230        "Generated Compose syntax has a target-specific compatibility constraint.",
231        "Review the target-specific classification before authorizing non-exact output."
232    ),
233    rule!(
234        ComposeUnsetVariable,
235        "BFC0101",
236        "compose-unset-variable",
237        Warning,
238        "A Compose interpolation variable is not set.",
239        "Provide the missing value with --env-file FILE or --env NAME=VALUE."
240    ),
241    rule!(
242        ComposeRequiredVariable,
243        "BFC0102",
244        "compose-required-variable",
245        Error,
246        "A required Compose interpolation variable is not set.",
247        "Provide the required value with --env-file FILE or --env NAME=VALUE."
248    ),
249    rule!(
250        ComposeInterpolationInvalid,
251        "BFC0103",
252        "compose-interpolation-invalid",
253        Error,
254        "A Compose interpolation expression is invalid.",
255        "Correct the interpolation expression and retry."
256    ),
257    rule!(
258        ComposeInterpolationNestingLimit,
259        "BFC0104",
260        "compose-interpolation-nesting-limit",
261        Error,
262        "A Compose interpolation expression exceeds the supported nesting limit.",
263        "Simplify the interpolation expression before converting."
264    ),
265    rule!(
266        ComposeUnresolvedVariable,
267        "BFC0105",
268        "compose-unresolved-variable",
269        Warning,
270        "A Compose variable expression remains unresolved at the adapter boundary.",
271        "Use --interpolate and provide missing values with --env-file FILE or --env NAME=VALUE; partial authorization applies only when the affected intent can be omitted."
272    ),
273    rule!(
274        ComposeNativeError,
275        "BFC0197",
276        "compose-native-error",
277        Error,
278        "ComposeLens reported a native Compose error.",
279        "Use the retained source code and source span to correct the Compose document."
280    ),
281    rule!(
282        ComposeNativeWarning,
283        "BFC0198",
284        "compose-native-warning",
285        Warning,
286        "ComposeLens reported a native Compose warning.",
287        "Review the retained source code and source span before accepting the document."
288    ),
289    rule!(
290        ComposeNativeNote,
291        "BFC0199",
292        "compose-native-note",
293        Note,
294        "ComposeLens reported native Compose information.",
295        "Review the retained source code when more context is needed."
296    ),
297    rule!(
298        OrchestrationFailed,
299        "BFO1000",
300        "orchestration-failed",
301        Error,
302        "BoxFerry could not complete the requested orchestration stage.",
303        "Review the reason and correct the command inputs before retrying."
304    ),
305    rule!(
306        InputReadFailed,
307        "BFO1001",
308        "input-read-failed",
309        Error,
310        "An explicitly selected input could not be read.",
311        "Check that the input exists, is a readable regular file, and is not a symlink."
312    ),
313    rule!(
314        ComposeProjectRootInvalid,
315        "BFO1002",
316        "compose-project-root-invalid",
317        Error,
318        "The Compose project root could not be resolved.",
319        "Provide an existing absolute --project-directory or correct the input location."
320    ),
321    rule!(
322        PodmanTargetSelectionInvalid,
323        "BFO1003",
324        "podman-target-selection-invalid",
325        Error,
326        "The requested Podman target range could not be resolved.",
327        "Use major.minor or major.minor.patch values within the reviewed range."
328    ),
329    rule!(
330        InterpolationInputInvalid,
331        "BFO1004",
332        "interpolation-input-invalid",
333        Error,
334        "Compose interpolation inputs could not be resolved safely.",
335        "Correct the named --env-file or --env input and retry."
336    ),
337    rule!(
338        ComposeLoadFailed,
339        "BFO1005",
340        "compose-load-failed",
341        Error,
342        "Compose input could not be loaded or merged.",
343        "Correct the reported Compose source diagnostic before retrying."
344    ),
345    rule!(
346        ConversionFailed,
347        "BFO1006",
348        "conversion-failed",
349        Error,
350        "Source import or target conversion failed.",
351        "Correct the reported rule occurrences before retrying."
352    ),
353    rule!(
354        QuadletParseFailed,
355        "BFO1007",
356        "quadlet-parse-failed",
357        Error,
358        "Quadlet input could not be parsed into the native document boundary.",
359        "Correct the retained native diagnostics before retrying."
360    ),
361    rule!(
362        OutputDirectoryNotEmpty,
363        "BFO2001",
364        "output-directory-not-empty",
365        Error,
366        "The selected output directory is not empty.",
367        "Empty the selected --output-directory or choose a new path."
368    ),
369    rule!(
370        OutputPathInvalid,
371        "BFO2002",
372        "output-path-invalid",
373        Error,
374        "The selected output path is not a usable non-symlink directory.",
375        "Choose an absent path or an existing empty, non-symlink directory."
376    ),
377    rule!(
378        OutputWriteFailed,
379        "BFO2003",
380        "output-write-failed",
381        Error,
382        "Generated output could not be written safely.",
383        "Check parent-directory existence, permissions, free space, and path conflicts."
384    ),
385    rule!(
386        ReportWriteFailed,
387        "BFO3001",
388        "report-write-failed",
389        Error,
390        "The structured report file could not be written safely.",
391        "Choose a new writable --report-file path."
392    ),
393    rule!(
394        SupportBundleWriteFailed,
395        "BFO3002",
396        "support-bundle-write-failed",
397        Error,
398        "The diagnostic support bundle could not be written safely.",
399        "Choose a writable --error-report-directory with space for a new archive."
400    ),
401    rule!(
402        PodmanSourceInvalid,
403        "BFP0001",
404        "podman-source-invalid",
405        Error,
406        "Inspected Podman evidence is malformed, incomplete, or unavailable for a required mapping.",
407        "Correct the selected Podman resources or selectors, then acquire a complete read-only inventory again."
408    ),
409    rule!(
410        PodmanInputUnmodelled,
411        "BFP0002",
412        "podman-input-unmodelled",
413        Warning,
414        "Inspected Podman intent is not represented by the current neutral application model.",
415        "Review the named field; use --loss-policy partial only when omitting that intent is acceptable."
416    ),
417    rule!(
418        PodmanPromotionRequired,
419        "BFP0003",
420        "podman-promotion-required",
421        Warning,
422        "Effective or locally resolved Podman evidence requires an explicit promotion decision.",
423        "Choose an explicit promotion policy only after confirming the observed value is portable desired state."
424    ),
425    rule!(
426        PodmanIdentityConflict,
427        "BFP0004",
428        "podman-identity-conflict",
429        Error,
430        "Podman resource identities or references conflict in the neutral namespace.",
431        "Narrow the selectors or rename the conflicting resources before importing again."
432    ),
433    rule!(
434        PodmanSecretGrantIncomplete,
435        "BFP0005",
436        "podman-secret-grant-incomplete",
437        Warning,
438        "Podman inspection lacks authored secret-delivery information required for a portable grant.",
439        "Author the secret delivery form and target separately; inspection metadata alone is insufficient."
440    ),
441    rule!(
442        PodmanTargetInvalid,
443        "BFP0006",
444        "podman-target-invalid",
445        Error,
446        "The requested Podman output target is outside the reviewed exact version and execution-context catalogue.",
447        "Select a reviewed --podman-max-version and state the target rootful, rootless, or unknown context explicitly."
448    ),
449    rule!(
450        PodmanOutputUnsupported,
451        "BFP0007",
452        "podman-output-unsupported",
453        Warning,
454        "Neutral application intent is not represented by the current Podman deployment subset.",
455        "Review the named subject; use --loss-policy partial only when the omitted intent is acceptable."
456    ),
457    rule!(
458        PodmanPlanningFailed,
459        "BFP0008",
460        "podman-planning-failed",
461        Error,
462        "PodmanLens could not produce or render a complete inert deployment plan.",
463        "Review retained PodmanLens findings, correct the neutral intent or target, and retry."
464    ),
465    rule!(
466        QuadletTargetInvalid,
467        "BFQ0001",
468        "quadlet-target-invalid",
469        Error,
470        "The requested Quadlet target or Podman range is invalid.",
471        "Select the podman target and a version range covered by QuadletLens."
472    ),
473    rule!(
474        QuadletOpenEndedTarget,
475        "BFQ0002",
476        "quadlet-open-ended-target",
477        Note,
478        "An omitted Podman maximum extends beyond verified compatibility evidence.",
479        "Set --podman-maximum-version for a finite compatibility claim."
480    ),
481    rule!(
482        QuadletOutputUnsupported,
483        "BFQ0003",
484        "quadlet-output-unsupported",
485        Warning,
486        "Neutral application intent is not represented by the current Quadlet subset.",
487        "Review the named subject; use --loss-policy partial only when omission is acceptable."
488    ),
489    rule!(
490        QuadletValueInvalid,
491        "BFQ0004",
492        "quadlet-value-invalid",
493        Error,
494        "A neutral value cannot be emitted safely as native Quadlet syntax.",
495        "Correct or explicitly map the reported value before converting again."
496    ),
497    rule!(
498        QuadletGenerationFailed,
499        "BFQ0005",
500        "quadlet-generation-failed",
501        Error,
502        "QuadletLens rejected generated native output.",
503        "Correct the reported value or dependency and retry."
504    ),
505    rule!(
506        QuadletCapabilityUnavailable,
507        "BFQ0006",
508        "quadlet-capability-unavailable",
509        Warning,
510        "A required Quadlet capability is unavailable for part of the target range.",
511        "Narrow the Podman target range or accept the documented partial result."
512    ),
513    rule!(
514        QuadletGroupingApproximation,
515        "BFQ0007",
516        "quadlet-grouping-approximation",
517        Warning,
518        "Generated Quadlet grouping changes source service isolation.",
519        "Use --quadlet-grouping pod only when the shared-pod semantics are acceptable."
520    ),
521    rule!(
522        QuadletDependencyUnsupported,
523        "BFQ0008",
524        "quadlet-dependency-unsupported",
525        Warning,
526        "A service dependency cannot be represented exactly by Quadlet and systemd.",
527        "Review the dependency condition and complete unsupported behavior manually."
528    ),
529    rule!(
530        QuadletRestartApproximation,
531        "BFQ0009",
532        "quadlet-restart-policy-approximation",
533        Warning,
534        "Container restart behavior is approximated by the systemd service manager.",
535        "Use --loss-policy approximate only after accepting the documented systemd behavior difference."
536    ),
537    rule!(
538        QuadletEnvironmentFileApproximation,
539        "BFQ0010",
540        "quadlet-environment-file-approximation",
541        Warning,
542        "Environment-file parsing is delegated to Podman.",
543        "Verify the environment file with the target Podman version before deployment."
544    ),
545    rule!(
546        QuadletGroupingInvalid,
547        "BFQ0011",
548        "quadlet-grouping-invalid",
549        Error,
550        "The requested Quadlet grouping cannot preserve the application topology.",
551        "Keep separate containers or correct the reported grouping conflict."
552    ),
553    rule!(
554        QuadletDependencyInvalid,
555        "BFQ0012",
556        "quadlet-dependency-invalid",
557        Error,
558        "The service or image-artifact dependency graph is invalid.",
559        "Correct missing or cyclic dependencies before converting."
560    ),
561    rule!(
562        QuadletCapabilityDeprecated,
563        "BFQ0013",
564        "quadlet-capability-deprecated",
565        Note,
566        "A required Quadlet capability is deprecated for part of the target range.",
567        "Review the target range and plan migration away from the deprecated capability."
568    ),
569    rule!(
570        QuadletUnresolvedVariable,
571        "BFQ0014",
572        "quadlet-unresolved-source-variable",
573        Error,
574        "A source variable expression cannot be emitted as a Quadlet value.",
575        "Resolve source variables before conversion; for Compose input, use --interpolate and provide missing values with --env-file FILE or --env NAME=VALUE."
576    ),
577    rule!(
578        QuadletSourceInvalid,
579        "BFQ1001",
580        "quadlet-source-invalid",
581        Error,
582        "The Quadlet document set is invalid.",
583        "Correct native Quadlet syntax and document-set errors before converting."
584    ),
585    rule!(
586        QuadletModelInvalid,
587        "BFQ1002",
588        "quadlet-model-invalid",
589        Error,
590        "Quadlet intent cannot be represented by the neutral application model.",
591        "Correct the reported native value or relationship."
592    ),
593    rule!(
594        QuadletInputUnsupported,
595        "BFQ1003",
596        "quadlet-input-unsupported",
597        Warning,
598        "Quadlet intent is outside the current neutral-model importer subset.",
599        "Review the named key; use --loss-policy partial only when omission is acceptable."
600    ),
601    rule!(
602        QuadletInputApproximation,
603        "BFQ1004",
604        "quadlet-input-approximation",
605        Warning,
606        "Quadlet intent is approximated in the neutral model.",
607        "Review the documented semantic difference before authorizing approximate output."
608    ),
609    rule!(
610        QuadletNativeSyntax,
611        "BFQ1101",
612        "quadlet-native-syntax",
613        Error,
614        "QuadletLens reported a native syntax diagnostic.",
615        "Use the retained source code and source span to correct the Quadlet unit."
616    ),
617    rule!(
618        QuadletNativeModel,
619        "BFQ1102",
620        "quadlet-native-model",
621        Error,
622        "QuadletLens reported a native model diagnostic.",
623        "Use the retained source code and source span to correct the Quadlet value."
624    ),
625    rule!(
626        QuadletNativeDocumentSet,
627        "BFQ1103",
628        "quadlet-native-document-set",
629        Error,
630        "QuadletLens reported a native document-set diagnostic.",
631        "Correct missing, ambiguous, or invalid cross-document relationships."
632    ),
633    rule!(
634        QuadletNativeFailure,
635        "BFQ1104",
636        "quadlet-native-failure",
637        Error,
638        "QuadletLens could not construct the native input boundary.",
639        "Correct the reported input and native stage before retrying."
640    ),
641];
642
643impl RuleId {
644    /// Returns this rule's immutable catalogue definition.
645    #[must_use]
646    pub const fn definition(self) -> &'static DiagnosticRule {
647        match self {
648            Self::ComposeModelInvalid => &RULES[0],
649            Self::ComposeProfileRequired => &RULES[1],
650            Self::ComposeProfileMismatch => &RULES[2],
651            Self::ComposeIntentUnsupported => &RULES[3],
652            Self::ComposeValueInvalid => &RULES[4],
653            Self::ComposeTargetInvalid => &RULES[5],
654            Self::ComposeOutputUnsupported => &RULES[6],
655            Self::ComposeGenerationFailed => &RULES[7],
656            Self::ComposeCompatibilityConstraint => &RULES[8],
657            Self::ComposeUnsetVariable => &RULES[9],
658            Self::ComposeRequiredVariable => &RULES[10],
659            Self::ComposeInterpolationInvalid => &RULES[11],
660            Self::ComposeInterpolationNestingLimit => &RULES[12],
661            Self::ComposeUnresolvedVariable => &RULES[13],
662            Self::ComposeNativeError => &RULES[14],
663            Self::ComposeNativeWarning => &RULES[15],
664            Self::ComposeNativeNote => &RULES[16],
665            Self::OrchestrationFailed => &RULES[17],
666            Self::InputReadFailed => &RULES[18],
667            Self::ComposeProjectRootInvalid => &RULES[19],
668            Self::PodmanTargetSelectionInvalid => &RULES[20],
669            Self::InterpolationInputInvalid => &RULES[21],
670            Self::ComposeLoadFailed => &RULES[22],
671            Self::ConversionFailed => &RULES[23],
672            Self::QuadletParseFailed => &RULES[24],
673            Self::OutputDirectoryNotEmpty => &RULES[25],
674            Self::OutputPathInvalid => &RULES[26],
675            Self::OutputWriteFailed => &RULES[27],
676            Self::ReportWriteFailed => &RULES[28],
677            Self::SupportBundleWriteFailed => &RULES[29],
678            Self::PodmanSourceInvalid => &RULES[30],
679            Self::PodmanInputUnmodelled => &RULES[31],
680            Self::PodmanPromotionRequired => &RULES[32],
681            Self::PodmanIdentityConflict => &RULES[33],
682            Self::PodmanSecretGrantIncomplete => &RULES[34],
683            Self::PodmanTargetInvalid => &RULES[35],
684            Self::PodmanOutputUnsupported => &RULES[36],
685            Self::PodmanPlanningFailed => &RULES[37],
686            Self::QuadletTargetInvalid => &RULES[38],
687            Self::QuadletOpenEndedTarget => &RULES[39],
688            Self::QuadletOutputUnsupported => &RULES[40],
689            Self::QuadletValueInvalid => &RULES[41],
690            Self::QuadletGenerationFailed => &RULES[42],
691            Self::QuadletCapabilityUnavailable => &RULES[43],
692            Self::QuadletGroupingApproximation => &RULES[44],
693            Self::QuadletDependencyUnsupported => &RULES[45],
694            Self::QuadletRestartApproximation => &RULES[46],
695            Self::QuadletEnvironmentFileApproximation => &RULES[47],
696            Self::QuadletGroupingInvalid => &RULES[48],
697            Self::QuadletDependencyInvalid => &RULES[49],
698            Self::QuadletCapabilityDeprecated => &RULES[50],
699            Self::QuadletUnresolvedVariable => &RULES[51],
700            Self::QuadletSourceInvalid => &RULES[52],
701            Self::QuadletModelInvalid => &RULES[53],
702            Self::QuadletInputUnsupported => &RULES[54],
703            Self::QuadletInputApproximation => &RULES[55],
704            Self::QuadletNativeSyntax => &RULES[56],
705            Self::QuadletNativeModel => &RULES[57],
706            Self::QuadletNativeDocumentSet => &RULES[58],
707            Self::QuadletNativeFailure => &RULES[59],
708        }
709    }
710}
711
712/// Finds a rule by exact code or human-readable name.
713#[must_use]
714pub fn find_rule(value: &str) -> Option<&'static DiagnosticRule> {
715    RULES
716        .iter()
717        .find(|rule| rule.code.eq_ignore_ascii_case(value) || rule.name.eq_ignore_ascii_case(value))
718}
719
720#[cfg(test)]
721mod tests {
722    use std::collections::BTreeSet;
723
724    use super::{RULES, RuleId, find_rule};
725
726    #[test]
727    fn catalogue_codes_names_and_typed_indices_are_unique_and_valid() -> Result<(), String> {
728        let mut codes = BTreeSet::new();
729        let mut names = BTreeSet::new();
730        let mut ids = BTreeSet::new();
731        for rule in RULES {
732            rule.diagnostic_code().map_err(|error| error.to_string())?;
733            assert!(ids.insert(rule.id()), "duplicate typed id {:?}", rule.id());
734            assert_eq!(rule.id().definition(), rule);
735            assert!(codes.insert(rule.code()), "duplicate code {}", rule.code());
736            assert!(names.insert(rule.name()), "duplicate name {}", rule.name());
737            assert!(!rule.description().is_empty());
738            assert!(!rule.help().is_empty());
739            assert_ne!(
740                rule.owner(),
741                "BoxFerry engine",
742                "uncatalogued namespace for {}",
743                rule.code()
744            );
745        }
746        assert!(RULES.windows(2).all(|pair| pair[0].code() < pair[1].code()));
747        assert_eq!(RuleId::OutputWriteFailed.definition().code(), "BFO2003");
748        assert_eq!(RuleId::PodmanPromotionRequired.definition().code(), "BFP0003");
749        assert_eq!(
750            find_rule("podman-output-unsupported").map(|rule| rule.owner()),
751            Some("Podman adapter")
752        );
753        assert_eq!(find_rule("quadlet-restart-policy-approximation"), find_rule("BFQ0009"));
754        Ok(())
755    }
756}