lib

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

commit 62fccb4d203e7a137b3635aa8cfa4143dfc59755
parent 52912c1fe42bcfe38e0b549cd21afa05905581da
Author: triesap <tyson@radroots.org>
Date:   Sun, 12 Apr 2026 23:13:27 +0000

spec: align curated typescript sdk package model

Diffstat:
Mcrates/xtask/src/contract.rs | 111++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mspec/README.md | 2++
Mspec/exports/ts.toml | 10+++++-----
3 files changed, 111 insertions(+), 12 deletions(-)

diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -24,6 +24,7 @@ pub struct ContractManifest { pub contract: ManifestContract, pub surface: Surface, pub policy: Policy, + pub export: Option<ManifestExports>, } #[derive(Debug, Deserialize)] @@ -48,6 +49,16 @@ pub struct Policy { } #[derive(Debug, Deserialize)] +pub struct ManifestExports { + pub ts: Option<ManifestLanguagePackages>, +} + +#[derive(Debug, Deserialize)] +pub struct ManifestLanguagePackages { + pub packages: Vec<String>, +} + +#[derive(Debug, Deserialize)] pub struct OperationsContractManifest { pub contract: ManifestContract, pub public: PublicContract, @@ -1722,6 +1733,20 @@ fn collect_non_empty_set(items: &[String], field: &str) -> Result<BTreeSet<Strin Ok(set) } +fn ts_curated_package_set(bundle: &ContractBundle) -> Result<Option<BTreeSet<String>>, String> { + let Some(export_targets) = bundle.manifest.export.as_ref() else { + return Ok(None); + }; + let Some(ts_export) = export_targets.ts.as_ref() else { + return Ok(None); + }; + let packages = collect_non_empty_set(&ts_export.packages, "manifest export.ts.packages")?; + if packages.is_empty() { + return Err("manifest export.ts.packages must not be empty".to_string()); + } + Ok(Some(packages)) +} + fn validate_operations_contract( bundle: &ContractBundle, operations_manifest: &OperationsContractManifest, @@ -1949,6 +1974,7 @@ fn validate_operations_contract( ); } + let ts_packages = ts_curated_package_set(bundle)?; let mut has_ts_mapping = false; for mapping in &bundle.sdk_exports { if mapping.language.id.trim().is_empty() { @@ -2033,6 +2059,24 @@ fn validate_operations_contract( .to_string(), ); } + if let Some(expected_packages) = ts_packages.as_ref() { + if expected_packages.len() != 1 { + return Err( + "manifest export.ts.packages must define exactly one curated ts package" + .to_string(), + ); + } + let expected_package = expected_packages + .iter() + .next() + .expect("single-package ts export set"); + if mapping.sdk.package != *expected_package { + return Err(format!( + "sdk export ts package {} must match manifest export.ts.packages {}", + mapping.sdk.package, expected_package + )); + } + } let artifacts = mapping .artifacts .as_ref() @@ -2622,6 +2666,7 @@ fn validate_contract_bundle_with_release_policy_override( 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()); @@ -2662,6 +2707,16 @@ fn validate_contract_bundle_with_release_policy_override( { 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) + )); + } + } } } if bundle.version.contract.version.trim().is_empty() { @@ -3036,6 +3091,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), 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()); @@ -3076,6 +3132,16 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { { 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) + )); + } + } } } if bundle.version.contract.version.trim().is_empty() { @@ -3222,6 +3288,9 @@ model_crates = ["radroots_a"] algorithm_crates = ["radroots_b"] wasm_crates = ["radroots_a_wasm"] +[export.ts] +packages = ["@radroots/sdk"] + [policy] exclude_internal_workspace_crates = true require_reproducible_exports = true @@ -3252,7 +3321,7 @@ id = "ts" repository = "sdk-typescript" [packages] -"radroots_a" = "@radroots/a" +"radroots_a" = "@radroots/sdk" [artifacts] models_dir = "src/generated" @@ -3607,9 +3676,11 @@ crates = ["radroots_a"] let bundle = load_contract_bundle(&root).expect("load contract"); for mapping in &bundle.exports { if mapping.language.id == "ts" { - for package in mapping.packages.values() { - assert!(package.starts_with("@radroots/")); - } + 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()); @@ -5290,6 +5361,9 @@ model_crates = ["radroots_a"] algorithm_crates = ["radroots_b"] wasm_crates = ["radroots_a_wasm"] +[export.ts] +packages = ["@radroots/sdk"] + [policy] exclude_internal_workspace_crates = false require_reproducible_exports = true @@ -5421,7 +5495,7 @@ id = "ts" repository = "sdk-typescript" [packages] -"radroots_a" = "@radroots/a" +"radroots_a" = "@radroots/sdk" [artifacts] models_dir = "src/generated" @@ -5709,8 +5783,8 @@ id = "ts" repository = "sdk-typescript" [packages] -radroots_a = "@radroots/a" -radroots_b = "@radroots/b" +radroots_a = "@radroots/sdk" +radroots_b = "@radroots/sdk" [artifacts] models_dir = "src/generated" @@ -5736,6 +5810,29 @@ manifest_file = "export-manifest.json" assert!(artifacts_err.contains("artifacts map is required for ts")); let _ = fs::remove_dir_all(&missing_artifacts); + let curated_package_mismatch = create_synthetic_workspace("bundle_ts_package_mismatch"); + let mut mismatch_bundle = + load_contract_bundle(&curated_package_mismatch).expect("load mismatch bundle"); + mismatch_bundle.exports[0] + .packages + .insert("radroots_a".to_string(), "@radroots/other".to_string()); + assert_eq!( + ts_curated_package_set(&mismatch_bundle).expect("ts package set"), + Some(["@radroots/sdk".to_string()].into_iter().collect()) + ); + assert_eq!( + mismatch_bundle.exports[0] + .packages + .values() + .cloned() + .collect::<BTreeSet<_>>(), + ["@radroots/other".to_string()].into_iter().collect() + ); + let package_err = + validate_contract_bundle(&mismatch_bundle).expect_err("ts package mismatch"); + assert!(package_err.contains("must match manifest export.ts.packages")); + let _ = fs::remove_dir_all(&curated_package_mismatch); + let release_error_root = create_synthetic_workspace("bundle_release_policy_error"); write_file( &root_release_policy_path(&release_error_root), diff --git a/spec/README.md b/spec/README.md @@ -48,6 +48,8 @@ under `spec/exports/`: 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. ## Internal Replica Contract diff --git a/spec/exports/ts.toml b/spec/exports/ts.toml @@ -3,11 +3,11 @@ id = "ts" repository = "sdk-typescript" [packages] -"radroots_core" = "@radroots/core" -"radroots_events" = "@radroots/events" -"radroots_trade" = "@radroots/trade" -"radroots_identity" = "@radroots/identity" -"radroots_events_codec_wasm" = "@radroots/events-codec-wasm" +"radroots_core" = "@radroots/sdk" +"radroots_events" = "@radroots/sdk" +"radroots_trade" = "@radroots/sdk" +"radroots_identity" = "@radroots/sdk" +"radroots_events_codec_wasm" = "@radroots/sdk" [artifacts] models_dir = "src/generated"