arcature 2026.2.1

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! The application binary subcommands (AP2.1-10).
//!
//! [`Subcommand`] is the typed set of operations an Arcature application
//! binary dispatches: `serve`, `migrate`, `queue`, `schedule`, `doctor`,
//! `about` (PROGRAM.md AP2.1-10: "Application binary subcommands (`serve`,
//! `migrate`, `queue`, `schedule`, `doctor`, `about`) sharing one internal
//! operation layer"). The app's `main` parses argv into a [`Subcommand`]
//! and dispatches to its [`super::Operations`] implementation.
//!
//! `serve` is the normal production path (the app serves with the health
//! lifecycle). `migrate` applies schema migrations. `queue` runs the job
//! worker. `schedule` runs the recurring-job scheduler. `doctor` runs the
//! app-level diagnostics (distinct from the CLI's `arc doctor`, which
//! inspects the *local environment*; the app's `doctor` inspects the
//! *running application's* dependencies at runtime). `about` prints the
//! application's identity (name, version, Arcature version) for operators.
//!
//! The enum and parsing are pure `std` — no feature flags — so an expert
//! user on a custom runtime can parse subcommands and dispatch their own
//! way. The [`super::dispatch`] runner is `macros`-gated (it drives the
//! async operations on the certified Tokio runtime).

use std::ffi::OsString;

/// A typed application binary subcommand.
///
/// Parsed from argv by [`parse`]; dispatched by
/// [`dispatch`](super::dispatch) to the application's
/// [`Operations`](super::Operations) implementation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Subcommand {
    /// `serve` — the normal production path: serve with the health
    /// lifecycle until a termination signal.
    Serve,
    /// `migrate` — apply pending schema migrations (the app owns the
    /// migrator; the dispatch calls `Operations::migrate`).
    Migrate,
    /// `queue` — run the job worker (claim and execute queued jobs until
    /// a termination signal).
    Queue,
    /// `schedule` — run the recurring-job scheduler (enqueue scheduled
    /// jobs on their cadence until a termination signal).
    Schedule,
    /// `doctor` — run the app-level runtime diagnostics (dependency
    /// health checks that need the app's configured services). Distinct
    /// from the CLI's `arc doctor`, which inspects the local toolchain
    /// environment.
    Doctor,
    /// `about` — print the application's identity (name, version, the
    /// Arcature engine version) to stdout. Side-effect-free; used by
    /// operators and deployment tooling to identify a deployed artifact.
    About,
}

/// A typed subcommand parse failure. Preserved (not collapsed to `String`)
/// so the operator sees the malformed invocation (AGENTS.md §18). An
/// unknown subcommand surfaces the name; a missing subcommand surfaces the
/// invocation hint.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubcommandError {
    /// No subcommand was supplied (the binary was invoked with no
    /// positional argument).
    Missing,
    /// The supplied argument is not a known subcommand. The `name` is the
    /// unknown argument (never a secret — it came from argv).
    Unknown { name: String },
}

impl std::fmt::Display for SubcommandError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Missing => write!(
                f,
                "missing subcommand (expected one of: serve, migrate, queue, schedule, doctor, \
                 about)"
            ),
            Self::Unknown { name } => write!(
                f,
                "unknown subcommand {name:?} (expected one of: serve, migrate, queue, schedule, \
                 doctor, about)"
            ),
        }
    }
}

impl std::error::Error for SubcommandError {}

/// Parse the application binary subcommand from an argv iterator.
///
/// The iterator should skip the program name (i.e. start at `args().skip(1)`);
/// this mirrors `std::env::args` usage. The first positional argument is the
/// subcommand; trailing arguments are forwarded to the operation via
/// [`Operations`](super::Operations) (the dispatch passes the remaining
/// argv slice). This function parses only the subcommand selector.
///
/// Unknown subcommands return [`SubcommandError::Unknown`] (not a fallback
/// to `serve`): a deployment that invokes the wrong subcommand name should
/// fail loudly, not silently run the server (AGENTS.md §9, §24).
pub fn parse<I, T>(arguments: I) -> Result<Subcommand, SubcommandError>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    let mut iter = arguments.into_iter();
    let Some(first) = iter.next() else {
        return Err(SubcommandError::Missing);
    };
    let name = first.into().to_string_lossy().into_owned();
    match name.as_str() {
        "serve" => Ok(Subcommand::Serve),
        "migrate" => Ok(Subcommand::Migrate),
        "queue" => Ok(Subcommand::Queue),
        "schedule" => Ok(Subcommand::Schedule),
        "doctor" => Ok(Subcommand::Doctor),
        "about" => Ok(Subcommand::About),
        other => Err(SubcommandError::Unknown {
            name: other.to_owned(),
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::{Subcommand, SubcommandError, parse};

    fn parse_args(args: &[&str]) -> Result<Subcommand, SubcommandError> {
        parse(args.iter().copied())
    }

    #[test]
    fn parses_all_known_subcommands() {
        assert_eq!(parse_args(&["serve"]), Ok(Subcommand::Serve));
        assert_eq!(parse_args(&["migrate"]), Ok(Subcommand::Migrate));
        assert_eq!(parse_args(&["queue"]), Ok(Subcommand::Queue));
        assert_eq!(parse_args(&["schedule"]), Ok(Subcommand::Schedule));
        assert_eq!(parse_args(&["doctor"]), Ok(Subcommand::Doctor));
        assert_eq!(parse_args(&["about"]), Ok(Subcommand::About));
    }

    #[test]
    fn missing_subcommand_is_typed_error() {
        assert_eq!(parse_args(&[]), Err(SubcommandError::Missing));
    }

    #[test]
    fn unknown_subcommand_is_typed_error_with_name() {
        assert_eq!(
            parse_args(&["deploy"]),
            Err(SubcommandError::Unknown {
                name: "deploy".to_owned()
            })
        );
        assert_eq!(
            parse_args(&["--help"]),
            Err(SubcommandError::Unknown {
                name: "--help".to_owned()
            })
        );
    }

    #[test]
    fn trailing_arguments_do_not_change_subcommand() {
        // The parser consumes only the first positional; trailing args are
        // forwarded by the dispatcher, not parsed here.
        assert_eq!(parse_args(&["migrate", "up"]), Ok(Subcommand::Migrate));
    }
}