cellos-ctl 0.6.0-pre

cellctl — kubectl-style CLI for CellOS execution cells and formations. Thin HTTP client over cellos-server with apply/get/describe/logs/events/webui.
Documentation
//! `cellctl audit` — operator surface over the cross-domain audit bundle
//! library (S19/S20, ADR-0029).
//!
//! These are **local file operations** (no server client): the export/import/
//! verify logic — and its tests — live in `cellos-sink-spool::bundle`. This
//! module is thin plumbing: it parses operator flags, calls the library, maps
//! its [`cellos_core::CellosError`] into the doctrine exit taxonomy, and prints
//! a one-line human result to stdout.
//!
//! Like `cellctl config` and `cellctl webui`, `audit` bypasses the
//! `CellosClient` dispatch — there is nothing to talk to but the local spool
//! directory.
//!
//! ## Exit-code mapping
//!
//! * `export-bundle` / `import-bundle` failures (bad range, continuity break,
//!   tampered/gap'd bundle, I/O) are local validation/preflight failures →
//!   [`CtlError::validation`] (`Exit::Preflight`, code 3).
//! * `verify-chain` reads the chain and reports a [`ChainStatus`]; a chain that
//!   does NOT verify is an assertion failure, not a usage error. axiom-exit
//!   reserves code 1 (`Exit::AssertionFailed`) for exactly this, so a failed
//!   verify exits 1 directly (it never goes through `CtlError`, which has no
//!   assertion variant).

use std::path::Path;

use cellos_sink_spool::bundle::{self, AuditBundle};

use crate::exit::{CtlError, CtlResult};

/// `cellctl audit export-bundle` — export a contiguous chain range as a bundle
/// file. Pure-local: reads the spool, writes a JSON bundle.
pub fn export_bundle(
    dir: &Path,
    chain_id: &str,
    from_seq: u64,
    to_seq: Option<u64>,
    out: &Path,
) -> CtlResult<()> {
    let bundle = bundle::export_bundle(dir, chain_id, from_seq, to_seq)
        .map_err(|e| CtlError::validation(format!("export-bundle: {e}")))?;
    // Pretty JSON so the bundle file is diffable / sneakernet-inspectable.
    let json = serde_json::to_string_pretty(&bundle)
        .map_err(|e| CtlError::validation(format!("export-bundle: serialize: {e}")))?;
    std::fs::write(out, json)
        .map_err(|e| CtlError::usage(format!("export-bundle: write {}: {e}", out.display())))?;
    let m = &bundle.manifest;
    println!(
        "exported {} rows [seq {}..={}] of chain {} to {}",
        m.row_count,
        m.from_seq,
        m.to_seq,
        m.chain_id,
        out.display()
    );
    Ok(())
}

/// `cellctl audit import-bundle` — import a bundle file into the local spool.
/// Fail-closed: a rejected bundle appends nothing (enforced by the library).
pub fn import_bundle(dir: &Path, input: &Path) -> CtlResult<()> {
    let raw = std::fs::read_to_string(input)
        .map_err(|e| CtlError::usage(format!("import-bundle: read {}: {e}", input.display())))?;
    let bundle: AuditBundle = serde_json::from_str(&raw)
        .map_err(|e| CtlError::validation(format!("import-bundle: parse bundle: {e}")))?;
    let appended = bundle::import_bundle(dir, &bundle)
        .map_err(|e| CtlError::validation(format!("import-bundle: {e}")))?;
    println!("imported {appended} rows into {}", dir.display());
    Ok(())
}

/// `cellctl audit verify-chain` — verify the on-disk chain and report status.
///
/// Returns `Ok(())` only when the chain verifies. A non-verifying chain is an
/// assertion failure: it prints the break to stderr and exits 1
/// (`Exit::AssertionFailed`) directly, because the [`CtlError`] taxonomy has no
/// assertion variant (it covers usage/api/validation only).
pub fn verify_chain(dir: &Path) -> CtlResult<()> {
    let status =
        bundle::verify_chain(dir).map_err(|e| CtlError::usage(format!("verify-chain: {e}")))?;
    if status.ok {
        match status.verified_high_seq {
            Some(high) => println!(
                "chain OK: verified seq 0..={high} (head {})",
                status.head_row_hash
            ),
            None => println!("chain OK: empty chain"),
        }
        Ok(())
    } else {
        let gap = status.gap.as_deref().unwrap_or("unknown break");
        let high = status
            .verified_high_seq
            .map_or_else(|| "none".to_string(), |s| s.to_string());
        eprintln!("cellctl: assertion: chain FAILED: {gap} (verified high seq: {high})");
        std::process::exit(axiom_exit::Exit::AssertionFailed.code().into())
    }
}