lib

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

commit 3f35c869c827b35b27e7a7d789d409e6c3def6a8
parent 2fd262f2c8f4306c622437bc0c4c36dbfcb331e1
Author: triesap <tyson@radroots.org>
Date:   Wed,  5 Aug 2026 23:54:57 +0000

consolidation: preserve donor history

- Freeze verified source archives, path mappings, and dual-source controls.
- Validate bundle integrity, commit maps, ancestry, attribution, and patches.
- Rehearse merge-bearing filtering, restoration, and a ref-neutral import.
- Reject context, workflow, secret, bot, license, and object-integrity drift.

Diffstat:
Acontracts/consolidation/history.v1.toml | 304+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/consolidation.rs | 1143++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtools/xtask/src/main.rs | 8+++-----
3 files changed, 1448 insertions(+), 7 deletions(-)

diff --git a/contracts/consolidation/history.v1.toml b/contracts/consolidation/history.v1.toml @@ -0,0 +1,304 @@ +schema_version = 1 +history_id = "radroots.rust.consolidation.history.v1" +retention_locator = "external://radroots-rust-consolidation-v1-20260805" +hash_algorithm = "sha256" +bundle_hash_algorithm = "sha1" +required_commit_map_fields = [ + "source_commit", + "target_commit", + "source_parents", + "target_parents", + "source_subject", + "target_subject", + "source_author", + "target_author", + "source_author_time", + "target_author_time", + "source_committer_time", + "target_committer_time", + "source_paths", + "normalized_patch_sha256", + "empty_commit_disposition", +] +required_verifications = [ + "archive_restore", + "git_fsck", + "mapped_parent_closure", + "commit_count", + "path_coverage", + "normalized_patch_equivalence", + "final_tree_digest", + "authorship", + "timestamps", + "message_scope_only", + "git_log_follow", + "secret_scan", + "license_scan", + "bot_identity_scan", + "github_workflow_exclusion", + "context_firewall", +] + +[dual_source] +state = "pre_import" +canonical_owner_before_import = "frozen_donor_commit" +canonical_owner_after_import = "verified_lib_import_merge" +emergency_fix_flow = "fix canonical owner, qualify, then replay the normalized patch into the overlap copy" +divergence_policy = "forbidden" +exit_condition = "all consumers use one qualified lib revision and rollback evidence is green" + +[[archive]] +source_id = "sdk" +kind = "git_bundle" +frozen_commit = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" +artifact = "sdk-170ecf2b620107fadca85fcb11fbf3798b4ca1ef.bundle" +sha256 = "9d2b015bbcd5d516994eeb72064b1e938a04acd0c21eb2e22134b99599fdd2f0" +bytes = 2020833 +source_commit_count = 427 +source_path_commit_count = 397 +rewritten_commit_count = 395 +topology_support_commit_count = 0 +omitted_empty_commit_count = 2 +source_object_count = 6077 +refname = "refs/heads/master" +bot_identity_scan = true + +[[archive]] +source_id = "app_rt" +kind = "git_bundle" +frozen_commit = "7ab1a8624d50890d6d18545ffb47d8083afa8c67" +artifact = "app_rt-7ab1a8624d50890d6d18545ffb47d8083afa8c67.bundle" +sha256 = "9a9ba8e6002ee3d6fcb232e18fb84dbc2885bb37e7bdba736b44a1e5bb6489b0" +bytes = 167343 +source_commit_count = 60 +source_path_commit_count = 49 +rewritten_commit_count = 56 +topology_support_commit_count = 7 +omitted_empty_commit_count = 0 +source_object_count = 508 +refname = "refs/heads/master" +bot_identity_scan = true + +[[archive]] +source_id = "studio_app" +kind = "git_bundle" +frozen_commit = "2b5fe5d8321fd0e248a9d304651b1ba54ee6a180" +artifact = "studio_app-2b5fe5d8321fd0e248a9d304651b1ba54ee6a180.bundle" +sha256 = "27917b71b12392cb005d7125726853c51e56aa615d014a6caa087d4cbd261849" +bytes = 8772591 +source_commit_count = 1230 +source_path_commit_count = 95 +rewritten_commit_count = 93 +topology_support_commit_count = 0 +omitted_empty_commit_count = 2 +source_object_count = 16143 +refname = "refs/heads/master" +bot_identity_scan = true + +[[archive]] +source_id = "studio_mpl_legacy_core" +kind = "path_fast_export" +frozen_commit = "6074a4745be361f21bb47d4778c74a14b2d57954" +artifact = "studio_mpl_legacy_core-6074a4745be361f21bb47d4778c74a14b2d57954.fast-export" +sha256 = "55152f9f2b29fe902c85909f1c9147e5cf0a852f7e23e4ad781638be06389f89" +bytes = 31305 +source_commit_count = 1 +source_path_commit_count = 1 +rewritten_commit_count = 1 +topology_support_commit_count = 0 +omitted_empty_commit_count = 0 +source_object_count = 10 +refname = "refs/heads/master" +bot_identity_scan = true + +[[path_map]] +source_id = "sdk" +source = "crates/sdk" +target = "crates/sdk" +package = "radroots_sdk" +license = "MIT OR Apache-2.0" +disposition = "retain" + +[[path_map]] +source_id = "sdk" +source = "crates/radroots" +target = "crates/radroots" +package = "radroots" +license = "MIT OR Apache-2.0" +disposition = "retain" + +[[path_map]] +source_id = "sdk" +source = "crates/core_bindings" +target = "crates/core_bindings" +package = "radroots_core_bindings" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/event_bindings" +target = "crates/event_bindings" +package = "radroots_event_bindings" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/identity_bindings" +target = "crates/identity_bindings" +package = "radroots_identity_bindings" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/trade_bindings" +target = "crates/trade_bindings" +package = "radroots_trade_bindings" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/replica_schema_bindings" +target = "crates/replica_schema_bindings" +package = "radroots_replica_schema_bindings" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/event_codec_wasm" +target = "crates/event_codec_wasm" +package = "radroots_event_codec_wasm" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/replica_store_wasm" +target = "crates/replica_store_wasm" +package = "radroots_replica_store_wasm" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/replica_sync_wasm" +target = "crates/replica_sync_wasm" +package = "radroots_replica_sync_wasm" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/ffi" +target = "crates/sdk_ffi" +package = "radroots_sdk_ffi" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "crates/sql_wasm_runtime" +target = "crates/sdk_sql_wasm_runtime" +package = "radroots_sdk_sql_wasm_runtime" +license = "MIT OR Apache-2.0" +disposition = "retain_private" + +[[path_map]] +source_id = "sdk" +source = "tools/xtask" +target = "tools/sdk_xtask_import" +package = "xtask" +license = "MIT OR Apache-2.0" +disposition = "merge_then_retire" + +[[path_map]] +source_id = "app_rt" +source = "crates/core" +target = "crates/mobile_core" +package = "radroots_mobile_core" +license = "GPL-3.0-or-later" +disposition = "retain_private" + +[[path_map]] +source_id = "app_rt" +source = "crates/ffi" +target = "crates/mobile_ffi" +package = "radroots_mobile_ffi" +license = "GPL-3.0-or-later" +disposition = "retain_private" + +[[path_map]] +source_id = "app_rt" +source = "crates/wasm" +target = "crates/mobile_wasm" +package = "radroots_mobile_wasm" +license = "GPL-3.0-or-later" +disposition = "retain_private" + +[[path_map]] +source_id = "app_rt" +source = "crates/bindgen" +target = "crates/mobile_bindgen" +package = "radroots_mobile_bindgen" +license = "GPL-3.0-or-later" +disposition = "retain_private" + +[[path_map]] +source_id = "studio_app" +source = "core/crates/domain" +target = "crates/studio_domain" +package = "radroots_studio_domain" +license = "GPL-3.0-only" +disposition = "retain_private" + +[[path_map]] +source_id = "studio_app" +source = "core/crates/application" +target = "crates/studio_application" +package = "radroots_studio_application" +license = "GPL-3.0-only" +disposition = "retain_private" + +[[path_map]] +source_id = "studio_app" +source = "core/crates/nostr" +target = "crates/studio_nostr" +package = "radroots_studio_nostr" +license = "GPL-3.0-only" +disposition = "retain_private" + +[[path_map]] +source_id = "studio_app" +source = "core/crates/storage" +target = "crates/studio_storage" +package = "radroots_studio_storage" +license = "GPL-3.0-only" +disposition = "retain_private" + +[[path_map]] +source_id = "studio_app" +source = "core/crates/ffi" +target = "crates/studio_ffi" +package = "radroots_studio_ffi" +license = "GPL-3.0-only" +disposition = "retain_private" + +[[path_map]] +source_id = "studio_app" +source = "core/tools/uniffi-bindgen" +target = "crates/studio_uniffi_bindgen" +package = "radroots_studio_uniffi_bindgen" +license = "GPL-3.0-only" +disposition = "retain_private" + +[[path_map]] +source_id = "studio_mpl_legacy_core" +source = "studio_app/studio_app_core" +target = "imports/studio_mpl_legacy_core" +package = "studio_app_core" +license = "MPL-2.0" +disposition = "import_unique_behavior_then_retire" diff --git a/tools/xtask/src/consolidation.rs b/tools/xtask/src/consolidation.rs @@ -1,16 +1,21 @@ use std::{ collections::{BTreeMap, BTreeSet}, fs, + io::Read, path::{Component, Path}, + process::Command, }; use serde::Deserialize; +use sha2::{Digest, Sha256}; const BASELINE_RELATIVE: &str = "contracts/consolidation/baseline.v1.toml"; const STEP_MAP_RELATIVE: &str = "contracts/consolidation/handoff_steps.v1.toml"; +const HISTORY_RELATIVE: &str = "contracts/consolidation/history.v1.toml"; const BASELINE_ID: &str = "radroots.rust.consolidation.baseline.v1"; const STEP_MAP_ID: &str = "radroots.rust.consolidation.handoff-steps.v1"; const ARCHITECTURE_TARGET: &str = "radroots.crates.release.v2"; +const HISTORY_ID: &str = "radroots.rust.consolidation.history.v1"; const PACKAGE_VERSION: &str = "0.1.0-alpha"; const EXPECTED_PUBLIC_PACKAGES: u16 = 19; const EXPECTED_HANDOFF_STEPS: u16 = 275; @@ -119,13 +124,102 @@ struct StepRange { reason: String, } -pub fn validate(workspace_root: &Path) -> Result<(), String> { +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct HistoryContract { + schema_version: u16, + history_id: String, + retention_locator: String, + hash_algorithm: String, + bundle_hash_algorithm: String, + required_commit_map_fields: Vec<String>, + required_verifications: Vec<String>, + dual_source: DualSource, + archive: Vec<Archive>, + path_map: Vec<PathMap>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DualSource { + state: String, + canonical_owner_before_import: String, + canonical_owner_after_import: String, + emergency_fix_flow: String, + divergence_policy: String, + exit_condition: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Archive { + source_id: String, + kind: String, + frozen_commit: String, + artifact: String, + sha256: String, + bytes: u64, + source_commit_count: u64, + source_path_commit_count: u64, + rewritten_commit_count: u64, + topology_support_commit_count: u64, + omitted_empty_commit_count: u64, + source_object_count: u64, + refname: String, + bot_identity_scan: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct PathMap { + source_id: String, + source: String, + target: String, + package: String, + license: String, + disposition: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct CommitMapEntry { + source: String, + target: Option<String>, +} + +pub fn run(args: &[String], workspace_root: &Path) -> Result<(), String> { + match args { + [command] if command == "baseline" => validate_baseline_contracts(workspace_root), + [command] if command == "history" => validate_history_contract(workspace_root, None), + [command] if command == "history-rehearsal" => run_history_rehearsal(), + [command, flag, archive_root] if command == "history" && flag == "--archive-root" => { + validate_history_contract(workspace_root, Some(Path::new(archive_root))) + } + _ => Err( + "consolidation accepts baseline, history [--archive-root <absolute-directory>], or history-rehearsal" + .to_owned(), + ), + } +} + +pub fn validate_baseline_contracts(workspace_root: &Path) -> Result<(), String> { let baseline = read_toml::<Baseline>(workspace_root, BASELINE_RELATIVE)?; let step_map = read_toml::<StepMap>(workspace_root, STEP_MAP_RELATIVE)?; validate_baseline(&baseline)?; validate_step_map(&step_map) } +fn validate_history_contract( + workspace_root: &Path, + archive_root: Option<&Path>, +) -> Result<(), String> { + let history = read_toml::<HistoryContract>(workspace_root, HISTORY_RELATIVE)?; + validate_history(&history)?; + if let Some(archive_root) = archive_root { + validate_archives(&history, archive_root)?; + } + Ok(()) +} + fn read_toml<T: for<'de> Deserialize<'de>>( workspace_root: &Path, relative: &str, @@ -348,6 +442,1034 @@ fn validate_step_map(step_map: &StepMap) -> Result<(), String> { Ok(()) } +fn validate_history(history: &HistoryContract) -> Result<(), String> { + if history.schema_version != 1 + || history.history_id != HISTORY_ID + || history.retention_locator != "external://radroots-rust-consolidation-v1-20260805" + || history.hash_algorithm != "sha256" + || history.bundle_hash_algorithm != "sha1" + { + return Err("history preservation identity drifted".to_owned()); + } + + let expected_commit_map_fields = BTreeSet::from([ + "empty_commit_disposition", + "normalized_patch_sha256", + "source_author", + "source_author_time", + "source_commit", + "source_committer_time", + "source_parents", + "source_paths", + "source_subject", + "target_author", + "target_author_time", + "target_commit", + "target_committer_time", + "target_parents", + "target_subject", + ]); + if to_unique_set(&history.required_commit_map_fields, "commit map field")? + != expected_commit_map_fields + { + return Err("commit map requirements drifted".to_owned()); + } + let expected_verifications = BTreeSet::from([ + "archive_restore", + "authorship", + "bot_identity_scan", + "commit_count", + "context_firewall", + "final_tree_digest", + "git_fsck", + "git_log_follow", + "github_workflow_exclusion", + "license_scan", + "mapped_parent_closure", + "message_scope_only", + "normalized_patch_equivalence", + "path_coverage", + "secret_scan", + "timestamps", + ]); + if to_unique_set(&history.required_verifications, "verification")? != expected_verifications { + return Err("history verification requirements drifted".to_owned()); + } + let dual_source = &history.dual_source; + if dual_source.state != "pre_import" + || dual_source.canonical_owner_before_import != "frozen_donor_commit" + || dual_source.canonical_owner_after_import != "verified_lib_import_merge" + || dual_source.divergence_policy != "forbidden" + || dual_source.emergency_fix_flow.trim().is_empty() + || dual_source.exit_condition.trim().is_empty() + { + return Err("dual-source control drifted".to_owned()); + } + + let expected_sources = + BTreeSet::from(["app_rt", "sdk", "studio_app", "studio_mpl_legacy_core"]); + let mut archives = BTreeMap::new(); + let mut artifact_names = BTreeSet::new(); + for archive in &history.archive { + validate_identifier(&archive.source_id, "archive source id")?; + if archives + .insert(archive.source_id.as_str(), archive) + .is_some() + { + return Err(format!("duplicate archive source {}", archive.source_id)); + } + validate_oid(&archive.frozen_commit, "archive frozen commit")?; + validate_sha256(&archive.sha256, "archive digest")?; + validate_artifact_name(&archive.artifact)?; + if !artifact_names.insert(archive.artifact.as_str()) { + return Err(format!("duplicate archive artifact {}", archive.artifact)); + } + if !matches!(archive.kind.as_str(), "git_bundle" | "path_fast_export") + || archive.bytes == 0 + || archive.source_commit_count == 0 + || archive.source_path_commit_count == 0 + || archive.source_path_commit_count > archive.source_commit_count + || archive.rewritten_commit_count == 0 + || archive.rewritten_commit_count + archive.omitted_empty_commit_count + != archive.source_path_commit_count + archive.topology_support_commit_count + || archive.source_object_count == 0 + || archive.refname != "refs/heads/master" + || !archive.bot_identity_scan + { + return Err(format!("archive {} is incomplete", archive.source_id)); + } + } + if archives.keys().copied().collect::<BTreeSet<_>>() != expected_sources { + return Err("history archive inventory is incomplete".to_owned()); + } + if archives["studio_mpl_legacy_core"].kind != "path_fast_export" + || archives + .iter() + .filter(|(id, archive)| { + **id != "studio_mpl_legacy_core" && archive.kind == "git_bundle" + }) + .count() + != 3 + { + return Err("history archive kinds drifted".to_owned()); + } + + let allowed_licenses = BTreeSet::from([ + "GPL-3.0-only", + "GPL-3.0-or-later", + "MIT OR Apache-2.0", + "MPL-2.0", + ]); + let allowed_dispositions = BTreeSet::from([ + "import_unique_behavior_then_retire", + "merge_then_retire", + "retain", + "retain_private", + ]); + let mut source_paths = BTreeSet::new(); + let mut target_paths = BTreeSet::new(); + let mut package_sources = BTreeSet::new(); + for path_map in &history.path_map { + if !archives.contains_key(path_map.source_id.as_str()) { + return Err(format!("unknown path-map source {}", path_map.source_id)); + } + validate_relative_path(&path_map.source)?; + validate_relative_path(&path_map.target)?; + validate_identifier(&path_map.package, "path-map package")?; + if !allowed_licenses.contains(path_map.license.as_str()) + || !allowed_dispositions.contains(path_map.disposition.as_str()) + { + return Err(format!( + "path map {}/{} has invalid license or disposition", + path_map.source_id, path_map.source + )); + } + if !source_paths.insert((path_map.source_id.as_str(), path_map.source.as_str())) { + return Err(format!( + "duplicate source path {}/{}", + path_map.source_id, path_map.source + )); + } + if !target_paths.insert(path_map.target.as_str()) { + return Err(format!("duplicate target path {}", path_map.target)); + } + if !package_sources.insert((path_map.source_id.as_str(), path_map.package.as_str())) { + return Err(format!( + "duplicate package {} in source {}", + path_map.package, path_map.source_id + )); + } + } + for source in expected_sources { + if !source_paths + .iter() + .any(|(candidate, _)| *candidate == source) + { + return Err(format!("source {source} has no path map")); + } + } + Ok(()) +} + +fn validate_archives(history: &HistoryContract, archive_root: &Path) -> Result<(), String> { + if !archive_root.is_absolute() { + return Err("archive root must be absolute".to_owned()); + } + let root_metadata = fs::symlink_metadata(archive_root) + .map_err(|error| format!("inspect archive root {}: {error}", archive_root.display()))?; + if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() { + return Err("archive root must be a real directory, not a symlink".to_owned()); + } + + for archive in &history.archive { + let artifact = archive_root.join(&archive.artifact); + let metadata = fs::symlink_metadata(&artifact) + .map_err(|error| format!("inspect archive {}: {error}", artifact.display()))?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(format!( + "archive {} must be a regular non-symlink file", + artifact.display() + )); + } + if metadata.len() != archive.bytes { + return Err(format!( + "archive {} size drifted: expected {}, got {}", + artifact.display(), + archive.bytes, + metadata.len() + )); + } + let digest = sha256_file(&artifact)?; + if digest != archive.sha256 { + return Err(format!("archive {} digest drifted", artifact.display())); + } + if archive.kind == "git_bundle" { + let output = Command::new("git") + .args(["bundle", "verify"]) + .arg(&artifact) + .output() + .map_err(|error| format!("run git bundle verify: {error}"))?; + if !output.status.success() { + return Err(format!( + "git bundle verify failed for {}: {}", + artifact.display(), + String::from_utf8_lossy(&output.stderr).trim() + )); + } + } else { + validate_fast_export(&artifact, archive)?; + } + } + Ok(()) +} + +fn validate_fast_export(path: &Path, archive: &Archive) -> Result<(), String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("read fast-export archive {}: {error}", path.display()))?; + if !raw.contains(&format!("original-oid {}", archive.frozen_commit)) + || !raw.contains("reset refs/heads/master") + || !raw.contains("author triesap <tyson@radroots.org>") + || !raw.contains("committer triesap <tyson@radroots.org>") + || raw.to_ascii_lowercase().contains("github-actions") + || raw.to_ascii_lowercase().contains("[bot]") + { + return Err("legacy Studio fast-export identity or ref drifted".to_owned()); + } + for line in raw.lines() { + let Some(path) = line + .strip_prefix("M ") + .and_then(|line| line.splitn(3, ' ').nth(2)) + else { + continue; + }; + if !path.starts_with("studio_app/studio_app_core/") { + return Err(format!("fast-export contains out-of-scope path {path}")); + } + } + Ok(()) +} + +fn run_history_rehearsal() -> Result<(), String> { + let fixture = tempfile::TempDir::new() + .map_err(|error| format!("create history rehearsal root: {error}"))?; + let source = fixture.path().join("source"); + let restored = fixture.path().join("restored"); + let filtered = fixture.path().join("filtered"); + let import_target = fixture.path().join("import-target"); + let bundle = fixture.path().join("source.bundle"); + + create_history_fixture(&source)?; + git(&source, &["bundle", "create", path_arg(&bundle)?, "master"])?; + git( + fixture.path(), + &["clone", path_arg(&bundle)?, path_arg(&restored)?], + )?; + git(&restored, &["fsck", "--full", "--strict"])?; + + git( + fixture.path(), + &["clone", path_arg(&bundle)?, path_arg(&filtered)?], + )?; + git( + &filtered, + &[ + "filter-repo", + "--force", + "--path", + "src/", + "--path-rename", + "src/:crates/imported/", + ], + )?; + let commit_map_path = filtered.join(".git/filter-repo/commit-map"); + let commit_map = parse_commit_map(&commit_map_path)?; + verify_filtered_history(&source, &filtered, &commit_map, "src", "crates/imported")?; + + create_import_target(&import_target)?; + git( + &import_target, + &[ + "fetch", + path_arg(&filtered)?, + "master:refs/remotes/rehearsal/imported", + ], + )?; + let merge_tree = git_stdout( + &import_target, + &[ + "merge-tree", + "--write-tree", + "--allow-unrelated-histories", + "HEAD", + "refs/remotes/rehearsal/imported", + ], + )?; + let merge_tree = merge_tree.trim(); + validate_oid(merge_tree, "rehearsal merge tree")?; + if git_stdout(&import_target, &["cat-file", "-t", merge_tree])?.trim() != "tree" { + return Err("no-op import rehearsal did not produce a tree".to_owned()); + } + if git_stdout(&import_target, &["status", "--porcelain"]) + .map(|output| !output.trim().is_empty())? + { + return Err("no-op import rehearsal changed the target worktree".to_owned()); + } + + exercise_history_negative_cases(&source, &filtered, &commit_map)?; + Ok(()) +} + +fn create_history_fixture(root: &Path) -> Result<(), String> { + fs::create_dir(root).map_err(|error| format!("create {}: {error}", root.display()))?; + git(root, &["init", "--initial-branch=master"])?; + git(root, &["config", "user.name", "Radroots History Fixture"])?; + git( + root, + &["config", "user.email", "history-fixture@radroots.org"], + )?; + write_fixture(root, "src/LICENSE", "MIT OR Apache-2.0\n")?; + write_fixture(root, "src/item.txt", "base\n")?; + write_fixture(root, "AGENTS.md", "context only\n")?; + fixture_commit(root, "seed reusable source", "2001-01-01T00:00:00+00:00")?; + + git(root, &["branch", "feature"])?; + append_fixture(root, "src/item.txt", "main\n")?; + fixture_commit(root, "extend main source", "2001-01-02T00:00:00+00:00")?; + append_fixture(root, "AGENTS.md", "must not import\n")?; + fixture_commit(root, "change donor context", "2001-01-03T00:00:00+00:00")?; + + git(root, &["checkout", "feature"])?; + write_fixture(root, "src/feature.txt", "feature\n")?; + fixture_commit(root, "add feature source", "2001-01-04T00:00:00+00:00")?; + git(root, &["checkout", "master"])?; + git_with_identity( + root, + &["merge", "--no-ff", "feature", "-m", "merge reusable source"], + "2001-01-05T00:00:00+00:00", + )?; + write_fixture(root, ".github/workflows/forbidden.yml", "forbidden: true\n")?; + fixture_commit( + root, + "add forbidden donor automation", + "2001-01-06T00:00:00+00:00", + )?; + append_fixture(root, "src/item.txt", "final\n")?; + fixture_commit(root, "finish reusable source", "2001-01-07T00:00:00+00:00")?; + Ok(()) +} + +fn create_import_target(root: &Path) -> Result<(), String> { + fs::create_dir(root).map_err(|error| format!("create {}: {error}", root.display()))?; + git(root, &["init", "--initial-branch=master"])?; + git(root, &["config", "user.name", "Radroots History Fixture"])?; + git( + root, + &["config", "user.email", "history-fixture@radroots.org"], + )?; + write_fixture(root, "README", "import target\n")?; + fixture_commit(root, "seed import target", "2001-01-08T00:00:00+00:00") +} + +fn write_fixture(root: &Path, relative: &str, contents: &str) -> Result<(), String> { + let path = root.join(relative); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("create {}: {error}", parent.display()))?; + } + fs::write(&path, contents).map_err(|error| format!("write {}: {error}", path.display())) +} + +fn append_fixture(root: &Path, relative: &str, contents: &str) -> Result<(), String> { + use std::io::Write; + + let path = root.join(relative); + let mut file = fs::OpenOptions::new() + .append(true) + .open(&path) + .map_err(|error| format!("open {}: {error}", path.display()))?; + file.write_all(contents.as_bytes()) + .map_err(|error| format!("append {}: {error}", path.display())) +} + +fn fixture_commit(root: &Path, subject: &str, timestamp: &str) -> Result<(), String> { + git(root, &["add", "--all"])?; + git_with_identity(root, &["commit", "-m", subject], timestamp) +} + +fn git_with_identity(root: &Path, args: &[&str], timestamp: &str) -> Result<(), String> { + let output = Command::new("git") + .args(args) + .current_dir(root) + .env("GIT_AUTHOR_DATE", timestamp) + .env("GIT_COMMITTER_DATE", timestamp) + .output() + .map_err(|error| format!("run git {}: {error}", args.join(" ")))?; + command_success(output, root, args).map(|_| ()) +} + +fn parse_commit_map(path: &Path) -> Result<Vec<CommitMapEntry>, String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("read commit map {}: {error}", path.display()))?; + let mut entries = Vec::new(); + for (line_index, line) in raw.lines().enumerate() { + if line_index == 0 && line == "old new" { + continue; + } + let fields = line.split_ascii_whitespace().collect::<Vec<_>>(); + if fields.len() != 2 { + return Err(format!("commit map line {} is malformed", line_index + 1)); + } + validate_oid(fields[0], "source commit-map object")?; + validate_oid(fields[1], "target commit-map object")?; + entries.push(CommitMapEntry { + source: fields[0].to_owned(), + target: (fields[1] != "0000000000000000000000000000000000000000") + .then(|| fields[1].to_owned()), + }); + } + if entries.is_empty() { + return Err("commit map contains no entries".to_owned()); + } + let unique = entries + .iter() + .map(|entry| entry.source.as_str()) + .collect::<BTreeSet<_>>(); + if unique.len() != entries.len() { + return Err("commit map contains duplicate source commits".to_owned()); + } + Ok(entries) +} + +fn verify_filtered_history( + source: &Path, + target: &Path, + entries: &[CommitMapEntry], + source_prefix: &str, + target_prefix: &str, +) -> Result<(), String> { + validate_relative_path(source_prefix)?; + validate_relative_path(target_prefix)?; + git(source, &["fsck", "--full", "--strict"])?; + git(target, &["fsck", "--full", "--strict"])?; + + let by_source = entries + .iter() + .map(|entry| (entry.source.as_str(), entry.target.as_deref())) + .collect::<BTreeMap<_, _>>(); + let mut saw_merge = false; + let mut saw_omitted = false; + for entry in entries { + git( + source, + &["cat-file", "-e", &format!("{}^{{commit}}", entry.source)], + )?; + let Some(target_commit) = entry.target.as_deref() else { + saw_omitted = true; + continue; + }; + git( + target, + &["cat-file", "-e", &format!("{target_commit}^{{commit}}")], + )?; + verify_commit_metadata(source, target, &entry.source, target_commit)?; + let source_parents = commit_parents(source, &entry.source)?; + saw_merge |= source_parents.len() > 1; + let mut expected_target_parents = Vec::new(); + for parent in source_parents { + collect_effective_parents(source, &parent, &by_source, &mut expected_target_parents)?; + } + deduplicate(&mut expected_target_parents); + if commit_parents(target, target_commit)? != expected_target_parents { + return Err(format!( + "mapped parent closure drifted for {}", + entry.source + )); + } + let source_patch = normalized_patch(source, &entry.source, source_prefix, "__IMPORT__")?; + let target_patch = normalized_patch(target, target_commit, target_prefix, "__IMPORT__")?; + if source_patch != target_patch { + return Err(format!("normalized patch drifted for {}", entry.source)); + } + } + if !saw_merge || !saw_omitted { + return Err("history rehearsal must include a merge and an omitted commit".to_owned()); + } + + let source_head = git_stdout(source, &["rev-parse", "master"])?; + let source_head = source_head.trim(); + let expected_target_head = by_source + .get(source_head) + .and_then(|target| *target) + .ok_or_else(|| "source master is not retained in commit map".to_owned())?; + if git_stdout(target, &["rev-parse", "master"])?.trim() != expected_target_head { + return Err("filtered master has unexpected commits".to_owned()); + } + verify_final_tree( + source, + target, + source_head, + expected_target_head, + source_prefix, + target_prefix, + )?; + verify_context_firewall(target, expected_target_head, target_prefix)?; + verify_commit_identities(target, expected_target_head)?; + verify_follow_history(source, target, source_prefix, target_prefix)?; + Ok(()) +} + +fn verify_commit_metadata( + source: &Path, + target: &Path, + source_commit: &str, + target_commit: &str, +) -> Result<(), String> { + let format = "%an%x00%ae%x00%aI%x00%cn%x00%ce%x00%cI%x00%s"; + let source_meta = git_stdout( + source, + &["show", "-s", &format!("--format={format}"), source_commit], + )?; + let target_meta = git_stdout( + target, + &["show", "-s", &format!("--format={format}"), target_commit], + )?; + let mut source_fields = source_meta.trim_end().split('\0').collect::<Vec<_>>(); + let mut target_fields = target_meta.trim_end().split('\0').collect::<Vec<_>>(); + if source_fields.len() != 7 || target_fields.len() != 7 { + return Err("commit metadata has unexpected cardinality".to_owned()); + } + let source_subject = source_fields.pop().expect("cardinality checked"); + let target_subject = target_fields.pop().expect("cardinality checked"); + if source_fields != target_fields || !message_is_preserved(source_subject, target_subject) { + return Err(format!( + "attribution or message drifted for {source_commit}" + )); + } + Ok(()) +} + +fn message_is_preserved(source: &str, target: &str) -> bool { + if source == target { + return true; + } + let Some(scope) = target + .strip_prefix(source) + .and_then(|suffix| suffix.strip_prefix(" (")) + .and_then(|suffix| suffix.strip_suffix(')')) + else { + return false; + }; + !scope.is_empty() + && scope.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-' | b'/') + }) +} + +fn commit_parents(repo: &Path, commit: &str) -> Result<Vec<String>, String> { + let output = git_stdout(repo, &["show", "-s", "--format=%P", commit])?; + Ok(output.split_ascii_whitespace().map(str::to_owned).collect()) +} + +fn collect_effective_parents( + source: &Path, + commit: &str, + by_source: &BTreeMap<&str, Option<&str>>, + output: &mut Vec<String>, +) -> Result<(), String> { + match by_source.get(commit).copied().flatten() { + Some(target) => output.push(target.to_owned()), + None => { + for parent in commit_parents(source, commit)? { + collect_effective_parents(source, &parent, by_source, output)?; + } + } + } + Ok(()) +} + +fn deduplicate(values: &mut Vec<String>) { + let mut seen = BTreeSet::new(); + values.retain(|value| seen.insert(value.clone())); +} + +fn normalized_patch( + repo: &Path, + commit: &str, + prefix: &str, + normalized_prefix: &str, +) -> Result<String, String> { + let patch = git_stdout( + repo, + &[ + "-c", + "core.quotePath=false", + "diff-tree", + "--root", + "-m", + "-r", + "--binary", + "--full-index", + "--no-commit-id", + commit, + "--", + prefix, + ], + )?; + Ok(patch.replace(prefix, normalized_prefix)) +} + +fn verify_final_tree( + source: &Path, + target: &Path, + source_commit: &str, + target_commit: &str, + source_prefix: &str, + target_prefix: &str, +) -> Result<(), String> { + let source_tree = tree_manifest(source, source_commit, source_prefix, source_prefix)?; + let target_tree = tree_manifest(target, target_commit, target_prefix, source_prefix)?; + if source_tree != target_tree { + return Err("filtered final tree drifted".to_owned()); + } + Ok(()) +} + +fn tree_manifest( + repo: &Path, + commit: &str, + prefix: &str, + normalized_prefix: &str, +) -> Result<String, String> { + let output = git_stdout( + repo, + &["ls-tree", "-r", "--full-tree", commit, "--", prefix], + )?; + Ok(output.replace(prefix, normalized_prefix)) +} + +fn verify_context_firewall(repo: &Path, commit: &str, target_prefix: &str) -> Result<(), String> { + let paths = git_stdout(repo, &["ls-tree", "-r", "--name-only", commit])?; + let required_prefix = format!("{target_prefix}/"); + for path in paths.lines() { + let lower = path.to_ascii_lowercase(); + if !path.starts_with(&required_prefix) + || path.split('/').any(|segment| segment == ".github") + || matches!(path.rsplit('/').next(), Some("AGENTS.md" | "CLAUDE.md")) + || lower.ends_with(".pem") + || lower.ends_with(".key") + || lower.ends_with("/.env") + { + return Err(format!("context firewall rejected {path}")); + } + let contents = git_stdout(repo, &["show", &format!("{commit}:{path}")])?; + let lower_contents = contents.to_ascii_lowercase(); + if contents.contains("PRIVATE KEY-----") + || contents.contains("ghp_") + || lower_contents.contains("github-actions[bot]") + { + return Err(format!("secret or bot content rejected in {path}")); + } + } + Ok(()) +} + +fn verify_commit_identities(repo: &Path, commit: &str) -> Result<(), String> { + let identities = git_stdout(repo, &["log", "--format=%an%x00%ae%x00%cn%x00%ce", commit])?; + let lower = identities.to_ascii_lowercase(); + if lower.contains("github-actions") || lower.contains("[bot]") { + return Err("bot identity found in filtered history".to_owned()); + } + Ok(()) +} + +fn verify_follow_history( + source: &Path, + target: &Path, + source_prefix: &str, + target_prefix: &str, +) -> Result<(), String> { + let source_file = format!("{source_prefix}/item.txt"); + let target_file = format!("{target_prefix}/item.txt"); + let source_log = git_stdout( + source, + &["log", "--follow", "--format=%s", "--", &source_file], + )?; + let target_log = git_stdout( + target, + &["log", "--follow", "--format=%s", "--", &target_file], + )?; + if source_log != target_log { + return Err("git log --follow attribution drifted".to_owned()); + } + Ok(()) +} + +fn exercise_history_negative_cases( + source: &Path, + filtered: &Path, + entries: &[CommitMapEntry], +) -> Result<(), String> { + if message_is_preserved("preserve this", "rewritten message") + || message_is_preserved("preserve this", "preserve this (Bad Scope)") + { + return Err("message negative fixture was accepted".to_owned()); + } + let malformed_map = filtered.join("malformed-commit-map"); + fs::write(&malformed_map, "old new\nnot-an-oid still-not-an-oid\n") + .map_err(|error| format!("write negative commit map: {error}"))?; + if parse_commit_map(&malformed_map).is_ok() { + return Err("malformed commit map was accepted".to_owned()); + } + let head = entries + .iter() + .rev() + .find_map(|entry| entry.target.as_deref()) + .ok_or_else(|| "negative fixture has no target head".to_owned())?; + if verify_context_firewall(filtered, head, "wrong/prefix").is_ok() { + return Err("context-firewall negative fixture was accepted".to_owned()); + } + if normalized_patch(source, &entries[0].source, "src", "__IMPORT__")? + == normalized_patch(filtered, head, "crates/imported", "__WRONG__")? + { + return Err("normalized-patch negative fixture was accepted".to_owned()); + } + + let source_head = git_stdout(source, &["rev-parse", "master"])?; + let source_head = source_head.trim(); + let negative_root = filtered + .parent() + .ok_or_else(|| "filtered fixture has no parent".to_owned())?; + + let context = clone_negative(filtered, negative_root, "negative-context")?; + write_fixture( + &context, + "AGENTS.md", + "must not cross the source boundary\n", + )?; + fixture_commit(&context, "inject context", "2001-02-01T00:00:00+00:00")?; + let context_head = git_stdout(&context, &["rev-parse", "HEAD"])?; + if verify_context_firewall(&context, context_head.trim(), "crates/imported").is_ok() { + return Err("context negative fixture was accepted".to_owned()); + } + + let workflow = clone_negative(filtered, negative_root, "negative-workflow")?; + write_fixture( + &workflow, + ".github/workflows/forbidden.yml", + "forbidden: true\n", + )?; + fixture_commit(&workflow, "inject workflow", "2001-02-02T00:00:00+00:00")?; + let workflow_head = git_stdout(&workflow, &["rev-parse", "HEAD"])?; + if verify_context_firewall(&workflow, workflow_head.trim(), "crates/imported").is_ok() { + return Err("GitHub-workflow negative fixture was accepted".to_owned()); + } + + let secret = clone_negative(filtered, negative_root, "negative-secret")?; + write_fixture( + &secret, + "crates/imported/secret.pem", + "-----BEGIN PRIVATE KEY-----\nfixture\n", + )?; + fixture_commit(&secret, "inject secret", "2001-02-03T00:00:00+00:00")?; + let secret_head = git_stdout(&secret, &["rev-parse", "HEAD"])?; + if verify_context_firewall(&secret, secret_head.trim(), "crates/imported").is_ok() { + return Err("secret negative fixture was accepted".to_owned()); + } + + let bot = clone_negative(filtered, negative_root, "negative-bot")?; + git_commit_with_actor( + &bot, + "inject bot identity", + "github-actions[bot]", + "41898282+github-actions[bot]@users.noreply.github.com", + "2001-02-04T00:00:00+00:00", + true, + )?; + let bot_head = git_stdout(&bot, &["rev-parse", "HEAD"])?; + if verify_commit_identities(&bot, bot_head.trim()).is_ok() { + return Err("bot-identity negative fixture was accepted".to_owned()); + } + + let license = clone_negative(filtered, negative_root, "negative-license")?; + write_fixture(&license, "crates/imported/LICENSE", "GPL-3.0-only\n")?; + fixture_commit(&license, "change license", "2001-02-05T00:00:00+00:00")?; + let license_head = git_stdout(&license, &["rev-parse", "HEAD"])?; + if verify_final_tree( + source, + &license, + source_head, + license_head.trim(), + "src", + "crates/imported", + ) + .is_ok() + { + return Err("license/tree negative fixture was accepted".to_owned()); + } + + let attribution = clone_negative(filtered, negative_root, "negative-attribution")?; + git_commit_with_actor( + &attribution, + "finish reusable source", + "Wrong Author", + "wrong-author@radroots.org", + "2001-01-07T00:00:00+00:00", + false, + )?; + let attribution_head = git_stdout(&attribution, &["rev-parse", "HEAD"])?; + if verify_commit_metadata(source, &attribution, source_head, attribution_head.trim()).is_ok() { + return Err("attribution negative fixture was accepted".to_owned()); + } + + let timestamp = clone_negative(filtered, negative_root, "negative-timestamp")?; + git_commit_with_actor( + &timestamp, + "finish reusable source", + "Radroots History Fixture", + "history-fixture@radroots.org", + "2002-01-07T00:00:00+00:00", + false, + )?; + let timestamp_head = git_stdout(&timestamp, &["rev-parse", "HEAD"])?; + if verify_commit_metadata(source, &timestamp, source_head, timestamp_head.trim()).is_ok() { + return Err("timestamp negative fixture was accepted".to_owned()); + } + + let message = clone_negative(filtered, negative_root, "negative-message")?; + git_commit_with_actor( + &message, + "replace the original message", + "Radroots History Fixture", + "history-fixture@radroots.org", + "2001-01-07T00:00:00+00:00", + false, + )?; + let message_head = git_stdout(&message, &["rev-parse", "HEAD"])?; + if verify_commit_metadata(source, &message, source_head, message_head.trim()).is_ok() { + return Err("message negative fixture was accepted".to_owned()); + } + + let follow = clone_negative(filtered, negative_root, "negative-follow")?; + append_fixture(&follow, "crates/imported/item.txt", "unmapped\n")?; + fixture_commit( + &follow, + "inject unmapped history", + "2001-02-06T00:00:00+00:00", + )?; + if verify_follow_history(source, &follow, "src", "crates/imported").is_ok() { + return Err("git-log-follow negative fixture was accepted".to_owned()); + } + + let mut broken_map = entries.to_vec(); + let replacement = broken_map + .iter() + .find_map(|entry| entry.target.clone()) + .ok_or_else(|| "negative fixture has no replacement target".to_owned())?; + broken_map + .last_mut() + .ok_or_else(|| "negative fixture has no final map entry".to_owned())? + .target = Some(replacement); + if verify_filtered_history(source, filtered, &broken_map, "src", "crates/imported").is_ok() { + return Err("commit-map topology negative fixture was accepted".to_owned()); + } + + let corrupt = clone_negative(filtered, negative_root, "negative-fsck")?; + let payload = corrupt.join("fsck-negative-payload"); + fs::write(&payload, "unique fsck negative fixture payload\n") + .map_err(|error| format!("write fsck negative payload: {error}"))?; + let object = git_stdout(&corrupt, &["hash-object", "-w", path_arg(&payload)?])?; + let object = object.trim(); + validate_oid(object, "fsck negative object")?; + let object_path = corrupt + .join(".git/objects") + .join(&object[..2]) + .join(&object[2..]); + fs::remove_file(&object_path).map_err(|error| { + format!( + "unlink exact temporary negative object {}: {error}", + object_path.display() + ) + })?; + fs::write(&object_path, "corrupt") + .map_err(|error| format!("corrupt negative object {}: {error}", object_path.display()))?; + if git(&corrupt, &["fsck", "--full", "--strict"]).is_ok() { + return Err("fsck negative fixture was accepted".to_owned()); + } + Ok(()) +} + +fn clone_negative(source: &Path, root: &Path, name: &str) -> Result<std::path::PathBuf, String> { + let target = root.join(name); + git(root, &["clone", path_arg(source)?, path_arg(&target)?])?; + git( + &target, + &["config", "user.name", "Radroots History Fixture"], + )?; + git( + &target, + &["config", "user.email", "history-fixture@radroots.org"], + )?; + Ok(target) +} + +fn git_commit_with_actor( + root: &Path, + subject: &str, + actor: &str, + email: &str, + timestamp: &str, + allow_empty: bool, +) -> Result<(), String> { + let mut command = Command::new("git"); + command + .current_dir(root) + .args(["commit", "--amend", "-m", subject]) + .env("GIT_AUTHOR_NAME", actor) + .env("GIT_AUTHOR_EMAIL", email) + .env("GIT_COMMITTER_NAME", actor) + .env("GIT_COMMITTER_EMAIL", email) + .env("GIT_AUTHOR_DATE", timestamp) + .env("GIT_COMMITTER_DATE", timestamp); + if allow_empty { + command.arg("--allow-empty"); + } + let output = command + .output() + .map_err(|error| format!("run negative commit fixture: {error}"))?; + command_success(output, root, &["commit", "--amend"]).map(|_| ()) +} + +fn git(root: &Path, args: &[&str]) -> Result<(), String> { + let output = Command::new("git") + .args(args) + .current_dir(root) + .output() + .map_err(|error| format!("run git {}: {error}", args.join(" ")))?; + command_success(output, root, args).map(|_| ()) +} + +fn git_stdout(root: &Path, args: &[&str]) -> Result<String, String> { + let output = Command::new("git") + .args(args) + .current_dir(root) + .output() + .map_err(|error| format!("run git {}: {error}", args.join(" ")))?; + String::from_utf8(command_success(output, root, args)?) + .map_err(|error| format!("git {} emitted non-UTF-8 output: {error}", args.join(" "))) +} + +fn command_success( + output: std::process::Output, + root: &Path, + args: &[&str], +) -> Result<Vec<u8>, String> { + if output.status.success() { + return Ok(output.stdout); + } + Err(format!( + "git {} failed in {}: {}", + args.join(" "), + root.display(), + String::from_utf8_lossy(&output.stderr).trim() + )) +} + +fn path_arg(path: &Path) -> Result<&str, String> { + path.to_str() + .ok_or_else(|| format!("path {} is not valid UTF-8", path.display())) +} + +fn sha256_file(path: &Path) -> Result<String, String> { + let mut file = fs::File::open(path) + .map_err(|error| format!("open archive {}: {error}", path.display()))?; + let mut hasher = Sha256::new(); + let mut buffer = [0_u8; 64 * 1024]; + loop { + let read = file + .read(&mut buffer) + .map_err(|error| format!("read archive {}: {error}", path.display()))?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(format!("{:x}", hasher.finalize())) +} + +fn to_unique_set<'a>(values: &'a [String], context: &str) -> Result<BTreeSet<&'a str>, String> { + let set = values.iter().map(String::as_str).collect::<BTreeSet<_>>(); + if set.len() != values.len() || set.iter().any(|value| value.trim().is_empty()) { + return Err(format!("{context} entries must be nonempty and unique")); + } + Ok(set) +} + +fn validate_sha256(value: &str, context: &str) -> Result<(), String> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(format!("{context} must be lowercase 64-hex SHA-256")); + } + Ok(()) +} + +fn validate_artifact_name(value: &str) -> Result<(), String> { + if value.is_empty() + || value.contains('/') + || value.contains('\\') + || value == "." + || value == ".." + || !value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) + { + return Err("archive artifact must be a safe portable file name".to_owned()); + } + Ok(()) +} + fn validate_oid(value: &str, context: &str) -> Result<(), String> { if value.len() != 40 || !value @@ -412,7 +1534,10 @@ mod tests { #[test] fn checked_in_baseline_and_step_map_validate() { - validate(&crate::workspace_root()).expect("checked-in consolidation baseline"); + validate_baseline_contracts(&crate::workspace_root()) + .expect("checked-in consolidation baseline"); + validate_history_contract(&crate::workspace_root(), None) + .expect("checked-in history contract"); } #[test] @@ -452,4 +1577,18 @@ mod tests { gapped.range[0].start = 2; assert!(validate_step_map(&gapped).is_err()); } + + #[test] + fn archive_names_and_digests_are_strict() { + assert!(validate_artifact_name("sdk-0123.bundle").is_ok()); + assert!(validate_artifact_name("../sdk.bundle").is_err()); + assert!(validate_artifact_name("sdk/bundle").is_err()); + assert!(validate_sha256(&"a".repeat(64), "digest").is_ok()); + assert!(validate_sha256(&"A".repeat(64), "digest").is_err()); + } + + #[test] + fn merge_bearing_history_rehearsal_is_green() { + run_history_rehearsal().expect("history rewrite rehearsal"); + } } diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -48,6 +48,8 @@ fn usage() { eprintln!(" cargo xtask contract event-contract-registry-v7 [--write]"); eprintln!(" cargo xtask contract knowledge-manifest [--write]"); eprintln!(" cargo xtask consolidation baseline"); + eprintln!(" cargo xtask consolidation history [--archive-root <absolute-directory>]"); + eprintln!(" cargo xtask consolidation history-rehearsal"); eprintln!(" cargo xtask dto-roots --check|--write"); eprintln!(" cargo xtask generate protocol --check|--write"); eprintln!(" cargo xtask release preflight"); @@ -180,11 +182,7 @@ fn run(args: &[String]) -> Result<(), String> { architecture::validate_dependency_boundaries(&workspace_root()) } Some("contract") => run_contract(&args[1..]), - Some("consolidation") - if args.get(1).map(String::as_str) == Some("baseline") && args.len() == 2 => - { - consolidation::validate(&workspace_root()) - } + Some("consolidation") => consolidation::run(&args[1..], &workspace_root()), Some("coverage") => coverage::run(&args[1..]), Some("dto-roots") => dto_roots::run(&args[1..], &workspace_root()), Some("generate") => generate::run(&args[1..], &workspace_root()),