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>,
pub refused_create_once_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 refused_create_once_count(&self) -> usize {
self.refused_create_once_paths.len()
}
pub fn refuse_drifted(&mut self, path: &Path, create_once: bool) {
self.refused_paths.insert(path.to_path_buf());
self.refused_drifted_paths.insert(path.to_path_buf());
if create_once {
self.refused_create_once_paths.insert(path.to_path_buf());
}
}
pub fn refuse_text(&mut self, path: &Path, existing: Option<&str>, generated: &str, create_once: bool) {
match existing {
Some(existing) if matches_alef_output(path, existing, generated) => {
self.refused_paths.insert(path.to_path_buf());
if create_once {
self.refused_create_once_paths.insert(path.to_path_buf());
}
}
_ => self.refuse_drifted(path, create_once),
}
}
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());
self.refused_create_once_paths
.extend(other.refused_create_once_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 adoptable: Vec<&std::path::PathBuf> = report
.refused_paths
.iter()
.filter(|path| !report.refused_create_once_paths.contains(*path))
.collect();
adoptable.sort();
if !adoptable.is_empty() {
let drifted_count = adoptable
.iter()
.filter(|path| report.refused_drifted_paths.contains(**path))
.count();
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. A rerun does not clear this -- the marker can \
only be written by writing the file, which is what the guard declines.",
adoptable.len(),
drifted_count
);
warn!(" Review the diff and adopt: alef adopt <path>");
warn!(
" At scale: `alef adopt <glob>` previews the set; `alef adopt <glob> --converged-only \
--write` clears the files that already match generated output"
);
warn!(
" Formats that cannot carry a marker (package.json, *.jar), on a fresh clone or CI \
checkout: check .alef-ownership.toml was committed -- their ownership is recorded there"
);
warn!(" Do NOT hand-add the marker line -- the write guard reads it as consent to clobber a hand-edit");
for path in adoptable {
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()
);
}
}
}
if !report.refused_create_once_paths.is_empty() {
let mut seeds: Vec<&std::path::PathBuf> = report.refused_create_once_paths.iter().collect();
seeds.sort();
warn!(
"{} file(s) were NOT written because they are create-once seeds: alef emits these \
paths only when absent and never rewrites them, so this is not drift.",
seeds.len()
);
warn!(
" Do NOT run `alef adopt` on these -- the on-disk copy has almost certainly grown \
past alef's placeholder, and adopting consents to alef replacing it wholesale on the \
next overwriting regen (an `alef version` sync, `alef all --clobber-create-once-seeds`)"
);
warn!(" `alef adopt --write` already refuses these unless --clobber-create-once-seeds is passed");
for path in seeds {
warn!(" create-once seed, not rewritten: {}", 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: alef does not overwrite, stamp, or verify them. \
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;