compose-lens 0.1.1

Loss-aware parsing, processing, validation, and rendering of Compose projects
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
//! Built-in compatibility targets, rules, and evidence.

use super::{ImplementationVersion, VersionRange};
use crate::diagnostic::Severity;

const SPEC_URL: &str = "https://github.com/compose-spec/compose-spec/blob/main/spec.md";
const DOCKER_MERGE_URL: &str = "https://docs.docker.com/reference/compose-file/merge/";
const DOCKER_SELINUX_ISSUE_URL: &str = "https://github.com/docker/compose/issues/13396";
const DOCKER_HOST_GATEWAY_URL: &str = "https://docs.docker.com/compose/how-tos/networking/";
const PODMAN_5_4_RUN_URL: &str = "https://docs.podman.io/en/v5.4.0/markdown/podman-run.1.html";
const PROVIDER_CONFORMANCE_URL: &str =
    "https://github.com/Strukturpiloten/compose-lens/blob/main/docs/research/provider-config-conformance-2026-07-31.md";

const SPEC_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
    EvidenceKind::Specification,
    SPEC_URL,
    "current Compose Specification syntax",
    None,
    None,
)];
const DOCKER_OVERRIDE_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
    EvidenceKind::OfficialDocumentation,
    DOCKER_MERGE_URL,
    "Docker documents !override as requiring Compose 2.24.4 or newer",
    Some(VersionRange::from_minimum(ImplementationVersion::new(2, 24, 4))),
    None,
)];
const DOCKER_RESET_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
    EvidenceKind::OfficialDocumentation,
    DOCKER_MERGE_URL,
    "current Docker documentation describes !reset but does not identify its first supported version",
    None,
    None,
)];
const DOCKER_PODMAN_SELINUX_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
    EvidenceKind::IssueReproduction,
    DOCKER_SELINUX_ISSUE_URL,
    "Docker Compose 2.40.3 with Podman 5.6.2 applied short-form relabeling but long-form relabeling was ineffective",
    Some(VersionRange::exact(ImplementationVersion::new(2, 40, 3))),
    Some(VersionRange::exact(ImplementationVersion::new(5, 6, 2))),
)];
const HOST_GATEWAY_EVIDENCE: &[CompatibilityEvidence] = &[
    CompatibilityEvidence::new(
        EvidenceKind::OfficialDocumentation,
        DOCKER_HOST_GATEWAY_URL,
        "Docker documents host-gateway as an implementation-provided host address",
        None,
        None,
    ),
    CompatibilityEvidence::new(
        EvidenceKind::OfficialDocumentation,
        PODMAN_5_4_RUN_URL,
        "Podman 5.4 documents host-gateway for --add-host",
        None,
        Some(VersionRange::from_minimum(ImplementationVersion::new(5, 4, 0))),
    ),
];
const PODMAN_USERNS_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
    EvidenceKind::OfficialDocumentation,
    PODMAN_5_4_RUN_URL,
    "Podman 5.4 documents keep-id, auto, and nomap user namespace modes",
    None,
    Some(VersionRange::from_minimum(ImplementationVersion::new(5, 4, 0))),
)];
const DOCKER_2_24_3_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
    "reviewed feature-specific Docker Compose 2.24.3 config observations",
    ImplementationVersion::new(2, 24, 3),
)];
const DOCKER_2_24_4_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
    "reviewed feature-specific Docker Compose 2.24.4 config observations",
    ImplementationVersion::new(2, 24, 4),
)];
const DOCKER_2_40_3_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
    "reviewed feature-specific Docker Compose 2.40.3 config observations",
    ImplementationVersion::new(2, 40, 3),
)];
const DOCKER_5_3_1_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
    "reviewed feature-specific Docker Compose 5.3.1 config observations",
    ImplementationVersion::new(5, 3, 1),
)];
const PODMAN_COMPOSE_1_3_0_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
    "reviewed feature-specific podman-compose 1.3.0 config observations",
    ImplementationVersion::new(1, 3, 0),
)];
const PODMAN_COMPOSE_1_5_0_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
    "reviewed feature-specific podman-compose 1.5.0 config observations",
    ImplementationVersion::new(1, 5, 0),
)];

