arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
//! Publish report types (RV2.9).
//!
//! After a Release Transaction executes, the engine produces a structured
//! report of what happened to each crate. The report is human-readable
//! for CLI output and serializable for machine-readable JSON.

use crate::release::publish::tag::CrateTag;
use crate::release::version::Ybf;
use std::collections::BTreeMap;

/// The final state of one crate after the publish engine runs.
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CrateResult {
    /// The crate was successfully published in this run.
    Published { tag: String },
    /// The crate version already existed on the registry (idempotent
    /// resume — verified and skipped). No tag was created (it already
    /// exists from a prior partial run).
    AlreadyPublished,
    /// Publishing this crate failed. The error message is included.
    /// Failures fail fast: remaining crates in the plan are not
    /// attempted (they are listed as `Skipped`).
    Failed { reason: String },
    /// This crate was not attempted because an earlier crate failed.
    Skipped { reason: String },
}

/// The complete report of a Release Transaction execution.
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub(crate) struct PublishReport {
    /// The transaction id from the plan.
    pub(crate) transaction_id: String,
    /// The commit sha the plan was pinned to.
    pub(crate) commit: String,
    /// Per-crate results, keyed by crate name.
    pub(crate) results: BTreeMap<String, CrateResult>,
    /// Whether every publishable crate in the plan ended up published
    /// (or was already published).
    pub(crate) all_published: bool,
}

impl PublishReport {
    /// Count of crates successfully published in this run.
    #[allow(dead_code)]
    pub(crate) fn published_count(&self) -> usize {
        self.results
            .values()
            .filter(|r| matches!(r, CrateResult::Published { .. }))
            .count()
    }

    /// Count of crates that were already published (idempotent skip).
    #[allow(dead_code)]
    pub(crate) fn already_published_count(&self) -> usize {
        self.results
            .values()
            .filter(|r| matches!(r, CrateResult::AlreadyPublished))
            .count()
    }

    /// Count of crates that failed.
    #[allow(dead_code)]
    pub(crate) fn failed_count(&self) -> usize {
        self.results
            .values()
            .filter(|r| matches!(r, CrateResult::Failed { .. }))
            .count()
    }

    /// Count of crates that were skipped (an earlier failure stopped them).
    #[allow(dead_code)]
    pub(crate) fn skipped_count(&self) -> usize {
        self.results
            .values()
            .filter(|r| matches!(r, CrateResult::Skipped { .. }))
            .count()
    }
}

/// A single step in the planned publish sequence — used for dry-run
/// previews (shows what *would* happen, side-effect-free).
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PublishStep {
    /// The crate name.
    pub(crate) crate_name: String,
    /// The target version.
    pub(crate) version: Ybf,
    /// The order in the publish sequence.
    pub(crate) order: usize,
    /// The tag that would be created after publish success.
    pub(crate) tag: String,
}

/// Build the list of planned publish steps from a loaded plan. This is
/// the dry-run preview (ADR-0005 invariant 9 — publish is separate; the
/// preview is side-effect-free).
pub(crate) fn build_publish_steps(crate_name: &str, version: &Ybf, order: usize) -> PublishStep {
    let tag = CrateTag::render(crate_name, version);
    PublishStep {
        crate_name: crate_name.to_string(),
        version: *version,
        order,
        tag,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn build_step_has_correct_tag() {
        let v = Ybf::parse("2026.1.7").unwrap();
        let step = build_publish_steps("arcature-auth", &v, 1);
        assert_eq!(step.crate_name, "arcature-auth");
        assert_eq!(step.version, v);
        assert_eq!(step.order, 1);
        assert_eq!(step.tag, "arcature-auth-v2026.1.7");
    }

    #[test]
    fn report_counts_published() {
        let mut results = BTreeMap::new();
        results.insert(
            "arcature-auth".to_string(),
            CrateResult::Published {
                tag: "arcature-auth-v2026.1.7".to_string(),
            },
        );
        results.insert("arcature-cli".to_string(), CrateResult::AlreadyPublished);
        results.insert(
            "arcature-jobs".to_string(),
            CrateResult::Failed {
                reason: "build error".to_string(),
            },
        );
        results.insert(
            "arcature-pages".to_string(),
            CrateResult::Skipped {
                reason: "prior failure".to_string(),
            },
        );
        let report = PublishReport {
            transaction_id: "2026-08-15.01".to_string(),
            commit: "abc".to_string(),
            results,
            all_published: false,
        };

        assert_eq!(report.published_count(), 1);
        assert_eq!(report.already_published_count(), 1);
        assert_eq!(report.failed_count(), 1);
        assert_eq!(report.skipped_count(), 1);
    }

    #[test]
    fn report_all_published_true() {
        let mut results = BTreeMap::new();
        results.insert(
            "arcature-auth".to_string(),
            CrateResult::Published {
                tag: "arcature-auth-v2026.1.7".to_string(),
            },
        );
        results.insert("arcature-cli".to_string(), CrateResult::AlreadyPublished);
        let report = PublishReport {
            transaction_id: "2026-08-15.01".to_string(),
            commit: "abc".to_string(),
            results,
            all_published: true,
        };
        assert!(report.all_published);
    }
}