1use crate::{DiagnosticCode, InvalidDiagnosticCode, Severity};
4
5#[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#[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 #[must_use]
92 pub const fn id(self) -> RuleId {
93 self.id
94 }
95
96 #[must_use]
98 pub const fn code(self) -> &'static str {
99 self.code
100 }
101
102 #[must_use]
104 pub const fn name(self) -> &'static str {
105 self.name
106 }
107
108 #[must_use]
110 pub const fn default_severity(self) -> Severity {
111 self.default_severity
112 }
113
114 #[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 #[must_use]
128 pub const fn description(self) -> &'static str {
129 self.description
130 }
131
132 #[must_use]
134 pub const fn help(self) -> &'static str {
135 self.help
136 }
137
138 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
161pub 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 #[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#[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}