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 `&'static` system-check registry (AP2.1-7).
//!
//! The registry is the aggregation point: each contributor (the framework,
//! a subsystem the application uses, the application itself) exposes a
//! `pub const CHECKS: &'static [&'static dyn SystemCheck] = &[...]` slice,
//! and [`run`] merges the slices into one [`SystemCheckReport`] (see
//! `report`). There is NO global mutable state, NO `inventory` crate, NO
//! `linkme` distributed slice — the registry is a pure function over the
//! slices the caller passes (AGENTS.md §20: no hidden request-scoped or
//! global mutable state). The application wires its slices at bootstrap;
//! the CLI (and the MCP `system_checks` tool, later) calls `run` with
//! them.
//!
//! # Why no inventory/linkme
//!
//! `inventory` and `linkme` register items through distributed slices that
//! collect at link time. They are global, mutable-link-time registries —
//! exactly the kind of hidden global AGENTS.md §20 forbids, and they pull
//! new external dependencies (AGENTS.md §8). The explicit-slice approach is
//! a few lines of code, zero new dependencies, and makes the wiring visible
//! and auditable at the bootstrap site.
//!
//! # Order
//!
//! Results appear in the order the slices are passed to [`run`], and in
//! declaration order within each slice. The order is stable and
//! deterministic so a CI diff of two `arc doctor --checks --json` runs is
//! meaningful.
//!
//! # No abort on failure
//!
//! A check whose `run` returns `status = Fail` is recorded; the report
//! continues. A check that panics is caught (defensive — checks must not
//! panic, but the aggregator is robust to a misbehaving check so one bad
//! check cannot hide the rest of the report). The caught failure is
//! recorded with `status = Fail`, `severity = Error`, and a context that
//! names the check; the panic message is NOT recorded (it may carry a
//! secret).

use crate::system_check::check::SystemCheck;
use crate::system_check::report::{CheckResult, CheckStatus, SystemCheckReport};

/// Run a set of contributor slices and return the aggregated report.
///
/// Each slice is `&'static [&'static dyn SystemCheck]` — a contributor's
/// declared checks. The slices are run in order; within a slice, checks
/// are run in declaration order. The report never aborts on a single
/// check's failure or panic.
///
/// # No panic, defensively
///
/// A check's `run` MUST NOT panic (AGENTS.md §17). If one does, the
/// aggregator catches it (via [`std::panic::catch_unwind`]) and records a
/// synthetic Error result naming the check ID, so one misbehaving check
/// cannot hide the rest of the report. `catch_unwind` requires the
/// closure be `UnwindSafe`; we assert that with a wrapper. Note: a panic
/// in `#![forbid(unsafe_code)]` code is still a logic bug to fix at the
/// source — this is defense-in-depth, not permission to panic.
pub fn run(slices: &[&'static [&'static dyn SystemCheck]]) -> SystemCheckReport {
    let mut report = SystemCheckReport::default();
    for slice in slices {
        for &check in *slice {
            let result = run_one(check);
            report.results.push(result);
        }
    }
    report
}

