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