Skip to main content

riffdb_application/
adapter.rs

1use std::error::Error;
2use std::fmt;
3
4use riffdb_types::{
5    AdapterConformanceManifestHash, ApplicationLockHash, ApplicationManifestHash,
6    ApplicationRoleHash, ContractBundleHash, ContractLineage, ContractVersion,
7    GeneratedArtifactHash, MAX_ADAPTER_PROBE_ITEMS, MigrationBundleHash,
8    hash_adapter_conformance_manifest,
9};
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    ApplicationInstallationPlan, InstallationArtifact, InstallationArtifactKind,
14    InstallationContract, InstallationDriver, InstallationFeature, InstallationSymbol,
15    RoleOperation, RoleOperationKind,
16};
17
18/// Canonical schema for adapter-owned conformance manifests.
19pub const ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V1: &str = "riffdb.adapter-conformance-manifest/v1";
20/// Current schema requiring an explicit disposition for every closed alpha feature.
21pub const ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2: &str = "riffdb.adapter-conformance-manifest/v2";
22/// Maximum canonical manifest size.
23pub const MAX_ADAPTER_CONFORMANCE_MANIFEST_BYTES: usize = 1_048_576;
24/// Maximum feature claims in one manifest.
25pub const MAX_ADAPTER_FEATURE_CLAIMS: usize = 32;
26/// Maximum exact artifacts in one manifest.
27pub const MAX_ADAPTER_ARTIFACTS: usize = 256;
28/// Maximum exact roles in one manifest.
29pub const MAX_ADAPTER_ROLES: usize = 128;
30/// Maximum driver/runtime requirements in one manifest.
31pub const MAX_ADAPTER_DRIVERS: usize = 16;
32/// Maximum supported platforms per driver requirement.
33pub const MAX_ADAPTER_PLATFORMS: usize = 16;
34/// Maximum conformance probes in one manifest.
35pub const MAX_ADAPTER_CONFORMANCE_PROBES: usize = 256;
36/// Maximum evolution cases in one manifest.
37pub const MAX_ADAPTER_EVOLUTIONS: usize = 32;
38
39/// Stable, value-free adapter manifest failure class.
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum AdapterConformanceErrorKind {
42    /// A fixed byte or collection ceiling was exceeded.
43    LimitExceeded,
44    /// JSON or a closed registry value was invalid.
45    InvalidEncoding,
46    /// The manifest schema is well formed but unsupported by this release.
47    UnsupportedVersion,
48    /// Valid bytes were not the one canonical encoding.
49    NonCanonical,
50    /// A required shape or disposition was inconsistent.
51    InvalidShape,
52    /// A symbolic or exact identity was duplicated.
53    Duplicate,
54    /// Manifest and application-plan identities disagree.
55    IdentityMismatch,
56    /// A required closed product feature is unavailable.
57    UnsupportedFeature,
58    /// A required first-party driver identity is unavailable.
59    UnsupportedDriver,
60}
61
62/// Bounded adapter manifest error without paths, values, or engine prose.
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub struct AdapterConformanceError {
65    kind: AdapterConformanceErrorKind,
66}
67
68impl AdapterConformanceError {
69    const fn new(kind: AdapterConformanceErrorKind) -> Self {
70        Self { kind }
71    }
72
73    /// Stable failure class.
74    #[must_use]
75    pub const fn kind(self) -> AdapterConformanceErrorKind {
76        self.kind
77    }
78}
79
80impl fmt::Display for AdapterConformanceError {
81    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82        formatter.write_str(match self.kind {
83            AdapterConformanceErrorKind::LimitExceeded => {
84                "adapter conformance manifest exceeds a fixed limit"
85            }
86            AdapterConformanceErrorKind::InvalidEncoding => {
87                "adapter conformance manifest encoding is invalid"
88            }
89            AdapterConformanceErrorKind::UnsupportedVersion => {
90                "adapter conformance manifest version is unsupported"
91            }
92            AdapterConformanceErrorKind::NonCanonical => {
93                "adapter conformance manifest encoding is not canonical"
94            }
95            AdapterConformanceErrorKind::InvalidShape => {
96                "adapter conformance manifest shape is invalid"
97            }
98            AdapterConformanceErrorKind::Duplicate => {
99                "adapter conformance manifest contains a duplicate identity"
100            }
101            AdapterConformanceErrorKind::IdentityMismatch => {
102                "adapter conformance manifest identity does not match the application plan"
103            }
104            AdapterConformanceErrorKind::UnsupportedFeature => {
105                "adapter requires an unavailable RiffDB feature"
106            }
107            AdapterConformanceErrorKind::UnsupportedDriver => {
108                "adapter requires an unavailable driver or platform"
109            }
110        })
111    }
112}
113
114impl Error for AdapterConformanceError {}
115
116/// Explicit support posture for one closed RiffDB feature.
117#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
118pub enum AdapterFeatureDisposition {
119    /// Installation must stop when the feature is unavailable.
120    Required,
121    /// The adapter does not require or emulate this feature.
122    Optional,
123    /// The feature is used with a declared bounded limitation.
124    Degraded,
125    /// The feature is knowingly unavailable and is not emulated.
126    Unavailable,
127}
128
129impl AdapterFeatureDisposition {
130    const fn tag(self) -> &'static str {
131        match self {
132            Self::Required => "required",
133            Self::Optional => "optional",
134            Self::Degraded => "degraded",
135            Self::Unavailable => "unavailable",
136        }
137    }
138
139    fn parse(value: &str) -> Option<Self> {
140        Some(match value {
141            "required" => Self::Required,
142            "optional" => Self::Optional,
143            "degraded" => Self::Degraded,
144            "unavailable" => Self::Unavailable,
145            _ => return None,
146        })
147    }
148}
149
150/// Closed reason attached only to degraded or unavailable claims.
151#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
152pub enum AdapterLimitation {
153    /// The product feature is not implemented in this exact release.
154    ProductUnavailable,
155    /// The exact released driver platform matrix is narrower than requested.
156    PlatformLimited,
157    /// The supported operation exists with a documented bounded performance limit.
158    PerformanceLimited,
159}
160
161impl AdapterLimitation {
162    const fn tag(self) -> &'static str {
163        match self {
164            Self::ProductUnavailable => "product_unavailable",
165            Self::PlatformLimited => "platform_limited",
166            Self::PerformanceLimited => "performance_limited",
167        }
168    }
169
170    fn parse(value: &str) -> Option<Self> {
171        Some(match value {
172            "product_unavailable" => Self::ProductUnavailable,
173            "platform_limited" => Self::PlatformLimited,
174            "performance_limited" => Self::PerformanceLimited,
175            _ => return None,
176        })
177    }
178}
179
180/// One explicit feature claim. There is no fallback or emulation field.
181#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
182pub struct AdapterFeatureClaim {
183    feature: InstallationFeature,
184    disposition: AdapterFeatureDisposition,
185    limitation: Option<AdapterLimitation>,
186}
187
188impl AdapterFeatureClaim {
189    /// Validates one closed support claim.
190    pub fn new(
191        feature: InstallationFeature,
192        disposition: AdapterFeatureDisposition,
193        limitation: Option<AdapterLimitation>,
194    ) -> Result<Self, AdapterConformanceError> {
195        if matches!(
196            disposition,
197            AdapterFeatureDisposition::Degraded | AdapterFeatureDisposition::Unavailable
198        ) != limitation.is_some()
199        {
200            return Err(AdapterConformanceError::new(
201                AdapterConformanceErrorKind::InvalidShape,
202            ));
203        }
204        Ok(Self {
205            feature,
206            disposition,
207            limitation,
208        })
209    }
210
211    /// Required claim with no silent fallback.
212    #[must_use]
213    pub const fn required(feature: InstallationFeature) -> Self {
214        Self {
215            feature,
216            disposition: AdapterFeatureDisposition::Required,
217            limitation: None,
218        }
219    }
220
221    /// Explicit unavailable claim with a closed reason.
222    #[must_use]
223    pub const fn unavailable(feature: InstallationFeature, limitation: AdapterLimitation) -> Self {
224        Self {
225            feature,
226            disposition: AdapterFeatureDisposition::Unavailable,
227            limitation: Some(limitation),
228        }
229    }
230
231    /// Closed feature identity.
232    #[must_use]
233    pub const fn feature(self) -> InstallationFeature {
234        self.feature
235    }
236
237    /// Explicit support disposition.
238    #[must_use]
239    pub const fn disposition(self) -> AdapterFeatureDisposition {
240        self.disposition
241    }
242
243    /// Closed limitation for degraded or unavailable support.
244    #[must_use]
245    pub const fn limitation(self) -> Option<AdapterLimitation> {
246        self.limitation
247    }
248}
249
250/// Closed platform triples supported by the alpha driver matrix.
251#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
252pub enum AdapterPlatform {
253    /// GNU Linux on x86-64.
254    LinuxX86_64Gnu,
255    /// musl Linux on x86-64.
256    LinuxX86_64Musl,
257    /// GNU Linux on AArch64.
258    LinuxAarch64Gnu,
259    /// macOS on x86-64.
260    MacosX86_64,
261    /// macOS on Apple silicon.
262    MacosAarch64,
263    /// Windows on x86-64.
264    WindowsX86_64,
265}
266
267impl AdapterPlatform {
268    const fn tag(self) -> &'static str {
269        match self {
270            Self::LinuxX86_64Gnu => "linux-x86_64-gnu",
271            Self::LinuxX86_64Musl => "linux-x86_64-musl",
272            Self::LinuxAarch64Gnu => "linux-aarch64-gnu",
273            Self::MacosX86_64 => "macos-x86_64",
274            Self::MacosAarch64 => "macos-aarch64",
275            Self::WindowsX86_64 => "windows-x86_64",
276        }
277    }
278
279    fn parse(value: &str) -> Option<Self> {
280        Some(match value {
281            "linux-x86_64-gnu" => Self::LinuxX86_64Gnu,
282            "linux-x86_64-musl" => Self::LinuxX86_64Musl,
283            "linux-aarch64-gnu" => Self::LinuxAarch64Gnu,
284            "macos-x86_64" => Self::MacosX86_64,
285            "macos-aarch64" => Self::MacosAarch64,
286            "windows-x86_64" => Self::WindowsX86_64,
287            _ => return None,
288        })
289    }
290}
291
292/// One exact first-party driver/runtime/platform claim.
293#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
294pub struct AdapterDriverRequirement {
295    driver: InstallationDriver,
296    runtime_version: InstallationSymbol,
297    platforms: Vec<AdapterPlatform>,
298    conformance_hash: GeneratedArtifactHash,
299}
300
301impl AdapterDriverRequirement {
302    /// Creates one exact driver requirement; ranges and empty platforms are impossible.
303    pub fn new(
304        driver: InstallationDriver,
305        runtime_version: InstallationSymbol,
306        mut platforms: Vec<AdapterPlatform>,
307        conformance_hash: GeneratedArtifactHash,
308    ) -> Result<Self, AdapterConformanceError> {
309        let runtime = runtime_version.as_str();
310        if !runtime.bytes().any(|byte| byte.is_ascii_digit())
311            || runtime.contains("..")
312            || matches!(runtime, "latest" | "current" | "stable" | "any")
313        {
314            return Err(AdapterConformanceError::new(
315                AdapterConformanceErrorKind::InvalidShape,
316            ));
317        }
318        platforms.sort();
319        if platforms.is_empty()
320            || platforms.len() > MAX_ADAPTER_PLATFORMS
321            || platforms.windows(2).any(|pair| pair[0] == pair[1])
322        {
323            return Err(AdapterConformanceError::new(if platforms.is_empty() {
324                AdapterConformanceErrorKind::InvalidShape
325            } else if platforms.len() > MAX_ADAPTER_PLATFORMS {
326                AdapterConformanceErrorKind::LimitExceeded
327            } else {
328                AdapterConformanceErrorKind::Duplicate
329            }));
330        }
331        Ok(Self {
332            driver,
333            runtime_version,
334            platforms,
335            conformance_hash,
336        })
337    }
338
339    /// First-party transport owner selected by this requirement.
340    #[must_use]
341    pub const fn driver(&self) -> InstallationDriver {
342        self.driver
343    }
344
345    /// Exact runtime/toolchain version symbol, never a version range.
346    #[must_use]
347    pub const fn runtime_version(&self) -> &InstallationSymbol {
348        &self.runtime_version
349    }
350
351    /// Closed supported platforms.
352    #[must_use]
353    pub fn platforms(&self) -> &[AdapterPlatform] {
354        &self.platforms
355    }
356
357    /// Exact golden driver-conformance observation identity.
358    #[must_use]
359    pub const fn conformance_hash(&self) -> GeneratedArtifactHash {
360        self.conformance_hash
361    }
362}
363
364/// Exact symbolic role required by an adapter.
365#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
366pub struct AdapterRoleRequirement {
367    name: InstallationSymbol,
368    role_hash: ApplicationRoleHash,
369    operations: Vec<RoleOperation>,
370}
371
372impl AdapterRoleRequirement {
373    /// Creates one exact, nonempty, name-only role surface.
374    pub fn new(
375        name: InstallationSymbol,
376        role_hash: ApplicationRoleHash,
377        mut operations: Vec<RoleOperation>,
378    ) -> Result<Self, AdapterConformanceError> {
379        operations.sort();
380        if operations.is_empty()
381            || operations.len() > crate::MAX_ROLE_OPERATIONS
382            || operations.windows(2).any(|pair| pair[0] == pair[1])
383        {
384            return Err(AdapterConformanceError::new(if operations.is_empty() {
385                AdapterConformanceErrorKind::InvalidShape
386            } else if operations.len() > crate::MAX_ROLE_OPERATIONS {
387                AdapterConformanceErrorKind::LimitExceeded
388            } else {
389                AdapterConformanceErrorKind::Duplicate
390            }));
391        }
392        Ok(Self {
393            name,
394            role_hash,
395            operations,
396        })
397    }
398
399    /// Symbolic role name.
400    #[must_use]
401    pub const fn name(&self) -> &InstallationSymbol {
402        &self.name
403    }
404
405    /// Exact compiled role identity.
406    #[must_use]
407    pub const fn role_hash(&self) -> ApplicationRoleHash {
408        self.role_hash
409    }
410
411    /// Closed named application operations.
412    #[must_use]
413    pub fn operations(&self) -> &[RoleOperation] {
414        &self.operations
415    }
416}
417
418/// One public operation and bounded golden observation.
419#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
420pub struct AdapterConformanceProbe {
421    name: InstallationSymbol,
422    role: InstallationSymbol,
423    operation: RoleOperation,
424    expected_observation_hash: GeneratedArtifactHash,
425    maximum_items: u32,
426}
427
428impl AdapterConformanceProbe {
429    /// Creates one bounded named-operation probe. It cannot carry code or a method path.
430    pub fn new(
431        name: InstallationSymbol,
432        role: InstallationSymbol,
433        operation: RoleOperation,
434        expected_observation_hash: GeneratedArtifactHash,
435        maximum_items: u32,
436    ) -> Result<Self, AdapterConformanceError> {
437        if maximum_items == 0 || maximum_items > MAX_ADAPTER_PROBE_ITEMS {
438            return Err(AdapterConformanceError::new(
439                AdapterConformanceErrorKind::LimitExceeded,
440            ));
441        }
442        Ok(Self {
443            name,
444            role,
445            operation,
446            expected_observation_hash,
447            maximum_items,
448        })
449    }
450
451    /// Stable probe name.
452    #[must_use]
453    pub const fn name(&self) -> &InstallationSymbol {
454        &self.name
455    }
456
457    /// Exact least-authority role used by the probe.
458    #[must_use]
459    pub const fn role(&self) -> &InstallationSymbol {
460        &self.role
461    }
462
463    /// Public symbolic operation invoked by the probe.
464    #[must_use]
465    pub const fn operation(&self) -> &RoleOperation {
466        &self.operation
467    }
468
469    /// Exact value-free golden observation identity.
470    #[must_use]
471    pub const fn expected_observation_hash(&self) -> GeneratedArtifactHash {
472        self.expected_observation_hash
473    }
474
475    /// Maximum number of returned or mutated items accepted by the probe.
476    #[must_use]
477    pub const fn maximum_items(&self) -> u32 {
478        self.maximum_items
479    }
480}
481
482/// Closed application evolution class. No open version range exists.
483#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
484pub enum AdapterEvolutionClass {
485    /// Install into an empty selected database.
486    EmptyInstall,
487    /// Upgrade one exact predecessor through ordinary compatible deployment.
488    CompatibleUpgrade,
489    /// Upgrade one exact predecessor through one exact reviewed migration.
490    ExplicitMigration,
491}
492
493impl AdapterEvolutionClass {
494    const fn tag(self) -> &'static str {
495        match self {
496            Self::EmptyInstall => "empty_install",
497            Self::CompatibleUpgrade => "compatible_upgrade",
498            Self::ExplicitMigration => "explicit_migration",
499        }
500    }
501
502    fn parse(value: &str) -> Option<Self> {
503        Some(match value {
504            "empty_install" => Self::EmptyInstall,
505            "compatible_upgrade" => Self::CompatibleUpgrade,
506            "explicit_migration" => Self::ExplicitMigration,
507            _ => return None,
508        })
509    }
510}
511
512/// One exact install/upgrade case and its golden observation identity.
513#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
514pub struct AdapterEvolutionRequirement {
515    class: AdapterEvolutionClass,
516    predecessor: Option<InstallationContract>,
517    successor: InstallationContract,
518    migration_hash: Option<MigrationBundleHash>,
519    expected_observation_hash: GeneratedArtifactHash,
520}
521
522impl AdapterEvolutionRequirement {
523    /// Creates a closed exact evolution case.
524    pub fn new(
525        class: AdapterEvolutionClass,
526        predecessor: Option<InstallationContract>,
527        successor: InstallationContract,
528        migration_hash: Option<MigrationBundleHash>,
529        expected_observation_hash: GeneratedArtifactHash,
530    ) -> Result<Self, AdapterConformanceError> {
531        let shape_valid = match class {
532            AdapterEvolutionClass::EmptyInstall => {
533                predecessor.is_none() && migration_hash.is_none()
534            }
535            AdapterEvolutionClass::CompatibleUpgrade => {
536                predecessor.is_some_and(|value| {
537                    value.version() < successor.version()
538                        || (value.version() == successor.version()
539                            && value.bundle_hash() == successor.bundle_hash())
540                }) && migration_hash.is_none()
541            }
542            AdapterEvolutionClass::ExplicitMigration => {
543                predecessor.is_some_and(|value| value.version() < successor.version())
544                    && migration_hash.is_some()
545            }
546        };
547        if !shape_valid {
548            return Err(AdapterConformanceError::new(
549                AdapterConformanceErrorKind::InvalidShape,
550            ));
551        }
552        Ok(Self {
553            class,
554            predecessor,
555            successor,
556            migration_hash,
557            expected_observation_hash,
558        })
559    }
560
561    /// Exact successor contract installed by this case.
562    #[must_use]
563    pub const fn successor(&self) -> InstallationContract {
564        self.successor
565    }
566
567    /// Closed evolution class.
568    #[must_use]
569    pub const fn class(&self) -> AdapterEvolutionClass {
570        self.class
571    }
572
573    /// Exact predecessor, absent only for empty installation.
574    #[must_use]
575    pub const fn predecessor(&self) -> Option<InstallationContract> {
576        self.predecessor
577    }
578
579    /// Exact migration identity for migration-required evolution.
580    #[must_use]
581    pub const fn migration_hash(&self) -> Option<MigrationBundleHash> {
582        self.migration_hash
583    }
584
585    /// Exact golden evolution observation identity.
586    #[must_use]
587    pub const fn expected_observation_hash(&self) -> GeneratedArtifactHash {
588        self.expected_observation_hash
589    }
590}
591
592/// Complete bounded adapter-owned input.
593#[derive(Clone, Debug, Eq, PartialEq)]
594pub struct AdapterConformanceManifestInput {
595    /// Stable symbolic adapter name.
596    pub adapter: InstallationSymbol,
597    /// Exact adapter version; ranges are not representable.
598    pub adapter_version: InstallationSymbol,
599    /// Exact generated application manifest identity.
600    pub application_manifest_hash: ApplicationManifestHash,
601    /// Exact compiler-owned application lock identity.
602    pub application_lock_hash: ApplicationLockHash,
603    /// Exact application lineage.
604    pub contract_lineage: ContractLineage,
605    /// Closed feature claims, including explicit unavailable/degraded states.
606    pub feature_claims: Vec<AdapterFeatureClaim>,
607    /// Exact required generated artifacts.
608    pub artifacts: Vec<InstallationArtifact>,
609    /// Exact least-authority symbolic roles.
610    pub roles: Vec<AdapterRoleRequirement>,
611    /// Exact first-party driver/runtime/platform claims.
612    pub drivers: Vec<AdapterDriverRequirement>,
613    /// Bounded public named-operation probes and golden observations.
614    pub conformance: Vec<AdapterConformanceProbe>,
615    /// Exact empty-install and evolution cases.
616    pub evolution: Vec<AdapterEvolutionRequirement>,
617}
618
619/// One immutable content-addressed adapter conformance manifest.
620#[derive(Clone, Eq, PartialEq)]
621pub struct AdapterConformanceManifest {
622    schema: &'static str,
623    input: AdapterConformanceManifestInput,
624    identity: AdapterConformanceManifestHash,
625    canonical_bytes: Vec<u8>,
626}
627
628impl AdapterConformanceManifest {
629    /// Compiles one canonical manifest without filesystem, server, or application execution.
630    pub fn compile(
631        input: AdapterConformanceManifestInput,
632    ) -> Result<Self, AdapterConformanceError> {
633        Self::compile_version(ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V1, input, false)
634    }
635
636    /// Compiles the current manifest format with an exhaustive feature disposition catalog.
637    pub fn compile_v2(
638        input: AdapterConformanceManifestInput,
639    ) -> Result<Self, AdapterConformanceError> {
640        Self::compile_version(ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2, input, true)
641    }
642
643    fn compile_version(
644        schema: &'static str,
645        mut input: AdapterConformanceManifestInput,
646        exhaustive_features: bool,
647    ) -> Result<Self, AdapterConformanceError> {
648        validate_and_sort(&mut input, exhaustive_features)?;
649        let dto = ManifestDto::from_input(schema, &input);
650        let mut canonical_bytes = serde_json::to_vec(&dto).map_err(|_| {
651            AdapterConformanceError::new(AdapterConformanceErrorKind::InvalidEncoding)
652        })?;
653        canonical_bytes.push(b'\n');
654        if canonical_bytes.len() > MAX_ADAPTER_CONFORMANCE_MANIFEST_BYTES {
655            return Err(AdapterConformanceError::new(
656                AdapterConformanceErrorKind::LimitExceeded,
657            ));
658        }
659        let identity = hash_adapter_conformance_manifest(&canonical_bytes);
660        Ok(Self {
661            schema,
662            input,
663            identity,
664            canonical_bytes,
665        })
666    }
667
668    /// Strictly decodes and revalidates one canonical manifest.
669    pub fn decode_canonical(bytes: &[u8]) -> Result<Self, AdapterConformanceError> {
670        if bytes.is_empty() || bytes.len() > MAX_ADAPTER_CONFORMANCE_MANIFEST_BYTES {
671            return Err(AdapterConformanceError::new(
672                AdapterConformanceErrorKind::LimitExceeded,
673            ));
674        }
675        let dto: ManifestDto = serde_json::from_slice(bytes).map_err(|_| {
676            AdapterConformanceError::new(AdapterConformanceErrorKind::InvalidEncoding)
677        })?;
678        let schema = match dto.schema.as_str() {
679            ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V1 => ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V1,
680            ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2 => ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2,
681            _ => {
682                return Err(AdapterConformanceError::new(
683                    AdapterConformanceErrorKind::UnsupportedVersion,
684                ));
685            }
686        };
687        let input = dto.into_input()?;
688        let manifest = Self::compile_version(
689            schema,
690            input,
691            schema == ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2,
692        )?;
693        if manifest.canonical_bytes != bytes {
694            return Err(AdapterConformanceError::new(
695                AdapterConformanceErrorKind::NonCanonical,
696            ));
697        }
698        Ok(manifest)
699    }
700
701    /// Content identity recorded in installation plans and receipts.
702    #[must_use]
703    pub const fn identity(&self) -> AdapterConformanceManifestHash {
704        self.identity
705    }
706
707    /// Exact accepted compatibility schema.
708    #[must_use]
709    pub const fn schema(&self) -> &'static str {
710        self.schema
711    }
712
713    /// Validated manifest contents.
714    #[must_use]
715    pub const fn input(&self) -> &AdapterConformanceManifestInput {
716        &self.input
717    }
718
719    /// Canonical compatibility bytes.
720    #[must_use]
721    pub fn canonical_bytes(&self) -> &[u8] {
722        &self.canonical_bytes
723    }
724
725    /// Verifies one installation plan is exactly the adapter-reviewed plan.
726    pub fn validate_installation_plan(
727        &self,
728        plan: &ApplicationInstallationPlan,
729    ) -> Result<(), AdapterConformanceError> {
730        let planned = plan.input();
731        if planned.adapter_manifest_hash != Some(self.identity)
732            || planned.manifest_hash != self.input.application_manifest_hash
733            || planned.lock_hash != self.input.application_lock_hash
734            || planned.target.lineage() != &self.input.contract_lineage
735            || self
736                .input
737                .evolution
738                .iter()
739                .any(|evolution| evolution.successor != planned.contract)
740        {
741            return Err(AdapterConformanceError::new(
742                AdapterConformanceErrorKind::IdentityMismatch,
743            ));
744        }
745        let self_artifact_hash = GeneratedArtifactHash::from_bytes(*self.identity.as_bytes());
746        if !planned.artifacts.iter().any(|artifact| {
747            artifact.kind() == InstallationArtifactKind::AdapterManifest
748                && artifact.name() == &self.input.adapter
749                && artifact.content_hash() == self_artifact_hash
750        }) || self
751            .input
752            .artifacts
753            .iter()
754            .any(|required| planned.artifacts.binary_search(required).is_err())
755        {
756            return Err(AdapterConformanceError::new(
757                AdapterConformanceErrorKind::IdentityMismatch,
758            ));
759        }
760        for required in &self.input.roles {
761            let Some(role) = planned
762                .roles
763                .iter()
764                .find(|role| role.name() == required.name())
765            else {
766                return Err(AdapterConformanceError::new(
767                    AdapterConformanceErrorKind::IdentityMismatch,
768                ));
769            };
770            if role.role_hash() != required.role_hash
771                || role.desired_operations() != required.operations
772            {
773                return Err(AdapterConformanceError::new(
774                    AdapterConformanceErrorKind::IdentityMismatch,
775                ));
776            }
777        }
778        if self
779            .input
780            .drivers
781            .iter()
782            .any(|required| planned.drivers.binary_search(&required.driver).is_err())
783            || self.input.feature_claims.iter().any(|claim| {
784                claim.disposition == AdapterFeatureDisposition::Required
785                    && planned
786                        .required_features
787                        .binary_search(&claim.feature)
788                        .is_err()
789            })
790        {
791            return Err(AdapterConformanceError::new(
792                AdapterConformanceErrorKind::IdentityMismatch,
793            ));
794        }
795        Ok(())
796    }
797
798    /// Verifies required features and exact driver conformance identities against a closed catalog.
799    pub fn validate_catalog(
800        &self,
801        supported_features: &[InstallationFeature],
802        supported_drivers: &[AdapterDriverRequirement],
803    ) -> Result<(), AdapterConformanceError> {
804        if self.input.feature_claims.iter().any(|claim| {
805            claim.disposition == AdapterFeatureDisposition::Required
806                && !supported_features.contains(&claim.feature)
807        }) {
808            return Err(AdapterConformanceError::new(
809                AdapterConformanceErrorKind::UnsupportedFeature,
810            ));
811        }
812        if self
813            .input
814            .drivers
815            .iter()
816            .any(|required| !supported_drivers.contains(required))
817        {
818            return Err(AdapterConformanceError::new(
819                AdapterConformanceErrorKind::UnsupportedDriver,
820            ));
821        }
822        Ok(())
823    }
824}
825
826impl fmt::Debug for AdapterConformanceManifest {
827    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
828        formatter
829            .debug_struct("AdapterConformanceManifest")
830            .field("adapter", &self.input.adapter)
831            .field("adapter_version", &self.input.adapter_version)
832            .field("identity", &self.identity)
833            .finish_non_exhaustive()
834    }
835}
836
837fn validate_and_sort(
838    input: &mut AdapterConformanceManifestInput,
839    exhaustive_features: bool,
840) -> Result<(), AdapterConformanceError> {
841    if input.feature_claims.len() > MAX_ADAPTER_FEATURE_CLAIMS
842        || input.artifacts.is_empty()
843        || input.artifacts.len() > MAX_ADAPTER_ARTIFACTS
844        || input.roles.is_empty()
845        || input.roles.len() > MAX_ADAPTER_ROLES
846        || input.drivers.is_empty()
847        || input.drivers.len() > MAX_ADAPTER_DRIVERS
848        || input.conformance.is_empty()
849        || input.conformance.len() > MAX_ADAPTER_CONFORMANCE_PROBES
850        || input.evolution.is_empty()
851        || input.evolution.len() > MAX_ADAPTER_EVOLUTIONS
852    {
853        return Err(AdapterConformanceError::new(
854            AdapterConformanceErrorKind::LimitExceeded,
855        ));
856    }
857    if exhaustive_features
858        && input.feature_claims.len() != InstallationFeature::ADAPTER_CONFORMANCE.len()
859    {
860        return Err(AdapterConformanceError::new(
861            AdapterConformanceErrorKind::InvalidShape,
862        ));
863    }
864    input.feature_claims.sort();
865    reject_duplicate(
866        input
867            .feature_claims
868            .windows(2)
869            .any(|pair| pair[0].feature == pair[1].feature),
870    )?;
871    if exhaustive_features
872        && input
873            .feature_claims
874            .iter()
875            .map(|claim| claim.feature)
876            .ne(InstallationFeature::ADAPTER_CONFORMANCE)
877    {
878        return Err(AdapterConformanceError::new(
879            AdapterConformanceErrorKind::InvalidShape,
880        ));
881    }
882    if !input.feature_claims.iter().any(|claim| {
883        claim.feature == InstallationFeature::InstallationCampaigns
884            && claim.disposition == AdapterFeatureDisposition::Required
885    }) {
886        return Err(AdapterConformanceError::new(
887            AdapterConformanceErrorKind::InvalidShape,
888        ));
889    }
890    input.artifacts.sort();
891    reject_duplicate(
892        input
893            .artifacts
894            .windows(2)
895            .any(|pair| (pair[0].kind(), pair[0].name()) == (pair[1].kind(), pair[1].name())),
896    )?;
897    input.roles.sort();
898    reject_duplicate(
899        input
900            .roles
901            .windows(2)
902            .any(|pair| pair[0].name == pair[1].name),
903    )?;
904    input.drivers.sort();
905    reject_duplicate(
906        input
907            .drivers
908            .windows(2)
909            .any(|pair| pair[0].driver == pair[1].driver),
910    )?;
911    input.conformance.sort();
912    reject_duplicate(
913        input
914            .conformance
915            .windows(2)
916            .any(|pair| pair[0].name == pair[1].name),
917    )?;
918    for probe in &input.conformance {
919        let Some(role) = input.roles.iter().find(|role| role.name == probe.role) else {
920            return Err(AdapterConformanceError::new(
921                AdapterConformanceErrorKind::IdentityMismatch,
922            ));
923        };
924        if role.operations.binary_search(&probe.operation).is_err() {
925            return Err(AdapterConformanceError::new(
926                AdapterConformanceErrorKind::IdentityMismatch,
927            ));
928        }
929    }
930    input.evolution.sort();
931    reject_duplicate(input.evolution.windows(2).any(|pair| pair[0] == pair[1]))?;
932    let genesis = input
933        .evolution
934        .iter()
935        .filter(|item| item.class == AdapterEvolutionClass::EmptyInstall)
936        .collect::<Vec<_>>();
937    if genesis.len() != 1
938        || input
939            .evolution
940            .iter()
941            .any(|item| item.successor != genesis[0].successor)
942    {
943        return Err(AdapterConformanceError::new(
944            AdapterConformanceErrorKind::InvalidShape,
945        ));
946    }
947    Ok(())
948}
949
950fn reject_duplicate(value: bool) -> Result<(), AdapterConformanceError> {
951    if value {
952        Err(AdapterConformanceError::new(
953            AdapterConformanceErrorKind::Duplicate,
954        ))
955    } else {
956        Ok(())
957    }
958}
959
960#[derive(Serialize, Deserialize)]
961#[serde(deny_unknown_fields)]
962struct ManifestDto {
963    schema: String,
964    adapter: String,
965    adapter_version: String,
966    application_manifest_hash: String,
967    application_lock_hash: String,
968    contract_lineage: String,
969    feature_claims: Vec<FeatureClaimDto>,
970    artifacts: Vec<ArtifactDto>,
971    roles: Vec<RoleDto>,
972    drivers: Vec<DriverDto>,
973    conformance: Vec<ProbeDto>,
974    evolution: Vec<EvolutionDto>,
975}
976
977#[derive(Serialize, Deserialize)]
978#[serde(deny_unknown_fields)]
979struct FeatureClaimDto {
980    feature: String,
981    disposition: String,
982    limitation: Option<String>,
983}
984
985#[derive(Serialize, Deserialize)]
986#[serde(deny_unknown_fields)]
987struct ArtifactDto {
988    kind: String,
989    name: String,
990    content_hash: String,
991}
992
993#[derive(Serialize, Deserialize)]
994#[serde(deny_unknown_fields)]
995struct RoleDto {
996    name: String,
997    role_hash: String,
998    operations: Vec<OperationDto>,
999}
1000
1001#[derive(Serialize, Deserialize)]
1002#[serde(deny_unknown_fields)]
1003struct OperationDto {
1004    kind: String,
1005    name: String,
1006}
1007
1008#[derive(Serialize, Deserialize)]
1009#[serde(deny_unknown_fields)]
1010struct DriverDto {
1011    driver: String,
1012    runtime_version: String,
1013    platforms: Vec<String>,
1014    conformance_hash: String,
1015}
1016
1017#[derive(Serialize, Deserialize)]
1018#[serde(deny_unknown_fields)]
1019struct ProbeDto {
1020    name: String,
1021    role: String,
1022    operation: OperationDto,
1023    expected_observation_hash: String,
1024    maximum_items: u32,
1025}
1026
1027#[derive(Serialize, Deserialize)]
1028#[serde(deny_unknown_fields)]
1029struct EvolutionDto {
1030    class: String,
1031    predecessor: Option<ContractDto>,
1032    successor: ContractDto,
1033    migration_hash: Option<String>,
1034    expected_observation_hash: String,
1035}
1036
1037#[derive(Serialize, Deserialize)]
1038#[serde(deny_unknown_fields)]
1039struct ContractDto {
1040    version: u64,
1041    bundle_hash: String,
1042}
1043
1044impl ManifestDto {
1045    fn from_input(schema: &str, input: &AdapterConformanceManifestInput) -> Self {
1046        Self {
1047            schema: schema.to_owned(),
1048            adapter: input.adapter.as_str().to_owned(),
1049            adapter_version: input.adapter_version.as_str().to_owned(),
1050            application_manifest_hash: hex(input.application_manifest_hash.as_bytes()),
1051            application_lock_hash: hex(input.application_lock_hash.as_bytes()),
1052            contract_lineage: input.contract_lineage.as_str().to_owned(),
1053            feature_claims: input
1054                .feature_claims
1055                .iter()
1056                .map(FeatureClaimDto::from_claim)
1057                .collect(),
1058            artifacts: input
1059                .artifacts
1060                .iter()
1061                .map(ArtifactDto::from_artifact)
1062                .collect(),
1063            roles: input.roles.iter().map(RoleDto::from_role).collect(),
1064            drivers: input.drivers.iter().map(DriverDto::from_driver).collect(),
1065            conformance: input.conformance.iter().map(ProbeDto::from_probe).collect(),
1066            evolution: input
1067                .evolution
1068                .iter()
1069                .map(EvolutionDto::from_evolution)
1070                .collect(),
1071        }
1072    }
1073
1074    fn into_input(self) -> Result<AdapterConformanceManifestInput, AdapterConformanceError> {
1075        Ok(AdapterConformanceManifestInput {
1076            adapter: symbol(self.adapter)?,
1077            adapter_version: symbol(self.adapter_version)?,
1078            application_manifest_hash: ApplicationManifestHash::from_bytes(parse_hash(
1079                &self.application_manifest_hash,
1080            )?),
1081            application_lock_hash: ApplicationLockHash::from_bytes(parse_hash(
1082                &self.application_lock_hash,
1083            )?),
1084            contract_lineage: ContractLineage::new(self.contract_lineage)
1085                .map_err(|_| invalid_encoding())?,
1086            feature_claims: self
1087                .feature_claims
1088                .into_iter()
1089                .map(FeatureClaimDto::into_claim)
1090                .collect::<Result<_, _>>()?,
1091            artifacts: self
1092                .artifacts
1093                .into_iter()
1094                .map(ArtifactDto::into_artifact)
1095                .collect::<Result<_, _>>()?,
1096            roles: self
1097                .roles
1098                .into_iter()
1099                .map(RoleDto::into_role)
1100                .collect::<Result<_, _>>()?,
1101            drivers: self
1102                .drivers
1103                .into_iter()
1104                .map(DriverDto::into_driver)
1105                .collect::<Result<_, _>>()?,
1106            conformance: self
1107                .conformance
1108                .into_iter()
1109                .map(ProbeDto::into_probe)
1110                .collect::<Result<_, _>>()?,
1111            evolution: self
1112                .evolution
1113                .into_iter()
1114                .map(EvolutionDto::into_evolution)
1115                .collect::<Result<_, _>>()?,
1116        })
1117    }
1118}
1119
1120impl FeatureClaimDto {
1121    fn from_claim(claim: &AdapterFeatureClaim) -> Self {
1122        Self {
1123            feature: claim.feature.tag().to_owned(),
1124            disposition: claim.disposition.tag().to_owned(),
1125            limitation: claim.limitation.map(|value| value.tag().to_owned()),
1126        }
1127    }
1128
1129    fn into_claim(self) -> Result<AdapterFeatureClaim, AdapterConformanceError> {
1130        AdapterFeatureClaim::new(
1131            InstallationFeature::parse(&self.feature).ok_or_else(invalid_encoding)?,
1132            AdapterFeatureDisposition::parse(&self.disposition).ok_or_else(invalid_encoding)?,
1133            self.limitation
1134                .map(|value| AdapterLimitation::parse(&value).ok_or_else(invalid_encoding))
1135                .transpose()?,
1136        )
1137    }
1138}
1139
1140impl ArtifactDto {
1141    fn from_artifact(value: &InstallationArtifact) -> Self {
1142        Self {
1143            kind: value.kind().tag().to_owned(),
1144            name: value.name().as_str().to_owned(),
1145            content_hash: hex(value.content_hash().as_bytes()),
1146        }
1147    }
1148
1149    fn into_artifact(self) -> Result<InstallationArtifact, AdapterConformanceError> {
1150        Ok(InstallationArtifact::new(
1151            InstallationArtifactKind::parse(&self.kind).ok_or_else(invalid_encoding)?,
1152            symbol(self.name)?,
1153            GeneratedArtifactHash::from_bytes(parse_hash(&self.content_hash)?),
1154        ))
1155    }
1156}
1157
1158impl RoleDto {
1159    fn from_role(value: &AdapterRoleRequirement) -> Self {
1160        Self {
1161            name: value.name.as_str().to_owned(),
1162            role_hash: hex(value.role_hash.as_bytes()),
1163            operations: value
1164                .operations
1165                .iter()
1166                .map(OperationDto::from_operation)
1167                .collect(),
1168        }
1169    }
1170
1171    fn into_role(self) -> Result<AdapterRoleRequirement, AdapterConformanceError> {
1172        AdapterRoleRequirement::new(
1173            symbol(self.name)?,
1174            ApplicationRoleHash::from_bytes(parse_hash(&self.role_hash)?),
1175            self.operations
1176                .into_iter()
1177                .map(OperationDto::into_operation)
1178                .collect::<Result<_, _>>()?,
1179        )
1180    }
1181}
1182
1183impl OperationDto {
1184    fn from_operation(value: &RoleOperation) -> Self {
1185        Self {
1186            kind: value.kind().tag().to_owned(),
1187            name: value.name().as_str().to_owned(),
1188        }
1189    }
1190
1191    fn into_operation(self) -> Result<RoleOperation, AdapterConformanceError> {
1192        Ok(RoleOperation::new(
1193            RoleOperationKind::parse(&self.kind).ok_or_else(invalid_encoding)?,
1194            symbol(self.name)?,
1195        ))
1196    }
1197}
1198
1199impl DriverDto {
1200    fn from_driver(value: &AdapterDriverRequirement) -> Self {
1201        Self {
1202            driver: value.driver.tag().to_owned(),
1203            runtime_version: value.runtime_version.as_str().to_owned(),
1204            platforms: value
1205                .platforms
1206                .iter()
1207                .map(|value| value.tag().to_owned())
1208                .collect(),
1209            conformance_hash: hex(value.conformance_hash.as_bytes()),
1210        }
1211    }
1212
1213    fn into_driver(self) -> Result<AdapterDriverRequirement, AdapterConformanceError> {
1214        AdapterDriverRequirement::new(
1215            InstallationDriver::parse(&self.driver).ok_or_else(invalid_encoding)?,
1216            symbol(self.runtime_version)?,
1217            self.platforms
1218                .into_iter()
1219                .map(|value| AdapterPlatform::parse(&value).ok_or_else(invalid_encoding))
1220                .collect::<Result<_, _>>()?,
1221            GeneratedArtifactHash::from_bytes(parse_hash(&self.conformance_hash)?),
1222        )
1223    }
1224}
1225
1226impl ProbeDto {
1227    fn from_probe(value: &AdapterConformanceProbe) -> Self {
1228        Self {
1229            name: value.name.as_str().to_owned(),
1230            role: value.role.as_str().to_owned(),
1231            operation: OperationDto::from_operation(&value.operation),
1232            expected_observation_hash: hex(value.expected_observation_hash.as_bytes()),
1233            maximum_items: value.maximum_items,
1234        }
1235    }
1236
1237    fn into_probe(self) -> Result<AdapterConformanceProbe, AdapterConformanceError> {
1238        AdapterConformanceProbe::new(
1239            symbol(self.name)?,
1240            symbol(self.role)?,
1241            self.operation.into_operation()?,
1242            GeneratedArtifactHash::from_bytes(parse_hash(&self.expected_observation_hash)?),
1243            self.maximum_items,
1244        )
1245    }
1246}
1247
1248impl EvolutionDto {
1249    fn from_evolution(value: &AdapterEvolutionRequirement) -> Self {
1250        Self {
1251            class: value.class.tag().to_owned(),
1252            predecessor: value.predecessor.map(ContractDto::from_contract),
1253            successor: ContractDto::from_contract(value.successor),
1254            migration_hash: value.migration_hash.map(|value| hex(value.as_bytes())),
1255            expected_observation_hash: hex(value.expected_observation_hash.as_bytes()),
1256        }
1257    }
1258
1259    fn into_evolution(self) -> Result<AdapterEvolutionRequirement, AdapterConformanceError> {
1260        AdapterEvolutionRequirement::new(
1261            AdapterEvolutionClass::parse(&self.class).ok_or_else(invalid_encoding)?,
1262            self.predecessor
1263                .map(ContractDto::into_contract)
1264                .transpose()?,
1265            self.successor.into_contract()?,
1266            self.migration_hash
1267                .map(|value| parse_hash(&value).map(MigrationBundleHash::from_bytes))
1268                .transpose()?,
1269            GeneratedArtifactHash::from_bytes(parse_hash(&self.expected_observation_hash)?),
1270        )
1271    }
1272}
1273
1274impl ContractDto {
1275    fn from_contract(value: InstallationContract) -> Self {
1276        Self {
1277            version: value.version().get(),
1278            bundle_hash: hex(value.bundle_hash().as_bytes()),
1279        }
1280    }
1281
1282    fn into_contract(self) -> Result<InstallationContract, AdapterConformanceError> {
1283        Ok(InstallationContract::new(
1284            ContractVersion::new(self.version).ok_or_else(invalid_encoding)?,
1285            ContractBundleHash::from_bytes(parse_hash(&self.bundle_hash)?),
1286        ))
1287    }
1288}
1289
1290fn symbol(value: String) -> Result<InstallationSymbol, AdapterConformanceError> {
1291    InstallationSymbol::new(value).map_err(|_| invalid_encoding())
1292}
1293
1294fn invalid_encoding() -> AdapterConformanceError {
1295    AdapterConformanceError::new(AdapterConformanceErrorKind::InvalidEncoding)
1296}
1297
1298fn hex(bytes: &[u8; 32]) -> String {
1299    const DIGITS: &[u8; 16] = b"0123456789abcdef";
1300    let mut output = String::with_capacity(64);
1301    for byte in bytes {
1302        output.push(char::from(DIGITS[usize::from(byte >> 4)]));
1303        output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
1304    }
1305    output
1306}
1307
1308fn parse_hash(value: &str) -> Result<[u8; 32], AdapterConformanceError> {
1309    if value.len() != 64 {
1310        return Err(invalid_encoding());
1311    }
1312    let mut bytes = [0_u8; 32];
1313    for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
1314        bytes[index] = (nibble(pair[0]).ok_or_else(invalid_encoding)? << 4)
1315            | nibble(pair[1]).ok_or_else(invalid_encoding)?;
1316    }
1317    Ok(bytes)
1318}
1319
1320const fn nibble(value: u8) -> Option<u8> {
1321    match value {
1322        b'0'..=b'9' => Some(value - b'0'),
1323        b'a'..=b'f' => Some(value - b'a' + 10),
1324        _ => None,
1325    }
1326}