bijux-dag-app 0.4.1

Application orchestration and response-shaping layer for Bijux DAG command surfaces.
Documentation
use crate::commands::DagCli;
use serde::Serialize;
use serde_json::Value;
use std::process::ExitCode;

#[derive(Debug, Serialize)]
pub(crate) struct LintDiagnostic {
    pub code: String,
    pub message: String,
    pub path: String,
    pub hint: Option<String>,
}

#[derive(Debug, Serialize)]
struct JsonEnvelope {
    ok: bool,
    status: String,
    command: String,
    data: Value,
    diagnostics: Vec<Value>,
    error: Option<JsonError>,
}

#[derive(Debug, Serialize)]
struct JsonError {
    category: String,
    code: String,
    message: String,
    exit_code: u8,
}

fn build_json_envelope(
    command: &str,
    ok: bool,
    data: Value,
    diagnostics: Vec<Value>,
    code: ExitCode,
) -> JsonEnvelope {
    let exit_value = exit_code_to_u8(code);
    let error = if ok {
        None
    } else {
        let (category, stable_code) = classify_exit(command, exit_value);
        Some(JsonError {
            category: category.to_string(),
            code: stable_code.to_string(),
            message: "command execution failed".to_string(),
            exit_code: exit_value,
        })
    };
    JsonEnvelope {
        ok,
        status: if ok { "ok" } else { "invalid" }.to_string(),
        command: command.to_string(),
        data,
        diagnostics,
        error,
    }
}

pub(crate) fn emit_json(
    cli: &DagCli,
    command: &str,
    ok: bool,
    data: Value,
    diagnostics: Vec<Value>,
    code: ExitCode,
) -> Result<ExitCode, ExitCode> {
    if !cli.quiet {
        let envelope = build_json_envelope(command, ok, data, diagnostics, code);
        println!("{}", serde_json::to_string_pretty(&envelope).unwrap());
    }
    if ok {
        Ok(code)
    } else {
        Err(code)
    }
}

pub(crate) fn emit_json_line(
    command: &str,
    ok: bool,
    data: Value,
    diagnostics: Vec<Value>,
    code: ExitCode,
) {
    let envelope = build_json_envelope(command, ok, data, diagnostics, code);
    println!("{}", serde_json::to_string(&envelope).unwrap());
}

fn exit_code_to_u8(code: ExitCode) -> u8 {
    if code == ExitCode::SUCCESS {
        0
    } else if code == ExitCode::from(2) {
        2
    } else if code == ExitCode::from(3) {
        3
    } else {
        1
    }
}

fn classify_exit(command: &str, code: u8) -> (&'static str, &'static str) {
    match (command, code) {
        (_, 2) if command.contains("validate") || command.contains("lint") => {
            ("validation", "BJX-VALIDATION-001")
        }
        (_, 2) if command.contains("replay") => ("replay", "BJX-REPLAY-001"),
        (_, 2) if command.contains("cache") => ("cache", "BJX-CACHE-001"),
        (_, 2) => ("compatibility", "BJX-COMPAT-001"),
        (_, 3) => ("io", "BJX-IO-001"),
        _ => ("internal", "BJX-INTERNAL-001"),
    }
}

#[cfg(test)]
mod tests {
    use super::build_json_envelope;
    use serde_json::json;
    use std::process::ExitCode;

    #[test]
    fn json_envelope_uses_machine_stable_error_metadata() {
        let envelope = build_json_envelope(
            "dag.run",
            false,
            json!({"reason":"failed"}),
            Vec::new(),
            ExitCode::from(3),
        );

        let rendered = serde_json::to_value(&envelope).expect("serialize envelope");
        assert_eq!(rendered["status"], "invalid");
        assert_eq!(rendered["command"], "dag.run");
        assert_eq!(rendered["error"]["category"], "io");
        assert_eq!(rendered["error"]["code"], "BJX-IO-001");
        assert_eq!(rendered["error"]["exit_code"], 3);
    }
}