lib

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

commit 4476586cc10a6f1b1ee58f271991a6be765c791d
parent c4d7ebfd91d4725fa440578df7d59de400c0065b
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 20:49:23 +0000

workspace: enforce snake case crate identities

- reject kebab-case Cargo package names and crate directories
- reject kebab-case Radroots dependency keys in every manifest section
- correct smoke, preview-gate, and normative package identities
- preserve synchronized specification integrity hashes

Diffstat:
Mtools/sdk_xtask_import/src/architecture.rs | 158++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtools/sdk_xtask_import/src/check.rs | 16++++++----------
Mtools/sdk_xtask_import/src/smoke.rs | 3++-
3 files changed, 165 insertions(+), 12 deletions(-)

diff --git a/tools/sdk_xtask_import/src/architecture.rs b/tools/sdk_xtask_import/src/architecture.rs @@ -810,6 +810,10 @@ fn validate_workspace_members(workspace_root: &Path) -> Result<(), String> { .map_err(|error| format!("read {}: {error}", manifest_path.display()))?; let manifest = toml::from_str::<WorkspaceMembershipManifest>(&manifest_raw) .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?; + let manifest_value = manifest_raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?; + validate_manifest_dependency_keys(&manifest_value, "Cargo.toml")?; let declared = manifest .workspace .members @@ -829,7 +833,42 @@ fn validate_workspace_members(workspace_root: &Path) -> Result<(), String> { .is_dir() && entry.path().join("Cargo.toml").is_file() { - discovered.insert(format!("{root}/{}", entry.file_name().to_string_lossy())); + let directory_name = entry.file_name().to_string_lossy().into_owned(); + require_lower_snake_case( + &directory_name, + &format!("{root}/{directory_name} crate directory"), + )?; + let package_manifest_path = entry.path().join("Cargo.toml"); + let package_manifest_raw = + fs::read_to_string(&package_manifest_path).map_err(|error| { + format!("read {}: {error}", package_manifest_path.display()) + })?; + let package_manifest = + package_manifest_raw + .parse::<toml::Value>() + .map_err(|error| { + format!("parse {}: {error}", package_manifest_path.display()) + })?; + let package_name = package_manifest + .get("package") + .and_then(toml::Value::as_table) + .and_then(|package| package.get("name")) + .and_then(toml::Value::as_str) + .ok_or_else(|| { + format!( + "{} is missing package.name", + package_manifest_path.display() + ) + })?; + require_lower_snake_case( + package_name, + &format!("{} package.name", package_manifest_path.display()), + )?; + validate_manifest_dependency_keys( + &package_manifest, + &package_manifest_path.display().to_string(), + )?; + discovered.insert(format!("{root}/{directory_name}")); } } } @@ -851,6 +890,79 @@ fn validate_workspace_members(workspace_root: &Path) -> Result<(), String> { Ok(()) } +fn validate_manifest_dependency_keys(manifest: &toml::Value, label: &str) -> Result<(), String> { + let Some(manifest) = manifest.as_table() else { + return Err(format!("{label} must be a TOML table")); + }; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(dependencies) = manifest.get(section).and_then(toml::Value::as_table) { + validate_dependency_keys(dependencies, label, section)?; + } + } + if let Some(workspace_dependencies) = manifest + .get("workspace") + .and_then(toml::Value::as_table) + .and_then(|workspace| workspace.get("dependencies")) + .and_then(toml::Value::as_table) + { + validate_dependency_keys(workspace_dependencies, label, "workspace.dependencies")?; + } + if let Some(targets) = manifest.get("target").and_then(toml::Value::as_table) { + for (target, target_value) in targets { + let Some(target_table) = target_value.as_table() else { + continue; + }; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(dependencies) = + target_table.get(section).and_then(toml::Value::as_table) + { + validate_dependency_keys( + dependencies, + label, + &format!("target.{target}.{section}"), + )?; + } + } + } + } + Ok(()) +} + +fn validate_dependency_keys( + dependencies: &toml::value::Table, + label: &str, + section: &str, +) -> Result<(), String> { + for key in dependencies + .keys() + .filter(|key| key.starts_with("radroots")) + { + require_lower_snake_case(key, &format!("{label} {section} dependency key"))?; + } + Ok(()) +} + +fn require_lower_snake_case(value: &str, label: &str) -> Result<(), String> { + if is_lower_snake_case(value) { + Ok(()) + } else { + Err(format!("{label} `{value}` must use lowercase snake case")) + } +} + +fn is_lower_snake_case(value: &str) -> bool { + !value.is_empty() + && value.split('_').all(|segment| { + segment + .as_bytes() + .first() + .is_some_and(u8::is_ascii_lowercase) + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit()) + }) +} + fn validate_ledger( workspace_root: &Path, expected_architecture_id: &str, @@ -1125,6 +1237,50 @@ adr_required = false } #[test] + fn workspace_identity_rejects_kebab_case_crate_names_and_dependency_keys() { + let root = test_root("workspace_snake_case_identity"); + fs::create_dir_all(root.join("crates/radroots_probe")).expect("create package root"); + fs::create_dir_all(root.join("tools/xtask")).expect("create xtask root"); + fs::write( + root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/radroots_probe\", \"tools/xtask\"]\nresolver = \"3\"\n", + ) + .expect("write workspace manifest"); + fs::write( + root.join("crates/radroots_probe/Cargo.toml"), + "[package]\nname = \"radroots-probe\"\n", + ) + .expect("write kebab package manifest"); + fs::write( + root.join("tools/xtask/Cargo.toml"), + "[package]\nname = \"xtask\"\n", + ) + .expect("write xtask manifest"); + + let package_error = validate_workspace_members(&root) + .expect_err("kebab-case Cargo package identity must fail"); + assert!(package_error.contains("package.name")); + assert!(package_error.contains("lowercase snake case")); + + fs::write( + root.join("crates/radroots_probe/Cargo.toml"), + "[package]\nname = \"radroots_probe\"\n", + ) + .expect("write snake package manifest"); + fs::write( + root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/radroots_probe\", \"tools/xtask\"]\nresolver = \"3\"\n\n[workspace.dependencies]\nradroots-probe = \"0.1.0\"\n", + ) + .expect("write kebab dependency key"); + + let dependency_error = validate_workspace_members(&root) + .expect_err("kebab-case Radroots dependency key must fail"); + assert!(dependency_error.contains("workspace.dependencies dependency key")); + assert!(dependency_error.contains("lowercase snake case")); + let _ = fs::remove_dir_all(root); + } + + #[test] fn workspace_toolchain_requires_exact_resolver_and_rust_version() { let root = test_root("workspace_toolchain"); fs::write(root.join("Cargo.toml"), complete_workspace_manifest("")) diff --git a/tools/sdk_xtask_import/src/check.rs b/tools/sdk_xtask_import/src/check.rs @@ -29,7 +29,7 @@ const PACKAGE_LICENSE: &str = "MIT OR Apache-2.0"; const PACKAGE_HOMEPAGE: &str = "https://radroots.org"; const PACKAGE_REPOSITORY_URL: &str = "git+https://github.com/radrootslabs/sdk.git"; const PUBLISH_ACCESS: &str = "public"; -const ALLOWED_RETICULUM_PREVIEW_PACKAGE: &str = "radroots-transport-reticulum"; +const ALLOWED_RETICULUM_PREVIEW_PACKAGE: &str = "radroots_transport_reticulum"; const RADROOTS_FACADE_MODULES: [&str; 11] = [ "client", "event", @@ -730,8 +730,7 @@ fn check_reticulum_feature_entry( .map_or((feature_dependency, None), |(name, feature)| { (name, Some(feature)) }); - let normalized_dependency = normalize_package_name(dependency_name); - if normalized_dependency == ALLOWED_RETICULUM_PREVIEW_PACKAGE && dependency_feature.is_some() { + if dependency_name == ALLOWED_RETICULUM_PREVIEW_PACKAGE && dependency_feature.is_some() { return Err(format!( "{manifest_label} feature `{feature_name}` must not enable Reticulum preview crate features through `{entry}`" )); @@ -796,10 +795,10 @@ fn reject_forbidden_reticulum_runtime_name( } fn is_forbidden_reticulum_runtime_name(name: &str) -> bool { - let normalized = normalize_package_name(name); - if normalized == ALLOWED_RETICULUM_PREVIEW_PACKAGE { + if name == ALLOWED_RETICULUM_PREVIEW_PACKAGE { return false; } + let normalized = normalize_package_name(name); matches!( normalized.as_str(), "rns" | "rnsd" | "reticulum" | "reticulum-rs" | "python" | "pyo3" @@ -2442,17 +2441,14 @@ transport-reticulum-preview = [] "python-runtime", "pyo3", "pyo3-ffi", + "radroots-transport-reticulum", ] { assert!( is_forbidden_reticulum_runtime_name(name), "{name} must be forbidden" ); } - for name in [ - "radroots_transport_reticulum", - "radroots-transport-reticulum", - "dto_bindgen_backend_python", - ] { + for name in ["radroots_transport_reticulum", "dto_bindgen_backend_python"] { assert!( !is_forbidden_reticulum_runtime_name(name), "{name} must be allowed" diff --git a/tools/sdk_xtask_import/src/smoke.rs b/tools/sdk_xtask_import/src/smoke.rs @@ -60,7 +60,7 @@ fn render_consumer_manifest(sdk_path: &Path, nostr_version: &str) -> Result<Stri .map_err(|error| format!("failed to render SDK path: {error}"))?; Ok(format!( r#"[package] -name = "radroots-sdk-knowledge-smoke" +name = "radroots_sdk_knowledge_smoke" version = "0.1.0" edition = "2024" publish = false @@ -220,6 +220,7 @@ mod tests { let manifest = render_consumer_manifest(Path::new("/tmp/radroots_sdk"), "=0.44.2").expect("manifest"); + assert!(manifest.contains("name = \"radroots_sdk_knowledge_smoke\"")); assert!(manifest.contains("nostr = \"=0.44.2\"")); assert!(!manifest.contains("nostr = \"0.44.2\"")); }