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
//! Framework-contributed system checks (AP2.1-7).
//!
//! A small set of checks the `arcature` framework itself contributes. They
//! inspect framework-internal state that is always available (no external
//! service, no feature gate beyond `serde`) so they run on every `arc
//! doctor --checks` invocation. Subsystem-specific checks (database,
//! cache, …) are contributed by the application's bootstrap wiring each
//! subsystem it uses; those are future work gated on AP2.1-6 (Data) and
//! later subsystem phases.
//!
//! # The `CHECKS` slice
//!
//! [`CHECKS`] is the `&'static [&'static dyn SystemCheck]` slice the CLI
//! (and the MCP `system_checks` tool, later) passes to
//! [`run`](super::registry::run) alongside the application's own slices.

use crate::system_check::category::CheckCategory;
use crate::system_check::check::SystemCheck;
use crate::system_check::id::CheckId;
use crate::system_check::report::{CheckResult, CheckStatus};
use crate::system_check::severity::CheckSeverity;

/// `ARC0001` — the framework version is recorded.
///
/// The Unified Application Graph manifest records `FRAMEWORK_VERSION` so
/// frontend tooling and `@arcature/client` can negotiate cross-stack
/// compatibility (ADR-0006 §2). This check verifies the constant is
/// non-empty and parseable as a YBF version, so a build that accidentally
/// blanked it is caught at `arc doctor --checks` rather than at a
/// confusing cross-stack negotiation failure.
struct FrameworkVersionCheck;

impl SystemCheck for FrameworkVersionCheck {
    fn id(&self) -> CheckId {
        CheckId::new_unchecked("ARC0001")
    }
    fn severity(&self) -> CheckSeverity {
        CheckSeverity::Error
    }
    fn category(&self) -> CheckCategory {
        CheckCategory::Framework
    }
    fn description(&self) -> &'static str {
        "framework version is recorded as a non-empty YBF string"
    }
    fn run(&self) -> CheckResult {
        let version = crate::FRAMEWORK_VERSION;
        if version.is_empty() {
            return CheckResult {
                id: self.id(),
                category: self.category(),
                severity: self.severity(),
                status: CheckStatus::Fail,
                description: self.description(),
                context: "FRAMEWORK_VERSION is empty".to_string(),
                fix_hint: Some(
                    "the arcature crate's CARGO_PKG_VERSION is empty; rebuild with a real version"
                        .to_string(),
                ),
            };
        }
        if !is_plausible_ybf(version) {
            return CheckResult {
                id: self.id(),
                category: self.category(),
                severity: self.severity(),
                status: CheckStatus::Fail,
                description: self.description(),
                context: format!("FRAMEWORK_VERSION is not a YBF version: {version}"),
                fix_hint: Some(
                    "expected YEAR.BREAK.FIX (e.g. 2026.1.0); rebuild with a real YBF version"
                        .to_string(),
                ),
            };
        }
        CheckResult {
            id: self.id(),
            category: self.category(),
            severity: self.severity(),
            status: CheckStatus::Pass,
            description: self.description(),
            context: format!("framework {version}"),
            fix_hint: None,
        }
    }
}

/// `ARC0002` — the stable span-name contract is non-empty.
///
/// The Inspector / MCP / Realtime lanes consume the stable span names from
/// `arcature-observe`. This check verifies the framework's own span
/// contract is reachable and non-empty — a build that accidentally emptied
/// it (a feature-gate bug) is caught here rather than producing an
/// Inspector with no spans to correlate. Lives behind `observe` because it
/// reads the observe subsystem's contract.
#[cfg(feature = "observe")]
struct SpanContractCheck;

#[cfg(feature = "observe")]
impl SystemCheck for SpanContractCheck {
    fn id(&self) -> CheckId {
        CheckId::new_unchecked("ARC0002")
    }
    fn severity(&self) -> CheckSeverity {
        CheckSeverity::Warning
    }
    fn category(&self) -> CheckCategory {
        CheckCategory::Observability
    }
    fn description(&self) -> &'static str {
        "stable Arcature span-name contract is non-empty"
    }
    fn run(&self) -> CheckResult {
        let spans = crate::observe::spans::ALL;
        if spans.is_empty() {
            return CheckResult {
                id: self.id(),
                category: self.category(),
                severity: self.severity(),
                status: CheckStatus::Fail,
                description: self.description(),
                context: "the span-name contract is empty".to_string(),
                fix_hint: Some(
                    "arcature-observe's spans::ALL is empty; the Inspector and MCP traces tool \
                     will have no spans to correlate"
                        .to_string(),
                ),
            };
        }
        CheckResult {
            id: self.id(),
            category: self.category(),
            severity: self.severity(),
            status: CheckStatus::Pass,
            description: self.description(),
            context: format!("{} stable span names registered", spans.len()),
            fix_hint: None,
        }
    }
}

/// Returns `true` iff `version` looks like a YBF `YEAR.BREAK.FIX` string
/// (three dot-separated non-empty numeric components). This is a
/// plausibility check, not a full semver parser — a deeper check lives in
/// the release tooling.
fn is_plausible_ybf(version: &str) -> bool {
    let parts: Vec<&str> = version.split('.').collect();
    parts.len() == 3
        && parts
            .iter()
            .all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
}

/// The framework-contributed system checks, as a `&'static` slice for
/// [`run`](super::registry::run).
pub const CHECKS: &[&dyn SystemCheck] = {
    &[
        &FrameworkVersionCheck,
        #[cfg(feature = "observe")]
        &SpanContractCheck,
    ]
};

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

    #[test]
    fn framework_version_check_passes_on_a_real_ybf_version() {
        // The crate's own FRAMEWORK_VERSION is "2026.1.0" in this workspace;
        // this also proves the check reads the real constant.
        let check = FrameworkVersionCheck;
        let result = check.run();
        assert_eq!(result.id.as_str(), "ARC0001");
        assert_eq!(result.status, CheckStatus::Pass, "{}", result.context);
        assert!(result.context.contains("framework"));
    }

    #[test]
    fn framework_version_check_fails_on_empty_version() {
        // Direct logic test (not via the real constant): empty is a fail.
        assert!(!is_plausible_ybf(""));
        assert!(!is_plausible_ybf("2026.1"));
        assert!(!is_plausible_ybf("2026.1.0.0"));
        assert!(!is_plausible_ybf("v2026.1.0"));
        assert!(is_plausible_ybf("2026.1.0"));
        assert!(is_plausible_ybf("2026.12.999"));
    }

    #[test]
    fn checks_slice_is_non_empty_and_well_formed() {
        assert!(!CHECKS.is_empty());
        for check in CHECKS {
            // Every ID is well-formed (the const slice uses new_unchecked,
            // so this is the validation the const path skips).
            assert!(crate::system_check::id::CheckId::new(check.id().as_str()).is_ok());
            assert!(!check.description().is_empty());
        }
    }

    #[cfg(feature = "observe")]
    #[test]
    fn span_contract_check_passes_when_observe_is_enabled() {
        let check = SpanContractCheck;
        let result = check.run();
        assert_eq!(result.id.as_str(), "ARC0002");
        assert_eq!(result.status, CheckStatus::Pass);
        assert!(result.context.contains("span names"));
    }
}