arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
//! Typed errors for the Release V2 engine (AGENTS.md ยง18).

use std::fmt;

/// A single metadata-validation finding, addressed to one crate.
///
/// The validator is side-effect-free and collects *all* findings before
/// failing, so a workspace surfaces every problem in one pass rather than
/// aborting on the first. The crate name is the machine-readable address;
/// the message is human context that never carries a secret.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Diagnostic {
    pub(crate) crate_name: String,
    pub(crate) message: String,
}

impl fmt::Display for Diagnostic {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}: {}", self.crate_name, self.message)
    }
}

/// Errors raised by the Release engine itself (distinct from the
/// transport-layer [`crate::process::ProcessError`]).
#[derive(Debug)]
pub(crate) enum ReleaseError {
    /// Invoking `cargo metadata` failed.
    MetadataUnavailable(crate::process::ProcessError),
    /// The `cargo metadata` JSON could not be parsed.
    MetadataParse(serde_json::Error),
    /// One or more crates failed metadata validation.
    Validation(Vec<Diagnostic>),
    /// A filesystem error occurred while reading change fragments.
    FragmentIo(std::io::Error),
}

impl fmt::Display for ReleaseError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MetadataUnavailable(error) => {
                write!(formatter, "cannot read cargo metadata: {error}")
            }
            Self::MetadataParse(error) => {
                write!(formatter, "cannot parse cargo metadata: {error}")
            }
            Self::Validation(diagnostics) => {
                if diagnostics.len() == 1 {
                    write!(
                        formatter,
                        "release metadata validation failed: {}",
                        diagnostics[0]
                    )
                } else {
                    writeln!(
                        formatter,
                        "release metadata validation failed ({} crates):",
                        diagnostics.len()
                    )?;
                    for diagnostic in diagnostics {
                        writeln!(formatter, "  {diagnostic}")?;
                    }
                    Ok(())
                }
            }
            Self::FragmentIo(error) => {
                write!(formatter, "cannot read change fragments: {error}")
            }
        }
    }
}

impl std::error::Error for ReleaseError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::MetadataUnavailable(error) => Some(error),
            Self::MetadataParse(error) => Some(error),
            Self::Validation(_) => None,
            Self::FragmentIo(error) => Some(error),
        }
    }
}

impl From<crate::process::ProcessError> for ReleaseError {
    fn from(value: crate::process::ProcessError) -> Self {
        Self::MetadataUnavailable(value)
    }
}

impl From<serde_json::Error> for ReleaseError {
    fn from(value: serde_json::Error) -> Self {
        Self::MetadataParse(value)
    }
}

impl From<std::io::Error> for ReleaseError {
    fn from(value: std::io::Error) -> Self {
        Self::FragmentIo(value)
    }
}