commit 8ed1e0500f232489c23620f2ceb445bcafbb079e
parent 21543db0546b0768e8b3d114d7af4ae36c7e5607
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Jul 2026 09:09:59 +0000
workspace: normalize public package metadata
- define the independent public 0.1.0 package metadata source
- make the radroots facade inherit canonical workspace authorship
- enforce repository metadata and publication freeze invariants
- format synchronized architecture data and refresh its checksum
Diffstat:
5 files changed, 701 insertions(+), 55 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -25,6 +25,12 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/radrootslabs/sdk"
homepage = "https://radroots.org"
readme = "README"
+authors = ["Tyson Lupul <tyson@radroots.org>"]
+
+[workspace.metadata.radroots.public-package]
+version = "0.1.0"
+authors = ["Tyson Lupul <tyson@radroots.org>"]
+readme = "README.md"
[workspace.dependencies]
dto_bindgen = { version = "0.1.0" }
diff --git a/crates/radroots/Cargo.toml b/crates/radroots/Cargo.toml
@@ -7,6 +7,7 @@ rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
+authors.workspace = true
readme = "README.md"
publish = false
diff --git a/docs/specs/radroots_crates_release_v1.sha256 b/docs/specs/radroots_crates_release_v1.sha256
@@ -1,4 +1,4 @@
8ddb51b5f186cb7ebcd7d2cd60217dce183ad9c87d7363f91fbaa6ff8db9777e radroots_crates_release_v1.md
-f1f6661da8cec1bb1128b4f2846d4c2c0d97f391cb458003ced7296074c7bc9f radroots_crates_release_v1.toml
+a9dfec76ec11a1f1817133b0ad40dc062df3b2b5f77c3c96eeb92f0eff718aa7 radroots_crates_release_v1.toml
6e4a1c00d762e36e18e530d8e88846d500aeb8889d6f36d7ef906a055ef30a37 radroots_crates_release_v1_inventory.csv
dc5a24247df8feb6bad11985475cd3ccb5b7740a0dce8ad5198f47da7aff3480 radroots_crates_release_v1.dot
diff --git a/docs/specs/radroots_crates_release_v1.toml b/docs/specs/radroots_crates_release_v1.toml
@@ -64,8 +64,25 @@ default_features = ["std", "serde"]
features = ["std", "serde"]
required_radroots_dependencies = []
optional_radroots_dependencies = []
-modules = ["currency", "decimal", "money", "percent", "pricing", "quantity", "unit"]
-root_exports = ["Currency", "Decimal", "Money", "Percent", "Quantity", "QuantityPrice", "Unit", "Error"]
+modules = [
+ "currency",
+ "decimal",
+ "money",
+ "percent",
+ "pricing",
+ "quantity",
+ "unit",
+]
+root_exports = [
+ "Currency",
+ "Decimal",
+ "Money",
+ "Percent",
+ "Quantity",
+ "QuantityPrice",
+ "Unit",
+ "Error",
+]
forbidden = "Identifiers, identities, event kinds, networking, persistence, clocks, filesystem paths, process behavior, or application configuration."
[[package]]
@@ -80,7 +97,15 @@ features = ["std", "serde"]
required_radroots_dependencies = []
optional_radroots_dependencies = []
modules = ["account", "key", "profile", "username"]
-root_exports = ["AccountId", "IdentityId", "PublicIdentity", "PublicKey", "Profile", "Username", "Error"]
+root_exports = [
+ "AccountId",
+ "IdentityId",
+ "PublicIdentity",
+ "PublicKey",
+ "Profile",
+ "Username",
+ "Error",
+]
forbidden = "Secret keys, key generation, NIP-49 encryption, keyrings, files, SQLite, runtime paths, upstream nostr::Event values, signer sessions, or host account selection."
[[package]]
@@ -95,7 +120,15 @@ features = ["std", "serde"]
required_radroots_dependencies = []
optional_radroots_dependencies = []
modules = ["authorization", "descriptor", "hash", "media_type", "url"]
-root_exports = ["BlobUrl", "Sha256", "MediaType", "BlobDescriptor", "ByteVerifiedDescriptor", "AuthorizationClaim", "Error"]
+root_exports = [
+ "BlobUrl",
+ "Sha256",
+ "MediaType",
+ "BlobDescriptor",
+ "ByteVerifiedDescriptor",
+ "AuthorizationClaim",
+ "Error",
+]
forbidden = "HTTP clients, upload scheduling, cache management, filesystem traversal, application media policy, or global authentication state."
[[package]]
@@ -109,7 +142,14 @@ default_features = ["std", "serde"]
features = ["std", "serde"]
required_radroots_dependencies = []
optional_radroots_dependencies = []
-modules = ["capability::v1", "error::v1", "event::v1", "runtime::v1", "radrootsd::transport_publish::v5", "schema"]
+modules = [
+ "capability::v1",
+ "error::v1",
+ "event::v1",
+ "runtime::v1",
+ "radrootsd::transport_publish::v5",
+ "schema",
+]
root_exports = []
forbidden = "Native clients, storage, network I/O, executor/runtime ownership, domain reducers, upstream dependency types, unversioned serialized DTOs, or package names containing protocol generations."
@@ -122,10 +162,42 @@ platform = "no_std + alloc; std feature"
responsibility = "Canonical Radroots event-domain models, validated event identifiers, tags, event contracts, authoring drafts, signed/verified typestates, and NIP-01 wire-neutral representations."
default_features = ["std", "serde"]
features = ["std", "serde", "knowledge"]
-required_radroots_dependencies = ["radroots-core", "radroots-identity", "radroots-blossom", "radroots-protocol"]
+required_radroots_dependencies = [
+ "radroots-core",
+ "radroots-identity",
+ "radroots-blossom",
+ "radroots-protocol",
+]
optional_radroots_dependencies = []
-modules = ["admission", "calendar", "contract", "draft", "envelope", "farm", "food", "id", "knowledge", "listing", "media", "post", "profile", "social", "tag", "trade", "wire"]
-root_exports = ["Event", "EventDraft", "SignedEvent", "VerifiedEvent", "EventId", "EventKind", "EventTag", "Error"]
+modules = [
+ "admission",
+ "calendar",
+ "contract",
+ "draft",
+ "envelope",
+ "farm",
+ "food",
+ "id",
+ "knowledge",
+ "listing",
+ "media",
+ "post",
+ "profile",
+ "social",
+ "tag",
+ "trade",
+ "wire",
+]
+root_exports = [
+ "Event",
+ "EventDraft",
+ "SignedEvent",
+ "VerifiedEvent",
+ "EventId",
+ "EventKind",
+ "EventTag",
+ "Error",
+]
forbidden = "Live Nostr clients, relay pools, signing backends, SQLite, outbox claims, retry scheduling, application state, or duplicate trade/order identifier concepts."
[[package]]
@@ -137,7 +209,11 @@ platform = "no_std + alloc where selected features permit; std feature"
responsibility = "Deterministic canonical encoding, decoding, ID/signature verification, contract validation, admission, and manifest generation for Radroots events."
default_features = ["std", "json"]
features = ["std", "serde", "json", "knowledge", "manifests"]
-required_radroots_dependencies = ["radroots-event", "radroots-blossom", "radroots-protocol"]
+required_radroots_dependencies = [
+ "radroots-event",
+ "radroots-blossom",
+ "radroots-protocol",
+]
optional_radroots_dependencies = []
modules = ["admission", "canonical", "decode", "encode", "manifest", "verify"]
root_exports = ["Codec", "DecodeError", "EncodeError", "VerificationError"]
@@ -152,10 +228,21 @@ platform = "no_std + alloc for model/reducer; std feature"
responsibility = "Trade validation, evidence models, deterministic reduction, conflict analysis, and side-effect-free workflow plans over the canonical event trade model."
default_features = ["std", "serde", "json"]
features = ["std", "serde", "json"]
-required_radroots_dependencies = ["radroots-core", "radroots-identity", "radroots-event"]
+required_radroots_dependencies = [
+ "radroots-core",
+ "radroots-identity",
+ "radroots-event",
+]
optional_radroots_dependencies = []
modules = ["evidence", "model", "reducer", "validation", "workflow"]
-root_exports = ["Projection", "ReductionInput", "ReducerIssue", "WorkflowPlan", "ValidationError", "Error"]
+root_exports = [
+ "Projection",
+ "ReductionInput",
+ "ReducerIssue",
+ "WorkflowPlan",
+ "ValidationError",
+ "Error",
+]
forbidden = "A second TradeId definition, actor authorization, signers, event-store access, SQLx, filesystem state, transport delivery, outbox mutation, or process scheduling."
[[package]]
@@ -167,10 +254,29 @@ platform = "no_std + alloc core; std feature"
responsibility = "Object-safe author/signing SPI, actor provenance, authorization checks, requests, receipts, progress, capabilities, and normalized signing errors."
default_features = ["std", "serde"]
features = ["std", "serde"]
-required_radroots_dependencies = ["radroots-identity", "radroots-event", "radroots-protocol"]
+required_radroots_dependencies = [
+ "radroots-identity",
+ "radroots-event",
+ "radroots-protocol",
+]
optional_radroots_dependencies = []
-modules = ["actor", "capability", "error", "request", "receipt", "signer", "status"]
-root_exports = ["Actor", "Signer", "SignRequest", "SignReceipt", "SignerStatus", "Error"]
+modules = [
+ "actor",
+ "capability",
+ "error",
+ "request",
+ "receipt",
+ "signer",
+ "status",
+]
+root_exports = [
+ "Actor",
+ "Signer",
+ "SignRequest",
+ "SignReceipt",
+ "SignerStatus",
+ "Error",
+]
forbidden = "Raw secret-key ownership, keyrings, relay networking, NIP-46 session persistence, SQL, UI prompts, or executor creation."
[[package]]
@@ -182,10 +288,34 @@ platform = "no_std + alloc data model; std feature"
responsibility = "Transport-neutral target identities, capability/status models, source and sink SPIs, delivery/fetch policies, bounded requests, provenance, and normalized outcomes."
default_features = ["std", "serde"]
features = ["std", "serde"]
-required_radroots_dependencies = ["radroots-identity", "radroots-event", "radroots-protocol"]
+required_radroots_dependencies = [
+ "radroots-identity",
+ "radroots-event",
+ "radroots-protocol",
+]
optional_radroots_dependencies = []
-modules = ["capability", "endpoint", "error", "outcome", "policy", "sink", "source", "target"]
-root_exports = ["TransportId", "Target", "TargetSet", "EventSource", "EventSink", "DeliveryRequest", "DeliveryReceipt", "FetchRequest", "FetchPage", "Error"]
+modules = [
+ "capability",
+ "endpoint",
+ "error",
+ "outcome",
+ "policy",
+ "sink",
+ "source",
+ "target",
+]
+root_exports = [
+ "TransportId",
+ "Target",
+ "TargetSet",
+ "EventSource",
+ "EventSink",
+ "DeliveryRequest",
+ "DeliveryReceipt",
+ "FetchRequest",
+ "FetchPage",
+ "Error",
+]
forbidden = "Closed enums that prevent new transports, Reticulum-specific constants, Nostr URLs at the generic root, storage/outbox access, retries, scheduler ownership, or silent fallback."
[[package]]
@@ -197,7 +327,11 @@ platform = "no_std + alloc for conversion surface; std feature"
responsibility = "Portable conversion between Radroots native event/identity types and Nostr protocol types, typed NIP helpers, and concrete local signing adapters; no live relay client."
default_features = ["std", "events"]
features = ["std", "events", "signing", "nip17", "blossom"]
-required_radroots_dependencies = ["radroots-identity", "radroots-event", "radroots-event-codec"]
+required_radroots_dependencies = [
+ "radroots-identity",
+ "radroots-event",
+ "radroots-event-codec",
+]
optional_radroots_dependencies = ["radroots-signing", "radroots-blossom"]
modules = ["blossom", "event", "filter", "key", "nip17", "signing", "tag"]
root_exports = ["Error"]
@@ -212,10 +346,33 @@ platform = "std for v1; protocol data kept portable"
responsibility = "Nostr Connect/NIP-46 URIs, methods, permissions, requests, responses, client/server state machines, timeout-independent protocol validation, and normalized errors."
default_features = ["serde"]
features = ["serde"]
-required_radroots_dependencies = ["radroots-identity", "radroots-event", "radroots-nostr", "radroots-protocol"]
+required_radroots_dependencies = [
+ "radroots-identity",
+ "radroots-event",
+ "radroots-nostr",
+ "radroots-protocol",
+]
optional_radroots_dependencies = []
-modules = ["client", "error", "message", "method", "permission", "server", "uri"]
-root_exports = ["Client", "Server", "Method", "Permission", "Request", "Response", "BunkerUri", "ClientUri", "Error"]
+modules = [
+ "client",
+ "error",
+ "message",
+ "method",
+ "permission",
+ "server",
+ "uri",
+]
+root_exports = [
+ "Client",
+ "Server",
+ "Method",
+ "Permission",
+ "Request",
+ "Response",
+ "BunkerUri",
+ "ClientUri",
+ "Error",
+]
forbidden = "Relay-pool implementation, secret persistence, approval UI, global sessions, Tokio runtime ownership, or Myc-specific service storage."
[[package]]
@@ -229,8 +386,24 @@ default_features = ["std", "serde"]
features = ["std", "serde", "memory", "file", "keyring"]
required_radroots_dependencies = []
optional_radroots_dependencies = []
-modules = ["envelope", "error", "id", "provider", "wrapping", "memory", "file", "keyring"]
-root_exports = ["SecretId", "SecretRef", "SecretProvider", "KeyWrapping", "EncryptedEnvelope", "Error"]
+modules = [
+ "envelope",
+ "error",
+ "id",
+ "provider",
+ "wrapping",
+ "memory",
+ "file",
+ "keyring",
+]
+root_exports = [
+ "SecretId",
+ "SecretRef",
+ "SecretProvider",
+ "KeyWrapping",
+ "EncryptedEnvelope",
+ "Error",
+]
forbidden = "Public secret bytes, Clone/Debug/Serialize for secret-bearing values, identity profiles, domain tables, arbitrary key/value storage, hidden key generation, or process-global vaults."
[[package]]
@@ -242,10 +415,34 @@ platform = "std for v1"
responsibility = "Backend-neutral canonical event, operation journal, outbox, transport evidence, projection, private-artifact metadata, backup, status, and atomic commit interfaces, plus an in-memory reference backend."
default_features = ["memory", "serde"]
features = ["memory", "serde"]
-required_radroots_dependencies = ["radroots-event", "radroots-trade", "radroots-transport", "radroots-protocol"]
+required_radroots_dependencies = [
+ "radroots-event",
+ "radroots-trade",
+ "radroots-transport",
+ "radroots-protocol",
+]
optional_radroots_dependencies = []
-modules = ["atomic", "backup", "event", "journal", "memory", "outbox", "private_artifact", "projection", "status"]
-root_exports = ["Storage", "EventStore", "Journal", "Outbox", "ProjectionStore", "BackupSource", "StorageStatus", "Error"]
+modules = [
+ "atomic",
+ "backup",
+ "event",
+ "journal",
+ "memory",
+ "outbox",
+ "private_artifact",
+ "projection",
+ "status",
+]
+root_exports = [
+ "Storage",
+ "EventStore",
+ "Journal",
+ "Outbox",
+ "ProjectionStore",
+ "BackupSource",
+ "StorageStatus",
+ "Error",
+]
forbidden = "SQL text, SQLx pools or transactions, filesystem paths, application UI state, concrete retry loops, Nostr clients, or unconstrained raw key/value escape hatches."
[[package]]
@@ -257,9 +454,21 @@ platform = "std-only native backend"
responsibility = "SQLite implementation of the storage SPIs with schema migration, WAL, locking, integrity, backup/restore, crash recovery, and encrypted private storage."
default_features = []
features = []
-required_radroots_dependencies = ["radroots-storage", "radroots-event-codec", "radroots-secrets"]
+required_radroots_dependencies = [
+ "radroots-storage",
+ "radroots-event-codec",
+ "radroots-secrets",
+]
optional_radroots_dependencies = []
-modules = ["backup", "config", "integrity", "lock", "migration", "open", "status"]
+modules = [
+ "backup",
+ "config",
+ "integrity",
+ "lock",
+ "migration",
+ "open",
+ "status",
+]
root_exports = ["SqliteStorage", "OpenOptions", "OpenMode", "Paths", "Error"]
forbidden = "Public SqlitePool/Connection/Transaction handles, caller-supplied arbitrary SQL, Studio state, global connection pools, runtime installation, or silent schema downgrade."
@@ -272,10 +481,21 @@ platform = "std-only; Tokio implementation detail in v1"
responsibility = "Concrete Nostr EventSource/EventSink implementation: relay URL policy, connection, NIP-42 authentication, bounded fetch pages, delivery, status, and relay-outcome normalization."
default_features = []
features = []
-required_radroots_dependencies = ["radroots-transport", "radroots-nostr", "radroots-event-codec", "radroots-protocol"]
+required_radroots_dependencies = [
+ "radroots-transport",
+ "radroots-nostr",
+ "radroots-event-codec",
+ "radroots-protocol",
+]
optional_radroots_dependencies = []
modules = ["auth", "client", "relay", "sink", "source", "status"]
-root_exports = ["NostrTransport", "Config", "RelayUrl", "RelayUrlPolicy", "Error"]
+root_exports = [
+ "NostrTransport",
+ "Config",
+ "RelayUrl",
+ "RelayUrlPolicy",
+ "Error",
+]
forbidden = "Event-store ingestion, outbox claiming, retry scheduling, projection refresh, global relay clients, direct SQL, or transport fallback."
[[package]]
@@ -287,10 +507,26 @@ platform = "std-only in v1; executor-neutral public API"
responsibility = "Shared pull, verification, canonical admission, duplicate handling, projection refresh, outbox signing/delivery, status, and retry-decision orchestration without owning scheduling."
default_features = ["serde"]
features = ["serde"]
-required_radroots_dependencies = ["radroots-event", "radroots-event-codec", "radroots-signing", "radroots-transport", "radroots-storage", "radroots-trade", "radroots-protocol"]
+required_radroots_dependencies = [
+ "radroots-event",
+ "radroots-event-codec",
+ "radroots-signing",
+ "radroots-transport",
+ "radroots-storage",
+ "radroots-trade",
+ "radroots-protocol",
+]
optional_radroots_dependencies = []
modules = ["ingest", "policy", "projection", "pull", "push", "status"]
-root_exports = ["Engine", "PullRequest", "PullReceipt", "PushRequest", "PushReceipt", "SyncStatus", "Error"]
+root_exports = [
+ "Engine",
+ "PullRequest",
+ "PullReceipt",
+ "PushRequest",
+ "PushReceipt",
+ "SyncStatus",
+ "Error",
+]
forbidden = "Creating an executor, spawning hidden workers, installing timers globally, owning process lifecycle, storing UI state, or transport-specific branches outside adapters."
[[package]]
@@ -305,7 +541,15 @@ features = []
required_radroots_dependencies = []
optional_radroots_dependencies = []
modules = ["asset", "database", "download", "model", "query"]
-root_exports = ["Geocoder", "AssetSpec", "AssetStatus", "Query", "Candidate", "Point", "Error"]
+root_exports = [
+ "Geocoder",
+ "AssetSpec",
+ "AssetStatus",
+ "Query",
+ "Candidate",
+ "Point",
+ "Error",
+]
forbidden = "Generic multi-provider abstraction before a second provider exists, runtime-path policy, hidden downloads, SDK configuration types, SQLx/reqwest types in the public API, or test-fixture features."
[[package]]
@@ -316,10 +560,52 @@ tier = "advanced front door"
platform = "std-only native engine"
responsibility = "Host-neutral asynchronous client engine, product operations, capability reporting, explicit storage/signing/transport composition, diagnostics, backup/restore, and safe commit semantics."
default_features = ["memory"]
-features = ["memory", "sqlite", "sync", "nostr", "nip46", "local-signing", "radrootsd", "geonames", "knowledge", "native", "full"]
-required_radroots_dependencies = ["radroots-core", "radroots-identity", "radroots-protocol", "radroots-event", "radroots-event-codec", "radroots-trade", "radroots-signing", "radroots-transport", "radroots-storage"]
-optional_radroots_dependencies = ["radroots-secrets", "radroots-storage-sqlite", "radroots-nostr", "radroots-nostr-connect", "radroots-transport-nostr", "radroots-sync", "radroots-geonames"]
-modules = ["capability", "client", "diagnostics", "error", "farm", "listing", "signing", "storage", "sync", "trade", "transport"]
+features = [
+ "memory",
+ "sqlite",
+ "sync",
+ "nostr",
+ "nip46",
+ "local-signing",
+ "radrootsd",
+ "geonames",
+ "knowledge",
+ "native",
+ "full",
+]
+required_radroots_dependencies = [
+ "radroots-core",
+ "radroots-identity",
+ "radroots-protocol",
+ "radroots-event",
+ "radroots-event-codec",
+ "radroots-trade",
+ "radroots-signing",
+ "radroots-transport",
+ "radroots-storage",
+]
+optional_radroots_dependencies = [
+ "radroots-secrets",
+ "radroots-storage-sqlite",
+ "radroots-nostr",
+ "radroots-nostr-connect",
+ "radroots-transport-nostr",
+ "radroots-sync",
+ "radroots-geonames",
+]
+modules = [
+ "capability",
+ "client",
+ "diagnostics",
+ "error",
+ "farm",
+ "listing",
+ "signing",
+ "storage",
+ "sync",
+ "trade",
+ "transport",
+]
root_exports = ["Client", "ClientBuilder", "Error", "Result"]
forbidden = "Global runtimes or subscribers, hidden workers, process signals, CLI parsing, UI state, Studio databases, raw SQLx/upstream client types, broad wildcard reexports, or a nominal no_std claim."
@@ -331,9 +617,37 @@ tier = "ordinary-user front door"
platform = "std-only"
responsibility = "Canonical Rust onboarding package with curated modules, safe defaults, stable convenience builders, domain aggregation, examples, and primary documentation."
default_features = ["client"]
-features = ["client", "native", "nostr", "nip46", "radrootsd", "geonames", "knowledge", "full"]
-required_radroots_dependencies = ["radroots-sdk", "radroots-core", "radroots-identity", "radroots-event", "radroots-trade", "radroots-transport"]
+features = [
+ "client",
+ "native",
+ "nostr",
+ "nip46",
+ "radrootsd",
+ "geonames",
+ "knowledge",
+ "full",
+]
+required_radroots_dependencies = [
+ "radroots-sdk",
+ "radroots-core",
+ "radroots-identity",
+ "radroots-event",
+ "radroots-trade",
+ "radroots-transport",
+]
optional_radroots_dependencies = []
-modules = ["client", "event", "farm", "identity", "knowledge", "listing", "signing", "storage", "sync", "trade", "transport"]
+modules = [
+ "client",
+ "event",
+ "farm",
+ "identity",
+ "knowledge",
+ "listing",
+ "signing",
+ "storage",
+ "sync",
+ "trade",
+ "transport",
+]
root_exports = ["Client", "ClientBuilder", "Error", "Result"]
forbidden = "A public radroots::sdk namespace, wildcard reexport of radroots-sdk, duplicate engine implementation, CLI binary, hidden network/filesystem/keychain side effects, or exposure of every lower-crate symbol."
diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs
@@ -1,5 +1,5 @@
use std::{
- collections::BTreeSet,
+ collections::{BTreeMap, BTreeSet},
fs,
path::{Component, Path},
};
@@ -9,6 +9,9 @@ use serde::Deserialize;
const DEVIATIONS_RELATIVE: &str = "docs/implementation/deviations.toml";
const ARCHITECTURE_RELATIVE: &str = "docs/specs/radroots_crates_release_v1.toml";
const ARCHITECTURE_ID: &str = "radroots.crates.release.v1";
+const PUBLIC_HOMEPAGE: &str = "https://radroots.org";
+const PUBLIC_README: &str = "README.md";
+const PUBLIC_AUTHORS: &[&str] = &["Tyson Lupul <tyson@radroots.org>"];
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
@@ -40,8 +43,25 @@ struct DeviationRecord {
#[derive(Debug, Deserialize)]
struct ArchitectureIdentity {
spec_id: String,
+ initial_version: String,
+ edition: String,
resolver: String,
rust_version: String,
+ license: String,
+ canonical_repositories: Vec<String>,
+ repositories: BTreeMap<String, ArchitectureRepository>,
+ package: Vec<ArchitecturePackage>,
+}
+
+#[derive(Debug, Deserialize)]
+struct ArchitectureRepository {
+ url: String,
+ packages: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+struct ArchitecturePackage {
+ name: String,
}
#[derive(Debug, Deserialize)]
@@ -50,16 +70,52 @@ struct WorkspaceManifest {
}
#[derive(Debug, Deserialize)]
+struct WorkspaceMembershipManifest {
+ workspace: WorkspaceMembership,
+}
+
+#[derive(Debug, Deserialize)]
+struct WorkspaceMembership {
+ members: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
struct WorkspaceMembers {
members: Vec<String>,
resolver: String,
package: WorkspacePackage,
+ metadata: WorkspaceMetadata,
+}
+
+#[derive(Debug, Deserialize)]
+struct WorkspaceMetadata {
+ radroots: RadrootsWorkspaceMetadata,
+}
+
+#[derive(Debug, Deserialize)]
+struct RadrootsWorkspaceMetadata {
+ #[serde(rename = "public-package")]
+ public_package: PublicPackageMetadata,
+}
+
+#[derive(Debug, Deserialize)]
+struct PublicPackageMetadata {
+ version: String,
+ authors: Vec<String>,
+ readme: String,
}
#[derive(Debug, Deserialize)]
struct WorkspacePackage {
+ version: String,
+ edition: String,
#[serde(rename = "rust-version")]
rust_version: String,
+ license: String,
+ repository: String,
+ homepage: String,
+ readme: String,
+ authors: Vec<String>,
}
pub fn validate(workspace_root: &Path) -> Result<(), String> {
@@ -71,6 +127,7 @@ pub fn validate(workspace_root: &Path) -> Result<(), String> {
.map_err(|error| format!("parse {}: {error}", architecture_path.display()))?;
validate_workspace_toolchain(workspace_root, &architecture)?;
+ validate_public_package_metadata(workspace_root, &architecture)?;
let ledger_path = workspace_root.join(DEVIATIONS_RELATIVE);
let ledger_raw = fs::read_to_string(&ledger_path)
@@ -101,6 +158,65 @@ fn validate_workspace_toolchain(
manifest.workspace.package.rust_version, architecture.rust_version
));
}
+ let workspace_package = &manifest.workspace.package;
+ let public_metadata = &manifest.workspace.metadata.radroots.public_package;
+ if public_metadata.version != architecture.initial_version
+ || architecture.initial_version != "0.1.0"
+ {
+ return Err(format!(
+ "public package version source {} must match architecture initial_version {}",
+ public_metadata.version, architecture.initial_version
+ ));
+ }
+ if public_metadata.authors != PUBLIC_AUTHORS {
+ return Err("public package authors source must match the workspace convention".to_owned());
+ }
+ if public_metadata.readme != PUBLIC_README {
+ return Err(format!(
+ "public package readme source must be {PUBLIC_README}"
+ ));
+ }
+ if workspace_package.edition != architecture.edition || architecture.edition != "2024" {
+ return Err(format!(
+ "workspace edition {} must match architecture edition {}",
+ workspace_package.edition, architecture.edition
+ ));
+ }
+ if workspace_package.license != architecture.license {
+ return Err(format!(
+ "workspace license {} must match architecture license {}",
+ workspace_package.license, architecture.license
+ ));
+ }
+ if !architecture
+ .canonical_repositories
+ .contains(&workspace_package.repository)
+ {
+ return Err(format!(
+ "workspace repository {} is not canonical",
+ workspace_package.repository
+ ));
+ }
+ if workspace_package.homepage != PUBLIC_HOMEPAGE {
+ return Err(format!(
+ "workspace homepage {} must be {PUBLIC_HOMEPAGE}",
+ workspace_package.homepage
+ ));
+ }
+ if workspace_package.authors != PUBLIC_AUTHORS {
+ return Err("workspace authors must match the public package convention".to_owned());
+ }
+ if !workspace_root.join(&workspace_package.readme).is_file() {
+ return Err(format!(
+ "workspace readme {} does not exist",
+ workspace_package.readme
+ ));
+ }
+ for license in ["LICENSE-MIT", "LICENSE-APACHE"] {
+ if !workspace_root.join(license).is_file() {
+ return Err(format!("workspace is missing {license}"));
+ }
+ }
let toolchain_path = workspace_root.join("rust-toolchain.toml");
let toolchain_raw = fs::read_to_string(&toolchain_path)
.map_err(|error| format!("read {}: {error}", toolchain_path.display()))?;
@@ -120,11 +236,168 @@ fn validate_workspace_toolchain(
Ok(())
}
+fn validate_public_package_metadata(
+ workspace_root: &Path,
+ architecture: &ArchitectureIdentity,
+) -> Result<(), String> {
+ let workspace_raw = fs::read_to_string(workspace_root.join("Cargo.toml"))
+ .map_err(|error| format!("read workspace Cargo.toml: {error}"))?;
+ let workspace = toml::from_str::<WorkspaceManifest>(&workspace_raw)
+ .map_err(|error| format!("parse workspace Cargo.toml: {error}"))?;
+ let repository = architecture
+ .repositories
+ .values()
+ .find(|repository| repository.url == workspace.workspace.package.repository)
+ .ok_or_else(|| "workspace repository has no architecture allocation".to_owned())?;
+ let local_packages = repository.packages.iter().collect::<BTreeSet<_>>();
+ let all_packages = architecture
+ .package
+ .iter()
+ .map(|package| package.name.as_str())
+ .collect::<BTreeSet<_>>();
+
+ for member in &workspace.workspace.members {
+ let manifest_path = workspace_root.join(member).join("Cargo.toml");
+ let raw = fs::read_to_string(&manifest_path)
+ .map_err(|error| format!("read {}: {error}", manifest_path.display()))?;
+ let manifest = raw
+ .parse::<toml::Value>()
+ .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?;
+ let package = manifest
+ .get("package")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{} is missing [package]", manifest_path.display()))?;
+ let name = package
+ .get("name")
+ .and_then(toml::Value::as_str)
+ .ok_or_else(|| format!("{} is missing package.name", manifest_path.display()))?;
+ if !all_packages.contains(name) {
+ continue;
+ }
+ if !local_packages.contains(&name.to_owned()) {
+ return Err(format!(
+ "public package {name} belongs to a different canonical repository"
+ ));
+ }
+ validate_public_manifest_field(
+ package,
+ "version",
+ &workspace.workspace.package.version,
+ &architecture.initial_version,
+ name,
+ )?;
+ validate_public_manifest_field(
+ package,
+ "edition",
+ &workspace.workspace.package.edition,
+ &architecture.edition,
+ name,
+ )?;
+ validate_public_manifest_field(
+ package,
+ "rust-version",
+ &workspace.workspace.package.rust_version,
+ &architecture.rust_version,
+ name,
+ )?;
+ validate_public_manifest_field(
+ package,
+ "license",
+ &workspace.workspace.package.license,
+ &architecture.license,
+ name,
+ )?;
+ validate_public_manifest_field(
+ package,
+ "repository",
+ &workspace.workspace.package.repository,
+ &repository.url,
+ name,
+ )?;
+ validate_public_manifest_field(
+ package,
+ "homepage",
+ &workspace.workspace.package.homepage,
+ PUBLIC_HOMEPAGE,
+ name,
+ )?;
+ let authors = resolve_public_authors(package, &workspace.workspace.package.authors)
+ .ok_or_else(|| format!("public package {name} must declare authors"))?;
+ if authors != PUBLIC_AUTHORS {
+ return Err(format!(
+ "public package {name} authors must match the workspace convention"
+ ));
+ }
+ let readme = package.get("readme").and_then(toml::Value::as_str);
+ if readme != Some(PUBLIC_README)
+ || !workspace_root.join(member).join(PUBLIC_README).is_file()
+ {
+ return Err(format!(
+ "public package {name} must use an existing crate-local {PUBLIC_README}"
+ ));
+ }
+ if package.get("publish").and_then(toml::Value::as_bool) != Some(false) {
+ return Err(format!(
+ "public package {name} must remain publish = false during migration"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_public_manifest_field(
+ package: &toml::value::Table,
+ key: &str,
+ workspace_value: &str,
+ expected: &str,
+ package_name: &str,
+) -> Result<(), String> {
+ let value = resolve_public_string(package, key, workspace_value)
+ .ok_or_else(|| format!("public package {package_name} must declare {key}"))?;
+ if value != expected {
+ return Err(format!(
+ "public package {package_name} {key} {value} must be {expected}"
+ ));
+ }
+ Ok(())
+}
+
+fn resolve_public_string<'a>(
+ package: &'a toml::value::Table,
+ key: &str,
+ workspace_value: &'a str,
+) -> Option<&'a str> {
+ match package.get(key)? {
+ toml::Value::String(value) => Some(value),
+ toml::Value::Table(value)
+ if value.get("workspace").and_then(toml::Value::as_bool) == Some(true) =>
+ {
+ Some(workspace_value)
+ }
+ _ => None,
+ }
+}
+
+fn resolve_public_authors<'a>(
+ package: &'a toml::value::Table,
+ workspace_authors: &'a [String],
+) -> Option<Vec<&'a str>> {
+ match package.get("authors")? {
+ toml::Value::Array(values) => values.iter().map(toml::Value::as_str).collect(),
+ toml::Value::Table(value)
+ if value.get("workspace").and_then(toml::Value::as_bool) == Some(true) =>
+ {
+ Some(workspace_authors.iter().map(String::as_str).collect())
+ }
+ _ => None,
+ }
+}
+
fn validate_workspace_members(workspace_root: &Path) -> Result<(), String> {
let manifest_path = workspace_root.join("Cargo.toml");
let manifest_raw = fs::read_to_string(&manifest_path)
.map_err(|error| format!("read {}: {error}", manifest_path.display()))?;
- let manifest = toml::from_str::<WorkspaceManifest>(&manifest_raw)
+ let manifest = toml::from_str::<WorkspaceMembershipManifest>(&manifest_raw)
.map_err(|error| format!("parse {}: {error}", manifest_path.display()))?;
let declared = manifest
.workspace
@@ -316,14 +589,15 @@ fn is_iso_date(value: &str) -> bool {
#[cfg(test)]
mod tests {
use std::{
+ collections::BTreeMap,
fs,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
use super::{
- ArchitectureIdentity, validate_ledger, validate_workspace_members,
- validate_workspace_toolchain,
+ ArchitectureIdentity, ArchitecturePackage, ArchitectureRepository, validate_ledger,
+ validate_public_package_metadata, validate_workspace_members, validate_workspace_toolchain,
};
fn test_root(label: &str) -> PathBuf {
@@ -361,6 +635,34 @@ adr_required = false
"#
}
+ fn architecture() -> ArchitectureIdentity {
+ ArchitectureIdentity {
+ spec_id: "radroots.crates.release.v1".to_string(),
+ initial_version: "0.1.0".to_string(),
+ edition: "2024".to_string(),
+ resolver: "3".to_string(),
+ rust_version: "1.97.1".to_string(),
+ license: "MIT OR Apache-2.0".to_string(),
+ canonical_repositories: vec!["https://github.com/radrootslabs/sdk".to_string()],
+ repositories: BTreeMap::from([(
+ "sdk".to_string(),
+ ArchitectureRepository {
+ url: "https://github.com/radrootslabs/sdk".to_string(),
+ packages: vec!["radroots".to_string()],
+ },
+ )]),
+ package: vec![ArchitecturePackage {
+ name: "radroots".to_string(),
+ }],
+ }
+ }
+
+ fn complete_workspace_manifest(members: &str) -> String {
+ format!(
+ "[workspace]\nmembers = [{members}]\nresolver = \"3\"\n\n[workspace.package]\nversion = \"0.1.0\"\nedition = \"2024\"\nrust-version = \"1.97.1\"\nlicense = \"MIT OR Apache-2.0\"\nrepository = \"https://github.com/radrootslabs/sdk\"\nhomepage = \"https://radroots.org\"\nreadme = \"README\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\n\n[workspace.metadata.radroots.public-package]\nversion = \"0.1.0\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\nreadme = \"README.md\"\n"
+ )
+ }
+
#[test]
fn accepts_complete_active_deviation() {
let root = test_root("complete");
@@ -413,21 +715,17 @@ adr_required = false
#[test]
fn workspace_toolchain_requires_exact_resolver_and_rust_version() {
let root = test_root("workspace_toolchain");
- fs::write(
- root.join("Cargo.toml"),
- "[workspace]\nmembers = []\nresolver = \"3\"\n\n[workspace.package]\nrust-version = \"1.97.1\"\n",
- )
- .expect("write workspace manifest");
+ fs::write(root.join("Cargo.toml"), complete_workspace_manifest(""))
+ .expect("write workspace manifest");
+ for path in ["README", "LICENSE-MIT", "LICENSE-APACHE"] {
+ fs::write(root.join(path), "fixture\n").expect("write workspace metadata file");
+ }
fs::write(
root.join("rust-toolchain.toml"),
"[toolchain]\nchannel = \"1.97.1\"\n",
)
.expect("write toolchain");
- let architecture = ArchitectureIdentity {
- spec_id: "radroots.crates.release.v1".to_string(),
- resolver: "3".to_string(),
- rust_version: "1.97.1".to_string(),
- };
+ let architecture = architecture();
validate_workspace_toolchain(&root, &architecture).expect("exact toolchain policy");
fs::write(
@@ -440,4 +738,31 @@ adr_required = false
assert!(error.contains("channel must match"));
let _ = fs::remove_dir_all(root);
}
+
+ #[test]
+ fn public_package_metadata_requires_canonical_inheritance() {
+ let root = test_root("public_package_metadata");
+ fs::create_dir_all(root.join("crates/radroots")).expect("create public package");
+ fs::write(
+ root.join("Cargo.toml"),
+ complete_workspace_manifest("\"crates/radroots\""),
+ )
+ .expect("write workspace manifest");
+ let manifest = "[package]\nname = \"radroots\"\nversion.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\nauthors.workspace = true\nreadme = \"README.md\"\npublish = false\n";
+ fs::write(root.join("crates/radroots/Cargo.toml"), manifest)
+ .expect("write public manifest");
+ fs::write(root.join("crates/radroots/README.md"), "fixture\n")
+ .expect("write public readme");
+ validate_public_package_metadata(&root, &architecture()).expect("canonical metadata");
+
+ fs::write(
+ root.join("crates/radroots/Cargo.toml"),
+ manifest.replace("authors.workspace = true\n", ""),
+ )
+ .expect("write incomplete public manifest");
+ let error = validate_public_package_metadata(&root, &architecture())
+ .expect_err("missing authors must fail");
+ assert!(error.contains("must declare authors"));
+ let _ = fs::remove_dir_all(root);
+ }
}