lib

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

commit 431901f8c5524948c02946651bede377494f7aef
parent 8c3c1496ff6db8b4fa10b261fa04539341469df8
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Feb 2026 03:10:12 +0000

coverage: add workspace rollout parity guard


- expand coverage rollout contract to include every workspace crate entry
- validate rollout names against workspace package names in xtask contract checks
- enforce required list parity with rollout required statuses and contiguous order fields
- add xtask tests for rollout workspace parity and required status alignment

Diffstat:
Mcontract/coverage/rollout.toml | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/xtask/src/contract.rs | 236++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 330 insertions(+), 1 deletion(-)

diff --git a/contract/coverage/rollout.toml b/contract/coverage/rollout.toml @@ -52,3 +52,98 @@ order = 8 name = "xtask" status = "planned" order = 9 + +[[rollout.crates]] +name = "radroots-app-core" +status = "planned" +order = 10 + +[[rollout.crates]] +name = "radroots-app-ffi-swift" +status = "planned" +order = 11 + +[[rollout.crates]] +name = "radroots-app-wasm" +status = "planned" +order = 12 + +[[rollout.crates]] +name = "radroots-events-indexed" +status = "planned" +order = 13 + +[[rollout.crates]] +name = "radroots-log" +status = "planned" +order = 14 + +[[rollout.crates]] +name = "radroots-net-core" +status = "planned" +order = 15 + +[[rollout.crates]] +name = "radroots-net" +status = "planned" +order = 16 + +[[rollout.crates]] +name = "radroots-nostr" +status = "planned" +order = 17 + +[[rollout.crates]] +name = "radroots-nostr-accounts" +status = "planned" +order = 18 + +[[rollout.crates]] +name = "radroots-nostr-ndb" +status = "planned" +order = 19 + +[[rollout.crates]] +name = "radroots-nostr-runtime" +status = "planned" +order = 20 + +[[rollout.crates]] +name = "radroots-runtime" +status = "planned" +order = 21 + +[[rollout.crates]] +name = "radroots-sql-core" +status = "planned" +order = 22 + +[[rollout.crates]] +name = "radroots-sql-wasm-core" +status = "planned" +order = 23 + +[[rollout.crates]] +name = "radroots-sql-wasm-bridge" +status = "planned" +order = 24 + +[[rollout.crates]] +name = "radroots-tangle-events" +status = "planned" +order = 25 + +[[rollout.crates]] +name = "radroots-tangle-db" +status = "planned" +order = 26 + +[[rollout.crates]] +name = "radroots-tangle-events-wasm" +status = "planned" +order = 27 + +[[rollout.crates]] +name = "radroots-tangle-db-wasm" +status = "planned" +order = 28 diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -1,7 +1,7 @@ #![forbid(unsafe_code)] use serde::Deserialize; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; @@ -89,6 +89,53 @@ pub struct ContractBundle { pub exports: Vec<ExportMapping>, } +#[derive(Debug, Deserialize)] +struct WorkspaceCargoManifest { + workspace: WorkspaceSection, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceSection { + members: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct PackageCargoManifest { + package: PackageSection, +} + +#[derive(Debug, Deserialize)] +struct PackageSection { + name: String, +} + +#[derive(Debug, Deserialize)] +struct CoverageRolloutFile { + rollout: CoverageRolloutSection, +} + +#[derive(Debug, Deserialize)] +struct CoverageRolloutSection { + crates: Vec<CoverageRolloutCrate>, +} + +#[derive(Debug, Deserialize)] +struct CoverageRolloutCrate { + name: String, + status: String, + order: u32, +} + +#[derive(Debug, Deserialize)] +struct CoverageRequiredFile { + required: CoverageRequiredSection, +} + +#[derive(Debug, Deserialize)] +struct CoverageRequiredSection { + crates: Vec<String>, +} + fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { let raw = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?; toml::from_str::<T>(&raw).map_err(|e| format!("parse {}: {e}", path.display())) @@ -98,6 +145,30 @@ fn contract_root(workspace_root: &Path) -> PathBuf { workspace_root.join("contract") } +fn workspace_package_names(workspace_root: &Path) -> Result<Vec<String>, String> { + let workspace_manifest = + parse_toml::<WorkspaceCargoManifest>(&workspace_root.join("Cargo.toml"))?; + let mut names = Vec::with_capacity(workspace_manifest.workspace.members.len()); + for member in workspace_manifest.workspace.members { + let member_manifest = workspace_root.join(member).join("Cargo.toml"); + let package_manifest = parse_toml::<PackageCargoManifest>(&member_manifest)?; + names.push(package_manifest.package.name); + } + Ok(names) +} + +fn load_coverage_rollout(contract_root: &Path) -> Result<CoverageRolloutFile, String> { + parse_toml::<CoverageRolloutFile>(&contract_root.join("coverage").join("rollout.toml")) +} + +fn load_coverage_required(contract_root: &Path) -> Result<CoverageRequiredFile, String> { + parse_toml::<CoverageRequiredFile>(&contract_root.join("coverage").join("required-crates.toml")) +} + +fn join_set(items: &BTreeSet<String>) -> String { + items.iter().cloned().collect::<Vec<_>>().join(", ") +} + const CORE_UNIT_DIMENSION_ENUM: &str = "RadrootsCoreUnitDimension"; const CORE_UNIT_DIMENSION_ORDER: [&str; 3] = ["Count", "Mass", "Volume"]; @@ -187,6 +258,130 @@ fn validate_core_unit_dimension_variant_order(workspace_root: &Path) -> Result<( Ok(()) } +fn validate_coverage_rollout_parity( + workspace_root: &Path, + contract_root: &Path, +) -> Result<(), String> { + let workspace_packages = workspace_package_names(workspace_root)? + .into_iter() + .collect::<BTreeSet<_>>(); + let rollout = load_coverage_rollout(contract_root)?; + if rollout.rollout.crates.is_empty() { + return Err("coverage rollout crates list must not be empty".to_string()); + } + let mut rollout_packages = BTreeSet::new(); + let mut rollout_status = BTreeMap::new(); + let mut orders = Vec::with_capacity(rollout.rollout.crates.len()); + for entry in &rollout.rollout.crates { + if !matches!(entry.status.as_str(), "required" | "planned") { + return Err(format!( + "coverage rollout status must be required or planned for {}", + entry.name + )); + } + if !rollout_packages.insert(entry.name.clone()) { + return Err(format!("duplicate coverage rollout crate {}", entry.name)); + } + rollout_status.insert(entry.name.clone(), entry.status.clone()); + orders.push(entry.order); + } + let mut sorted_orders = orders; + sorted_orders.sort_unstable(); + for (index, order) in sorted_orders.iter().enumerate() { + let expected = (index + 1) as u32; + if *order != expected { + return Err(format!( + "coverage rollout order must be contiguous from 1; expected {expected} but found {order}" + )); + } + } + + if workspace_packages != rollout_packages { + let missing = workspace_packages + .difference(&rollout_packages) + .cloned() + .collect::<BTreeSet<_>>(); + let extra = rollout_packages + .difference(&workspace_packages) + .cloned() + .collect::<BTreeSet<_>>(); + if !missing.is_empty() { + return Err(format!( + "coverage rollout missing workspace crates: {}", + join_set(&missing) + )); + } + if !extra.is_empty() { + return Err(format!( + "coverage rollout includes unknown crates: {}", + join_set(&extra) + )); + } + } + + let required = load_coverage_required(contract_root)?; + if required.required.crates.is_empty() { + return Err("coverage required crates list must not be empty".to_string()); + } + let mut required_set = BTreeSet::new(); + for crate_name in &required.required.crates { + if !required_set.insert(crate_name.clone()) { + return Err(format!("duplicate coverage required crate {}", crate_name)); + } + if !workspace_packages.contains(crate_name) { + return Err(format!( + "coverage required crate is not a workspace crate: {}", + crate_name + )); + } + match rollout_status.get(crate_name) { + Some(status) if status == "required" => {} + Some(status) => { + return Err(format!( + "coverage required crate {} must have rollout status required, found {}", + crate_name, status + )); + } + None => { + return Err(format!( + "coverage required crate {} missing from rollout", + crate_name + )); + } + } + } + + let rollout_required = rollout_status + .iter() + .filter(|(_, status)| *status == "required") + .map(|(name, _)| name.clone()) + .collect::<BTreeSet<_>>(); + if rollout_required != required_set { + let missing = rollout_required + .difference(&required_set) + .cloned() + .collect::<BTreeSet<_>>(); + let extra = required_set + .difference(&rollout_required) + .cloned() + .collect::<BTreeSet<_>>(); + if !missing.is_empty() { + return Err(format!( + "coverage required list missing rollout required crates: {}", + join_set(&missing) + )); + } + if !extra.is_empty() { + return Err(format!( + "coverage required list has crates without rollout required status: {}", + join_set(&extra) + )); + } + } + + Ok(()) +} + pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, String> { let root = contract_root(workspace_root); let manifest = parse_toml::<ContractManifest>(&root.join("manifest.toml"))?; @@ -309,6 +504,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { .parent() .ok_or_else(|| "failed to resolve workspace root from contract root".to_string())?; validate_core_unit_dimension_variant_order(workspace_root)?; + validate_coverage_rollout_parity(workspace_root, &bundle.root)?; Ok(()) } @@ -422,4 +618,42 @@ pub enum RadrootsCoreUnitDimension { .collect::<Vec<_>>(); assert_ne!(variants, expected); } + + #[test] + fn coverage_rollout_includes_workspace_crates() { + let root = workspace_root(); + let workspace_names = workspace_package_names(&root) + .expect("workspace crates") + .into_iter() + .collect::<BTreeSet<_>>(); + let rollout = load_coverage_rollout(&root.join("contract")).expect("coverage rollout"); + let rollout_names = rollout + .rollout + .crates + .iter() + .map(|entry| entry.name.clone()) + .collect::<BTreeSet<_>>(); + assert_eq!(workspace_names, rollout_names); + } + + #[test] + fn coverage_required_crates_match_rollout_required_status() { + let root = workspace_root(); + let contract_root = root.join("contract"); + let required = load_coverage_required(&contract_root).expect("coverage required"); + let required_names = required + .required + .crates + .into_iter() + .collect::<BTreeSet<_>>(); + let rollout = load_coverage_rollout(&contract_root).expect("coverage rollout"); + let rollout_required = rollout + .rollout + .crates + .iter() + .filter(|entry| entry.status == "required") + .map(|entry| entry.name.clone()) + .collect::<BTreeSet<_>>(); + assert_eq!(required_names, rollout_required); + } }