lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

v1.rs (48003B)


      1 //! Runtime operation descriptor contract generation 1.
      2 //!
      3 //! This module owns passive, serialized operation identities and policy
      4 //! descriptors. Native SDK operation implementations and host execution state
      5 //! deliberately remain outside the wire-contract package.
      6 
      7 use alloc::{
      8     collections::BTreeSet,
      9     format,
     10     string::{String, ToString},
     11     vec::Vec,
     12 };
     13 use core::fmt;
     14 
     15 use crate::{
     16     capability::v1::{Maturity, TransportKind},
     17     schema::{Descriptor as SchemaDescriptor, ModuleVersion, Registry},
     18 };
     19 
     20 /// Schema generation shared by every runtime operation request and receipt.
     21 pub const OPERATION_SCHEMA_VERSION: u16 = 1;
     22 
     23 /// Typed readiness of one optional synchronization capability.
     24 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     25 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     26 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     27 pub enum SyncCapabilityState {
     28     Unsupported,
     29     Compiled,
     30     Configured,
     31     Available,
     32     Degraded,
     33 }
     34 
     35 /// Aggregate synchronization health for the passive status operation.
     36 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     37 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     38 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     39 pub enum SyncHealth {
     40     Healthy,
     41     Degraded,
     42     Unavailable,
     43 }
     44 
     45 /// Host-action classification for one durable outbox record.
     46 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     47 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     48 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     49 pub enum SyncRetryDecision {
     50     Ready,
     51     DeferredUntil { unix_ms: u64 },
     52     InFlightUntil { unix_ms: u64 },
     53     Satisfied,
     54     Exhausted,
     55     Expired,
     56 }
     57 
     58 /// Passive durable outbox cardinalities for `sync.status` generation 1.
     59 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     60 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
     61 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     62 pub struct SyncOutboxStatus {
     63     pub pending: u64,
     64     pub leased: u64,
     65     pub retryable: u64,
     66     pub satisfied: u64,
     67     pub exhausted: u64,
     68 }
     69 
     70 /// Passive projection cardinalities for `sync.status` generation 1.
     71 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     72 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
     73 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     74 pub struct SyncProjectionStatus {
     75     pub ready: u32,
     76     pub invalidated: u32,
     77     pub rebuilding: u32,
     78     pub failed: u32,
     79     pub untracked: u32,
     80 }
     81 
     82 /// Versioned passive receipt for the `sync.status` operation.
     83 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     84 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
     85 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     86 pub struct SyncStatusReceipt {
     87     pub schema_version: u16,
     88     pub health: SyncHealth,
     89     pub storage: SyncCapabilityState,
     90     pub source: SyncCapabilityState,
     91     pub sink: SyncCapabilityState,
     92     pub signer: SyncCapabilityState,
     93     pub outbox: SyncOutboxStatus,
     94     pub projections: SyncProjectionStatus,
     95 }
     96 
     97 impl SyncStatusReceipt {
     98     /// Rejects status receipts from an unsupported operation generation.
     99     pub const fn validate(&self) -> Result<(), Error> {
    100         if self.schema_version != OPERATION_SCHEMA_VERSION {
    101             return Err(Error::UnsupportedOperationSchemaVersion {
    102                 operation_id: OperationId::SyncStatus,
    103                 version: self.schema_version,
    104             });
    105         }
    106         Ok(())
    107     }
    108 }
    109 
    110 #[cfg(feature = "serde")]
    111 impl<'de> serde::Deserialize<'de> for SyncStatusReceipt {
    112     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    113     where
    114         D: serde::Deserializer<'de>,
    115     {
    116         #[derive(serde::Deserialize)]
    117         #[serde(deny_unknown_fields)]
    118         struct Wire {
    119             schema_version: u16,
    120             health: SyncHealth,
    121             storage: SyncCapabilityState,
    122             source: SyncCapabilityState,
    123             sink: SyncCapabilityState,
    124             signer: SyncCapabilityState,
    125             outbox: SyncOutboxStatus,
    126             projections: SyncProjectionStatus,
    127         }
    128         let wire = Wire::deserialize(deserializer)?;
    129         let receipt = Self {
    130             schema_version: wire.schema_version,
    131             health: wire.health,
    132             storage: wire.storage,
    133             source: wire.source,
    134             sink: wire.sink,
    135             signer: wire.signer,
    136             outbox: wire.outbox,
    137             projections: wire.projections,
    138         };
    139         receipt.validate().map_err(serde::de::Error::custom)?;
    140         Ok(receipt)
    141     }
    142 }
    143 
    144 macro_rules! operation_ids {
    145     ($( $variant:ident => $value:literal ),+ $(,)?) => {
    146         #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    147         pub enum OperationId {
    148             $( $variant, )+
    149         }
    150 
    151         impl OperationId {
    152             pub fn as_str(self) -> &'static str {
    153                 match self {
    154                     $( Self::$variant => $value, )+
    155                 }
    156             }
    157 
    158             pub fn parse(value: &str) -> Result<Self, Error> {
    159                 match value {
    160                     $( $value => Ok(Self::$variant), )+
    161                     _ => Err(Error::UnknownOperationId {
    162                         operation_id: value.to_string(),
    163                     }),
    164                 }
    165             }
    166 
    167             pub fn request_schema_id(self) -> &'static str {
    168                 match self {
    169                     $( Self::$variant => concat!("radroots.runtime.", $value, ".request.v1"), )+
    170                 }
    171             }
    172 
    173             pub fn receipt_schema_id(self) -> &'static str {
    174                 match self {
    175                     $( Self::$variant => concat!("radroots.runtime.", $value, ".receipt.v1"), )+
    176                 }
    177             }
    178         }
    179 
    180         #[cfg(feature = "serde")]
    181         impl serde::Serialize for OperationId {
    182             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    183             where
    184                 S: serde::Serializer,
    185             {
    186                 serializer.serialize_str(self.as_str())
    187             }
    188         }
    189 
    190         #[cfg(feature = "serde")]
    191         impl<'de> serde::Deserialize<'de> for OperationId {
    192             fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    193             where
    194                 D: serde::Deserializer<'de>,
    195             {
    196                 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
    197                 Self::parse(value.as_str()).map_err(serde::de::Error::custom)
    198             }
    199         }
    200     };
    201 }
    202 
    203 operation_ids! {
    204     ProfileInspect => "profile.inspect",
    205     ProfileReset => "profile.reset",
    206     AccountCreate => "account.create",
    207     AccountImport => "account.import",
    208     AccountSelect => "account.select",
    209     AccountList => "account.list",
    210     AccountRemove => "account.remove",
    211     SignerStatus => "signer.status",
    212     StoreInspect => "store.inspect",
    213     StoreBackup => "store.backup",
    214     StoreRestore => "store.restore",
    215     FarmCreate => "farm.create",
    216     FarmUpdate => "farm.update",
    217     FarmPublish => "farm.publish",
    218     FarmGet => "farm.get",
    219     FarmList => "farm.list",
    220     ListingCreate => "listing.create",
    221     ListingUpdate => "listing.update",
    222     ListingPublish => "listing.publish",
    223     ListingPause => "listing.pause",
    224     ListingWithdraw => "listing.withdraw",
    225     ListingGet => "listing.get",
    226     ListingList => "listing.list",
    227     MarketPull => "market.pull",
    228     MarketSearch => "market.search",
    229     MarketGet => "market.get",
    230     BasketCreate => "basket.create",
    231     BasketGet => "basket.get",
    232     BasketList => "basket.list",
    233     BasketItemAdd => "basket.item.add",
    234     BasketItemUpdate => "basket.item.update",
    235     BasketItemRemove => "basket.item.remove",
    236     BasketQuote => "basket.quote",
    237     TradeProposalSubmit => "trade.proposal.submit",
    238     TradeRevisionPropose => "trade.revision.propose",
    239     TradeCandidateDecide => "trade.candidate.decide",
    240     TradeCancellationSubmit => "trade.cancellation.submit",
    241     TradeOperationResume => "trade.operation.resume",
    242     TradeGet => "trade.get",
    243     TradeList => "trade.list",
    244     TradeEvidenceRefresh => "trade.evidence.refresh",
    245     TradeEvidenceInspect => "trade.evidence.inspect",
    246     TradePrivateArtifactSeal => "trade.private_artifact.seal",
    247     TradePrivateArtifactOpen => "trade.private_artifact.open",
    248     TradePrivateArtifactDelete => "trade.private_artifact.delete",
    249     ValidationStatus => "validation.status",
    250     SyncStatus => "sync.status",
    251     SyncPull => "sync.pull",
    252     SyncPush => "sync.push",
    253     HealthInspect => "health.inspect",
    254     TransportCapabilityList => "transport.capability.list",
    255     TransportConfigInspect => "transport.config.inspect",
    256     TransportConfigUpdate => "transport.config.update",
    257     TransportStatusInspect => "transport.status.inspect",
    258     TransportDeliveryInspect => "transport.delivery.inspect",
    259     TransportDeliveryRetry => "transport.delivery.retry",
    260     DiagnosticsInspect => "diagnostics.inspect",
    261 }
    262 
    263 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    264 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    265 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    266 pub enum Mutability {
    267     Read,
    268     Mutation,
    269 }
    270 
    271 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    272 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    273 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    274 pub enum Risk {
    275     Low,
    276     Medium,
    277     High,
    278     Critical,
    279 }
    280 
    281 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    282 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    283 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    284 pub enum ApprovalRequirement {
    285     None,
    286     ConditionalOrRequiredByMode,
    287     Required,
    288 }
    289 
    290 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    291 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    292 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    293 pub enum SignerRequirement {
    294     None,
    295     Required,
    296     ConditionalRelayAuth,
    297 }
    298 
    299 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    300 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    301 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    302 pub enum IdempotencyPolicy {
    303     Forbidden,
    304     RequiredUuidV7,
    305 }
    306 
    307 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    308 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    309 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    310 pub enum DryRunSupport {
    311     NotApplicable,
    312     PureLocalPlan,
    313 }
    314 
    315 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    316 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    317 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    318 pub enum DeadlinePolicy {
    319     DefaultBounded,
    320     OperationDeclared,
    321 }
    322 
    323 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    324 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    325 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    326 pub enum PrivacyEffect {
    327     None,
    328     PublicEvent,
    329     PrivateCoordination,
    330     PrivateStore,
    331     BackupRestore,
    332 }
    333 
    334 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    335 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    336 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    337 pub enum ProjectionEffect {
    338     None,
    339     ReadsProjection,
    340     WritesProjection,
    341     MayUpdateProjection,
    342 }
    343 
    344 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    345 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
    346 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    347 pub struct TransportRoute {
    348     pub local: bool,
    349     pub nostr: bool,
    350     pub reticulum: bool,
    351     pub deliver: bool,
    352     pub fetch: bool,
    353     pub synchronize: bool,
    354     pub diagnostics: bool,
    355 }
    356 
    357 impl TransportRoute {
    358     pub const fn none() -> Self {
    359         Self {
    360             local: false,
    361             nostr: false,
    362             reticulum: false,
    363             deliver: false,
    364             fetch: false,
    365             synchronize: false,
    366             diagnostics: false,
    367         }
    368     }
    369 
    370     pub const fn local() -> Self {
    371         Self {
    372             local: true,
    373             nostr: false,
    374             reticulum: false,
    375             deliver: false,
    376             fetch: false,
    377             synchronize: false,
    378             diagnostics: false,
    379         }
    380     }
    381 
    382     pub const fn delivery() -> Self {
    383         Self {
    384             local: false,
    385             nostr: true,
    386             reticulum: true,
    387             deliver: true,
    388             fetch: false,
    389             synchronize: false,
    390             diagnostics: false,
    391         }
    392     }
    393 
    394     pub const fn fetch() -> Self {
    395         Self {
    396             local: false,
    397             nostr: true,
    398             reticulum: true,
    399             deliver: false,
    400             fetch: true,
    401             synchronize: true,
    402             diagnostics: false,
    403         }
    404     }
    405 
    406     pub const fn diagnostics() -> Self {
    407         Self {
    408             local: true,
    409             nostr: true,
    410             reticulum: true,
    411             deliver: false,
    412             fetch: false,
    413             synchronize: false,
    414             diagnostics: true,
    415         }
    416     }
    417 
    418     pub fn includes_transport(self, kind: TransportKind) -> bool {
    419         match kind {
    420             TransportKind::LOCAL => self.local,
    421             TransportKind::NOSTR => self.nostr,
    422             TransportKind::RETICULUM => self.reticulum,
    423             _ => false,
    424         }
    425     }
    426 }
    427 
    428 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    429 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
    430 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    431 pub struct OperationDescriptor {
    432     pub operation_id: OperationId,
    433     pub schema_version: u16,
    434     pub mutability: Mutability,
    435     pub risk: Risk,
    436     pub approval: ApprovalRequirement,
    437     pub signer: SignerRequirement,
    438     pub transport_capability: TransportRoute,
    439     pub idempotency: IdempotencyPolicy,
    440     pub dry_run: DryRunSupport,
    441     pub deadline: DeadlinePolicy,
    442     pub privacy: PrivacyEffect,
    443     pub projection: ProjectionEffect,
    444     pub maturity: Maturity,
    445 }
    446 
    447 impl OperationDescriptor {
    448     pub fn request_schema_id(self) -> &'static str {
    449         self.operation_id.request_schema_id()
    450     }
    451 
    452     pub fn receipt_schema_id(self) -> &'static str {
    453         self.operation_id.receipt_schema_id()
    454     }
    455 }
    456 
    457 struct DescriptorSpec {
    458     operation_id: OperationId,
    459     mutability: Mutability,
    460     risk: Risk,
    461     approval: ApprovalRequirement,
    462     signer: SignerRequirement,
    463     transport_capability: TransportRoute,
    464     idempotency: IdempotencyPolicy,
    465     dry_run: DryRunSupport,
    466     privacy: PrivacyEffect,
    467     projection: ProjectionEffect,
    468 }
    469 
    470 const fn read(
    471     operation_id: OperationId,
    472     risk: Risk,
    473     transport_capability: TransportRoute,
    474     privacy: PrivacyEffect,
    475     projection: ProjectionEffect,
    476 ) -> OperationDescriptor {
    477     descriptor(DescriptorSpec {
    478         operation_id,
    479         mutability: Mutability::Read,
    480         risk,
    481         approval: ApprovalRequirement::None,
    482         signer: SignerRequirement::None,
    483         transport_capability,
    484         idempotency: IdempotencyPolicy::Forbidden,
    485         dry_run: DryRunSupport::NotApplicable,
    486         privacy,
    487         projection,
    488     })
    489 }
    490 
    491 const fn mutation(
    492     operation_id: OperationId,
    493     risk: Risk,
    494     approval: ApprovalRequirement,
    495     signer: SignerRequirement,
    496     transport_capability: TransportRoute,
    497     privacy: PrivacyEffect,
    498     projection: ProjectionEffect,
    499 ) -> OperationDescriptor {
    500     descriptor(DescriptorSpec {
    501         operation_id,
    502         mutability: Mutability::Mutation,
    503         risk,
    504         approval,
    505         signer,
    506         transport_capability,
    507         idempotency: IdempotencyPolicy::RequiredUuidV7,
    508         dry_run: DryRunSupport::PureLocalPlan,
    509         privacy,
    510         projection,
    511     })
    512 }
    513 
    514 const fn descriptor(spec: DescriptorSpec) -> OperationDescriptor {
    515     OperationDescriptor {
    516         operation_id: spec.operation_id,
    517         schema_version: OPERATION_SCHEMA_VERSION,
    518         mutability: spec.mutability,
    519         risk: spec.risk,
    520         approval: spec.approval,
    521         signer: spec.signer,
    522         transport_capability: spec.transport_capability,
    523         idempotency: spec.idempotency,
    524         dry_run: spec.dry_run,
    525         deadline: DeadlinePolicy::DefaultBounded,
    526         privacy: spec.privacy,
    527         projection: spec.projection,
    528         maturity: Maturity::Stable,
    529     }
    530 }
    531 
    532 pub const CATALOG: &[OperationDescriptor] = &[
    533     read(
    534         OperationId::ProfileInspect,
    535         Risk::Low,
    536         TransportRoute::local(),
    537         PrivacyEffect::PrivateStore,
    538         ProjectionEffect::ReadsProjection,
    539     ),
    540     mutation(
    541         OperationId::ProfileReset,
    542         Risk::Critical,
    543         ApprovalRequirement::Required,
    544         SignerRequirement::None,
    545         TransportRoute::local(),
    546         PrivacyEffect::PrivateStore,
    547         ProjectionEffect::WritesProjection,
    548     ),
    549     mutation(
    550         OperationId::AccountCreate,
    551         Risk::High,
    552         ApprovalRequirement::ConditionalOrRequiredByMode,
    553         SignerRequirement::None,
    554         TransportRoute::local(),
    555         PrivacyEffect::PrivateStore,
    556         ProjectionEffect::WritesProjection,
    557     ),
    558     mutation(
    559         OperationId::AccountImport,
    560         Risk::High,
    561         ApprovalRequirement::ConditionalOrRequiredByMode,
    562         SignerRequirement::None,
    563         TransportRoute::local(),
    564         PrivacyEffect::PrivateStore,
    565         ProjectionEffect::WritesProjection,
    566     ),
    567     mutation(
    568         OperationId::AccountSelect,
    569         Risk::Medium,
    570         ApprovalRequirement::None,
    571         SignerRequirement::None,
    572         TransportRoute::local(),
    573         PrivacyEffect::PrivateStore,
    574         ProjectionEffect::WritesProjection,
    575     ),
    576     read(
    577         OperationId::AccountList,
    578         Risk::Low,
    579         TransportRoute::local(),
    580         PrivacyEffect::PrivateStore,
    581         ProjectionEffect::ReadsProjection,
    582     ),
    583     mutation(
    584         OperationId::AccountRemove,
    585         Risk::Critical,
    586         ApprovalRequirement::Required,
    587         SignerRequirement::None,
    588         TransportRoute::local(),
    589         PrivacyEffect::PrivateStore,
    590         ProjectionEffect::WritesProjection,
    591     ),
    592     read(
    593         OperationId::SignerStatus,
    594         Risk::Low,
    595         TransportRoute::local(),
    596         PrivacyEffect::None,
    597         ProjectionEffect::None,
    598     ),
    599     read(
    600         OperationId::StoreInspect,
    601         Risk::Low,
    602         TransportRoute::local(),
    603         PrivacyEffect::PrivateStore,
    604         ProjectionEffect::ReadsProjection,
    605     ),
    606     mutation(
    607         OperationId::StoreBackup,
    608         Risk::High,
    609         ApprovalRequirement::ConditionalOrRequiredByMode,
    610         SignerRequirement::None,
    611         TransportRoute::local(),
    612         PrivacyEffect::BackupRestore,
    613         ProjectionEffect::ReadsProjection,
    614     ),
    615     mutation(
    616         OperationId::StoreRestore,
    617         Risk::Critical,
    618         ApprovalRequirement::Required,
    619         SignerRequirement::None,
    620         TransportRoute::local(),
    621         PrivacyEffect::BackupRestore,
    622         ProjectionEffect::WritesProjection,
    623     ),
    624     mutation(
    625         OperationId::FarmCreate,
    626         Risk::Medium,
    627         ApprovalRequirement::None,
    628         SignerRequirement::None,
    629         TransportRoute::local(),
    630         PrivacyEffect::PrivateStore,
    631         ProjectionEffect::WritesProjection,
    632     ),
    633     mutation(
    634         OperationId::FarmUpdate,
    635         Risk::Medium,
    636         ApprovalRequirement::None,
    637         SignerRequirement::None,
    638         TransportRoute::local(),
    639         PrivacyEffect::PrivateStore,
    640         ProjectionEffect::WritesProjection,
    641     ),
    642     mutation(
    643         OperationId::FarmPublish,
    644         Risk::Medium,
    645         ApprovalRequirement::None,
    646         SignerRequirement::Required,
    647         TransportRoute::delivery(),
    648         PrivacyEffect::PublicEvent,
    649         ProjectionEffect::MayUpdateProjection,
    650     ),
    651     read(
    652         OperationId::FarmGet,
    653         Risk::Low,
    654         TransportRoute::local(),
    655         PrivacyEffect::None,
    656         ProjectionEffect::ReadsProjection,
    657     ),
    658     read(
    659         OperationId::FarmList,
    660         Risk::Low,
    661         TransportRoute::local(),
    662         PrivacyEffect::None,
    663         ProjectionEffect::ReadsProjection,
    664     ),
    665     mutation(
    666         OperationId::ListingCreate,
    667         Risk::Medium,
    668         ApprovalRequirement::None,
    669         SignerRequirement::None,
    670         TransportRoute::local(),
    671         PrivacyEffect::PrivateStore,
    672         ProjectionEffect::WritesProjection,
    673     ),
    674     mutation(
    675         OperationId::ListingUpdate,
    676         Risk::Medium,
    677         ApprovalRequirement::None,
    678         SignerRequirement::None,
    679         TransportRoute::local(),
    680         PrivacyEffect::PrivateStore,
    681         ProjectionEffect::WritesProjection,
    682     ),
    683     mutation(
    684         OperationId::ListingPublish,
    685         Risk::Medium,
    686         ApprovalRequirement::None,
    687         SignerRequirement::Required,
    688         TransportRoute::delivery(),
    689         PrivacyEffect::PublicEvent,
    690         ProjectionEffect::MayUpdateProjection,
    691     ),
    692     mutation(
    693         OperationId::ListingPause,
    694         Risk::Medium,
    695         ApprovalRequirement::None,
    696         SignerRequirement::Required,
    697         TransportRoute::delivery(),
    698         PrivacyEffect::PublicEvent,
    699         ProjectionEffect::MayUpdateProjection,
    700     ),
    701     mutation(
    702         OperationId::ListingWithdraw,
    703         Risk::High,
    704         ApprovalRequirement::ConditionalOrRequiredByMode,
    705         SignerRequirement::Required,
    706         TransportRoute::delivery(),
    707         PrivacyEffect::PublicEvent,
    708         ProjectionEffect::MayUpdateProjection,
    709     ),
    710     read(
    711         OperationId::ListingGet,
    712         Risk::Low,
    713         TransportRoute::local(),
    714         PrivacyEffect::None,
    715         ProjectionEffect::ReadsProjection,
    716     ),
    717     read(
    718         OperationId::ListingList,
    719         Risk::Low,
    720         TransportRoute::local(),
    721         PrivacyEffect::None,
    722         ProjectionEffect::ReadsProjection,
    723     ),
    724     mutation(
    725         OperationId::MarketPull,
    726         Risk::Medium,
    727         ApprovalRequirement::None,
    728         SignerRequirement::ConditionalRelayAuth,
    729         TransportRoute::fetch(),
    730         PrivacyEffect::None,
    731         ProjectionEffect::MayUpdateProjection,
    732     ),
    733     read(
    734         OperationId::MarketSearch,
    735         Risk::Low,
    736         TransportRoute::local(),
    737         PrivacyEffect::None,
    738         ProjectionEffect::ReadsProjection,
    739     ),
    740     read(
    741         OperationId::MarketGet,
    742         Risk::Low,
    743         TransportRoute::local(),
    744         PrivacyEffect::None,
    745         ProjectionEffect::ReadsProjection,
    746     ),
    747     mutation(
    748         OperationId::BasketCreate,
    749         Risk::Medium,
    750         ApprovalRequirement::None,
    751         SignerRequirement::None,
    752         TransportRoute::local(),
    753         PrivacyEffect::PrivateStore,
    754         ProjectionEffect::WritesProjection,
    755     ),
    756     read(
    757         OperationId::BasketGet,
    758         Risk::Low,
    759         TransportRoute::local(),
    760         PrivacyEffect::PrivateStore,
    761         ProjectionEffect::ReadsProjection,
    762     ),
    763     read(
    764         OperationId::BasketList,
    765         Risk::Low,
    766         TransportRoute::local(),
    767         PrivacyEffect::PrivateStore,
    768         ProjectionEffect::ReadsProjection,
    769     ),
    770     mutation(
    771         OperationId::BasketItemAdd,
    772         Risk::Medium,
    773         ApprovalRequirement::None,
    774         SignerRequirement::None,
    775         TransportRoute::local(),
    776         PrivacyEffect::PrivateStore,
    777         ProjectionEffect::WritesProjection,
    778     ),
    779     mutation(
    780         OperationId::BasketItemUpdate,
    781         Risk::Medium,
    782         ApprovalRequirement::None,
    783         SignerRequirement::None,
    784         TransportRoute::local(),
    785         PrivacyEffect::PrivateStore,
    786         ProjectionEffect::WritesProjection,
    787     ),
    788     mutation(
    789         OperationId::BasketItemRemove,
    790         Risk::Medium,
    791         ApprovalRequirement::None,
    792         SignerRequirement::None,
    793         TransportRoute::local(),
    794         PrivacyEffect::PrivateStore,
    795         ProjectionEffect::WritesProjection,
    796     ),
    797     mutation(
    798         OperationId::BasketQuote,
    799         Risk::Medium,
    800         ApprovalRequirement::None,
    801         SignerRequirement::None,
    802         TransportRoute::local(),
    803         PrivacyEffect::PrivateStore,
    804         ProjectionEffect::WritesProjection,
    805     ),
    806     mutation(
    807         OperationId::TradeProposalSubmit,
    808         Risk::High,
    809         ApprovalRequirement::ConditionalOrRequiredByMode,
    810         SignerRequirement::Required,
    811         TransportRoute::delivery(),
    812         PrivacyEffect::PrivateCoordination,
    813         ProjectionEffect::MayUpdateProjection,
    814     ),
    815     mutation(
    816         OperationId::TradeRevisionPropose,
    817         Risk::High,
    818         ApprovalRequirement::ConditionalOrRequiredByMode,
    819         SignerRequirement::Required,
    820         TransportRoute::delivery(),
    821         PrivacyEffect::PrivateCoordination,
    822         ProjectionEffect::MayUpdateProjection,
    823     ),
    824     mutation(
    825         OperationId::TradeCandidateDecide,
    826         Risk::High,
    827         ApprovalRequirement::ConditionalOrRequiredByMode,
    828         SignerRequirement::Required,
    829         TransportRoute::delivery(),
    830         PrivacyEffect::PrivateCoordination,
    831         ProjectionEffect::MayUpdateProjection,
    832     ),
    833     mutation(
    834         OperationId::TradeCancellationSubmit,
    835         Risk::High,
    836         ApprovalRequirement::ConditionalOrRequiredByMode,
    837         SignerRequirement::Required,
    838         TransportRoute::delivery(),
    839         PrivacyEffect::PrivateCoordination,
    840         ProjectionEffect::MayUpdateProjection,
    841     ),
    842     mutation(
    843         OperationId::TradeOperationResume,
    844         Risk::High,
    845         ApprovalRequirement::ConditionalOrRequiredByMode,
    846         SignerRequirement::Required,
    847         TransportRoute::delivery(),
    848         PrivacyEffect::PrivateCoordination,
    849         ProjectionEffect::MayUpdateProjection,
    850     ),
    851     read(
    852         OperationId::TradeGet,
    853         Risk::Low,
    854         TransportRoute::local(),
    855         PrivacyEffect::PrivateCoordination,
    856         ProjectionEffect::ReadsProjection,
    857     ),
    858     read(
    859         OperationId::TradeList,
    860         Risk::Low,
    861         TransportRoute::local(),
    862         PrivacyEffect::PrivateCoordination,
    863         ProjectionEffect::ReadsProjection,
    864     ),
    865     mutation(
    866         OperationId::TradeEvidenceRefresh,
    867         Risk::Medium,
    868         ApprovalRequirement::None,
    869         SignerRequirement::None,
    870         TransportRoute::local(),
    871         PrivacyEffect::PrivateCoordination,
    872         ProjectionEffect::WritesProjection,
    873     ),
    874     read(
    875         OperationId::TradeEvidenceInspect,
    876         Risk::Low,
    877         TransportRoute::local(),
    878         PrivacyEffect::PrivateCoordination,
    879         ProjectionEffect::ReadsProjection,
    880     ),
    881     mutation(
    882         OperationId::TradePrivateArtifactSeal,
    883         Risk::High,
    884         ApprovalRequirement::ConditionalOrRequiredByMode,
    885         SignerRequirement::None,
    886         TransportRoute::local(),
    887         PrivacyEffect::PrivateCoordination,
    888         ProjectionEffect::WritesProjection,
    889     ),
    890     read(
    891         OperationId::TradePrivateArtifactOpen,
    892         Risk::High,
    893         TransportRoute::local(),
    894         PrivacyEffect::PrivateCoordination,
    895         ProjectionEffect::ReadsProjection,
    896     ),
    897     mutation(
    898         OperationId::TradePrivateArtifactDelete,
    899         Risk::Critical,
    900         ApprovalRequirement::Required,
    901         SignerRequirement::None,
    902         TransportRoute::local(),
    903         PrivacyEffect::PrivateCoordination,
    904         ProjectionEffect::WritesProjection,
    905     ),
    906     read(
    907         OperationId::ValidationStatus,
    908         Risk::Low,
    909         TransportRoute::local(),
    910         PrivacyEffect::None,
    911         ProjectionEffect::ReadsProjection,
    912     ),
    913     read(
    914         OperationId::SyncStatus,
    915         Risk::Low,
    916         TransportRoute::diagnostics(),
    917         PrivacyEffect::None,
    918         ProjectionEffect::ReadsProjection,
    919     ),
    920     mutation(
    921         OperationId::SyncPull,
    922         Risk::Medium,
    923         ApprovalRequirement::None,
    924         SignerRequirement::ConditionalRelayAuth,
    925         TransportRoute::fetch(),
    926         PrivacyEffect::None,
    927         ProjectionEffect::MayUpdateProjection,
    928     ),
    929     mutation(
    930         OperationId::SyncPush,
    931         Risk::Medium,
    932         ApprovalRequirement::None,
    933         SignerRequirement::ConditionalRelayAuth,
    934         TransportRoute::delivery(),
    935         PrivacyEffect::PublicEvent,
    936         ProjectionEffect::MayUpdateProjection,
    937     ),
    938     read(
    939         OperationId::HealthInspect,
    940         Risk::Low,
    941         TransportRoute::diagnostics(),
    942         PrivacyEffect::None,
    943         ProjectionEffect::None,
    944     ),
    945     read(
    946         OperationId::TransportCapabilityList,
    947         Risk::Low,
    948         TransportRoute::diagnostics(),
    949         PrivacyEffect::None,
    950         ProjectionEffect::None,
    951     ),
    952     read(
    953         OperationId::TransportConfigInspect,
    954         Risk::Low,
    955         TransportRoute::local(),
    956         PrivacyEffect::PrivateStore,
    957         ProjectionEffect::ReadsProjection,
    958     ),
    959     mutation(
    960         OperationId::TransportConfigUpdate,
    961         Risk::High,
    962         ApprovalRequirement::ConditionalOrRequiredByMode,
    963         SignerRequirement::None,
    964         TransportRoute::local(),
    965         PrivacyEffect::PrivateStore,
    966         ProjectionEffect::WritesProjection,
    967     ),
    968     read(
    969         OperationId::TransportStatusInspect,
    970         Risk::Low,
    971         TransportRoute::diagnostics(),
    972         PrivacyEffect::None,
    973         ProjectionEffect::ReadsProjection,
    974     ),
    975     read(
    976         OperationId::TransportDeliveryInspect,
    977         Risk::Low,
    978         TransportRoute::diagnostics(),
    979         PrivacyEffect::None,
    980         ProjectionEffect::ReadsProjection,
    981     ),
    982     mutation(
    983         OperationId::TransportDeliveryRetry,
    984         Risk::Medium,
    985         ApprovalRequirement::None,
    986         SignerRequirement::ConditionalRelayAuth,
    987         TransportRoute::delivery(),
    988         PrivacyEffect::PublicEvent,
    989         ProjectionEffect::MayUpdateProjection,
    990     ),
    991     read(
    992         OperationId::DiagnosticsInspect,
    993         Risk::Low,
    994         TransportRoute::diagnostics(),
    995         PrivacyEffect::None,
    996         ProjectionEffect::None,
    997     ),
    998 ];
    999 
   1000 /// Returns the descriptor for an exact operation identity.
   1001 pub fn operation_descriptor(operation_id: OperationId) -> Result<OperationDescriptor, Error> {
   1002     CATALOG
   1003         .iter()
   1004         .copied()
   1005         .find(|descriptor| descriptor.operation_id == operation_id)
   1006         .ok_or_else(|| Error::UnknownOperationId {
   1007             operation_id: operation_id.as_str().to_string(),
   1008         })
   1009 }
   1010 
   1011 /// Validates uniqueness, schema generation, and descriptor policy invariants.
   1012 pub fn validate_catalog(descriptors: &[OperationDescriptor]) -> Result<(), Error> {
   1013     let mut operation_ids = BTreeSet::new();
   1014     for descriptor in descriptors {
   1015         if !operation_ids.insert(descriptor.operation_id) {
   1016             return Err(Error::DuplicateOperationId {
   1017                 operation_id: descriptor.operation_id,
   1018             });
   1019         }
   1020         match (descriptor.mutability, descriptor.idempotency) {
   1021             (Mutability::Read, IdempotencyPolicy::Forbidden)
   1022             | (Mutability::Mutation, IdempotencyPolicy::RequiredUuidV7) => {}
   1023             _ => {
   1024                 return Err(Error::CatalogInvalid {
   1025                     message: format!(
   1026                         "operation {} has invalid idempotency policy",
   1027                         descriptor.operation_id.as_str()
   1028                     ),
   1029                 });
   1030             }
   1031         }
   1032         if descriptor.schema_version != OPERATION_SCHEMA_VERSION {
   1033             return Err(Error::UnsupportedOperationSchemaVersion {
   1034                 operation_id: descriptor.operation_id,
   1035                 version: descriptor.schema_version,
   1036             });
   1037         }
   1038     }
   1039 
   1040     for required in [
   1041         OperationId::TransportCapabilityList,
   1042         OperationId::TransportConfigInspect,
   1043         OperationId::TransportConfigUpdate,
   1044         OperationId::TransportStatusInspect,
   1045         OperationId::TransportDeliveryInspect,
   1046         OperationId::TransportDeliveryRetry,
   1047         OperationId::SyncStatus,
   1048         OperationId::SyncPull,
   1049         OperationId::SyncPush,
   1050         OperationId::DiagnosticsInspect,
   1051     ] {
   1052         if !operation_ids.contains(&required) {
   1053             return Err(Error::MissingRequiredOperation {
   1054                 operation_id: required,
   1055             });
   1056         }
   1057     }
   1058 
   1059     for delivery in [
   1060         OperationId::FarmPublish,
   1061         OperationId::ListingPublish,
   1062         OperationId::ListingPause,
   1063         OperationId::ListingWithdraw,
   1064         OperationId::TradeProposalSubmit,
   1065         OperationId::TradeRevisionPropose,
   1066         OperationId::TradeCandidateDecide,
   1067         OperationId::TradeCancellationSubmit,
   1068         OperationId::TradeOperationResume,
   1069         OperationId::SyncPush,
   1070         OperationId::TransportDeliveryRetry,
   1071     ] {
   1072         let descriptor = descriptors
   1073             .iter()
   1074             .find(|descriptor| descriptor.operation_id == delivery)
   1075             .ok_or(Error::MissingRequiredOperation {
   1076                 operation_id: delivery,
   1077             })?;
   1078         if !descriptor.transport_capability.deliver
   1079             || !descriptor
   1080                 .transport_capability
   1081                 .includes_transport(TransportKind::NOSTR)
   1082             || !descriptor
   1083                 .transport_capability
   1084                 .includes_transport(TransportKind::RETICULUM)
   1085         {
   1086             return Err(Error::CatalogInvalid {
   1087                 message: format!(
   1088                     "operation {} must use Nostr and Reticulum delivery capability",
   1089                     descriptor.operation_id.as_str()
   1090                 ),
   1091             });
   1092         }
   1093     }
   1094     Ok(())
   1095 }
   1096 
   1097 /// Builds the registry for every exact request and receipt schema identity.
   1098 pub fn schema_registry() -> Result<Registry, crate::schema::Error> {
   1099     let mut schemas = Vec::with_capacity(CATALOG.len() * 2);
   1100     for descriptor in CATALOG {
   1101         schemas.push(SchemaDescriptor::try_new(
   1102             descriptor.request_schema_id(),
   1103             ModuleVersion::RuntimeV1,
   1104         )?);
   1105         schemas.push(SchemaDescriptor::try_new(
   1106             descriptor.receipt_schema_id(),
   1107             ModuleVersion::RuntimeV1,
   1108         )?);
   1109     }
   1110     Registry::try_new(schemas)
   1111 }
   1112 
   1113 /// Runtime operation catalog validation failure.
   1114 #[derive(Clone, Debug, Eq, PartialEq)]
   1115 #[non_exhaustive]
   1116 pub enum Error {
   1117     /// An operation identity appears more than once.
   1118     DuplicateOperationId {
   1119         /// Duplicated operation identity.
   1120         operation_id: OperationId,
   1121     },
   1122     /// A required operation is missing from a candidate catalog.
   1123     MissingRequiredOperation {
   1124         /// Missing operation identity.
   1125         operation_id: OperationId,
   1126     },
   1127     /// An operation identity is unknown.
   1128     UnknownOperationId {
   1129         /// Rejected identity.
   1130         operation_id: String,
   1131     },
   1132     /// An operation declares a non-V1 schema generation.
   1133     UnsupportedOperationSchemaVersion {
   1134         /// Operation with the incompatible version.
   1135         operation_id: OperationId,
   1136         /// Rejected version.
   1137         version: u16,
   1138     },
   1139     /// A descriptor invariant is violated.
   1140     CatalogInvalid {
   1141         /// Secret-safe validation diagnostic.
   1142         message: String,
   1143     },
   1144 }
   1145 
   1146 impl fmt::Display for Error {
   1147     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
   1148         match self {
   1149             Self::DuplicateOperationId { operation_id } => {
   1150                 write!(
   1151                     formatter,
   1152                     "duplicate operation id {}",
   1153                     operation_id.as_str()
   1154                 )
   1155             }
   1156             Self::MissingRequiredOperation { operation_id } => {
   1157                 write!(
   1158                     formatter,
   1159                     "missing required operation {}",
   1160                     operation_id.as_str()
   1161                 )
   1162             }
   1163             Self::UnknownOperationId { operation_id } => {
   1164                 write!(formatter, "unknown operation id {operation_id}")
   1165             }
   1166             Self::UnsupportedOperationSchemaVersion {
   1167                 operation_id,
   1168                 version,
   1169             } => write!(
   1170                 formatter,
   1171                 "unsupported operation schema version {version} for {}",
   1172                 operation_id.as_str()
   1173             ),
   1174             Self::CatalogInvalid { message } => formatter.write_str(message),
   1175         }
   1176     }
   1177 }
   1178 
   1179 #[cfg(feature = "std")]
   1180 impl std::error::Error for Error {}
   1181 
   1182 #[cfg(test)]
   1183 mod tests {
   1184     use super::*;
   1185 
   1186     #[test]
   1187     fn catalog_is_unique_and_preserves_every_v1_operation() {
   1188         validate_catalog(CATALOG).expect("runtime operation catalog");
   1189         assert_eq!(CATALOG.len(), 57);
   1190 
   1191         let identities = CATALOG
   1192             .iter()
   1193             .map(|descriptor| descriptor.operation_id.as_str())
   1194             .collect::<BTreeSet<_>>();
   1195         assert_eq!(identities.len(), CATALOG.len());
   1196         for descriptor in CATALOG {
   1197             assert_eq!(
   1198                 OperationId::parse(descriptor.operation_id.as_str()),
   1199                 Ok(descriptor.operation_id)
   1200             );
   1201             assert!(
   1202                 descriptor
   1203                     .request_schema_id()
   1204                     .starts_with("radroots.runtime.")
   1205             );
   1206             assert!(descriptor.request_schema_id().ends_with(".request.v1"));
   1207             assert!(
   1208                 descriptor
   1209                     .receipt_schema_id()
   1210                     .starts_with("radroots.runtime.")
   1211             );
   1212             assert!(descriptor.receipt_schema_id().ends_with(".receipt.v1"));
   1213             assert_eq!(
   1214                 operation_descriptor(descriptor.operation_id),
   1215                 Ok(*descriptor)
   1216             );
   1217         }
   1218     }
   1219 
   1220     #[test]
   1221     fn parser_is_exact_and_rejects_retired_preview_names() {
   1222         assert_eq!(
   1223             OperationId::parse("trade.proposal.submit"),
   1224             Ok(OperationId::TradeProposalSubmit)
   1225         );
   1226         assert_eq!(
   1227             OperationId::parse("runtime.unknown")
   1228                 .expect_err("unknown")
   1229                 .to_string(),
   1230             "unknown operation id runtime.unknown"
   1231         );
   1232         for value in [
   1233             ["sync.try_reticulum", "_preview_now"].concat(),
   1234             ["transport.reticulum", "_preview.status"].concat(),
   1235             ["transport.", "hybrid", ".publish"].concat(),
   1236             ["radrootsd.", "proxy", ".publish"].concat(),
   1237         ] {
   1238             assert!(OperationId::parse(value.as_str()).is_err());
   1239         }
   1240     }
   1241 
   1242     #[test]
   1243     fn schema_ids_and_registry_preserve_v1_vectors() {
   1244         let profile = operation_descriptor(OperationId::ProfileInspect).expect("profile");
   1245         assert_eq!(
   1246             profile.request_schema_id(),
   1247             "radroots.runtime.profile.inspect.request.v1"
   1248         );
   1249         assert_eq!(
   1250             profile.receipt_schema_id(),
   1251             "radroots.runtime.profile.inspect.receipt.v1"
   1252         );
   1253 
   1254         let registry = schema_registry().expect("runtime schema registry");
   1255         assert_eq!(registry.len(), CATALOG.len() * 2);
   1256         assert!(
   1257             registry
   1258                 .descriptors()
   1259                 .iter()
   1260                 .all(|descriptor| descriptor.module() == ModuleVersion::RuntimeV1)
   1261         );
   1262     }
   1263 
   1264     #[test]
   1265     fn validation_rejects_duplicates_and_policy_drift() {
   1266         assert_eq!(
   1267             validate_catalog(&[CATALOG[0], CATALOG[0]]),
   1268             Err(Error::DuplicateOperationId {
   1269                 operation_id: OperationId::ProfileInspect,
   1270             })
   1271         );
   1272 
   1273         let mut invalid = CATALOG.to_vec();
   1274         invalid[0].idempotency = IdempotencyPolicy::RequiredUuidV7;
   1275         assert_eq!(
   1276             validate_catalog(invalid.as_slice()),
   1277             Err(Error::CatalogInvalid {
   1278                 message: "operation profile.inspect has invalid idempotency policy".into(),
   1279             })
   1280         );
   1281 
   1282         let mut invalid = CATALOG.to_vec();
   1283         invalid[1].idempotency = IdempotencyPolicy::Forbidden;
   1284         assert!(matches!(
   1285             validate_catalog(&invalid),
   1286             Err(Error::CatalogInvalid { .. })
   1287         ));
   1288 
   1289         let mut invalid = CATALOG.to_vec();
   1290         invalid[0].schema_version = 2;
   1291         assert_eq!(
   1292             validate_catalog(&invalid),
   1293             Err(Error::UnsupportedOperationSchemaVersion {
   1294                 operation_id: OperationId::ProfileInspect,
   1295                 version: 2,
   1296             })
   1297         );
   1298 
   1299         for required in [
   1300             OperationId::TransportCapabilityList,
   1301             OperationId::TransportConfigInspect,
   1302             OperationId::TransportConfigUpdate,
   1303             OperationId::TransportStatusInspect,
   1304             OperationId::TransportDeliveryInspect,
   1305             OperationId::TransportDeliveryRetry,
   1306             OperationId::SyncStatus,
   1307             OperationId::SyncPull,
   1308             OperationId::SyncPush,
   1309             OperationId::DiagnosticsInspect,
   1310         ] {
   1311             let missing = CATALOG
   1312                 .iter()
   1313                 .copied()
   1314                 .filter(|descriptor| descriptor.operation_id != required)
   1315                 .collect::<Vec<_>>();
   1316             assert_eq!(
   1317                 validate_catalog(&missing),
   1318                 Err(Error::MissingRequiredOperation {
   1319                     operation_id: required,
   1320                 })
   1321             );
   1322         }
   1323 
   1324         for delivery in [
   1325             OperationId::FarmPublish,
   1326             OperationId::ListingPublish,
   1327             OperationId::ListingPause,
   1328             OperationId::ListingWithdraw,
   1329             OperationId::TradeProposalSubmit,
   1330             OperationId::TradeRevisionPropose,
   1331             OperationId::TradeCandidateDecide,
   1332             OperationId::TradeCancellationSubmit,
   1333             OperationId::TradeOperationResume,
   1334         ] {
   1335             let missing = CATALOG
   1336                 .iter()
   1337                 .copied()
   1338                 .filter(|descriptor| descriptor.operation_id != delivery)
   1339                 .collect::<Vec<_>>();
   1340             assert_eq!(
   1341                 validate_catalog(&missing),
   1342                 Err(Error::MissingRequiredOperation {
   1343                     operation_id: delivery,
   1344                 })
   1345             );
   1346         }
   1347 
   1348         let mut invalid = CATALOG.to_vec();
   1349         let delivery = invalid
   1350             .iter_mut()
   1351             .find(|descriptor| descriptor.operation_id == OperationId::FarmPublish)
   1352             .expect("delivery descriptor");
   1353         delivery.transport_capability.deliver = false;
   1354         assert!(matches!(
   1355             validate_catalog(&invalid),
   1356             Err(Error::CatalogInvalid { .. })
   1357         ));
   1358     }
   1359 
   1360     #[test]
   1361     fn route_constructors_and_errors_cover_all_variants() {
   1362         let none = TransportRoute::none();
   1363         assert!(!none.includes_transport(TransportKind::LOCAL));
   1364         assert!(!none.includes_transport(TransportKind::NOSTR));
   1365         assert!(!none.includes_transport(TransportKind::RETICULUM));
   1366         assert!(!none.includes_transport(TransportKind::parse("future").expect("custom")));
   1367         assert!(TransportRoute::local().includes_transport(TransportKind::LOCAL));
   1368         assert!(TransportRoute::delivery().includes_transport(TransportKind::NOSTR));
   1369         assert!(TransportRoute::delivery().includes_transport(TransportKind::RETICULUM));
   1370         assert!(TransportRoute::fetch().fetch);
   1371         assert!(TransportRoute::diagnostics().diagnostics);
   1372 
   1373         let errors = [
   1374             Error::DuplicateOperationId {
   1375                 operation_id: OperationId::ProfileInspect,
   1376             },
   1377             Error::MissingRequiredOperation {
   1378                 operation_id: OperationId::SyncStatus,
   1379             },
   1380             Error::UnknownOperationId {
   1381                 operation_id: "unknown".to_owned(),
   1382             },
   1383             Error::UnsupportedOperationSchemaVersion {
   1384                 operation_id: OperationId::SyncStatus,
   1385                 version: 2,
   1386             },
   1387             Error::CatalogInvalid {
   1388                 message: "invalid catalog".to_owned(),
   1389             },
   1390         ];
   1391         for error in errors {
   1392             assert!(!error.to_string().is_empty());
   1393         }
   1394 
   1395         let invalid = SyncStatusReceipt {
   1396             schema_version: 2,
   1397             health: SyncHealth::Unavailable,
   1398             storage: SyncCapabilityState::Unsupported,
   1399             source: SyncCapabilityState::Compiled,
   1400             sink: SyncCapabilityState::Configured,
   1401             signer: SyncCapabilityState::Degraded,
   1402             outbox: SyncOutboxStatus {
   1403                 pending: 0,
   1404                 leased: 0,
   1405                 retryable: 0,
   1406                 satisfied: 0,
   1407                 exhausted: 0,
   1408             },
   1409             projections: SyncProjectionStatus {
   1410                 ready: 0,
   1411                 invalidated: 0,
   1412                 rebuilding: 0,
   1413                 failed: 0,
   1414                 untracked: 0,
   1415             },
   1416         };
   1417         assert_eq!(
   1418             invalid.validate(),
   1419             Err(Error::UnsupportedOperationSchemaVersion {
   1420                 operation_id: OperationId::SyncStatus,
   1421                 version: 2,
   1422             })
   1423         );
   1424     }
   1425 
   1426     #[cfg(feature = "serde")]
   1427     #[test]
   1428     fn serialized_descriptor_vector_is_exact() {
   1429         let descriptor = operation_descriptor(OperationId::ProfileInspect).expect("profile");
   1430         assert_eq!(
   1431             serde_json::to_value(descriptor).expect("descriptor JSON"),
   1432             serde_json::json!({
   1433                 "operation_id": "profile.inspect",
   1434                 "schema_version": 1,
   1435                 "mutability": "read",
   1436                 "risk": "low",
   1437                 "approval": "none",
   1438                 "signer": "none",
   1439                 "transport_capability": {
   1440                     "local": true,
   1441                     "nostr": false,
   1442                     "reticulum": false,
   1443                     "deliver": false,
   1444                     "fetch": false,
   1445                     "synchronize": false,
   1446                     "diagnostics": false
   1447                 },
   1448                 "idempotency": "forbidden",
   1449                 "dry_run": "not_applicable",
   1450                 "deadline": "default_bounded",
   1451                 "privacy": "private_store",
   1452                 "projection": "reads_projection",
   1453                 "maturity": "stable"
   1454             })
   1455         );
   1456         assert_eq!(
   1457             serde_json::to_string(&OperationId::ProfileInspect).expect("operation JSON"),
   1458             "\"profile.inspect\""
   1459         );
   1460     }
   1461 
   1462     #[cfg(feature = "serde")]
   1463     #[test]
   1464     fn sync_status_receipt_is_typed_and_rejects_wire_drift() {
   1465         let receipt = SyncStatusReceipt {
   1466             schema_version: OPERATION_SCHEMA_VERSION,
   1467             health: SyncHealth::Degraded,
   1468             storage: SyncCapabilityState::Available,
   1469             source: SyncCapabilityState::Available,
   1470             sink: SyncCapabilityState::Degraded,
   1471             signer: SyncCapabilityState::Configured,
   1472             outbox: SyncOutboxStatus {
   1473                 pending: 1,
   1474                 leased: 2,
   1475                 retryable: 3,
   1476                 satisfied: 4,
   1477                 exhausted: 5,
   1478             },
   1479             projections: SyncProjectionStatus {
   1480                 ready: 6,
   1481                 invalidated: 7,
   1482                 rebuilding: 8,
   1483                 failed: 9,
   1484                 untracked: 10,
   1485             },
   1486         };
   1487         receipt.validate().expect("status receipt");
   1488         let value = serde_json::to_value(receipt).expect("status JSON");
   1489         assert_eq!(value["source"], "available");
   1490         assert_eq!(value["outbox"]["retryable"], 3);
   1491         assert_eq!(value["projections"]["untracked"], 10);
   1492         assert_eq!(
   1493             serde_json::from_value::<SyncStatusReceipt>(value.clone()).expect("status decode"),
   1494             receipt
   1495         );
   1496         let mut invalid_version = value.clone();
   1497         invalid_version["schema_version"] = 2.into();
   1498         assert!(serde_json::from_value::<SyncStatusReceipt>(invalid_version).is_err());
   1499         let mut unknown = value;
   1500         unknown["unknown"] = true.into();
   1501         assert!(serde_json::from_value::<SyncStatusReceipt>(unknown).is_err());
   1502     }
   1503 }