/// A compatibility-sensitive Compose construct recognized by this release.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompatibilityFeature {
    /// An image reference combining a tag and a digest.
    ImageTagAndDigest,
    /// `SELinux` relabeling requested through short volume syntax.
    ShortBindSelinuxRelabel,
    /// `SELinux` relabeling requested through long bind syntax.
    LongBindSelinuxRelabel,
    /// Compose's `!reset` merge tag.
    ResetTag,
    /// Compose's `!override` merge tag.
    OverrideTag,
    /// The runtime-resolved `host-gateway` extra-host token.
    HostGatewayToken,
    /// A Podman-specific `userns_mode` value such as `keep-id`.
    PodmanUserNamespaceMode,
    /// A reserved `x-` extension field.
    ExtensionField,
}

/// How a selected profile classifies one construct.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompatibilityClassification {
    /// The evidence supports the construct for the selected context.
    Supported,
    /// The construct uses Compose's reserved extension mechanism.
    Extension,
    /// The construct is accepted or meaningful only under implementation-specific behavior.
    ImplementationSpecific,
    /// The construct remains accepted but is deprecated in the selected context.
    Deprecated,
    /// Evidence shows that the construct is unavailable or ineffective.
    Unsupported,
    /// Available evidence is insufficient for the selected versions.
    Unknown,
}

/// The Compose parser/provider whose behavior is being assessed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ComposeProvider {
    /// The current Compose Specification, without claiming runtime support.
    Specification,
    /// Docker Compose at an exact released version.
    DockerCompose(ImplementationVersion),
    /// The independent `containers/podman-compose` provider at an exact released version.
    PodmanCompose(ImplementationVersion),
    /// Preservation-oriented handling that deliberately makes no runtime claim.
    Tolerant,
}

/// The backend container runtime used by a Compose provider.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContainerRuntime {
    /// Docker Engine at an exact released version.
    DockerEngine(ImplementationVersion),
    /// Podman at an exact released version.
    Podman(ImplementationVersion),
}

/// A caller-selected provider and optional backend runtime.
///
/// `podman compose` is intentionally not represented as a provider: Podman documents that command
/// as a wrapper around an external provider. Callers must identify the provider it actually runs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompatibilityProfile {
    provider: ComposeProvider,
    runtime: Option<ContainerRuntime>,
}

impl CompatibilityProfile {
    /// Creates the specification-oriented profile.
    #[must_use]
    pub const fn specification() -> Self {
        Self {
            provider: ComposeProvider::Specification,
            runtime: None,
        }
    }

    /// Creates a Docker Compose profile for an exact provider version.
    #[must_use]
    pub const fn docker_compose(version: ImplementationVersion) -> Self {
        Self {
            provider: ComposeProvider::DockerCompose(version),
            runtime: None,
        }
    }

    /// Creates a `containers/podman-compose` profile for an exact provider version.
    #[must_use]
    pub const fn podman_compose(version: ImplementationVersion) -> Self {
        Self {
            provider: ComposeProvider::PodmanCompose(version),
            runtime: None,
        }
    }

    /// Creates a tolerant preservation profile that makes no implementation-support claim.
    #[must_use]
    pub const fn tolerant() -> Self {
        Self {
            provider: ComposeProvider::Tolerant,
            runtime: None,
        }
    }

    /// Attaches the exact backend runtime selected by the caller.
    #[must_use]
    pub const fn with_runtime(mut self, runtime: ContainerRuntime) -> Self {
        self.runtime = Some(runtime);
        self
    }

    /// Returns the selected Compose provider.
    #[must_use]
    pub const fn provider(self) -> ComposeProvider {
        self.provider
    }

    /// Returns the selected backend runtime, if supplied.
    #[must_use]
    pub const fn runtime(self) -> Option<ContainerRuntime> {
        self.runtime
    }

    /// Classifies one feature using only versioned built-in evidence.
    #[must_use]
    pub fn classify(self, feature: CompatibilityFeature) -> CompatibilityRule {
        match self.provider {
            ComposeProvider::Specification => specification_rule(feature),
            ComposeProvider::DockerCompose(version) => docker_rule(self, version, feature),
            ComposeProvider::PodmanCompose(version) => podman_compose_rule(version, feature),
            ComposeProvider::Tolerant => tolerant_rule(feature),
        }
    }
}

/// The provenance category of one compatibility claim.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EvidenceKind {
    /// Normative or descriptive Compose Specification text.
    Specification,
    /// Documentation published by the implementation owner.
    OfficialDocumentation,
    /// A versioned public issue containing a reproducible observation.
    IssueReproduction,
    /// A reviewed `ComposeLens` provider-only config observation.
    ProviderConformance,
    /// A ComposeLens-controlled runtime conformance result.
    RuntimeConformance,
}

/// One source supporting a compatibility rule.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompatibilityEvidence {
    kind: EvidenceKind,
    source: &'static str,
    summary: &'static str,
    provider_versions: Option<VersionRange>,
    runtime_versions: Option<VersionRange>,
}

