use std::fs;
use std::path::{Path, PathBuf};
const CACHE_DIR: &str = ".alef";
const PER_FILE_CACHE_NAME: &str = "sources_hash.cache";
pub fn read_alef_toml_bytes(config_path: &Path) -> Vec<u8> {
fs::read(config_path).unwrap_or_default()
}
pub fn sources_hash(sources: &[PathBuf]) -> anyhow::Result<String> {
let mut sorted: Vec<&PathBuf> = sources.iter().collect();
sorted.sort();
let memo = read_per_file_memo();
let mut current: Vec<(String, u64, u64)> = Vec::with_capacity(sorted.len());
let mut all_match = !memo.entries.is_empty() && memo.aggregate.is_some();
for source in &sorted {
let metadata =
fs::metadata(source).map_err(|e| anyhow::anyhow!("failed to stat source {}: {e}", source.display()))?;
let mtime_nanos = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let size = metadata.len();
let path_str = source.to_string_lossy().to_string();
if all_match {
match memo.entries.get(&path_str) {
Some((m, s)) if *m == mtime_nanos && *s == size => {}
_ => all_match = false,
}
}
current.push((path_str, mtime_nanos, size));
}
if all_match
&& current.len() == memo.entries.len()
&& let Some(agg) = memo.aggregate
{
return Ok(agg);
}
let aggregate = crate::core::hash::compute_sources_hash(sources)?;
let _ = write_per_file_memo(¤t, &aggregate);
Ok(aggregate)
}
struct PerFileMemo {
aggregate: Option<String>,
entries: std::collections::HashMap<String, (u64, u64)>,
}
fn read_per_file_memo() -> PerFileMemo {
let path = Path::new(CACHE_DIR).join(PER_FILE_CACHE_NAME);
let Ok(content) = fs::read_to_string(&path) else {
return PerFileMemo {
aggregate: None,
entries: std::collections::HashMap::new(),
};
};
let mut aggregate: Option<String> = None;
let mut entries = std::collections::HashMap::new();
for line in content.lines() {
if let Some(rest) = line.strip_prefix("aggregate\t") {
aggregate = Some(rest.to_string());
continue;
}
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() != 3 {
continue;
}
let mtime_nanos = parts[1].parse::<u64>().unwrap_or(0);
let size = parts[2].parse::<u64>().unwrap_or(0);
entries.insert(parts[0].to_string(), (mtime_nanos, size));
}
PerFileMemo { aggregate, entries }
}
fn write_per_file_memo(entries: &[(String, u64, u64)], aggregate: &str) -> anyhow::Result<()> {
let dir = Path::new(CACHE_DIR);
fs::create_dir_all(dir)?;
let mut content = format!("aggregate\t{aggregate}\n");
for (path, mtime, size) in entries {
content.push_str(&format!("{path}\t{mtime}\t{size}\n"));
}
fs::write(dir.join(PER_FILE_CACHE_NAME), content)?;
Ok(())
}
pub fn validate_cache_crate_name(crate_name: &str) -> anyhow::Result<()> {
if crate_name.contains('\0') {
anyhow::bail!("invalid crate name for cache: NUL byte not allowed in {crate_name:?}");
}
if crate_name.contains('/') || crate_name.contains('\\') {
anyhow::bail!("invalid crate name for cache: path separator not allowed in {crate_name:?}");
}
if crate_name == ".." || crate_name == "." {
anyhow::bail!("invalid crate name for cache: {crate_name:?} is not a valid crate name");
}
Ok(())
}
fn ir_cache_dir(crate_name: &str) -> PathBuf {
Path::new(CACHE_DIR).join(crate_name)
}
pub fn is_ir_cached(crate_name: &str, source_hash: &str) -> bool {
let dir = ir_cache_dir(crate_name);
let hash_path = dir.join("ir.hash");
let ir_path = dir.join("ir.json");
if !ir_path.exists() {
return false;
}
match fs::read_to_string(&hash_path) {
Ok(cached) => cached.trim() == source_hash,
Err(_) => false,
}
}
pub fn read_cached_ir(crate_name: &str) -> anyhow::Result<crate::core::ir::ApiSurface> {
let ir_path = ir_cache_dir(crate_name).join("ir.json");
let content = fs::read_to_string(&ir_path)?;
Ok(serde_json::from_str(&content)?)
}
pub fn write_ir_cache(crate_name: &str, api: &crate::core::ir::ApiSurface, source_hash: &str) -> anyhow::Result<()> {
let cache_dir = ir_cache_dir(crate_name);
fs::create_dir_all(&cache_dir)?;
fs::write(cache_dir.join("ir.json"), serde_json::to_string_pretty(api)?)?;
fs::write(cache_dir.join("ir.hash"), source_hash)?;
Ok(())
}
pub use crate::cli::cache_identity::{compute_lang_hash, compute_stage_hash};
fn hashes_dir(crate_name: &str) -> PathBuf {
ir_cache_dir(crate_name).join("hashes")
}
pub fn is_lang_cached(crate_name: &str, lang: &str, lang_hash: &str) -> bool {
let dir = hashes_dir(crate_name);
let hash_path = dir.join(format!("{lang}.hash"));
let manifest_path = dir.join(format!("{lang}.manifest"));
match fs::read_to_string(&hash_path) {
Ok(cached) => {
if cached.trim() != lang_hash {
return false;
}
outputs_exist(&manifest_path)
}
Err(_) => false,
}
}
pub fn write_lang_hash(crate_name: &str, lang: &str, lang_hash: &str, output_paths: &[PathBuf]) -> anyhow::Result<()> {
let dir = hashes_dir(crate_name);
fs::create_dir_all(&dir)?;
fs::write(dir.join(format!("{lang}.hash")), lang_hash)?;
write_manifest(&dir.join(format!("{lang}.manifest")), output_paths)?;
tracing::debug!(
crate_name,
lang,
paths = output_paths.len(),
"wrote language manifest via write_lang_hash"
);
Ok(())
}
pub fn write_lang_manifest(crate_name: &str, lang: &str, output_paths: &[PathBuf]) -> anyhow::Result<()> {
let dir = hashes_dir(crate_name);
fs::create_dir_all(&dir)?;
write_manifest(&dir.join(format!("{lang}.manifest")), output_paths)?;
tracing::debug!(
crate_name,
lang,
paths = output_paths.len(),
"wrote language manifest via write_lang_manifest"
);
Ok(())
}
pub fn read_lang_manifest(crate_name: &str, lang: &str) -> Vec<PathBuf> {
let manifest_path = hashes_dir(crate_name).join(format!("{lang}.manifest"));
match fs::read_to_string(manifest_path) {
Ok(content) => content
.lines()
.filter(|line| !line.is_empty())
.map(PathBuf::from)
.collect(),
Err(_) => Vec::new(),
}
}
pub fn write_scaffold_manifest(crate_name: &str, output_paths: &[PathBuf]) -> anyhow::Result<()> {
let dir = hashes_dir(crate_name);
fs::create_dir_all(&dir)?;
write_manifest(&dir.join("scaffold-ownership.manifest"), output_paths)
}
pub fn read_scaffold_manifest(crate_name: &str) -> Vec<PathBuf> {
let manifest_path = hashes_dir(crate_name).join("scaffold-ownership.manifest");
match fs::read_to_string(manifest_path) {
Ok(content) => content
.lines()
.filter(|line| !line.is_empty())
.map(PathBuf::from)
.collect(),
Err(_) => Vec::new(),
}
}
const OWNERSHIP_MANIFEST: &str = ".alef-ownership.toml";
const LEGACY_SCAFFOLD_OWNED_PATHS_MANIFEST: &str = "scaffold-owned-paths.manifest";
const OWNERSHIP_MANIFEST_HEADER: &str = "\
# alef ownership record -- COMMIT THIS FILE, do not add it to .gitignore.
#
# Lists the alef-generated paths whose format cannot carry an `alef:hash:`
# provenance marker (`package.json`, `*.jar`, ...). Every other format proves
# alef's ownership from the marker in the file itself and never appears here.
# Without this list committed, a fresh clone cannot tell an alef-generated
# `package.json` from a hand-written one and refuses to regenerate it.
#
# Ownership is a fact about history, not about content: a path lands here only
# because alef created the file, or because a human ran `alef adopt` on it.
# Nothing here is inferred by comparing bytes against generated output -- a
# hand-written file that happens to match must never be claimed. Do not hand-add
# entries; run `alef adopt <path>`, read the diff it prints, and let it write.
";
fn scaffold_owned_path_key(base_dir: &Path, path: &Path) -> String {
path.strip_prefix(base_dir)
.unwrap_or(path)
.to_string_lossy()
.into_owned()
}
#[derive(serde::Deserialize)]
struct OwnershipManifest {
#[serde(default)]
owned_paths: Vec<String>,
}
fn ownership_manifest_path(base_dir: &Path) -> PathBuf {
base_dir.join(OWNERSHIP_MANIFEST)
}
enum OwnedPathsRecord {
Absent,
Present(Vec<String>),
Unreadable(String),
}
fn read_owned_paths_record(base_dir: &Path) -> OwnedPathsRecord {
match fs::read_to_string(ownership_manifest_path(base_dir)) {
Ok(content) => match toml::from_str::<OwnershipManifest>(&content) {
Ok(manifest) => OwnedPathsRecord::Present(manifest.owned_paths),
Err(error) => OwnedPathsRecord::Unreadable(error.to_string()),
},
Err(error) if error.kind() == std::io::ErrorKind::NotFound => OwnedPathsRecord::Absent,
Err(error) => OwnedPathsRecord::Unreadable(error.to_string()),
}
}
fn read_committed_owned_paths(base_dir: &Path) -> Vec<String> {
match read_owned_paths_record(base_dir) {
OwnedPathsRecord::Present(paths) => paths,
OwnedPathsRecord::Absent => Vec::new(),
OwnedPathsRecord::Unreadable(reason) => {
tracing::warn!(
manifest = %OWNERSHIP_MANIFEST,
reason = %reason,
"the alef ownership record could not be read; treating every path in it as unowned, \
so writes to unmarkable files will be refused until it is repaired"
);
Vec::new()
}
}
}
fn read_legacy_owned_paths(base_dir: &Path) -> Vec<String> {
let manifest_path = base_dir.join(CACHE_DIR).join(LEGACY_SCAFFOLD_OWNED_PATHS_MANIFEST);
fs::read_to_string(manifest_path)
.map(|content| {
content
.lines()
.filter(|line| !line.is_empty())
.map(str::to_owned)
.collect()
})
.unwrap_or_default()
}
const RECORD_ARRAY_INDENT: &str = " ";
const RECORD_ARRAY_MAX_INLINE_WIDTH: usize = 120;
fn render_record_assignment(key: &str, values: &[String]) -> String {
let elements: Vec<String> = values
.iter()
.map(|value| toml_edit::Value::from(value.as_str()).to_string())
.collect();
let inline = format!("{key} = [{}]", elements.join(", "));
if inline.chars().count() <= RECORD_ARRAY_MAX_INLINE_WIDTH {
return inline;
}
let mut rendered = format!("{key} = [\n");
for element in &elements {
rendered.push_str(RECORD_ARRAY_INDENT);
rendered.push_str(element);
rendered.push_str(",\n");
}
rendered.push(']');
rendered
}
fn render_ownership_manifest(paths: &[String]) -> String {
format!(
"{OWNERSHIP_MANIFEST_HEADER}\n{}\n",
render_record_assignment("owned_paths", paths)
)
}
pub fn record_scaffold_owned_path(base_dir: &Path, path: &Path) -> anyhow::Result<()> {
record_scaffold_owned_paths(base_dir, std::slice::from_ref(&path))
}
pub fn record_scaffold_owned_paths(base_dir: &Path, paths: &[&Path]) -> anyhow::Result<()> {
static WRITE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = WRITE_LOCK.lock().unwrap_or_else(|error| error.into_inner());
if paths.is_empty() {
return Ok(());
}
fs::create_dir_all(base_dir)?;
let record = read_owned_paths_record(base_dir);
let is_new_manifest = matches!(record, OwnedPathsRecord::Absent);
let mut recorded: std::collections::BTreeSet<String> = match record {
OwnedPathsRecord::Present(paths) => paths.into_iter().collect(),
OwnedPathsRecord::Absent => std::collections::BTreeSet::new(),
OwnedPathsRecord::Unreadable(reason) => anyhow::bail!(
"refusing to update the alef ownership record at {}: it exists but could not be read \
({reason}). Repair or restore it (`git checkout -- {OWNERSHIP_MANIFEST}`) and re-run \
-- rewriting it from a state alef could not read would drop every path already recorded.",
ownership_manifest_path(base_dir).display()
),
};
let mut added = false;
for path in paths {
added |= recorded.insert(scaffold_owned_path_key(base_dir, path));
}
if !added {
return Ok(());
}
let ordered: Vec<String> = recorded.into_iter().collect();
fs::write(ownership_manifest_path(base_dir), render_ownership_manifest(&ordered))?;
if is_new_manifest {
tracing::info!(
manifest = %OWNERSHIP_MANIFEST,
"created the alef ownership record: commit it, or a fresh clone cannot regenerate \
the unmarkable files listed in it"
);
rearm_untracked_record_notice(base_dir);
}
note_untracked_required_records(base_dir);
Ok(())
}
pub fn is_scaffold_owned_path(base_dir: &Path, path: &Path) -> bool {
note_untracked_required_records(base_dir);
let key = scaffold_owned_path_key(base_dir, path);
read_committed_owned_paths(base_dir)
.iter()
.chain(read_legacy_owned_paths(base_dir).iter())
.any(|existing| *existing == key)
}
const ALEF_RESERVED_NAME_PREFIX: &str = ".alef-";
const ALEF_DERIVED_OUTPUT_NAMES: &[&str] = &[crate::e2e::snippets::COVERAGE_MANIFEST];
pub fn is_alef_derived_output(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(ALEF_RESERVED_NAME_PREFIX) && ALEF_DERIVED_OUTPUT_NAMES.contains(&name))
}
const REQUIRED_COMMITTED_RECORDS: &[&str] = &[OWNERSHIP_MANIFEST, TOML_MERGE_PROVENANCE_MANIFEST];
pub fn untracked_required_records(base_dir: &Path) -> Vec<&'static str> {
REQUIRED_COMMITTED_RECORDS
.iter()
.filter(|record| base_dir.join(record).is_file() && git_tracks(base_dir, record) == Some(false))
.copied()
.collect()
}
fn git_tracks(base_dir: &Path, relative: &str) -> Option<bool> {
let output = std::process::Command::new("git")
.arg("-C")
.arg(base_dir)
.args(["ls-files", "--error-unmatch", "--", relative])
.output()
.ok()?;
if output.status.success() {
return Some(true);
}
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not a git repository") || stderr.contains("this operation must be run in a work tree") {
return None;
}
Some(false)
}
static REPORTED_RECORD_TRACKING: std::sync::Mutex<Option<std::collections::BTreeSet<PathBuf>>> =
std::sync::Mutex::new(None);
fn note_untracked_required_records(base_dir: &Path) {
{
let mut reported = REPORTED_RECORD_TRACKING
.lock()
.unwrap_or_else(|error| error.into_inner());
let seen = reported.get_or_insert_with(std::collections::BTreeSet::new);
if !seen.insert(base_dir.to_path_buf()) {
return;
}
}
for record in untracked_required_records(base_dir) {
tracing::warn!(
manifest = %record,
"alef depends on `{record}` but git does not track it: this run's writes succeeded only \
because of a file no other checkout has. Run `git add {record}` and commit it, or a \
fresh clone and CI will refuse to regenerate everything it vouches for"
);
}
}
fn rearm_untracked_record_notice(base_dir: &Path) {
let mut reported = REPORTED_RECORD_TRACKING
.lock()
.unwrap_or_else(|error| error.into_inner());
if let Some(seen) = reported.as_mut() {
seen.remove(base_dir);
}
}
const TOML_MERGE_PROVENANCE_MANIFEST: &str = ".alef-toml-merge-provenance.toml";
const TOML_MERGE_PROVENANCE_HEADER: &str = "\
# alef toml-merge provenance record -- COMMIT THIS FILE, do not add it to .gitignore.
#
# Records, per merge target and key path, the array values alef itself generated on
# the most recent `alef generate` run -- the baseline the poly.toml merge's prune step
# diffs against to tell \"alef proposed this and later stopped\" from \"the consumer
# wrote this by hand.\" Without this file committed, a fresh clone has no baseline, so
# a value alef stops generating can never be pruned there -- it accumulates forever.
#
# Nothing here is inferred by comparing bytes -- an entry is only ever a copy of
# alef's own past `generated` output for the given key path, captured before merging
# with consumer content. Do not hand-edit; it is rewritten on every `alef generate`.
";
#[derive(serde::Deserialize)]
struct TomlMergeProvenanceEntry {
relative_path: String,
key_path: String,
values: Vec<String>,
}
#[derive(Default, serde::Deserialize)]
struct TomlMergeProvenanceFile {
#[serde(default)]
entries: Vec<TomlMergeProvenanceEntry>,
}
type TomlMergeProvenance = std::collections::BTreeMap<String, std::collections::BTreeMap<String, Vec<String>>>;
fn toml_merge_provenance_path(base_dir: &Path) -> PathBuf {
base_dir.join(TOML_MERGE_PROVENANCE_MANIFEST)
}
fn read_toml_merge_provenance_file(base_dir: &Path) -> TomlMergeProvenance {
let Ok(content) = fs::read_to_string(toml_merge_provenance_path(base_dir)) else {
return TomlMergeProvenance::new();
};
let Ok(parsed) = toml::from_str::<TomlMergeProvenanceFile>(&content) else {
return TomlMergeProvenance::new();
};
let mut all = TomlMergeProvenance::new();
for entry in parsed.entries {
all.entry(entry.relative_path)
.or_default()
.insert(entry.key_path, entry.values);
}
all
}
pub fn read_toml_merge_provenance(
base_dir: &Path,
relative_path: &Path,
) -> std::collections::BTreeMap<String, Vec<String>> {
read_toml_merge_provenance_file(base_dir)
.remove(&relative_path.to_string_lossy().into_owned())
.unwrap_or_default()
}
fn render_toml_merge_provenance(entries: &[TomlMergeProvenanceEntry]) -> String {
let mut body = String::new();
for entry in entries {
if !body.is_empty() {
body.push('\n');
}
body.push_str("[[entries]]\n");
for (key, value) in [("relative_path", &entry.relative_path), ("key_path", &entry.key_path)] {
body.push_str(key);
body.push_str(" = ");
body.push_str(&toml_edit::Value::from(value.as_str()).to_string());
body.push('\n');
}
body.push_str(&render_record_assignment("values", &entry.values));
body.push('\n');
}
body
}
pub fn write_toml_merge_provenance(
base_dir: &Path,
relative_path: &Path,
arrays_by_key_path: &std::collections::BTreeMap<String, Vec<String>>,
) -> anyhow::Result<()> {
let manifest_path = toml_merge_provenance_path(base_dir);
let is_new_manifest = !manifest_path.exists();
let mut all = read_toml_merge_provenance_file(base_dir);
all.insert(relative_path.to_string_lossy().into_owned(), arrays_by_key_path.clone());
let entries: Vec<TomlMergeProvenanceEntry> = all
.into_iter()
.flat_map(|(relative_path, by_key_path)| {
by_key_path
.into_iter()
.map(move |(key_path, values)| TomlMergeProvenanceEntry {
relative_path: relative_path.clone(),
key_path,
values,
})
})
.collect();
fs::create_dir_all(base_dir)?;
let body = render_toml_merge_provenance(&entries);
fs::write(&manifest_path, format!("{TOML_MERGE_PROVENANCE_HEADER}\n{body}"))?;
if is_new_manifest {
tracing::info!(
manifest = %TOML_MERGE_PROVENANCE_MANIFEST,
"created the alef toml-merge provenance record: commit it, or a fresh clone can never \
prune a value alef stops generating"
);
}
Ok(())
}
pub fn is_stage_cached(crate_name: &str, stage: &str, stage_hash: &str) -> bool {
let dir = hashes_dir(crate_name);
let hash_path = dir.join(format!("{stage}.hash"));
let manifest_path = dir.join(format!("{stage}.manifest"));
match fs::read_to_string(&hash_path) {
Ok(cached) => {
if cached.trim() != stage_hash {
return false;
}
outputs_exist(&manifest_path)
}
Err(_) => false,
}
}
pub fn read_stage_paths(crate_name: &str, stage: &str) -> Vec<PathBuf> {
let dir = hashes_dir(crate_name);
let manifest_path = dir.join(format!("{stage}.manifest"));
match fs::read_to_string(&manifest_path) {
Ok(content) => content
.lines()
.filter(|line| !line.is_empty())
.map(PathBuf::from)
.collect(),
Err(_) => Vec::new(),
}
}
pub fn write_stage_hash(
crate_name: &str,
stage: &str,
stage_hash: &str,
output_paths: &[PathBuf],
) -> anyhow::Result<()> {
let dir = hashes_dir(crate_name);
fs::create_dir_all(&dir)?;
fs::write(dir.join(format!("{stage}.hash")), stage_hash)?;
write_manifest(&dir.join(format!("{stage}.manifest")), output_paths)?;
Ok(())
}
fn write_manifest(manifest_path: &Path, output_paths: &[PathBuf]) -> anyhow::Result<()> {
let mut paths: Vec<_> = output_paths.iter().map(|p| p.to_string_lossy()).collect();
paths.sort_unstable();
paths.dedup();
let mut content = paths.join("\n");
if !content.is_empty() {
content.push('\n');
}
fs::write(manifest_path, content)?;
Ok(())
}
fn outputs_exist(manifest_path: &Path) -> bool {
match fs::read_to_string(manifest_path) {
Ok(content) => {
let mut paths = content.lines().filter(|line| !line.is_empty()).peekable();
paths.peek().is_some() && paths.all(|line| Path::new(line).exists())
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
tracing::debug!(
manifest = %manifest_path.display(),
"no output manifest recorded; treating the cache entry as a miss"
);
false
}
Err(error) => {
tracing::warn!(
manifest = %manifest_path.display(),
%error,
"output manifest exists but could not be read; treating the cache entry as a miss"
);
false
}
}
}
pub fn hash_directory(dir: &Path) -> anyhow::Result<Vec<u8>> {
let mut hasher = blake3::Hasher::new();
if dir.exists() {
let mut entries: Vec<_> = walkdir(dir)?;
entries.sort();
for path in entries {
let content = fs::read(&path)?;
hasher.update(path.to_string_lossy().as_bytes());
hasher.update(&content);
}
}
Ok(hasher.finalize().as_bytes().to_vec())
}
fn walkdir(dir: &Path) -> anyhow::Result<Vec<PathBuf>> {
let mut files = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
files.extend(walkdir(&path)?);
} else {
files.push(path);
}
}
Ok(files)
}
pub fn hash_content(content: &str) -> String {
blake3::hash(content.as_bytes()).to_hex().to_string()
}
pub fn write_generation_hashes(name: &str, hashes: &[(String, String)]) -> anyhow::Result<()> {
let dir = Path::new(CACHE_DIR).join("hashes");
fs::create_dir_all(&dir)?;
let lines: Vec<String> = hashes.iter().map(|(p, h)| format!("{p}\t{h}")).collect();
fs::write(dir.join(format!("{name}.output_hashes")), lines.join("\n"))?;
Ok(())
}
pub fn read_generation_hashes(name: &str) -> anyhow::Result<std::collections::HashMap<String, String>> {
let path = Path::new(CACHE_DIR)
.join("hashes")
.join(format!("{name}.output_hashes"));
let content = fs::read_to_string(&path)?;
Ok(content
.lines()
.filter(|l| !l.is_empty())
.filter_map(|l| l.split_once('\t'))
.map(|(p, h)| (p.to_string(), h.to_string()))
.collect())
}
pub fn clear_cache() -> anyhow::Result<()> {
let cache_dir = Path::new(CACHE_DIR);
if cache_dir.exists() {
fs::remove_dir_all(cache_dir)?;
}
Ok(())
}
pub fn show_status() {
let cache_dir = Path::new(CACHE_DIR);
if !cache_dir.exists() {
crate::bin_cli::output::line("No cache directory.");
return;
}
crate::bin_cli::output::line("Cache directory: .alef/");
let ir_path = cache_dir.join("ir.json");
if ir_path.exists() {
if let Ok(meta) = fs::metadata(&ir_path) {
crate::bin_cli::output::line(format!(" ir.json: {} bytes", meta.len()));
}
} else {
crate::bin_cli::output::line(" ir.json: not cached");
}
let hashes_dir = cache_dir.join("hashes");
if hashes_dir.exists() {
if let Ok(entries) = fs::read_dir(&hashes_dir) {
let langs: Vec<String> = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.path().file_stem().and_then(|s| s.to_str().map(String::from)))
.collect();
if langs.is_empty() {
crate::bin_cli::output::line(" language hashes: none");
} else {
crate::bin_cli::output::line(format!(" language hashes: {}", langs.join(", ")));
}
}
} else {
crate::bin_cli::output::line(" language hashes: none");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn api_with_ordered_entries(entries: &[(&str, &str)]) -> crate::core::ir::ApiSurface {
let mut api = crate::core::ir::ApiSurface {
crate_name: "sample_crate".to_string(),
..Default::default()
};
for (name, path) in entries {
api.excluded_type_paths.insert((*name).to_string(), (*path).to_string());
api.excluded_trait_names.insert((*name).to_string());
}
api
}
#[test]
fn validate_cache_crate_name_accepts_normal_names() {
validate_cache_crate_name("my-lib").unwrap();
validate_cache_crate_name("sample_crate").unwrap();
validate_cache_crate_name("sample_markdown").unwrap();
}
#[test]
fn validate_cache_crate_name_rejects_path_separators() {
assert!(validate_cache_crate_name("../escape").is_err());
assert!(validate_cache_crate_name("foo/bar").is_err());
assert!(validate_cache_crate_name("foo\\bar").is_err());
}
#[test]
fn validate_cache_crate_name_rejects_dot_aliases() {
assert!(validate_cache_crate_name("..").is_err());
assert!(validate_cache_crate_name(".").is_err());
}
#[test]
fn validate_cache_crate_name_rejects_nul_byte() {
assert!(validate_cache_crate_name("foo\0bar").is_err());
}
#[test]
fn ir_cache_dir_scopes_by_crate_name() {
assert_eq!(ir_cache_dir("crate-a"), Path::new(CACHE_DIR).join("crate-a"));
assert_eq!(ir_cache_dir("crate-b"), Path::new(CACHE_DIR).join("crate-b"));
assert_ne!(ir_cache_dir("crate-a"), ir_cache_dir("crate-b"));
}
#[test]
fn repeated_ir_serialization_preserves_cache_and_provenance_hashes() {
let first = api_with_ordered_entries(&[
("Gamma", "sample_crate::gamma::Gamma"),
("Alpha", "sample_crate::alpha::Alpha"),
("Beta", "sample_crate::beta::Beta"),
]);
let second = api_with_ordered_entries(&[
("Beta", "sample_crate::beta::Beta"),
("Gamma", "sample_crate::gamma::Gamma"),
("Alpha", "sample_crate::alpha::Alpha"),
]);
let first_json = serde_json::to_string_pretty(&first).expect("serialize first IR");
let second_json = serde_json::to_string_pretty(&second).expect("serialize second IR");
let generated = "// auto-generated by alef\npub fn sample() {}\n";
let first_cache_hash = compute_lang_hash(&first_json, "sample", "[sample]\n");
let second_cache_hash = compute_lang_hash(&second_json, "sample", "[sample]\n");
let first_file_hash = crate::core::hash::compute_file_hash(&first_cache_hash, generated);
let second_file_hash = crate::core::hash::compute_file_hash(&second_cache_hash, generated);
assert_eq!(first_json, second_json);
assert_eq!(first_cache_hash, second_cache_hash);
assert_eq!(first_file_hash, second_file_hash);
assert_eq!(
crate::core::hash::inject_hash_line(generated, &first_file_hash),
crate::core::hash::inject_hash_line(generated, &second_file_hash)
);
}
#[test]
fn manifest_is_sorted_deduplicated_and_newline_terminated() {
let directory = tempfile::tempdir().expect("tempdir");
let manifest = directory.path().join("rust.manifest");
let alpha = directory.path().join("alpha.rs");
let beta = directory.path().join("beta.rs");
write_manifest(&manifest, &[beta.clone(), alpha.clone(), beta.clone()]).expect("write manifest");
let content = std::fs::read_to_string(manifest).expect("read manifest");
assert_eq!(content, format!("{}\n{}\n", alpha.display(), beta.display()));
}
#[test]
fn empty_manifest_is_not_a_cache_hit() {
let directory = tempfile::tempdir().expect("tempdir");
let manifest = directory.path().join("rust.manifest");
std::fs::write(&manifest, "").expect("write empty manifest");
assert!(!outputs_exist(&manifest));
}
#[test]
fn unreadable_output_manifest_is_a_cache_miss() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let generated = tmp.path().join("bindings.py");
std::fs::write(&generated, "# generated\n").expect("write generated output");
write_lang_hash("sample-crate", "python", "hash-1", &[generated]).expect("write hash and manifest");
assert!(
is_lang_cached("sample-crate", "python", "hash-1"),
"a matching hash whose manifested outputs are all present must be a hit"
);
let manifest = hashes_dir("sample-crate").join("python.manifest");
std::fs::remove_file(&manifest).expect("remove the manifest, leaving the hash behind");
assert!(
!is_lang_cached("sample-crate", "python", "hash-1"),
"a hash with no manifest at all must not validate a cache hit"
);
std::fs::create_dir_all(&manifest).expect("put something unreadable where the manifest belongs");
assert!(
!is_lang_cached("sample-crate", "python", "hash-1"),
"a manifest that exists but cannot be read must not validate a cache hit either"
);
}
#[test]
fn a_deleted_recorded_output_downgrades_a_cache_hit_to_a_miss() {
struct Scenario {
name: &'static str,
stage: &'static str,
recorded_outputs: &'static [&'static str],
delete_output: Option<&'static str>,
query_hash: &'static str,
expect_hit: bool,
}
let scenarios = [
Scenario {
name: "matching hash with every recorded output present is a hit",
stage: "hit",
recorded_outputs: &["a.rs", "b.rs"],
delete_output: None,
query_hash: "hash-1",
expect_hit: true,
},
Scenario {
name: "matching hash with one recorded output deleted is a miss",
stage: "deleted-output",
recorded_outputs: &["a.rs", "b.rs"],
delete_output: Some("b.rs"),
query_hash: "hash-1",
expect_hit: false,
},
Scenario {
name: "non-matching input hash is a miss regardless of outputs",
stage: "stale-hash",
recorded_outputs: &["a.rs"],
delete_output: None,
query_hash: "hash-2",
expect_hit: false,
},
Scenario {
name: "empty recorded paths is a miss, not an automatic hit",
stage: "empty",
recorded_outputs: &[],
delete_output: None,
query_hash: "hash-1",
expect_hit: false,
},
];
for scenario in scenarios {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let outputs: Vec<PathBuf> = scenario
.recorded_outputs
.iter()
.map(|name| {
let path = tmp.path().join(name);
std::fs::write(&path, "// generated\n").expect("write generated output");
path
})
.collect();
write_stage_hash("sample-crate", scenario.stage, "hash-1", &outputs)
.expect("write stage hash and manifest");
if let Some(to_delete) = scenario.delete_output {
std::fs::remove_file(tmp.path().join(to_delete)).expect("delete recorded output");
}
assert_eq!(
is_stage_cached("sample-crate", scenario.stage, scenario.query_hash),
scenario.expect_hit,
"scenario `{}` expected hit={}",
scenario.name,
scenario.expect_hit
);
}
}
#[test]
fn scaffold_manifest_round_trips_through_write_and_read() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let composer = tmp.path().join("packages/php/composer.json");
let cargo_toml = tmp.path().join("Cargo.toml");
let write_result = write_scaffold_manifest("sample-crate", &[composer.clone(), cargo_toml.clone()]);
let read_back = read_scaffold_manifest("sample-crate");
write_result.expect("write scaffold manifest");
assert_eq!(
read_back,
vec![cargo_toml, composer],
"manifest must round-trip both paths in sorted order"
);
}
#[test]
fn scaffold_manifest_reads_empty_when_never_written() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let read_back = read_scaffold_manifest("never-scaffolded-crate");
assert_eq!(read_back, Vec::<PathBuf>::new());
}
#[test]
fn scaffold_manifest_wiring_lets_next_run_reclaim_dropped_manifest() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let package_dir = tmp.path().join("packages/php");
std::fs::create_dir_all(&package_dir).expect("create package dir");
let composer_json = package_dir.join("composer.json");
std::fs::write(&composer_json, "{\n \"name\": \"acme/demo\"\n}\n").expect("write composer.json");
write_scaffold_manifest("sample-php", std::slice::from_ref(&composer_json)).expect("write manifest for run 1");
let previous_scaffold = read_scaffold_manifest("sample-php");
let keep = std::collections::HashSet::new();
let removed = crate::cli::pipeline::sweep_manifest_orphans(&previous_scaffold, &keep, &[package_dir], &[])
.expect("sweep");
assert_eq!(
removed, 1,
"composer.json recorded by run 1's manifest must be reclaimed in run 2"
);
assert!(!composer_json.exists(), "orphaned composer.json must be deleted");
}
#[test]
fn all_bindings_ownership_baseline_survives_the_lang_manifest_collision_that_used_to_erase_it() {
let tmp = tempfile::tempdir().expect("tempdir");
let dropped_type_file = tmp.path().join("packages/python/dropped_type.py");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let result = (|| -> anyhow::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
write_stage_hash(
"sample",
"all-bindings-python-ownership",
"sources-hash-n-minus-1",
std::slice::from_ref(&dropped_type_file),
)?;
write_lang_hash("sample", "python", "lang-hash-n", &[])?;
let dedicated_baseline = read_stage_paths("sample", "all-bindings-python-ownership");
let lang_manifest = read_lang_manifest("sample", "python");
Ok((dedicated_baseline, lang_manifest))
})();
let (dedicated_baseline, lang_manifest) = result.expect("baseline read");
assert_eq!(
dedicated_baseline,
vec![dropped_type_file],
"the dedicated ownership stage must still report last run's file list, unaffected by \
`write_lang_hash` overwriting the unrelated `<lang>.manifest` file"
);
assert!(
lang_manifest.is_empty(),
"`<lang>.manifest` itself is expected to have been overwritten by `write_lang_hash` -- \
that overwrite is legitimate cache-invalidation behaviour; the fix is to stop reading \
this file as the sweep baseline, not to change what it stores"
);
}
#[test]
fn all_bindings_ownership_correct_baseline_sweeps_a_binding_this_run_no_longer_emits() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/python");
std::fs::create_dir_all(&package_dir).expect("create package dir");
let kept_file = package_dir.join("kept_type.py");
let dropped_file = package_dir.join("dropped_type.py");
std::fs::write(&kept_file, "kept\n").expect("write kept file");
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(&dropped_file, &hashed).expect("write dropped file");
let previous_paths = vec![kept_file.clone(), dropped_file.clone()];
let mut keep = std::collections::HashSet::new();
keep.insert(kept_file.clone());
let removed =
crate::cli::pipeline::sweep_manifest_orphans(&previous_paths, &keep, &[package_dir], &[]).expect("sweep");
assert_eq!(removed, 1, "exactly the dropped binding must be swept");
assert!(
!dropped_file.exists(),
"the binding this run no longer emits must be deleted"
);
assert!(
kept_file.exists(),
"a binding still in this run's keep set must survive"
);
}
#[test]
fn all_bindings_ownership_missing_baseline_sweeps_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/python");
std::fs::create_dir_all(&package_dir).expect("create package dir");
let untouched_file = package_dir.join("untouched_type.py");
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(&untouched_file, &hashed).expect("write file");
let previous_paths = read_stage_paths(
"crate-with-no-prior-all-bindings-ownership-record",
"all-bindings-python-ownership",
);
assert!(previous_paths.is_empty(), "a never-written stage must read back empty");
let keep = std::collections::HashSet::new();
let removed =
crate::cli::pipeline::sweep_manifest_orphans(&previous_paths, &keep, &[package_dir], &[]).expect("sweep");
assert_eq!(removed, 0, "a missing baseline must sweep nothing, never everything");
assert!(
untouched_file.exists(),
"a file must never be deleted on the strength of an absent baseline"
);
}
#[test]
fn all_bindings_ownership_never_owned_path_is_left_untouched_even_when_present_in_sweep_root() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let result = (|| -> anyhow::Result<(usize, bool, bool, bool)> {
let package_dir = tmp.path().join("packages/python");
std::fs::create_dir_all(&package_dir)?;
let owned_file = package_dir.join("owned_type.py");
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(&owned_file, &hashed)?;
let foreign_file = package_dir.join("hand_written.py");
std::fs::write(&foreign_file, "# never generated by alef\n")?;
write_stage_hash(
"sample",
"all-bindings-python-ownership",
"sources-hash",
std::slice::from_ref(&owned_file),
)?;
let previous_paths = read_stage_paths("sample", "all-bindings-python-ownership");
let leaked = previous_paths.iter().any(|path| path.ends_with("hand_written.py"));
let keep = std::collections::HashSet::new();
let removed = crate::cli::pipeline::sweep_manifest_orphans(&previous_paths, &keep, &[package_dir], &[])?;
Ok((removed, owned_file.exists(), foreign_file.exists(), leaked))
})();
let (removed, owned_exists, foreign_exists, leaked) = result.expect("sweep");
assert!(!leaked, "the never-owned file must not have leaked into the baseline");
assert_eq!(removed, 1, "only the recorded, owned path may be removed");
assert!(!owned_exists, "the recorded, no-longer-kept binding must be swept");
assert!(
foreign_exists,
"a path alef never recorded owning must survive the sweep"
);
}
#[test]
fn is_alef_derived_output_recognises_the_snippet_coverage_ledger() {
assert!(is_alef_derived_output(Path::new(
"docs-site/src/snippets-generated/.alef-snippet-coverage.json"
)));
assert!(is_alef_derived_output(Path::new(
crate::e2e::snippets::COVERAGE_MANIFEST
)));
}
#[test]
fn is_alef_derived_output_refuses_every_hand_growable_generated_path() {
for hand_growable in [
"packages/php/composer.json",
"packages/node/package.json",
"packages/java/pom.xml",
"packages/zig/build.zig",
"packages/zig/test/sample_core_test.zig",
"packages/dart/test/sample_core_test.dart",
"e2e/go/helpers_test.go",
] {
assert!(
!is_alef_derived_output(Path::new(hand_growable)),
"{hand_growable} is content a human grows: it must never be classified as derived output"
);
}
}
#[test]
fn is_alef_derived_output_requires_the_reserved_namespace_not_only_list_membership() {
for name in ALEF_DERIVED_OUTPUT_NAMES {
assert!(
name.starts_with(ALEF_RESERVED_NAME_PREFIX),
"{name} is registered as derived output but sits outside alef's reserved namespace, \
so the backstop silently disables it"
);
}
assert!(
!is_alef_derived_output(Path::new("docs/snippets/.alef-snippet-coverage.json.bak")),
"a name that merely contains the ledger's name must not match"
);
assert!(
!is_alef_derived_output(Path::new("docs/snippets/.alef-unregistered-state.json")),
"the reserved prefix alone is not enough: membership in the registry is still required"
);
}
fn init_git_work_tree(base_dir: &Path) -> Option<()> {
let status = std::process::Command::new("git")
.arg("-C")
.arg(base_dir)
.args(["init", "--quiet"])
.status()
.ok()?;
status.success().then_some(())
}
fn git_add(base_dir: &Path, relative: &str) {
let status = std::process::Command::new("git")
.arg("-C")
.arg(base_dir)
.args(["add", "--", relative])
.status()
.expect("git add");
assert!(status.success(), "git add {relative} failed");
}
#[test]
fn untracked_required_records_reports_a_record_git_does_not_track() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
if init_git_work_tree(base).is_none() {
return;
}
record_scaffold_owned_path(base, &base.join("packages/node/package.json")).expect("record");
assert_eq!(
untracked_required_records(base),
vec![OWNERSHIP_MANIFEST],
"a record alef just created and now depends on must be reported as untracked"
);
}
#[test]
fn untracked_required_records_is_silent_once_the_record_is_staged() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
if init_git_work_tree(base).is_none() {
return;
}
record_scaffold_owned_path(base, &base.join("packages/node/package.json")).expect("record");
git_add(base, OWNERSHIP_MANIFEST);
assert!(
untracked_required_records(base).is_empty(),
"a staged record is tracked; reporting it anyway trains the operator to ignore the warning"
);
}
#[test]
fn untracked_required_records_is_silent_outside_a_git_work_tree() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
record_scaffold_owned_path(base, &base.join("packages/node/package.json")).expect("record");
assert!(base.join(OWNERSHIP_MANIFEST).is_file(), "sanity: the record exists");
assert!(
untracked_required_records(base).is_empty(),
"with no repository to ask, tracked-ness is unanswerable and must not be reported as a fault"
);
}
#[test]
fn untracked_required_records_ignores_a_record_that_does_not_exist_yet() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
if init_git_work_tree(base).is_none() {
return;
}
assert!(untracked_required_records(base).is_empty());
}
#[test]
fn scaffold_owned_path_round_trips_and_is_idempotent() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
let target = base.join("packages/java/pom.xml");
assert!(!is_scaffold_owned_path(base, &target), "must start unrecorded");
record_scaffold_owned_path(base, &target).expect("record");
record_scaffold_owned_path(base, &target).expect("record again (idempotent)");
assert!(is_scaffold_owned_path(base, &target));
let manifest = std::fs::read_to_string(base.join(OWNERSHIP_MANIFEST)).expect("read manifest");
assert_eq!(
manifest.matches("packages/java/pom.xml").count(),
1,
"recording the same path twice must not duplicate it, got:\n{manifest}"
);
assert!(
!base.join(".alef").join(LEGACY_SCAFFOLD_OWNED_PATHS_MANIFEST).exists(),
"the gitignored legacy record must no longer be written, got:\n{manifest}"
);
}
#[test]
fn batch_recording_matches_per_path_recording_entry_for_entry() {
let batched = tempfile::tempdir().expect("tempdir");
let one_at_a_time = tempfile::tempdir().expect("tempdir");
let relatives = [
"docs/snippets/python/api/z.md",
"packages/node/package.json",
"docs/snippets/python/api/a.md",
"packages/java/pom.xml",
];
record_scaffold_owned_path(batched.path(), &batched.path().join("pre/existing.json")).expect("seed");
record_scaffold_owned_path(one_at_a_time.path(), &one_at_a_time.path().join("pre/existing.json"))
.expect("seed");
let joined: Vec<PathBuf> = relatives.iter().map(|rel| batched.path().join(rel)).collect();
let refs: Vec<&Path> = joined.iter().map(PathBuf::as_path).collect();
record_scaffold_owned_paths(batched.path(), &refs).expect("batch record");
record_scaffold_owned_paths(batched.path(), &refs).expect("batch record again (idempotent)");
for relative in relatives {
record_scaffold_owned_path(one_at_a_time.path(), &one_at_a_time.path().join(relative)).expect("record");
}
assert_eq!(
std::fs::read_to_string(batched.path().join(OWNERSHIP_MANIFEST)).expect("batched manifest"),
std::fs::read_to_string(one_at_a_time.path().join(OWNERSHIP_MANIFEST)).expect("sequential manifest"),
);
for relative in relatives {
assert!(is_scaffold_owned_path(batched.path(), &batched.path().join(relative)));
}
assert!(
is_scaffold_owned_path(batched.path(), &batched.path().join("pre/existing.json")),
"a batch must extend the record, never replace it"
);
}
#[test]
fn ownership_record_lives_outside_the_gitignored_cache_and_is_valid_toml() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
record_scaffold_owned_path(base, &base.join("packages/typescript/package.json")).expect("record");
let manifest_path = base.join(OWNERSHIP_MANIFEST);
assert!(manifest_path.exists(), "the record must exist at the repo root");
assert!(
!manifest_path.starts_with(base.join(CACHE_DIR)),
"the record must not live under the gitignored `{CACHE_DIR}` directory"
);
let content = std::fs::read_to_string(&manifest_path).expect("read manifest");
let parsed: OwnershipManifest = toml::from_str(&content).expect("the record must be valid TOML");
assert_eq!(parsed.owned_paths, vec!["packages/typescript/package.json".to_owned()]);
}
#[test]
fn committed_record_answers_identically_on_a_cache_less_clone() {
let warm = tempfile::tempdir().expect("tempdir warm");
let clone = tempfile::tempdir().expect("tempdir clone");
let relative = std::path::Path::new("packages/typescript/package.json");
record_scaffold_owned_path(warm.path(), &warm.path().join(relative)).expect("record");
std::fs::copy(
warm.path().join(OWNERSHIP_MANIFEST),
clone.path().join(OWNERSHIP_MANIFEST),
)
.expect("check out the committed record");
assert!(
!clone.path().join(CACHE_DIR).exists(),
"the simulated clone must have no machine-local cache"
);
assert!(
is_scaffold_owned_path(clone.path(), &clone.path().join(relative)),
"a fresh clone must agree with the warm machine about what alef owns"
);
}
#[test]
fn legacy_gitignored_record_is_still_honoured_for_reads() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
let relative = std::path::Path::new("packages/java/pom.xml");
std::fs::create_dir_all(base.join(CACHE_DIR)).expect("create legacy cache dir");
std::fs::write(
base.join(CACHE_DIR).join(LEGACY_SCAFFOLD_OWNED_PATHS_MANIFEST),
"packages/java/pom.xml\n",
)
.expect("seed legacy record");
assert!(!base.join(OWNERSHIP_MANIFEST).exists(), "no committed record yet");
assert!(is_scaffold_owned_path(base, &base.join(relative)));
}
#[test]
fn malformed_ownership_record_refuses_rather_than_dropping_recorded_paths() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
record_scaffold_owned_path(base, &base.join("packages/java/pom.xml")).expect("seed the record");
let manifest_path = base.join(OWNERSHIP_MANIFEST);
let seeded = std::fs::read_to_string(&manifest_path).expect("read the seeded record");
let corrupted = format!("{seeded}this line is not toml\n");
std::fs::write(&manifest_path, &corrupted).expect("hand-edit the record into invalid TOML");
let newly_scaffolded = base.join("packages/node/package.json");
let error = record_scaffold_owned_paths(base, &[newly_scaffolded.as_path()])
.expect_err("recording against an unreadable record must fail rather than rewrite it");
assert_eq!(
std::fs::read_to_string(&manifest_path).expect("read the record after the refusal"),
corrupted,
"the refused run must leave the record byte-identical, keeping every recorded path"
);
assert!(
error.to_string().contains(OWNERSHIP_MANIFEST),
"the failure must name the file the operator has to repair, got: {error}"
);
}
#[test]
fn unparseable_ownership_record_claims_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
std::fs::write(base.join(OWNERSHIP_MANIFEST), "this is not = = valid toml [[[").expect("write junk");
assert!(!is_scaffold_owned_path(
base,
&base.join("packages/typescript/package.json")
));
}
#[test]
fn ownership_record_header_does_not_read_as_a_provenance_marker() {
let rendered = render_ownership_manifest(&["packages/typescript/package.json".to_owned()]);
assert!(
!crate::core::hash::content_has_alef_marker(&rendered),
"the record's own header must not look like an alef provenance marker, got:\n{rendered}"
);
}
#[test]
fn ownership_record_escapes_paths_that_need_it() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
let awkward = "packages/we\"ird\\name.json";
record_scaffold_owned_path(base, &base.join(awkward)).expect("record");
record_scaffold_owned_path(base, &base.join("packages/plain.json")).expect("record plain");
let content = std::fs::read_to_string(base.join(OWNERSHIP_MANIFEST)).expect("read manifest");
let parsed: OwnershipManifest = toml::from_str(&content).expect("manifest must stay parseable");
assert!(
parsed.owned_paths.iter().any(|path| path == awkward),
"the awkward path must round-trip unchanged, got: {:?}",
parsed.owned_paths
);
assert!(is_scaffold_owned_path(base, &base.join(awkward)));
assert!(
is_scaffold_owned_path(base, &base.join("packages/plain.json")),
"a bad escape must not take the rest of the record down with it"
);
}
#[test]
fn scaffold_owned_path_is_scoped_to_base_dir() {
let dir_a = tempfile::tempdir().expect("tempdir a");
let dir_b = tempfile::tempdir().expect("tempdir b");
let target = std::path::PathBuf::from("packages/java/pom.xml");
record_scaffold_owned_path(dir_a.path(), &dir_a.path().join(&target)).expect("record in a");
assert!(!is_scaffold_owned_path(dir_b.path(), &dir_b.path().join(&target)));
}
#[test]
fn scaffold_owned_path_matches_across_absolute_and_relative_base_dir_spellings() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let absolute_base = std::env::current_dir().expect("absolute cwd");
let relative_base = Path::new(".");
let relative_target = relative_base.join("packages/java/pom.xml");
let result = (|| -> anyhow::Result<(bool, bool)> {
record_scaffold_owned_path(&absolute_base, &absolute_base.join("packages/java/pom.xml"))?;
let found_from_relative = is_scaffold_owned_path(relative_base, &relative_target);
record_scaffold_owned_path(relative_base, &relative_base.join("packages/csharp/foo.csproj"))?;
let found_from_absolute =
is_scaffold_owned_path(&absolute_base, &absolute_base.join("packages/csharp/foo.csproj"));
Ok((found_from_relative, found_from_absolute))
})();
let (found_from_relative, found_from_absolute) = result.expect("record/check round-trip");
assert!(
found_from_relative,
"a record written with an absolute base_dir must be found by a relative-base_dir lookup"
);
assert!(
found_from_absolute,
"a record written with a relative base_dir must be found by an absolute-base_dir lookup"
);
}
#[test]
fn toml_merge_provenance_record_lives_outside_the_gitignored_cache_and_is_valid_toml() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
let mut arrays = std::collections::BTreeMap::new();
arrays.insert(
"discovery.exclude".to_string(),
vec!["target/**".to_string(), "docs/assets/**".to_string()],
);
write_toml_merge_provenance(base, Path::new("poly.toml"), &arrays).expect("write provenance");
let manifest_path = base.join(TOML_MERGE_PROVENANCE_MANIFEST);
assert!(manifest_path.exists(), "the record must exist at the repo root");
assert!(
!manifest_path.starts_with(base.join(CACHE_DIR)),
"the record must not live under the gitignored `{CACHE_DIR}` directory"
);
let content = std::fs::read_to_string(&manifest_path).expect("read manifest");
let parsed: TomlMergeProvenanceFile = toml::from_str(&content).expect("the record must be valid TOML");
assert_eq!(parsed.entries.len(), 1);
assert_eq!(parsed.entries[0].relative_path, "poly.toml");
assert_eq!(parsed.entries[0].key_path, "discovery.exclude");
assert_eq!(
parsed.entries[0].values,
vec!["target/**".to_string(), "docs/assets/**".to_string()]
);
}
#[test]
fn toml_merge_provenance_answers_identically_on_a_cache_less_clone() {
let warm = tempfile::tempdir().expect("tempdir warm");
let clone = tempfile::tempdir().expect("tempdir clone");
let mut arrays = std::collections::BTreeMap::new();
arrays.insert("discovery.exclude".to_string(), vec!["docs/assets/**".to_string()]);
write_toml_merge_provenance(warm.path(), Path::new("poly.toml"), &arrays).expect("write provenance");
std::fs::copy(
warm.path().join(TOML_MERGE_PROVENANCE_MANIFEST),
clone.path().join(TOML_MERGE_PROVENANCE_MANIFEST),
)
.expect("check out the committed record");
assert!(
!clone.path().join(CACHE_DIR).exists(),
"the simulated clone must have no machine-local cache"
);
assert_eq!(
read_toml_merge_provenance(warm.path(), Path::new("poly.toml")),
read_toml_merge_provenance(clone.path(), Path::new("poly.toml")),
"a fresh clone must agree with the warm machine about alef's prior proposal"
);
}
#[test]
fn unparseable_toml_merge_provenance_record_prunes_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
std::fs::write(
base.join(TOML_MERGE_PROVENANCE_MANIFEST),
"this is not = = valid toml [[[",
)
.expect("write junk");
assert_eq!(
read_toml_merge_provenance(base, Path::new("poly.toml")),
std::collections::BTreeMap::new()
);
}
#[test]
fn toml_merge_provenance_header_does_not_read_as_a_provenance_marker() {
assert!(
!crate::core::hash::content_has_alef_marker(TOML_MERGE_PROVENANCE_HEADER),
"the record's own header must not look like an alef provenance marker, got:\n{TOML_MERGE_PROVENANCE_HEADER}"
);
}
#[test]
fn toml_merge_provenance_write_extends_rather_than_replaces_other_targets() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
let mut poly_arrays = std::collections::BTreeMap::new();
poly_arrays.insert("discovery.exclude".to_string(), vec!["target/**".to_string()]);
write_toml_merge_provenance(base, Path::new("poly.toml"), &poly_arrays).expect("write poly.toml provenance");
let mut other_arrays = std::collections::BTreeMap::new();
other_arrays.insert("some.key".to_string(), vec!["value".to_string()]);
write_toml_merge_provenance(base, Path::new("other.toml"), &other_arrays).expect("write other.toml provenance");
assert_eq!(
read_toml_merge_provenance(base, Path::new("poly.toml")),
poly_arrays,
"recording a second merge target's provenance must leave the first's untouched"
);
assert_eq!(read_toml_merge_provenance(base, Path::new("other.toml")), other_arrays);
}
fn array_element_indents(rendered: &str) -> Vec<String> {
let mut indents = Vec::new();
let mut inside_array = false;
for line in rendered.lines() {
let trimmed = line.trim();
if inside_array {
if trimmed == "]" {
inside_array = false;
} else {
indents.push(line.chars().take_while(|character| character.is_whitespace()).collect());
}
} else if trimmed.ends_with("= [") {
inside_array = true;
}
}
indents
}
#[test]
fn both_committed_records_indent_array_elements_identically() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
let values = vec![
"packages/generated-bindings/some-language/build/**".to_string(),
"packages/generated-bindings/other-language/build/**".to_string(),
"packages/generated-bindings/third-language/build/**".to_string(),
];
let mut arrays = std::collections::BTreeMap::new();
arrays.insert("discovery.exclude".to_string(), values.clone());
write_toml_merge_provenance(base, Path::new("poly.toml"), &arrays).expect("write provenance");
let provenance = std::fs::read_to_string(base.join(TOML_MERGE_PROVENANCE_MANIFEST)).expect("read provenance");
let ownership = render_ownership_manifest(&values);
let provenance_indents = array_element_indents(&provenance);
let ownership_indents = array_element_indents(&ownership);
assert_eq!(
ownership_indents.len(),
values.len(),
"apparatus check: the ownership record must render one element line per value, got:\n{ownership}"
);
assert_eq!(
provenance_indents.len(),
values.len(),
"apparatus check: the provenance record must render one element line per value, got:\n{provenance}"
);
assert_eq!(
provenance_indents, ownership_indents,
"the two committed records must indent array elements identically, got \
{provenance_indents:?} for the provenance record and {ownership_indents:?} for the \
ownership record"
);
}
#[test]
fn record_arrays_collapse_exactly_where_the_format_gate_collapses_them() {
let short = render_record_assignment("values", &["one".to_string(), "two".to_string()]);
assert_eq!(
short, r#"values = ["one", "two"]"#,
"an array the format gate would collapse must be written inline"
);
let empty = render_record_assignment("values", &[]);
assert_eq!(empty, "values = []", "an empty array has nothing to spread over lines");
let filler = "x".repeat(RECORD_ARRAY_MAX_INLINE_WIDTH - r#"values = [""]"#.len());
let at_limit = render_record_assignment("values", std::slice::from_ref(&filler));
assert_eq!(
at_limit.chars().count(),
RECORD_ARRAY_MAX_INLINE_WIDTH,
"apparatus check: the fixture must land exactly on the limit, got:\n{at_limit}"
);
assert!(
!at_limit.contains('\n'),
"a line exactly at the limit is still collapsed by the gate, so it must stay inline"
);
let over_limit = render_record_assignment("values", &[format!("{filler}y")]);
assert_eq!(
over_limit,
format!("values = [\n{RECORD_ARRAY_INDENT}\"{filler}y\",\n]"),
"one column past the limit the gate leaves the array expanded, so alef must too"
);
}
#[test]
fn toml_merge_provenance_escapes_values_that_need_it() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
let awkward = vec!["we\"ird\\value/**".to_string(), "plain/**".to_string()];
let mut arrays = std::collections::BTreeMap::new();
arrays.insert("discovery.ex\"clude".to_string(), awkward);
write_toml_merge_provenance(base, Path::new("poly.toml"), &arrays).expect("write provenance");
assert_eq!(
read_toml_merge_provenance(base, Path::new("poly.toml")),
arrays,
"an awkward key path and value must round-trip through the hand-rolled writer unchanged"
);
}
}