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