impl CompatibilityEvidence {
    const fn new(
        kind: EvidenceKind,
        source: &'static str,
        summary: &'static str,
        provider_versions: Option<VersionRange>,
        runtime_versions: Option<VersionRange>,
    ) -> Self {
        Self {
            kind,
            source,
            summary,
            provider_versions,
            runtime_versions,
        }
    }

    /// Returns the evidence category.
    #[must_use]
    pub const fn kind(self) -> EvidenceKind {
        self.kind
    }

    /// Returns the authoritative or public evidence URL.
    #[must_use]
    pub const fn source(self) -> &'static str {
        self.source
    }

    /// Returns a concise claim supported by the source.
    #[must_use]
    pub const fn summary(self) -> &'static str {
        self.summary
    }

    /// Returns the provider-version scope, when established.
    #[must_use]
    pub const fn provider_versions(self) -> Option<VersionRange> {
        self.provider_versions
    }

    /// Returns the runtime-version scope, when established.
    #[must_use]
    pub const fn runtime_versions(self) -> Option<VersionRange> {
        self.runtime_versions
    }
}

/// A profile's decision for one compatibility feature.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompatibilityRule {
    feature: CompatibilityFeature,
    classification: CompatibilityClassification,
    diagnostic_severity: Option<Severity>,
    explanation: &'static str,
    evidence: &'static [CompatibilityEvidence],
}

impl CompatibilityRule {
    /// Returns the classified feature.
    #[must_use]
    pub const fn feature(&self) -> CompatibilityFeature {
        self.feature
    }

    /// Returns the compatibility classification.
    #[must_use]
    pub const fn classification(&self) -> CompatibilityClassification {
        self.classification
    }

    /// Returns the diagnostic severity, or `None` when no diagnostic should be emitted.
    #[must_use]
    pub const fn diagnostic_severity(&self) -> Option<Severity> {
        self.diagnostic_severity
    }

    /// Returns a value-free explanation suitable for diagnostics.
    #[must_use]
    pub const fn explanation(&self) -> &'static str {
        self.explanation
    }

    /// Returns the evidence supporting the classification.
    #[must_use]
    pub const fn evidence(&self) -> &'static [CompatibilityEvidence] {
        self.evidence
    }
}

fn specification_rule(feature: CompatibilityFeature) -> CompatibilityRule {
    match feature {
        CompatibilityFeature::ImageTagAndDigest => rule(
            feature,
            CompatibilityClassification::ImplementationSpecific,
            Some(Severity::Warning),
            "the documented image grammar selects a tag or a digest, while real implementations may accept both",
            SPEC_EVIDENCE,
        ),
        CompatibilityFeature::ExtensionField => rule(
            feature,
            CompatibilityClassification::Extension,
            None,
            "x- fields use the Compose extension namespace",
            SPEC_EVIDENCE,
        ),
        CompatibilityFeature::HostGatewayToken => rule(
            feature,
            CompatibilityClassification::ImplementationSpecific,
            Some(Severity::Warning),
            "host-gateway is resolved by container implementations rather than by Compose syntax alone",
            HOST_GATEWAY_EVIDENCE,
        ),
        CompatibilityFeature::PodmanUserNamespaceMode => rule(
            feature,
            CompatibilityClassification::ImplementationSpecific,
            Some(Severity::Warning),
            "the Compose field is portable but keep-id, auto, and nomap values are Podman-specific",
            PODMAN_USERNS_EVIDENCE,
        ),
        CompatibilityFeature::ShortBindSelinuxRelabel
        | CompatibilityFeature::LongBindSelinuxRelabel
        | CompatibilityFeature::ResetTag
        | CompatibilityFeature::OverrideTag => rule(
            feature,
            CompatibilityClassification::Supported,
            None,
            "the construct is defined by the current Compose Specification",
            SPEC_EVIDENCE,
        ),
    }
}

