arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
//! Compare runtime state against the Certified Stack Contract.
//!
//! Reads a project's `Cargo.lock` and `frontend/package.json` and reports each
//! certified component's actual version against the contract version. A
//! mismatch is `UNVERIFIED OVERRIDE` — not an error — because certification is
//! information policy, not ecosystem lock-in. Cargo overrides are not
//! prevented; they are reported clearly.

use std::collections::BTreeMap;

use serde::Deserialize;

use super::contract::Contract;
use crate::tool::{HealthReport, HealthStatus};

/// Check the project's `Cargo.lock` against the contract and append findings
/// to the report. Components not present in the lockfile are skipped (a
/// no-DB app has no sqlx in its lockfile).
pub(crate) fn check_cargo_lock(
    contract: &Contract,
    lockfile_path: &std::path::Path,
    report: &mut HealthReport,
) {
    let packages = match read_lockfile(lockfile_path) {
        Ok(packages) => packages,
        Err(error) => {
            report.push(
                "cargo.lock",
                HealthStatus::Warning,
                format!("cannot read {}: {error}", lockfile_path.display()),
            );
            return;
        }
    };
    for (name, component) in &contract.components {
        let lockfile_name = name.replace('-', "_");
        if let Some(actual) = packages.get(&lockfile_name) {
            // Known-incompatible versions are errors, not mere unverified.
            for entry in &contract.incompatible.entries {
                if entry.component == *name && version_matches(&entry.version, actual) {
                    report.push(
                        name,
                        HealthStatus::Error,
                        format!("{actual} is KNOWN INCOMPATIBLE: {}", entry.reason),
                    );
                }
            }
            compare_version(report, name, &component.version, actual);
        }
    }
}

/// Check the frontend `package.json` dependencies against the contract.
pub(crate) fn check_frontend(
    contract: &Contract,
    package_json_path: &std::path::Path,
    report: &mut HealthReport,
) {
    let manifest = match read_package_json(package_json_path) {
        Ok(manifest) => manifest,
        Err(error) => {
            report.push(
                "frontend",
                HealthStatus::Warning,
                format!("cannot read {}: {error}", package_json_path.display()),
            );
            return;
        }
    };
    let checks: &[(&str, &str)] = &[
        ("vite", "vite"),
        ("typescript", "typescript"),
        ("react", "react"),
        ("react_dom", "react-dom"),
        ("vue", "vue"),
        ("inertia_react", "@inertiajs/react"),
        ("inertia_vue", "@inertiajs/vue3"),
    ];
    for (contract_name, npm_name) in checks {
        let Some(component) = contract.components.get(*contract_name) else {
            continue;
        };
        let actual = manifest
            .dependencies
            .get(*npm_name)
            .or_else(|| manifest.dev_dependencies.get(*npm_name));
        if let Some(actual) = actual {
            compare_version(report, contract_name, &component.version, actual);
        }
    }
}

fn compare_version(report: &mut HealthReport, name: &str, certified: &str, actual: &str) {
    if version_matches(certified, actual) {
        report.push(name, HealthStatus::Ok, format!("{actual} (certified)"));
    } else {
        report.push(
            name,
            HealthStatus::Unverified,
            format!("{actual} (certified: {certified}) — UNVERIFIED OVERRIDE"),
        );
    }
}

/// Check if an actual version matches the contract version. The contract may
/// specify an exact version (`"0.8.9"`) or a range (`">=24.19.0,<25"`).
fn version_matches(certified: &str, actual: &str) -> bool {
    if certified.contains('<') || certified.contains('>') {
        return range_matches(certified, actual);
    }
    // Exact match: compare numeric components, ignoring leading 'v' or
    // prefixes in the actual version (e.g. "^0.8.9" → "0.8.9").
    let clean_actual = actual.trim_start_matches(['^', '~', '=', 'v', ' ']);
    let clean_certified = certified.trim_start_matches('v');
    semver_eq(clean_certified, clean_actual)
}

