use crate::core::hash;
use std::path::Path;
use tracing::{debug, warn};
#[derive(Debug, Default)]
pub struct WriteReport {
pub expected_paths: std::collections::HashSet<std::path::PathBuf>,
pub changed_paths: std::collections::HashSet<std::path::PathBuf>,
pub refused_paths: std::collections::BTreeSet<std::path::PathBuf>,
pub refused_drifted_paths: std::collections::BTreeSet<std::path::PathBuf>,
pub user_owned_paths: std::collections::BTreeSet<std::path::PathBuf>,
}
impl WriteReport {
pub fn changed_count(&self) -> usize {
self.changed_paths.len()
}
pub fn expected_count(&self) -> usize {
self.expected_paths.len()
}
pub fn refused_count(&self) -> usize {
self.refused_paths.len()
}
pub fn refused_drifted_count(&self) -> usize {
self.refused_drifted_paths.len()
}
pub fn user_owned_count(&self) -> usize {
self.user_owned_paths.len()
}
pub fn refuse_drifted(&mut self, path: &Path) {
self.refused_paths.insert(path.to_path_buf());
self.refused_drifted_paths.insert(path.to_path_buf());
}
pub fn refuse_text(&mut self, path: &Path, existing: Option<&str>, generated: &str) {
match existing {
Some(existing) if matches_alef_output(path, existing, generated) => {
self.refused_paths.insert(path.to_path_buf());
}
_ => self.refuse_drifted(path),
}
}
pub fn absorb_unwritten(&mut self, other: &WriteReport) {
self.refused_paths.extend(other.refused_paths.iter().cloned());
self.refused_drifted_paths
.extend(other.refused_drifted_paths.iter().cloned());
self.user_owned_paths.extend(other.user_owned_paths.iter().cloned());
}
}
pub(crate) fn matches_alef_output(path: &Path, existing: &str, generated: &str) -> bool {
let existing_body = hash::strip_hash_line(existing);
let generated_body = hash::strip_hash_line(generated);
existing_body == generated_body
|| hash::strip_hash_line(&super::ensure_generated_header(path, &existing_body)) == generated_body
}
pub fn report_refused_writes(report: &WriteReport) {
if report.refused_paths.is_empty() {
return;
}
let mut paths: Vec<&std::path::PathBuf> = report.refused_paths.iter().collect();
paths.sort();
warn!(
"{} file(s) were NOT written, {} of them holding content that DIFFERS from what alef \
would now generate: each already exists, carries no alef provenance marker, and \
alef has no durable record of owning it. This will not resolve on its own — the marker can \
only be written by writing the file, which is exactly what the guard declines. The \
differing ones are stale for as long as they stay frozen, and a file whose content is \
derived from the release version is the case that bites, because a stale copy stays \
plausible. Review the \
diff for each and adopt the ones alef should own with `alef adopt <path>`. At migration \
scale, `alef adopt <glob>` previews the whole set and `alef adopt <glob> --converged-only \
--write` clears every file that already matches generated output, leaving the drifted \
ones for you to read one at a time. If these are \
formats that cannot carry a marker (package.json, *.jar) and this is a fresh clone or a CI \
checkout, check whether .alef-ownership.toml was committed — that file is where their \
ownership is recorded. Do NOT hand-add the marker line: a refusal can be protecting a \
deliberate hand-edit, and stamping it blind re-enables exactly the clobbering the guard \
exists to prevent.",
paths.len(),
report.refused_drifted_paths.len()
);
for path in paths {
if report.refused_drifted_paths.contains(path) {
warn!(
" not written, content DIFFERS (stale until adopted or deleted): {}",
path.display()
);
} else {
warn!(
" not written, content already matches generated output: {}",
path.display()
);
}
}
}
pub fn report_user_owned_skips(report: &WriteReport) {
if report.user_owned_paths.is_empty() {
return;
}
tracing::info!(
"{} file(s) were not written because `[workspace.ownership] user_owned` in alef.toml \
declares them owned by this repository rather than by alef. alef does not overwrite \
them, does not stamp them with a provenance marker, and does not verify their \
contents. Remove the matching pattern to hand a path back to alef.",
report.user_owned_paths.len()
);
for path in &report.user_owned_paths {
debug!(" declared user-owned, not written: {}", path.display());
}
}
#[cfg(test)]
mod tests;