Skip to main content

compose_lens/validation/
profile.rs

1//! Built-in compatibility targets, rules, and evidence.
2
3use super::{ImplementationVersion, VersionRange};
4use crate::diagnostic::Severity;
5
6const SPEC_URL: &str = "https://github.com/compose-spec/compose-spec/blob/main/spec.md";
7const DOCKER_MERGE_URL: &str = "https://docs.docker.com/reference/compose-file/merge/";
8const DOCKER_SELINUX_ISSUE_URL: &str = "https://github.com/docker/compose/issues/13396";
9const DOCKER_HOST_GATEWAY_URL: &str = "https://docs.docker.com/compose/how-tos/networking/";
10const PODMAN_5_4_RUN_URL: &str = "https://docs.podman.io/en/v5.4.0/markdown/podman-run.1.html";
11const PROVIDER_CONFORMANCE_URL: &str =
12    "https://github.com/Strukturpiloten/compose-lens/blob/main/docs/research/provider-config-conformance-2026-07-31.md";
13
14const SPEC_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
15    EvidenceKind::Specification,
16    SPEC_URL,
17    "current Compose Specification syntax",
18    None,
19    None,
20)];
21const DOCKER_OVERRIDE_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
22    EvidenceKind::OfficialDocumentation,
23    DOCKER_MERGE_URL,
24    "Docker documents !override as requiring Compose 2.24.4 or newer",
25    Some(VersionRange::from_minimum(ImplementationVersion::new(2, 24, 4))),
26    None,
27)];
28const DOCKER_RESET_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
29    EvidenceKind::OfficialDocumentation,
30    DOCKER_MERGE_URL,
31    "current Docker documentation describes !reset but does not identify its first supported version",
32    None,
33    None,
34)];
35const DOCKER_PODMAN_SELINUX_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
36    EvidenceKind::IssueReproduction,
37    DOCKER_SELINUX_ISSUE_URL,
38    "Docker Compose 2.40.3 with Podman 5.6.2 applied short-form relabeling but long-form relabeling was ineffective",
39    Some(VersionRange::exact(ImplementationVersion::new(2, 40, 3))),
40    Some(VersionRange::exact(ImplementationVersion::new(5, 6, 2))),
41)];
42const HOST_GATEWAY_EVIDENCE: &[CompatibilityEvidence] = &[
43    CompatibilityEvidence::new(
44        EvidenceKind::OfficialDocumentation,
45        DOCKER_HOST_GATEWAY_URL,
46        "Docker documents host-gateway as an implementation-provided host address",
47        None,
48        None,
49    ),
50    CompatibilityEvidence::new(
51        EvidenceKind::OfficialDocumentation,
52        PODMAN_5_4_RUN_URL,
53        "Podman 5.4 documents host-gateway for --add-host",
54        None,
55        Some(VersionRange::from_minimum(ImplementationVersion::new(5, 4, 0))),
56    ),
57];
58const PODMAN_USERNS_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
59    EvidenceKind::OfficialDocumentation,
60    PODMAN_5_4_RUN_URL,
61    "Podman 5.4 documents keep-id, auto, and nomap user namespace modes",
62    None,
63    Some(VersionRange::from_minimum(ImplementationVersion::new(5, 4, 0))),
64)];
65const DOCKER_2_24_3_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
66    "reviewed feature-specific Docker Compose 2.24.3 config observations",
67    ImplementationVersion::new(2, 24, 3),
68)];
69const DOCKER_2_24_4_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
70    "reviewed feature-specific Docker Compose 2.24.4 config observations",
71    ImplementationVersion::new(2, 24, 4),
72)];
73const DOCKER_2_40_3_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
74    "reviewed feature-specific Docker Compose 2.40.3 config observations",
75    ImplementationVersion::new(2, 40, 3),
76)];
77const DOCKER_5_3_1_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
78    "reviewed feature-specific Docker Compose 5.3.1 config observations",
79    ImplementationVersion::new(5, 3, 1),
80)];
81const PODMAN_COMPOSE_1_3_0_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
82    "reviewed feature-specific podman-compose 1.3.0 config observations",
83    ImplementationVersion::new(1, 3, 0),
84)];
85const PODMAN_COMPOSE_1_5_0_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
86    "reviewed feature-specific podman-compose 1.5.0 config observations",
87    ImplementationVersion::new(1, 5, 0),
88)];
89
90/// A compatibility-sensitive Compose construct recognized by this release.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub enum CompatibilityFeature {
94    /// An image reference combining a tag and a digest.
95    ImageTagAndDigest,
96    /// `SELinux` relabeling requested through short volume syntax.
97    ShortBindSelinuxRelabel,
98    /// `SELinux` relabeling requested through long bind syntax.
99    LongBindSelinuxRelabel,
100    /// Compose's `!reset` merge tag.
101    ResetTag,
102    /// Compose's `!override` merge tag.
103    OverrideTag,
104    /// The runtime-resolved `host-gateway` extra-host token.
105    HostGatewayToken,
106    /// A Podman-specific `userns_mode` value such as `keep-id`.
107    PodmanUserNamespaceMode,
108    /// A reserved `x-` extension field.
109    ExtensionField,
110}
111
112/// How a selected profile classifies one construct.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114#[non_exhaustive]
115pub enum CompatibilityClassification {
116    /// The evidence supports the construct for the selected context.
117    Supported,
118    /// The construct uses Compose's reserved extension mechanism.
119    Extension,
120    /// The construct is accepted or meaningful only under implementation-specific behavior.
121    ImplementationSpecific,
122    /// The construct remains accepted but is deprecated in the selected context.
123    Deprecated,
124    /// Evidence shows that the construct is unavailable or ineffective.
125    Unsupported,
126    /// Available evidence is insufficient for the selected versions.
127    Unknown,
128}
129
130/// The Compose parser/provider whose behavior is being assessed.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132#[non_exhaustive]
133pub enum ComposeProvider {
134    /// The current Compose Specification, without claiming runtime support.
135    Specification,
136    /// Docker Compose at an exact released version.
137    DockerCompose(ImplementationVersion),
138    /// The independent `containers/podman-compose` provider at an exact released version.
139    PodmanCompose(ImplementationVersion),
140    /// Preservation-oriented handling that deliberately makes no runtime claim.
141    Tolerant,
142}
143
144/// The backend container runtime used by a Compose provider.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
146#[non_exhaustive]
147pub enum ContainerRuntime {
148    /// Docker Engine at an exact released version.
149    DockerEngine(ImplementationVersion),
150    /// Podman at an exact released version.
151    Podman(ImplementationVersion),
152}
153
154/// A caller-selected provider and optional backend runtime.
155///
156/// `podman compose` is intentionally not represented as a provider: Podman documents that command
157/// as a wrapper around an external provider. Callers must identify the provider it actually runs.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159pub struct CompatibilityProfile {
160    provider: ComposeProvider,
161    runtime: Option<ContainerRuntime>,
162}
163
164impl CompatibilityProfile {
165    /// Creates the specification-oriented profile.
166    #[must_use]
167    pub const fn specification() -> Self {
168        Self {
169            provider: ComposeProvider::Specification,
170            runtime: None,
171        }
172    }
173
174    /// Creates a Docker Compose profile for an exact provider version.
175    #[must_use]
176    pub const fn docker_compose(version: ImplementationVersion) -> Self {
177        Self {
178            provider: ComposeProvider::DockerCompose(version),
179            runtime: None,
180        }
181    }
182
183    /// Creates a `containers/podman-compose` profile for an exact provider version.
184    #[must_use]
185    pub const fn podman_compose(version: ImplementationVersion) -> Self {
186        Self {
187            provider: ComposeProvider::PodmanCompose(version),
188            runtime: None,
189        }
190    }
191
192    /// Creates a tolerant preservation profile that makes no implementation-support claim.
193    #[must_use]
194    pub const fn tolerant() -> Self {
195        Self {
196            provider: ComposeProvider::Tolerant,
197            runtime: None,
198        }
199    }
200
201    /// Attaches the exact backend runtime selected by the caller.
202    #[must_use]
203    pub const fn with_runtime(mut self, runtime: ContainerRuntime) -> Self {
204        self.runtime = Some(runtime);
205        self
206    }
207
208    /// Returns the selected Compose provider.
209    #[must_use]
210    pub const fn provider(self) -> ComposeProvider {
211        self.provider
212    }
213
214    /// Returns the selected backend runtime, if supplied.
215    #[must_use]
216    pub const fn runtime(self) -> Option<ContainerRuntime> {
217        self.runtime
218    }
219
220    /// Classifies one feature using only versioned built-in evidence.
221    #[must_use]
222    pub fn classify(self, feature: CompatibilityFeature) -> CompatibilityRule {
223        match self.provider {
224            ComposeProvider::Specification => specification_rule(feature),
225            ComposeProvider::DockerCompose(version) => docker_rule(self, version, feature),
226            ComposeProvider::PodmanCompose(version) => podman_compose_rule(version, feature),
227            ComposeProvider::Tolerant => tolerant_rule(feature),
228        }
229    }
230}
231
232/// The provenance category of one compatibility claim.
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
234#[non_exhaustive]
235pub enum EvidenceKind {
236    /// Normative or descriptive Compose Specification text.
237    Specification,
238    /// Documentation published by the implementation owner.
239    OfficialDocumentation,
240    /// A versioned public issue containing a reproducible observation.
241    IssueReproduction,
242    /// A reviewed `ComposeLens` provider-only config observation.
243    ProviderConformance,
244    /// A ComposeLens-controlled runtime conformance result.
245    RuntimeConformance,
246}
247
248/// One source supporting a compatibility rule.
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250pub struct CompatibilityEvidence {
251    kind: EvidenceKind,
252    source: &'static str,
253    summary: &'static str,
254    provider_versions: Option<VersionRange>,
255    runtime_versions: Option<VersionRange>,
256}
257
258impl CompatibilityEvidence {
259    const fn new(
260        kind: EvidenceKind,
261        source: &'static str,
262        summary: &'static str,
263        provider_versions: Option<VersionRange>,
264        runtime_versions: Option<VersionRange>,
265    ) -> Self {
266        Self {
267            kind,
268            source,
269            summary,
270            provider_versions,
271            runtime_versions,
272        }
273    }
274
275    /// Returns the evidence category.
276    #[must_use]
277    pub const fn kind(self) -> EvidenceKind {
278        self.kind
279    }
280
281    /// Returns the authoritative or public evidence URL.
282    #[must_use]
283    pub const fn source(self) -> &'static str {
284        self.source
285    }
286
287    /// Returns a concise claim supported by the source.
288    #[must_use]
289    pub const fn summary(self) -> &'static str {
290        self.summary
291    }
292
293    /// Returns the provider-version scope, when established.
294    #[must_use]
295    pub const fn provider_versions(self) -> Option<VersionRange> {
296        self.provider_versions
297    }
298
299    /// Returns the runtime-version scope, when established.
300    #[must_use]
301    pub const fn runtime_versions(self) -> Option<VersionRange> {
302        self.runtime_versions
303    }
304}
305
306/// A profile's decision for one compatibility feature.
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct CompatibilityRule {
309    feature: CompatibilityFeature,
310    classification: CompatibilityClassification,
311    diagnostic_severity: Option<Severity>,
312    explanation: &'static str,
313    evidence: &'static [CompatibilityEvidence],
314}
315
316impl CompatibilityRule {
317    /// Returns the classified feature.
318    #[must_use]
319    pub const fn feature(&self) -> CompatibilityFeature {
320        self.feature
321    }
322
323    /// Returns the compatibility classification.
324    #[must_use]
325    pub const fn classification(&self) -> CompatibilityClassification {
326        self.classification
327    }
328
329    /// Returns the diagnostic severity, or `None` when no diagnostic should be emitted.
330    #[must_use]
331    pub const fn diagnostic_severity(&self) -> Option<Severity> {
332        self.diagnostic_severity
333    }
334
335    /// Returns a value-free explanation suitable for diagnostics.
336    #[must_use]
337    pub const fn explanation(&self) -> &'static str {
338        self.explanation
339    }
340
341    /// Returns the evidence supporting the classification.
342    #[must_use]
343    pub const fn evidence(&self) -> &'static [CompatibilityEvidence] {
344        self.evidence
345    }
346}
347
348fn specification_rule(feature: CompatibilityFeature) -> CompatibilityRule {
349    match feature {
350        CompatibilityFeature::ImageTagAndDigest => rule(
351            feature,
352            CompatibilityClassification::ImplementationSpecific,
353            Some(Severity::Warning),
354            "the documented image grammar selects a tag or a digest, while real implementations may accept both",
355            SPEC_EVIDENCE,
356        ),
357        CompatibilityFeature::ExtensionField => rule(
358            feature,
359            CompatibilityClassification::Extension,
360            None,
361            "x- fields use the Compose extension namespace",
362            SPEC_EVIDENCE,
363        ),
364        CompatibilityFeature::HostGatewayToken => rule(
365            feature,
366            CompatibilityClassification::ImplementationSpecific,
367            Some(Severity::Warning),
368            "host-gateway is resolved by container implementations rather than by Compose syntax alone",
369            HOST_GATEWAY_EVIDENCE,
370        ),
371        CompatibilityFeature::PodmanUserNamespaceMode => rule(
372            feature,
373            CompatibilityClassification::ImplementationSpecific,
374            Some(Severity::Warning),
375            "the Compose field is portable but keep-id, auto, and nomap values are Podman-specific",
376            PODMAN_USERNS_EVIDENCE,
377        ),
378        CompatibilityFeature::ShortBindSelinuxRelabel
379        | CompatibilityFeature::LongBindSelinuxRelabel
380        | CompatibilityFeature::ResetTag
381        | CompatibilityFeature::OverrideTag => rule(
382            feature,
383            CompatibilityClassification::Supported,
384            None,
385            "the construct is defined by the current Compose Specification",
386            SPEC_EVIDENCE,
387        ),
388    }
389}
390
391fn docker_rule(
392    profile: CompatibilityProfile,
393    version: ImplementationVersion,
394    feature: CompatibilityFeature,
395) -> CompatibilityRule {
396    match feature {
397        CompatibilityFeature::OverrideTag if version == ImplementationVersion::new(2, 24, 3) => rule(
398            feature,
399            CompatibilityClassification::Unsupported,
400            Some(Severity::Error),
401            "Docker Compose 2.24.3 accepted !override syntax but did not apply replacement semantics",
402            DOCKER_2_24_3_PROVIDER_EVIDENCE,
403        ),
404        CompatibilityFeature::OverrideTag if version < ImplementationVersion::new(2, 24, 4) => rule(
405            feature,
406            CompatibilityClassification::Unsupported,
407            Some(Severity::Error),
408            "the selected Docker Compose version predates documented !override support",
409            DOCKER_OVERRIDE_EVIDENCE,
410        ),
411        CompatibilityFeature::OverrideTag => rule(
412            feature,
413            CompatibilityClassification::Supported,
414            None,
415            "the selected Docker Compose version meets the documented !override minimum",
416            DOCKER_OVERRIDE_EVIDENCE,
417        ),
418        CompatibilityFeature::ResetTag if !docker_provider_evidence(version).is_empty() => rule(
419            feature,
420            CompatibilityClassification::Supported,
421            None,
422            "the selected exact Docker Compose version applied !reset in reviewed provider conformance",
423            docker_provider_evidence(version),
424        ),
425        CompatibilityFeature::ResetTag => rule(
426            feature,
427            CompatibilityClassification::ImplementationSpecific,
428            Some(Severity::Warning),
429            "Docker documents !reset, but the available evidence does not establish its first supported release",
430            DOCKER_RESET_EVIDENCE,
431        ),
432        CompatibilityFeature::ShortBindSelinuxRelabel if is_reported_selinux_context(profile) => rule(
433            feature,
434            CompatibilityClassification::Supported,
435            None,
436            "the exact reported provider/runtime pair applied short-form SELinux relabeling",
437            DOCKER_PODMAN_SELINUX_EVIDENCE,
438        ),
439        CompatibilityFeature::LongBindSelinuxRelabel if is_reported_selinux_context(profile) => rule(
440            feature,
441            CompatibilityClassification::Unsupported,
442            Some(Severity::Error),
443            "the exact reported provider/runtime pair accepted this form but did not relabel the host path",
444            DOCKER_PODMAN_SELINUX_EVIDENCE,
445        ),
446        CompatibilityFeature::ShortBindSelinuxRelabel => rule(
447            feature,
448            CompatibilityClassification::ImplementationSpecific,
449            Some(Severity::Warning),
450            "SELinux relabeling depends on the backend runtime, host platform, and authored mount form",
451            docker_provider_evidence(version),
452        ),
453        CompatibilityFeature::LongBindSelinuxRelabel => rule(
454            feature,
455            CompatibilityClassification::Unknown,
456            Some(Severity::Warning),
457            "no versioned evidence covers long-form SELinux behavior for the selected provider/runtime pair",
458            docker_provider_evidence(version),
459        ),
460        CompatibilityFeature::ImageTagAndDigest if !docker_provider_evidence(version).is_empty() => rule(
461            feature,
462            CompatibilityClassification::Supported,
463            None,
464            "the selected exact Docker Compose version retained the combined tag and digest",
465            docker_provider_evidence(version),
466        ),
467        CompatibilityFeature::ImageTagAndDigest => rule(
468            feature,
469            CompatibilityClassification::ImplementationSpecific,
470            Some(Severity::Warning),
471            "combined image tags and digests require implementation evidence beyond the documented Compose grammar",
472            SPEC_EVIDENCE,
473        ),
474        CompatibilityFeature::ExtensionField => rule(
475            feature,
476            CompatibilityClassification::Extension,
477            None,
478            "x- fields use the Compose extension namespace",
479            SPEC_EVIDENCE,
480        ),
481        CompatibilityFeature::HostGatewayToken => rule(
482            feature,
483            CompatibilityClassification::ImplementationSpecific,
484            Some(Severity::Warning),
485            "host-gateway depends on provider pass-through and runtime network configuration",
486            HOST_GATEWAY_EVIDENCE,
487        ),
488        CompatibilityFeature::PodmanUserNamespaceMode => rule(
489            feature,
490            CompatibilityClassification::Unknown,
491            Some(Severity::Warning),
492            "Podman documents this runtime value, but no versioned Docker Compose pass-through observation is recorded",
493            PODMAN_USERNS_EVIDENCE,
494        ),
495    }
496}
497
498fn podman_compose_rule(version: ImplementationVersion, feature: CompatibilityFeature) -> CompatibilityRule {
499    if feature == CompatibilityFeature::ExtensionField {
500        return rule(
501            feature,
502            CompatibilityClassification::Extension,
503            None,
504            "x- fields use the Compose extension namespace",
505            SPEC_EVIDENCE,
506        );
507    }
508    let evidence = podman_compose_provider_evidence(version);
509    if !evidence.is_empty() {
510        return match feature {
511            CompatibilityFeature::ImageTagAndDigest => rule(
512                feature,
513                CompatibilityClassification::Supported,
514                None,
515                "the selected exact podman-compose version retained the combined tag and digest",
516                evidence,
517            ),
518            CompatibilityFeature::ResetTag => rule(
519                feature,
520                CompatibilityClassification::Unsupported,
521                Some(Severity::Error),
522                "the selected exact podman-compose version failed while processing !reset",
523                evidence,
524            ),
525            CompatibilityFeature::OverrideTag if version == ImplementationVersion::new(1, 3, 0) => rule(
526                feature,
527                CompatibilityClassification::Unsupported,
528                Some(Severity::Error),
529                "podman-compose 1.3.0 rejected !override",
530                evidence,
531            ),
532            CompatibilityFeature::OverrideTag => rule(
533                feature,
534                CompatibilityClassification::Supported,
535                None,
536                "podman-compose 1.5.0 applied !override replacement semantics",
537                evidence,
538            ),
539            CompatibilityFeature::ShortBindSelinuxRelabel | CompatibilityFeature::LongBindSelinuxRelabel => rule(
540                feature,
541                CompatibilityClassification::Unknown,
542                Some(Severity::Warning),
543                "provider config accepted the SELinux form, but no reviewed runtime-effect record establishes relabeling",
544                evidence,
545            ),
546            CompatibilityFeature::HostGatewayToken => rule(
547                feature,
548                CompatibilityClassification::Unknown,
549                Some(Severity::Warning),
550                "Podman 5.4 documents the runtime token, but provider pass-through has not been recorded",
551                HOST_GATEWAY_EVIDENCE,
552            ),
553            CompatibilityFeature::PodmanUserNamespaceMode => rule(
554                feature,
555                CompatibilityClassification::Unknown,
556                Some(Severity::Warning),
557                "Podman 5.4 documents the runtime mode, but provider pass-through has not been recorded",
558                PODMAN_USERNS_EVIDENCE,
559            ),
560            CompatibilityFeature::ExtensionField => unreachable!("extension fields returned above"),
561        };
562    }
563    rule(
564        feature,
565        CompatibilityClassification::Unknown,
566        Some(Severity::Warning),
567        "no versioned podman-compose conformance evidence covers this construct yet",
568        &[],
569    )
570}
571
572const fn provider_evidence(summary: &'static str, version: ImplementationVersion) -> CompatibilityEvidence {
573    CompatibilityEvidence::new(
574        EvidenceKind::ProviderConformance,
575        PROVIDER_CONFORMANCE_URL,
576        summary,
577        Some(VersionRange::exact(version)),
578        None,
579    )
580}
581
582fn docker_provider_evidence(version: ImplementationVersion) -> &'static [CompatibilityEvidence] {
583    match version {
584        value if value == ImplementationVersion::new(2, 24, 3) => DOCKER_2_24_3_PROVIDER_EVIDENCE,
585        value if value == ImplementationVersion::new(2, 24, 4) => DOCKER_2_24_4_PROVIDER_EVIDENCE,
586        value if value == ImplementationVersion::new(2, 40, 3) => DOCKER_2_40_3_PROVIDER_EVIDENCE,
587        value if value == ImplementationVersion::new(5, 3, 1) => DOCKER_5_3_1_PROVIDER_EVIDENCE,
588        _ => &[],
589    }
590}
591
592fn podman_compose_provider_evidence(version: ImplementationVersion) -> &'static [CompatibilityEvidence] {
593    match version {
594        value if value == ImplementationVersion::new(1, 3, 0) => PODMAN_COMPOSE_1_3_0_PROVIDER_EVIDENCE,
595        value if value == ImplementationVersion::new(1, 5, 0) => PODMAN_COMPOSE_1_5_0_PROVIDER_EVIDENCE,
596        _ => &[],
597    }
598}
599
600fn tolerant_rule(feature: CompatibilityFeature) -> CompatibilityRule {
601    if feature == CompatibilityFeature::ExtensionField {
602        return rule(
603            feature,
604            CompatibilityClassification::Extension,
605            None,
606            "x- fields are preserved as Compose extensions",
607            SPEC_EVIDENCE,
608        );
609    }
610    rule(
611        feature,
612        CompatibilityClassification::Unknown,
613        Some(Severity::Note),
614        "tolerant preservation deliberately makes no runtime-support claim",
615        &[],
616    )
617}
618
619fn is_reported_selinux_context(profile: CompatibilityProfile) -> bool {
620    profile.provider == ComposeProvider::DockerCompose(ImplementationVersion::new(2, 40, 3))
621        && profile.runtime == Some(ContainerRuntime::Podman(ImplementationVersion::new(5, 6, 2)))
622}
623
624fn rule(
625    feature: CompatibilityFeature,
626    classification: CompatibilityClassification,
627    diagnostic_severity: Option<Severity>,
628    explanation: &'static str,
629    evidence: &'static [CompatibilityEvidence],
630) -> CompatibilityRule {
631    CompatibilityRule {
632        feature,
633        classification,
634        diagnostic_severity,
635        explanation,
636        evidence,
637    }
638}