commit 4b653ca15108b173617adc7842684e01190dfdea
parent 2f960e1053ef0e0354d4575b9cd14bfdbe7739d0
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Jul 2026 07:32:22 +0000
release: freeze package publication during refactor
- Make every SDK workspace package explicitly non-publishable.
- Add the two-package Step 305 enablement contract.
- Enforce the freeze in the repository-wide SDK check.
- Test frozen, implicit, approved, and extra package states.
Diffstat:
6 files changed, 237 insertions(+), 6 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -68,7 +68,7 @@ overrides this file for its subtree.
native Rust module layout.
- During migration, every package remains non-publishable until its
package-realistic release gates pass and publication is explicitly
- authorized.
+ authorized. Follow `docs/implementation/PUBLICATION_FREEZE.md`.
## Commits, deviations, and irreversible actions
diff --git a/contracts/releases/publication.toml b/contracts/releases/publication.toml
@@ -0,0 +1,5 @@
+[publication]
+frozen = true
+registry = "crates-io"
+final_enablement_step = 305
+approved_packages = ["radroots-sdk", "radroots"]
diff --git a/crates/runtime_contract_v1/Cargo.toml b/crates/runtime_contract_v1/Cargo.toml
@@ -1,5 +1,6 @@
[package]
name = "radroots_runtime_contract_v1"
+publish = false
description = "Runtime Contract V1 descriptors for Radroots SDK hosts"
version.workspace = true
edition.workspace = true
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "radroots_sdk"
-publish = ["crates-io"]
+publish = false
version.workspace = true
edition.workspace = true
authors = ["Tyson Lupul <tyson@radroots.org>"]
diff --git a/docs/implementation/PUBLICATION_FREEZE.md b/docs/implementation/PUBLICATION_FREEZE.md
@@ -0,0 +1,24 @@
+# Crates.io publication freeze
+
+Crates.io publication is frozen for the complete release-v1 crate refactor.
+Every workspace package, including `radroots-sdk` and the future `radroots`
+facade, must set:
+
+```toml
+publish = false
+```
+
+`contracts/releases/publication.toml` is the machine authority for this
+freeze. `cargo xtask check` rejects a missing publication policy, an unexpected
+registry or enablement checkpoint, and any package that is implicitly or
+explicitly publishable.
+
+The only planned exception is release plan Step 305, after the final package
+inventory, resolved public dependency graph, API surface, target matrix, and
+security gates are green. That checkpoint may set `publication.frozen = false`
+and enable exactly `radroots-sdk` and `radroots` for package-validation
+staging. It does not authorize upload or any crates.io mutation.
+
+Changing the freeze requires an independently reviewed release-control commit.
+Actual publication, tag creation, registry ownership changes, and
+trusted-publisher changes always require separate explicit authorization.
diff --git a/tools/xtask/src/check.rs b/tools/xtask/src/check.rs
@@ -64,9 +64,23 @@ struct CargoMetadataPackage {
name: String,
}
+#[derive(Debug, Deserialize)]
+struct PublicationPolicyFile {
+ publication: PublicationPolicy,
+}
+
+#[derive(Debug, Deserialize)]
+struct PublicationPolicy {
+ frozen: bool,
+ registry: String,
+ final_enablement_step: u16,
+ approved_packages: Vec<String>,
+}
+
pub fn check() -> Result<(), String> {
validate_package_matrix()?;
let root = workspace_root()?;
+ check_publication_policy(&root)?;
validate_sdk_contracts(&root)?;
check_sdk_feature_matrix(&root)?;
check_forbidden_packages(&root)?;
@@ -79,6 +93,87 @@ pub fn check() -> Result<(), String> {
Ok(())
}
+fn check_publication_policy(root: &Path) -> Result<(), String> {
+ let policy_path = root.join("contracts/releases/publication.toml");
+ let raw = fs::read_to_string(&policy_path)
+ .map_err(|error| format!("failed to read {}: {error}", policy_path.display()))?;
+ let policy = toml::from_str::<PublicationPolicyFile>(&raw)
+ .map_err(|error| format!("failed to parse {}: {error}", policy_path.display()))?
+ .publication;
+ if policy.registry != "crates-io" {
+ return Err("publication.registry must be crates-io".to_owned());
+ }
+ if policy.final_enablement_step != 305 {
+ return Err("publication.final_enablement_step must be 305".to_owned());
+ }
+ let approved_package_count = policy.approved_packages.len();
+ let approved = policy
+ .approved_packages
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ if approved.len() != approved_package_count {
+ return Err("publication.approved_packages must not contain duplicates".to_owned());
+ }
+ if approved != BTreeSet::from(["radroots".to_owned(), "radroots-sdk".to_owned()]) {
+ return Err(
+ "publication.approved_packages must contain exactly radroots-sdk and radroots"
+ .to_owned(),
+ );
+ }
+
+ let mut workspace_packages = BTreeSet::new();
+ for path in sdk_manifest_paths(root)?.into_iter().skip(1) {
+ let raw = fs::read_to_string(&path)
+ .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
+ let manifest = raw
+ .parse::<toml::Value>()
+ .map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
+ let package = manifest
+ .get("package")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{} must define [package]", path.display()))?;
+ let name = package
+ .get("name")
+ .and_then(toml::Value::as_str)
+ .ok_or_else(|| format!("{} package.name must be a string", path.display()))?;
+ if !workspace_packages.insert(name.to_owned()) {
+ return Err(format!("duplicate workspace package name {name}"));
+ }
+ let publish = package.get("publish");
+ if policy.frozen {
+ if publish.and_then(toml::Value::as_bool) != Some(false) {
+ return Err(format!(
+ "publication freeze requires workspace package {name} to set publish = false"
+ ));
+ }
+ } else if approved.contains(name) {
+ let registries = publish.and_then(toml::Value::as_array).ok_or_else(|| {
+ format!("approved package {name} must set publish = [\"crates-io\"]")
+ })?;
+ if registries.len() != 1 || registries[0].as_str() != Some("crates-io") {
+ return Err(format!(
+ "approved package {name} must set publish = [\"crates-io\"]"
+ ));
+ }
+ } else if publish.and_then(toml::Value::as_bool) != Some(false) {
+ return Err(format!(
+ "non-approved workspace package {name} must set publish = false"
+ ));
+ }
+ }
+ if !policy.frozen && !approved.is_subset(&workspace_packages) {
+ let missing = approved
+ .difference(&workspace_packages)
+ .cloned()
+ .collect::<Vec<_>>()
+ .join(", ");
+ return Err(format!(
+ "publication enablement is missing approved workspace packages: {missing}"
+ ));
+ }
+ Ok(())
+}
+
fn check_sdk_feature_matrix(root: &Path) -> Result<(), String> {
let path = root.join("crates/sdk/Cargo.toml");
let raw = fs::read_to_string(&path)
@@ -1674,10 +1769,11 @@ mod tests {
PackedPackage, check_binding_crate_sources, check_generated_package_artifact_inventory,
check_manifest_reticulum_dependency_boundaries, check_no_typescript_files,
check_package_distribution_metadata, check_package_index, check_package_json,
- check_package_surface_artifacts, check_packed_package_json, check_reticulum_feature_entry,
- check_wasm_package_surface, consumer_smoke_script, expected_packed_dist_files,
- is_forbidden_reticulum_runtime_name, normalized_package_path, parse_pnpm_pack_entry,
- read_packed_package_json, validate_npm_pack_payload, validate_packed_dist_inventory,
+ check_package_surface_artifacts, check_packed_package_json, check_publication_policy,
+ check_reticulum_feature_entry, check_wasm_package_surface, consumer_smoke_script,
+ expected_packed_dist_files, is_forbidden_reticulum_runtime_name, normalized_package_path,
+ parse_pnpm_pack_entry, read_packed_package_json, validate_npm_pack_payload,
+ validate_packed_dist_inventory,
};
#[test]
@@ -1686,6 +1782,111 @@ mod tests {
}
#[test]
+ fn publication_freeze_rejects_publishable_or_implicit_packages() {
+ let root = test_root("publication_freeze");
+ fs::create_dir_all(root.join("contracts/releases")).expect("create contracts");
+ fs::create_dir_all(root.join("crates/a")).expect("create crate a");
+ fs::create_dir_all(root.join("crates/b")).expect("create crate b");
+ fs::write(
+ root.join("Cargo.toml"),
+ "[workspace]\nmembers = [\"crates/a\", \"crates/b\"]\n",
+ )
+ .expect("write workspace");
+ fs::write(
+ root.join("contracts/releases/publication.toml"),
+ r#"[publication]
+frozen = true
+registry = "crates-io"
+final_enablement_step = 305
+approved_packages = ["radroots-sdk", "radroots"]
+"#,
+ )
+ .expect("write policy");
+ fs::write(
+ root.join("crates/a/Cargo.toml"),
+ "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\npublish = false\n",
+ )
+ .expect("write crate a");
+ fs::write(
+ root.join("crates/b/Cargo.toml"),
+ "[package]\nname = \"crate-b\"\nversion = \"0.1.0\"\npublish = false\n",
+ )
+ .expect("write crate b");
+ check_publication_policy(&root).expect("private workspace satisfies freeze");
+
+ fs::write(
+ root.join("crates/a/Cargo.toml"),
+ "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n",
+ )
+ .expect("make crate a publishable");
+ let publishable =
+ check_publication_policy(&root).expect_err("publishable crate must fail freeze");
+ assert!(publishable.contains("publication freeze requires workspace package crate-a"));
+
+ fs::write(
+ root.join("crates/a/Cargo.toml"),
+ "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\n",
+ )
+ .expect("remove publish control");
+ let implicit =
+ check_publication_policy(&root).expect_err("implicit publishability must fail freeze");
+ assert!(implicit.contains("publication freeze requires workspace package crate-a"));
+
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn publication_enablement_allows_only_the_two_approved_packages() {
+ let root = test_root("publication_enablement");
+ fs::create_dir_all(root.join("contracts/releases")).expect("create contracts");
+ for member in ["sdk", "facade", "tool"] {
+ fs::create_dir_all(root.join("crates").join(member)).expect("create member");
+ }
+ fs::write(
+ root.join("Cargo.toml"),
+ "[workspace]\nmembers = [\"crates/sdk\", \"crates/facade\", \"crates/tool\"]\n",
+ )
+ .expect("write workspace");
+ fs::write(
+ root.join("contracts/releases/publication.toml"),
+ r#"[publication]
+frozen = false
+registry = "crates-io"
+final_enablement_step = 305
+approved_packages = ["radroots-sdk", "radroots"]
+"#,
+ )
+ .expect("write policy");
+ fs::write(
+ root.join("crates/sdk/Cargo.toml"),
+ "[package]\nname = \"radroots-sdk\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n",
+ )
+ .expect("write SDK manifest");
+ fs::write(
+ root.join("crates/facade/Cargo.toml"),
+ "[package]\nname = \"radroots\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n",
+ )
+ .expect("write facade manifest");
+ fs::write(
+ root.join("crates/tool/Cargo.toml"),
+ "[package]\nname = \"build-tool\"\nversion = \"0.1.0\"\npublish = false\n",
+ )
+ .expect("write tool manifest");
+ check_publication_policy(&root).expect("approved package staging should pass");
+
+ fs::write(
+ root.join("crates/tool/Cargo.toml"),
+ "[package]\nname = \"build-tool\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n",
+ )
+ .expect("make tool publishable");
+ let extra = check_publication_policy(&root)
+ .expect_err("non-approved publishable package must fail enablement");
+ assert!(extra.contains("non-approved workspace package build-tool"));
+
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
fn reticulum_gate_allows_first_party_preview_dependency_only() {
let manifest = manifest_value(
r#"