lib

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

commit 0cc536f9a34128236e76da8cd5d864ea2ebba098
parent f1b600200dc887c85283c8a0c3166572b3dca5fa
Author: triesap <tyson@radroots.org>
Date:   Sat, 11 Apr 2026 16:24:58 +0000

repo: remove mounted sdk orchestration

Diffstat:
MMakefile | 6+-----
Dcrates/xtask/src/export_ts.rs | 2156-------------------------------------------------------------------------------
Mcrates/xtask/src/main.rs | 379+------------------------------------------------------------------------------
Mdocs/nix.md | 4----
Mnix/apps.nix | 16----------------
Mnix/common.nix | 25-------------------------
Mspec/RCLD.md | 18+++++++-----------
7 files changed, 10 insertions(+), 2594 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build clean help export-ts-sdk-bindings \ +.PHONY: all build clean help \ build-events-codec-wasm build-replica-db-wasm build-replica-sync-wasm SHELL := /bin/bash @@ -21,13 +21,9 @@ help: @echo " make all" @echo " make build" @echo " make clean" - @echo " make export-ts-sdk-bindings" @echo " make help" @printf "%s\n" $(BUILD_TARGETS) -export-ts-sdk-bindings: - cargo run -q -p xtask -- sdk export-ts - build-replica-db-wasm: wasm-pack build crates/replica_db_wasm --release --target web \ --out-dir ../replica-db-wasm/pkg/dist --scope radroots diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs @@ -1,2156 +0,0 @@ -#![forbid(unsafe_code)] - -use crate::contract; -use serde::Serialize; -use sha2::{Digest, Sha256}; -use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; - -#[derive(Serialize)] -struct ExportManifest { - language: String, - files: Vec<ExportManifestEntry>, -} - -#[derive(Serialize)] -struct ExportManifestEntry { - path: String, - sha256: String, -} - -fn to_package_dir(base: &Path, package_name: &str) -> PathBuf { - let stripped = package_name - .strip_prefix("@radroots/") - .unwrap_or(package_name); - base.join(stripped) -} - -fn package_dir_name(package_name: &str) -> &str { - package_name - .strip_prefix("@radroots/") - .unwrap_or(package_name) -} - -fn crate_dir_name(crate_name: &str) -> &str { - crate_name.strip_prefix("radroots_").unwrap_or(crate_name) -} - -fn is_wasm_crate(crate_name: &str) -> bool { - crate_name.ends_with("_wasm") -} - -fn ts_export_mapping( - bundle: &contract::ContractBundle, -) -> Result<&contract::ExportMapping, String> { - if let Some(mapping) = bundle - .exports - .iter() - .find(|mapping| mapping.language.id == "ts") - { - return Ok(mapping); - } - Err("missing ts export mapping".to_string()) -} - -fn ts_artifacts(mapping: &contract::ExportMapping) -> Result<&contract::ExportArtifacts, String> { - if let Some(artifacts) = mapping.artifacts.as_ref() { - return Ok(artifacts); - } - Err("missing ts artifacts mapping".to_string()) -} - -fn selected_package_entries<'a>( - mapping: &'a contract::ExportMapping, - selector: Option<&str>, -) -> Result<Vec<(&'a String, &'a String)>, String> { - if let Some(selector) = selector { - if let Some(entry) = mapping.packages.get_key_value(selector) { - return Ok(vec![entry]); - } - if let Some(entry) = mapping - .packages - .iter() - .find(|(_, package_name)| package_name.as_str() == selector) - { - return Ok(vec![entry]); - } - if !selector.starts_with("radroots_") { - let crate_candidate = format!("radroots_{selector}"); - if let Some(entry) = mapping.packages.get_key_value(&crate_candidate) { - return Ok(vec![entry]); - } - } - if !selector.starts_with("@radroots/") { - let package_candidate = format!("@radroots/{selector}"); - if let Some(entry) = mapping - .packages - .iter() - .find(|(_, package_name)| package_name.as_str() == package_candidate) - { - return Ok(vec![entry]); - } - } - let known_crates = mapping - .packages - .keys() - .map(String::as_str) - .collect::<Vec<_>>() - .join(", "); - return Err(format!( - "unknown ts export crate selector {selector}; available crates: {known_crates}" - )); - } - Ok(mapping.packages.iter().collect()) -} - -fn required_artifact_value<'a>(value: &'a Option<String>, field: &str) -> Result<&'a str, String> { - if let Some(raw) = value.as_deref() { - if !raw.trim().is_empty() { - return Ok(raw); - } - } - Err(format!("missing ts artifacts.{field}")) -} - -fn crate_supports_ts_rs(workspace_root: &Path, crate_dir: &str) -> Result<bool, String> { - let manifest = workspace_root - .join("crates") - .join(crate_dir) - .join("Cargo.toml"); - if !manifest.exists() { - return Ok(false); - } - let raw = match fs::read_to_string(&manifest) { - Ok(raw) => raw, - Err(e) => return Err(format!("read {}: {e}", manifest.display())), - }; - Ok(raw.contains("ts-rs")) -} - -fn copy_if_exists(src: &Path, dst: &Path) -> Result<bool, String> { - if !src.exists() { - return Ok(false); - } - let parent = dst.parent().expect("destination path must have parent"); - if let Err(e) = fs::create_dir_all(parent) { - return Err(format!("create {}: {e}", parent.display())); - } - if let Err(e) = fs::copy(src, dst) { - return Err(format!("copy {} -> {}: {e}", src.display(), dst.display())); - } - Ok(true) -} - -fn copy_dir_contents(src: &Path, dst: &Path) -> Result<usize, String> { - if !src.exists() { - return Ok(0); - } - if let Err(e) = fs::create_dir_all(dst) { - return Err(format!("create {}: {e}", dst.display())); - } - let mut copied = 0usize; - let read_dir = match fs::read_dir(src) { - Ok(entries) => entries, - Err(e) => return Err(format!("read dir {}: {e}", src.display())), - }; - let mut entries = read_dir.filter_map(Result::ok).collect::<Vec<_>>(); - entries.sort_by_key(|entry| entry.file_name()); - for entry in entries { - let path = entry.path(); - let target = dst.join(entry.file_name()); - if path.is_dir() { - copied += copy_dir_contents(&path, &target)?; - continue; - } - if !path.is_file() { - continue; - } - if let Err(e) = fs::copy(&path, &target) { - return Err(format!( - "copy {} -> {}: {e}", - path.display(), - target.display() - )); - } - copied += 1; - } - Ok(copied) -} - -fn summarize_command_output(output: &[u8]) -> Option<String> { - let lines = String::from_utf8_lossy(output) - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .map(ToOwned::to_owned) - .collect::<Vec<_>>(); - if lines.is_empty() { - return None; - } - let keep = lines.len().saturating_sub(8); - Some(lines[keep..].join(" | ")) -} - -fn cargo_test_failure(crate_name: &str, output: &Output) -> String { - let status = output - .status - .code() - .map(|code| format!("exit code {code}")) - .unwrap_or_else(|| "terminated by signal".to_string()); - let stderr = summarize_command_output(&output.stderr); - let stdout = summarize_command_output(&output.stdout); - match (stderr, stdout) { - (Some(stderr), _) => format!("cargo test failed for {crate_name} ({status}): {stderr}"), - (None, Some(stdout)) => format!("cargo test failed for {crate_name} ({status}): {stdout}"), - (None, None) => format!("cargo test failed for {crate_name} ({status})"), - } -} - -fn run_ts_rs_export_test( - crate_name: &str, - workspace_root: &Path, - export_dir: &Path, -) -> Result<Output, String> { - Command::new("cargo") - .arg("test") - .arg("-q") - .arg("-p") - .arg(crate_name) - .arg("--features") - .arg("ts-rs") - .env("RADROOTS_TS_RS_EXPORT_DIR", export_dir) - .current_dir(workspace_root) - .output() - .map_err(|e| format!("run cargo test for {crate_name}: {e}")) -} - -fn collect_manifest_entries( - root: &Path, - current: &Path, - skip_path: &Path, - entries: &mut Vec<ExportManifestEntry>, -) -> Result<(), String> { - if !current.exists() { - return Ok(()); - } - let read_dir = match fs::read_dir(current) { - Ok(entries) => entries, - Err(e) => return Err(format!("read dir {}: {e}", current.display())), - }; - let mut dir_entries = read_dir.filter_map(Result::ok).collect::<Vec<_>>(); - dir_entries.sort_by_key(|entry| entry.file_name()); - for entry in dir_entries { - let path = entry.path(); - if path == skip_path { - continue; - } - if path.is_dir() { - collect_manifest_entries(root, &path, skip_path, entries)?; - continue; - } - if !path.is_file() { - continue; - } - let bytes = match fs::read(&path) { - Ok(bytes) => bytes, - Err(_) => continue, - }; - let digest = Sha256::digest(&bytes); - let relative = match path.strip_prefix(root) { - Ok(relative) => relative.to_string_lossy().replace('\\', "/"), - Err(e) => return Err(format!("strip prefix {}: {e}", path.display())), - }; - entries.push(ExportManifestEntry { - path: relative, - sha256: hex::encode(digest), - }); - } - Ok(()) -} - -fn export_ts_models_with_selector( - workspace_root: &Path, - out_dir: &Path, - selector: Option<&str>, -) -> Result<(), String> { - let bundle = contract::load_contract_bundle(workspace_root)?; - contract::validate_contract_bundle(&bundle)?; - let ts_export = ts_export_mapping(&bundle)?; - let selected_entries = selected_package_entries(ts_export, selector)?; - let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts"); - let models_dir = required_artifact_value(&artifacts.models_dir, "models_dir") - .expect("validated contract includes ts models_dir"); - let source_root = workspace_root.join("target").join("ts-rs"); - if !source_root.exists() { - return Err(format!( - "missing ts-rs source root {}", - source_root.display() - )); - } - let ts_out_root = out_dir.join("ts").join("packages"); - let mut expected = 0usize; - let mut copied = 0usize; - for (crate_name, package_name) in selected_entries { - let crate_dir = crate_dir_name(crate_name); - if is_wasm_crate(crate_name) { - continue; - } - if !crate_supports_ts_rs(workspace_root, crate_dir) - .expect("validated workspace crate manifests are readable") - { - continue; - } - expected += 1; - let src = source_root - .join(package_dir_name(package_name)) - .join("types.ts"); - let dst = to_package_dir(&ts_out_root, package_name) - .join(models_dir) - .join("types.ts"); - if copy_if_exists(&src, &dst)? { - copied += 1; - } - } - if expected > 0 && copied == 0 { - return Err("no ts model files were exported".to_string()); - } - Ok(()) -} - -pub fn export_ts_models(workspace_root: &Path, out_dir: &Path) -> Result<(), String> { - export_ts_models_with_selector(workspace_root, out_dir, None) -} - -pub fn export_ts_models_for_crate( - workspace_root: &Path, - out_dir: &Path, - crate_selector: &str, -) -> Result<(), String> { - export_ts_models_with_selector(workspace_root, out_dir, Some(crate_selector)) -} - -fn export_ts_constants_with_selector( - workspace_root: &Path, - out_dir: &Path, - selector: Option<&str>, -) -> Result<(), String> { - let bundle = contract::load_contract_bundle(workspace_root)?; - contract::validate_contract_bundle(&bundle)?; - let ts_export = ts_export_mapping(&bundle)?; - let selected_entries = selected_package_entries(ts_export, selector)?; - let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts"); - let constants_dir = required_artifact_value(&artifacts.constants_dir, "constants_dir") - .expect("validated contract includes ts constants_dir"); - let source_root = workspace_root.join("target").join("ts-rs"); - if !source_root.exists() { - return Err(format!( - "missing ts-rs source root {}", - source_root.display() - )); - } - let ts_out_root = out_dir.join("ts").join("packages"); - for (crate_name, package_name) in selected_entries { - if is_wasm_crate(crate_name) { - continue; - } - for filename in ["constants.ts", "kinds.ts"] { - let src = source_root - .join(package_dir_name(package_name)) - .join(filename); - let dst = to_package_dir(&ts_out_root, package_name) - .join(constants_dir) - .join(filename); - copy_if_exists(&src, &dst)?; - } - } - Ok(()) -} - -pub fn export_ts_constants(workspace_root: &Path, out_dir: &Path) -> Result<(), String> { - export_ts_constants_with_selector(workspace_root, out_dir, None) -} - -pub fn export_ts_constants_for_crate( - workspace_root: &Path, - out_dir: &Path, - crate_selector: &str, -) -> Result<(), String> { - export_ts_constants_with_selector(workspace_root, out_dir, Some(crate_selector)) -} - -fn export_ts_wasm_artifacts_with_selector( - workspace_root: &Path, - out_dir: &Path, - selector: Option<&str>, -) -> Result<(), String> { - let bundle = contract::load_contract_bundle(workspace_root)?; - contract::validate_contract_bundle(&bundle)?; - let ts_export = ts_export_mapping(&bundle)?; - let selected_entries = selected_package_entries(ts_export, selector)?; - let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts"); - let wasm_dist_dir = required_artifact_value(&artifacts.wasm_dist_dir, "wasm_dist_dir") - .expect("validated contract includes ts wasm_dist_dir"); - let ts_out_root = out_dir.join("ts").join("packages"); - let mut copied = 0usize; - for (crate_name, package_name) in selected_entries { - if !is_wasm_crate(crate_name) { - continue; - } - let crate_dir = crate_dir_name(crate_name); - let source_root = workspace_root - .join("crates") - .join(crate_dir) - .join("pkg") - .join("dist"); - let target_root = to_package_dir(&ts_out_root, package_name).join(wasm_dist_dir); - copied += copy_dir_contents(&source_root, &target_root)?; - } - if copied == 0 { - return Ok(()); - } - Ok(()) -} - -pub fn export_ts_wasm_artifacts(workspace_root: &Path, out_dir: &Path) -> Result<(), String> { - export_ts_wasm_artifacts_with_selector(workspace_root, out_dir, None) -} - -pub fn export_ts_wasm_artifacts_for_crate( - workspace_root: &Path, - out_dir: &Path, - crate_selector: &str, -) -> Result<(), String> { - export_ts_wasm_artifacts_with_selector(workspace_root, out_dir, Some(crate_selector)) -} - -pub fn write_ts_export_manifest(workspace_root: &Path, out_dir: &Path) -> Result<PathBuf, String> { - let bundle = contract::load_contract_bundle(workspace_root)?; - contract::validate_contract_bundle(&bundle)?; - let ts_export = ts_export_mapping(&bundle)?; - let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts"); - let manifest_file = required_artifact_value(&artifacts.manifest_file, "manifest_file") - .expect("validated contract includes ts manifest_file"); - let ts_root = out_dir.join("ts"); - let manifest_path = ts_root.join(manifest_file); - let mut files = Vec::new(); - let packages_root = ts_root.join("packages"); - collect_manifest_entries(&ts_root, &packages_root, &manifest_path, &mut files)?; - let manifest = ExportManifest { - language: ts_export.language.id.clone(), - files, - }; - let parent = manifest_path - .parent() - .expect("manifest path must have parent"); - if let Err(e) = fs::create_dir_all(parent) { - return Err(format!("create {}: {e}", parent.display())); - } - let bytes = serde_json::to_vec_pretty(&manifest) - .expect("serializing export manifest should be infallible"); - if let Err(e) = fs::write(&manifest_path, bytes) { - return Err(format!("write {}: {e}", manifest_path.display())); - } - Ok(manifest_path) -} - -type TsRsExportRunner = dyn FnMut(&str, &Path, &Path) -> Result<Output, String>; - -fn generate_ts_rs_sources_with_selector_and_runner( - workspace_root: &Path, - selector: Option<&str>, - run_export_test: &mut TsRsExportRunner, -) -> Result<PathBuf, String> { - let bundle = contract::load_contract_bundle(workspace_root)?; - contract::validate_contract_bundle(&bundle)?; - let ts_export = ts_export_mapping(&bundle)?; - let selected_entries = selected_package_entries(ts_export, selector)?; - let source_root = workspace_root.join("target").join("ts-rs"); - if source_root.exists() { - if let Err(e) = fs::remove_dir_all(&source_root) { - return Err(format!("remove {}: {e}", source_root.display())); - } - } - if let Err(e) = fs::create_dir_all(&source_root) { - return Err(format!("create {}: {e}", source_root.display())); - } - let mut expected = 0usize; - let mut supports_ts_rs = BTreeMap::new(); - for (crate_name, _) in &selected_entries { - if is_wasm_crate(crate_name) { - continue; - } - let crate_dir = crate_dir_name(crate_name); - let supports = crate_supports_ts_rs(workspace_root, crate_dir) - .expect("validated workspace crate manifests are readable"); - supports_ts_rs.insert(crate_name.as_str(), supports); - if supports { - expected += 1; - } - } - if expected == 0 { - return Ok(source_root); - } - for (crate_name, package_name) in selected_entries { - if is_wasm_crate(crate_name) { - continue; - } - if !supports_ts_rs - .get(crate_name.as_str()) - .copied() - .unwrap_or(false) - { - continue; - } - let package_dir = package_name - .strip_prefix("@radroots/") - .unwrap_or(package_name); - let export_dir = source_root.join(package_dir); - let _ = fs::create_dir_all(&export_dir); - let output = run_export_test(crate_name, workspace_root, &export_dir)?; - if !output.status.success() { - return Err(cargo_test_failure(crate_name, &output)); - } - } - Ok(source_root) -} - -fn generate_ts_rs_sources_with_selector( - workspace_root: &Path, - selector: Option<&str>, -) -> Result<PathBuf, String> { - let mut run_export_test = run_ts_rs_export_test; - generate_ts_rs_sources_with_selector_and_runner(workspace_root, selector, &mut run_export_test) -} - -pub fn generate_ts_rs_sources(workspace_root: &Path) -> Result<PathBuf, String> { - generate_ts_rs_sources_with_selector(workspace_root, None) -} - -pub fn generate_ts_rs_sources_for_crate( - workspace_root: &Path, - crate_selector: &str, -) -> Result<PathBuf, String> { - generate_ts_rs_sources_with_selector(workspace_root, Some(crate_selector)) -} - -pub fn export_ts_bundle(workspace_root: &Path, out_dir: &Path) -> Result<PathBuf, String> { - generate_ts_rs_sources(workspace_root)?; - export_ts_models(workspace_root, out_dir)?; - export_ts_constants(workspace_root, out_dir)?; - export_ts_wasm_artifacts(workspace_root, out_dir)?; - write_ts_export_manifest(workspace_root, out_dir) -} - -pub fn export_ts_bundle_for_crate( - workspace_root: &Path, - out_dir: &Path, - crate_selector: &str, -) -> Result<PathBuf, String> { - generate_ts_rs_sources_for_crate(workspace_root, crate_selector)?; - export_ts_models_for_crate(workspace_root, out_dir, crate_selector)?; - export_ts_constants_for_crate(workspace_root, out_dir, crate_selector)?; - export_ts_wasm_artifacts_for_crate(workspace_root, out_dir, crate_selector)?; - write_ts_export_manifest(workspace_root, out_dir) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeMap; - use std::path::Path; - use std::process::Command; - use std::sync::{Mutex, OnceLock}; - use std::time::{SystemTime, UNIX_EPOCH}; - #[cfg(unix)] - use std::{os::unix::process::ExitStatusExt, process::ExitStatus}; - - fn workspace_root() -> PathBuf { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - manifest_dir - .join("../..") - .canonicalize() - .expect("workspace root") - } - - fn workspace_lock() -> &'static Mutex<()> { - static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - } - - fn unique_temp_dir(prefix: &str) -> PathBuf { - let ns = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time") - .as_nanos(); - std::env::temp_dir().join(format!("radroots_xtask_{prefix}_{ns}")) - } - - fn write_file(path: &Path, content: &str) { - let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new(""))); - fs::write(path, content).expect("write file"); - } - - fn create_synthetic_workspace(prefix: &str, crate_a_ts_rs: bool) -> PathBuf { - let root = unique_temp_dir(prefix); - fs::create_dir_all(&root).expect("create root"); - write_file( - &root.join("Cargo.toml"), - r#"[workspace] -members = ["crates/a", "crates/b"] -resolver = "2" -"#, - ); - let crate_a_features = if crate_a_ts_rs { - "\n[features]\nts-rs = []\n" - } else { - "" - }; - write_file( - &root.join("crates").join("a").join("Cargo.toml"), - &format!( - r#"[package] -name = "radroots_a" -publish = ["crates-io"] -version = "0.1.0" -edition = "2024" -description = "crate a" -repository = "https://example.com/a" -homepage = "https://example.com/a" -documentation = "https://docs.example.com/a" -readme = "README" -{}"#, - crate_a_features - ), - ); - write_file( - &root.join("crates").join("a").join("src").join("lib.rs"), - "pub fn crate_a() {}\n", - ); - write_file( - &root.join("crates").join("b").join("Cargo.toml"), - r#"[package] -name = "radroots_b" -version = "0.1.0" -edition = "2024" -publish = false -"#, - ); - write_file( - &root.join("crates").join("b").join("src").join("lib.rs"), - "pub fn crate_b() {}\n", - ); - write_file( - &root.join("crates").join("core").join("src").join("unit.rs"), - r#"pub enum RadrootsCoreUnitDimension { - Count, - Mass, - Volume, -} -"#, - ); - write_file( - &root.join("spec").join("manifest.toml"), - r#"[contract] -name = "radroots_contract" -version = "1.0.0" -source = "synthetic" - -[surface] -model_crates = ["radroots_a"] -algorithm_crates = ["radroots_b"] -wasm_crates = ["radroots_a_wasm"] - -[policy] -exclude_internal_workspace_crates = true -require_reproducible_exports = true -require_conformance_vectors = true -"#, - ); - write_file( - &root.join("spec").join("version.toml"), - r#"[contract] -version = "1.0.0" -stability = "alpha" - -[semver] -major_on = ["breaking"] -minor_on = ["feature"] -patch_on = ["fix"] - -[compatibility] -requires_conformance_pass = true -requires_export_manifest_diff = true -requires_release_notes = true -"#, - ); - write_file( - &root.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -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 -fail_under_functions = 100.0 -fail_under_regions = 100.0 -fail_under_branches = 100.0 -require_branches = true - -[required] -crates = ["radroots_a", "radroots_b"] -"#, - ); - write_file( - &root - .join("contracts") - .join("release") - .join("mounted-rust-crates") - .join("publish-policy.toml"), - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, - ); - write_file( - &root - .join("target") - .join("coverage") - .join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\nradroots_b\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", - ); - root - } - - fn write_ts_rs_probe_lib(root: &Path) { - write_file( - &root.join("crates").join("a").join("src").join("lib.rs"), - r#"pub fn crate_a() {} - -#[cfg(test)] -mod tests { - use std::fs; - use std::path::PathBuf; - - #[test] - fn write_ts_exports() { - if let Ok(path) = std::env::var("RADROOTS_TS_RS_EXPORT_DIR") { - let export_dir = PathBuf::from(path); - let _ = fs::create_dir_all(&export_dir); - fs::write( - export_dir.join("types.ts"), - "export type Probe = { id: string };\n", - ) - .expect("write types"); - fs::write(export_dir.join("constants.ts"), "export const PROBE = 1;\n") - .expect("write constants"); - fs::write(export_dir.join("kinds.ts"), "export const KIND = 1;\n") - .expect("write kinds"); - } - } -} -"#, - ); - } - - fn test_ts_mapping() -> contract::ExportMapping { - let mut packages = BTreeMap::new(); - packages.insert("radroots_core".to_string(), "@radroots/core".to_string()); - packages.insert( - "radroots_events_codec_wasm".to_string(), - "@radroots/events-codec-wasm".to_string(), - ); - contract::ExportMapping { - language: contract::ExportLanguage { - id: "ts".to_string(), - repository: "sdk-typescript".to_string(), - }, - packages, - artifacts: Some(contract::ExportArtifacts { - models_dir: Some("src/generated".to_string()), - constants_dir: Some("src/generated".to_string()), - wasm_dist_dir: Some("dist".to_string()), - manifest_file: Some("export-manifest.json".to_string()), - }), - } - } - - #[test] - fn cargo_test_failure_covers_stdout_and_empty_output_paths() { - let stdout_only = Command::new("sh") - .arg("-c") - .arg("printf 'stdout only'; exit 7") - .output() - .expect("stdout-only command"); - let stdout_msg = cargo_test_failure("radroots_a", &stdout_only); - assert!(stdout_msg.contains("cargo test failed for radroots_a (exit code 7): stdout only")); - - let no_output = Command::new("sh") - .arg("-c") - .arg("exit 9") - .output() - .expect("silent command"); - let no_output_msg = cargo_test_failure("radroots_b", &no_output); - assert_eq!( - no_output_msg, - "cargo test failed for radroots_b (exit code 9)" - ); - } - - #[cfg(unix)] - #[test] - fn cargo_test_failure_covers_signal_termination_status() { - let signal_output = Output { - status: ExitStatus::from_raw(9), - stdout: Vec::new(), - stderr: Vec::new(), - }; - let signal_msg = cargo_test_failure("radroots_signal", &signal_output); - assert_eq!( - signal_msg, - "cargo test failed for radroots_signal (terminated by signal)" - ); - } - - #[test] - fn selected_package_entries_match_crate_and_package_selectors() { - let mapping = test_ts_mapping(); - - let all = selected_package_entries(&mapping, None).expect("select all"); - assert_eq!(all.len(), 2); - - let by_crate = selected_package_entries(&mapping, Some("radroots_core")).expect("by crate"); - assert_eq!(by_crate.len(), 1); - assert_eq!(by_crate[0].0.as_str(), "radroots_core"); - - let by_short = selected_package_entries(&mapping, Some("core")).expect("by short crate"); - assert_eq!(by_short.len(), 1); - assert_eq!(by_short[0].1.as_str(), "@radroots/core"); - - let by_package = - selected_package_entries(&mapping, Some("@radroots/core")).expect("by package"); - assert_eq!(by_package.len(), 1); - assert_eq!(by_package[0].0.as_str(), "radroots_core"); - - let wasm = selected_package_entries(&mapping, Some("events-codec-wasm")).expect("wasm"); - assert_eq!(wasm.len(), 1); - assert_eq!(wasm[0].0.as_str(), "radroots_events_codec_wasm"); - } - - #[test] - fn selected_package_entries_fail_for_unknown_selector() { - let mapping = test_ts_mapping(); - let err = selected_package_entries(&mapping, Some("unknown")).expect_err("unknown"); - assert!(err.contains("unknown ts export crate selector")); - } - - #[test] - fn selected_package_entries_handle_prefixed_unknown_selectors() { - let mapping = test_ts_mapping(); - let by_crate = selected_package_entries(&mapping, Some("radroots_missing")) - .expect_err("unknown prefixed crate selector"); - assert!(by_crate.contains("unknown ts export crate selector")); - let by_package = selected_package_entries(&mapping, Some("@radroots/missing")) - .expect_err("unknown prefixed package selector"); - assert!(by_package.contains("unknown ts export crate selector")); - } - - #[test] - fn ts_mapping_and_artifacts_report_missing_entries() { - let root = unique_temp_dir("missing_ts_mapping"); - fs::create_dir_all(&root).expect("create root"); - let bundle = contract::ContractBundle { - root: root.clone(), - manifest: contract::ContractManifest { - contract: contract::ManifestContract { - name: "name".to_string(), - version: "1.0.0".to_string(), - source: "source".to_string(), - }, - surface: contract::Surface { - model_crates: vec!["radroots_a".to_string()], - algorithm_crates: vec!["radroots_b".to_string()], - wasm_crates: vec![], - }, - policy: contract::Policy { - exclude_internal_workspace_crates: true, - require_reproducible_exports: true, - require_conformance_vectors: true, - }, - }, - version: contract::VersionPolicy { - contract: contract::VersionContract { - version: "1.0.0".to_string(), - stability: "alpha".to_string(), - }, - semver: contract::SemverRules { - major_on: vec!["breaking".to_string()], - minor_on: vec!["feature".to_string()], - patch_on: vec!["fix".to_string()], - }, - compatibility: contract::CompatibilityRules { - requires_conformance_pass: true, - requires_export_manifest_diff: true, - requires_release_notes: true, - }, - }, - exports: Vec::new(), - operations_manifest: None, - sdk_exports: Vec::new(), - }; - let mapping_err = ts_export_mapping(&bundle).expect_err("missing ts mapping"); - assert!(mapping_err.contains("missing ts export mapping")); - - let mut packages = BTreeMap::new(); - packages.insert("radroots_a".to_string(), "@radroots/a".to_string()); - let no_artifacts = contract::ExportMapping { - language: contract::ExportLanguage { - id: "ts".to_string(), - repository: "sdk-typescript".to_string(), - }, - packages, - artifacts: None, - }; - let artifacts_err = ts_artifacts(&no_artifacts).expect_err("missing ts artifacts mapping"); - assert!(artifacts_err.contains("missing ts artifacts mapping")); - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn selected_package_entries_supports_package_candidate_lookup() { - let mut packages = BTreeMap::new(); - packages.insert( - "radroots_special".to_string(), - "@radroots/special-pkg".to_string(), - ); - let mapping = contract::ExportMapping { - language: contract::ExportLanguage { - id: "ts".to_string(), - repository: "sdk-typescript".to_string(), - }, - packages, - artifacts: Some(contract::ExportArtifacts::default()), - }; - let selected = - selected_package_entries(&mapping, Some("special-pkg")).expect("package candidate"); - assert_eq!(selected.len(), 1); - assert_eq!(selected[0].0.as_str(), "radroots_special"); - assert_eq!(selected[0].1.as_str(), "@radroots/special-pkg"); - } - - #[test] - fn package_dir_and_artifact_helpers_validate_values() { - let base = PathBuf::from("/tmp/base"); - assert_eq!(to_package_dir(&base, "@radroots/core"), base.join("core")); - assert_eq!(to_package_dir(&base, "custom"), base.join("custom")); - - let some = Some("src/generated".to_string()); - assert_eq!( - required_artifact_value(&some, "models_dir").expect("required value"), - "src/generated" - ); - let none = None; - assert!(required_artifact_value(&none, "models_dir").is_err()); - let blank = Some(" ".to_string()); - assert!(required_artifact_value(&blank, "models_dir").is_err()); - } - - #[test] - fn helper_error_paths_cover_copy_manifest_and_support_checks() { - let root = unique_temp_dir("helper_errors"); - fs::create_dir_all(&root).expect("create root"); - - let manifest_dir = root.join("crates").join("probe").join("Cargo.toml"); - fs::create_dir_all(&manifest_dir).expect("create directory in place of manifest"); - let supports_err = crate_supports_ts_rs(&root, "probe").expect_err("manifest read error"); - assert!(supports_err.contains("read")); - - let src_file = root.join("src").join("one.txt"); - write_file(&src_file, "one"); - let dst_parent_file = root.join("dst-parent-file"); - write_file(&dst_parent_file, "block"); - let create_err = copy_if_exists(&src_file, &dst_parent_file.join("out.txt")) - .expect_err("create parent error"); - assert!(create_err.contains("create")); - - let dst_file = root.join("dst-dir"); - fs::create_dir_all(&dst_file).expect("create destination directory"); - let copy_err = - copy_if_exists(&src_file, &dst_file).expect_err("copy to directory should fail"); - assert!(copy_err.contains("copy")); - - let missing_dir = root.join("missing-dir"); - assert_eq!( - copy_dir_contents(&missing_dir, &root.join("dst-missing")).expect("missing dir"), - 0 - ); - - let src_dir = root.join("src-dir"); - fs::create_dir_all(&src_dir).expect("create src dir"); - let dst_blocker = root.join("dst-blocker"); - write_file(&dst_blocker, "blocker"); - let dst_err = copy_dir_contents(&src_dir, &dst_blocker).expect_err("create dst error"); - assert!(dst_err.contains("create")); - - let src_file_not_dir = root.join("src-file-not-dir"); - write_file(&src_file_not_dir, "not dir"); - let read_dir_err = - copy_dir_contents(&src_file_not_dir, &root.join("dst-ok")).expect_err("read dir error"); - assert!(read_dir_err.contains("read dir")); - - let src_tree = root.join("src-tree"); - let dst_tree = root.join("dst-tree"); - fs::create_dir_all(&src_tree).expect("create src-tree"); - fs::create_dir_all(&dst_tree).expect("create dst-tree"); - write_file(&src_tree.join("entry.txt"), "entry"); - fs::create_dir_all(dst_tree.join("entry.txt")).expect("create colliding directory"); - let copy_tree_err = copy_dir_contents(&src_tree, &dst_tree).expect_err("copy tree error"); - assert!(copy_tree_err.contains("copy")); - - let current_is_file = root.join("manifest-file"); - write_file(&current_is_file, "x"); - let mut entries = Vec::new(); - let collect_err = collect_manifest_entries( - &root, - &current_is_file, - &root.join("skip.json"), - &mut entries, - ) - .expect_err("read dir error for file"); - assert!(collect_err.contains("read dir")); - - let other_root = unique_temp_dir("manifest_strip_prefix"); - fs::create_dir_all(&other_root).expect("create other root"); - write_file(&other_root.join("x.txt"), "x"); - let mut entries = Vec::new(); - let strip_err = collect_manifest_entries( - &root, - &other_root, - &other_root.join("skip.json"), - &mut entries, - ) - .expect_err("strip prefix error"); - assert!(strip_err.contains("strip prefix")); - fs::remove_dir_all(other_root).expect("remove other root"); - - fs::remove_dir_all(root).expect("remove helper root"); - } - - #[test] - fn copy_helpers_and_manifest_collection_cover_file_paths() { - let root = unique_temp_dir("copy_helpers"); - let src_file = root.join("src").join("one.txt"); - let dst_file = root.join("dst").join("one.txt"); - fs::create_dir_all(src_file.parent().expect("src parent")).expect("create src parent"); - fs::write(&src_file, "one").expect("write src file"); - assert!(copy_if_exists(&src_file, &dst_file).expect("copy file")); - assert_eq!(fs::read_to_string(&dst_file).expect("read dst file"), "one"); - - let missing = root.join("src").join("missing.txt"); - assert!(!copy_if_exists(&missing, &root.join("dst").join("missing.txt")).expect("missing")); - - let src_dir = root.join("src-tree"); - fs::create_dir_all(src_dir.join("nested")).expect("create src dir"); - fs::write(src_dir.join("a.txt"), "a").expect("write a"); - fs::write(src_dir.join("nested").join("b.txt"), "b").expect("write b"); - #[cfg(unix)] - { - use std::os::unix::fs::symlink; - symlink(src_dir.join("missing.txt"), src_dir.join("broken-link")) - .expect("create broken link"); - } - let dst_dir = root.join("dst-tree"); - let copied = copy_dir_contents(&src_dir, &dst_dir).expect("copy dir"); - assert_eq!(copied, 2); - assert!(dst_dir.join("a.txt").exists()); - assert!(dst_dir.join("nested").join("b.txt").exists()); - - let manifest_skip = dst_dir.join("export-manifest.json"); - fs::write(&manifest_skip, "{}").expect("write manifest skip"); - let mut entries = Vec::new(); - collect_manifest_entries(&dst_dir, &dst_dir, &manifest_skip, &mut entries) - .expect("collect entries"); - assert_eq!(entries.len(), 2); - assert!(entries.iter().any(|entry| entry.path == "a.txt")); - assert!(entries.iter().any(|entry| entry.path == "nested/b.txt")); - - fs::remove_dir_all(root).expect("remove temp root"); - } - - #[cfg(unix)] - #[test] - fn collect_manifest_entries_skips_symlinks() { - use std::os::unix::fs::{PermissionsExt, symlink}; - - let root = unique_temp_dir("manifest_symlink_skip"); - let source = root.join("source"); - fs::create_dir_all(&source).expect("create source"); - write_file(&source.join("a.txt"), "a"); - symlink(source.join("a.txt"), source.join("a-link")).expect("create symlink"); - symlink(source.join("missing.txt"), source.join("broken-link")) - .expect("create broken link"); - write_file(&source.join("secret.txt"), "secret"); - let mut restricted = fs::metadata(source.join("secret.txt")) - .expect("secret metadata") - .permissions(); - restricted.set_mode(0o000); - fs::set_permissions(source.join("secret.txt"), restricted).expect("set restricted mode"); - let mut entries = Vec::new(); - collect_manifest_entries( - &source, - &source, - &source.join("export-manifest.json"), - &mut entries, - ) - .expect("collect entries"); - assert_eq!(entries.len(), 2); - assert!(entries.iter().any(|entry| entry.path == "a.txt")); - assert!(entries.iter().any(|entry| entry.path == "a-link")); - assert!(!entries.iter().any(|entry| entry.path == "broken-link")); - assert!(!entries.iter().any(|entry| entry.path == "secret.txt")); - let mut readable = fs::metadata(source.join("secret.txt")) - .expect("secret metadata") - .permissions(); - readable.set_mode(0o600); - fs::set_permissions(source.join("secret.txt"), readable).expect("restore readable mode"); - fs::remove_dir_all(root).expect("remove temp root"); - } - - #[test] - fn export_ts_files_with_workspace_contract() { - let _guard = workspace_lock().lock().expect("workspace lock"); - let root = workspace_root(); - let bundle = contract::load_contract_bundle(&root).expect("load contract"); - contract::validate_contract_bundle(&bundle).expect("validate contract"); - let ts = ts_export_mapping(&bundle).expect("ts mapping"); - let artifacts = ts_artifacts(ts).expect("ts artifacts"); - let models_dir = required_artifact_value(&artifacts.models_dir, "models_dir") - .expect("models dir") - .to_string(); - let constants_dir = required_artifact_value(&artifacts.constants_dir, "constants_dir") - .expect("constants dir") - .to_string(); - - let source_root = root.join("target").join("ts-rs").join("core"); - fs::create_dir_all(&source_root).expect("create ts-rs source root"); - fs::write( - source_root.join("types.ts"), - "export type CoreProbe = { id: string };\n", - ) - .expect("write ts-rs model"); - let events_source = root.join("target").join("ts-rs").join("events"); - fs::create_dir_all(&events_source).expect("create events source"); - fs::write(events_source.join("constants.ts"), "export const A = 1;\n") - .expect("write events constants"); - fs::write(events_source.join("kinds.ts"), "export const K = 1;\n") - .expect("write events kinds"); - - let out_dir = root.join("target").join("xtask-export-tests").join( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time") - .as_nanos() - .to_string(), - ); - fs::create_dir_all(&out_dir).expect("create out dir"); - - export_ts_models(&root, &out_dir).expect("export models"); - assert!( - out_dir - .join("ts") - .join("packages") - .join("core") - .join(&models_dir) - .join("types.ts") - .exists() - ); - - export_ts_constants(&root, &out_dir).expect("export constants"); - let events_constants = out_dir - .join("ts") - .join("packages") - .join("events") - .join(&constants_dir) - .join("constants.ts"); - let events_kinds = out_dir - .join("ts") - .join("packages") - .join("events") - .join(&constants_dir) - .join("kinds.ts"); - assert!(events_source.join("constants.ts").exists()); - assert!(events_source.join("kinds.ts").exists()); - assert!(events_constants.exists()); - assert!(events_kinds.exists()); - - export_ts_wasm_artifacts(&root, &out_dir).expect("export wasm"); - let manifest_path = write_ts_export_manifest(&root, &out_dir).expect("write manifest"); - let manifest_raw = fs::read_to_string(&manifest_path).expect("read manifest"); - assert!(manifest_raw.contains("\"language\": \"ts\"")); - assert!(manifest_raw.contains("packages/core")); - - fs::remove_dir_all(&out_dir).expect("remove out dir"); - } - - #[test] - fn export_ts_wasm_artifacts_returns_ok_when_no_wasm_packages_are_selected() { - let root = create_synthetic_workspace("export_ts_no_wasm_packages", false); - let out_dir = root.join("out"); - fs::create_dir_all(&out_dir).expect("create output dir"); - - export_ts_wasm_artifacts(&root, &out_dir).expect("export wasm without wasm packages"); - assert!(!out_dir.join("ts").join("packages").exists()); - - let _ = fs::remove_dir_all(&root); - } - - #[test] - fn export_ts_wasm_artifacts_copies_selected_wasm_package_dist() { - let root = create_synthetic_workspace("export_ts_with_wasm_dist", false); - write_file( - &root.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" -"radroots_a_wasm" = "@radroots/a-wasm" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - write_file( - &root - .join("crates") - .join("a_wasm") - .join("pkg") - .join("dist") - .join("radroots_a_wasm.js"), - "export const probe = true;\n", - ); - let out_dir = root.join("out"); - fs::create_dir_all(&out_dir).expect("create output dir"); - - export_ts_wasm_artifacts(&root, &out_dir).expect("export wasm dist"); - assert!( - out_dir - .join("ts") - .join("packages") - .join("a-wasm") - .join("dist") - .join("radroots_a_wasm.js") - .exists() - ); - - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn crate_supports_ts_rs_reflects_manifest_presence() { - let root = unique_temp_dir("crate_supports_ts_rs"); - let crate_dir = root.join("crates").join("probe"); - fs::create_dir_all(&crate_dir).expect("create crate dir"); - fs::write( - crate_dir.join("Cargo.toml"), - "[package]\nname = \"probe\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[features]\nts-rs = []\n", - ) - .expect("write manifest"); - assert!(crate_supports_ts_rs(&root, "probe").expect("supports ts-rs")); - assert!(!crate_supports_ts_rs(&root, "missing").expect("missing crate")); - fs::remove_dir_all(root).expect("remove temp root"); - } - - #[test] - fn export_models_and_constants_report_missing_source_roots() { - let root = create_synthetic_workspace("export_missing_source", true); - let out_dir = root.join("out"); - fs::create_dir_all(&out_dir).expect("create out dir"); - - let models_err = export_ts_models(&root, &out_dir).expect_err("missing models source root"); - assert!(models_err.contains("missing ts-rs source root")); - - let constants_err = - export_ts_constants(&root, &out_dir).expect_err("missing constants source root"); - assert!(constants_err.contains("missing ts-rs source root")); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn export_models_reports_when_expected_files_are_missing() { - let root = create_synthetic_workspace("export_models_missing_files", true); - fs::create_dir_all(root.join("target").join("ts-rs")).expect("create ts-rs root"); - let out_dir = root.join("out"); - fs::create_dir_all(&out_dir).expect("create out dir"); - - let err = export_ts_models(&root, &out_dir).expect_err("expected model files are missing"); - assert!(err.contains("no ts model files were exported")); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn export_models_succeeds_when_expected_files_exist() { - let root = create_synthetic_workspace("export_models_success", true); - write_file( - &root.join("target").join("ts-rs").join("a").join("types.ts"), - "export type Probe = { id: string };\n", - ); - let out_dir = root.join("out"); - fs::create_dir_all(&out_dir).expect("create out dir"); - export_ts_models(&root, &out_dir).expect("export models"); - assert!( - out_dir - .join("ts") - .join("packages") - .join("a") - .join("src/generated") - .join("types.ts") - .exists() - ); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn export_models_skip_non_ts_rs_crates() { - let root = create_synthetic_workspace("export_models_skip_non_ts_rs", false); - write_file( - &root.join("target").join("ts-rs").join("a").join("types.ts"), - "export type Probe = { id: string };\n", - ); - let out_dir = root.join("out"); - fs::create_dir_all(&out_dir).expect("create out dir"); - export_ts_models(&root, &out_dir).expect("skip non ts-rs"); - assert!(!out_dir.join("ts").join("packages").join("a").exists()); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn write_manifest_reports_write_failures() { - let root = create_synthetic_workspace("manifest_write_failure", false); - write_file( - &root.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "packages" -"#, - ); - let out_dir = root.join("out"); - fs::create_dir_all(out_dir.join("ts").join("packages")).expect("create packages directory"); - let err = write_ts_export_manifest(&root, &out_dir).expect_err("manifest write to dir"); - assert!(err.contains("write")); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn write_manifest_reports_parent_create_failures() { - let root = create_synthetic_workspace("manifest_create_failure", false); - write_file( - &root.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "nested/export-manifest.json" -"#, - ); - let out_dir = root.join("out"); - fs::create_dir_all(&out_dir).expect("create out dir"); - write_file(&out_dir.join("ts"), "blocker"); - let err = - write_ts_export_manifest(&root, &out_dir).expect_err("manifest parent create fail"); - assert!(err.contains("create")); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn write_manifest_succeeds_for_synthetic_workspace() { - let root = create_synthetic_workspace("manifest_success", false); - let out_dir = root.join("out"); - write_file( - &out_dir - .join("ts") - .join("packages") - .join("a") - .join("src") - .join("generated") - .join("types.ts"), - "export type Probe = { id: string };\n", - ); - let manifest = write_ts_export_manifest(&root, &out_dir).expect("manifest success"); - assert!(manifest.exists()); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn generate_ts_rs_sources_reports_path_and_command_failures() { - let root_remove = create_synthetic_workspace("generate_remove_fail", true); - write_file(&root_remove.join("target").join("ts-rs"), "not-a-directory"); - let remove_err = generate_ts_rs_sources(&root_remove) - .expect_err("remove existing source root should fail"); - assert!(remove_err.contains("remove")); - let _ = fs::remove_dir_all(root_remove); - - let root_create = create_synthetic_workspace("generate_create_fail", true); - let _ = fs::remove_dir_all(root_create.join("target")); - write_file(&root_create.join("target"), "blocker"); - let create_err = generate_ts_rs_sources(&root_create) - .expect_err("create source root parent should fail"); - assert!(create_err.contains("create")); - let _ = fs::remove_dir_all(root_create); - - let root_no_expected = create_synthetic_workspace("generate_no_expected", false); - let generated = generate_ts_rs_sources(&root_no_expected).expect("no expected crates"); - assert_eq!(generated, root_no_expected.join("target").join("ts-rs")); - let _ = fs::remove_dir_all(root_no_expected); - - let root_command_fail = create_synthetic_workspace("generate_command_fail", true); - write_file( - &root_command_fail - .join("crates") - .join("a") - .join("src") - .join("lib.rs"), - "pub fn broken( {\n", - ); - let command_fail_err = generate_ts_rs_sources(&root_command_fail) - .expect_err("cargo test failure should surface"); - assert!(command_fail_err.contains("cargo test failed for radroots_a")); - assert!(command_fail_err.contains("unclosed delimiter")); - let _ = fs::remove_dir_all(root_command_fail); - - let root_spawn_fail = create_synthetic_workspace("generate_spawn_fail", true); - let mut spawn_fail_runner = - |crate_name: &str, _workspace_root: &Path, _export_dir: &Path| { - Err(format!( - "run cargo test for {crate_name}: synthetic spawn failure" - )) - }; - let spawn_fail_err = generate_ts_rs_sources_with_selector_and_runner( - &root_spawn_fail, - None, - &mut spawn_fail_runner, - ) - .expect_err("cargo spawn failure should surface"); - assert_eq!( - spawn_fail_err, - "run cargo test for radroots_a: synthetic spawn failure" - ); - let _ = fs::remove_dir_all(root_spawn_fail); - } - - #[test] - fn run_ts_rs_export_test_executes_successfully_for_ts_rs_crate() { - let _guard = workspace_lock().lock().expect("workspace lock"); - let root = create_synthetic_workspace("run_ts_rs_export_test_success", true); - let export_dir = root.join("target").join("ts-rs").join("a"); - let output = - run_ts_rs_export_test("radroots_a", &root, &export_dir).expect("cargo test success"); - assert!(output.status.success()); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn run_ts_rs_export_test_reports_spawn_failures() { - let missing_root = unique_temp_dir("run_ts_rs_export_test_missing_root"); - let export_dir = missing_root.join("target").join("ts-rs").join("a"); - let err = run_ts_rs_export_test("radroots_a", &missing_root, &export_dir) - .expect_err("missing current_dir should fail"); - assert!(err.contains("run cargo test for radroots_a:")); - } - - #[test] - fn generate_ts_rs_sources_succeeds_and_skips_non_ts_rs_crates() { - let _guard = workspace_lock().lock().expect("workspace lock"); - let root = create_synthetic_workspace("generate_skip_non_ts_rs", true); - write_file( - &root.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" -"radroots_b" = "@radroots/b" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - write_file( - &root.join("crates").join("a").join("src").join("lib.rs"), - "pub fn probe() {}\n", - ); - let generated = generate_ts_rs_sources(&root).expect("ts-rs generation should pass"); - assert!(generated.join("a").exists()); - assert!(!generated.join("b").exists()); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn wrapper_calls_succeed_for_bundle_and_single_crate() { - let _guard = workspace_lock().lock().expect("workspace lock"); - let root = create_synthetic_workspace("wrapper_bundle_success", true); - write_file( - &root.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" -"radroots_b" = "@radroots/b" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - write_ts_rs_probe_lib(&root); - - let out_dir = root.join("out"); - fs::create_dir_all(&out_dir).expect("create bundle out"); - let manifest = export_ts_bundle(&root, &out_dir).expect("bundle success"); - assert!(manifest.exists()); - assert!( - out_dir - .join("ts") - .join("packages") - .join("a") - .join("src/generated") - .join("types.ts") - .exists() - ); - assert!( - out_dir - .join("ts") - .join("packages") - .join("a") - .join("src/generated") - .join("constants.ts") - .exists() - ); - assert!( - out_dir - .join("ts") - .join("packages") - .join("a") - .join("src/generated") - .join("kinds.ts") - .exists() - ); - - let single_out = root.join("single-out"); - fs::create_dir_all(&single_out).expect("create single out"); - let single_manifest = - export_ts_bundle_for_crate(&root, &single_out, "radroots_a").expect("single bundle"); - assert!(single_manifest.exists()); - assert!( - single_out - .join("ts") - .join("packages") - .join("a") - .join("src/generated") - .join("types.ts") - .exists() - ); - - let _ = fs::remove_dir_all(&root); - } - - #[test] - fn wrapper_calls_surface_mid_pipeline_errors() { - let _guard = workspace_lock().lock().expect("workspace lock"); - - let bundle_models = create_synthetic_workspace("wrapper_bundle_models_fail", true); - write_ts_rs_probe_lib(&bundle_models); - let bundle_models_out = bundle_models.join("out"); - write_file( - &bundle_models_out.join("ts").join("packages").join("a"), - "blocker", - ); - let bundle_models_err = - export_ts_bundle(&bundle_models, &bundle_models_out).expect_err("bundle models fail"); - assert!(bundle_models_err.contains("create")); - let _ = fs::remove_dir_all(&bundle_models); - - let bundle_constants = create_synthetic_workspace("wrapper_bundle_constants_fail", true); - write_file( - &bundle_constants - .join("spec") - .join("exports") - .join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated/types.ts" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - write_ts_rs_probe_lib(&bundle_constants); - let bundle_constants_out = bundle_constants.join("out"); - fs::create_dir_all(&bundle_constants_out).expect("create bundle constants out"); - let bundle_constants_err = export_ts_bundle(&bundle_constants, &bundle_constants_out) - .expect_err("bundle constants fail"); - assert!(bundle_constants_err.contains("create")); - let _ = fs::remove_dir_all(&bundle_constants); - - let bundle_wasm = create_synthetic_workspace("wrapper_bundle_wasm_fail", true); - write_file( - &bundle_wasm.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" -"radroots_a_wasm" = "@radroots/a-wasm" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - write_ts_rs_probe_lib(&bundle_wasm); - write_file( - &bundle_wasm - .join("crates") - .join("a_wasm") - .join("pkg") - .join("dist") - .join("nested") - .join("artifact.js"), - "export const wasm = 1;\n", - ); - let bundle_wasm_out = bundle_wasm.join("out"); - write_file( - &bundle_wasm_out - .join("ts") - .join("packages") - .join("a-wasm") - .join("dist") - .join("nested"), - "blocker", - ); - let bundle_wasm_err = - export_ts_bundle(&bundle_wasm, &bundle_wasm_out).expect_err("bundle wasm fail"); - assert!(bundle_wasm_err.contains("create")); - let _ = fs::remove_dir_all(&bundle_wasm); - - let crate_models = create_synthetic_workspace("wrapper_crate_models_fail", true); - write_ts_rs_probe_lib(&crate_models); - let crate_models_out = crate_models.join("out"); - write_file( - &crate_models_out.join("ts").join("packages").join("a"), - "blocker", - ); - let crate_models_err = - export_ts_bundle_for_crate(&crate_models, &crate_models_out, "radroots_a") - .expect_err("crate models fail"); - assert!(crate_models_err.contains("create")); - let _ = fs::remove_dir_all(&crate_models); - - let crate_constants = create_synthetic_workspace("wrapper_crate_constants_fail", true); - write_file( - &crate_constants.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated/types.ts" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - write_ts_rs_probe_lib(&crate_constants); - let crate_constants_out = crate_constants.join("out"); - fs::create_dir_all(&crate_constants_out).expect("create crate constants out"); - let crate_constants_err = - export_ts_bundle_for_crate(&crate_constants, &crate_constants_out, "radroots_a") - .expect_err("crate constants fail"); - assert!(crate_constants_err.contains("create")); - let _ = fs::remove_dir_all(&crate_constants); - - let crate_wasm = create_synthetic_workspace("wrapper_crate_wasm_fail", true); - write_file( - &crate_wasm.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a_wasm" = "@radroots/a-wasm" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - write_file( - &crate_wasm - .join("crates") - .join("a_wasm") - .join("pkg") - .join("dist") - .join("nested") - .join("artifact.js"), - "export const wasm = 1;\n", - ); - let crate_wasm_out = crate_wasm.join("out"); - write_file( - &crate_wasm_out - .join("ts") - .join("packages") - .join("a-wasm") - .join("dist") - .join("nested"), - "blocker", - ); - let crate_wasm_err = - export_ts_bundle_for_crate(&crate_wasm, &crate_wasm_out, "radroots_a_wasm") - .expect_err("crate wasm fail"); - assert!(crate_wasm_err.contains("create")); - let _ = fs::remove_dir_all(&crate_wasm); - } - - #[test] - fn wrapper_calls_surface_contract_and_selector_errors() { - let missing_root = unique_temp_dir("wrapper_missing_contract"); - fs::create_dir_all(&missing_root).expect("create missing root"); - let missing_out = missing_root.join("out"); - fs::create_dir_all(&missing_out).expect("create missing out"); - assert!(export_ts_models(&missing_root, &missing_out).is_err()); - assert!(export_ts_constants(&missing_root, &missing_out).is_err()); - assert!(export_ts_wasm_artifacts(&missing_root, &missing_out).is_err()); - assert!(write_ts_export_manifest(&missing_root, &missing_out).is_err()); - assert!(generate_ts_rs_sources(&missing_root).is_err()); - assert!(export_ts_bundle(&missing_root, &missing_out).is_err()); - assert!(export_ts_bundle_for_crate(&missing_root, &missing_out, "radroots_a").is_err()); - let _ = fs::remove_dir_all(&missing_root); - - let invalid_contract = create_synthetic_workspace("wrapper_invalid_contract", true); - write_file( - &invalid_contract.join("spec").join("manifest.toml"), - r#"[contract] -name = "" -version = "1.0.0" -source = "synthetic" - -[surface] -model_crates = ["radroots_a"] -algorithm_crates = ["radroots_b"] -wasm_crates = ["radroots_a_wasm"] - -[policy] -exclude_internal_workspace_crates = true -require_reproducible_exports = true -require_conformance_vectors = true -"#, - ); - let invalid_out = invalid_contract.join("out"); - fs::create_dir_all(&invalid_out).expect("create invalid out"); - assert!(export_ts_models(&invalid_contract, &invalid_out).is_err()); - assert!(export_ts_constants(&invalid_contract, &invalid_out).is_err()); - assert!(export_ts_wasm_artifacts(&invalid_contract, &invalid_out).is_err()); - assert!(write_ts_export_manifest(&invalid_contract, &invalid_out).is_err()); - assert!(generate_ts_rs_sources(&invalid_contract).is_err()); - let _ = fs::remove_dir_all(&invalid_contract); - - let missing_ts_export = create_synthetic_workspace("wrapper_missing_ts_export", true); - write_file( - &missing_ts_export - .join("spec") - .join("exports") - .join("ts.toml"), - r#"[language] -id = "py" -repository = "sdk-python" - -[packages] -"radroots_a" = "radroots_a" -"#, - ); - let missing_ts_out = missing_ts_export.join("out"); - fs::create_dir_all(&missing_ts_out).expect("create missing ts out"); - assert!(export_ts_models(&missing_ts_export, &missing_ts_out).is_err()); - assert!(export_ts_constants(&missing_ts_export, &missing_ts_out).is_err()); - assert!(export_ts_wasm_artifacts(&missing_ts_export, &missing_ts_out).is_err()); - assert!(write_ts_export_manifest(&missing_ts_export, &missing_ts_out).is_err()); - assert!(generate_ts_rs_sources(&missing_ts_export).is_err()); - let _ = fs::remove_dir_all(&missing_ts_export); - - let selector_root = create_synthetic_workspace("wrapper_selector_errors", true); - let selector_out = selector_root.join("out"); - fs::create_dir_all(&selector_out).expect("create selector out"); - let models_selector_err = - export_ts_models_for_crate(&selector_root, &selector_out, "missing-crate") - .expect_err("models unknown selector"); - assert!(models_selector_err.contains("unknown ts export crate selector")); - let constants_selector_err = - export_ts_constants_for_crate(&selector_root, &selector_out, "missing-crate") - .expect_err("constants unknown selector"); - assert!(constants_selector_err.contains("unknown ts export crate selector")); - let wasm_selector_err = - export_ts_wasm_artifacts_for_crate(&selector_root, &selector_out, "missing-crate") - .expect_err("wasm unknown selector"); - assert!(wasm_selector_err.contains("unknown ts export crate selector")); - let generate_selector_err = - generate_ts_rs_sources_for_crate(&selector_root, "missing-crate") - .expect_err("generate unknown selector"); - assert!(generate_selector_err.contains("unknown ts export crate selector")); - let bundle_selector_err = - export_ts_bundle_for_crate(&selector_root, &selector_out, "missing-crate") - .expect_err("bundle unknown selector"); - assert!(bundle_selector_err.contains("unknown ts export crate selector")); - let _ = fs::remove_dir_all(&selector_root); - } - - #[test] - fn wrapper_calls_surface_artifact_and_copy_errors() { - let missing_artifacts = create_synthetic_workspace("wrapper_missing_artifacts", true); - write_file( - &missing_artifacts - .join("spec") - .join("exports") - .join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" -"#, - ); - let missing_artifacts_out = missing_artifacts.join("out"); - fs::create_dir_all(&missing_artifacts_out).expect("create missing artifacts out"); - assert!(export_ts_models(&missing_artifacts, &missing_artifacts_out).is_err()); - assert!(export_ts_constants(&missing_artifacts, &missing_artifacts_out).is_err()); - assert!(export_ts_wasm_artifacts(&missing_artifacts, &missing_artifacts_out).is_err()); - assert!(write_ts_export_manifest(&missing_artifacts, &missing_artifacts_out).is_err()); - let _ = fs::remove_dir_all(&missing_artifacts); - - let missing_models_dir = create_synthetic_workspace("wrapper_missing_models_dir", true); - write_file( - &missing_models_dir - .join("spec") - .join("exports") - .join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - let missing_models_out = missing_models_dir.join("out"); - fs::create_dir_all(&missing_models_out).expect("create missing models out"); - let missing_models_err = - export_ts_models(&missing_models_dir, &missing_models_out).expect_err("models dir"); - assert!(missing_models_err.contains("artifacts fields must be non-empty for ts")); - let _ = fs::remove_dir_all(&missing_models_dir); - - let missing_constants_dir = - create_synthetic_workspace("wrapper_missing_constants_dir", true); - write_file( - &missing_constants_dir - .join("spec") - .join("exports") - .join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "src/generated" -constants_dir = "" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - let missing_constants_out = missing_constants_dir.join("out"); - fs::create_dir_all(&missing_constants_out).expect("create missing constants out"); - let missing_constants_err = - export_ts_constants(&missing_constants_dir, &missing_constants_out) - .expect_err("constants dir"); - assert!(missing_constants_err.contains("artifacts fields must be non-empty for ts")); - let _ = fs::remove_dir_all(&missing_constants_dir); - - let missing_wasm_dir = create_synthetic_workspace("wrapper_missing_wasm_dir", true); - write_file( - &missing_wasm_dir - .join("spec") - .join("exports") - .join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a_wasm" = "@radroots/a-wasm" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "" -manifest_file = "export-manifest.json" -"#, - ); - let missing_wasm_out = missing_wasm_dir.join("out"); - fs::create_dir_all(&missing_wasm_out).expect("create missing wasm out"); - let missing_wasm_err = - export_ts_wasm_artifacts(&missing_wasm_dir, &missing_wasm_out).expect_err("wasm dir"); - assert!(missing_wasm_err.contains("artifacts fields must be non-empty for ts")); - let _ = fs::remove_dir_all(&missing_wasm_dir); - - let missing_manifest_file = - create_synthetic_workspace("wrapper_missing_manifest_file", true); - write_file( - &missing_manifest_file - .join("spec") - .join("exports") - .join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "" -"#, - ); - let missing_manifest_out = missing_manifest_file.join("out"); - fs::create_dir_all(&missing_manifest_out).expect("create missing manifest out"); - let missing_manifest_err = - write_ts_export_manifest(&missing_manifest_file, &missing_manifest_out) - .expect_err("manifest file"); - assert!(missing_manifest_err.contains("artifacts fields must be non-empty for ts")); - let _ = fs::remove_dir_all(&missing_manifest_file); - - let models_copy_err_root = create_synthetic_workspace("wrapper_models_copy_err", true); - write_file( - &models_copy_err_root - .join("target") - .join("ts-rs") - .join("a") - .join("types.ts"), - "export type Probe = { id: string };\n", - ); - let models_copy_out = models_copy_err_root.join("out"); - write_file( - &models_copy_out.join("ts").join("packages").join("a"), - "blocker", - ); - let models_copy_err = - export_ts_models(&models_copy_err_root, &models_copy_out).expect_err("copy models"); - assert!(models_copy_err.contains("create")); - let _ = fs::remove_dir_all(&models_copy_err_root); - - let constants_copy_err_root = - create_synthetic_workspace("wrapper_constants_copy_err", true); - write_file( - &constants_copy_err_root - .join("target") - .join("ts-rs") - .join("a") - .join("constants.ts"), - "export const A = 1;\n", - ); - write_file( - &constants_copy_err_root - .join("target") - .join("ts-rs") - .join("a") - .join("kinds.ts"), - "export const K = 1;\n", - ); - let constants_copy_out = constants_copy_err_root.join("out"); - write_file( - &constants_copy_out.join("ts").join("packages").join("a"), - "blocker", - ); - let constants_copy_err = export_ts_constants(&constants_copy_err_root, &constants_copy_out) - .expect_err("copy constants"); - assert!(constants_copy_err.contains("create")); - let _ = fs::remove_dir_all(&constants_copy_err_root); - - let wasm_copy_err_root = create_synthetic_workspace("wrapper_wasm_copy_err", true); - write_file( - &wasm_copy_err_root - .join("spec") - .join("exports") - .join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a_wasm" = "@radroots/a-wasm" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -"#, - ); - write_file( - &wasm_copy_err_root - .join("crates") - .join("a_wasm") - .join("pkg") - .join("dist") - .join("nested") - .join("artifact.js"), - "export const wasm = 1;\n", - ); - let wasm_copy_out = wasm_copy_err_root.join("out"); - write_file( - &wasm_copy_out - .join("ts") - .join("packages") - .join("a-wasm") - .join("dist") - .join("nested"), - "blocker", - ); - let wasm_copy_err = - export_ts_wasm_artifacts(&wasm_copy_err_root, &wasm_copy_out).expect_err("copy wasm"); - assert!(wasm_copy_err.contains("create")); - let _ = fs::remove_dir_all(&wasm_copy_err_root); - - let manifest_collect_err_root = - create_synthetic_workspace("wrapper_manifest_collect_err", true); - let manifest_collect_out = manifest_collect_err_root.join("out"); - write_file( - &manifest_collect_out.join("ts").join("packages"), - "not-a-directory", - ); - let manifest_collect_err = - write_ts_export_manifest(&manifest_collect_err_root, &manifest_collect_out) - .expect_err("manifest collect error"); - assert!(manifest_collect_err.contains("read dir")); - let _ = fs::remove_dir_all(&manifest_collect_err_root); - - let recursive_copy_root = unique_temp_dir("wrapper_recursive_copy"); - let recursive_src = recursive_copy_root.join("src"); - let recursive_dst = recursive_copy_root.join("dst"); - write_file(&recursive_src.join("nested").join("value.txt"), "value"); - write_file(&recursive_dst.join("nested"), "blocker"); - let recursive_copy_err = - copy_dir_contents(&recursive_src, &recursive_dst).expect_err("recursive copy error"); - assert!(recursive_copy_err.contains("create")); - let _ = fs::remove_dir_all(&recursive_copy_root); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - - let recursive_manifest_root = unique_temp_dir("wrapper_recursive_manifest"); - let recursive_manifest_src = recursive_manifest_root.join("src"); - write_file( - &recursive_manifest_src.join("nested").join("value.txt"), - "value", - ); - let mut restricted = fs::metadata(recursive_manifest_src.join("nested")) - .expect("nested metadata") - .permissions(); - restricted.set_mode(0o000); - fs::set_permissions(recursive_manifest_src.join("nested"), restricted) - .expect("set nested restricted mode"); - - let mut entries = Vec::new(); - let recursive_manifest_err = collect_manifest_entries( - &recursive_manifest_src, - &recursive_manifest_src, - &recursive_manifest_src.join("skip.json"), - &mut entries, - ) - .expect_err("recursive manifest error"); - assert!(recursive_manifest_err.contains("read dir")); - - let mut readable = fs::metadata(recursive_manifest_src.join("nested")) - .expect("nested metadata") - .permissions(); - readable.set_mode(0o755); - fs::set_permissions(recursive_manifest_src.join("nested"), readable) - .expect("restore nested readable mode"); - let _ = fs::remove_dir_all(&recursive_manifest_root); - } - } -} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -2,7 +2,6 @@ mod contract; mod coverage; -mod export_ts; use std::env; use std::path::{Path, PathBuf}; @@ -10,12 +9,6 @@ use std::process::ExitCode; fn usage() { eprintln!("usage:"); - eprintln!(" cargo xtask sdk export-ts [--out <dir>]"); - eprintln!(" cargo xtask sdk export-ts-crate --crate <crate> [--out <dir>]"); - eprintln!(" cargo xtask sdk export-ts-models [--out <dir>]"); - eprintln!(" cargo xtask sdk export-ts-constants [--out <dir>]"); - eprintln!(" cargo xtask sdk export-ts-wasm [--out <dir>]"); - eprintln!(" cargo xtask sdk export-manifest [--out <dir>]"); eprintln!(" cargo xtask sdk validate"); eprintln!(" cargo xtask sdk release preflight"); eprintln!(" cargo xtask sdk coverage run-crate --crate <crate> [--out <dir>]"); @@ -50,111 +43,6 @@ fn workspace_root() -> PathBuf { workspace_root_with_override(override_root.as_deref()) } -fn parse_out_dir(args: &[String], workspace_root: &Path) -> Result<PathBuf, String> { - if args.is_empty() { - return Ok(workspace_root.join("target").join("sdk-export")); - } - if args.len() == 2 && args[0] == "--out" { - return Ok(PathBuf::from(&args[1])); - } - Err("invalid export args, expected --out <dir>".to_string()) -} - -fn parse_crate_out_dir( - args: &[String], - workspace_root: &Path, -) -> Result<(String, PathBuf), String> { - let mut crate_selector = None; - let mut out_dir = workspace_root.join("target").join("sdk-export"); - let mut index = 0usize; - while index < args.len() { - match args[index].as_str() { - "--crate" => { - let Some(value) = args.get(index + 1) else { - return Err("invalid export args, expected --crate <crate>".to_string()); - }; - crate_selector = Some(value.clone()); - index += 2; - } - "--out" => { - let Some(value) = args.get(index + 1) else { - return Err("invalid export args, expected --out <dir>".to_string()); - }; - out_dir = PathBuf::from(value); - index += 2; - } - _ => { - return Err( - "invalid export args, expected --crate <crate> [--out <dir>]".to_string(), - ); - } - } - } - let crate_selector = - crate_selector.ok_or_else(|| "missing required --crate <crate>".to_string())?; - Ok((crate_selector, out_dir)) -} - -fn export_ts_models_with_root(args: &[String], root: &Path) -> Result<(), String> { - let out_dir = parse_out_dir(args, root)?; - export_ts::export_ts_models(root, &out_dir) -} - -fn export_ts_models(args: &[String]) -> Result<(), String> { - let root = workspace_root(); - export_ts_models_with_root(args, &root) -} - -fn export_ts_constants_with_root(args: &[String], root: &Path) -> Result<(), String> { - let out_dir = parse_out_dir(args, root)?; - export_ts::export_ts_constants(root, &out_dir) -} - -fn export_ts_constants(args: &[String]) -> Result<(), String> { - let root = workspace_root(); - export_ts_constants_with_root(args, &root) -} - -fn export_ts_wasm_with_root(args: &[String], root: &Path) -> Result<(), String> { - let out_dir = parse_out_dir(args, &root)?; - export_ts::export_ts_wasm_artifacts(root, &out_dir) -} - -fn export_ts_wasm(args: &[String]) -> Result<(), String> { - let root = workspace_root(); - export_ts_wasm_with_root(args, &root) -} - -fn export_manifest_with_root(args: &[String], root: &Path) -> Result<(), String> { - let out_dir = parse_out_dir(args, root)?; - export_ts::write_ts_export_manifest(root, &out_dir).map(|_| ()) -} - -fn export_manifest(args: &[String]) -> Result<(), String> { - let root = workspace_root(); - export_manifest_with_root(args, &root) -} - -fn export_ts_with_root(args: &[String], root: &Path) -> Result<(), String> { - let out_dir = parse_out_dir(args, root)?; - export_ts::export_ts_bundle(root, &out_dir).map(|_| ()) -} - -fn export_ts(args: &[String]) -> Result<(), String> { - let root = workspace_root(); - export_ts_with_root(args, &root) -} - -fn export_ts_crate_with_root(args: &[String], root: &Path) -> Result<(), String> { - let (crate_selector, out_dir) = parse_crate_out_dir(args, root)?; - export_ts::export_ts_bundle_for_crate(root, &out_dir, &crate_selector).map(|_| ()) -} - -fn export_ts_crate(args: &[String]) -> Result<(), String> { - let root = workspace_root(); - export_ts_crate_with_root(args, &root) -} - fn validate_contract() -> Result<(), String> { let root = workspace_root(); contract::load_contract_bundle(&root) @@ -174,12 +62,6 @@ fn run_release(args: &[String]) -> Result<(), String> { fn run_sdk(args: &[String]) -> Result<(), String> { match args.first().map(String::as_str) { - Some("export-ts") => export_ts(&args[1..]), - Some("export-ts-crate") => export_ts_crate(&args[1..]), - Some("export-ts-models") => export_ts_models(&args[1..]), - Some("export-ts-constants") => export_ts_constants(&args[1..]), - Some("export-ts-wasm") => export_ts_wasm(&args[1..]), - Some("export-manifest") => export_manifest(&args[1..]), Some("validate") => validate_contract(), Some("release") => run_release(&args[1..]), Some("coverage") => coverage::run(&args[1..]), @@ -266,225 +148,10 @@ mod tests { } } - fn create_synthetic_export_workspace(prefix: &str) -> PathBuf { - let root = unique_temp_dir(prefix); - fs::create_dir_all(&root).expect("create root"); - write_file( - &root.join("Cargo.toml"), - r#"[workspace] -members = ["crates/a", "crates/b"] -resolver = "2" -"#, - ); - write_file( - &root.join("crates").join("a").join("Cargo.toml"), - r#"[package] -name = "radroots_a" -publish = ["crates-io"] -version = "0.1.0" -edition = "2024" -description = "crate a" -repository = "https://example.com/a" -homepage = "https://example.com/a" -documentation = "https://docs.example.com/a" -readme = "README" - -[features] -ts-rs = [] -"#, - ); - write_file( - &root.join("crates").join("a").join("src").join("lib.rs"), - r#"pub fn crate_a() {} - -#[cfg(test)] -mod tests { - use std::fs; - use std::path::PathBuf; - - #[test] - fn write_ts_exports() { - if let Ok(path) = std::env::var("RADROOTS_TS_RS_EXPORT_DIR") { - let export_dir = PathBuf::from(path); - let _ = fs::create_dir_all(&export_dir); - fs::write( - export_dir.join("types.ts"), - "export type Probe = { id: string };\n", - ) - .expect("write generated types"); - } - } -} -"#, - ); - write_file( - &root.join("crates").join("b").join("Cargo.toml"), - r#"[package] -name = "radroots_b" -version = "0.1.0" -edition = "2024" -publish = false -"#, - ); - write_file( - &root.join("crates").join("b").join("src").join("lib.rs"), - "pub fn crate_b() {}\n", - ); - write_file( - &root.join("crates").join("core").join("src").join("unit.rs"), - r#"pub enum RadrootsCoreUnitDimension { - Count, - Mass, - Volume, -} -"#, - ); - write_file( - &root.join("spec").join("manifest.toml"), - r#"[contract] -name = "radroots_contract" -version = "1.0.0" -source = "synthetic" - -[surface] -model_crates = ["radroots_a"] -algorithm_crates = ["radroots_b"] -wasm_crates = ["radroots_a_wasm"] - -[policy] -exclude_internal_workspace_crates = true -require_reproducible_exports = true -require_conformance_vectors = true -"#, - ); - write_file( - &root.join("spec").join("version.toml"), - r#"[contract] -version = "1.0.0" -stability = "alpha" - -[semver] -major_on = ["breaking"] -minor_on = ["feature"] -patch_on = ["fix"] - -[compatibility] -requires_conformance_pass = true -requires_export_manifest_diff = true -requires_release_notes = true -"#, - ); - write_file( - &root.join("spec").join("exports").join("ts.toml"), - r#"[language] -id = "ts" -repository = "sdk-typescript" - -[packages] -"radroots_a" = "@radroots/a" - -[artifacts] -models_dir = "src/generated" -constants_dir = "src/generated" -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 -fail_under_functions = 100.0 -fail_under_regions = 100.0 -fail_under_branches = 100.0 -require_branches = true - -[required] -crates = ["radroots_a", "radroots_b"] -"#, - ); - write_file( - &root - .join("contracts") - .join("release") - .join("mounted-rust-crates") - .join("publish-policy.toml"), - r#"[release] -version = "1.0.0" - -[publish] -crates = ["radroots_a"] - -[internal] -crates = ["radroots_b"] - -[publish_order] -crates = ["radroots_a"] -"#, - ); - root - } - #[test] - fn workspace_root_resolves_and_parse_helpers_cover_branches() { + fn workspace_root_resolves() { let root = workspace_root(); assert!(root.join("Cargo.toml").exists()); - - let default_out = parse_out_dir(&[], &root).expect("default out dir"); - assert_eq!(default_out, root.join("target").join("sdk-export")); - - let custom_out = parse_out_dir(&["--out".to_string(), "custom/out".to_string()], &root) - .expect("custom out dir"); - assert_eq!(custom_out, PathBuf::from("custom/out")); - - let invalid_out = parse_out_dir(&["--bad".to_string()], &root).expect_err("invalid out"); - assert!(invalid_out.contains("invalid export args")); - let invalid_out_pair = - parse_out_dir(&["--bad".to_string(), "x".to_string()], &root).expect_err("invalid out"); - assert!(invalid_out_pair.contains("invalid export args")); - - let parsed = parse_crate_out_dir( - &[ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - "my/out".to_string(), - ], - &root, - ) - .expect("parsed crate out"); - assert_eq!(parsed.0, "radroots_core".to_string()); - assert_eq!(parsed.1, PathBuf::from("my/out")); - - let missing_crate = parse_crate_out_dir(&["--out".to_string(), "x".to_string()], &root) - .expect_err("missing crate selector"); - assert!(missing_crate.contains("missing required --crate")); - - let invalid_crate_args = parse_crate_out_dir( - &[ - "--crate".to_string(), - "radroots_core".to_string(), - "--bad".to_string(), - ], - &root, - ) - .expect_err("invalid crate args"); - assert!(invalid_crate_args.contains("invalid export args")); - - let missing_crate_value = - parse_crate_out_dir(&["--crate".to_string()], &root).expect_err("missing crate value"); - assert!(missing_crate_value.contains("expected --crate <crate>")); - - let missing_out_value = parse_crate_out_dir( - &[ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - ], - &root, - ) - .expect_err("missing out value"); - assert!(missing_out_value.contains("expected --out <dir>")); } #[test] @@ -513,42 +180,6 @@ crates = ["radroots_a"] } #[test] - fn export_wrappers_cover_success_and_error_paths() { - let _guard = lock_workspace(); - let root = create_synthetic_export_workspace("export_wrappers"); - let out_dir = unique_temp_dir("export_wrappers"); - fs::create_dir_all(&out_dir).expect("create out dir"); - - let invalid_args = vec!["--bad".to_string()]; - assert!(export_ts_models_with_root(&invalid_args, &root).is_err()); - assert!(export_ts_constants_with_root(&invalid_args, &root).is_err()); - assert!(export_ts_wasm_with_root(&invalid_args, &root).is_err()); - assert!(export_manifest_with_root(&invalid_args, &root).is_err()); - assert!(export_ts_with_root(&invalid_args, &root).is_err()); - assert!(export_ts_crate_with_root(&invalid_args, &root).is_err()); - - let args = vec!["--out".to_string(), out_dir.display().to_string()]; - export_ts_with_root(&args, &root).expect("export ts bundle"); - export_manifest_with_root(&args, &root).expect("export manifest"); - export_ts_wasm_with_root(&args, &root).expect("export wasm"); - export_ts_constants_with_root(&args, &root).expect("export constants"); - export_ts_models_with_root(&args, &root).expect("export models"); - - let crate_args = vec![ - "--crate".to_string(), - "a".to_string(), - "--out".to_string(), - out_dir.display().to_string(), - ]; - export_ts_crate_with_root(&crate_args, &root).expect("export ts crate"); - - assert!(out_dir.join("ts").exists()); - - let _ = fs::remove_dir_all(out_dir); - let _ = fs::remove_dir_all(root); - } - - #[test] fn lock_workspace_recovers_from_poisoned_mutex() { let handle = std::thread::spawn(|| { let _guard = workspace_lock().lock().expect("lock workspace"); @@ -665,14 +296,8 @@ crates = ["radroots_a"] } #[test] - fn run_sdk_dispatches_export_and_validate_commands() { + fn run_sdk_dispatches_validate_command() { let _guard = lock_workspace(); - assert!(run_sdk(&["export-ts".to_string(), "--bad".to_string()]).is_err()); - assert!(run_sdk(&["export-ts-crate".to_string(), "--bad".to_string()]).is_err()); - assert!(run_sdk(&["export-ts-models".to_string(), "--bad".to_string()]).is_err()); - assert!(run_sdk(&["export-ts-constants".to_string(), "--bad".to_string()]).is_err()); - assert!(run_sdk(&["export-ts-wasm".to_string(), "--bad".to_string()]).is_err()); - assert!(run_sdk(&["export-manifest".to_string(), "--bad".to_string()]).is_err()); run_sdk(&["validate".to_string()]).expect("sdk validate"); } } diff --git a/docs/nix.md b/docs/nix.md @@ -83,11 +83,9 @@ Repo-aware command apps: nix run .#fmt nix run .#check nix run .#contract -nix run .#export-ts nix run .#coverage-report nix run .#wasm-builds nix run .#release-preflight -nix run .#validate-sdk-typescript -- ./sdk-typescript ``` `nix flake check` is intentionally limited to pure surfaces: @@ -99,8 +97,6 @@ nix run .#validate-sdk-typescript -- ./sdk-typescript Repo-aware flows stay behind `nix run` apps because they need a real checkout: -- `sdk export-ts` writes into repo-local `target/` -- sdk sync validation runs `bun` against a checked-out `sdk-typescript` repo path - coverage refresh and release preflight produce repo-local artifacts derived from measured per-crate gate reports - wasm packaging writes package output directories - publish commands read runtime tokens and the live checkout state diff --git a/nix/apps.nix b/nix/apps.nix @@ -67,15 +67,6 @@ in pathPrefix = coveragePath; }; - export-ts = mkRepoApp { - name = "export-ts"; - description = "Export generated typescript sdk artifacts"; - runtimeInputs = common.runtimeInputs.stable; - command = '' - cargo run -q -p xtask -- sdk export-ts "$@" - ''; - }; - guards = mkRepoApp { name = "guards"; description = "Run repository guard scripts"; @@ -109,13 +100,6 @@ in pathPrefix = coveragePath; }; - validate-sdk-typescript = mkRepoApp { - name = "validate-sdk-typescript"; - description = "Validate the synced sdk-typescript checkout with bun"; - runtimeInputs = common.runtimeInputs.sync; - command = common.validateSdkTypescriptCommand; - }; - wasm-builds = mkRepoApp { name = "wasm-builds"; description = "Build the wasm packages defined by the workspace makefile"; diff --git a/nix/common.nix b/nix/common.nix @@ -83,9 +83,6 @@ let python3 ] ++ darwinBuildInputs; - syncRuntimeInputs = stableRuntimeInputs ++ [ - pkgs.bun - ]; coverageRuntimeInputs = stableRuntimeInputs ++ [ toolchains.coverage cargoLlvmCov @@ -191,8 +188,6 @@ let cargo check -q ${sdkContractCargoArgs} cargo test -q ${sdkContractCargoArgs} cargo run -q -p xtask -- sdk validate - cargo run -q -p xtask -- sdk export-ts --out target/sdk-export-ci - test -f target/sdk-export-ci/ts/export-manifest.json ''; wasmBuildsCommand = '' make build @@ -200,24 +195,6 @@ let releasePreflightCommand = '' ./scripts/ci/release_preflight.sh ''; - validateSdkTypescriptCommand = '' - if [ "$#" -ne 1 ]; then - echo "usage: validate-sdk-typescript <path-to-sdk-typescript-checkout>" >&2 - exit 1 - fi - - target_dir="$1" - if [ ! -d "$target_dir" ]; then - echo "sdk-typescript checkout not found at $target_dir" >&2 - exit 1 - fi - - cd "$target_dir" - bun install --frozen-lockfile - bun run typecheck - bun run build - bun run test - ''; coverageReportCommand = '' rm -rf target/sdk-coverage mkdir -p target/sdk-coverage @@ -338,7 +315,6 @@ in releasePreflightCommand sdkContractCargoArgs sharedEnv - validateSdkTypescriptCommand version wasmBuildsCommand xtaskPackage @@ -349,7 +325,6 @@ in runtimeInputs = { stable = stableRuntimeInputs; - sync = syncRuntimeInputs; coverage = coverageRuntimeInputs; release = releaseRuntimeInputs; wasm = wasmRuntimeInputs; diff --git a/spec/RCLD.md b/spec/RCLD.md @@ -744,8 +744,8 @@ This facade should: - parses a crate-keyed surface - validates crate-keyed export coverage -- exports TypeScript by iterating crate-to-package mappings -- has tests that assert TypeScript export coverage matches model plus wasm crates +- retains TypeScript export metadata as part of the spec surface +- no longer owns downstream SDK packaging or repo-sync orchestration ### Required Changes @@ -758,25 +758,21 @@ This facade should: - conformance vector presence for each public operation - language export manifests mapping approved operations 4. replace crate-coverage assertions with operation-coverage assertions -5. update export manifest generation to report operation coverage -6. keep current crate provenance checks only as implementation validation +5. keep current crate provenance checks only as implementation validation ### Recommended `xtask` Command Evolution -Keep existing commands temporarily: +Keep only the validation commands in the mounted repo: -- `sdk export-ts` - `sdk validate` -Add new migration-aware behavior behind the same commands: +Add migration-aware behavior behind the retained validation command: - `sdk validate` validates both old and new contract surfaces -- `sdk export-ts` assembles an operation-first TypeScript SDK package from approved operations Optional additive commands: - `sdk validate-operations` -- `sdk export-ts-sdk` - `sdk conformance check --language <id>` ## Language SDK Strategy @@ -838,9 +834,9 @@ Implementation recommendation: ### Phase 4: Migrate TypeScript Export -- shift `xtask export-ts` to operation-first packaging +- move TypeScript package assembly to the owning downstream SDK repo or monorepo control plane - ship one main external TypeScript SDK package -- keep transitional generated artifacts internal if necessary +- keep only the spec metadata needed to describe that package here ### Phase 5: Introduce Python