lib

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

consolidation.rs (86168B)


      1 use std::{
      2     collections::{BTreeMap, BTreeSet},
      3     fs,
      4     io::Read,
      5     path::{Component, Path},
      6     process::Command,
      7 };
      8 
      9 use serde::{Deserialize, Serialize};
     10 use sha2::{Digest, Sha256};
     11 
     12 const BASELINE_RELATIVE: &str = "contracts/consolidation/baseline.v1.toml";
     13 const STEP_MAP_RELATIVE: &str = "contracts/consolidation/handoff_steps.v1.toml";
     14 const HISTORY_RELATIVE: &str = "contracts/consolidation/history.v1.toml";
     15 const BASELINE_ID: &str = "radroots.rust.consolidation.baseline.v1";
     16 const STEP_MAP_ID: &str = "radroots.rust.consolidation.handoff-steps.v1";
     17 const ARCHITECTURE_TARGET: &str = "radroots.crates.release.v2";
     18 const HISTORY_ID: &str = "radroots.rust.consolidation.history.v1";
     19 const PACKAGE_VERSION: &str = "0.1.0-alpha";
     20 const EXPECTED_PUBLIC_PACKAGES: u16 = 19;
     21 const EXPECTED_HANDOFF_STEPS: u16 = 275;
     22 
     23 const RCLD_OWNERS: &[&str] = &[
     24     "rcld-rlc-010",
     25     "rcld-rlc-020",
     26     "rcld-rlc-030",
     27     "rcld-rlc-040",
     28     "rcld-rlc-050",
     29     "rcld-rlc-060",
     30     "rcld-rlc-070",
     31     "rcld-rlc-080",
     32     "rcld-rlc-090",
     33     "rcld-rlc-100",
     34     "rcld-rlc-110",
     35     "rcld-rlc-120",
     36     "rcld-rlc-130",
     37     "rcld-rlc-140",
     38     "rcld-rlc-150",
     39     "rcld-rlc-160",
     40     "rcld-rlc-170",
     41     "rcld-rlc-180",
     42 ];
     43 
     44 #[derive(Debug, Deserialize)]
     45 #[serde(deny_unknown_fields)]
     46 struct Baseline {
     47     schema_version: u16,
     48     baseline_id: String,
     49     architecture_target: String,
     50     captured_date: String,
     51     public_package_version: String,
     52     expected_public_packages: u16,
     53     expected_handoff_steps: u16,
     54     repository: Vec<Repository>,
     55     surface: Vec<Surface>,
     56     consumer: Vec<Consumer>,
     57     additional_source: Vec<AdditionalSource>,
     58 }
     59 
     60 #[derive(Debug, Deserialize)]
     61 #[serde(deny_unknown_fields)]
     62 struct Repository {
     63     id: String,
     64     canonical_url: String,
     65     commit: String,
     66     tree: String,
     67     branch: String,
     68     clean: bool,
     69     origin_synchronized: bool,
     70     worktree_count: u16,
     71     rust_version: String,
     72     resolver: String,
     73     package_version: String,
     74     commands: Vec<String>,
     75 }
     76 
     77 #[derive(Debug, Deserialize)]
     78 #[serde(deny_unknown_fields)]
     79 struct Surface {
     80     repository: String,
     81     path: String,
     82     tree: String,
     83     authority: String,
     84 }
     85 
     86 #[derive(Debug, Deserialize)]
     87 #[serde(deny_unknown_fields)]
     88 struct Consumer {
     89     id: String,
     90     repository: String,
     91     current_source: String,
     92     current_revision: String,
     93     target_source: String,
     94     target_acquisition: String,
     95 }
     96 
     97 #[derive(Debug, Deserialize)]
     98 #[serde(deny_unknown_fields)]
     99 struct AdditionalSource {
    100     id: String,
    101     commit: String,
    102     tree: String,
    103     license: String,
    104     disposition: String,
    105 }
    106 
    107 #[derive(Debug, Deserialize)]
    108 #[serde(deny_unknown_fields)]
    109 struct StepMap {
    110     schema_version: u16,
    111     map_id: String,
    112     source_step_count: u16,
    113     source_sequence: String,
    114     range: Vec<StepRange>,
    115 }
    116 
    117 #[derive(Debug, Deserialize)]
    118 #[serde(deny_unknown_fields)]
    119 struct StepRange {
    120     start: u16,
    121     end: u16,
    122     owners: Vec<String>,
    123     disposition: String,
    124     reason: String,
    125 }
    126 
    127 #[derive(Debug, Deserialize)]
    128 #[serde(deny_unknown_fields)]
    129 struct HistoryContract {
    130     schema_version: u16,
    131     history_id: String,
    132     retention_locator: String,
    133     hash_algorithm: String,
    134     bundle_hash_algorithm: String,
    135     required_commit_map_fields: Vec<String>,
    136     required_verifications: Vec<String>,
    137     dual_source: DualSource,
    138     archive: Vec<Archive>,
    139     path_map: Vec<PathMap>,
    140     #[serde(default)]
    141     import: Vec<ImportRecord>,
    142 }
    143 
    144 #[derive(Debug, Deserialize)]
    145 #[serde(deny_unknown_fields)]
    146 struct DualSource {
    147     state: String,
    148     canonical_owner_before_import: String,
    149     canonical_owner_after_import: String,
    150     emergency_fix_flow: String,
    151     divergence_policy: String,
    152     exit_condition: String,
    153 }
    154 
    155 #[derive(Debug, Deserialize)]
    156 #[serde(deny_unknown_fields)]
    157 struct Archive {
    158     source_id: String,
    159     kind: String,
    160     frozen_commit: String,
    161     artifact: String,
    162     sha256: String,
    163     bytes: u64,
    164     source_commit_count: u64,
    165     source_path_commit_count: u64,
    166     rewritten_commit_count: u64,
    167     topology_support_commit_count: u64,
    168     omitted_empty_commit_count: u64,
    169     source_object_count: u64,
    170     refname: String,
    171     bot_identity_scan: bool,
    172 }
    173 
    174 #[derive(Debug, Deserialize)]
    175 #[serde(deny_unknown_fields)]
    176 struct PathMap {
    177     source_id: String,
    178     source: String,
    179     target: String,
    180     package: String,
    181     license: String,
    182     disposition: String,
    183 }
    184 
    185 #[derive(Debug, Deserialize)]
    186 #[serde(deny_unknown_fields)]
    187 struct ImportRecord {
    188     source_id: String,
    189     frozen_commit: String,
    190     filtered_head: String,
    191     artifact: String,
    192     sha256: String,
    193     bytes: u64,
    194     final_tree_sha256: String,
    195     path_map_sha256: String,
    196 }
    197 
    198 #[derive(Clone, Debug, Eq, PartialEq)]
    199 struct CommitMapEntry {
    200     source: String,
    201     target: Option<String>,
    202 }
    203 
    204 #[derive(Debug, Deserialize, Serialize)]
    205 #[serde(deny_unknown_fields)]
    206 struct ImportCommitMap {
    207     schema_version: u16,
    208     schema: String,
    209     source_id: String,
    210     frozen_commit: String,
    211     filtered_head: String,
    212     source_commit_count: u64,
    213     source_path_commit_count: u64,
    214     rewritten_commit_count: u64,
    215     topology_support_commit_count: u64,
    216     omitted_empty_commit_count: u64,
    217     final_tree_sha256: String,
    218     path_map_sha256: String,
    219     verifications: Vec<String>,
    220     commits: Vec<ImportCommit>,
    221 }
    222 
    223 fn verify_history_import(
    224     workspace_root: &Path,
    225     source_id: &str,
    226     source_root: &Path,
    227     filtered_root: &Path,
    228     mode: &str,
    229 ) -> Result<(), String> {
    230     if !matches!(mode, "check" | "write") {
    231         return Err("import verification mode must be check or write".to_owned());
    232     }
    233     require_real_git_root(source_root, "source root")?;
    234     require_real_git_root(filtered_root, "filtered root")?;
    235     let history = read_toml::<HistoryContract>(workspace_root, HISTORY_RELATIVE)?;
    236     validate_history(&history)?;
    237     let archive = history
    238         .archive
    239         .iter()
    240         .find(|archive| archive.source_id == source_id)
    241         .ok_or_else(|| format!("unknown history source {source_id}"))?;
    242     if !matches!(archive.kind.as_str(), "git_bundle" | "path_fast_export") {
    243         return Err(format!(
    244             "history source {source_id} has an unsupported archive kind"
    245         ));
    246     }
    247     let path_maps = history
    248         .path_map
    249         .iter()
    250         .filter(|path_map| path_map.source_id == source_id)
    251         .collect::<Vec<_>>();
    252     if path_maps.is_empty() {
    253         return Err(format!("history source {source_id} has no path map"));
    254     }
    255     let source_head = git_stdout(source_root, &["rev-parse", "master"])?
    256         .trim()
    257         .to_owned();
    258     validate_oid(&source_head, "source head")?;
    259     if archive.kind == "git_bundle" && source_head != archive.frozen_commit {
    260         return Err(format!(
    261             "history source {source_id} is not at its frozen commit"
    262         ));
    263     }
    264     git(source_root, &["fsck", "--full", "--strict"])?;
    265     git(filtered_root, &["fsck", "--full", "--strict"])?;
    266 
    267     let commit_map = parse_commit_map(&filtered_root.join(".git/filter-repo/commit-map"))?;
    268     let source_commits = git_stdout(source_root, &["rev-list", "master"])?
    269         .lines()
    270         .map(str::to_owned)
    271         .collect::<BTreeSet<_>>();
    272     if source_commits.len() as u64 != archive.source_commit_count
    273         || commit_map.len() != source_commits.len()
    274         || commit_map
    275             .iter()
    276             .any(|entry| !source_commits.contains(&entry.source))
    277     {
    278         return Err("source history and filter-repo commit map differ".to_owned());
    279     }
    280 
    281     let source_path_args = path_maps
    282         .iter()
    283         .map(|path_map| path_map.source.as_str())
    284         .collect::<Vec<_>>();
    285     let source_path_commits = rev_list_paths(source_root, &source_path_args)?;
    286     let by_source = commit_map
    287         .iter()
    288         .map(|entry| (entry.source.as_str(), entry.target.as_deref()))
    289         .collect::<BTreeMap<_, _>>();
    290     let filtered_head = git_stdout(filtered_root, &["rev-parse", "master"])?
    291         .trim()
    292         .to_owned();
    293     validate_oid(&filtered_head, "filtered head")?;
    294     let expected_filtered_head = by_source
    295         .get(source_head.as_str())
    296         .and_then(|target| *target)
    297         .ok_or_else(|| "frozen source head was omitted by filtering".to_owned())?;
    298     if filtered_head != expected_filtered_head {
    299         return Err("filtered master does not map from the frozen source head".to_owned());
    300     }
    301 
    302     verify_import_path_coverage(
    303         source_root,
    304         filtered_root,
    305         &source_head,
    306         &path_maps,
    307         &filtered_head,
    308     )?;
    309     verify_import_final_tree(
    310         source_root,
    311         filtered_root,
    312         &source_head,
    313         &filtered_head,
    314         &path_maps,
    315     )?;
    316     verify_import_context(filtered_root, &filtered_head, &path_maps)?;
    317     verify_commit_identities(filtered_root, &filtered_head)?;
    318     verify_import_follow_history(source_root, filtered_root, &path_maps, &by_source)?;
    319 
    320     let mut commits = Vec::with_capacity(commit_map.len());
    321     let mut rewritten = 0_u64;
    322     let mut topology = 0_u64;
    323     let mut omitted = 0_u64;
    324     for entry in &commit_map {
    325         let source_parents = commit_parents(source_root, &entry.source)?;
    326         let source_paths = changed_import_paths(source_root, &entry.source, &source_path_args)?;
    327         let in_path_history = source_path_commits.contains(&entry.source);
    328         let source_metadata = import_commit_metadata(source_root, &entry.source)?;
    329         let source_patch = normalized_import_patch(source_root, &entry.source, &path_maps, true)?;
    330         let (target_parents, target_metadata, disposition) = match entry.target.as_deref() {
    331             Some(target_commit) => {
    332                 rewritten += 1;
    333                 let mut expected_parents = Vec::new();
    334                 for parent in &source_parents {
    335                     collect_effective_parents(
    336                         source_root,
    337                         parent,
    338                         &by_source,
    339                         &mut expected_parents,
    340                     )?;
    341                 }
    342                 deduplicate(&mut expected_parents);
    343                 prune_ancestor_parents(filtered_root, &mut expected_parents)?;
    344                 let target_parents = commit_parents(filtered_root, target_commit)?;
    345                 if target_parents != expected_parents {
    346                     return Err(format!(
    347                         "mapped parent closure drifted for {}",
    348                         entry.source
    349                     ));
    350                 }
    351                 let target_metadata = import_commit_metadata(filtered_root, target_commit)?;
    352                 verify_import_metadata(&entry.source, &source_metadata, &target_metadata)?;
    353                 let target_patch =
    354                     normalized_import_patch(filtered_root, target_commit, &path_maps, false)?;
    355                 let source_tree =
    356                     normalized_import_tree(source_root, &entry.source, &path_maps, true)?;
    357                 let target_tree =
    358                     normalized_import_tree(filtered_root, target_commit, &path_maps, false)?;
    359                 if source_tree != target_tree {
    360                     return Err(format!("normalized tree drifted for {}", entry.source));
    361                 }
    362                 let mut direct_target_parents = source_parents
    363                     .iter()
    364                     .filter_map(|parent| by_source.get(parent.as_str()).copied().flatten())
    365                     .map(str::to_owned)
    366                     .collect::<Vec<_>>();
    367                 let every_parent_retained = direct_target_parents.len() == source_parents.len();
    368                 deduplicate(&mut direct_target_parents);
    369                 prune_ancestor_parents(filtered_root, &mut direct_target_parents)?;
    370                 if every_parent_retained
    371                     && direct_target_parents == target_parents
    372                     && source_patch != target_patch
    373                 {
    374                     return Err(format!("normalized patch drifted for {}", entry.source));
    375                 }
    376                 let disposition = if in_path_history {
    377                     "retained"
    378                 } else {
    379                     topology += 1;
    380                     "topology_support"
    381                 };
    382                 (target_parents, Some(target_metadata), disposition)
    383             }
    384             None => {
    385                 let disposition = if in_path_history {
    386                     omitted += 1;
    387                     "omitted_empty"
    388                 } else {
    389                     "out_of_scope"
    390                 };
    391                 (Vec::new(), None, disposition)
    392             }
    393         };
    394         commits.push(ImportCommit {
    395             source_commit: if archive.kind == "path_fast_export" {
    396                 archive.frozen_commit.clone()
    397             } else {
    398                 entry.source.clone()
    399             },
    400             target_commit: entry.target.clone(),
    401             source_parents,
    402             target_parents,
    403             source_subject: source_metadata.subject,
    404             target_subject: target_metadata
    405                 .as_ref()
    406                 .map(|metadata| metadata.subject.clone()),
    407             source_author: source_metadata.author,
    408             target_author: target_metadata
    409                 .as_ref()
    410                 .map(|metadata| metadata.author.clone()),
    411             source_author_time: source_metadata.author_time,
    412             target_author_time: target_metadata
    413                 .as_ref()
    414                 .map(|metadata| metadata.author_time.clone()),
    415             source_committer_time: source_metadata.committer_time,
    416             target_committer_time: target_metadata
    417                 .as_ref()
    418                 .map(|metadata| metadata.committer_time.clone()),
    419             source_paths,
    420             normalized_patch_sha256: sha256_bytes(source_patch.as_bytes()),
    421             empty_commit_disposition: disposition.to_owned(),
    422         });
    423     }
    424     if source_path_commits.len() as u64 != archive.source_path_commit_count
    425         || rewritten != archive.rewritten_commit_count
    426         || topology != archive.topology_support_commit_count
    427         || omitted != archive.omitted_empty_commit_count
    428     {
    429         return Err(format!(
    430             "history counts drifted: path={}, rewritten={rewritten}, topology={topology}, omitted={omitted}",
    431             source_path_commits.len()
    432         ));
    433     }
    434 
    435     let path_map_bytes = path_maps
    436         .iter()
    437         .map(|path_map| {
    438             format!(
    439                 "{}\0{}\0{}\n",
    440                 path_map.source, path_map.target, path_map.license
    441             )
    442         })
    443         .collect::<String>();
    444     let final_tree = normalized_import_tree(filtered_root, &filtered_head, &path_maps, false)?;
    445     let artifact = ImportCommitMap {
    446         schema_version: 1,
    447         schema: "radroots.history-import.commit-map.v1".to_owned(),
    448         source_id: source_id.to_owned(),
    449         frozen_commit: archive.frozen_commit.clone(),
    450         filtered_head,
    451         source_commit_count: archive.source_commit_count,
    452         source_path_commit_count: archive.source_path_commit_count,
    453         rewritten_commit_count: archive.rewritten_commit_count,
    454         topology_support_commit_count: archive.topology_support_commit_count,
    455         omitted_empty_commit_count: archive.omitted_empty_commit_count,
    456         final_tree_sha256: sha256_bytes(final_tree.as_bytes()),
    457         path_map_sha256: sha256_bytes(path_map_bytes.as_bytes()),
    458         verifications: history.required_verifications.clone(),
    459         commits,
    460     };
    461     let mut bytes = serde_json::to_vec_pretty(&artifact)
    462         .map_err(|error| format!("serialize history import artifact: {error}"))?;
    463     bytes.push(b'\n');
    464     let output = workspace_root.join(format!(
    465         "contracts/consolidation/imports/{source_id}.commit-map.v1.json"
    466     ));
    467     match mode {
    468         "write" => crate::build_control::atomic_write(&output, &bytes),
    469         "check" => {
    470             let current =
    471                 fs::read(&output).map_err(|error| format!("read {}: {error}", output.display()))?;
    472             if current == bytes {
    473                 Ok(())
    474             } else {
    475                 Err(format!(
    476                     "history import artifact {} is stale",
    477                     output.display()
    478                 ))
    479             }
    480         }
    481         _ => unreachable!("mode validated"),
    482     }
    483 }
    484 
    485 fn prune_ancestor_parents(root: &Path, parents: &mut Vec<String>) -> Result<(), String> {
    486     let original = parents.clone();
    487     let mut keep = Vec::new();
    488     for parent in &original {
    489         let mut redundant = false;
    490         for candidate in &original {
    491             if parent == candidate {
    492                 continue;
    493             }
    494             let status = Command::new("git")
    495                 .args(["merge-base", "--is-ancestor", parent, candidate])
    496                 .current_dir(root)
    497                 .status()
    498                 .map_err(|error| format!("run git merge-base --is-ancestor: {error}"))?;
    499             match status.code() {
    500                 Some(0) => {
    501                     redundant = true;
    502                     break;
    503                 }
    504                 Some(1) => {}
    505                 _ => return Err("git merge-base --is-ancestor failed".to_owned()),
    506             }
    507         }
    508         if !redundant {
    509             keep.push(parent.clone());
    510         }
    511     }
    512     *parents = keep;
    513     Ok(())
    514 }
    515 
    516 #[derive(Debug)]
    517 struct ImportMetadata {
    518     author: String,
    519     author_time: String,
    520     committer_time: String,
    521     subject: String,
    522 }
    523 
    524 fn require_real_git_root(path: &Path, label: &str) -> Result<(), String> {
    525     if !path.is_absolute() {
    526         return Err(format!("{label} must be absolute"));
    527     }
    528     let metadata = fs::symlink_metadata(path)
    529         .map_err(|error| format!("inspect {label} {}: {error}", path.display()))?;
    530     if metadata.file_type().is_symlink() || !metadata.is_dir() {
    531         return Err(format!("{label} must be a real directory"));
    532     }
    533     let git_dir = git_stdout(path, &["rev-parse", "--absolute-git-dir"])?;
    534     let git_dir = Path::new(git_dir.trim());
    535     if !git_dir.is_absolute() {
    536         return Err(format!("{label} Git directory must be absolute"));
    537     }
    538     let metadata = fs::symlink_metadata(git_dir)
    539         .map_err(|error| format!("inspect {label} Git directory: {error}"))?;
    540     if metadata.file_type().is_symlink() || !metadata.is_dir() {
    541         return Err(format!("{label} must contain a real Git directory"));
    542     }
    543     Ok(())
    544 }
    545 
    546 fn rev_list_paths(root: &Path, paths: &[&str]) -> Result<BTreeSet<String>, String> {
    547     let mut args = vec!["rev-list", "--full-history", "master", "--"];
    548     args.extend_from_slice(paths);
    549     Ok(git_stdout(root, &args)?
    550         .lines()
    551         .map(str::to_owned)
    552         .collect())
    553 }
    554 
    555 fn changed_import_paths(root: &Path, commit: &str, paths: &[&str]) -> Result<Vec<String>, String> {
    556     let mut args = vec![
    557         "diff-tree",
    558         "--root",
    559         "-m",
    560         "-r",
    561         "--no-commit-id",
    562         "--name-only",
    563         commit,
    564         "--",
    565     ];
    566     args.extend_from_slice(paths);
    567     let mut changed = git_stdout(root, &args)?
    568         .lines()
    569         .map(str::to_owned)
    570         .collect::<Vec<_>>();
    571     changed.sort();
    572     changed.dedup();
    573     Ok(changed)
    574 }
    575 
    576 fn import_commit_metadata(root: &Path, commit: &str) -> Result<ImportMetadata, String> {
    577     let raw = git_stdout(
    578         root,
    579         &[
    580             "show",
    581             "-s",
    582             "--format=%an%x00%ae%x00%aI%x00%cI%x00%s",
    583             commit,
    584         ],
    585     )?;
    586     let fields = raw.trim_end().split('\0').collect::<Vec<_>>();
    587     if fields.len() != 5 {
    588         return Err(format!("commit metadata cardinality drifted for {commit}"));
    589     }
    590     Ok(ImportMetadata {
    591         author: format!("{} <{}>", fields[0], fields[1]),
    592         author_time: fields[2].to_owned(),
    593         committer_time: fields[3].to_owned(),
    594         subject: fields[4].to_owned(),
    595     })
    596 }
    597 
    598 fn verify_import_metadata(
    599     source_commit: &str,
    600     source: &ImportMetadata,
    601     target: &ImportMetadata,
    602 ) -> Result<(), String> {
    603     if source.author != target.author
    604         || source.author_time != target.author_time
    605         || source.committer_time != target.committer_time
    606         || !message_is_preserved(&source.subject, &target.subject)
    607     {
    608         return Err(format!(
    609             "attribution or message drifted for {source_commit}"
    610         ));
    611     }
    612     Ok(())
    613 }
    614 
    615 fn normalized_import_patch(
    616     root: &Path,
    617     commit: &str,
    618     path_maps: &[&PathMap],
    619     source_side: bool,
    620 ) -> Result<String, String> {
    621     let mut normalized = String::new();
    622     for path_map in path_maps {
    623         let path = if source_side {
    624             path_map.source.as_str()
    625         } else {
    626             path_map.target.as_str()
    627         };
    628         let patch = git_stdout(
    629             root,
    630             &[
    631                 "-c",
    632                 "core.quotePath=false",
    633                 "diff-tree",
    634                 "--root",
    635                 "-m",
    636                 "-r",
    637                 "--binary",
    638                 "--full-index",
    639                 "--no-commit-id",
    640                 commit,
    641                 "--",
    642                 path,
    643             ],
    644         )?;
    645         normalized.push_str(&normalize_import_patch_paths(patch, path_maps, source_side));
    646     }
    647     Ok(normalized)
    648 }
    649 
    650 fn normalize_import_patch_paths(
    651     value: String,
    652     path_maps: &[&PathMap],
    653     source_side: bool,
    654 ) -> String {
    655     value
    656         .split_inclusive('\n')
    657         .map(|line| {
    658             if line.starts_with("diff --git ")
    659                 || line.starts_with("--- ")
    660                 || line.starts_with("+++ ")
    661                 || line.starts_with("rename from ")
    662                 || line.starts_with("rename to ")
    663                 || line.starts_with("copy from ")
    664                 || line.starts_with("copy to ")
    665                 || line.starts_with("Binary files ")
    666             {
    667                 normalize_import_paths(line.to_owned(), path_maps, source_side)
    668             } else {
    669                 line.to_owned()
    670             }
    671         })
    672         .collect()
    673 }
    674 
    675 fn normalize_import_paths(mut value: String, path_maps: &[&PathMap], source_side: bool) -> String {
    676     let mut replacements = path_maps
    677         .iter()
    678         .enumerate()
    679         .map(|(index, path_map)| {
    680             let path = if source_side {
    681                 path_map.source.as_str()
    682             } else {
    683                 path_map.target.as_str()
    684             };
    685             (path, format!("__IMPORT__/{index:02}"))
    686         })
    687         .collect::<Vec<_>>();
    688     replacements.sort_by_key(|(path, _)| std::cmp::Reverse(path.len()));
    689     for (path, replacement) in replacements {
    690         value = value.replace(path, &replacement);
    691     }
    692     value
    693 }
    694 
    695 fn normalized_import_tree(
    696     root: &Path,
    697     commit: &str,
    698     path_maps: &[&PathMap],
    699     source_side: bool,
    700 ) -> Result<String, String> {
    701     let mut args = vec!["ls-tree", "-r", "--full-tree", commit, "--"];
    702     args.extend(path_maps.iter().map(|path_map| {
    703         if source_side {
    704             path_map.source.as_str()
    705         } else {
    706             path_map.target.as_str()
    707         }
    708     }));
    709     let normalized = normalize_import_paths(git_stdout(root, &args)?, path_maps, source_side);
    710     let mut lines = normalized.lines().collect::<Vec<_>>();
    711     lines.sort_unstable();
    712     Ok(format!("{}\n", lines.join("\n")))
    713 }
    714 
    715 fn verify_import_final_tree(
    716     source_root: &Path,
    717     filtered_root: &Path,
    718     source_commit: &str,
    719     target_commit: &str,
    720     path_maps: &[&PathMap],
    721 ) -> Result<(), String> {
    722     let source = normalized_import_tree(source_root, source_commit, path_maps, true)?;
    723     let target = normalized_import_tree(filtered_root, target_commit, path_maps, false)?;
    724     if source == target {
    725         Ok(())
    726     } else {
    727         Err("filtered import final tree drifted".to_owned())
    728     }
    729 }
    730 
    731 fn verify_import_path_coverage(
    732     source_root: &Path,
    733     filtered_root: &Path,
    734     source_commit: &str,
    735     path_maps: &[&PathMap],
    736     filtered_head: &str,
    737 ) -> Result<(), String> {
    738     for path_map in path_maps {
    739         git(
    740             source_root,
    741             &[
    742                 "cat-file",
    743                 "-e",
    744                 &format!("{source_commit}:{}", path_map.source),
    745             ],
    746         )?;
    747         git(
    748             filtered_root,
    749             &[
    750                 "cat-file",
    751                 "-e",
    752                 &format!("{filtered_head}:{}", path_map.target),
    753             ],
    754         )?;
    755     }
    756     Ok(())
    757 }
    758 
    759 fn verify_import_context(
    760     filtered_root: &Path,
    761     filtered_head: &str,
    762     path_maps: &[&PathMap],
    763 ) -> Result<(), String> {
    764     let paths = git_stdout(
    765         filtered_root,
    766         &["ls-tree", "-r", "--name-only", filtered_head],
    767     )?;
    768     for path in paths.lines() {
    769         let lower = path.to_ascii_lowercase();
    770         if !path_maps.iter().any(|path_map| {
    771             path == path_map.target || path.starts_with(&format!("{}/", path_map.target))
    772         }) || path.split('/').any(|segment| segment == ".github")
    773             || matches!(path.rsplit('/').next(), Some("AGENTS.md" | "CLAUDE.md"))
    774             || lower.ends_with(".pem")
    775             || lower.ends_with(".key")
    776             || lower.ends_with("/.env")
    777         {
    778             return Err(format!("context firewall rejected {path}"));
    779         }
    780         let contents = git_bytes(filtered_root, &["show", &format!("{filtered_head}:{path}")])?;
    781         let text = String::from_utf8_lossy(&contents);
    782         let lower_contents = text.to_ascii_lowercase();
    783         let license_metadata_stripped = lower_contents
    784             .replace("license = \"gpl-3.0-only\"", "")
    785             .replace("license = \"gpl-3.0-or-later\"", "")
    786             .replace("license = \"mit or apache-2.0\"", "")
    787             .replace("license = \"mpl-2.0\"", "");
    788         if text.contains("PRIVATE KEY-----")
    789             || text.contains("ghp_")
    790             || lower_contents.contains("github-actions[bot]")
    791             || license_metadata_stripped.contains("gpl-3.0")
    792         {
    793             return Err(format!(
    794                 "secret, bot, or license content rejected in {path}"
    795             ));
    796         }
    797     }
    798     Ok(())
    799 }
    800 
    801 fn verify_import_follow_history(
    802     source_root: &Path,
    803     filtered_root: &Path,
    804     path_maps: &[&PathMap],
    805     by_source: &BTreeMap<&str, Option<&str>>,
    806 ) -> Result<(), String> {
    807     let mapped_targets = by_source
    808         .values()
    809         .filter_map(|target| *target)
    810         .collect::<BTreeSet<_>>();
    811     for path_map in path_maps {
    812         let target_paths = git_stdout(
    813             filtered_root,
    814             &[
    815                 "ls-tree",
    816                 "-r",
    817                 "--name-only",
    818                 "master",
    819                 "--",
    820                 &path_map.target,
    821             ],
    822         )?;
    823         for target_path in target_paths.lines() {
    824             let suffix = target_path
    825                 .strip_prefix(&path_map.target)
    826                 .ok_or_else(|| "target path escaped its import root".to_owned())?;
    827             let source_path = format!("{}{}", path_map.source, suffix);
    828             let source_log_has_mapped_commit = git_stdout(
    829                 source_root,
    830                 &["log", "--follow", "--format=%H", "--", &source_path],
    831             )?
    832             .lines()
    833             .any(|commit| by_source.get(commit).copied().flatten().is_some());
    834             let target_log = git_stdout(
    835                 filtered_root,
    836                 &["log", "--follow", "--format=%H", "--", target_path],
    837             )?
    838             .lines()
    839             .map(str::to_owned)
    840             .collect::<Vec<_>>();
    841             if !source_log_has_mapped_commit
    842                 || target_log.is_empty()
    843                 || target_log
    844                     .iter()
    845                     .any(|commit| !mapped_targets.contains(commit.as_str()))
    846             {
    847                 return Err(format!("git log --follow drifted for {source_path}"));
    848             }
    849         }
    850     }
    851     Ok(())
    852 }
    853 
    854 fn sha256_bytes(bytes: &[u8]) -> String {
    855     let mut hasher = Sha256::new();
    856     hasher.update(bytes);
    857     format!("{:x}", hasher.finalize())
    858 }
    859 
    860 #[derive(Debug, Deserialize, Serialize)]
    861 #[serde(deny_unknown_fields)]
    862 struct ImportCommit {
    863     source_commit: String,
    864     target_commit: Option<String>,
    865     source_parents: Vec<String>,
    866     target_parents: Vec<String>,
    867     source_subject: String,
    868     target_subject: Option<String>,
    869     source_author: String,
    870     target_author: Option<String>,
    871     source_author_time: String,
    872     target_author_time: Option<String>,
    873     source_committer_time: String,
    874     target_committer_time: Option<String>,
    875     source_paths: Vec<String>,
    876     normalized_patch_sha256: String,
    877     empty_commit_disposition: String,
    878 }
    879 
    880 pub fn run(args: &[String], workspace_root: &Path) -> Result<(), String> {
    881     match args {
    882         [command] if command == "baseline" => validate_baseline_contracts(workspace_root),
    883         [command] if command == "history" => validate_history_contract(workspace_root, None),
    884         [command] if command == "history-rehearsal" => run_history_rehearsal(),
    885         [command, source_flag, source_id, source_root_flag, source_root, filtered_root_flag, filtered_root, mode_flag, mode]
    886             if command == "import-verify"
    887                 && source_flag == "--source"
    888                 && source_root_flag == "--source-root"
    889                 && filtered_root_flag == "--filtered-root"
    890                 && mode_flag == "--mode" =>
    891         {
    892             verify_history_import(
    893                 workspace_root,
    894                 source_id,
    895                 Path::new(source_root),
    896                 Path::new(filtered_root),
    897                 mode,
    898             )
    899         }
    900         [command, flag, archive_root] if command == "history" && flag == "--archive-root" => {
    901             validate_history_contract(workspace_root, Some(Path::new(archive_root)))
    902         }
    903         _ => Err(
    904             "consolidation accepts baseline, history [--archive-root <absolute-directory>], history-rehearsal, or import-verify --source <id> --source-root <absolute-directory> --filtered-root <absolute-directory> --mode <check|write>"
    905                 .to_owned(),
    906         ),
    907     }
    908 }
    909 
    910 pub fn validate_baseline_contracts(workspace_root: &Path) -> Result<(), String> {
    911     let baseline = read_toml::<Baseline>(workspace_root, BASELINE_RELATIVE)?;
    912     let step_map = read_toml::<StepMap>(workspace_root, STEP_MAP_RELATIVE)?;
    913     validate_baseline(&baseline)?;
    914     validate_step_map(&step_map)
    915 }
    916 
    917 fn validate_history_contract(
    918     workspace_root: &Path,
    919     archive_root: Option<&Path>,
    920 ) -> Result<(), String> {
    921     let history = read_toml::<HistoryContract>(workspace_root, HISTORY_RELATIVE)?;
    922     validate_history(&history)?;
    923     validate_retired_import_targets(workspace_root, &history.path_map)?;
    924     validate_import_records(workspace_root, &history)?;
    925     if let Some(archive_root) = archive_root {
    926         validate_archives(&history, archive_root)?;
    927     }
    928     Ok(())
    929 }
    930 
    931 fn validate_retired_import_targets(
    932     workspace_root: &Path,
    933     path_maps: &[PathMap],
    934 ) -> Result<(), String> {
    935     for path_map in path_maps
    936         .iter()
    937         .filter(|path_map| path_map.disposition == "import_unique_behavior_then_retire")
    938     {
    939         let target = workspace_root.join(&path_map.target);
    940         match fs::symlink_metadata(&target) {
    941             Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
    942             Err(error) => {
    943                 return Err(format!(
    944                     "inspect retired import target {}: {error}",
    945                     path_map.target
    946                 ));
    947             }
    948             Ok(_) => {
    949                 return Err(format!(
    950                     "retired import target remains in the active tree: {}",
    951                     path_map.target
    952                 ));
    953             }
    954         }
    955     }
    956     Ok(())
    957 }
    958 
    959 fn validate_import_records(workspace_root: &Path, history: &HistoryContract) -> Result<(), String> {
    960     let mut sources = BTreeSet::new();
    961     for record in &history.import {
    962         if !sources.insert(record.source_id.as_str()) {
    963             return Err(format!(
    964                 "duplicate history import source {}",
    965                 record.source_id
    966             ));
    967         }
    968         let archive = history
    969             .archive
    970             .iter()
    971             .find(|archive| archive.source_id == record.source_id)
    972             .ok_or_else(|| format!("unknown history import source {}", record.source_id))?;
    973         if record.frozen_commit != archive.frozen_commit {
    974             return Err(format!(
    975                 "history import {} frozen commit drifted",
    976                 record.source_id
    977             ));
    978         }
    979         validate_oid(&record.filtered_head, "filtered import head")?;
    980         validate_artifact_name(&record.artifact)?;
    981         validate_sha256(&record.sha256, "commit-map artifact digest")?;
    982         validate_sha256(&record.final_tree_sha256, "import final-tree digest")?;
    983         validate_sha256(&record.path_map_sha256, "import path-map digest")?;
    984         let path = workspace_root
    985             .join("contracts/consolidation/imports")
    986             .join(&record.artifact);
    987         let metadata = fs::symlink_metadata(&path)
    988             .map_err(|error| format!("inspect {}: {error}", path.display()))?;
    989         if metadata.file_type().is_symlink()
    990             || !metadata.is_file()
    991             || metadata.len() != record.bytes
    992         {
    993             return Err(format!(
    994                 "history import artifact {} metadata drifted",
    995                 path.display()
    996             ));
    997         }
    998         let bytes = fs::read(&path)
    999             .map_err(|error| format!("read history import artifact {}: {error}", path.display()))?;
   1000         if sha256_bytes(&bytes) != record.sha256 {
   1001             return Err(format!(
   1002                 "history import artifact {} digest drifted",
   1003                 path.display()
   1004             ));
   1005         }
   1006         let artifact = serde_json::from_slice::<ImportCommitMap>(&bytes).map_err(|error| {
   1007             format!("parse history import artifact {}: {error}", path.display())
   1008         })?;
   1009         if artifact.schema_version != 1
   1010             || artifact.schema != "radroots.history-import.commit-map.v1"
   1011             || artifact.source_id != record.source_id
   1012             || artifact.frozen_commit != record.frozen_commit
   1013             || artifact.filtered_head != record.filtered_head
   1014             || artifact.final_tree_sha256 != record.final_tree_sha256
   1015             || artifact.path_map_sha256 != record.path_map_sha256
   1016             || artifact.source_commit_count != archive.source_commit_count
   1017             || artifact.source_path_commit_count != archive.source_path_commit_count
   1018             || artifact.rewritten_commit_count != archive.rewritten_commit_count
   1019             || artifact.topology_support_commit_count != archive.topology_support_commit_count
   1020             || artifact.omitted_empty_commit_count != archive.omitted_empty_commit_count
   1021             || artifact.commits.len() as u64 != archive.source_commit_count
   1022             || to_unique_set(&artifact.verifications, "import verification")?
   1023                 != to_unique_set(&history.required_verifications, "required verification")?
   1024         {
   1025             return Err(format!(
   1026                 "history import artifact {} contract drifted",
   1027                 record.source_id
   1028             ));
   1029         }
   1030     }
   1031     Ok(())
   1032 }
   1033 
   1034 fn read_toml<T: for<'de> Deserialize<'de>>(
   1035     workspace_root: &Path,
   1036     relative: &str,
   1037 ) -> Result<T, String> {
   1038     let path = workspace_root.join(relative);
   1039     let raw =
   1040         fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?;
   1041     toml::from_str(&raw).map_err(|error| format!("parse {}: {error}", path.display()))
   1042 }
   1043 
   1044 fn validate_baseline(baseline: &Baseline) -> Result<(), String> {
   1045     if baseline.schema_version != 1
   1046         || baseline.baseline_id != BASELINE_ID
   1047         || baseline.architecture_target != ARCHITECTURE_TARGET
   1048         || baseline.public_package_version != PACKAGE_VERSION
   1049         || baseline.expected_public_packages != EXPECTED_PUBLIC_PACKAGES
   1050         || baseline.expected_handoff_steps != EXPECTED_HANDOFF_STEPS
   1051     {
   1052         return Err("consolidation baseline identity or cardinality drifted".to_owned());
   1053     }
   1054     validate_date(&baseline.captured_date)?;
   1055 
   1056     let expected_repositories = BTreeSet::from(["app_rt", "lib", "sdk", "studio_app"]);
   1057     let mut repositories = BTreeMap::new();
   1058     for repository in &baseline.repository {
   1059         if repositories
   1060             .insert(repository.id.as_str(), repository)
   1061             .is_some()
   1062         {
   1063             return Err(format!("duplicate repository id {}", repository.id));
   1064         }
   1065         validate_identifier(&repository.id, "repository id")?;
   1066         if repository.canonical_url != format!("https://github.com/radrootslabs/{}", repository.id)
   1067         {
   1068             return Err(format!(
   1069                 "repository {} has a noncanonical URL",
   1070                 repository.id
   1071             ));
   1072         }
   1073         validate_oid(&repository.commit, "repository commit")?;
   1074         validate_oid(&repository.tree, "repository tree")?;
   1075         if repository.branch != "master"
   1076             || !repository.clean
   1077             || !repository.origin_synchronized
   1078             || repository.worktree_count != 1
   1079             || repository.rust_version != "1.97.1"
   1080             || !matches!(repository.resolver.as_str(), "2" | "3")
   1081             || repository.commands.is_empty()
   1082         {
   1083             return Err(format!(
   1084                 "repository {} baseline is not frozen",
   1085                 repository.id
   1086             ));
   1087         }
   1088         if repository
   1089             .commands
   1090             .iter()
   1091             .any(|command| command.trim().is_empty())
   1092         {
   1093             return Err(format!("repository {} has an empty command", repository.id));
   1094         }
   1095     }
   1096     if repositories.keys().copied().collect::<BTreeSet<_>>() != expected_repositories {
   1097         return Err(
   1098             "consolidation baseline must contain exactly lib, sdk, app_rt, and studio_app"
   1099                 .to_owned(),
   1100         );
   1101     }
   1102     if repositories["lib"].resolver != "3"
   1103         || repositories["sdk"].resolver != "3"
   1104         || repositories["studio_app"].resolver != "3"
   1105         || repositories["app_rt"].resolver != "2"
   1106     {
   1107         return Err("reviewed resolver baseline drifted".to_owned());
   1108     }
   1109     if repositories["lib"].package_version != PACKAGE_VERSION
   1110         || repositories["sdk"].package_version != PACKAGE_VERSION
   1111         || repositories["studio_app"].package_version != PACKAGE_VERSION
   1112         || repositories["app_rt"].package_version != "0.1.0-alpha.1"
   1113     {
   1114         return Err("reviewed package version baseline drifted".to_owned());
   1115     }
   1116 
   1117     let mut surfaces = BTreeSet::new();
   1118     let mut authorities = BTreeSet::new();
   1119     for surface in &baseline.surface {
   1120         if !repositories.contains_key(surface.repository.as_str()) {
   1121             return Err(format!("unknown surface repository {}", surface.repository));
   1122         }
   1123         validate_relative_path(&surface.path)?;
   1124         validate_oid(&surface.tree, "surface tree")?;
   1125         validate_identifier(&surface.authority, "surface authority")?;
   1126         if !surfaces.insert((surface.repository.as_str(), surface.path.as_str())) {
   1127             return Err(format!(
   1128                 "duplicate surface {}/{}",
   1129                 surface.repository, surface.path
   1130             ));
   1131         }
   1132         if !authorities.insert(surface.authority.as_str()) {
   1133             return Err(format!("duplicate surface authority {}", surface.authority));
   1134         }
   1135     }
   1136     for repository in expected_repositories {
   1137         if !surfaces
   1138             .iter()
   1139             .any(|(candidate, _)| *candidate == repository)
   1140         {
   1141             return Err(format!(
   1142                 "repository {repository} has no compatibility surface"
   1143             ));
   1144         }
   1145     }
   1146 
   1147     let expected_consumers = BTreeSet::from([
   1148         "cli",
   1149         "integration_parent",
   1150         "ios_app",
   1151         "mobile_runtime",
   1152         "myc",
   1153         "radrootsd",
   1154         "rhi",
   1155         "sdk_product",
   1156         "studio_product",
   1157         "event_indexer",
   1158     ]);
   1159     let mut consumers = BTreeSet::new();
   1160     for consumer in &baseline.consumer {
   1161         validate_identifier(&consumer.id, "consumer id")?;
   1162         if !consumers.insert(consumer.id.as_str()) {
   1163             return Err(format!("duplicate consumer id {}", consumer.id));
   1164         }
   1165         if consumer.repository.trim().is_empty()
   1166             || consumer.current_source.trim().is_empty()
   1167             || consumer.target_source != "lib"
   1168         {
   1169             return Err(format!("consumer {} has incomplete ownership", consumer.id));
   1170         }
   1171         validate_oid(&consumer.current_revision, "consumer revision")?;
   1172         if !matches!(
   1173             consumer.target_acquisition.as_str(),
   1174             "exact_git_rev" | "exact_registered_gitlink"
   1175         ) {
   1176             return Err(format!(
   1177                 "consumer {} has invalid target acquisition",
   1178                 consumer.id
   1179             ));
   1180         }
   1181     }
   1182     if consumers != expected_consumers {
   1183         return Err("consumer census is incomplete".to_owned());
   1184     }
   1185 
   1186     if baseline.additional_source.len() != 1 {
   1187         return Err("exactly one additional Studio source is required".to_owned());
   1188     }
   1189     let additional = &baseline.additional_source[0];
   1190     validate_identifier(&additional.id, "additional source id")?;
   1191     validate_oid(&additional.commit, "additional source commit")?;
   1192     validate_oid(&additional.tree, "additional source tree")?;
   1193     if additional.id != "studio_mpl_legacy_core"
   1194         || additional.license != "MPL-2.0"
   1195         || additional.disposition != "import_unique_behavior_then_zero_logic_capsule"
   1196     {
   1197         return Err("additional Studio source disposition drifted".to_owned());
   1198     }
   1199     Ok(())
   1200 }
   1201 
   1202 fn validate_step_map(step_map: &StepMap) -> Result<(), String> {
   1203     if step_map.schema_version != 1
   1204         || step_map.map_id != STEP_MAP_ID
   1205         || step_map.source_step_count != EXPECTED_HANDOFF_STEPS
   1206         || !step_map
   1207             .source_sequence
   1208             .ends_with("implementation/COMMIT_SEQUENCE.md")
   1209     {
   1210         return Err("handoff step map identity drifted".to_owned());
   1211     }
   1212     let allowed_owners = RCLD_OWNERS.iter().copied().collect::<BTreeSet<_>>();
   1213     let allowed_dispositions = BTreeSet::from(["already_satisfied", "execute", "reassigned"]);
   1214     let mut next = 1_u16;
   1215     for range in &step_map.range {
   1216         if range.start != next || range.end < range.start || range.end > EXPECTED_HANDOFF_STEPS {
   1217             return Err(format!(
   1218                 "handoff step range {}-{} is overlapping, gapped, or invalid; expected {}",
   1219                 range.start, range.end, next
   1220             ));
   1221         }
   1222         if range.owners.is_empty()
   1223             || range
   1224                 .owners
   1225                 .iter()
   1226                 .any(|owner| !allowed_owners.contains(owner.as_str()))
   1227         {
   1228             return Err(format!(
   1229                 "handoff step range {}-{} has an invalid owner",
   1230                 range.start, range.end
   1231             ));
   1232         }
   1233         if !allowed_dispositions.contains(range.disposition.as_str())
   1234             || range.reason.trim().is_empty()
   1235         {
   1236             return Err(format!(
   1237                 "handoff step range {}-{} has an invalid disposition",
   1238                 range.start, range.end
   1239             ));
   1240         }
   1241         next = range
   1242             .end
   1243             .checked_add(1)
   1244             .ok_or_else(|| "handoff step range overflow".to_owned())?;
   1245     }
   1246     if next != EXPECTED_HANDOFF_STEPS + 1 {
   1247         return Err(format!(
   1248             "handoff step map ends at {}, expected {}",
   1249             next.saturating_sub(1),
   1250             EXPECTED_HANDOFF_STEPS
   1251         ));
   1252     }
   1253     Ok(())
   1254 }
   1255 
   1256 fn validate_history(history: &HistoryContract) -> Result<(), String> {
   1257     if history.schema_version != 1
   1258         || history.history_id != HISTORY_ID
   1259         || history.retention_locator != "external://radroots-rust-consolidation-v1-20260805"
   1260         || history.hash_algorithm != "sha256"
   1261         || history.bundle_hash_algorithm != "sha1"
   1262     {
   1263         return Err("history preservation identity drifted".to_owned());
   1264     }
   1265 
   1266     let expected_commit_map_fields = BTreeSet::from([
   1267         "empty_commit_disposition",
   1268         "normalized_patch_sha256",
   1269         "source_author",
   1270         "source_author_time",
   1271         "source_commit",
   1272         "source_committer_time",
   1273         "source_parents",
   1274         "source_paths",
   1275         "source_subject",
   1276         "target_author",
   1277         "target_author_time",
   1278         "target_commit",
   1279         "target_committer_time",
   1280         "target_parents",
   1281         "target_subject",
   1282     ]);
   1283     if to_unique_set(&history.required_commit_map_fields, "commit map field")?
   1284         != expected_commit_map_fields
   1285     {
   1286         return Err("commit map requirements drifted".to_owned());
   1287     }
   1288     let expected_verifications = BTreeSet::from([
   1289         "archive_restore",
   1290         "authorship",
   1291         "bot_identity_scan",
   1292         "commit_count",
   1293         "context_firewall",
   1294         "final_tree_digest",
   1295         "git_fsck",
   1296         "git_log_follow",
   1297         "github_workflow_exclusion",
   1298         "license_scan",
   1299         "mapped_parent_closure",
   1300         "message_scope_only",
   1301         "normalized_patch_equivalence",
   1302         "path_coverage",
   1303         "secret_scan",
   1304         "timestamps",
   1305     ]);
   1306     if to_unique_set(&history.required_verifications, "verification")? != expected_verifications {
   1307         return Err("history verification requirements drifted".to_owned());
   1308     }
   1309     let dual_source = &history.dual_source;
   1310     if dual_source.state != "pre_import"
   1311         || dual_source.canonical_owner_before_import != "frozen_donor_commit"
   1312         || dual_source.canonical_owner_after_import != "verified_lib_import_merge"
   1313         || dual_source.divergence_policy != "forbidden"
   1314         || dual_source.emergency_fix_flow.trim().is_empty()
   1315         || dual_source.exit_condition.trim().is_empty()
   1316     {
   1317         return Err("dual-source control drifted".to_owned());
   1318     }
   1319 
   1320     let expected_sources =
   1321         BTreeSet::from(["app_rt", "sdk", "studio_app", "studio_mpl_legacy_core"]);
   1322     let mut archives = BTreeMap::new();
   1323     let mut artifact_names = BTreeSet::new();
   1324     for archive in &history.archive {
   1325         validate_identifier(&archive.source_id, "archive source id")?;
   1326         if archives
   1327             .insert(archive.source_id.as_str(), archive)
   1328             .is_some()
   1329         {
   1330             return Err(format!("duplicate archive source {}", archive.source_id));
   1331         }
   1332         validate_oid(&archive.frozen_commit, "archive frozen commit")?;
   1333         validate_sha256(&archive.sha256, "archive digest")?;
   1334         validate_artifact_name(&archive.artifact)?;
   1335         if !artifact_names.insert(archive.artifact.as_str()) {
   1336             return Err(format!("duplicate archive artifact {}", archive.artifact));
   1337         }
   1338         if !matches!(archive.kind.as_str(), "git_bundle" | "path_fast_export")
   1339             || archive.bytes == 0
   1340             || archive.source_commit_count == 0
   1341             || archive.source_path_commit_count == 0
   1342             || archive.source_path_commit_count > archive.source_commit_count
   1343             || archive.rewritten_commit_count == 0
   1344             || archive.rewritten_commit_count + archive.omitted_empty_commit_count
   1345                 != archive.source_path_commit_count + archive.topology_support_commit_count
   1346             || archive.source_object_count == 0
   1347             || archive.refname != "refs/heads/master"
   1348             || !archive.bot_identity_scan
   1349         {
   1350             return Err(format!("archive {} is incomplete", archive.source_id));
   1351         }
   1352     }
   1353     if archives.keys().copied().collect::<BTreeSet<_>>() != expected_sources {
   1354         return Err("history archive inventory is incomplete".to_owned());
   1355     }
   1356     if archives["studio_mpl_legacy_core"].kind != "path_fast_export"
   1357         || archives
   1358             .iter()
   1359             .filter(|(id, archive)| {
   1360                 **id != "studio_mpl_legacy_core" && archive.kind == "git_bundle"
   1361             })
   1362             .count()
   1363             != 3
   1364     {
   1365         return Err("history archive kinds drifted".to_owned());
   1366     }
   1367 
   1368     let allowed_licenses = BTreeSet::from([
   1369         "GPL-3.0-only",
   1370         "GPL-3.0-or-later",
   1371         "MIT OR Apache-2.0",
   1372         "MPL-2.0",
   1373     ]);
   1374     let allowed_dispositions = BTreeSet::from([
   1375         "import_unique_behavior_then_retire",
   1376         "merge_then_retire",
   1377         "retain",
   1378         "retain_private",
   1379     ]);
   1380     let mut source_paths = BTreeSet::new();
   1381     let mut target_paths = BTreeSet::new();
   1382     let mut package_sources = BTreeSet::new();
   1383     for path_map in &history.path_map {
   1384         if !archives.contains_key(path_map.source_id.as_str()) {
   1385             return Err(format!("unknown path-map source {}", path_map.source_id));
   1386         }
   1387         validate_relative_path(&path_map.source)?;
   1388         validate_relative_path(&path_map.target)?;
   1389         validate_identifier(&path_map.package, "path-map package")?;
   1390         if !allowed_licenses.contains(path_map.license.as_str())
   1391             || !allowed_dispositions.contains(path_map.disposition.as_str())
   1392         {
   1393             return Err(format!(
   1394                 "path map {}/{} has invalid license or disposition",
   1395                 path_map.source_id, path_map.source
   1396             ));
   1397         }
   1398         if !source_paths.insert((path_map.source_id.as_str(), path_map.source.as_str())) {
   1399             return Err(format!(
   1400                 "duplicate source path {}/{}",
   1401                 path_map.source_id, path_map.source
   1402             ));
   1403         }
   1404         if !target_paths.insert(path_map.target.as_str()) {
   1405             return Err(format!("duplicate target path {}", path_map.target));
   1406         }
   1407         if !package_sources.insert((path_map.source_id.as_str(), path_map.package.as_str())) {
   1408             return Err(format!(
   1409                 "duplicate package {} in source {}",
   1410                 path_map.package, path_map.source_id
   1411             ));
   1412         }
   1413     }
   1414     for source in expected_sources {
   1415         if !source_paths
   1416             .iter()
   1417             .any(|(candidate, _)| *candidate == source)
   1418         {
   1419             return Err(format!("source {source} has no path map"));
   1420         }
   1421     }
   1422     Ok(())
   1423 }
   1424 
   1425 fn validate_archives(history: &HistoryContract, archive_root: &Path) -> Result<(), String> {
   1426     if !archive_root.is_absolute() {
   1427         return Err("archive root must be absolute".to_owned());
   1428     }
   1429     let root_metadata = fs::symlink_metadata(archive_root)
   1430         .map_err(|error| format!("inspect archive root {}: {error}", archive_root.display()))?;
   1431     if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
   1432         return Err("archive root must be a real directory, not a symlink".to_owned());
   1433     }
   1434 
   1435     for archive in &history.archive {
   1436         let artifact = archive_root.join(&archive.artifact);
   1437         let metadata = fs::symlink_metadata(&artifact)
   1438             .map_err(|error| format!("inspect archive {}: {error}", artifact.display()))?;
   1439         if metadata.file_type().is_symlink() || !metadata.is_file() {
   1440             return Err(format!(
   1441                 "archive {} must be a regular non-symlink file",
   1442                 artifact.display()
   1443             ));
   1444         }
   1445         if metadata.len() != archive.bytes {
   1446             return Err(format!(
   1447                 "archive {} size drifted: expected {}, got {}",
   1448                 artifact.display(),
   1449                 archive.bytes,
   1450                 metadata.len()
   1451             ));
   1452         }
   1453         let digest = sha256_file(&artifact)?;
   1454         if digest != archive.sha256 {
   1455             return Err(format!("archive {} digest drifted", artifact.display()));
   1456         }
   1457         if archive.kind == "git_bundle" {
   1458             let output = Command::new("git")
   1459                 .args(["bundle", "verify"])
   1460                 .arg(&artifact)
   1461                 .output()
   1462                 .map_err(|error| format!("run git bundle verify: {error}"))?;
   1463             if !output.status.success() {
   1464                 return Err(format!(
   1465                     "git bundle verify failed for {}: {}",
   1466                     artifact.display(),
   1467                     String::from_utf8_lossy(&output.stderr).trim()
   1468                 ));
   1469             }
   1470         } else {
   1471             validate_fast_export(&artifact, archive)?;
   1472         }
   1473     }
   1474     Ok(())
   1475 }
   1476 
   1477 fn validate_fast_export(path: &Path, archive: &Archive) -> Result<(), String> {
   1478     let raw = fs::read_to_string(path)
   1479         .map_err(|error| format!("read fast-export archive {}: {error}", path.display()))?;
   1480     if !raw.contains(&format!("original-oid {}", archive.frozen_commit))
   1481         || !raw.contains("reset refs/heads/master")
   1482         || !raw.contains("author triesap <tyson@radroots.org>")
   1483         || !raw.contains("committer triesap <tyson@radroots.org>")
   1484         || raw.to_ascii_lowercase().contains("github-actions")
   1485         || raw.to_ascii_lowercase().contains("[bot]")
   1486     {
   1487         return Err("legacy Studio fast-export identity or ref drifted".to_owned());
   1488     }
   1489     for line in raw.lines() {
   1490         let Some(path) = line
   1491             .strip_prefix("M ")
   1492             .and_then(|line| line.splitn(3, ' ').nth(2))
   1493         else {
   1494             continue;
   1495         };
   1496         if !path.starts_with("studio_app/studio_app_core/") {
   1497             return Err(format!("fast-export contains out-of-scope path {path}"));
   1498         }
   1499     }
   1500     Ok(())
   1501 }
   1502 
   1503 fn run_history_rehearsal() -> Result<(), String> {
   1504     let fixture = tempfile::TempDir::new()
   1505         .map_err(|error| format!("create history rehearsal root: {error}"))?;
   1506     let source = fixture.path().join("source");
   1507     let restored = fixture.path().join("restored");
   1508     let filtered = fixture.path().join("filtered");
   1509     let import_target = fixture.path().join("import-target");
   1510     let bundle = fixture.path().join("source.bundle");
   1511 
   1512     create_history_fixture(&source)?;
   1513     git(&source, &["bundle", "create", path_arg(&bundle)?, "master"])?;
   1514     git(
   1515         fixture.path(),
   1516         &["clone", path_arg(&bundle)?, path_arg(&restored)?],
   1517     )?;
   1518     git(&restored, &["fsck", "--full", "--strict"])?;
   1519 
   1520     git(
   1521         fixture.path(),
   1522         &["clone", path_arg(&bundle)?, path_arg(&filtered)?],
   1523     )?;
   1524     git(
   1525         &filtered,
   1526         &[
   1527             "filter-repo",
   1528             "--force",
   1529             "--path",
   1530             "src/",
   1531             "--path-rename",
   1532             "src/:crates/imported/",
   1533         ],
   1534     )?;
   1535     let commit_map_path = filtered.join(".git/filter-repo/commit-map");
   1536     let commit_map = parse_commit_map(&commit_map_path)?;
   1537     verify_filtered_history(&source, &filtered, &commit_map, "src", "crates/imported")?;
   1538 
   1539     create_import_target(&import_target)?;
   1540     git(
   1541         &import_target,
   1542         &[
   1543             "fetch",
   1544             path_arg(&filtered)?,
   1545             "master:refs/remotes/rehearsal/imported",
   1546         ],
   1547     )?;
   1548     let merge_tree = git_stdout(
   1549         &import_target,
   1550         &[
   1551             "merge-tree",
   1552             "--write-tree",
   1553             "--allow-unrelated-histories",
   1554             "HEAD",
   1555             "refs/remotes/rehearsal/imported",
   1556         ],
   1557     )?;
   1558     let merge_tree = merge_tree.trim();
   1559     validate_oid(merge_tree, "rehearsal merge tree")?;
   1560     if git_stdout(&import_target, &["cat-file", "-t", merge_tree])?.trim() != "tree" {
   1561         return Err("no-op import rehearsal did not produce a tree".to_owned());
   1562     }
   1563     if git_stdout(&import_target, &["status", "--porcelain"])
   1564         .map(|output| !output.trim().is_empty())?
   1565     {
   1566         return Err("no-op import rehearsal changed the target worktree".to_owned());
   1567     }
   1568 
   1569     exercise_history_negative_cases(&source, &filtered, &commit_map)?;
   1570     Ok(())
   1571 }
   1572 
   1573 fn create_history_fixture(root: &Path) -> Result<(), String> {
   1574     fs::create_dir(root).map_err(|error| format!("create {}: {error}", root.display()))?;
   1575     git(root, &["init", "--initial-branch=master"])?;
   1576     git(root, &["config", "user.name", "Radroots History Fixture"])?;
   1577     git(
   1578         root,
   1579         &["config", "user.email", "history-fixture@radroots.org"],
   1580     )?;
   1581     write_fixture(root, "src/LICENSE", "MIT OR Apache-2.0\n")?;
   1582     write_fixture(root, "src/item.txt", "base\n")?;
   1583     write_fixture(root, "AGENTS.md", "context only\n")?;
   1584     fixture_commit(root, "seed reusable source", "2001-01-01T00:00:00+00:00")?;
   1585 
   1586     git(root, &["branch", "feature"])?;
   1587     append_fixture(root, "src/item.txt", "main\n")?;
   1588     fixture_commit(root, "extend main source", "2001-01-02T00:00:00+00:00")?;
   1589     append_fixture(root, "AGENTS.md", "must not import\n")?;
   1590     fixture_commit(root, "change donor context", "2001-01-03T00:00:00+00:00")?;
   1591 
   1592     git(root, &["checkout", "feature"])?;
   1593     write_fixture(root, "src/feature.txt", "feature\n")?;
   1594     fixture_commit(root, "add feature source", "2001-01-04T00:00:00+00:00")?;
   1595     git(root, &["checkout", "master"])?;
   1596     git_with_identity(
   1597         root,
   1598         &["merge", "--no-ff", "feature", "-m", "merge reusable source"],
   1599         "2001-01-05T00:00:00+00:00",
   1600     )?;
   1601     write_fixture(root, ".github/workflows/forbidden.yml", "forbidden: true\n")?;
   1602     fixture_commit(
   1603         root,
   1604         "add forbidden donor automation",
   1605         "2001-01-06T00:00:00+00:00",
   1606     )?;
   1607     append_fixture(root, "src/item.txt", "final\n")?;
   1608     fixture_commit(root, "finish reusable source", "2001-01-07T00:00:00+00:00")?;
   1609     Ok(())
   1610 }
   1611 
   1612 fn create_import_target(root: &Path) -> Result<(), String> {
   1613     fs::create_dir(root).map_err(|error| format!("create {}: {error}", root.display()))?;
   1614     git(root, &["init", "--initial-branch=master"])?;
   1615     git(root, &["config", "user.name", "Radroots History Fixture"])?;
   1616     git(
   1617         root,
   1618         &["config", "user.email", "history-fixture@radroots.org"],
   1619     )?;
   1620     write_fixture(root, "README", "import target\n")?;
   1621     fixture_commit(root, "seed import target", "2001-01-08T00:00:00+00:00")
   1622 }
   1623 
   1624 fn write_fixture(root: &Path, relative: &str, contents: &str) -> Result<(), String> {
   1625     let path = root.join(relative);
   1626     if let Some(parent) = path.parent() {
   1627         fs::create_dir_all(parent)
   1628             .map_err(|error| format!("create {}: {error}", parent.display()))?;
   1629     }
   1630     fs::write(&path, contents).map_err(|error| format!("write {}: {error}", path.display()))
   1631 }
   1632 
   1633 fn append_fixture(root: &Path, relative: &str, contents: &str) -> Result<(), String> {
   1634     use std::io::Write;
   1635 
   1636     let path = root.join(relative);
   1637     let mut file = fs::OpenOptions::new()
   1638         .append(true)
   1639         .open(&path)
   1640         .map_err(|error| format!("open {}: {error}", path.display()))?;
   1641     file.write_all(contents.as_bytes())
   1642         .map_err(|error| format!("append {}: {error}", path.display()))
   1643 }
   1644 
   1645 fn fixture_commit(root: &Path, subject: &str, timestamp: &str) -> Result<(), String> {
   1646     git(root, &["add", "--all"])?;
   1647     git_with_identity(root, &["commit", "-m", subject], timestamp)
   1648 }
   1649 
   1650 fn git_with_identity(root: &Path, args: &[&str], timestamp: &str) -> Result<(), String> {
   1651     let output = Command::new("git")
   1652         .args(args)
   1653         .current_dir(root)
   1654         .env("GIT_AUTHOR_DATE", timestamp)
   1655         .env("GIT_COMMITTER_DATE", timestamp)
   1656         .output()
   1657         .map_err(|error| format!("run git {}: {error}", args.join(" ")))?;
   1658     command_success(output, root, args).map(|_| ())
   1659 }
   1660 
   1661 fn parse_commit_map(path: &Path) -> Result<Vec<CommitMapEntry>, String> {
   1662     let raw = fs::read_to_string(path)
   1663         .map_err(|error| format!("read commit map {}: {error}", path.display()))?;
   1664     let mut entries = Vec::new();
   1665     for (line_index, line) in raw.lines().enumerate() {
   1666         if line_index == 0 && line == "old                                      new" {
   1667             continue;
   1668         }
   1669         let fields = line.split_ascii_whitespace().collect::<Vec<_>>();
   1670         if fields.len() != 2 {
   1671             return Err(format!("commit map line {} is malformed", line_index + 1));
   1672         }
   1673         validate_oid(fields[0], "source commit-map object")?;
   1674         validate_oid(fields[1], "target commit-map object")?;
   1675         entries.push(CommitMapEntry {
   1676             source: fields[0].to_owned(),
   1677             target: (fields[1] != "0000000000000000000000000000000000000000")
   1678                 .then(|| fields[1].to_owned()),
   1679         });
   1680     }
   1681     if entries.is_empty() {
   1682         return Err("commit map contains no entries".to_owned());
   1683     }
   1684     let unique = entries
   1685         .iter()
   1686         .map(|entry| entry.source.as_str())
   1687         .collect::<BTreeSet<_>>();
   1688     if unique.len() != entries.len() {
   1689         return Err("commit map contains duplicate source commits".to_owned());
   1690     }
   1691     Ok(entries)
   1692 }
   1693 
   1694 fn verify_filtered_history(
   1695     source: &Path,
   1696     target: &Path,
   1697     entries: &[CommitMapEntry],
   1698     source_prefix: &str,
   1699     target_prefix: &str,
   1700 ) -> Result<(), String> {
   1701     validate_relative_path(source_prefix)?;
   1702     validate_relative_path(target_prefix)?;
   1703     git(source, &["fsck", "--full", "--strict"])?;
   1704     git(target, &["fsck", "--full", "--strict"])?;
   1705 
   1706     let by_source = entries
   1707         .iter()
   1708         .map(|entry| (entry.source.as_str(), entry.target.as_deref()))
   1709         .collect::<BTreeMap<_, _>>();
   1710     let mut saw_merge = false;
   1711     let mut saw_omitted = false;
   1712     for entry in entries {
   1713         git(
   1714             source,
   1715             &["cat-file", "-e", &format!("{}^{{commit}}", entry.source)],
   1716         )?;
   1717         let Some(target_commit) = entry.target.as_deref() else {
   1718             saw_omitted = true;
   1719             continue;
   1720         };
   1721         git(
   1722             target,
   1723             &["cat-file", "-e", &format!("{target_commit}^{{commit}}")],
   1724         )?;
   1725         verify_commit_metadata(source, target, &entry.source, target_commit)?;
   1726         let source_parents = commit_parents(source, &entry.source)?;
   1727         saw_merge |= source_parents.len() > 1;
   1728         let mut expected_target_parents = Vec::new();
   1729         for parent in source_parents {
   1730             collect_effective_parents(source, &parent, &by_source, &mut expected_target_parents)?;
   1731         }
   1732         deduplicate(&mut expected_target_parents);
   1733         if commit_parents(target, target_commit)? != expected_target_parents {
   1734             return Err(format!(
   1735                 "mapped parent closure drifted for {}",
   1736                 entry.source
   1737             ));
   1738         }
   1739         let source_patch = normalized_patch(source, &entry.source, source_prefix, "__IMPORT__")?;
   1740         let target_patch = normalized_patch(target, target_commit, target_prefix, "__IMPORT__")?;
   1741         if source_patch != target_patch {
   1742             return Err(format!("normalized patch drifted for {}", entry.source));
   1743         }
   1744     }
   1745     if !saw_merge || !saw_omitted {
   1746         return Err("history rehearsal must include a merge and an omitted commit".to_owned());
   1747     }
   1748 
   1749     let source_head = git_stdout(source, &["rev-parse", "master"])?;
   1750     let source_head = source_head.trim();
   1751     let expected_target_head = by_source
   1752         .get(source_head)
   1753         .and_then(|target| *target)
   1754         .ok_or_else(|| "source master is not retained in commit map".to_owned())?;
   1755     if git_stdout(target, &["rev-parse", "master"])?.trim() != expected_target_head {
   1756         return Err("filtered master has unexpected commits".to_owned());
   1757     }
   1758     verify_final_tree(
   1759         source,
   1760         target,
   1761         source_head,
   1762         expected_target_head,
   1763         source_prefix,
   1764         target_prefix,
   1765     )?;
   1766     verify_context_firewall(target, expected_target_head, target_prefix)?;
   1767     verify_commit_identities(target, expected_target_head)?;
   1768     verify_follow_history(source, target, source_prefix, target_prefix)?;
   1769     Ok(())
   1770 }
   1771 
   1772 fn verify_commit_metadata(
   1773     source: &Path,
   1774     target: &Path,
   1775     source_commit: &str,
   1776     target_commit: &str,
   1777 ) -> Result<(), String> {
   1778     let format = "%an%x00%ae%x00%aI%x00%cn%x00%ce%x00%cI%x00%s";
   1779     let source_meta = git_stdout(
   1780         source,
   1781         &["show", "-s", &format!("--format={format}"), source_commit],
   1782     )?;
   1783     let target_meta = git_stdout(
   1784         target,
   1785         &["show", "-s", &format!("--format={format}"), target_commit],
   1786     )?;
   1787     let mut source_fields = source_meta.trim_end().split('\0').collect::<Vec<_>>();
   1788     let mut target_fields = target_meta.trim_end().split('\0').collect::<Vec<_>>();
   1789     if source_fields.len() != 7 || target_fields.len() != 7 {
   1790         return Err("commit metadata has unexpected cardinality".to_owned());
   1791     }
   1792     let source_subject = source_fields.pop().expect("cardinality checked");
   1793     let target_subject = target_fields.pop().expect("cardinality checked");
   1794     if source_fields != target_fields || !message_is_preserved(source_subject, target_subject) {
   1795         return Err(format!(
   1796             "attribution or message drifted for {source_commit}"
   1797         ));
   1798     }
   1799     Ok(())
   1800 }
   1801 
   1802 fn message_is_preserved(source: &str, target: &str) -> bool {
   1803     if source == target {
   1804         return true;
   1805     }
   1806     let Some(scope) = target
   1807         .strip_prefix(source)
   1808         .and_then(|suffix| suffix.strip_prefix(" ("))
   1809         .and_then(|suffix| suffix.strip_suffix(')'))
   1810     else {
   1811         return false;
   1812     };
   1813     !scope.is_empty()
   1814         && scope.bytes().all(|byte| {
   1815             byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-' | b'/')
   1816         })
   1817 }
   1818 
   1819 fn commit_parents(repo: &Path, commit: &str) -> Result<Vec<String>, String> {
   1820     let output = git_stdout(repo, &["show", "-s", "--format=%P", commit])?;
   1821     Ok(output.split_ascii_whitespace().map(str::to_owned).collect())
   1822 }
   1823 
   1824 fn collect_effective_parents(
   1825     source: &Path,
   1826     commit: &str,
   1827     by_source: &BTreeMap<&str, Option<&str>>,
   1828     output: &mut Vec<String>,
   1829 ) -> Result<(), String> {
   1830     match by_source.get(commit).copied().flatten() {
   1831         Some(target) => output.push(target.to_owned()),
   1832         None => {
   1833             for parent in commit_parents(source, commit)? {
   1834                 collect_effective_parents(source, &parent, by_source, output)?;
   1835             }
   1836         }
   1837     }
   1838     Ok(())
   1839 }
   1840 
   1841 fn deduplicate(values: &mut Vec<String>) {
   1842     let mut seen = BTreeSet::new();
   1843     values.retain(|value| seen.insert(value.clone()));
   1844 }
   1845 
   1846 fn normalized_patch(
   1847     repo: &Path,
   1848     commit: &str,
   1849     prefix: &str,
   1850     normalized_prefix: &str,
   1851 ) -> Result<String, String> {
   1852     let patch = git_stdout(
   1853         repo,
   1854         &[
   1855             "-c",
   1856             "core.quotePath=false",
   1857             "diff-tree",
   1858             "--root",
   1859             "-m",
   1860             "-r",
   1861             "--binary",
   1862             "--full-index",
   1863             "--no-commit-id",
   1864             commit,
   1865             "--",
   1866             prefix,
   1867         ],
   1868     )?;
   1869     Ok(patch.replace(prefix, normalized_prefix))
   1870 }
   1871 
   1872 fn verify_final_tree(
   1873     source: &Path,
   1874     target: &Path,
   1875     source_commit: &str,
   1876     target_commit: &str,
   1877     source_prefix: &str,
   1878     target_prefix: &str,
   1879 ) -> Result<(), String> {
   1880     let source_tree = tree_manifest(source, source_commit, source_prefix, source_prefix)?;
   1881     let target_tree = tree_manifest(target, target_commit, target_prefix, source_prefix)?;
   1882     if source_tree != target_tree {
   1883         return Err("filtered final tree drifted".to_owned());
   1884     }
   1885     Ok(())
   1886 }
   1887 
   1888 fn tree_manifest(
   1889     repo: &Path,
   1890     commit: &str,
   1891     prefix: &str,
   1892     normalized_prefix: &str,
   1893 ) -> Result<String, String> {
   1894     let output = git_stdout(
   1895         repo,
   1896         &["ls-tree", "-r", "--full-tree", commit, "--", prefix],
   1897     )?;
   1898     Ok(output.replace(prefix, normalized_prefix))
   1899 }
   1900 
   1901 fn verify_context_firewall(repo: &Path, commit: &str, target_prefix: &str) -> Result<(), String> {
   1902     let paths = git_stdout(repo, &["ls-tree", "-r", "--name-only", commit])?;
   1903     let required_prefix = format!("{target_prefix}/");
   1904     for path in paths.lines() {
   1905         let lower = path.to_ascii_lowercase();
   1906         if !path.starts_with(&required_prefix)
   1907             || path.split('/').any(|segment| segment == ".github")
   1908             || matches!(path.rsplit('/').next(), Some("AGENTS.md" | "CLAUDE.md"))
   1909             || lower.ends_with(".pem")
   1910             || lower.ends_with(".key")
   1911             || lower.ends_with("/.env")
   1912         {
   1913             return Err(format!("context firewall rejected {path}"));
   1914         }
   1915         let contents = git_stdout(repo, &["show", &format!("{commit}:{path}")])?;
   1916         let lower_contents = contents.to_ascii_lowercase();
   1917         if contents.contains("PRIVATE KEY-----")
   1918             || contents.contains("ghp_")
   1919             || lower_contents.contains("github-actions[bot]")
   1920         {
   1921             return Err(format!("secret or bot content rejected in {path}"));
   1922         }
   1923     }
   1924     Ok(())
   1925 }
   1926 
   1927 fn verify_commit_identities(repo: &Path, commit: &str) -> Result<(), String> {
   1928     let identities = git_stdout(repo, &["log", "--format=%an%x00%ae%x00%cn%x00%ce", commit])?;
   1929     let lower = identities.to_ascii_lowercase();
   1930     if lower.contains("github-actions") || lower.contains("[bot]") {
   1931         return Err("bot identity found in filtered history".to_owned());
   1932     }
   1933     Ok(())
   1934 }
   1935 
   1936 fn verify_follow_history(
   1937     source: &Path,
   1938     target: &Path,
   1939     source_prefix: &str,
   1940     target_prefix: &str,
   1941 ) -> Result<(), String> {
   1942     let source_file = format!("{source_prefix}/item.txt");
   1943     let target_file = format!("{target_prefix}/item.txt");
   1944     let source_log = git_stdout(
   1945         source,
   1946         &["log", "--follow", "--format=%s", "--", &source_file],
   1947     )?;
   1948     let target_log = git_stdout(
   1949         target,
   1950         &["log", "--follow", "--format=%s", "--", &target_file],
   1951     )?;
   1952     if source_log != target_log {
   1953         return Err("git log --follow attribution drifted".to_owned());
   1954     }
   1955     Ok(())
   1956 }
   1957 
   1958 fn exercise_history_negative_cases(
   1959     source: &Path,
   1960     filtered: &Path,
   1961     entries: &[CommitMapEntry],
   1962 ) -> Result<(), String> {
   1963     if message_is_preserved("preserve this", "rewritten message")
   1964         || message_is_preserved("preserve this", "preserve this (Bad Scope)")
   1965     {
   1966         return Err("message negative fixture was accepted".to_owned());
   1967     }
   1968     let malformed_map = filtered.join("malformed-commit-map");
   1969     fs::write(&malformed_map, "old new\nnot-an-oid still-not-an-oid\n")
   1970         .map_err(|error| format!("write negative commit map: {error}"))?;
   1971     if parse_commit_map(&malformed_map).is_ok() {
   1972         return Err("malformed commit map was accepted".to_owned());
   1973     }
   1974     let head = entries
   1975         .iter()
   1976         .rev()
   1977         .find_map(|entry| entry.target.as_deref())
   1978         .ok_or_else(|| "negative fixture has no target head".to_owned())?;
   1979     if verify_context_firewall(filtered, head, "wrong/prefix").is_ok() {
   1980         return Err("context-firewall negative fixture was accepted".to_owned());
   1981     }
   1982     if normalized_patch(source, &entries[0].source, "src", "__IMPORT__")?
   1983         == normalized_patch(filtered, head, "crates/imported", "__WRONG__")?
   1984     {
   1985         return Err("normalized-patch negative fixture was accepted".to_owned());
   1986     }
   1987 
   1988     let source_head = git_stdout(source, &["rev-parse", "master"])?;
   1989     let source_head = source_head.trim();
   1990     let negative_root = filtered
   1991         .parent()
   1992         .ok_or_else(|| "filtered fixture has no parent".to_owned())?;
   1993 
   1994     let context = clone_negative(filtered, negative_root, "negative-context")?;
   1995     write_fixture(
   1996         &context,
   1997         "AGENTS.md",
   1998         "must not cross the source boundary\n",
   1999     )?;
   2000     fixture_commit(&context, "inject context", "2001-02-01T00:00:00+00:00")?;
   2001     let context_head = git_stdout(&context, &["rev-parse", "HEAD"])?;
   2002     if verify_context_firewall(&context, context_head.trim(), "crates/imported").is_ok() {
   2003         return Err("context negative fixture was accepted".to_owned());
   2004     }
   2005 
   2006     let workflow = clone_negative(filtered, negative_root, "negative-workflow")?;
   2007     write_fixture(
   2008         &workflow,
   2009         ".github/workflows/forbidden.yml",
   2010         "forbidden: true\n",
   2011     )?;
   2012     fixture_commit(&workflow, "inject workflow", "2001-02-02T00:00:00+00:00")?;
   2013     let workflow_head = git_stdout(&workflow, &["rev-parse", "HEAD"])?;
   2014     if verify_context_firewall(&workflow, workflow_head.trim(), "crates/imported").is_ok() {
   2015         return Err("GitHub-workflow negative fixture was accepted".to_owned());
   2016     }
   2017 
   2018     let secret = clone_negative(filtered, negative_root, "negative-secret")?;
   2019     write_fixture(
   2020         &secret,
   2021         "crates/imported/secret.pem",
   2022         "-----BEGIN PRIVATE KEY-----\nfixture\n",
   2023     )?;
   2024     fixture_commit(&secret, "inject secret", "2001-02-03T00:00:00+00:00")?;
   2025     let secret_head = git_stdout(&secret, &["rev-parse", "HEAD"])?;
   2026     if verify_context_firewall(&secret, secret_head.trim(), "crates/imported").is_ok() {
   2027         return Err("secret negative fixture was accepted".to_owned());
   2028     }
   2029 
   2030     let bot = clone_negative(filtered, negative_root, "negative-bot")?;
   2031     git_commit_with_actor(
   2032         &bot,
   2033         "inject bot identity",
   2034         "github-actions[bot]",
   2035         "41898282+github-actions[bot]@users.noreply.github.com",
   2036         "2001-02-04T00:00:00+00:00",
   2037         true,
   2038     )?;
   2039     let bot_head = git_stdout(&bot, &["rev-parse", "HEAD"])?;
   2040     if verify_commit_identities(&bot, bot_head.trim()).is_ok() {
   2041         return Err("bot-identity negative fixture was accepted".to_owned());
   2042     }
   2043 
   2044     let license = clone_negative(filtered, negative_root, "negative-license")?;
   2045     write_fixture(&license, "crates/imported/LICENSE", "GPL-3.0-only\n")?;
   2046     fixture_commit(&license, "change license", "2001-02-05T00:00:00+00:00")?;
   2047     let license_head = git_stdout(&license, &["rev-parse", "HEAD"])?;
   2048     if verify_final_tree(
   2049         source,
   2050         &license,
   2051         source_head,
   2052         license_head.trim(),
   2053         "src",
   2054         "crates/imported",
   2055     )
   2056     .is_ok()
   2057     {
   2058         return Err("license/tree negative fixture was accepted".to_owned());
   2059     }
   2060 
   2061     let attribution = clone_negative(filtered, negative_root, "negative-attribution")?;
   2062     git_commit_with_actor(
   2063         &attribution,
   2064         "finish reusable source",
   2065         "Wrong Author",
   2066         "wrong-author@radroots.org",
   2067         "2001-01-07T00:00:00+00:00",
   2068         false,
   2069     )?;
   2070     let attribution_head = git_stdout(&attribution, &["rev-parse", "HEAD"])?;
   2071     if verify_commit_metadata(source, &attribution, source_head, attribution_head.trim()).is_ok() {
   2072         return Err("attribution negative fixture was accepted".to_owned());
   2073     }
   2074 
   2075     let timestamp = clone_negative(filtered, negative_root, "negative-timestamp")?;
   2076     git_commit_with_actor(
   2077         &timestamp,
   2078         "finish reusable source",
   2079         "Radroots History Fixture",
   2080         "history-fixture@radroots.org",
   2081         "2002-01-07T00:00:00+00:00",
   2082         false,
   2083     )?;
   2084     let timestamp_head = git_stdout(&timestamp, &["rev-parse", "HEAD"])?;
   2085     if verify_commit_metadata(source, &timestamp, source_head, timestamp_head.trim()).is_ok() {
   2086         return Err("timestamp negative fixture was accepted".to_owned());
   2087     }
   2088 
   2089     let message = clone_negative(filtered, negative_root, "negative-message")?;
   2090     git_commit_with_actor(
   2091         &message,
   2092         "replace the original message",
   2093         "Radroots History Fixture",
   2094         "history-fixture@radroots.org",
   2095         "2001-01-07T00:00:00+00:00",
   2096         false,
   2097     )?;
   2098     let message_head = git_stdout(&message, &["rev-parse", "HEAD"])?;
   2099     if verify_commit_metadata(source, &message, source_head, message_head.trim()).is_ok() {
   2100         return Err("message negative fixture was accepted".to_owned());
   2101     }
   2102 
   2103     let follow = clone_negative(filtered, negative_root, "negative-follow")?;
   2104     append_fixture(&follow, "crates/imported/item.txt", "unmapped\n")?;
   2105     fixture_commit(
   2106         &follow,
   2107         "inject unmapped history",
   2108         "2001-02-06T00:00:00+00:00",
   2109     )?;
   2110     if verify_follow_history(source, &follow, "src", "crates/imported").is_ok() {
   2111         return Err("git-log-follow negative fixture was accepted".to_owned());
   2112     }
   2113 
   2114     let mut broken_map = entries.to_vec();
   2115     let replacement = broken_map
   2116         .iter()
   2117         .find_map(|entry| entry.target.clone())
   2118         .ok_or_else(|| "negative fixture has no replacement target".to_owned())?;
   2119     broken_map
   2120         .last_mut()
   2121         .ok_or_else(|| "negative fixture has no final map entry".to_owned())?
   2122         .target = Some(replacement);
   2123     if verify_filtered_history(source, filtered, &broken_map, "src", "crates/imported").is_ok() {
   2124         return Err("commit-map topology negative fixture was accepted".to_owned());
   2125     }
   2126 
   2127     let corrupt = clone_negative(filtered, negative_root, "negative-fsck")?;
   2128     let payload = corrupt.join("fsck-negative-payload");
   2129     fs::write(&payload, "unique fsck negative fixture payload\n")
   2130         .map_err(|error| format!("write fsck negative payload: {error}"))?;
   2131     let object = git_stdout(&corrupt, &["hash-object", "-w", path_arg(&payload)?])?;
   2132     let object = object.trim();
   2133     validate_oid(object, "fsck negative object")?;
   2134     let object_path = corrupt
   2135         .join(".git/objects")
   2136         .join(&object[..2])
   2137         .join(&object[2..]);
   2138     fs::remove_file(&object_path).map_err(|error| {
   2139         format!(
   2140             "unlink exact temporary negative object {}: {error}",
   2141             object_path.display()
   2142         )
   2143     })?;
   2144     fs::write(&object_path, "corrupt")
   2145         .map_err(|error| format!("corrupt negative object {}: {error}", object_path.display()))?;
   2146     if git(&corrupt, &["fsck", "--full", "--strict"]).is_ok() {
   2147         return Err("fsck negative fixture was accepted".to_owned());
   2148     }
   2149     Ok(())
   2150 }
   2151 
   2152 fn clone_negative(source: &Path, root: &Path, name: &str) -> Result<std::path::PathBuf, String> {
   2153     let target = root.join(name);
   2154     git(root, &["clone", path_arg(source)?, path_arg(&target)?])?;
   2155     git(
   2156         &target,
   2157         &["config", "user.name", "Radroots History Fixture"],
   2158     )?;
   2159     git(
   2160         &target,
   2161         &["config", "user.email", "history-fixture@radroots.org"],
   2162     )?;
   2163     Ok(target)
   2164 }
   2165 
   2166 fn git_commit_with_actor(
   2167     root: &Path,
   2168     subject: &str,
   2169     actor: &str,
   2170     email: &str,
   2171     timestamp: &str,
   2172     allow_empty: bool,
   2173 ) -> Result<(), String> {
   2174     let mut command = Command::new("git");
   2175     command
   2176         .current_dir(root)
   2177         .args(["commit", "--amend", "-m", subject])
   2178         .env("GIT_AUTHOR_NAME", actor)
   2179         .env("GIT_AUTHOR_EMAIL", email)
   2180         .env("GIT_COMMITTER_NAME", actor)
   2181         .env("GIT_COMMITTER_EMAIL", email)
   2182         .env("GIT_AUTHOR_DATE", timestamp)
   2183         .env("GIT_COMMITTER_DATE", timestamp);
   2184     if allow_empty {
   2185         command.arg("--allow-empty");
   2186     }
   2187     let output = command
   2188         .output()
   2189         .map_err(|error| format!("run negative commit fixture: {error}"))?;
   2190     command_success(output, root, &["commit", "--amend"]).map(|_| ())
   2191 }
   2192 
   2193 fn git(root: &Path, args: &[&str]) -> Result<(), String> {
   2194     let output = Command::new("git")
   2195         .args(args)
   2196         .current_dir(root)
   2197         .output()
   2198         .map_err(|error| format!("run git {}: {error}", args.join(" ")))?;
   2199     command_success(output, root, args).map(|_| ())
   2200 }
   2201 
   2202 fn git_stdout(root: &Path, args: &[&str]) -> Result<String, String> {
   2203     String::from_utf8(git_bytes(root, args)?)
   2204         .map_err(|error| format!("git {} emitted non-UTF-8 output: {error}", args.join(" ")))
   2205 }
   2206 
   2207 fn git_bytes(root: &Path, args: &[&str]) -> Result<Vec<u8>, String> {
   2208     let output = Command::new("git")
   2209         .args(args)
   2210         .current_dir(root)
   2211         .output()
   2212         .map_err(|error| format!("run git {}: {error}", args.join(" ")))?;
   2213     command_success(output, root, args)
   2214 }
   2215 
   2216 fn command_success(
   2217     output: std::process::Output,
   2218     root: &Path,
   2219     args: &[&str],
   2220 ) -> Result<Vec<u8>, String> {
   2221     if output.status.success() {
   2222         return Ok(output.stdout);
   2223     }
   2224     Err(format!(
   2225         "git {} failed in {}: {}",
   2226         args.join(" "),
   2227         root.display(),
   2228         String::from_utf8_lossy(&output.stderr).trim()
   2229     ))
   2230 }
   2231 
   2232 fn path_arg(path: &Path) -> Result<&str, String> {
   2233     path.to_str()
   2234         .ok_or_else(|| format!("path {} is not valid UTF-8", path.display()))
   2235 }
   2236 
   2237 fn sha256_file(path: &Path) -> Result<String, String> {
   2238     let mut file = fs::File::open(path)
   2239         .map_err(|error| format!("open archive {}: {error}", path.display()))?;
   2240     let mut hasher = Sha256::new();
   2241     let mut buffer = [0_u8; 64 * 1024];
   2242     loop {
   2243         let read = file
   2244             .read(&mut buffer)
   2245             .map_err(|error| format!("read archive {}: {error}", path.display()))?;
   2246         if read == 0 {
   2247             break;
   2248         }
   2249         hasher.update(&buffer[..read]);
   2250     }
   2251     Ok(format!("{:x}", hasher.finalize()))
   2252 }
   2253 
   2254 fn to_unique_set<'a>(values: &'a [String], context: &str) -> Result<BTreeSet<&'a str>, String> {
   2255     let set = values.iter().map(String::as_str).collect::<BTreeSet<_>>();
   2256     if set.len() != values.len() || set.iter().any(|value| value.trim().is_empty()) {
   2257         return Err(format!("{context} entries must be nonempty and unique"));
   2258     }
   2259     Ok(set)
   2260 }
   2261 
   2262 fn validate_sha256(value: &str, context: &str) -> Result<(), String> {
   2263     if value.len() != 64
   2264         || !value
   2265             .bytes()
   2266             .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
   2267     {
   2268         return Err(format!("{context} must be lowercase 64-hex SHA-256"));
   2269     }
   2270     Ok(())
   2271 }
   2272 
   2273 fn validate_artifact_name(value: &str) -> Result<(), String> {
   2274     if value.is_empty()
   2275         || value.contains('/')
   2276         || value.contains('\\')
   2277         || value == "."
   2278         || value == ".."
   2279         || !value
   2280             .bytes()
   2281             .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
   2282     {
   2283         return Err("archive artifact must be a safe portable file name".to_owned());
   2284     }
   2285     Ok(())
   2286 }
   2287 
   2288 fn validate_oid(value: &str, context: &str) -> Result<(), String> {
   2289     if value.len() != 40
   2290         || !value
   2291             .bytes()
   2292             .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
   2293     {
   2294         return Err(format!(
   2295             "{context} must be a full lowercase 40-hex Git object id"
   2296         ));
   2297     }
   2298     Ok(())
   2299 }
   2300 
   2301 fn validate_identifier(value: &str, context: &str) -> Result<(), String> {
   2302     if value.is_empty()
   2303         || !value
   2304             .bytes()
   2305             .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
   2306     {
   2307         return Err(format!("{context} must use lowercase snake case"));
   2308     }
   2309     Ok(())
   2310 }
   2311 
   2312 fn validate_relative_path(value: &str) -> Result<(), String> {
   2313     let path = Path::new(value);
   2314     if path.as_os_str().is_empty()
   2315         || path.is_absolute()
   2316         || value.contains('\\')
   2317         || value
   2318             .split('/')
   2319             .any(|segment| segment.is_empty() || matches!(segment, "." | ".."))
   2320         || path
   2321             .components()
   2322             .any(|component| !matches!(component, Component::Normal(_)))
   2323     {
   2324         return Err(format!(
   2325             "surface path {value:?} must be a safe relative path"
   2326         ));
   2327     }
   2328     Ok(())
   2329 }
   2330 
   2331 fn validate_date(value: &str) -> Result<(), String> {
   2332     let bytes = value.as_bytes();
   2333     if bytes.len() != 10
   2334         || bytes[4] != b'-'
   2335         || bytes[7] != b'-'
   2336         || bytes
   2337             .iter()
   2338             .enumerate()
   2339             .any(|(index, byte)| index != 4 && index != 7 && !byte.is_ascii_digit())
   2340     {
   2341         return Err("captured_date must use YYYY-MM-DD".to_owned());
   2342     }
   2343     Ok(())
   2344 }
   2345 
   2346 #[cfg(test)]
   2347 mod tests {
   2348     use super::*;
   2349 
   2350     #[test]
   2351     fn checked_in_baseline_and_step_map_validate() {
   2352         validate_baseline_contracts(&crate::workspace_root())
   2353             .expect("checked-in consolidation baseline");
   2354         validate_history_contract(&crate::workspace_root(), None)
   2355             .expect("checked-in history contract");
   2356     }
   2357 
   2358     #[test]
   2359     fn object_ids_require_full_lowercase_hex() {
   2360         assert!(validate_oid("0123456789abcdef0123456789abcdef01234567", "commit").is_ok());
   2361         assert!(validate_oid("0123456", "commit").is_err());
   2362         assert!(validate_oid("0123456789ABCDEF0123456789abcdef01234567", "commit").is_err());
   2363         assert!(validate_oid("g123456789abcdef0123456789abcdef01234567", "commit").is_err());
   2364     }
   2365 
   2366     #[test]
   2367     fn paths_reject_escape_and_non_normal_components() {
   2368         assert!(validate_relative_path("crates/sdk").is_ok());
   2369         assert!(validate_relative_path("../sdk").is_err());
   2370         assert!(validate_relative_path("crates/./sdk").is_err());
   2371         assert!(validate_relative_path("/crates/sdk").is_err());
   2372     }
   2373 
   2374     #[test]
   2375     fn retired_import_targets_must_be_absent() {
   2376         let root = tempfile::TempDir::new().expect("temporary workspace");
   2377         let target = "imports/retired_core";
   2378         let path_maps = vec![PathMap {
   2379             source_id: "retired_core".to_owned(),
   2380             source: "legacy/core".to_owned(),
   2381             target: target.to_owned(),
   2382             package: "retired_core".to_owned(),
   2383             license: "MPL-2.0".to_owned(),
   2384             disposition: "import_unique_behavior_then_retire".to_owned(),
   2385         }];
   2386 
   2387         validate_retired_import_targets(root.path(), &path_maps).expect("absent retired import");
   2388         fs::create_dir_all(root.path().join(target)).expect("create retired import fixture");
   2389         assert!(validate_retired_import_targets(root.path(), &path_maps).is_err());
   2390     }
   2391 
   2392     #[test]
   2393     fn step_ranges_must_cover_every_step_once() {
   2394         let valid = StepMap {
   2395             schema_version: 1,
   2396             map_id: STEP_MAP_ID.to_owned(),
   2397             source_step_count: EXPECTED_HANDOFF_STEPS,
   2398             source_sequence: "implementation/COMMIT_SEQUENCE.md".to_owned(),
   2399             range: vec![StepRange {
   2400                 start: 1,
   2401                 end: EXPECTED_HANDOFF_STEPS,
   2402                 owners: vec!["rcld-rlc-010".to_owned()],
   2403                 disposition: "execute".to_owned(),
   2404                 reason: "fixture".to_owned(),
   2405             }],
   2406         };
   2407         validate_step_map(&valid).expect("complete map");
   2408 
   2409         let mut gapped = valid;
   2410         gapped.range[0].start = 2;
   2411         assert!(validate_step_map(&gapped).is_err());
   2412     }
   2413 
   2414     #[test]
   2415     fn archive_names_and_digests_are_strict() {
   2416         assert!(validate_artifact_name("sdk-0123.bundle").is_ok());
   2417         assert!(validate_artifact_name("../sdk.bundle").is_err());
   2418         assert!(validate_artifact_name("sdk/bundle").is_err());
   2419         assert!(validate_sha256(&"a".repeat(64), "digest").is_ok());
   2420         assert!(validate_sha256(&"A".repeat(64), "digest").is_err());
   2421     }
   2422 
   2423     #[test]
   2424     fn merge_bearing_history_rehearsal_is_green() {
   2425         run_history_rehearsal().expect("history rewrite rehearsal");
   2426     }
   2427 }