arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
//! `arc db lint` — classify migration SQL for PostgreSQL risks (AP2.1-6).
//!
//! Reads a SQL migration script from **stdin**, classifies it with
//! [`arcature_data::classify`] (deterministic, database-free, never panics),
//! and prints the [`LintReport`]. `--json` emits machine-readable output.
//!
//! # Side-effect-free
//!
//! `arc db lint` never connects to a database and never reads environment
//! variables — it is pure SQL-text analysis (PROGRAM.md AP2.1-6: the shadow-DB
//! `diff`/`drift`/`verify` paths are deferred; `lint` surfaces destructive
//! changes explicitly without assuming a reverse migration). Surfacing a
//! destructive change is the deliverable; the operator decides whether to
//! apply.
//!
//! # Exit code
//!
//! The command succeeds (exit 0) when the report is clean or has only
//! non-critical findings; it fails (exit 1 via the CLI error path) when at
//! least one **critical** finding is present, so CI can gate on destructive
//! migrations.

use std::io::Read;

use arcature_data::LintReport;

use crate::cli::OutputFormat;
use crate::error::CommandError;

/// Run `arc db lint`. Reads SQL from stdin; classifies; prints the report.
pub(crate) fn run_lint(format: OutputFormat) -> Result<(), CommandError> {
    let mut sql = String::new();
    std::io::stdin()
        .read_to_string(&mut sql)
        .map_err(|source| CommandError::Db(crate::error::DbCommandError::ReadStdin(source)))?;
    let report: LintReport = arcature_data::classify(&sql);
    print_report(&report, format)?;
    if report.has_critical() {
        return Err(CommandError::Db(crate::error::DbCommandError::LintCritical));
    }
    Ok(())
}

fn print_report(report: &LintReport, format: OutputFormat) -> Result<(), CommandError> {
    match format {
        OutputFormat::Human => println!("{report}"),
        OutputFormat::Json => {
            let json = serde_json::to_string_pretty(report)?;
            println!("{json}");
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use arcature_data::{LintCategory, LintSeverity, classify};

    /// The classifier surfaces a `DROP TABLE` as a critical finding — the
    /// finding `arc db lint` would gate CI on.
    #[test]
    fn classify_flags_drop_table_critical() {
        let report = classify("DROP TABLE users;");
        assert!(report.has_critical());
        assert_eq!(report.findings[0].category, LintCategory::DestructiveDrop);
        assert_eq!(report.findings[0].severity, LintSeverity::Critical);
    }

    /// A clean migration has no findings — `arc db lint` would exit 0.
    #[test]
    fn classify_clean_migration_is_clean() {
        let report = classify("CREATE TABLE t (id int);\nCREATE INDEX CONCURRENTLY i ON t (c);");
        assert!(report.is_clean());
    }

    /// The report serializes to JSON (the `--json` output path).
    #[test]
    fn report_serializes_to_json() {
        let report = classify("DROP TABLE x;");
        let json = serde_json::to_string(&report).expect("serialize");
        assert!(json.contains("destructive-drop"));
        assert!(json.contains("critical"));
    }
}