alef 0.71.0

Opinionated polyglot binding generator for Rust libraries
Documentation
//! `alef docs` handling, split out of `core_commands.rs` to keep that already
//! over-cap file from growing (`file-modularization`).

use anyhow::Result;
use std::path::PathBuf;

use crate::bin_cli::args::Commands;
use crate::bin_cli::dispatch::DispatchContext;
use crate::bin_cli::helpers::{format_languages, load_config, resolve_doc_languages};
use crate::cli::{cache, dispatch, pipeline};

/// Handles `Commands::Docs`.
///
/// `skip_snippet_validation` selects between `docs::generate_docs_stage` and
/// `docs::generate_docs_stage_without_snippet_compile_validation` -- see that
/// function's doc comment for why the compile step is worth skipping on
/// purpose rather than just tolerating its failure. ~keep
pub(crate) fn handle(
    config_path: &std::path::Path,
    context: &DispatchContext,
    lang: Option<Vec<String>>,
    output: Option<String>,
    skip_snippet_validation: bool,
) -> Result<Option<Commands>> {
    let (_workspace, resolved) = load_config(config_path)?;
    let crates_to_process = dispatch::select_crates(&resolved, &context.crate_filter)?;
    let multi = dispatch::is_multi_crate(&crates_to_process);
    let base_dir = std::env::current_dir()?;
    let config_toml = std::fs::read_to_string(config_path)?;
    let mut grand_total: usize = 0;
    for resolved_cfg in &crates_to_process {
        let languages = resolve_doc_languages(resolved_cfg, lang.as_deref())?;
        let selector = languages.iter().map(ToString::to_string).collect::<Vec<_>>().join("-");
        let output_key = output.as_deref().unwrap_or("default");
        let docs_stage_key = format!("docs-{}", cache::hash_content(&format!("{selector}:{output_key}")));
        let api = pipeline::extract(resolved_cfg, config_path, false)?;
        let ir_json = serde_json::to_string(&api)?;
        let stage_hash = cache::compute_stage_hash(&ir_json, &docs_stage_key, &config_toml, &[]);
        let use_stage_cache = resolved_cfg.docs.is_none();
        // Computed before the cache check -- `is_stage_cached` needs the same `inputs_hash`
        // every stamped doc page was stamped under to tell a hand-edited page from an
        // untouched one, not just whether the file still exists. ~keep
        let sources_hash = cache::sources_hash(&resolved_cfg.sources)?;
        let alef_toml_bytes = cache::read_alef_toml_bytes(config_path);
        let inputs_hash = crate::core::hash::compute_inputs_hash(&sources_hash, &alef_toml_bytes);
        if use_stage_cache && cache::is_stage_cached(&resolved_cfg.name, &docs_stage_key, &stage_hash, &inputs_hash) {
            if multi {
                tracing::info!("[{}] Docs up to date (cached)", resolved_cfg.name);
            } else {
                tracing::info!("Docs up to date (cached)");
            }
            continue;
        }
        if multi {
            tracing::info!(
                "[{}] Generating docs for: {}",
                resolved_cfg.name,
                format_languages(&languages)
            );
        } else {
            tracing::info!("Generating docs for: {}", format_languages(&languages));
        }
        // `generate_docs_stage` hands back every page it rendered even when a later step
        // (snippet validation, CLI/MCP adoption, llms/skills) fails, specifically so a
        // strict-mode bail never discards already-rendered API reference pages. Write
        // `files` before propagating `docs_result`, not after. ~keep
        let (files, docs_result) = if skip_snippet_validation {
            crate::docs::generate_docs_stage_without_snippet_compile_validation(
                &api,
                resolved_cfg,
                &languages,
                output.as_deref(),
                &base_dir,
            )
        } else {
            crate::docs::generate_docs_stage(&api, resolved_cfg, &languages, output.as_deref(), &base_dir)
        };
        let report = pipeline::write_scaffold_files_report(&files, &base_dir, true)?;
        pipeline::report_refused_writes(&report);
        docs_result?;
        let count = report.changed_count();
        let output_paths: Vec<PathBuf> = files
            .iter()
            .filter(|file| file.carries_alef_marker())
            .map(|file| base_dir.join(&file.path))
            .collect();
        let doc_paths = pipeline::stampable_output_paths(&files, &base_dir);
        pipeline::finalize_hashes(&doc_paths, &sources_hash, &alef_toml_bytes)?;
        if use_stage_cache {
            cache::write_stage_hash(&resolved_cfg.name, &docs_stage_key, stage_hash.as_str(), &output_paths)?;
        }
        grand_total += count;
    }
    tracing::info!("Generated {grand_total} doc files");
    Ok(None)
}