use anyhow::Result;
use crate::cli::{cache, dispatch, pipeline};
use super::super::args::Commands;
use super::super::dispatch::DispatchContext;
use super::super::helpers::*;
use super::super::verify_orphans;
pub(super) fn run(context: &DispatchContext, report_only: bool) -> Result<Option<Commands>> {
let config_path = &context.config_path;
let (_workspace, resolved) = load_config(config_path)?;
let crates_to_process = dispatch::select_crates(&resolved, &context.crate_filter)?;
tracing::info!("Verifying alef-generated files (per-file inputs+content hash)");
let base_dir = std::env::current_dir()?;
let missing_snippet_roots: Vec<String> = crates_to_process
.iter()
.flat_map(|resolved_cfg| missing_snippet_directories(resolved_cfg, &base_dir))
.collect();
let has_missing_snippet_roots = !missing_snippet_roots.is_empty();
let alef_toml_bytes = cache::read_alef_toml_bytes(config_path);
let all_inputs_hashes: Vec<String> = crates_to_process
.iter()
.filter_map(|c| cache::sources_hash(&c.sources).ok())
.map(|sh| crate::core::hash::compute_inputs_hash(&sh, &alef_toml_bytes))
.collect();
let stale = verify_walk_multi(&base_dir, &all_inputs_hashes)?;
let mut snippet_coverage_issues = Vec::new();
let mut missing_generated_files: Vec<String> = Vec::new();
let mut missing_gitignored_generated_files: Vec<String> = Vec::new();
let mut frozen_generated_files: Vec<FrozenFile> = Vec::new();
let mut all_managed_paths: std::collections::HashSet<std::path::PathBuf> = std::collections::HashSet::new();
let mut stage_failures: Vec<String> = Vec::new();
let mut create_once_template_drift: Vec<String> = Vec::new();
for resolved_cfg in &crates_to_process {
let languages = resolve_languages(resolved_cfg, None)?;
let api = pipeline::extract(resolved_cfg, config_path, false)?;
let scaffold_files = pipeline::scaffold(&api, resolved_cfg, &languages, config_path)?;
create_once_template_drift.extend(
pipeline::find_create_once_template_drift(&scaffold_files, &base_dir)
.into_iter()
.map(|path| format!("[{}] {}", resolved_cfg.name, path.display())),
);
let found = find_missing_and_frozen_generated_files(&languages, &api, resolved_cfg, config_path, &base_dir)?;
missing_generated_files.extend(found.missing);
missing_gitignored_generated_files.extend(found.missing_gitignored);
frozen_generated_files.extend(found.frozen);
all_managed_paths.extend(found.managed_paths);
stage_failures.extend(
found
.stage_failures
.into_iter()
.map(|failure| format!("[{}] {failure}", resolved_cfg.name)),
);
let Some(e2e_config) = &resolved_cfg.e2e else {
continue;
};
if let Err(error) = crate::e2e::verify_fresh_snippet_coverage(
&base_dir,
resolved_cfg,
e2e_config,
&api.types,
&api.enums,
&api.functions,
) {
snippet_coverage_issues.push(format!("[{}] {error:#}", resolved_cfg.name));
}
}
missing_generated_files.sort();
missing_generated_files.dedup();
missing_gitignored_generated_files.sort();
missing_gitignored_generated_files.dedup();
frozen_generated_files.sort_by(|a, b| a.path.cmp(&b.path));
frozen_generated_files.dedup_by(|a, b| a.path == b.path);
stage_failures.sort();
stage_failures.dedup();
create_once_template_drift.sort();
create_once_template_drift.dedup();
let has_stage_failures = !stage_failures.is_empty();
let has_missing_files = !missing_generated_files.is_empty();
let has_missing_gitignored_files = !missing_gitignored_generated_files.is_empty();
let has_frozen_files = !frozen_generated_files.is_empty();
let has_adoptable_frozen_files =
crate::bin_cli::helpers::frozen::has_adoptable_frozen_files(&frozen_generated_files);
let orphan_generated_files = verify_orphans::find_orphaned_generated_files(&base_dir, &all_managed_paths);
let has_orphan_files = !orphan_generated_files.is_empty();
let abi_disagreement = find_stamp_disagreement(&base_dir, crate::core::hash::HANDLE_ABI_STAMP_KEY);
let has_abi_disagreement = abi_disagreement.is_some();
if let Some(disagreement) = &abi_disagreement {
crate::bin_cli::output::line(format_args!(
"ABI generation disagreement detected for `{}`:",
disagreement.key
));
for (path, value) in &disagreement.examples {
crate::bin_cli::output::line(format_args!(" {path} -> {value}"));
}
}
let mut all_version_mismatches: Vec<String> = Vec::new();
for resolved_cfg in &crates_to_process {
let mismatches = pipeline::verify_versions(resolved_cfg)?;
all_version_mismatches.extend(mismatches);
}
let has_version_issues = !all_version_mismatches.is_empty();
if has_version_issues {
crate::bin_cli::output::line("Version mismatches detected:");
for mismatch in &all_version_mismatches {
crate::bin_cli::output::line(format_args!(" {mismatch}"));
}
}
if !snippet_coverage_issues.is_empty() {
crate::bin_cli::output::line("Snippet coverage issues detected:");
for issue in &snippet_coverage_issues {
crate::bin_cli::output::line(format_args!(" {issue}"));
}
}
if has_missing_snippet_roots {
crate::bin_cli::output::line(
"Configured docs.snippets roots that do not exist (every snippet check that walks \
these passes having examined nothing -- fix the dirs/inline_dirs entry or create \
the directory):",
);
for directory in &missing_snippet_roots {
crate::bin_cli::output::line(format_args!(" {directory}"));
}
}
if !create_once_template_drift.is_empty() {
crate::bin_cli::output::line(
"Create-once scaffold files that predate a template fix (informational -- these are \
user-owned after their first write, so alef never rewrites them; review the current \
template and hand-port the fix if it applies to your copy):",
);
for path in &create_once_template_drift {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
let untracked_records = cache::untracked_required_records(&base_dir);
if !untracked_records.is_empty() {
crate::bin_cli::output::line(
"Required alef records are not tracked by git (alef writes these and depends on them \
being committed):",
);
for record in &untracked_records {
crate::bin_cli::output::line(format_args!(" {record} -- fix with: git add {record}"));
}
}
if stale.is_empty()
&& !has_missing_files
&& !has_missing_gitignored_files
&& !has_adoptable_frozen_files
&& !has_orphan_files
&& !has_abi_disagreement
&& !has_version_issues
&& snippet_coverage_issues.is_empty()
&& untracked_records.is_empty()
&& !has_stage_failures
&& !has_missing_snippet_roots
{
crate::bin_cli::output::line("All bindings and versions are up to date.");
} else {
if !stale.is_empty() {
crate::bin_cli::output::line("Stale bindings detected:");
for s in &stale {
crate::bin_cli::output::line(format_args!(" {}", s.path));
if tracing::enabled!(tracing::Level::DEBUG) {
crate::bin_cli::output::line(format_args!(" embedded: {}", s.embedded));
let computed_str = s.computed.join(", ");
crate::bin_cli::output::line(format_args!(" computed: {computed_str}"));
}
}
}
if has_missing_files {
crate::bin_cli::output::line("Missing generated files detected:");
for path in &missing_generated_files {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
if has_missing_gitignored_files {
crate::bin_cli::output::line(
"Missing generated files that are also gitignored detected (running `alef generate` \
cannot fix these -- the file would be written, then discarded by the matching \
.gitignore rule before it can be committed; narrow the ignore rule for each path \
below, then commit the file):",
);
for path in &missing_gitignored_generated_files {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
if has_frozen_files {
let (create_once_frozen, adoptable_frozen): (Vec<&FrozenFile>, Vec<&FrozenFile>) =
frozen_generated_files.iter().partition(|frozen| frozen.create_once);
if !adoptable_frozen.is_empty() {
crate::bin_cli::output::line(
"Frozen generated files detected (alef owns these paths but the files carry no \
provenance marker, so alef refuses to write them -- review each file, then either \
add the marker shown and rerun `alef generate`, or delete the file so generation \
can write it cleanly):",
);
for frozen in &adoptable_frozen {
crate::bin_cli::output::line(format_args!(" {}", frozen.path));
if let Some(near_miss) = &frozen.near_miss {
crate::bin_cli::output::line(format_args!(
" close but not recognized: {near_miss:?} (alef accepts \"generated by alef\" \
case-insensitively)"
));
}
match &frozen.remedy {
Some(remedy) => crate::bin_cli::output::line(format_args!(" add marker: {remedy}")),
None => crate::bin_cli::output::line(
" this format has no comment syntax to carry a marker, so alef proves ownership \
through the committed .alef-ownership.toml record instead -- run `alef adopt \
<path> --write` to record it there, or delete the file so the next `alef generate` \
writes and records it directly",
),
}
}
}
if !create_once_frozen.is_empty() {
crate::bin_cli::output::line(
"Frozen create-once seeds detected (alef writes each of these paths only once and \
never revisits them, so the missing provenance marker is deliberate, not a bug -- the \
file on disk is presumed to be your grown copy of the placeholder alef seeded, exactly \
the case `alef adopt` refuses by default. Review each one, confirm it holds nothing you \
wrote -- a byte-identical file has nothing to lose either way -- then run `alef adopt \
<path> --write --clobber-create-once-seeds`):",
);
for frozen in &create_once_frozen {
crate::bin_cli::output::line(format_args!(" {}", frozen.path));
}
}
}
if has_orphan_files {
crate::bin_cli::output::line(
"Orphaned generated files detected (alef's marker is present but the current run's \
backends would not produce these paths -- a backend may have stopped emitting them, \
they were dropped from generation config, or the file is a create-once seed alef only \
writes when absent; review each and delete by hand if genuinely stale, alef never \
deletes automatically):",
);
for path in &orphan_generated_files {
crate::bin_cli::output::line(format_args!(" {path}"));
}
}
if has_stage_failures {
crate::bin_cli::output::line(
"Generation debt detected while collecting the managed surface (missing/frozen \
files above are still accurate; this is additional, separate debt):",
);
for failure in &stage_failures {
crate::bin_cli::output::line(format_args!(" {failure}"));
}
}
}
super::super::verify_outcome::ensure_success(
!stale.is_empty()
|| has_missing_files
|| has_missing_gitignored_files
|| has_adoptable_frozen_files
|| has_orphan_files
|| has_abi_disagreement
|| has_stage_failures,
has_version_issues,
snippet_coverage_issues.len(),
report_only,
)?;
super::ensure_required_records_tracked(&untracked_records, report_only)?;
ensure_configured_snippet_directories_exist(&missing_snippet_roots, report_only)?;
Ok(None)
}
fn missing_snippet_directories(
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
) -> Vec<String> {
let Some(snippets) = config.docs.as_ref().and_then(|docs| docs.snippets.as_ref()) else {
return Vec::new();
};
snippets
.dirs
.iter()
.chain(&snippets.inline_dirs)
.filter_map(|dir| {
let resolved = base_dir.join(dir);
(!resolved.exists()).then(|| {
format!(
"[{}] {} (resolved to {})",
config.name,
dir.display(),
resolved.display()
)
})
})
.collect()
}
fn ensure_configured_snippet_directories_exist(missing: &[String], report_only: bool) -> Result<()> {
if report_only || missing.is_empty() {
return Ok(());
}
anyhow::bail!(
"configured docs.snippets roots do not exist: {}. Fix the dirs/inline_dirs entries in \
alef.toml or create the directories -- until then every snippet check that walks them \
reports a clean run having examined nothing",
missing.join(", ")
)
}