arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
//! `arc release version` — YBF version engine inspection (RV2.5).
//!
//! Loads cargo metadata, discovers the publishable crates, builds the crate
//! graph, validates the sibling dependency ranges against the YBF rules,
//! and reports the current version of every publishable crate plus any
//! range findings. Side-effect-free: no writes, no network beyond `cargo
//! metadata`.

use crate::cli::OutputFormat;
use crate::error::CommandError;
use crate::release;

/// Run the `arc release version` command.
pub(crate) fn run_version(format: OutputFormat) -> Result<(), CommandError> {
    let document = release::load_metadata()?;
    let crates = release::discover(&document);
    release::validate(&crates).map_err(|diagnostics| {
        CommandError::Release(release::ReleaseError::Validation(diagnostics))
    })?;

    let crate_graph = release::build_crate_graph(&document, &crates);

    // Current versions keyed by crate name, parsed as YBF.
    let mut current_versions: std::collections::BTreeMap<String, release::Ybf> =
        std::collections::BTreeMap::new();
    let mut version_diagnostics: Vec<release::Diagnostic> = Vec::new();
    for pkg in &document.packages {
        if let Some(raw) = &pkg.version {
            match release::Ybf::parse(raw) {
                Ok(v) => {
                    current_versions.insert(pkg.name.clone(), v);
                }
                Err(error) => {
                    version_diagnostics.push(release::Diagnostic {
                        crate_name: pkg.name.clone(),
                        message: format!("{error}"),
                    });
                }
            }
        }
    }

    // YBF range validation against the publishable graph.
    let range_findings = release::validate_ranges(&document, &crates, &crate_graph);
    let mut all_findings = version_diagnostics;
    all_findings.extend(range_findings);
    all_findings.sort_by(|a, b| {
        a.crate_name
            .cmp(&b.crate_name)
            .then(a.message.cmp(&b.message))
    });

    match format {
        OutputFormat::Human => {
            println!("Publishable crates: {}", crate_graph.nodes.len());
            println!();
            println!("Current versions:");
            for (name, version) in &current_versions {
                let in_graph = crate_graph.nodes.contains_key(name);
                if in_graph {
                    println!("  {name}: {version}");
                }
            }
            println!();
            if all_findings.is_empty() {
                println!("YBF range validation: OK (no findings)");
            } else {
                println!("YBF range validation: {} finding(s)", all_findings.len());
                for finding in &all_findings {
                    println!("  {finding}");
                }
            }
            Ok(())
        }
        OutputFormat::Json => {
            let payload = serde_json::json!({
                "crate_count": crate_graph.nodes.len(),
                "versions": current_versions.iter()
                    .filter(|(name, _)| crate_graph.nodes.contains_key(name.as_str()))
                    .map(|(name, v)| (name.clone(), v.to_string()))
                    .collect::<std::collections::BTreeMap<String, String>>(),
                "findings": all_findings.iter().map(|d| serde_json::json!({
                    "crate": d.crate_name,
                    "message": d.message,
                })).collect::<Vec<_>>(),
                "ok": all_findings.is_empty(),
            });
            println!("{}", serde_json::to_string_pretty(&payload)?);
            Ok(())
        }
    }
}