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:
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");