lib

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

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:
Mcrates/xtask/src/contract.rs | 454+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mspec/README.md | 13+++++++++++--
Aspec/exports/go.toml | 19+++++++++++++++++++
Mspec/exports/kotlin.toml | 8++++----
Mspec/exports/py.toml | 8++++----
Mspec/exports/swift.toml | 8++++----
Aspec/sdk-exports/go.toml | 36++++++++++++++++++++++++++++++++++++
Mspec/sdk-exports/kotlin.toml | 4++++
Mspec/sdk-exports/py.toml | 4++++
Mspec/sdk-exports/swift.toml | 4++++
Mspec/sdk-exports/ts.toml | 4++++
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"