/// Parse a simple `>=min,<max` range and check if `actual` falls within it.
fn range_matches(range: &str, actual: &str) -> bool {
    let clean = actual.trim_start_matches(['^', '~', '=', 'v', ' ']);
    let actual_parts: Vec<u64> = clean
        .split(['.', '-'])
        .filter_map(|p| p.parse().ok())
        .collect();
    for bound in range.split(',') {
        let bound = bound.trim();
        if let Some(min) = bound.strip_prefix(">=") {
            let min_parts: Vec<u64> = min
                .trim()
                .split('.')
                .filter_map(|p| p.parse().ok())
                .collect();
            if !gte(&actual_parts, &min_parts) {
                return false;
            }
        } else if let Some(max) = bound.strip_prefix("<") {
            let max_parts: Vec<u64> = max
                .trim()
                .split('.')
                .filter_map(|p| p.parse().ok())
                .collect();
            if gte(&actual_parts, &max_parts) {
                return false;
            }
        }
    }
    true
}

fn gte(a: &[u64], b: &[u64]) -> bool {
    for i in 0..b.len().max(a.len()) {
        let av = a.get(i).copied().unwrap_or(0);
        let bv = b.get(i).copied().unwrap_or(0);
        if av != bv {
            return av > bv;
        }
    }
    true
}

fn semver_eq(a: &str, b: &str) -> bool {
    let pa: Vec<u64> = a.split(['.', '-']).filter_map(|p| p.parse().ok()).collect();
    let pb: Vec<u64> = b.split(['.', '-']).filter_map(|p| p.parse().ok()).collect();
    gte(&pa, &pb) && gte(&pb, &pa)
}

#[derive(Debug, Deserialize)]
struct Lockfile {
    package: Vec<LockfilePackage>,
}

#[derive(Debug, Deserialize)]
struct LockfilePackage {
    name: String,
    version: String,
}

fn read_lockfile(
    path: &std::path::Path,
) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string(path)?;
    let lockfile: Lockfile = toml::from_str(&content)?;
    let mut map = BTreeMap::new();
    for package in lockfile.package {
        map.entry(package.name).or_insert(package.version);
    }
    Ok(map)
}

#[derive(Debug, Deserialize)]
struct PackageJson {
    #[serde(default)]
    dependencies: BTreeMap<String, String>,
    #[serde(default, rename = "devDependencies")]
    dev_dependencies: BTreeMap<String, String>,
}

