use crate::core::config::{Language, ResolvedCrateConfig};
use anyhow::Context as _;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
const UNFILTERED_TS_ROOTS: [&str; 2] = ["packages/wasm", "packages/typescript"];
pub fn generate_sweep_roots(
languages: &[Language],
filtered: bool,
config: &ResolvedCrateConfig,
base_dir: &Path,
) -> Vec<PathBuf> {
let mut roots: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::new();
for &lang in languages {
roots.insert(base_dir.join(config.package_dir(lang)));
if let Some(out) = config.output_for(&lang.to_string()) {
roots.insert(base_dir.join(out));
}
}
if !filtered {
for root in UNFILTERED_TS_ROOTS {
roots.insert(base_dir.join(root));
}
}
roots.into_iter().collect()
}
pub fn targeted_e2e_sweep_roots(
output_paths: &[PathBuf],
e2e_output_root: &Path,
snippet_output_root: Option<&Path>,
) -> Vec<PathBuf> {
let mut roots = std::collections::BTreeSet::new();
for path in output_paths {
if snippet_output_root.is_some_and(|snippet_root| path.starts_with(snippet_root)) {
continue;
}
if let Ok(relative) = path.strip_prefix(e2e_output_root) {
let mut components = relative.components();
let Some(language) = components.next() else {
continue;
};
let Some(owned_subtree) = components.next() else {
continue;
};
if components.next().is_none() {
continue;
}
roots.insert(
e2e_output_root
.join(language.as_os_str())
.join(owned_subtree.as_os_str()),
);
}
}
roots.into_iter().collect()
}
pub fn sweep_orphans(
roots: &[std::path::PathBuf],
keep: &std::collections::HashSet<std::path::PathBuf>,
) -> anyhow::Result<usize> {
let mut removed = 0usize;
let mut touched_dirs: std::collections::BTreeSet<std::path::PathBuf> = std::collections::BTreeSet::new();
for root in roots {
if !root.exists() {
continue;
}
let mut stack = vec![root.clone()];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(it) => it,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if matches!(
name,
".git"
| "target"
| "node_modules"
| "vendor"
| "_build"
| "deps"
| ".venv"
| "venv"
| "build"
| "dist"
| "Pods"
) {
continue;
}
stack.push(path);
continue;
}
if !file_type.is_file() {
continue;
}
if keep.contains(&path) {
continue;
}
if !path_is_alef_owned(&path) {
continue;
}
if let Err(err) = std::fs::remove_file(&path) {
debug!(" sweep skip (remove failed): {} ({err})", path.display());
continue;
}
debug!(" swept orphan: {}", path.display());
if let Some(parent) = path.parent() {
touched_dirs.insert(parent.to_path_buf());
}
removed += 1;
}
}
}
let mut dirs: Vec<_> = touched_dirs.into_iter().collect();
dirs.sort_by_key(|p| std::cmp::Reverse(p.components().count()));
for dir in dirs {
let _ = std::fs::remove_dir(&dir);
}
if removed > 0 {
info!("Swept {removed} orphan generated file(s)");
}
Ok(removed)
}
pub fn sweep_manifest_orphans(
previous_paths: &[PathBuf],
keep: &std::collections::HashSet<PathBuf>,
allowed_roots: &[PathBuf],
disk_scan_roots: &[PathBuf],
) -> anyhow::Result<usize> {
let mut removed = 0;
for path in previous_paths {
if keep.contains(path) || !allowed_roots.iter().any(|root| path.starts_with(root)) || !path.is_file() {
continue;
}
if !path_is_reclaimable(path) {
continue;
}
std::fs::remove_file(path).with_context(|| format!("failed to remove orphan {}", path.display()))?;
debug!(" swept manifest orphan: {}", path.display());
removed += 1;
removed += reclaim_lockfile_siblings(path, keep)?;
}
for root in disk_scan_roots {
if !root.exists() {
continue;
}
let manifest_entries_under_root = previous_paths.iter().filter(|path| path.starts_with(root)).count();
let keep_entries_under_root = keep.iter().filter(|path| path.starts_with(root)).count();
if manifest_entries_under_root == 0 || keep_entries_under_root == 0 {
tracing::warn!(
"disk-scan orphan reclaim skipped for {}: {manifest_entries_under_root} manifest entry(s) and \
{keep_entries_under_root} keep entry(s) recorded under this root -- a backend whose own \
bookkeeping has never vouched for anything under its output root cannot be trusted to tell a \
real orphan from output it simply never recorded; a stale file here is left in place until that \
backend's own path tracking is fixed",
root.display()
);
continue;
}
let Some(tracked) = git_tracked_paths_under(root) else {
tracing::warn!(
"disk-scan orphan reclaim skipped for {}: could not determine which files under this root are \
git-tracked (not inside a git work tree, or `git` is unavailable) -- an alef marker alone cannot \
distinguish real generated output from a build tool's staged copy of it, so tracked-ness is \
required before disk-scan deletion is safe",
root.display()
);
continue;
};
let candidates: Vec<_> = collect_alef_headered_paths(root)
.into_iter()
.filter(|path| !keep.contains(path) && tracked.contains(path) && path_is_alef_owned(path))
.collect();
if !candidates.is_empty() {
report_disk_scan_candidates(root, &candidates);
}
}
if removed > 0 {
info!("Swept {removed} manifest orphan(s)");
}
Ok(removed)
}
fn path_is_reclaimable(path: &Path) -> bool {
let has_marker = std::fs::read_to_string(path).is_ok_and(|content| content_is_alef_owned(&content));
has_marker || is_unmarkable_alef_manifest(path)
}
const UNMARKABLE_ALEF_MANIFESTS: &[&str] = &["composer.json", "package.json"];
pub(super) fn is_unmarkable_alef_manifest(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| UNMARKABLE_ALEF_MANIFESTS.contains(&name))
}
const MANIFEST_LOCKFILE_SIBLINGS: &[(&str, &[&str])] = &[
("composer.json", &["composer.lock"]),
("package.json", &["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]),
];
fn reclaim_lockfile_siblings(manifest_path: &Path, keep: &std::collections::HashSet<PathBuf>) -> anyhow::Result<usize> {
let Some(manifest_name) = manifest_path.file_name().and_then(|name| name.to_str()) else {
return Ok(0);
};
let Some(dir) = manifest_path.parent() else {
return Ok(0);
};
let mut removed = 0;
for entry in MANIFEST_LOCKFILE_SIBLINGS {
if entry.0 != manifest_name {
continue;
}
for &lockfile_name in entry.1 {
let lockfile_path = dir.join(lockfile_name);
if keep.contains(&lockfile_path) || !lockfile_path.is_file() {
continue;
}
std::fs::remove_file(&lockfile_path)
.with_context(|| format!("failed to remove orphan lockfile {}", lockfile_path.display()))?;
removed += 1;
}
}
Ok(removed)
}
pub fn collect_alef_headered_paths(root: &std::path::Path) -> std::collections::HashSet<std::path::PathBuf> {
fn is_alef_owned(path: &std::path::Path) -> bool {
let Ok(content) = std::fs::read_to_string(path) else {
return false;
};
crate::core::hash::content_has_alef_marker(&content)
}
let mut paths = std::collections::HashSet::new();
if !root.exists() {
return paths;
}
let visible = crate::cli::git::IgnoreFilter::for_root(root);
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(it) => it,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(ft) = entry.file_type() else { continue };
if ft.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if matches!(
name,
".git"
| "target"
| "node_modules"
| "vendor"
| "_build"
| "deps"
| ".venv"
| "venv"
| "build"
| "dist"
| "Pods"
) || !visible.allows(&path)
{
continue;
}
stack.push(path);
} else if ft.is_file() && visible.allows(&path) && is_alef_owned(&path) {
paths.insert(path);
}
}
}
paths
}
fn path_is_alef_owned(path: &Path) -> bool {
std::fs::read_to_string(path).is_ok_and(|content| content_is_alef_owned(&content))
}
fn report_disk_scan_candidates(root: &Path, candidates: &[PathBuf]) {
tracing::warn!(
"{} alef-marked, git-tracked file(s) under {} were not emitted by this run. These are NOT \
deleted: absence from a run's output does not prove a file is an orphan (the emitter may \
have stopped emitting it, failed to emit it, or emit it only when absent). Review each and \
remove by hand if genuinely stale:",
candidates.len(),
root.display()
);
for path in candidates {
tracing::warn!(" unemitted alef-marked file: {}", path.display());
}
}
fn git_tracked_paths_under(root: &Path) -> Option<std::collections::HashSet<PathBuf>> {
crate::cli::git::tracked_paths_under(root)
}
fn content_is_alef_owned(content: &str) -> bool {
crate::core::hash::content_has_alef_marker(content) && crate::core::hash::extract_hash(content).is_some()
}
#[cfg(test)]
mod sweep_roots_tests {
use super::*;
use crate::test_support::{git_add, git_init, write_file};
fn config() -> ResolvedCrateConfig {
ResolvedCrateConfig {
name: "sample".to_string(),
..ResolvedCrateConfig::default()
}
}
#[test]
fn filtered_run_excludes_other_languages_and_ts_fallback_roots() {
let base = Path::new("/base");
let roots = generate_sweep_roots(&[Language::Python], true, &config(), base);
assert!(
roots.contains(&base.join("packages/python")),
"must keep the requested language"
);
assert!(
!roots.contains(&base.join("packages/ruby")),
"must not touch an unrequested language"
);
assert!(
!roots.contains(&base.join("packages/wasm")),
"must not add the wasm fallback root on a filtered run"
);
assert!(
!roots.contains(&base.join("packages/typescript")),
"must not add the typescript fallback root on a filtered run"
);
}
#[test]
fn unfiltered_run_includes_all_languages_and_ts_fallback_roots() {
let base = Path::new("/base");
let roots = generate_sweep_roots(&[Language::Python, Language::Ruby], false, &config(), base);
assert!(roots.contains(&base.join("packages/python")));
assert!(roots.contains(&base.join("packages/ruby")));
assert!(
roots.contains(&base.join("packages/wasm")),
"unfiltered run keeps the wasm fallback root"
);
assert!(
roots.contains(&base.join("packages/typescript")),
"unfiltered run keeps the typescript fallback root"
);
}
#[test]
fn manifest_sweep_removes_only_prior_managed_orphans_in_selected_root() {
let dir = tempfile::tempdir().expect("tempdir");
let selected = dir.path().join("python");
let other = dir.path().join("ruby");
std::fs::create_dir_all(&selected).expect("selected root");
std::fs::create_dir_all(&other).expect("other root");
let managed = selected.join("orphan.py");
let handwritten = selected.join("notes.py");
let unselected = other.join("orphan.rb");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&managed, &hashed).expect("managed");
std::fs::write(&handwritten, "handwritten\n").expect("handwritten");
std::fs::write(&unselected, &hashed).expect("unselected");
let previous = vec![managed.clone(), handwritten.clone(), unselected.clone()];
let removed = sweep_manifest_orphans(&previous, &std::collections::HashSet::new(), &[selected], &[])
.expect("manifest sweep");
assert_eq!(removed, 1);
assert!(!managed.exists());
assert!(handwritten.exists());
assert!(unselected.exists());
}
#[test]
fn manifest_sweep_preserves_non_header_generator_manifests() {
let dir = tempfile::tempdir().expect("tempdir");
let selected = dir.path().join("swift");
let manifest = selected.join("rust/Cargo.toml");
std::fs::create_dir_all(manifest.parent().expect("manifest parent")).expect("selected root");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&manifest, hashed).expect("manifest");
let previous = vec![manifest.clone()];
let keep = std::collections::HashSet::from([manifest.clone()]);
let removed = sweep_manifest_orphans(&previous, &keep, &[selected], &[]).expect("manifest sweep");
assert_eq!(removed, 0);
assert!(manifest.exists());
}
#[test]
fn manifest_sweep_reclaims_unmarkable_manifest_absent_from_current_run() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/php");
std::fs::create_dir_all(&package_dir).expect("package dir");
let composer_json = package_dir.join("composer.json");
std::fs::write(&composer_json, "{\n \"name\": \"acme/demo\"\n}\n").expect("composer.json");
let previous = vec![composer_json.clone()];
let removed = sweep_manifest_orphans(&previous, &std::collections::HashSet::new(), &[package_dir], &[])
.expect("manifest sweep");
assert_eq!(removed, 1, "unmarkable manifest absent from this run must be reclaimed");
assert!(!composer_json.exists());
}
#[test]
fn manifest_sweep_preserves_unmarkable_manifest_still_written_this_run() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/php");
std::fs::create_dir_all(&package_dir).expect("package dir");
let composer_json = package_dir.join("composer.json");
std::fs::write(&composer_json, "{\n \"name\": \"acme/demo\"\n}\n").expect("composer.json");
let previous = vec![composer_json.clone()];
let keep = std::collections::HashSet::from([composer_json.clone()]);
let removed = sweep_manifest_orphans(&previous, &keep, &[package_dir], &[]).expect("manifest sweep");
assert_eq!(removed, 0, "manifest still written this run must survive");
assert!(composer_json.exists());
}
#[test]
fn manifest_sweep_reclaims_lockfile_sibling_of_reclaimed_manifest() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/php");
std::fs::create_dir_all(&package_dir).expect("package dir");
let composer_json = package_dir.join("composer.json");
let composer_lock = package_dir.join("composer.lock");
std::fs::write(&composer_json, "{\n \"name\": \"acme/demo\"\n}\n").expect("composer.json");
std::fs::write(&composer_lock, "{\n \"_readme\": []\n}\n").expect("composer.lock");
let previous = vec![composer_json.clone()];
let removed = sweep_manifest_orphans(&previous, &std::collections::HashSet::new(), &[package_dir], &[])
.expect("manifest sweep");
assert_eq!(
removed, 2,
"both the manifest and its lockfile sibling must be reclaimed"
);
assert!(!composer_json.exists());
assert!(
!composer_lock.exists(),
"orphaned lockfile must be reclaimed alongside its manifest"
);
}
#[test]
fn manifest_sweep_preserves_hand_authored_lockfile_alef_never_emitted() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/php");
std::fs::create_dir_all(&package_dir).expect("package dir");
let hand_authored_lock = package_dir.join("composer.lock");
std::fs::write(&hand_authored_lock, "{\n \"_readme\": [\"hand rolled\"]\n}\n").expect("hand-authored lock");
let previous: Vec<PathBuf> = Vec::new();
let removed = sweep_manifest_orphans(&previous, &std::collections::HashSet::new(), &[package_dir], &[])
.expect("manifest sweep");
assert_eq!(
removed, 0,
"a lockfile with no reclaimed manifest sibling must never be touched"
);
assert!(
hand_authored_lock.exists(),
"hand-authored lockfile alef never emitted must survive"
);
}
#[test]
fn manifest_sweep_preserves_lockfile_when_its_manifest_is_still_kept() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/php");
std::fs::create_dir_all(&package_dir).expect("package dir");
let composer_json = package_dir.join("composer.json");
let composer_lock = package_dir.join("composer.lock");
std::fs::write(&composer_json, "{\n \"name\": \"acme/demo\"\n}\n").expect("composer.json");
std::fs::write(&composer_lock, "{\n \"_readme\": []\n}\n").expect("composer.lock");
let previous = vec![composer_json.clone()];
let keep = std::collections::HashSet::from([composer_json.clone()]);
let removed = sweep_manifest_orphans(&previous, &keep, &[package_dir], &[]).expect("manifest sweep");
assert_eq!(removed, 0);
assert!(composer_json.exists());
assert!(
composer_lock.exists(),
"lockfile must survive when its manifest is still written this run"
);
}
#[test]
fn manifest_sweep_reclaims_user_modified_unmarkable_manifest_at_disowned_path() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/php");
std::fs::create_dir_all(&package_dir).expect("package dir");
let composer_json = package_dir.join("composer.json");
std::fs::write(
&composer_json,
"{\n \"name\": \"acme/demo\",\n \"require\": {\n \"hand-added/package\": \"^9.9\"\n }\n}\n",
)
.expect("hand-edited composer.json");
let previous = vec![composer_json.clone()];
let removed = sweep_manifest_orphans(&previous, &std::collections::HashSet::new(), &[package_dir], &[])
.expect("manifest sweep");
assert_eq!(
removed, 1,
"hand-edited content at a disowned path is reclaimed, not preserved"
);
assert!(!composer_json.exists());
}
#[test]
fn manifest_sweep_does_not_widen_provenance_route_beyond_the_allowlist() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/dart");
std::fs::create_dir_all(&package_dir).expect("package dir");
let pubspec = package_dir.join("pubspec.yaml");
std::fs::write(&pubspec, "name: demo\nversion: 1.0.0\n").expect("pubspec.yaml");
let previous = vec![pubspec.clone()];
let removed = sweep_manifest_orphans(&previous, &std::collections::HashSet::new(), &[package_dir], &[])
.expect("manifest sweep");
assert_eq!(
removed, 0,
"a filename outside the allowlist must still require the marker check"
);
assert!(pubspec.exists());
}
#[test]
fn recursive_sweep_and_collection_ignore_hash_only_files() {
let dir = tempfile::tempdir().expect("tempdir");
let generated_root = dir.path().join("java");
std::fs::create_dir_all(&generated_root).expect("generated root");
let legacy_scaffold = generated_root.join("settings.gradle.kts");
std::fs::write(
&legacy_scaffold,
format!("// alef:hash:{}\nrootProject.name = \"sample\"\n", "0".repeat(64)),
)
.expect("legacy scaffold");
let removed = sweep_orphans(std::slice::from_ref(&generated_root), &std::collections::HashSet::new())
.expect("recursive sweep");
let collected = collect_alef_headered_paths(&generated_root);
assert_eq!(removed, 0);
assert!(legacy_scaffold.exists());
assert!(!collected.contains(&legacy_scaffold));
}
#[test]
fn manifest_sweep_disk_scan_reports_but_never_deletes_a_marked_file_absent_from_keep() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/kotlin_android");
std::fs::create_dir_all(&package_dir).expect("package dir");
git_init(&package_dir);
let kept = package_dir.join("Bridge.kt");
let orphan = package_dir.join("Language.kt");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::DoubleSlash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&kept, &hashed).expect("kept file");
std::fs::write(&orphan, &hashed).expect("orphan file");
git_add(&package_dir, &["Bridge.kt"]);
git_add(&package_dir, &["Language.kt"]);
let previous = vec![kept.clone()];
let keep = std::collections::HashSet::from([kept.clone()]);
let removed = sweep_manifest_orphans(
&previous,
&keep,
std::slice::from_ref(&package_dir),
std::slice::from_ref(&package_dir),
)
.expect("manifest sweep");
assert_eq!(removed, 0, "the disk-scan route must never delete");
assert!(
orphan.exists(),
"a marked, tracked file absent from keep must be REPORTED, not removed -- it may be a \
create-once seed or a file the emitter failed to emit this run"
);
assert!(kept.exists(), "the file still recorded in keep must survive");
}
#[test]
fn manifest_sweep_disk_scan_preserves_unmarked_tracked_file_absent_from_keep() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/kotlin_android");
std::fs::create_dir_all(&package_dir).expect("package dir");
git_init(&package_dir);
let kept = package_dir.join("Bridge.kt");
let handwritten = package_dir.join("Extensions.kt");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::DoubleSlash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&kept, &hashed).expect("kept file");
std::fs::write(&handwritten, "fun extra() {}\n").expect("handwritten file");
git_add(&package_dir, &["Bridge.kt"]);
git_add(&package_dir, &["Extensions.kt"]);
let previous = vec![kept.clone()];
let keep = std::collections::HashSet::from([kept.clone()]);
let removed = sweep_manifest_orphans(
&previous,
&keep,
std::slice::from_ref(&package_dir),
std::slice::from_ref(&package_dir),
)
.expect("manifest sweep");
assert_eq!(
removed, 0,
"a file with no alef marker must never be deleted by the disk-scan route"
);
assert!(handwritten.exists());
}
#[test]
fn manifest_sweep_disk_scan_preserves_tracked_marked_file_present_in_keep() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/kotlin_android");
std::fs::create_dir_all(&package_dir).expect("package dir");
git_init(&package_dir);
let kept = package_dir.join("Bridge.kt");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::DoubleSlash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&kept, &hashed).expect("kept file");
git_add(&package_dir, &["Bridge.kt"]);
let previous = vec![kept.clone()];
let keep = std::collections::HashSet::from([kept.clone()]);
let removed = sweep_manifest_orphans(
&previous,
&keep,
std::slice::from_ref(&package_dir),
std::slice::from_ref(&package_dir),
)
.expect("manifest sweep");
assert_eq!(removed, 0);
assert!(kept.exists());
}
#[test]
fn manifest_sweep_disk_scan_skips_root_with_degenerate_manifest_baseline() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/elixir");
std::fs::create_dir_all(&package_dir).expect("package dir");
git_init(&package_dir);
let orphan = package_dir.join("dropped.ex");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&orphan, &hashed).expect("orphan file");
git_add(&package_dir, &["dropped.ex"]);
let rust_source = dir.path().join("native/sample/src/lib.rs");
let previous = vec![rust_source.clone()];
let keep = std::collections::HashSet::from([rust_source]);
let removed = sweep_manifest_orphans(
&previous,
&keep,
std::slice::from_ref(&package_dir),
std::slice::from_ref(&package_dir),
)
.expect("manifest sweep");
assert_eq!(
removed, 0,
"a root with zero manifest/keep entries recorded under it must be skipped, not scanned"
);
assert!(orphan.exists());
}
#[test]
fn manifest_sweep_disk_scan_preserves_untracked_marked_file_absent_from_keep() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/ruby");
std::fs::create_dir_all(&package_dir).expect("package dir");
git_init(&package_dir);
let kept = package_dir.join("client.rb");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&kept, &hashed).expect("kept file");
git_add(&package_dir, &["client.rb"]);
let staged_dir = package_dir.join("tmp/ruby/stage/lib");
std::fs::create_dir_all(&staged_dir).expect("staged dir");
let staged_copy = staged_dir.join("client.rb");
std::fs::write(&staged_copy, &hashed).expect("staged copy");
let previous = vec![kept.clone()];
let keep = std::collections::HashSet::from([kept.clone()]);
let removed = sweep_manifest_orphans(
&previous,
&keep,
std::slice::from_ref(&package_dir),
std::slice::from_ref(&package_dir),
)
.expect("manifest sweep");
assert_eq!(
removed, 0,
"an untracked staged copy that merely inherits the marker must never be deleted"
);
assert!(staged_copy.exists());
assert!(kept.exists());
}
#[test]
fn collect_alef_headered_paths_refuses_git_ignored_copies_and_keeps_uncommitted_output() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/ruby");
std::fs::create_dir_all(&package_dir).expect("package dir");
git_init(&package_dir);
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
let marked = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
write_file(&package_dir, ".gitignore", "tmp/\n");
let tracked = write_file(&package_dir, "lib/client.rb", &marked);
git_add(&package_dir, &[".gitignore", "lib/client.rb"]);
let uncommitted = write_file(&package_dir, "lib/fresh.rb", &marked);
let staged_copy = write_file(&package_dir, "tmp/ruby/stage/lib/client.rb", &marked);
let collected = collect_alef_headered_paths(&package_dir);
assert!(
!collected.contains(&staged_copy),
"a gitignored gem-staging copy carries the same marker verbatim and must not be \
collected: {collected:?}"
);
assert!(
collected.contains(&tracked),
"the tracked original must still be collected, or this test passes by collecting \
nothing: {collected:?}"
);
assert!(
collected.contains(&uncommitted),
"generated output the consumer has not committed yet must still be collected, so the \
re-stamp pass can reach it: {collected:?}"
);
}
}