/// Run a single check, catching a panic defensively. The panic message is
/// never recorded (it may carry a secret); only the check ID and a
/// synthetic Error result are recorded.
fn run_one(check: &'static dyn SystemCheck) -> CheckResult {
    // catch_unwind is the only `std` API for defensive panic containment
    // that does not abort the process. It requires UnwindSafe; we wrap in
    // AssertUnwindSafe because a SystemCheck is 'static + Send + Sync and
    // has no interior mutability contract to violate (checks must not
    // mutate state — see the trait docs). This is defense-in-depth against
    // a misbehaving check, not an endorsement of panicking.
    let guard = std::panic::AssertUnwindSafe(check);
    match std::panic::catch_unwind(|| {
        let check = *guard;
        check.run()
    }) {
        Ok(result) => result,
        Err(_) => CheckResult {
            id: check.id(),
            category: check.category(),
            severity: check.severity(),
            status: CheckStatus::Fail,
            description: check.description(),
            context: format!(
                "check {} panicked during run; the check is buggy and must be fixed",
                check.id()
            ),
            fix_hint: Some("report this as a bug in the contributing crate".to_string()),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::system_check::category::CheckCategory;
    use crate::system_check::id::CheckId;
    use crate::system_check::report::CheckResult;
    use crate::system_check::severity::CheckSeverity;

    struct PassCheck {
        id: &'static str,
    }
    impl SystemCheck for PassCheck {
        fn id(&self) -> CheckId {
            CheckId::new_unchecked(self.id)
        }
        fn severity(&self) -> CheckSeverity {
            CheckSeverity::Info
        }
        fn category(&self) -> CheckCategory {
            CheckCategory::Framework
        }
        fn description(&self) -> &'static str {
            "a passing check"
        }
        fn run(&self) -> CheckResult {
            CheckResult {
                id: self.id(),
                category: self.category(),
                severity: self.severity(),
                status: CheckStatus::Pass,
                description: self.description(),
                context: "ok".to_string(),
                fix_hint: None,
            }
        }
    }

    struct FailCheck {
        id: &'static str,
        severity: CheckSeverity,
    }
    impl SystemCheck for FailCheck {
        fn id(&self) -> CheckId {
            CheckId::new_unchecked(self.id)
        }
        fn severity(&self) -> CheckSeverity {
            self.severity
        }
        fn category(&self) -> CheckCategory {
            CheckCategory::Security
        }
        fn description(&self) -> &'static str {
            "a failing check"
        }
        fn run(&self) -> CheckResult {
            CheckResult {
                id: self.id(),
                category: self.category(),
                severity: self.severity(),
                status: CheckStatus::Fail,
                description: self.description(),
                context: "something is wrong".to_string(),
                fix_hint: Some("fix it".to_string()),
            }
        }
    }

    struct SkipCheck;
    impl SystemCheck for SkipCheck {
        fn id(&self) -> CheckId {
            CheckId::new_unchecked("ARC0003")
        }
        fn severity(&self) -> CheckSeverity {
            CheckSeverity::Info
        }
        fn category(&self) -> CheckCategory {
            CheckCategory::Database
        }
        fn description(&self) -> &'static str {
            "a skipped check"
        }
        fn run(&self) -> CheckResult {
            CheckResult {
                id: self.id(),
                category: self.category(),
                severity: self.severity(),
                status: CheckStatus::Skip,
                description: self.description(),
                context: "database not enabled".to_string(),
                fix_hint: None,
            }
        }
    }

    #[test]
    fn run_aggregates_multiple_slices_in_order() {
        let slice_a: &'static [&'static dyn SystemCheck] = &[&PassCheck { id: "ARC0001" }];
        let slice_b: &'static [&'static dyn SystemCheck] = &[
            &FailCheck {
                id: "ARC0002",
                severity: CheckSeverity::Warning,
            },
            &SkipCheck,
        ];
        let report = run(&[slice_a, slice_b]);
        assert_eq!(report.len(), 3);
        assert_eq!(report.results[0].id.as_str(), "ARC0001");
        assert_eq!(report.results[1].id.as_str(), "ARC0002");
        assert_eq!(report.results[2].id.as_str(), "ARC0003");
    }

    #[test]
    fn run_does_not_abort_on_a_failing_check() {
        let slice: &'static [&'static dyn SystemCheck] = &[
            &FailCheck {
                id: "ARC0002",
                severity: CheckSeverity::Error,
            },
            &PassCheck { id: "ARC0001" },
        ];
        let report = run(&[slice]);
        assert_eq!(report.len(), 2);
        assert!(report.has_error());
        assert_eq!(report.results[1].status, CheckStatus::Pass);
    }

    #[test]
    fn run_has_error_only_for_error_severity_fails() {
        let slice: &'static [&'static dyn SystemCheck] = &[&FailCheck {
            id: "ARC0002",
            severity: CheckSeverity::Warning,
        }];
        let report = run(&[slice]);
        assert!(!report.has_error());
        assert!(report.has_failure());
    }

    #[test]
    fn run_with_no_slices_is_empty() {
        let report = run(&[]);
        assert!(report.is_empty());
    }
}