fn docker_rule(
    profile: CompatibilityProfile,
    version: ImplementationVersion,
    feature: CompatibilityFeature,
) -> CompatibilityRule {
    match feature {
        CompatibilityFeature::OverrideTag if version == ImplementationVersion::new(2, 24, 3) => rule(
            feature,
            CompatibilityClassification::Unsupported,
            Some(Severity::Error),
            "Docker Compose 2.24.3 accepted !override syntax but did not apply replacement semantics",
            DOCKER_2_24_3_PROVIDER_EVIDENCE,
        ),
        CompatibilityFeature::OverrideTag if version < ImplementationVersion::new(2, 24, 4) => rule(
            feature,
            CompatibilityClassification::Unsupported,
            Some(Severity::Error),
            "the selected Docker Compose version predates documented !override support",
            DOCKER_OVERRIDE_EVIDENCE,
        ),
        CompatibilityFeature::OverrideTag => rule(
            feature,
            CompatibilityClassification::Supported,
            None,
            "the selected Docker Compose version meets the documented !override minimum",
            DOCKER_OVERRIDE_EVIDENCE,
        ),
        CompatibilityFeature::ResetTag if !docker_provider_evidence(version).is_empty() => rule(
            feature,
            CompatibilityClassification::Supported,
            None,
            "the selected exact Docker Compose version applied !reset in reviewed provider conformance",
            docker_provider_evidence(version),
        ),
        CompatibilityFeature::ResetTag => rule(
            feature,
            CompatibilityClassification::ImplementationSpecific,
            Some(Severity::Warning),
            "Docker documents !reset, but the available evidence does not establish its first supported release",
            DOCKER_RESET_EVIDENCE,
        ),
        CompatibilityFeature::ShortBindSelinuxRelabel if is_reported_selinux_context(profile) => rule(
            feature,
            CompatibilityClassification::Supported,
            None,
            "the exact reported provider/runtime pair applied short-form SELinux relabeling",
            DOCKER_PODMAN_SELINUX_EVIDENCE,
        ),
        CompatibilityFeature::LongBindSelinuxRelabel if is_reported_selinux_context(profile) => rule(
            feature,
            CompatibilityClassification::Unsupported,
            Some(Severity::Error),
            "the exact reported provider/runtime pair accepted this form but did not relabel the host path",
            DOCKER_PODMAN_SELINUX_EVIDENCE,
        ),
        CompatibilityFeature::ShortBindSelinuxRelabel => rule(
            feature,
            CompatibilityClassification::ImplementationSpecific,
            Some(Severity::Warning),
            "SELinux relabeling depends on the backend runtime, host platform, and authored mount form",
            docker_provider_evidence(version),
        ),
        CompatibilityFeature::LongBindSelinuxRelabel => rule(
            feature,
            CompatibilityClassification::Unknown,
            Some(Severity::Warning),
            "no versioned evidence covers long-form SELinux behavior for the selected provider/runtime pair",
            docker_provider_evidence(version),
        ),
        CompatibilityFeature::ImageTagAndDigest if !docker_provider_evidence(version).is_empty() => rule(
            feature,
            CompatibilityClassification::Supported,
            None,
            "the selected exact Docker Compose version retained the combined tag and digest",
            docker_provider_evidence(version),
        ),
        CompatibilityFeature::ImageTagAndDigest => rule(
            feature,
            CompatibilityClassification::ImplementationSpecific,
            Some(Severity::Warning),
            "combined image tags and digests require implementation evidence beyond the documented Compose grammar",
            SPEC_EVIDENCE,
        ),
        CompatibilityFeature::ExtensionField => rule(
            feature,
            CompatibilityClassification::Extension,
            None,
            "x- fields use the Compose extension namespace",
            SPEC_EVIDENCE,
        ),
        CompatibilityFeature::HostGatewayToken => rule(
            feature,
            CompatibilityClassification::ImplementationSpecific,
            Some(Severity::Warning),
            "host-gateway depends on provider pass-through and runtime network configuration",
            HOST_GATEWAY_EVIDENCE,
        ),
        CompatibilityFeature::PodmanUserNamespaceMode => rule(
            feature,
            CompatibilityClassification::Unknown,
            Some(Severity::Warning),
            "Podman documents this runtime value, but no versioned Docker Compose pass-through observation is recorded",
            PODMAN_USERNS_EVIDENCE,
        ),
    }
}

