arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
//! The `[package.metadata.arcature]` schema (ADR-0005 invariant 7).
//!
//! Every public Arcature crate declares this table. It is the
//! machine-readable release intent that the validator (and later the
//! planner) discover automatically from cargo metadata — new crates need
//! no `release.yml` edit, only a `Cargo.toml` with this block (ADR-0005
//! invariant 13).
//!
//! The fields are intentionally small and closed. A publishable crate must
//! declare `publish = true`, a known `role`, and a `release-unit`; a
//! non-publishable crate may declare `publish = false` alone. Unknown roles
//! fail at deserialization (the [`Role`] enum is closed), and the validator
//! turns the failure into a named diagnostic.

/// The closed set of release roles a crate may hold.
///
/// `facade` is the umbrella application crate (`arcature`); `proc-macro` is
/// a proc-macro crate that emits `::arcature::` paths (`arcature-dx`); every
/// other publishable crate is a `subsystem`. The set is closed so an unknown
/// role fails with a useful "expected one of …" message rather than being
/// silently accepted.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Role {
    Facade,
    ProcMacro,
    Subsystem,
}

/// The release metadata declared under `[package.metadata.arcature]`.
///
/// `role` and `release_unit` are optional at the schema layer so a
/// non-publishable crate may declare `publish = false` alone; the validator
/// requires them when `publish` is `true` and reports a named diagnostic
/// otherwise. `release_unit` keeps its TOML spelling (`release-unit`) via
/// `serde(rename)` so the manifest reads naturally.
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
pub(crate) struct ReleaseMetadata {
    pub(crate) publish: bool,
    pub(crate) role: Option<Role>,
    #[serde(rename = "release-unit")]
    pub(crate) release_unit: Option<String>,
}

/// The per-crate state of the release metadata, as discovered from cargo
/// metadata.
///
/// [`Absent`] means no `[package.metadata.arcature]` table is present;
/// [`Valid`] holds a parsed block; [`Malformed`] records a deserialization
/// failure so the validator can report it with the crate name rather than
/// aborting discovery on the first bad crate.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CrateMetadata {
    Absent,
    Valid(ReleaseMetadata),
    Malformed(String),
}

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

    #[test]
    fn parses_full_publishable_block() {
        let toml = "\
publish = true
role = \"subsystem\"
release-unit = \"arcature-auth\"
";
        let parsed: ReleaseMetadata = toml::from_str(toml).expect("full block parses");
        assert!(parsed.publish);
        assert_eq!(parsed.role, Some(Role::Subsystem));
        assert_eq!(parsed.release_unit.as_deref(), Some("arcature-auth"));
    }

    #[test]
    fn parses_core_facade_and_proc_macro_roles() {
        let facade: ReleaseMetadata =
            toml::from_str("publish = true\nrole = \"facade\"\nrelease-unit = \"core\"\n")
                .expect("facade parses");
        assert_eq!(facade.role, Some(Role::Facade));
        let dx: ReleaseMetadata =
            toml::from_str("publish = true\nrole = \"proc-macro\"\nrelease-unit = \"core\"\n")
                .expect("proc-macro parses");
        assert_eq!(dx.role, Some(Role::ProcMacro));
    }

    #[test]
    fn parses_non_publishable_block_with_publish_only() {
        let parsed: ReleaseMetadata =
            toml::from_str("publish = false\n").expect("publish-only parses");
        assert!(!parsed.publish);
        assert_eq!(parsed.role, None);
        assert_eq!(parsed.release_unit, None);
    }

    #[test]
    fn rejects_unknown_role() {
        let toml = "publish = true\nrole = \"glue\"\nrelease-unit = \"x\"\n";
        let error = toml::from_str::<ReleaseMetadata>(toml)
            .expect_err("unknown role must fail deserialization");
        let message = error.to_string();
        assert!(
            message.contains("glue"),
            "diagnostic names the bad value: {message}"
        );
    }

    #[test]
    fn release_unit_preserves_hyphenated_spelling() {
        // Confirms `serde(rename = "release-unit")` matches the TOML key with a
        // hyphen, not the snake_case Rust field.
        let parsed: ReleaseMetadata = toml::from_str(
            "publish = true\nrole = \"subsystem\"\nrelease-unit = \"arcature-db\"\n",
        )
        .expect("hyphenated key parses");
        assert_eq!(parsed.release_unit.as_deref(), Some("arcature-db"));
    }
}