sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit b7cf74e494e703864fefd4a873b1177341c31721
parent d6f332bfde2eed006a3de72f0105d6816d676737
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 12:09:28 +0000

sdk: complete package conformance coverage

- align static CI validation with the final capability feature graph
- add a clean external Rust host smoke over final SDK APIs
- reject private dependencies, SDK-owned traits, and public field layout
- qualify every feature, target, package, rustdoc, and architecture lane

Diffstat:
Mcrates/sdk/tests/package_boundary.rs | 14++++++++++++++
Mcrates/sdk/tests/public_api.rs | 6+++++-
Adocs/engineering/sdk-package-conformance.md | 31+++++++++++++++++++++++++++++++
Mtools/xtask/src/check.rs | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mtools/xtask/src/main.rs | 6+++---
Mtools/xtask/src/smoke.rs | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
6 files changed, 225 insertions(+), 36 deletions(-)

diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs @@ -40,6 +40,20 @@ fn manifest_has_final_identity_and_dependency_boundary() { "radroots_transport_nostr", ]); assert_eq!(dependencies, expected); + for forbidden in [ + "radroots_event_store", + "radroots_nostr_signer", + "radroots_outbox", + "radroots_protected_store", + "radroots_runtime_paths", + "radroots_secret_vault", + "radroots_transport_reticulum", + ] { + assert!( + !dependencies.contains(forbidden), + "SDK depends on private or superseded package `{forbidden}`" + ); + } } #[test] diff --git a/crates/sdk/tests/public_api.rs b/crates/sdk/tests/public_api.rs @@ -82,10 +82,14 @@ fn public_native_type_snapshot_uses_contextual_names() { } #[test] -fn active_native_structs_are_field_layout_independent() { +fn active_native_api_has_no_sdk_owned_traits_or_public_field_layout() { for (module, source) in ACTIVE_MODULES { for line in source.lines() { let line = line.trim_start(); + assert!( + !line.starts_with("pub trait "), + "{module} must reuse lower host SPIs instead of exposing an SDK-owned trait" + ); for declaration in ["pub struct ", "pub enum ", "pub trait ", "pub type "] { if let Some(item) = line.strip_prefix(declaration) { let item = item diff --git a/docs/engineering/sdk-package-conformance.md b/docs/engineering/sdk-package-conformance.md @@ -0,0 +1,31 @@ +# SDK package conformance + +`radroots_sdk` is qualified as a standalone advanced-host package with the +repository-owned checks below. Run every command through the configured build +output router. + +The package matrix contains no-default, default, every public feature in +isolation, `native`, `full`, and all-features, always with all targets. Strict +Clippy covers the no-default and all-feature endpoints. Package tests cover +safe defaults, lifecycle, product planning and commits, native errors, public +API shape, dependency boundaries, and feature law. Rustdoc is checked and +tested with all features. + +The clean-host smoke command is: + +```sh +cargo xtask smoke sdk-rust-local +``` + +It creates a temporary external Cargo application, depends on `radroots_sdk` +through its package path, supplies migration-only local patches for the 17 +lower public packages, and compiles against only the final `ClientBuilder`, +`ErrorKind`, and fail-closed construction surface. It does not rely on a +workspace member, private SDK module, legacy feature, or compatibility alias. +Package-realistic registry and extracted-crate qualification remains owned by +the later release-validation sequence while publication is frozen. + +`cargo xtask architecture-ci` statically validates the same exact feature +vocabulary and activation graph. The public API tests reject SDK-owned host +traits, prefixed native types, public native struct fields, private lower +package dependencies, broad root reexports, and implementation-type leakage. diff --git a/tools/xtask/src/check.rs b/tools/xtask/src/check.rs @@ -552,27 +552,75 @@ fn check_sdk_feature_matrix(root: &Path) -> Result<(), String> { .get("features") .and_then(toml::Value::as_table) .ok_or_else(|| format!("{} must define [features]", path.display()))?; - let runtime = feature_entries(features, "runtime")?; - for entry in [ - "dep:radroots_transport_reticulum", - "dep:radroots_transport", - "dep:radroots_outbox", - ] { - require_feature_entry(&runtime, "runtime", entry)?; + let expected = BTreeSet::from([ + "default", + "full", + "geonames", + "knowledge", + "local-signing", + "memory", + "native", + "nip46", + "nostr", + "radrootsd", + "sqlite", + "sync", + ]); + let actual = features.keys().map(String::as_str).collect::<BTreeSet<_>>(); + if actual != expected { + return Err(format!( + "crates/sdk/Cargo.toml feature vocabulary mismatch: expected {expected:?}, found {actual:?}" + )); } - let nostr_runtime = feature_entries(features, "transport-nostr-runtime")?; - for entry in [ - "runtime", - "dep:radroots_nostr", - "radroots_transport_nostr/client", + for (feature, expected_entries) in [ + ("default", &["memory"][..]), + ("memory", &["radroots_storage/memory"]), + ("sqlite", &["dep:radroots_storage_sqlite"]), + ("sync", &["dep:radroots_sync"]), + ( + "nostr", + &["sync", "dep:radroots_nostr", "dep:radroots_transport_nostr"], + ), + ("nip46", &["nostr", "dep:radroots_nostr_connect"]), + ( + "local-signing", + &[ + "dep:radroots_nostr", + "dep:radroots_secrets", + "radroots_nostr/signing", + ], + ), + ( + "radrootsd", + &["sync", "dep:reqwest", "dep:serde", "dep:serde_json"], + ), + ("geonames", &["dep:radroots_geonames"]), + ( + "knowledge", + &["radroots_event/knowledge", "radroots_event_codec/knowledge"], + ), + ("native", &["sqlite", "sync", "local-signing"]), + ( + "full", + &[ + "native", + "nostr", + "nip46", + "radrootsd", + "geonames", + "knowledge", + ], + ), ] { - require_feature_entry(&nostr_runtime, "transport-nostr-runtime", entry)?; - } - if features.contains_key("transport-reticulum-preview") { - return Err( - "crates/sdk/Cargo.toml must not introduce transport-reticulum-preview as a runtime-owned Reticulum preview feature alias" - .to_owned(), - ); + let actual_entries = feature_entries(features, feature)? + .into_iter() + .collect::<BTreeSet<_>>(); + let expected_entries = expected_entries.iter().copied().collect::<BTreeSet<_>>(); + if actual_entries != expected_entries { + return Err(format!( + "crates/sdk/Cargo.toml feature `{feature}` mismatch: expected {expected_entries:?}, found {actual_entries:?}" + )); + } } check_sdk_workspace_reticulum_dependency_boundaries(root)?; Ok(()) @@ -830,16 +878,6 @@ fn feature_entries<'features>( .collect() } -fn require_feature_entry(entries: &[&str], feature: &str, entry: &str) -> Result<(), String> { - if entries.contains(&entry) { - Ok(()) - } else { - Err(format!( - "crates/sdk/Cargo.toml feature `{feature}` must include `{entry}`" - )) - } -} - fn check_package_source_metadata(root: &Path) -> Result<(), String> { for spec in package_specs() { let package_dir = root.join(spec.package_dir); diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -90,7 +90,7 @@ fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> { } fn usage() -> String { - "usage: cargo xtask architecture | cargo xtask architecture-ci | cargo xtask check-api-boundaries | cargo xtask check-dependency-boundaries | cargo xtask generate | cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke knowledge-rust-local | cargo xtask coverage run" + "usage: cargo xtask architecture | cargo xtask architecture-ci | cargo xtask check-api-boundaries | cargo xtask check-dependency-boundaries | cargo xtask generate | cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke sdk-rust-local | cargo xtask smoke knowledge-rust-local | cargo xtask coverage run" .to_owned() } @@ -181,10 +181,10 @@ mod tests { #[test] fn accepts_smoke() { - let args = ["smoke".to_owned(), "knowledge-rust-local".to_owned()]; + let args = ["smoke".to_owned(), "sdk-rust-local".to_owned()]; assert!(matches!( command_action(&args).expect("action"), - CommandAction::Smoke(rest) if rest == ["knowledge-rust-local"] + CommandAction::Smoke(rest) if rest == ["sdk-rust-local"] )); } diff --git a/tools/xtask/src/smoke.rs b/tools/xtask/src/smoke.rs @@ -7,11 +7,85 @@ use crate::{ pub fn run(args: &[String]) -> Result<(), String> { match args { + [target] if target == "sdk-rust-local" => sdk_rust_local(), [target] if target == "knowledge-rust-local" => knowledge_rust_local(), - _ => Err("usage: cargo xtask smoke knowledge-rust-local".to_owned()), + _ => Err( + "usage: cargo xtask smoke sdk-rust-local | cargo xtask smoke knowledge-rust-local" + .to_owned(), + ), } } +fn sdk_rust_local() -> Result<(), String> { + let root = workspace_root()?; + let sdk_path = root.join("crates/sdk"); + let lib_root = root + .parent() + .ok_or_else(|| format!("{} has no repository parent", root.display()))? + .join("lib"); + let toolchain = resolve_rust_toolchain(&root)?; + let tempdir = + tempfile::tempdir().map_err(|error| format!("failed to create smoke tempdir: {error}"))?; + let manifest = render_sdk_consumer_manifest(&sdk_path, &lib_root)?; + fs::write(tempdir.path().join("Cargo.toml"), manifest) + .map_err(|error| format!("failed to write SDK smoke manifest: {error}"))?; + let src_dir = tempdir.path().join("src"); + fs::create_dir_all(&src_dir) + .map_err(|error| format!("failed to create {}: {error}", src_dir.display()))?; + fs::write(src_dir.join("main.rs"), SDK_CONSUMER_MAIN) + .map_err(|error| format!("failed to write SDK smoke consumer: {error}"))?; + let status = smoke_cargo_check_command(tempdir.path(), &toolchain) + .status() + .map_err(|error| format!("failed to run SDK smoke cargo check: {error}"))?; + if status.success() { + Ok(()) + } else { + Err(format!("SDK Rust local smoke failed with status {status}")) + } +} + +fn render_sdk_consumer_manifest(sdk_path: &Path, lib_root: &Path) -> Result<String, String> { + let sdk_path = serde_json::to_string(&sdk_path.to_string_lossy()) + .map_err(|error| format!("failed to render SDK path: {error}"))?; + let mut manifest = format!( + r#"[package] +name = "radroots_sdk_host_smoke" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +radroots_sdk = {{ path = {sdk_path}, default-features = true }} + +[patch.crates-io] +"#, + ); + for (package, relative_path) in [ + ("radroots_blossom", "crates/blossom"), + ("radroots_core", "crates/core"), + ("radroots_event", "crates/event"), + ("radroots_event_codec", "crates/event_codec"), + ("radroots_geonames", "crates/geonames"), + ("radroots_identity", "crates/identity"), + ("radroots_nostr", "crates/nostr"), + ("radroots_nostr_connect", "crates/nostr_connect"), + ("radroots_protocol", "crates/protocol"), + ("radroots_secrets", "crates/secrets"), + ("radroots_signing", "crates/signing"), + ("radroots_storage", "crates/storage"), + ("radroots_storage_sqlite", "crates/storage_sqlite"), + ("radroots_sync", "crates/sync"), + ("radroots_trade", "crates/trade"), + ("radroots_transport", "crates/transport"), + ("radroots_transport_nostr", "crates/transport_nostr"), + ] { + let path = serde_json::to_string(&lib_root.join(relative_path).to_string_lossy()) + .map_err(|error| format!("failed to render {package} patch path: {error}"))?; + manifest.push_str(&format!("{package} = {{ path = {path} }}\n")); + } + Ok(manifest) +} + fn knowledge_rust_local() -> Result<(), String> { let root = workspace_root()?; let sdk_path = root.join("crates/sdk"); @@ -207,6 +281,17 @@ fn claim_builder() -> RadrootsKnowledgeClaimBuilder { } "#; +const SDK_CONSUMER_MAIN: &str = r#"use radroots_sdk::{ClientBuilder, error::ErrorKind}; + +fn main() { + let error = match ClientBuilder::new().build() { + Ok(_) => panic!("empty SDK builder must fail closed"), + Err(error) => error, + }; + assert_eq!(error.kind(), ErrorKind::MissingStorage); +} +"#; + #[cfg(test)] mod tests { use std::path::{Path, PathBuf}; @@ -214,11 +299,28 @@ mod tests { use crate::wasm::ResolvedRustToolchain; use super::{ - exact_dependency_pin, render_consumer_manifest, smoke_cargo_check_command, - workspace_dependency_version_from, + exact_dependency_pin, render_consumer_manifest, render_sdk_consumer_manifest, + smoke_cargo_check_command, workspace_dependency_version_from, }; #[test] + fn sdk_smoke_consumer_uses_final_api_and_local_lower_package_patches() { + let manifest = render_sdk_consumer_manifest( + Path::new("/tmp/radroots_sdk"), + Path::new("/tmp/radroots_lib"), + ) + .expect("manifest"); + + assert!(manifest.contains("name = \"radroots_sdk_host_smoke\"")); + assert!(manifest.contains("radroots_sdk = { path = \"/tmp/radroots_sdk\"")); + assert!( + manifest.contains("radroots_storage = { path = \"/tmp/radroots_lib/crates/storage\" }") + ); + assert!(!manifest.contains("runtime =")); + assert!(!manifest.contains("signer-adapters")); + } + + #[test] fn smoke_consumer_manifest_uses_exact_direct_dependency_pin() { let manifest = render_consumer_manifest(Path::new("/tmp/radroots_sdk"), "=0.44.2").expect("manifest");