fn read_package_json(path: &std::path::Path) -> Result<PackageJson, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string(path)?;
    Ok(serde_json::from_str(&content)?)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::stack::contract::load;

    #[test]
    fn exact_version_match() {
        assert!(version_matches("0.8.9", "0.8.9"));
        assert!(version_matches("0.8.9", "^0.8.9"));
        assert!(version_matches("0.8.9", "v0.8.9"));
        assert!(!version_matches("0.8.9", "0.8.8"));
    }

    #[test]
    fn range_match() {
        assert!(version_matches(">=24.19.0,<25", "24.19.0"));
        assert!(version_matches(">=24.19.0,<25", "24.20.0"));
        assert!(!version_matches(">=24.19.0,<25", "25.0.0"));
        assert!(!version_matches(">=24.19.0,<25", "24.18.0"));
    }

    #[test]
    fn unverified_override_is_reported_not_error() {
        let mut report = HealthReport::default();
        compare_version(&mut report, "axum", "0.8.9", "0.8.8");
        assert_eq!(report.checks[0].status, HealthStatus::Unverified);
        assert!(report.checks[0].detail.contains("UNVERIFIED OVERRIDE"));
        assert!(!report.has_error());
    }

    #[test]
    fn certified_match_is_ok() {
        let mut report = HealthReport::default();
        compare_version(&mut report, "axum", "0.8.9", "0.8.9");
        assert_eq!(report.checks[0].status, HealthStatus::Ok);
        assert!(!report.has_error());
    }

    // --- Upgrade-compatibility tests using frozen fixtures ---

    const CERTIFIED_LOCKFILE: &str = include_str!("fixtures/certified.lock");
    const OVERRIDDEN_LOCKFILE: &str = include_str!("fixtures/overridden.lock");
    const CERTIFIED_PACKAGE_JSON: &str = include_str!("fixtures/package.json");
    const OVERRIDDEN_PACKAGE_JSON: &str = include_str!("fixtures/package_overridden.json");

    fn write_temp(content: &str, name: &str) -> std::path::PathBuf {
        let path =
            std::env::temp_dir().join(format!("arcature-upgrade-{name}-{}", std::process::id()));
        std::fs::write(&path, content).expect("temp file should be written");
        path
    }

    #[test]
    fn certified_lockfile_reports_all_pass() {
        let contract = load();
        let lockfile = write_temp(CERTIFIED_LOCKFILE, "certified.lock");
        let mut report = HealthReport::default();
        check_cargo_lock(&contract, &lockfile, &mut report);
        let _ = std::fs::remove_file(&lockfile);
        let unverified: Vec<_> = report
            .checks
            .iter()
            .filter(|c| c.detail.contains("UNVERIFIED OVERRIDE"))
            .collect();
        assert!(
            unverified.is_empty(),
            "certified lockfile has unverified: {:?}",
            unverified.iter().map(|c| &c.name).collect::<Vec<_>>()
        );
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.name == "axum" && c.detail.contains("certified"))
        );
    }

    #[test]
    fn overridden_lockfile_reports_unverified_override() {
        let contract = load();
        let lockfile = write_temp(OVERRIDDEN_LOCKFILE, "overridden.lock");
        let mut report = HealthReport::default();
        check_cargo_lock(&contract, &lockfile, &mut report);
        let _ = std::fs::remove_file(&lockfile);
        let axum = report
            .checks
            .iter()
            .find(|c| c.name == "axum")
            .expect("axum should be in the report");
        assert!(
            axum.detail.contains("UNVERIFIED OVERRIDE"),
            "overridden axum should be UNVERIFIED: {}",
            axum.detail
        );
        assert!(!report.has_error(), "override is not an error");
    }

    #[test]
    fn certified_frontend_reports_all_pass() {
        let contract = load();
        let package_json = write_temp(CERTIFIED_PACKAGE_JSON, "package.json");
        let mut report = HealthReport::default();
        check_frontend(&contract, &package_json, &mut report);
        let _ = std::fs::remove_file(&package_json);
        let unverified: Vec<_> = report
            .checks
            .iter()
            .filter(|c| c.detail.contains("UNVERIFIED OVERRIDE"))
            .collect();
        assert!(
            unverified.is_empty(),
            "certified frontend has unverified: {:?}",
            unverified.iter().map(|c| &c.name).collect::<Vec<_>>()
        );
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.name == "vite" && c.detail.contains("certified"))
        );
    }

    #[test]
    fn overridden_frontend_reports_unverified_override() {
        let contract = load();
        let package_json = write_temp(OVERRIDDEN_PACKAGE_JSON, "package_overridden.json");
        let mut report = HealthReport::default();
        check_frontend(&contract, &package_json, &mut report);
        let _ = std::fs::remove_file(&package_json);
        let vite = report
            .checks
            .iter()
            .find(|c| c.name == "vite")
            .expect("vite should be in the report");
        assert!(
            vite.detail.contains("UNVERIFIED OVERRIDE"),
            "overridden vite should be UNVERIFIED: {}",
            vite.detail
        );
        assert!(!report.has_error(), "override is not an error");
    }

    #[test]
    fn contract_services_matrix_is_complete() {
        let contract = load();
        assert_eq!(
            contract.services["postgresql"].certified_versions,
            vec!["15.13", "16.9", "17.5", "18.4"]
        );
        assert!(
            contract.services["valkey"]
                .certified_versions
                .contains(&"8.1.2".to_owned())
        );
        assert!(
            contract.services["valkey"]
                .certified_versions
                .contains(&"9.1.1".to_owned())
        );
    }
}