bijux-dag-app 0.4.1

Application orchestration and response-shaping layer for Bijux DAG command surfaces.
Documentation
use crate::commands::DagCli;
use crate::{emit_json, load_graphs_or_emit, ExitCode};
use bijux_dag_core::Severity;
use serde_json::{json, Value};
use std::path::PathBuf;

pub(crate) fn handle_validate_command(
    cli: &DagCli,
    dags: &[PathBuf],
    strict: bool,
    print_fingerprints: bool,
    explain: bool,
) -> Result<ExitCode, ExitCode> {
    let graph = load_graphs_or_emit(cli, "dag.validate", dags)?;
    let diags = graph.validate_with_warnings();
    let has_errors = diags.iter().any(|d| d.severity == Severity::Error);
    let has_warnings = diags.iter().any(|d| d.severity == Severity::Warning);
    let fail = has_errors || (strict && has_warnings);

    let diagnostics: Vec<Value> = diags.iter().map(|d| serde_json::to_value(d).unwrap()).collect();
    let mut data = json!({});
    if print_fingerprints || explain {
        data["graph_fingerprint"] = json!(graph.graph_fingerprint().unwrap());
        let mut nodes = serde_json::Map::new();
        let resolved = graph.resolve_graph().ok().map(|g| g.resolved_params);
        for n in &graph.nodes {
            let fp = resolved
                .as_ref()
                .and_then(|m| m.get(&n.id))
                .and_then(|p| graph.node_fingerprint_with_params(n, p).ok())
                .unwrap_or_else(|| graph.node_fingerprint(n).unwrap());
            nodes.insert(n.id.clone(), json!(fp));
        }
        data["node_fingerprints"] = json!(nodes);
    }
    if explain {
        let canonical = graph.canonicalize();
        let order = canonical.nodes.iter().map(|n| n.id.clone()).collect::<Vec<_>>();
        data["canonical_order"] = json!(order);
        data["resolved_params"] =
            json!(graph.resolve_graph().map(|g| g.resolved_params).unwrap_or_default());
    }
    if cli.json {
        let code = if fail { ExitCode::from(2) } else { ExitCode::SUCCESS };
        return emit_json(cli, "dag.validate", !fail, data, diagnostics, code);
    } else if !cli.quiet {
        for d in &diags {
            println!("{} {} {}", d.code, d.path, d.message);
        }
        if print_fingerprints {
            println!("graph_fingerprint={}", graph.graph_fingerprint().unwrap());
            let resolved = graph.resolve_graph().ok().map(|g| g.resolved_params);
            for n in &graph.nodes {
                let fp = resolved
                    .as_ref()
                    .and_then(|m| m.get(&n.id))
                    .and_then(|p| graph.node_fingerprint_with_params(n, p).ok())
                    .unwrap_or_else(|| graph.node_fingerprint(n).unwrap());
                println!("node_fingerprint {}={}", n.id, fp);
            }
        }
        if explain {
            let canonical = graph.canonicalize();
            let order = canonical.nodes.iter().map(|n| n.id.clone()).collect::<Vec<_>>();
            println!("canonical_order: {:?}", order);
            println!(
                "resolved_params: {}",
                serde_json::to_string_pretty(
                    &graph.resolve_graph().map(|g| g.resolved_params).unwrap_or_default()
                )
                .unwrap()
            );
            println!("graph_fingerprint={}", graph.graph_fingerprint().unwrap());
            let resolved = graph.resolve_graph().ok().map(|g| g.resolved_params);
            for n in &graph.nodes {
                let fp = resolved
                    .as_ref()
                    .and_then(|m| m.get(&n.id))
                    .and_then(|p| graph.node_fingerprint_with_params(n, p).ok())
                    .unwrap_or_else(|| graph.node_fingerprint(n).unwrap());
                println!("node_fingerprint {}={}", n.id, fp);
            }
        }
    }
    if !cli.quiet {
        println!("status: {}", if fail { "invalid" } else { "ok" });
    }
    if fail {
        return Err(ExitCode::from(2));
    }
    Ok(ExitCode::SUCCESS)
}

#[cfg(test)]
mod tests {
    use super::handle_validate_command;
    use crate::commands::DagCli;
    use clap::Parser;
    use std::path::PathBuf;

    #[test]
    fn validate_route_rejects_missing_file_without_panic() {
        let cli = DagCli::parse_from(["bijux-dag", "validate", "/missing.json"]);
        let code =
            handle_validate_command(&cli, &[PathBuf::from("/missing.json")], false, false, false);
        assert!(code.is_err());
    }

    #[test]
    fn validate_route_accepts_composed_graph_fragments() {
        let dir = tempfile::tempdir().expect("tmp");
        let foundation = dir.path().join("foundation.json");
        let publication = dir.path().join("publication.json");
        std::fs::write(
            &foundation,
            r#"{
              "spec":"bijux-dag/v0.1",
              "nodes":[{"id":"extract","kind":"const","outputs":[{"name":"report","path":"extract/report.json"}],"params":{"value":"seed"}}],
              "edges":[]
            }"#,
        )
        .expect("write foundation");
        std::fs::write(
            &publication,
            r#"{
              "spec":"bijux-dag/v0.1",
              "nodes":[{"id":"publish","kind":"const","inputs":["report"],"outputs":[{"name":"out","path":"publish/out.json"}],"params":{"seed":{"node_output":{"node_id":"extract","output_name":"report"}}}}],
              "edges":[{"from":{"node_id":"extract","port":"report"},"to":{"node_id":"publish","port":"report"}}]
            }"#,
        )
        .expect("write publication");

        let cli = DagCli::parse_from([
            "bijux-dag",
            "--json",
            "validate",
            foundation.to_string_lossy().as_ref(),
            publication.to_string_lossy().as_ref(),
        ]);
        let code = handle_validate_command(&cli, &[foundation, publication], false, false, false)
            .expect("validate");
        assert_eq!(code, std::process::ExitCode::SUCCESS);
    }
}