fn podman_compose_rule(version: ImplementationVersion, feature: CompatibilityFeature) -> CompatibilityRule {
    if feature == CompatibilityFeature::ExtensionField {
        return rule(
            feature,
            CompatibilityClassification::Extension,
            None,
            "x- fields use the Compose extension namespace",
            SPEC_EVIDENCE,
        );
    }
    let evidence = podman_compose_provider_evidence(version);
    if !evidence.is_empty() {
        return match feature {
            CompatibilityFeature::ImageTagAndDigest => rule(
                feature,
                CompatibilityClassification::Supported,
                None,
                "the selected exact podman-compose version retained the combined tag and digest",
                evidence,
            ),
            CompatibilityFeature::ResetTag => rule(
                feature,
                CompatibilityClassification::Unsupported,
                Some(Severity::Error),
                "the selected exact podman-compose version failed while processing !reset",
                evidence,
            ),
            CompatibilityFeature::OverrideTag if version == ImplementationVersion::new(1, 3, 0) => rule(
                feature,
                CompatibilityClassification::Unsupported,
                Some(Severity::Error),
                "podman-compose 1.3.0 rejected !override",
                evidence,
            ),
            CompatibilityFeature::OverrideTag => rule(
                feature,
                CompatibilityClassification::Supported,
                None,
                "podman-compose 1.5.0 applied !override replacement semantics",
                evidence,
            ),
            CompatibilityFeature::ShortBindSelinuxRelabel | CompatibilityFeature::LongBindSelinuxRelabel => rule(
                feature,
                CompatibilityClassification::Unknown,
                Some(Severity::Warning),
                "provider config accepted the SELinux form, but no reviewed runtime-effect record establishes relabeling",
                evidence,
            ),
            CompatibilityFeature::HostGatewayToken => rule(
                feature,
                CompatibilityClassification::Unknown,
                Some(Severity::Warning),
                "Podman 5.4 documents the runtime token, but provider pass-through has not been recorded",
                HOST_GATEWAY_EVIDENCE,
            ),
            CompatibilityFeature::PodmanUserNamespaceMode => rule(
                feature,
                CompatibilityClassification::Unknown,
                Some(Severity::Warning),
                "Podman 5.4 documents the runtime mode, but provider pass-through has not been recorded",
                PODMAN_USERNS_EVIDENCE,
            ),
            CompatibilityFeature::ExtensionField => unreachable!("extension fields returned above"),
        };
    }
    rule(
        feature,
        CompatibilityClassification::Unknown,
        Some(Severity::Warning),
        "no versioned podman-compose conformance evidence covers this construct yet",
        &[],
    )
}

const fn provider_evidence(summary: &'static str, version: ImplementationVersion) -> CompatibilityEvidence {
    CompatibilityEvidence::new(
        EvidenceKind::ProviderConformance,
        PROVIDER_CONFORMANCE_URL,
        summary,
        Some(VersionRange::exact(version)),
        None,
    )
}

fn docker_provider_evidence(version: ImplementationVersion) -> &'static [CompatibilityEvidence] {
    match version {
        value if value == ImplementationVersion::new(2, 24, 3) => DOCKER_2_24_3_PROVIDER_EVIDENCE,
        value if value == ImplementationVersion::new(2, 24, 4) => DOCKER_2_24_4_PROVIDER_EVIDENCE,
        value if value == ImplementationVersion::new(2, 40, 3) => DOCKER_2_40_3_PROVIDER_EVIDENCE,
        value if value == ImplementationVersion::new(5, 3, 1) => DOCKER_5_3_1_PROVIDER_EVIDENCE,
        _ => &[],
    }
}

fn podman_compose_provider_evidence(version: ImplementationVersion) -> &'static [CompatibilityEvidence] {
    match version {
        value if value == ImplementationVersion::new(1, 3, 0) => PODMAN_COMPOSE_1_3_0_PROVIDER_EVIDENCE,
        value if value == ImplementationVersion::new(1, 5, 0) => PODMAN_COMPOSE_1_5_0_PROVIDER_EVIDENCE,
        _ => &[],
    }
}

fn tolerant_rule(feature: CompatibilityFeature) -> CompatibilityRule {
    if feature == CompatibilityFeature::ExtensionField {
        return rule(
            feature,
            CompatibilityClassification::Extension,
            None,
            "x- fields are preserved as Compose extensions",
            SPEC_EVIDENCE,
        );
    }
    rule(
        feature,
        CompatibilityClassification::Unknown,
        Some(Severity::Note),
        "tolerant preservation deliberately makes no runtime-support claim",
        &[],
    )
}

fn is_reported_selinux_context(profile: CompatibilityProfile) -> bool {
    profile.provider == ComposeProvider::DockerCompose(ImplementationVersion::new(2, 40, 3))
        && profile.runtime == Some(ContainerRuntime::Podman(ImplementationVersion::new(5, 6, 2)))
}

fn rule(
    feature: CompatibilityFeature,
    classification: CompatibilityClassification,
    diagnostic_severity: Option<Severity>,
    explanation: &'static str,
    evidence: &'static [CompatibilityEvidence],
) -> CompatibilityRule {
    CompatibilityRule {
        feature,
        classification,
        diagnostic_severity,
        explanation,
        evidence,
    }
}