commit fe0957ac7f79be56fcd1a5e592c02cbf5a6b4845
parent e44bd4ffd5825c6005cc4713c006ccc9e692484f
Author: triesap <tyson@radroots.org>
Date: Sun, 12 Apr 2026 23:40:20 +0000
spec: align staged language sdk posture
Diffstat:
11 files changed, 422 insertions(+), 140 deletions(-)
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -169,6 +169,7 @@ pub struct ExportArtifacts {
pub struct SdkExportMapping {
pub language: ExportLanguage,
pub sdk: SdkExportSdk,
+ pub rollout: SdkExportRollout,
pub operations: BTreeMap<String, String>,
pub shared_types: BTreeMap<String, String>,
pub artifacts: Option<SdkExportArtifacts>,
@@ -183,6 +184,12 @@ pub struct SdkExportSdk {
pub networking: String,
}
+#[derive(Debug, Deserialize)]
+pub struct SdkExportRollout {
+ pub stage: String,
+ pub order: u32,
+}
+
#[derive(Debug, Deserialize, Default)]
pub struct SdkExportArtifacts {
pub models_dir: Option<String>,
@@ -1747,6 +1754,45 @@ fn ts_curated_package_set(bundle: &ContractBundle) -> Result<Option<BTreeSet<Str
Ok(Some(packages))
}
+fn sdk_packages_by_language(bundle: &ContractBundle) -> Result<BTreeMap<String, String>, String> {
+ let mut packages = BTreeMap::new();
+ for mapping in &bundle.sdk_exports {
+ if packages
+ .insert(mapping.language.id.clone(), mapping.sdk.package.clone())
+ .is_some()
+ {
+ return Err(format!(
+ "sdk-exports has duplicate language {}",
+ mapping.language.id
+ ));
+ }
+ }
+ Ok(packages)
+}
+
+fn validate_sdk_rollout(mapping: &SdkExportMapping) -> Result<(), String> {
+ let stage = mapping.rollout.stage.trim();
+ if stage.is_empty() {
+ return Err(format!(
+ "sdk rollout.stage is required for {}",
+ mapping.language.id
+ ));
+ }
+ if !matches!(stage, "active" | "next" | "deferred") {
+ return Err(format!(
+ "sdk rollout.stage {} is invalid for {}",
+ mapping.rollout.stage, mapping.language.id
+ ));
+ }
+ if mapping.rollout.order == 0 {
+ return Err(format!(
+ "sdk rollout.order must be greater than zero for {}",
+ mapping.language.id
+ ));
+ }
+ Ok(())
+}
+
fn validate_operations_contract(
bundle: &ContractBundle,
operations_manifest: &OperationsContractManifest,
@@ -1975,6 +2021,12 @@ fn validate_operations_contract(
}
let ts_packages = ts_curated_package_set(bundle)?;
+ let sdk_packages = sdk_packages_by_language(bundle)?;
+ let export_languages = bundle
+ .exports
+ .iter()
+ .map(|mapping| mapping.language.id.clone())
+ .collect::<BTreeSet<_>>();
let mut has_ts_mapping = false;
for mapping in &bundle.sdk_exports {
if mapping.language.id.trim().is_empty() {
@@ -1989,6 +2041,7 @@ fn validate_operations_contract(
if mapping.language.id == "ts" {
has_ts_mapping = true;
}
+ validate_sdk_rollout(mapping)?;
if mapping.sdk.package.trim().is_empty() {
return Err(format!(
"sdk export package is required for {}",
@@ -2052,6 +2105,12 @@ fn validate_operations_contract(
));
}
}
+ if !export_languages.contains(&mapping.language.id) {
+ return Err(format!(
+ "sdk export {} is missing a matching export mapping",
+ mapping.language.id
+ ));
+ }
if mapping.language.id == "ts" {
if operation_ids != mapping.operations.keys().cloned().collect::<BTreeSet<_>>() {
return Err(
@@ -2094,6 +2153,30 @@ fn validate_operations_contract(
}
}
}
+ let expected_export_package = sdk_packages
+ .get(&mapping.language.id)
+ .expect("sdk language package map must include current language");
+ let matching_export = bundle
+ .exports
+ .iter()
+ .find(|export| export.language.id == mapping.language.id)
+ .expect("export language set must include validated sdk export language");
+ let export_packages = matching_export
+ .packages
+ .values()
+ .cloned()
+ .collect::<BTreeSet<_>>();
+ let expected_packages = [expected_export_package.clone()]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ if export_packages != expected_packages {
+ return Err(format!(
+ "export {} packages {} must resolve to curated sdk package {}",
+ mapping.language.id,
+ join_set(&export_packages),
+ expected_export_package
+ ));
+ }
}
if !has_ts_mapping {
return Err("sdk-exports must include a ts mapping".to_string());
@@ -2116,6 +2199,163 @@ fn package_field_configured(table: &toml::value::Table, field: &str) -> bool {
}
}
+fn validate_export_mappings(bundle: &ContractBundle) -> Result<(), String> {
+ if bundle.exports.is_empty() {
+ return Err("at least one language export mapping is required".to_string());
+ }
+ if bundle.sdk_exports.is_empty() {
+ return Err("sdk-exports must define at least one curated language mapping".to_string());
+ }
+ let ts_packages = ts_curated_package_set(bundle)?;
+ let sdk_packages = sdk_packages_by_language(bundle)?;
+ let export_languages = bundle
+ .exports
+ .iter()
+ .map(|mapping| mapping.language.id.clone())
+ .collect::<BTreeSet<_>>();
+ for mapping in &bundle.exports {
+ if mapping.language.id.trim().is_empty() {
+ return Err("language.id is required".to_string());
+ }
+ if mapping.language.repository.trim().is_empty() {
+ return Err(format!(
+ "language.repository is required for {}",
+ mapping.language.id
+ ));
+ }
+ if mapping.packages.is_empty() {
+ return Err(format!(
+ "packages map is required for {}",
+ mapping.language.id
+ ));
+ }
+ let expected_sdk_package = sdk_packages.get(&mapping.language.id).ok_or_else(|| {
+ format!(
+ "export {} is missing a matching sdk export mapping",
+ mapping.language.id
+ )
+ })?;
+ let mapped_packages = mapping.packages.values().cloned().collect::<BTreeSet<_>>();
+ let expected_packages = [expected_sdk_package.clone()]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ if mapped_packages != expected_packages {
+ return Err(format!(
+ "export {} packages {} must resolve to curated sdk package {}",
+ mapping.language.id,
+ join_set(&mapped_packages),
+ expected_sdk_package
+ ));
+ }
+ if mapping.language.id == "ts" {
+ let artifacts = match mapping.artifacts.as_ref() {
+ Some(artifacts) => artifacts,
+ None => return Err("artifacts map is required for ts".to_string()),
+ };
+ if artifacts
+ .models_dir
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ || artifacts
+ .constants_dir
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ || artifacts
+ .wasm_dist_dir
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ || artifacts
+ .manifest_file
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ {
+ return Err("artifacts fields must be non-empty for ts".to_string());
+ }
+ if let Some(expected_packages) = ts_packages.as_ref() {
+ if mapped_packages != *expected_packages {
+ return Err(format!(
+ "ts export packages {} must match manifest export.ts.packages {}",
+ join_set(&mapped_packages),
+ join_set(expected_packages)
+ ));
+ }
+ }
+ }
+ }
+ for mapping in &bundle.sdk_exports {
+ if !export_languages.contains(&mapping.language.id) {
+ return Err(format!(
+ "sdk export {} is missing a matching export mapping",
+ mapping.language.id
+ ));
+ }
+ if mapping.language.id.trim().is_empty() {
+ return Err("sdk export language.id is required".to_string());
+ }
+ if mapping.language.repository.trim().is_empty() {
+ return Err(format!(
+ "sdk export language.repository is required for {}",
+ mapping.language.id
+ ));
+ }
+ validate_sdk_rollout(mapping)?;
+ if mapping.sdk.package.trim().is_empty() {
+ return Err(format!(
+ "sdk export package is required for {}",
+ mapping.language.id
+ ));
+ }
+ if mapping.sdk.deterministic_codec.trim().is_empty()
+ || mapping.sdk.signing.trim().is_empty()
+ || mapping.sdk.networking.trim().is_empty()
+ {
+ return Err(format!(
+ "sdk runtime fields must be non-empty for {}",
+ mapping.language.id
+ ));
+ }
+ if let Some(module_format) = mapping.sdk.module_format.as_deref() {
+ if module_format.trim().is_empty() {
+ return Err(format!(
+ "sdk module_format must be non-empty for {}",
+ mapping.language.id
+ ));
+ }
+ }
+ if mapping.operations.is_empty() {
+ return Err(format!(
+ "sdk export operations map is required for {}",
+ mapping.language.id
+ ));
+ }
+ if mapping.shared_types.is_empty() {
+ return Err(format!(
+ "sdk export shared_types map is required for {}",
+ mapping.language.id
+ ));
+ }
+ if mapping.language.id == "ts" {
+ let artifacts = mapping
+ .artifacts
+ .as_ref()
+ .ok_or_else(|| "sdk export artifacts map is required for ts".to_string())?;
+ for (field, value) in [
+ ("models_dir", artifacts.models_dir.as_ref()),
+ ("runtime_dir", artifacts.runtime_dir.as_ref()),
+ ("wasm_dist_dir", artifacts.wasm_dist_dir.as_ref()),
+ ("manifest_file", artifacts.manifest_file.as_ref()),
+ ] {
+ if value.is_none_or(|raw| raw.trim().is_empty()) {
+ return Err(format!(
+ "sdk export artifacts.{field} must be non-empty for ts"
+ ));
+ }
+ }
+ }
+ }
+ Ok(())
+}
+
fn validate_publish_package_metadata(
workspace_root: &Path,
publish_crates: &BTreeSet<String>,
@@ -2663,62 +2903,7 @@ fn validate_contract_bundle_with_release_policy_override(
if bundle.manifest.surface.wasm_crates.is_empty() {
return Err("contract surface.wasm_crates must not be empty".to_string());
}
- if bundle.exports.is_empty() {
- return Err("at least one language export mapping is required".to_string());
- }
- let ts_packages = ts_curated_package_set(bundle)?;
- for mapping in &bundle.exports {
- if mapping.language.id.trim().is_empty() {
- return Err("language.id is required".to_string());
- }
- if mapping.language.repository.trim().is_empty() {
- return Err(format!(
- "language.repository is required for {}",
- mapping.language.id
- ));
- }
- if mapping.packages.is_empty() {
- return Err(format!(
- "packages map is required for {}",
- mapping.language.id
- ));
- }
- if mapping.language.id == "ts" {
- let artifacts = match mapping.artifacts.as_ref() {
- Some(artifacts) => artifacts,
- None => return Err("artifacts map is required for ts".to_string()),
- };
- if artifacts
- .models_dir
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- || artifacts
- .constants_dir
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- || artifacts
- .wasm_dist_dir
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- || artifacts
- .manifest_file
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- {
- return Err("artifacts fields must be non-empty for ts".to_string());
- }
- if let Some(expected_packages) = ts_packages.as_ref() {
- let mapped_packages = mapping.packages.values().cloned().collect::<BTreeSet<_>>();
- if mapped_packages != *expected_packages {
- return Err(format!(
- "ts export packages {} must match manifest export.ts.packages {}",
- join_set(&mapped_packages),
- join_set(expected_packages)
- ));
- }
- }
- }
- }
+ validate_export_mappings(bundle)?;
if bundle.version.contract.version.trim().is_empty() {
return Err("version.contract.version is required".to_string());
}
@@ -3088,62 +3273,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> {
if bundle.manifest.surface.wasm_crates.is_empty() {
return Err("contract surface.wasm_crates must not be empty".to_string());
}
- if bundle.exports.is_empty() {
- return Err("at least one language export mapping is required".to_string());
- }
- let ts_packages = ts_curated_package_set(bundle)?;
- for mapping in &bundle.exports {
- if mapping.language.id.trim().is_empty() {
- return Err("language.id is required".to_string());
- }
- if mapping.language.repository.trim().is_empty() {
- return Err(format!(
- "language.repository is required for {}",
- mapping.language.id
- ));
- }
- if mapping.packages.is_empty() {
- return Err(format!(
- "packages map is required for {}",
- mapping.language.id
- ));
- }
- if mapping.language.id == "ts" {
- let artifacts = match mapping.artifacts.as_ref() {
- Some(artifacts) => artifacts,
- None => return Err("artifacts map is required for ts".to_string()),
- };
- if artifacts
- .models_dir
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- || artifacts
- .constants_dir
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- || artifacts
- .wasm_dist_dir
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- || artifacts
- .manifest_file
- .as_deref()
- .is_none_or(|value| value.trim().is_empty())
- {
- return Err("artifacts fields must be non-empty for ts".to_string());
- }
- if let Some(expected_packages) = ts_packages.as_ref() {
- let mapped_packages = mapping.packages.values().cloned().collect::<BTreeSet<_>>();
- if mapped_packages != *expected_packages {
- return Err(format!(
- "ts export packages {} must match manifest export.ts.packages {}",
- join_set(&mapped_packages),
- join_set(expected_packages)
- ));
- }
- }
- }
- }
+ validate_export_mappings(bundle)?;
if bundle.version.contract.version.trim().is_empty() {
return Err("version.contract.version is required".to_string());
}
@@ -3196,7 +3326,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> {
#[cfg(test)]
mod tests {
use super::*;
- use std::collections::BTreeSet;
+ use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -3331,6 +3461,40 @@ manifest_file = "export-manifest.json"
"#,
);
write_file(
+ &root.join("spec").join("sdk-exports").join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[sdk]
+package = "@radroots/sdk"
+module_format = "esm"
+deterministic_codec = "wasm"
+signing = "native"
+networking = "native"
+
+[rollout]
+stage = "active"
+order = 1
+
+[operations]
+"profile.build_draft" = "profile.buildDraft"
+"listing.build_draft" = "listing.buildDraft"
+
+[shared_types]
+"WireEventParts" = "WireEventParts"
+"UnsignedEventDraft" = "UnsignedEventDraft"
+"RadrootsNostrEvent" = "RadrootsNostrEvent"
+"RadrootsListing" = "RadrootsListing"
+
+[artifacts]
+models_dir = "src/generated"
+runtime_dir = "src/runtime"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ write_file(
&root.join("policy").join("coverage").join("policy.toml"),
r#"[gate]
fail_under_exec_lines = 100.0
@@ -3450,6 +3614,10 @@ deterministic_codec = "wasm"
signing = "native"
networking = "native"
+[rollout]
+stage = "active"
+order = 1
+
[operations]
"profile.build_draft" = "profile.buildDraft"
"listing.build_draft" = "listing.buildDraft"
@@ -3674,22 +3842,44 @@ crates = ["radroots_a"]
fn exports_follow_package_scope_rules() {
let root = workspace_root();
let bundle = load_contract_bundle(&root).expect("load contract");
+ let sdk_packages = sdk_packages_by_language(&bundle).expect("sdk packages by language");
for mapping in &bundle.exports {
- if mapping.language.id == "ts" {
- let packages = mapping.packages.values().cloned().collect::<BTreeSet<_>>();
- assert_eq!(
- packages,
- ["@radroots/sdk".to_string()].into_iter().collect()
- );
- } else {
- for package in mapping.packages.values() {
- assert!(!package.trim().is_empty());
- }
- }
+ let packages = mapping.packages.values().cloned().collect::<BTreeSet<_>>();
+ let expected_package = sdk_packages
+ .get(&mapping.language.id)
+ .expect("export has matching sdk package");
+ let expected = [expected_package.clone()]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ assert_eq!(packages, expected);
}
}
#[test]
+ fn sdk_exports_follow_staged_rollout_order() {
+ let root = workspace_root();
+ let bundle = load_contract_bundle(&root).expect("load contract");
+ let rollout = bundle
+ .sdk_exports
+ .iter()
+ .map(|mapping| {
+ (
+ mapping.language.id.clone(),
+ (mapping.rollout.stage.clone(), mapping.rollout.order),
+ )
+ })
+ .collect::<BTreeMap<_, _>>();
+ let expected = BTreeMap::from([
+ ("go".to_string(), ("deferred".to_string(), 3)),
+ ("kotlin".to_string(), ("next".to_string(), 2)),
+ ("py".to_string(), ("deferred".to_string(), 3)),
+ ("swift".to_string(), ("next".to_string(), 2)),
+ ("ts".to_string(), ("active".to_string(), 1)),
+ ]);
+ assert_eq!(rollout, expected);
+ }
+
+ #[test]
fn non_ts_exports_only_include_model_surface() {
let root = workspace_root();
let bundle = load_contract_bundle(&root).expect("load contract");
@@ -4683,6 +4873,12 @@ edition = "2024"
bundle.exports.clear();
},
);
+ assert_bundle_error(
+ "sdk-exports must define at least one curated language mapping",
+ |bundle| {
+ bundle.sdk_exports.clear();
+ },
+ );
assert_bundle_error("language.id is required", |bundle| {
bundle.exports[0].language.id.clear();
});
@@ -4720,6 +4916,12 @@ edition = "2024"
.expect("ts artifacts")
.manifest_file = Some(String::new());
});
+ assert_bundle_error("sdk rollout.stage is required", |bundle| {
+ bundle.sdk_exports[0].rollout.stage.clear();
+ });
+ assert_bundle_error("sdk rollout.order must be greater than zero", |bundle| {
+ bundle.sdk_exports[0].rollout.order = 0;
+ });
assert_bundle_error("version.contract.version is required", |bundle| {
bundle.version.contract.version.clear();
});
@@ -4788,7 +4990,7 @@ edition = "2024"
.clear();
});
assert_bundle_error(
- "sdk-exports must define at least one operation-based language mapping",
+ "sdk-exports must define at least one curated language mapping",
|bundle| {
bundle.sdk_exports.clear();
},
@@ -5830,7 +6032,7 @@ manifest_file = "export-manifest.json"
);
let package_err =
validate_contract_bundle(&mismatch_bundle).expect_err("ts package mismatch");
- assert!(package_err.contains("must match manifest export.ts.packages"));
+ assert!(package_err.contains("must resolve to curated sdk package"));
let _ = fs::remove_dir_all(&curated_package_mismatch);
let release_error_root = create_synthetic_workspace("bundle_release_policy_error");
diff --git a/spec/README.md b/spec/README.md
@@ -84,6 +84,7 @@ Curated public SDK package definitions are defined under `spec/sdk-exports/`:
- `spec/sdk-exports/swift.toml`
- `spec/sdk-exports/kotlin.toml`
- `spec/sdk-exports/py.toml`
+- `spec/sdk-exports/go.toml`
Lower-level language package mappings and artifact layout rules remain defined
under `spec/exports/`:
@@ -92,11 +93,19 @@ under `spec/exports/`:
- `spec/exports/py.toml`
- `spec/exports/swift.toml`
- `spec/exports/kotlin.toml`
+- `spec/exports/go.toml`
The `sdk-exports` files are the authoritative public package model.
The `exports` files remain the lower-level substrate and artifact mapping layer.
-For TypeScript, that lower-level provenance still resolves to the single
-curated `@radroots/sdk` package rather than a crate-mirrored npm package set.
+For every language target, that lower-level provenance must still resolve to
+the same single curated SDK package defined in `sdk-exports/`, rather than a
+crate-mirrored package set.
+
+Rollout order is also explicit in `sdk-exports/`:
+
+- TypeScript is active now
+- Swift and Kotlin are next
+- Python and Go remain deferred until the Rust and TypeScript lines are proven
## Internal Replica Contract
diff --git a/spec/exports/go.toml b/spec/exports/go.toml
@@ -0,0 +1,19 @@
+[language]
+id = "go"
+repository = "sdk-go"
+
+[packages]
+"radroots_core" = "github.com/radrootslabs/sdk-go"
+"radroots_events" = "github.com/radrootslabs/sdk-go"
+"radroots_trade" = "github.com/radrootslabs/sdk-go"
+"radroots_identity" = "github.com/radrootslabs/sdk-go"
+
+[artifacts]
+models_dir = "internal/generated"
+constants_dir = "internal/generated"
+manifest_file = "export-manifest.json"
+
+[runtime]
+networking = "native"
+signing = "native"
+deterministic_codec = "native_or_wasm"
diff --git a/spec/exports/kotlin.toml b/spec/exports/kotlin.toml
@@ -3,10 +3,10 @@ id = "kotlin"
repository = "sdk-kotlin"
[packages]
-"radroots_core" = "radroots.core"
-"radroots_events" = "radroots.events"
-"radroots_trade" = "radroots.trade"
-"radroots_identity" = "radroots.identity"
+"radroots_core" = "radroots.sdk"
+"radroots_events" = "radroots.sdk"
+"radroots_trade" = "radroots.sdk"
+"radroots_identity" = "radroots.sdk"
[artifacts]
models_dir = "src/generated"
diff --git a/spec/exports/py.toml b/spec/exports/py.toml
@@ -3,10 +3,10 @@ id = "py"
repository = "sdk-python"
[packages]
-"radroots_core" = "radroots_core"
-"radroots_events" = "radroots_events"
-"radroots_trade" = "radroots_trade"
-"radroots_identity" = "radroots_identity"
+"radroots_core" = "radroots_sdk"
+"radroots_events" = "radroots_sdk"
+"radroots_trade" = "radroots_sdk"
+"radroots_identity" = "radroots_sdk"
[artifacts]
models_dir = "src/generated"
diff --git a/spec/exports/swift.toml b/spec/exports/swift.toml
@@ -3,10 +3,10 @@ id = "swift"
repository = "sdk-swift"
[packages]
-"radroots_core" = "RadrootsCore"
-"radroots_events" = "RadrootsEvents"
-"radroots_trade" = "RadrootsTrade"
-"radroots_identity" = "RadrootsIdentity"
+"radroots_core" = "RadrootsSDK"
+"radroots_events" = "RadrootsSDK"
+"radroots_trade" = "RadrootsSDK"
+"radroots_identity" = "RadrootsSDK"
[artifacts]
models_dir = "Sources/Generated"
diff --git a/spec/sdk-exports/go.toml b/spec/sdk-exports/go.toml
@@ -0,0 +1,36 @@
+[language]
+id = "go"
+repository = "sdk-go"
+
+[sdk]
+package = "github.com/radrootslabs/sdk-go"
+deterministic_codec = "native_or_wasm"
+signing = "native"
+networking = "native"
+
+[rollout]
+stage = "deferred"
+order = 3
+
+[operations]
+"profile.build_draft" = "profile.BuildDraft"
+"farm.build_draft" = "farm.BuildDraft"
+"listing.build_tags" = "listing.BuildTags"
+"listing.build_draft" = "listing.BuildDraft"
+"listing.parse_event" = "listing.ParseEvent"
+"trade.build_envelope_draft" = "trade.BuildEnvelopeDraft"
+"trade.parse_envelope" = "trade.ParseEnvelope"
+"trade.parse_listing_address" = "trade.ParseListingAddress"
+"trade.validate_listing_event" = "trade.ValidateListingEvent"
+
+[shared_types]
+"WireEventParts" = "WireEventParts"
+"UnsignedEventDraft" = "UnsignedEventDraft"
+"RadrootsNostrEvent" = "RadrootsNostrEvent"
+"RadrootsNostrEventRef" = "RadrootsNostrEventRef"
+"RadrootsNostrEventPtr" = "RadrootsNostrEventPtr"
+"RadrootsTradeListingAddress" = "TradeListingAddress"
+"RadrootsProfile" = "RadrootsProfile"
+"RadrootsFarm" = "RadrootsFarm"
+"RadrootsListing" = "RadrootsListing"
+"RadrootsTradeEnvelope" = "TradeEnvelope"
diff --git a/spec/sdk-exports/kotlin.toml b/spec/sdk-exports/kotlin.toml
@@ -8,6 +8,10 @@ deterministic_codec = "native_or_wasm"
signing = "native"
networking = "native"
+[rollout]
+stage = "next"
+order = 2
+
[operations]
"profile.build_draft" = "profile.buildDraft"
"farm.build_draft" = "farm.buildDraft"
diff --git a/spec/sdk-exports/py.toml b/spec/sdk-exports/py.toml
@@ -8,6 +8,10 @@ deterministic_codec = "native_or_wasm"
signing = "native"
networking = "native"
+[rollout]
+stage = "deferred"
+order = 3
+
[operations]
"profile.build_draft" = "profile_build_draft"
"farm.build_draft" = "farm_build_draft"
diff --git a/spec/sdk-exports/swift.toml b/spec/sdk-exports/swift.toml
@@ -8,6 +8,10 @@ deterministic_codec = "native_or_wasm"
signing = "native"
networking = "native"
+[rollout]
+stage = "next"
+order = 2
+
[operations]
"profile.build_draft" = "profile.buildDraft"
"farm.build_draft" = "farm.buildDraft"
diff --git a/spec/sdk-exports/ts.toml b/spec/sdk-exports/ts.toml
@@ -9,6 +9,10 @@ deterministic_codec = "wasm"
signing = "native"
networking = "native"
+[rollout]
+stage = "active"
+order = 1
+
[operations]
"profile.build_draft" = "profile.buildDraft"
"farm.build_draft" = "farm.buildDraft"