arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
//! The change-fragment data model (ADR-0005 invariant 8).
//!
//! A fragment file is a TOML document with one or more `[[change]]` entries.
//! Each entry declares a release unit, a change kind, and a human summary.
//! The kind is a closed enum so unknown values fail at deserialization
//! rather than being silently accepted.

/// The closed set of change kinds (the program's CHANGE FRAGMENTS section).
///
/// `None` means the change has no release-relevant public-behavior impact
/// (e.g. an internal refactor, a docs-only change to a non-releasable
/// surface). `Compatible` means a backward-compatible change (bug fix,
/// additive API, performance improvement). `Breaking` means a breaking
/// change that requires a new YBF BREAK generation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum ChangeKind {
    None,
    Compatible,
    Breaking,
}

/// One `[[change]]` entry from a fragment file.
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
pub(crate) struct ChangeEntry {
    pub(crate) unit: String,
    pub(crate) kind: ChangeKind,
    pub(crate) summary: String,
}

/// The top-level TOML document of a fragment file. TOML's `[[change]]`
/// array-of-tables syntax deserializes into a struct with a `change` field
/// containing a `Vec<ChangeEntry>` — not a bare `Vec<ChangeEntry>`.
#[derive(Debug, Clone, serde::Deserialize)]
pub(crate) struct FragmentDoc {
    #[serde(rename = "change")]
    pub entries: Vec<ChangeEntry>,
}

/// A parsed fragment file: the file name and its entries.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ChangeFragmentFile {
    pub(crate) file_name: String,
    pub(crate) entries: Vec<ChangeEntry>,
}

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

    #[test]
    fn parses_single_entry() {
        let toml = "\
[[change]]
unit = \"arcature-auth\"
kind = \"compatible\"
summary = \"Add refresh-session support\"
";
        let doc: FragmentDoc = toml::from_str(toml).expect("parses");
        assert_eq!(doc.entries.len(), 1);
        assert_eq!(doc.entries[0].unit, "arcature-auth");
        assert_eq!(doc.entries[0].kind, ChangeKind::Compatible);
        assert_eq!(doc.entries[0].summary, "Add refresh-session support");
    }

    #[test]
    fn parses_multiple_entries_in_one_file() {
        let toml = "\
[[change]]
unit = \"arcature-auth\"
kind = \"breaking\"
summary = \"Replace AuthUser identity contract\"

[[change]]
unit = \"arcature-db\"
kind = \"compatible\"
summary = \"Add connection-pool stats\"
";
        let doc: FragmentDoc = toml::from_str(toml).expect("parses");
        assert_eq!(doc.entries.len(), 2);
        assert_eq!(doc.entries[0].kind, ChangeKind::Breaking);
        assert_eq!(doc.entries[1].kind, ChangeKind::Compatible);
    }

    #[test]
    fn parses_none_kind() {
        let toml = "\
[[change]]
unit = \"arcature-auth\"
kind = \"none\"
summary = \"Internal refactor, no public API change\"
";
        let doc: FragmentDoc = toml::from_str(toml).expect("parses");
        assert_eq!(doc.entries[0].kind, ChangeKind::None);
    }

    #[test]
    fn rejects_unknown_kind() {
        let toml = "\
[[change]]
unit = \"arcature-auth\"
kind = \"major\"
summary = \"x\"
";
        let err = toml::from_str::<FragmentDoc>(toml).expect_err("unknown kind must fail");
        assert!(err.to_string().contains("major"));
    }

    #[test]
    fn rejects_missing_field() {
        let toml = "\
[[change]]
unit = \"arcature-auth\"
kind = \"compatible\"
";
        let err = toml::from_str::<FragmentDoc>(toml).expect_err("missing summary must fail");
        assert!(err.to_string().contains("summary"));
    }
}