commit e717bc7c190674f18d696b9b509d911d33888f07
parent f7a7d8bf62211ac090c922ba0a917bf3548c5b2f
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 11:59:31 +0000
sdk: rename public SDK types and remove representation coupling
- lock contextual native type names across supported feature surfaces
- reject public native struct fields in every active SDK module
- document the breaking constructor and accessor migration contract
- prove representation privacy with compile and compile-fail rustdoc examples
Diffstat:
4 files changed, 185 insertions(+), 0 deletions(-)
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -93,5 +93,9 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
name = "package_boundary"
path = "tests/package_boundary.rs"
+[[test]]
+name = "public_api"
+path = "tests/public_api.rs"
+
[lints]
workspace = true
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -3,6 +3,25 @@
//! Advanced hosts compose capabilities through [`ClientBuilder`] and operate
//! through a cloneable [`Client`]. All fallible root operations use [`Result`]
//! and the SDK-owned [`Error`] boundary.
+//!
+//! Constructing an empty builder performs no I/O and makes missing composition
+//! explicit:
+//!
+//! ```
+//! use radroots_sdk::ClientBuilder;
+//!
+//! let result = ClientBuilder::new().build();
+//! assert!(result.is_err());
+//! ```
+//!
+//! Native structs are constructor-led and representation-private. Hosts must
+//! not depend on field layout:
+//!
+//! ```compile_fail
+//! use radroots_sdk::ClientBuilder;
+//!
+//! let _ = ClientBuilder { storage: None };
+//! ```
#![forbid(unsafe_code)]
diff --git a/crates/sdk/tests/public_api.rs b/crates/sdk/tests/public_api.rs
@@ -0,0 +1,136 @@
+use std::{any::type_name, collections::BTreeSet};
+
+const ACTIVE_MODULES: &[(&str, &str)] = &[
+ ("capability", include_str!("../src/capability.rs")),
+ ("client", include_str!("../src/client.rs")),
+ ("diagnostics", include_str!("../src/diagnostics.rs")),
+ ("error", include_str!("../src/error.rs")),
+ ("farm", include_str!("../src/farm.rs")),
+ ("listing", include_str!("../src/listing.rs")),
+ ("signing", include_str!("../src/signing.rs")),
+ ("storage", include_str!("../src/storage.rs")),
+ ("sync", include_str!("../src/sync.rs")),
+ ("trade", include_str!("../src/trade.rs")),
+ ("transport", include_str!("../src/transport.rs")),
+];
+
+#[test]
+fn public_native_type_snapshot_uses_contextual_names() {
+ let actual = BTreeSet::from([
+ type_name::<radroots_sdk::Client>(),
+ type_name::<radroots_sdk::ClientBuilder>(),
+ type_name::<radroots_sdk::capability::Availability>(),
+ type_name::<radroots_sdk::capability::CapabilityId>(),
+ type_name::<radroots_sdk::capability::CapabilityReport>(),
+ type_name::<radroots_sdk::capability::CapabilityStatus>(),
+ type_name::<radroots_sdk::capability::Maturity>(),
+ type_name::<radroots_sdk::diagnostics::Report>(),
+ type_name::<radroots_sdk::error::Error>(),
+ type_name::<radroots_sdk::error::ErrorDescriptor>(),
+ type_name::<radroots_sdk::error::ErrorKind>(),
+ type_name::<radroots_sdk::farm::Plan>(),
+ type_name::<radroots_sdk::farm::PrepareError>(),
+ type_name::<radroots_sdk::farm::PrepareErrorKind>(),
+ type_name::<radroots_sdk::farm::PrepareRequest>(),
+ type_name::<radroots_sdk::listing::Action>(),
+ type_name::<radroots_sdk::listing::Plan>(),
+ type_name::<radroots_sdk::listing::PrepareError>(),
+ type_name::<radroots_sdk::listing::PrepareErrorKind>(),
+ type_name::<radroots_sdk::listing::PrepareRequest>(),
+ type_name::<radroots_sdk::signing::Mode>(),
+ type_name::<radroots_sdk::signing::Provider>(),
+ type_name::<radroots_sdk::storage::Operations<'static>>(),
+ type_name::<radroots_sdk::trade::Plan>(),
+ type_name::<radroots_sdk::trade::PrepareError>(),
+ type_name::<radroots_sdk::trade::PrepareErrorKind>(),
+ type_name::<radroots_sdk::trade::PrepareRequest>(),
+ type_name::<radroots_sdk::transport::Profile>(),
+ ]);
+ #[cfg(feature = "sync")]
+ let actual = actual
+ .into_iter()
+ .chain([
+ type_name::<radroots_sdk::farm::EnqueueRequest>(),
+ type_name::<radroots_sdk::farm::Operations<'static>>(),
+ type_name::<radroots_sdk::listing::EnqueueRequest>(),
+ type_name::<radroots_sdk::listing::Operations<'static>>(),
+ type_name::<radroots_sdk::sync::Operations<'static>>(),
+ type_name::<radroots_sdk::trade::EnqueueRequest>(),
+ type_name::<radroots_sdk::trade::Operations<'static>>(),
+ type_name::<radroots_sdk::trade::PrivateTermsError>(),
+ ])
+ .collect::<BTreeSet<_>>();
+ #[cfg(feature = "radrootsd")]
+ let actual = actual
+ .into_iter()
+ .chain([
+ type_name::<radroots_sdk::transport::DaemonAuth>(),
+ type_name::<radroots_sdk::transport::DaemonConfig>(),
+ type_name::<radroots_sdk::transport::DaemonDelivery>(),
+ type_name::<radroots_sdk::transport::DaemonError>(),
+ type_name::<radroots_sdk::transport::DaemonErrorKind>(),
+ ])
+ .collect::<BTreeSet<_>>();
+
+ assert!(actual.iter().all(|name| !name.contains("RadrootsSdk")));
+ assert!(actual.iter().all(|name| {
+ name.rsplit("::")
+ .next()
+ .is_some_and(|item| !item.starts_with("Sdk"))
+ }));
+ assert_eq!(actual.len(), expected_public_type_count());
+}
+
+#[test]
+fn active_native_structs_are_field_layout_independent() {
+ for (module, source) in ACTIVE_MODULES {
+ for line in source.lines() {
+ let line = line.trim_start();
+ for declaration in ["pub struct ", "pub enum ", "pub trait ", "pub type "] {
+ if let Some(item) = line.strip_prefix(declaration) {
+ let item = item
+ .split(|character: char| {
+ character == '<'
+ || character == '('
+ || character == '{'
+ || character == ';'
+ || character.is_whitespace()
+ })
+ .next()
+ .expect("public item name");
+ assert!(
+ !item.starts_with("RadrootsSdk") && !item.starts_with("Sdk"),
+ "{module} exposes representation-prefixed item {item}"
+ );
+ }
+ }
+
+ let public_field = line.starts_with("pub ")
+ && line.contains(':')
+ && ![
+ "pub async fn ",
+ "pub const ",
+ "pub enum ",
+ "pub fn ",
+ "pub mod ",
+ "pub static ",
+ "pub struct ",
+ "pub trait ",
+ "pub type ",
+ "pub use ",
+ ]
+ .iter()
+ .any(|prefix| line.starts_with(prefix));
+ assert!(!public_field, "{module} exposes native field `{line}`");
+ }
+ }
+}
+
+const fn expected_public_type_count() -> usize {
+ let count = 28;
+ #[cfg(feature = "sync")]
+ let count = count + 8;
+ #[cfg(feature = "radrootsd")]
+ let count = count + 5;
+ count
+}
diff --git a/docs/engineering/sdk-native-api-migration.md b/docs/engineering/sdk-native-api-migration.md
@@ -0,0 +1,26 @@
+# SDK native API migration
+
+The Release V1 SDK intentionally makes a breaking cut from the predecessor
+representation-shaped API. Native Rust callers must use module context,
+constructors, builders, and accessors rather than compatibility aliases or
+public field layout.
+
+| Predecessor pattern | Release V1 API |
+| --- | --- |
+| `RadrootsClient` | `radroots_sdk::Client` |
+| `RadrootsClientBuilder` | `radroots_sdk::ClientBuilder` |
+| `RadrootsSdkError` | `radroots_sdk::Error` |
+| `RadrootsSdk*` or `Sdk*` product wrappers | contextual types such as `farm::Plan`, `listing::PrepareRequest`, and `trade::Operations` |
+| SDK copies of event, trade, storage, sync, or transport values | the canonical type from its owning lower crate |
+| struct literals over SDK request, plan, receipt, status, or error fields | the type's constructor or builder plus stable accessors |
+
+There are no deprecated prefixed aliases. Code that previously projected SDK
+fields directly into Studio or another host must instead translate from stable
+accessors at that host boundary. This prevents additive native fields from
+causing field-skew failures in consumer struct patterns and keeps private
+storage, transport, and signer representations replaceable.
+
+The crate root exports only `Client`, `ClientBuilder`, `Error`, and `Result`.
+Advanced types remain under their owning modules. Deliberately passive,
+versioned wire DTOs remain owned by `radroots_protocol`; the SDK does not copy
+their public fields into native wrapper structs.