Skip to main content

minco_test/
plugin_conformance.rs

1use minco_core::{
2    ConfigurationValueKind, DistributionOperation, DistributionResource, Plugin, PluginDescriptor,
3    PluginDistributionKind, PluginDistributionManifest, PluginManager, PluginSelection,
4    ResourceIntent, WakeSource,
5};
6use serde::Serialize;
7use std::{
8    collections::BTreeSet,
9    path::{Path, PathBuf},
10    sync::Arc,
11};
12
13pub const PLUGIN_CONFORMANCE_PROFILE: &str = "minco-plugin-v1";
14pub const ADAPTER_CONFORMANCE_PROFILE: &str = "minco-adapter-v1";
15pub const RUNTIME_CONFORMANCE_PROFILE: &str = "minco-runtime-v1";
16const MAX_DISTRIBUTION_BYTES: u64 = 1024 * 1024;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "snake_case")]
20pub enum ConformanceStatus {
21    Passed,
22    Failed,
23    NotAssessed,
24    NotRun,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28pub struct ConformanceAssurance {
29    pub plugin_contract: ConformanceStatus,
30    pub plugin_lifecycle: ConformanceStatus,
31    pub application_readiness: ConformanceStatus,
32    pub provider_live: ConformanceStatus,
33    pub production_readiness: ConformanceStatus,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37pub struct ConformanceDiagnostic {
38    pub code: String,
39    pub path: String,
40    pub message: String,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
44pub struct PluginConformanceReport {
45    pub profile: String,
46    pub plugin_id: String,
47    pub status: ConformanceStatus,
48    pub assurance: ConformanceAssurance,
49    pub diagnostics: Vec<ConformanceDiagnostic>,
50}
51
52impl PluginConformanceReport {
53    #[must_use]
54    pub const fn is_passed(&self) -> bool {
55        matches!(self.status, ConformanceStatus::Passed)
56    }
57
58    pub fn assert_passed(&self) {
59        assert!(
60            self.is_passed(),
61            "plugin conformance failed: {}",
62            serde_json::to_string_pretty(self).expect("serialize conformance report")
63        );
64    }
65}
66
67pub struct PluginConformance {
68    package_root: PathBuf,
69    target_descriptor: Option<PluginDescriptor>,
70    target_plugin: Option<Arc<dyn Plugin>>,
71    supporting_plugins: Vec<Arc<dyn Plugin>>,
72    configuration: Option<serde_json::Value>,
73}
74
75impl std::fmt::Debug for PluginConformance {
76    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        formatter
78            .debug_struct("PluginConformance")
79            .field("package_root", &self.package_root)
80            .field(
81                "target_plugin",
82                &self
83                    .target_descriptor
84                    .as_ref()
85                    .map(|descriptor| &descriptor.id),
86            )
87            .field("supporting_plugin_count", &self.supporting_plugins.len())
88            .field("configuration_present", &self.configuration.is_some())
89            .finish()
90    }
91}
92
93impl PluginConformance {
94    #[must_use]
95    pub fn for_package(package_root: impl Into<PathBuf>) -> Self {
96        Self {
97            package_root: package_root.into(),
98            target_descriptor: None,
99            target_plugin: None,
100            supporting_plugins: Vec::new(),
101            configuration: None,
102        }
103    }
104
105    #[must_use]
106    pub fn with_descriptor(mut self, descriptor: PluginDescriptor) -> Self {
107        self.target_descriptor = Some(descriptor);
108        self
109    }
110
111    #[must_use]
112    pub fn with_plugin<P>(mut self, plugin: P) -> Self
113    where
114        P: Plugin,
115    {
116        self.target_descriptor = Some(plugin.descriptor());
117        self.target_plugin = Some(Arc::new(plugin));
118        self
119    }
120
121    #[must_use]
122    pub fn with_supporting_plugin<P>(mut self, plugin: P) -> Self
123    where
124        P: Plugin,
125    {
126        self.supporting_plugins.push(Arc::new(plugin));
127        self
128    }
129
130    #[must_use]
131    pub fn with_configuration(mut self, configuration: serde_json::Value) -> Self {
132        self.configuration = Some(configuration);
133        self
134    }
135
136    #[must_use]
137    pub fn run(self) -> PluginConformanceReport {
138        let mut diagnostics = Vec::new();
139        let cargo_path = self.package_root.join("Cargo.toml");
140        let cargo_source = match std::fs::read_to_string(&cargo_path) {
141            Ok(source) => source,
142            Err(error) => {
143                diagnostics.push(diagnostic(
144                    "package_manifest_unreadable",
145                    "Cargo.toml",
146                    format!("cannot read package manifest: {error}"),
147                ));
148                return report("unknown", "unknown", diagnostics);
149            }
150        };
151        let cargo: toml::Value = match toml::from_str(&cargo_source) {
152            Ok(cargo) => cargo,
153            Err(error) => {
154                diagnostics.push(diagnostic(
155                    "package_manifest_invalid",
156                    "Cargo.toml",
157                    format!("package manifest is invalid TOML: {error}"),
158                ));
159                return report("unknown", "unknown", diagnostics);
160            }
161        };
162        let package_name = cargo
163            .get("package")
164            .and_then(|value| value.get("name"))
165            .and_then(toml::Value::as_str)
166            .unwrap_or("unknown");
167        let Some(distribution_file) = cargo
168            .get("package")
169            .and_then(|value| value.get("metadata"))
170            .and_then(|value| value.get("minco"))
171            .and_then(|value| value.get("plugin"))
172            .and_then(toml::Value::as_str)
173        else {
174            diagnostics.push(diagnostic(
175                "distribution_pointer_missing",
176                "package.metadata.minco.plugin",
177                "package metadata must name one distribution record",
178            ));
179            return report("unknown", package_name, diagnostics);
180        };
181
182        let distribution_path = Path::new(distribution_file);
183        if distribution_path.components().count() != 1
184            || distribution_path.file_name().and_then(|name| name.to_str())
185                != Some(distribution_file)
186            || !distribution_path
187                .extension()
188                .is_some_and(|extension| extension.eq_ignore_ascii_case("json"))
189        {
190            diagnostics.push(diagnostic(
191                "distribution_pointer_invalid",
192                "package.metadata.minco.plugin",
193                "distribution pointer must be one package-root JSON filename",
194            ));
195            return report("unknown", package_name, diagnostics);
196        }
197
198        let included = cargo
199            .get("package")
200            .and_then(|value| value.get("include"))
201            .and_then(toml::Value::as_array)
202            .is_some_and(|entries| {
203                entries.iter().any(|entry| {
204                    entry
205                        .as_str()
206                        .is_some_and(|entry| package_include_covers(entry, distribution_file))
207                })
208            });
209        if !included {
210            diagnostics.push(diagnostic(
211                "distribution_not_packaged",
212                "package.include",
213                format!("package include list omits {distribution_file}"),
214            ));
215        }
216
217        let distribution_path = self.package_root.join(distribution_path);
218        let distribution_metadata = match std::fs::symlink_metadata(&distribution_path) {
219            Ok(metadata) => metadata,
220            Err(error) => {
221                diagnostics.push(diagnostic(
222                    "distribution_unreadable",
223                    distribution_file,
224                    format!("cannot inspect distribution record: {error}"),
225                ));
226                return report("unknown", package_name, diagnostics);
227            }
228        };
229        if !distribution_metadata.file_type().is_file() {
230            diagnostics.push(diagnostic(
231                "distribution_not_regular_file",
232                distribution_file,
233                "distribution record must be a regular package-root file",
234            ));
235            return report("unknown", package_name, diagnostics);
236        }
237        if distribution_metadata.len() > MAX_DISTRIBUTION_BYTES {
238            diagnostics.push(diagnostic(
239                "distribution_too_large",
240                distribution_file,
241                format!("distribution record exceeds {MAX_DISTRIBUTION_BYTES} bytes"),
242            ));
243            return report("unknown", package_name, diagnostics);
244        }
245        let distribution_source = match std::fs::read_to_string(&distribution_path) {
246            Ok(source) => source,
247            Err(error) => {
248                diagnostics.push(diagnostic(
249                    "distribution_unreadable",
250                    distribution_file,
251                    format!("cannot read distribution record: {error}"),
252                ));
253                return report("unknown", package_name, diagnostics);
254            }
255        };
256        let distribution: PluginDistributionManifest =
257            match serde_json::from_str(&distribution_source) {
258                Ok(distribution) => distribution,
259                Err(error) => {
260                    diagnostics.push(diagnostic(
261                        "distribution_invalid",
262                        distribution_file,
263                        format!("distribution record is invalid: {error}"),
264                    ));
265                    return report("unknown", package_name, diagnostics);
266                }
267            };
268
269        validate_distribution(&distribution, &mut diagnostics);
270        validate_provider_dependencies(&cargo, distribution.kind, &mut diagnostics);
271        validate_package_assets(&self.package_root, &cargo, &distribution, &mut diagnostics);
272        if let Some(descriptor) = &self.target_descriptor {
273            validate_linked_descriptor(&distribution, descriptor, &mut diagnostics);
274        }
275        let contract_status = status_for(&diagnostics);
276        let lifecycle = if contract_status == ConformanceStatus::Passed {
277            self.target_plugin
278                .as_ref()
279                .map_or(ConformanceStatus::NotAssessed, |target| {
280                    validate_lifecycle(
281                        Arc::clone(target),
282                        &self.supporting_plugins,
283                        self.configuration.as_ref(),
284                        &mut diagnostics,
285                    )
286                })
287        } else {
288            ConformanceStatus::NotAssessed
289        };
290        report_with_assurance(
291            &distribution.conformance.profile,
292            distribution.id.as_str(),
293            diagnostics,
294            contract_status,
295            lifecycle,
296        )
297    }
298}
299
300fn validate_provider_dependencies(
301    cargo: &toml::Value,
302    kind: PluginDistributionKind,
303    diagnostics: &mut Vec<ConformanceDiagnostic>,
304) {
305    if kind != PluginDistributionKind::Plugin {
306        return;
307    }
308    for section in ["dependencies", "build-dependencies"] {
309        validate_dependency_table(cargo.get(section), section, diagnostics);
310    }
311    for (target, configuration) in cargo
312        .get("target")
313        .and_then(toml::Value::as_table)
314        .into_iter()
315        .flat_map(|targets| targets.iter())
316    {
317        for section in ["dependencies", "build-dependencies"] {
318            validate_dependency_table(
319                configuration.get(section),
320                &format!("target.{target}.{section}"),
321                diagnostics,
322            );
323        }
324    }
325}
326
327fn validate_dependency_table(
328    dependencies: Option<&toml::Value>,
329    path: &str,
330    diagnostics: &mut Vec<ConformanceDiagnostic>,
331) {
332    for (dependency, specification) in dependencies
333        .and_then(toml::Value::as_table)
334        .into_iter()
335        .flat_map(toml::Table::iter)
336    {
337        let package = specification
338            .as_table()
339            .and_then(|specification| specification.get("package"))
340            .and_then(toml::Value::as_str)
341            .unwrap_or(dependency);
342        if is_provider_runtime_dependency(package) {
343            diagnostics.push(diagnostic(
344                "provider_dependency_leakage",
345                format!("{path}.{dependency}"),
346                format!(
347                    "provider-neutral plugins must place {package} in an explicit adapter or runtime crate"
348                ),
349            ));
350        }
351    }
352}
353
354fn validate_package_assets(
355    package_root: &Path,
356    cargo: &toml::Value,
357    distribution: &PluginDistributionManifest,
358    diagnostics: &mut Vec<ConformanceDiagnostic>,
359) {
360    for migration in &distribution.migrations {
361        validate_package_asset(
362            package_root,
363            cargo,
364            "migration",
365            &migration.id,
366            &migration.path,
367            diagnostics,
368        );
369    }
370    for seed in &distribution.seeds {
371        validate_package_asset(
372            package_root,
373            cargo,
374            "seed",
375            &seed.id,
376            &seed.path,
377            diagnostics,
378        );
379    }
380    let features = cargo.get("features").and_then(toml::Value::as_table);
381    for resource in &distribution.resources {
382        if let Some(feature) = &resource.feature
383            && !features.is_some_and(|features| features.contains_key(feature))
384        {
385            diagnostics.push(diagnostic(
386                "resource_feature_unknown",
387                format!("resources.{}.feature", resource.id),
388                format!("resource feature {feature} is absent from Cargo.toml"),
389            ));
390        }
391    }
392}
393
394fn validate_package_asset(
395    package_root: &Path,
396    cargo: &toml::Value,
397    kind: &str,
398    id: &str,
399    relative: &str,
400    diagnostics: &mut Vec<ConformanceDiagnostic>,
401) {
402    let field_path = format!("{kind}s.{id}.path");
403    let relative_path = Path::new(relative);
404    if relative.is_empty()
405        || relative_path.is_absolute()
406        || !relative_path
407            .components()
408            .all(|component| matches!(component, std::path::Component::Normal(_)))
409    {
410        diagnostics.push(diagnostic(
411            format!("{kind}_path_invalid"),
412            field_path,
413            "asset path must be normalized and package-relative",
414        ));
415        return;
416    }
417    let asset = package_root.join(relative_path);
418    if !asset.exists() {
419        diagnostics.push(diagnostic(
420            format!("{kind}_path_missing"),
421            field_path,
422            format!("declared asset {relative} does not exist"),
423        ));
424        return;
425    }
426    let inside_package = std::fs::canonicalize(package_root)
427        .and_then(|root| std::fs::canonicalize(&asset).map(|asset| asset.starts_with(root)))
428        .unwrap_or(false);
429    if !inside_package {
430        diagnostics.push(diagnostic(
431            format!("{kind}_path_escapes_package"),
432            field_path,
433            "declared asset resolves outside the package root",
434        ));
435        return;
436    }
437    let included = cargo
438        .get("package")
439        .and_then(|value| value.get("include"))
440        .and_then(toml::Value::as_array)
441        .is_some_and(|entries| {
442            entries.iter().any(|entry| {
443                entry
444                    .as_str()
445                    .is_some_and(|entry| package_include_covers(entry, relative))
446            })
447        });
448    if !included {
449        diagnostics.push(diagnostic(
450            format!("{kind}_not_packaged"),
451            field_path,
452            format!("package include list omits {relative}"),
453        ));
454    }
455}
456
457fn package_include_covers(pattern: &str, relative: &str) -> bool {
458    let pattern = pattern.trim_start_matches('/').trim_end_matches('/');
459    let relative = relative.trim_start_matches('/').trim_end_matches('/');
460    if let Some(prefix) = pattern.strip_suffix("/**") {
461        relative == prefix || relative.starts_with(&format!("{prefix}/"))
462    } else {
463        pattern == relative
464    }
465}
466
467fn is_provider_runtime_dependency(name: &str) -> bool {
468    name == "aws-config"
469        || name.starts_with("aws-sdk-")
470        || name.starts_with("lambda-")
471        || name.starts_with("lambda_")
472        || name == "aws-lambda-events"
473        || name == "aws_lambda_events"
474}
475
476fn validate_linked_descriptor(
477    distribution: &PluginDistributionManifest,
478    descriptor: &PluginDescriptor,
479    diagnostics: &mut Vec<ConformanceDiagnostic>,
480) {
481    if distribution.id != descriptor.id {
482        diagnostics.push(diagnostic(
483            "descriptor_id_mismatch",
484            "descriptor.id",
485            format!(
486                "linked descriptor ID {} does not match distribution ID {}",
487                descriptor.id, distribution.id
488            ),
489        ));
490    }
491    if distribution.plugin_version != descriptor.version {
492        diagnostics.push(diagnostic(
493            "descriptor_version_mismatch",
494            "descriptor.version",
495            format!(
496                "linked descriptor version {} does not match distribution version {}",
497                descriptor.version, distribution.plugin_version
498            ),
499        ));
500    }
501    if distribution.core_compatibility != descriptor.core_compatibility {
502        diagnostics.push(diagnostic(
503            "descriptor_core_compatibility_mismatch",
504            "descriptor.core_compatibility",
505            format!(
506                "linked descriptor core compatibility {} does not match distribution {}",
507                descriptor.core_compatibility, distribution.core_compatibility
508            ),
509        ));
510    }
511    if distribution.stability != descriptor.stability {
512        diagnostics.push(diagnostic(
513            "descriptor_stability_mismatch",
514            "descriptor.stability",
515            "linked descriptor stability does not match the distribution record",
516        ));
517    }
518    if distribution.default_enabled != descriptor.default_enabled {
519        diagnostics.push(diagnostic(
520            "descriptor_default_selection_mismatch",
521            "descriptor.default_enabled",
522            "linked descriptor default selection does not match the distribution record",
523        ));
524    }
525    compare_descriptor_field(
526        "plugin_dependencies",
527        &distribution.plugin_dependencies,
528        &descriptor.plugin_dependencies,
529        diagnostics,
530    );
531    compare_descriptor_field(
532        "requires",
533        &distribution.requires,
534        &descriptor.requires,
535        diagnostics,
536    );
537    compare_descriptor_field(
538        "provides",
539        &distribution.provides,
540        &descriptor.provides,
541        diagnostics,
542    );
543    compare_descriptor_field(
544        "configuration",
545        &distribution.configuration,
546        &descriptor.configuration,
547        diagnostics,
548    );
549    compare_descriptor_field(
550        "health_checks",
551        &distribution.health_checks,
552        &descriptor.health_checks,
553        diagnostics,
554    );
555    compare_descriptor_field(
556        "data_classes",
557        &distribution.data_classes,
558        &descriptor.data_classes,
559        diagnostics,
560    );
561    let distribution_operations = distribution
562        .operations
563        .iter()
564        .cloned()
565        .map(|mut operation| {
566            operation.headers.clear();
567            operation
568        })
569        .collect::<Vec<_>>();
570    let descriptor_operations = descriptor
571        .operations
572        .iter()
573        .map(|operation| DistributionOperation {
574            operation_id: operation.operation_id.clone(),
575            method: operation.method.clone(),
576            path: operation.path.clone(),
577            public: operation.public,
578            idempotent: operation.idempotent,
579            headers: Vec::new(),
580        })
581        .collect::<Vec<_>>();
582    compare_descriptor_field(
583        "operations",
584        &distribution_operations,
585        &descriptor_operations,
586        diagnostics,
587    );
588    if descriptor
589        .documentation
590        .as_deref()
591        .is_some_and(|reference| reference != distribution.documentation.reference)
592    {
593        diagnostics.push(diagnostic(
594            "descriptor_documentation_mismatch",
595            "descriptor.documentation",
596            "linked descriptor documentation does not match the distribution reference",
597        ));
598    }
599    for migration in &descriptor.migrations {
600        if !distribution.migrations.contains(migration) {
601            diagnostics.push(diagnostic(
602                "descriptor_migration_missing",
603                format!("descriptor.migrations.{}", migration.id),
604                "linked migration is absent from the distribution union",
605            ));
606        }
607    }
608    for resource in &descriptor.resources {
609        if !distribution
610            .resources
611            .iter()
612            .any(|candidate| resource_matches(candidate, resource))
613        {
614            diagnostics.push(diagnostic(
615                "descriptor_resource_missing",
616                format!("descriptor.resources.{}", resource.id),
617                "linked resource is absent from the distribution union",
618            ));
619        }
620    }
621}
622
623fn compare_descriptor_field<T: PartialEq>(
624    field: &str,
625    distribution: &[T],
626    descriptor: &[T],
627    diagnostics: &mut Vec<ConformanceDiagnostic>,
628) {
629    if distribution != descriptor {
630        diagnostics.push(diagnostic(
631            format!("descriptor_{field}_mismatch"),
632            format!("descriptor.{field}"),
633            format!("linked descriptor {field} do not match the distribution record"),
634        ));
635    }
636}
637
638fn resource_matches(distribution: &DistributionResource, runtime: &ResourceIntent) -> bool {
639    distribution.id == runtime.id
640        && distribution.kind == runtime.kind
641        && distribution.idle_cost == runtime.idle_cost
642        && distribution.wake_sources == runtime.wake_sources
643        && distribution.dependencies == runtime.dependencies
644}
645
646fn validate_lifecycle(
647    target: Arc<dyn Plugin>,
648    supporting_plugins: &[Arc<dyn Plugin>],
649    configuration: Option<&serde_json::Value>,
650    diagnostics: &mut Vec<ConformanceDiagnostic>,
651) -> ConformanceStatus {
652    let target_descriptor = target.descriptor();
653    let target_id = target_descriptor.id.clone();
654    let mut manager = PluginManager::default();
655    for plugin in supporting_plugins {
656        if let Err(error) = manager.register_arc(Arc::clone(plugin)) {
657            diagnostics.push(diagnostic(
658                "supporting_plugin_registration_failed",
659                "lifecycle.registration",
660                format!("supporting plugin registration failed: {error}"),
661            ));
662            return ConformanceStatus::Failed;
663        }
664    }
665    if let Err(error) = manager.register_arc(target) {
666        diagnostics.push(diagnostic(
667            "plugin_registration_failed",
668            "lifecycle.registration",
669            format!("plugin registration failed: {error}"),
670        ));
671        return ConformanceStatus::Failed;
672    }
673    let mut selection = PluginSelection::default();
674    selection.enabled.insert(target_id.clone());
675    if let Some(configuration) = configuration {
676        selection
677            .configuration
678            .insert(target_id, configuration.clone());
679    }
680    match manager.compose(&selection) {
681        Ok(application) => {
682            let first_provenance = match serde_json::to_value(application.registration_provenance())
683            {
684                Ok(provenance) => provenance,
685                Err(error) => {
686                    diagnostics.push(diagnostic(
687                        "registration_provenance_unserializable",
688                        "lifecycle.provenance",
689                        format!("registration provenance is not serializable: {error}"),
690                    ));
691                    return ConformanceStatus::Failed;
692                }
693            };
694            let second_application = match manager.compose(&selection) {
695                Ok(application) => application,
696                Err(error) => {
697                    diagnostics.push(diagnostic(
698                        "plugin_recomposition_failed",
699                        "lifecycle.composition",
700                        format!("repeated plugin composition failed: {error}"),
701                    ));
702                    return ConformanceStatus::Failed;
703                }
704            };
705            let second_provenance =
706                match serde_json::to_value(second_application.registration_provenance()) {
707                    Ok(provenance) => provenance,
708                    Err(error) => {
709                        diagnostics.push(diagnostic(
710                            "registration_provenance_unserializable",
711                            "lifecycle.provenance",
712                            format!("registration provenance is not serializable: {error}"),
713                        ));
714                        return ConformanceStatus::Failed;
715                    }
716                };
717            if first_provenance != second_provenance {
718                diagnostics.push(diagnostic(
719                    "registration_provenance_nondeterministic",
720                    "lifecycle.provenance",
721                    "repeated composition produced different registration provenance",
722                ));
723                return ConformanceStatus::Failed;
724            }
725            if !target_descriptor.configuration.is_empty()
726                && !rejects_unknown_configuration(&manager, &selection, &target_descriptor)
727            {
728                diagnostics.push(diagnostic(
729                    "unknown_configuration_accepted",
730                    "lifecycle.configuration",
731                    "plugin composition accepted a field absent from the descriptor schema",
732                ));
733                ConformanceStatus::Failed
734            } else {
735                ConformanceStatus::Passed
736            }
737        }
738        Err(error) => {
739            diagnostics.push(diagnostic(
740                "plugin_composition_failed",
741                "lifecycle.composition",
742                format!("plugin composition failed: {error}"),
743            ));
744            ConformanceStatus::Failed
745        }
746    }
747}
748
749fn rejects_unknown_configuration(
750    manager: &PluginManager,
751    selection: &PluginSelection,
752    descriptor: &PluginDescriptor,
753) -> bool {
754    let mut probe = selection.clone();
755    let mut supplied = probe
756        .configuration
757        .get(&descriptor.id)
758        .and_then(serde_json::Value::as_object)
759        .cloned()
760        .unwrap_or_default();
761    let mut unknown = "__minco_conformance_unknown".to_owned();
762    while descriptor
763        .configuration
764        .iter()
765        .any(|field| field.key == unknown)
766    {
767        unknown.push('_');
768    }
769    supplied.insert(unknown, serde_json::Value::Bool(true));
770    probe
771        .configuration
772        .insert(descriptor.id.clone(), serde_json::Value::Object(supplied));
773    matches!(
774        manager.build_graph(&probe),
775        Err(minco_core::PluginError::UnknownConfigurationField { .. })
776    )
777}
778
779fn validate_distribution(
780    distribution: &PluginDistributionManifest,
781    diagnostics: &mut Vec<ConformanceDiagnostic>,
782) {
783    if distribution.schema != 1 {
784        diagnostics.push(diagnostic(
785            "distribution_schema_unsupported",
786            "schema",
787            format!("unsupported distribution schema {}", distribution.schema),
788        ));
789    }
790    match minco_core::CORE_API_VERSION.parse() {
791        Ok(current) if !distribution.core_compatibility.matches(&current) => {
792            diagnostics.push(diagnostic(
793                "core_compatibility_excludes_current",
794                "core_compatibility",
795                format!(
796                    "{} excludes current Minco core {}",
797                    distribution.core_compatibility,
798                    minco_core::CORE_API_VERSION
799                ),
800            ));
801        }
802        Ok(_) => {}
803        Err(error) => diagnostics.push(diagnostic(
804            "current_core_version_invalid",
805            "core_compatibility",
806            format!(
807                "Minco core version {} is invalid: {error}",
808                minco_core::CORE_API_VERSION
809            ),
810        )),
811    }
812    let expected_profile = match distribution.kind {
813        PluginDistributionKind::Plugin => PLUGIN_CONFORMANCE_PROFILE,
814        PluginDistributionKind::Adapter => ADAPTER_CONFORMANCE_PROFILE,
815        PluginDistributionKind::Runtime => RUNTIME_CONFORMANCE_PROFILE,
816    };
817    if distribution.conformance.profile != expected_profile {
818        diagnostics.push(diagnostic(
819            "conformance_profile_mismatch",
820            "conformance.profile",
821            format!(
822                "{} components require conformance profile {expected_profile}",
823                kind_name(distribution.kind)
824            ),
825        ));
826    }
827    if distribution.runtimes.is_empty() {
828        diagnostics.push(diagnostic(
829            "runtime_missing",
830            "runtimes",
831            "at least one supported runtime is required",
832        ));
833    }
834    push_duplicate_strings(
835        "runtime_duplicate",
836        "runtimes",
837        &distribution.runtimes,
838        diagnostics,
839    );
840    push_duplicate_strings(
841        "database_duplicate",
842        "databases",
843        &distribution.databases,
844        diagnostics,
845    );
846    if !distribution.documentation.reference.starts_with("https://") {
847        diagnostics.push(diagnostic(
848            "documentation_reference_insecure",
849            "documentation.reference",
850            "reference documentation must use HTTPS",
851        ));
852    }
853    if distribution.failure_policy.description.trim().is_empty() {
854        diagnostics.push(diagnostic(
855            "failure_policy_undocumented",
856            "failure_policy.description",
857            "failure policy requires a non-empty description",
858        ));
859    }
860    if distribution.conformance.evidence.is_empty()
861        || distribution
862            .conformance
863            .evidence
864            .iter()
865            .any(|item| item.trim().is_empty())
866    {
867        diagnostics.push(diagnostic(
868            "conformance_evidence_missing",
869            "conformance.evidence",
870            "at least one inert conformance evidence label is required",
871        ));
872    }
873    let mut capability_provisions = BTreeSet::new();
874    for provision in &distribution.provides {
875        if !capability_provisions.insert(provision.name.as_str()) {
876            diagnostics.push(diagnostic(
877                "capability_provision_duplicate",
878                format!("provides.{}", provision.name),
879                "provided capability names must be unique",
880            ));
881        }
882    }
883    let mut capability_requirements = BTreeSet::new();
884    for requirement in &distribution.requires {
885        if !capability_requirements.insert(requirement.name.as_str()) {
886            diagnostics.push(diagnostic(
887                "capability_requirement_duplicate",
888                format!("requires.{}", requirement.name),
889                "required capability names must be unique",
890            ));
891        }
892    }
893    let mut configuration_keys = BTreeSet::new();
894    for field in &distribution.configuration {
895        if !configuration_keys.insert(field.key.as_str()) {
896            diagnostics.push(diagnostic(
897                "configuration_key_duplicate",
898                format!("configuration.{}", field.key),
899                "configuration field keys must be unique",
900            ));
901        }
902        if field.secret && field.default.is_some() {
903            diagnostics.push(diagnostic(
904                "secret_configuration_default",
905                format!("configuration.{}.default", field.key),
906                "secret configuration fields must not publish default values",
907            ));
908        }
909        if let Some(default) = &field.default
910            && !configuration_value_matches(field.kind, default)
911        {
912            diagnostics.push(diagnostic(
913                "configuration_default_type_mismatch",
914                format!("configuration.{}.default", field.key),
915                format!("default value must match declared type {:?}", field.kind),
916            ));
917        }
918    }
919    let mut operation_ids = BTreeSet::new();
920    let mut operation_routes = BTreeSet::new();
921    for operation in &distribution.operations {
922        if !operation_ids.insert(operation.operation_id.as_str()) {
923            diagnostics.push(diagnostic(
924                "operation_id_duplicate",
925                format!("operations.{}", operation.operation_id),
926                "operation IDs must be unique",
927            ));
928        }
929        let route = (
930            operation.method.to_ascii_uppercase(),
931            operation.path.as_str(),
932        );
933        if !operation_routes.insert(route) {
934            diagnostics.push(diagnostic(
935                "operation_route_duplicate",
936                format!("operations.{}", operation.operation_id),
937                format!(
938                    "route {} {} is declared more than once",
939                    operation.method, operation.path
940                ),
941            ));
942        }
943        for header in &operation.headers {
944            if !is_http_token(header) {
945                diagnostics.push(diagnostic(
946                    "operation_header_invalid",
947                    format!("operations.{}.headers", operation.operation_id),
948                    format!("{header:?} is not a valid HTTP field name"),
949                ));
950            }
951        }
952    }
953    let databases = distribution
954        .databases
955        .iter()
956        .map(String::as_str)
957        .collect::<BTreeSet<_>>();
958    let mut migration_ids = BTreeSet::new();
959    for migration in &distribution.migrations {
960        if !migration_ids.insert(migration.id.as_str()) {
961            diagnostics.push(diagnostic(
962                "migration_id_duplicate",
963                format!("migrations.{}", migration.id),
964                "migration set IDs must be unique",
965            ));
966        }
967        if !databases.contains(migration.database.as_str()) {
968            diagnostics.push(diagnostic(
969                "migration_database_undeclared",
970                format!("migrations.{}.database", migration.id),
971                format!("database {} is not declared", migration.database),
972            ));
973        }
974    }
975    let mut seed_ids = BTreeSet::new();
976    for seed in &distribution.seeds {
977        if !seed_ids.insert(seed.id.as_str()) {
978            diagnostics.push(diagnostic(
979                "seed_id_duplicate",
980                format!("seeds.{}", seed.id),
981                "seed IDs must be unique",
982            ));
983        }
984        if !databases.contains(seed.database.as_str()) {
985            diagnostics.push(diagnostic(
986                "seed_database_undeclared",
987                format!("seeds.{}.database", seed.id),
988                format!("database {} is not declared", seed.database),
989            ));
990        }
991    }
992    let resource_ids = distribution
993        .resources
994        .iter()
995        .map(|resource| resource.id.as_str())
996        .collect::<BTreeSet<_>>();
997    if resource_ids.len() != distribution.resources.len() {
998        diagnostics.push(diagnostic(
999            "resource_id_duplicate",
1000            "resources",
1001            "resource IDs must be unique",
1002        ));
1003    }
1004    for resource in &distribution.resources {
1005        for dependency in &resource.dependencies {
1006            if !resource_ids.contains(dependency.as_str()) {
1007                diagnostics.push(diagnostic(
1008                    "resource_dependency_unknown",
1009                    format!("resources.{}.dependencies", resource.id),
1010                    format!("resource dependency {dependency} is not declared"),
1011                ));
1012            }
1013        }
1014        for action in &resource.iam_actions {
1015            if !is_iam_action(action) {
1016                diagnostics.push(diagnostic(
1017                    "resource_iam_action_invalid",
1018                    format!("resources.{}.iam_actions", resource.id),
1019                    format!("{action:?} is not a valid IAM action"),
1020                ));
1021            }
1022        }
1023        for wake_source in &resource.wake_sources {
1024            if matches!(wake_source, WakeSource::Schedule { expression } if expression.trim().is_empty())
1025            {
1026                diagnostics.push(diagnostic(
1027                    "schedule_expression_empty",
1028                    format!("resources.{}.wake_sources", resource.id),
1029                    "scheduled wake sources require a non-empty expression",
1030                ));
1031            }
1032        }
1033    }
1034    let mut health_check_ids = BTreeSet::new();
1035    for check in &distribution.health_checks {
1036        if !health_check_ids.insert(check.id.as_str()) {
1037            diagnostics.push(diagnostic(
1038                "health_check_id_duplicate",
1039                format!("health_checks.{}", check.id),
1040                "health check IDs must be unique",
1041            ));
1042        }
1043    }
1044    let mut data_classes = BTreeSet::new();
1045    for class in &distribution.data_classes {
1046        if !data_classes.insert(*class) {
1047            diagnostics.push(diagnostic(
1048                "data_class_duplicate",
1049                "data_classes",
1050                "data classes must be unique",
1051            ));
1052        }
1053    }
1054}
1055
1056fn push_duplicate_strings(
1057    code: &str,
1058    path: &str,
1059    values: &[String],
1060    diagnostics: &mut Vec<ConformanceDiagnostic>,
1061) {
1062    let mut unique = BTreeSet::new();
1063    for value in values {
1064        if !unique.insert(value.as_str()) {
1065            diagnostics.push(diagnostic(
1066                code,
1067                path,
1068                format!("{value:?} is declared more than once"),
1069            ));
1070        }
1071    }
1072}
1073
1074fn configuration_value_matches(kind: ConfigurationValueKind, value: &serde_json::Value) -> bool {
1075    match kind {
1076        ConfigurationValueKind::String => value.is_string(),
1077        ConfigurationValueKind::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
1078        ConfigurationValueKind::Number => value.is_number(),
1079        ConfigurationValueKind::Boolean => value.is_boolean(),
1080        ConfigurationValueKind::StringList => value
1081            .as_array()
1082            .is_some_and(|values| values.iter().all(serde_json::Value::is_string)),
1083        ConfigurationValueKind::Object => value.is_object(),
1084    }
1085}
1086
1087fn is_http_token(value: &str) -> bool {
1088    !value.is_empty()
1089        && value.bytes().all(|byte| {
1090            byte.is_ascii_alphanumeric()
1091                || matches!(
1092                    byte,
1093                    b'!' | b'#'
1094                        | b'$'
1095                        | b'%'
1096                        | b'&'
1097                        | b'\''
1098                        | b'*'
1099                        | b'+'
1100                        | b'-'
1101                        | b'.'
1102                        | b'^'
1103                        | b'_'
1104                        | b'`'
1105                        | b'|'
1106                        | b'~'
1107                )
1108        })
1109}
1110
1111fn is_iam_action(value: &str) -> bool {
1112    let Some((service, action)) = value.split_once(':') else {
1113        return false;
1114    };
1115    !service.is_empty()
1116        && !action.is_empty()
1117        && !action.contains(':')
1118        && service
1119            .bytes()
1120            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
1121        && action
1122            .bytes()
1123            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'*' | b'-' | b'_'))
1124}
1125
1126const fn kind_name(kind: PluginDistributionKind) -> &'static str {
1127    match kind {
1128        PluginDistributionKind::Plugin => "plugin",
1129        PluginDistributionKind::Adapter => "adapter",
1130        PluginDistributionKind::Runtime => "runtime",
1131    }
1132}
1133
1134fn diagnostic(
1135    code: impl Into<String>,
1136    path: impl Into<String>,
1137    message: impl Into<String>,
1138) -> ConformanceDiagnostic {
1139    ConformanceDiagnostic {
1140        code: code.into(),
1141        path: path.into(),
1142        message: message.into(),
1143    }
1144}
1145
1146fn report(
1147    profile: &str,
1148    plugin_id: &str,
1149    diagnostics: Vec<ConformanceDiagnostic>,
1150) -> PluginConformanceReport {
1151    let plugin_contract = status_for(&diagnostics);
1152    report_with_assurance(
1153        profile,
1154        plugin_id,
1155        diagnostics,
1156        plugin_contract,
1157        ConformanceStatus::NotAssessed,
1158    )
1159}
1160
1161fn report_with_assurance(
1162    profile: &str,
1163    plugin_id: &str,
1164    mut diagnostics: Vec<ConformanceDiagnostic>,
1165    plugin_contract: ConformanceStatus,
1166    plugin_lifecycle: ConformanceStatus,
1167) -> PluginConformanceReport {
1168    diagnostics.sort_by(|left, right| {
1169        (&left.code, &left.path, &left.message).cmp(&(&right.code, &right.path, &right.message))
1170    });
1171    let status = if plugin_contract == ConformanceStatus::Failed
1172        || plugin_lifecycle == ConformanceStatus::Failed
1173    {
1174        ConformanceStatus::Failed
1175    } else {
1176        ConformanceStatus::Passed
1177    };
1178    PluginConformanceReport {
1179        profile: profile.to_owned(),
1180        plugin_id: plugin_id.to_owned(),
1181        status,
1182        assurance: ConformanceAssurance {
1183            plugin_contract,
1184            plugin_lifecycle,
1185            application_readiness: ConformanceStatus::NotAssessed,
1186            provider_live: ConformanceStatus::NotRun,
1187            production_readiness: ConformanceStatus::NotAssessed,
1188        },
1189        diagnostics,
1190    }
1191}
1192
1193const fn status_for(diagnostics: &[ConformanceDiagnostic]) -> ConformanceStatus {
1194    if diagnostics.is_empty() {
1195        ConformanceStatus::Passed
1196    } else {
1197        ConformanceStatus::Failed
1198    }
1199}