octra-sqlite 0.5.2

Real SQLite inside an Octra Circle, with a Rust CLI and client library
Documentation
fn main() {
    match octra_sqlite::cli::run_with_exit_code() {
        Ok(0) => {}
        Ok(code) => std::process::exit(code),
        Err(error) => {
            let message = format!("{error:#}");
            if wants_json_error() {
                eprintln!(
                    "{}",
                    serde_json::json!({
                        "ok": false,
                        "type": "error",
                        "schema": "octra-sqlite.cli.v1",
                        "exit_code": 1,
                        "error": {
                            "code": classify_error(&message),
                            "message": message,
                        }
                    })
                );
            } else {
                eprintln!("error: {message}");
            }
            std::process::exit(1);
        }
    }
}

fn wants_json_error() -> bool {
    std::env::args().any(|arg| is_json_error_arg(&arg))
}

fn is_json_error_arg(arg: &str) -> bool {
    arg == "--json" || arg == "--json-summary"
}

fn classify_error(message: &str) -> &'static str {
    let lower = message.to_ascii_lowercase();
    if lower.contains("http 429")
        || lower.contains("too many requests")
        || lower.contains("rate limit")
        || lower.contains("rate-limited")
    {
        "rpc_rate_limited"
    } else if lower.contains("non-json") || lower.contains("html") {
        "rpc_non_json"
    } else if message.contains("bootstrap first write was submitted")
        && message.contains("post-write auth_info")
    {
        "bootstrap_unverified"
    } else if lower.contains("already_bootstrapped")
        || lower.contains("already bootstrapped")
        || lower.contains("auth_info is already readable")
    {
        "bootstrap_already_done"
    } else if lower.contains("missing storage cache") || lower.contains("storage_uninitialized") {
        "storage_uninitialized"
    } else if lower.contains("auth_uninitialized") || lower.contains("auth_info") {
        "auth_uninitialized"
    } else if message.contains("Octra SQLite accepts at most")
        || message.contains("SQL payload")
        || message.contains("SQL statement")
    {
        "sql_too_large"
    } else if message.contains("transactions_not_supported") {
        "transactions_not_supported"
    } else if message.contains("read_only") {
        "read_only"
    } else if message.contains("result_limit_exceeded") {
        "result_limit_exceeded"
    } else if message.contains("response_too_large") {
        "result_too_large"
    } else if message.contains("Authorization")
        || message.contains("authorization")
        || message.contains("owner")
        || message.contains("not owner")
    {
        "auth_failed"
    } else if message.contains("SQL execution failed")
        || message.contains("circle_call_failed")
        || message.contains("receipt")
    {
        "circle_write_failed"
    } else if message.contains("sqlite_") || message.contains("database error") {
        "sql_rejected"
    } else if message.contains("wallet") {
        "wallet_error"
    } else if message.contains("unknown database")
        || message.contains("resolving database")
        || message.contains("database URI")
    {
        "target_error"
    } else if message.contains("timed out") || message.contains("timeout") {
        "timeout"
    } else if message.contains("decoding") || message.contains("decode") {
        "decode_error"
    } else if message.contains("calling ")
        || message.contains("HTTP")
        || message.contains("transport")
    {
        "rpc_unavailable"
    } else if message.contains("RPC") || message.contains("rpc") {
        "rpc_error"
    } else if message.contains("config") || message.contains("Config") {
        "config_error"
    } else {
        "command_failed"
    }
}

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

    #[test]
    fn json_errors_are_requested_by_summary_output_too() {
        assert!(is_json_error_arg("--json"));
        assert!(is_json_error_arg("--json-summary"));
        assert!(!is_json_error_arg("--trace-rpc-json"));
    }

    #[test]
    fn error_classification_uses_stable_automation_codes() {
        assert_eq!(
            classify_error("database error (result_limit_exceeded): query returned too many rows"),
            "result_limit_exceeded"
        );
        assert_eq!(
            classify_error(
                "database error (response_too_large): typed query result exceeded contract response buffer"
            ),
            "result_too_large"
        );
        assert_eq!(
            classify_error("database error (sqlite_prepare_failed): no such table: demo"),
            "sql_rejected"
        );
        assert_eq!(
            classify_error("read_only: SQL would write; remove --read-only"),
            "read_only"
        );
        assert_eq!(
            classify_error("calling octra_circleViewAuth: connection refused"),
            "rpc_unavailable"
        );
        assert_eq!(
            classify_error("octra_circleViewAuth failed with HTTP 429 Too Many Requests"),
            "rpc_rate_limited"
        );
        assert_eq!(
            classify_error("decoding octra_circleViewAuth non-JSON response from HTTP 503"),
            "rpc_non_json"
        );
        assert_eq!(
            classify_error("octra_circleViewAuth failed: missing storage cache: octABC:0000"),
            "storage_uninitialized"
        );
        assert_eq!(
            classify_error(
                "bootstrap first write was submitted but post-write auth_info still failed"
            ),
            "bootstrap_unverified"
        );
    }
}