lib

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

commit ee542a3333fee6e8a0c801777a547d78e182eba1
parent cb8fcfe1e058141220df6e3d33da4b81527595a7
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 20:46:36 +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 residual snake-case examples in the release specification
- preserve synchronized specification integrity hashes

Diffstat:
Mdocs/specs/radroots_crates_release_v1.md | 14+++++++-------
Mdocs/specs/radroots_crates_release_v1.sha256 | 2+-
Mtools/xtask/src/architecture.rs | 158++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 165 insertions(+), 9 deletions(-)

diff --git a/docs/specs/radroots_crates_release_v1.md b/docs/specs/radroots_crates_release_v1.md @@ -75,8 +75,8 @@ ordinary-user façade The final review makes the following deliberate changes: -1. **`radroots-contracts` becomes `radroots_protocol`.** The package is a durable, versioned wire/operation protocol boundary rather than a general-purpose “contracts” bucket. -2. **`radroots-store` and `radroots-store-sqlite` become `radroots_storage` and `radroots_storage_sqlite`.** “Storage” is unambiguous in an agricultural marketplace and describes the package family more accurately than “store.” +1. **`radroots_contracts` becomes `radroots_protocol`.** The package is a durable, versioned wire/operation protocol boundary rather than a general-purpose “contracts” bucket. +2. **`radroots_store` and `radroots_store_sqlite` become `radroots_storage` and `radroots_storage_sqlite`.** “Storage” is unambiguous in an agricultural marketplace and describes the package family more accurately than “store.” 3. **`radroots_nostr_connect` remains independent.** It is a bidirectional security protocol with URIs, permissions, client/server state, and independent SDK/Myc consumers. It is not merely a convenience NIP module. 4. **Actor ownership is refined.** Public keys/accounts live in identity; event author roles live in the event contract model; actor provenance, authorization, and signer behavior live in signing. 5. **Trade identity is made singular.** The conflicting `TradeId`/`OrderId` definitions MUST be replaced by one canonical protocol `TradeId` and a separately named business `OrderId`. @@ -1194,7 +1194,7 @@ Private preview code remains tested. It may become public only after passing the ## 19. New-package admission rule -After release V1, a new `radroots-*` package requires an ADR proving: +After release V1, a new `radroots_*` package requires an ADR proving: 1. a durable domain/protocol/SPI/backend boundary; 2. at least two meaningful direct consumers, or one unavoidable platform/backend isolation boundary; @@ -1229,7 +1229,7 @@ Names containing `common`, `utils`, `types`, `models`, `preview`, `unstable`, `v | `radroots_nostr_accounts` | `radroots_identity + radroots_secrets + radroots_storage + radroots_sdk` | Split mixed account, vault, persistence, and manager responsibilities. | | `radroots_nostr_signer` | `radroots_signing + radroots_nostr_connect + Myc-private state` | Do not publish current service-state package. | | `radroots_nostr_runtime` | `radroots_transport_nostr + radroots_sync` | Merge live relay runtime into adapter/orchestration layers. | -| `radroots_nostrdb` | `private; possible future radroots-storage-nostrdb` | Withhold until the storage SPI and external consumers justify a backend package. | +| `radroots_nostrdb` | `private; possible future radroots_storage_nostrdb` | Withhold until the storage SPI and external consumers justify a backend package. | | `radroots_event_store` | `radroots_storage + radroots_storage_sqlite` | Split backend-neutral contracts from SQLite implementation. | | `radroots_outbox` | `radroots_storage + radroots_storage_sqlite` | Merge as one persistence capability with atomic operation commits. | | `radroots_runtime_store` | `radroots_storage or host-private state` | Retire broad name and classify each table by owner. | @@ -1280,7 +1280,7 @@ Every public package MUST define: ```toml [package] -name = "radroots-..." +name = "radroots_..." version = "0.1.0" publish = ["crates-io"] edition.workspace = true @@ -1289,7 +1289,7 @@ license.workspace = true repository.workspace = true homepage.workspace = true readme = "README.md" -documentation = "https://docs.rs/radroots-..." +documentation = "https://docs.rs/radroots_..." ``` Additional rules: @@ -1467,7 +1467,7 @@ Rejected because temporary implementation boundaries would become permanent pack Rejected because domain, protocol, SPI, backend, and adapter packages have independent consumers and semver responsibilities. -### One `radroots-runtime` package +### One `radroots_runtime` package Rejected because runtime configuration, process lifecycle, paths, logging, queues, storage, and networking are not one coherent library boundary. diff --git a/docs/specs/radroots_crates_release_v1.sha256 b/docs/specs/radroots_crates_release_v1.sha256 @@ -1,4 +1,4 @@ -05d550f393f085b6cbaa0817e289e2487c0ed0861c7979c9225d9dc71f1ca6d4 radroots_crates_release_v1.md +9e60ffacc6bcaeb8ad9095206fb76efc53f82f0962b3bd5c5e6a4021f4eb3161 radroots_crates_release_v1.md 30d56267f33647524b3093051a4504afe7f330ab8700d7db09985af5b981b62e radroots_crates_release_v1.toml 479f4a09dcad343161c9c644cbde18919eb4365cc295c296ed8ea1458e213882 radroots_crates_release_v1_inventory.csv d47de10be596a4d33fee102a4f0617f66700b49515a75a1f42d62c9710043059 radroots_crates_release_v1.dot diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs @@ -812,6 +812,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 @@ -831,7 +835,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}")); } } } @@ -853,6 +892,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, @@ -1127,6 +1239,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(""))