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