Skip to main content

scud/commands/attractor/
export.rs

1//! `scud attractor export` — Export pipeline to DOT or SCG format.
2
3use anyhow::{Context, Result};
4use petgraph::visit::EdgeRef;
5use std::path::Path;
6
7use crate::attractor::dot_parser::parse_dot;
8use crate::attractor::graph::PipelineGraph;
9use crate::attractor::scg_bridge;
10use crate::formats::{parse_scg_result, serialize_scg_pipeline};
11
12/// Export a pipeline file to another format.
13pub fn run(file: &Path, format: &str, output: Option<&Path>) -> Result<()> {
14    let source =
15        std::fs::read_to_string(file).context(format!("Failed to read: {}", file.display()))?;
16
17    let is_scg = file.extension().and_then(|e| e.to_str()) == Some("scg");
18
19    let result = match format {
20        "dot" => {
21            if is_scg {
22                // SCG -> PipelineGraph -> DOT
23                let scg = parse_scg_result(&source).context("Failed to parse SCG file")?;
24                let pipeline = scg_bridge::pipeline_from_scg(&scg)
25                    .context("Failed to build pipeline graph from SCG")?;
26                pipeline_to_dot(&pipeline)
27            } else {
28                // Already DOT, just pass through
29                source
30            }
31        }
32        "scg" => {
33            if is_scg {
34                // Already SCG, pass through
35                source
36            } else {
37                // DOT -> PipelineGraph -> SCG
38                let dot = parse_dot(&source).context("Failed to parse DOT file")?;
39                let pipeline =
40                    PipelineGraph::from_dot(&dot).context("Failed to build pipeline graph")?;
41                let scg_result = scg_bridge::scg_from_pipeline(&pipeline);
42                serialize_scg_pipeline(&scg_result)
43            }
44        }
45        _ => anyhow::bail!(
46            "Unsupported export format '{}'. Use 'dot' or 'scg'.",
47            format
48        ),
49    };
50
51    if let Some(out_path) = output {
52        std::fs::write(out_path, &result)
53            .context(format!("Failed to write: {}", out_path.display()))?;
54        eprintln!("Wrote {} to {}", format, out_path.display());
55    } else {
56        print!("{}", result);
57    }
58
59    Ok(())
60}
61
62/// Serialize a PipelineGraph to DOT format.
63fn pipeline_to_dot(pipeline: &PipelineGraph) -> String {
64    let mut out = String::new();
65    out.push_str(&format!("digraph {} {{\n", pipeline.name));
66
67    // Graph attrs
68    if let Some(ref goal) = pipeline.graph_attrs.goal {
69        out.push_str(&format!("    graph [goal={:?}]\n", goal));
70    }
71
72    // Node defaults
73    out.push_str("    node [style=filled, fillcolor=white]\n\n");
74
75    // Nodes
76    for idx in pipeline.graph.node_indices() {
77        let node = &pipeline.graph[idx];
78        let mut attrs = vec![
79            format!("shape={}", node.shape),
80            format!("label={:?}", node.label),
81        ];
82
83        if !node.prompt.is_empty() {
84            attrs.push(format!("prompt={:?}", node.prompt));
85        }
86        if node.max_retries > 0 {
87            attrs.push(format!("max_retries={}", node.max_retries));
88        }
89        if node.goal_gate {
90            attrs.push("goal_gate=true".into());
91        }
92        if let Some(ref rt) = node.retry_target {
93            attrs.push(format!("retry_target={:?}", rt));
94        }
95        if let Some(ref timeout) = node.timeout {
96            out.push_str(""); // satisfy the compiler
97            attrs.push(format!("timeout=\"{}s\"", timeout.as_secs()));
98        }
99
100        out.push_str(&format!("    {} [{}]\n", node.id, attrs.join(", ")));
101    }
102
103    out.push('\n');
104
105    // Edges
106    for edge_ref in pipeline.graph.edge_references() {
107        let from = &pipeline.graph[edge_ref.source()];
108        let to = &pipeline.graph[edge_ref.target()];
109        let edge = edge_ref.weight();
110
111        let mut attrs = Vec::new();
112        if !edge.label.is_empty() {
113            attrs.push(format!("label={:?}", edge.label));
114        }
115        if !edge.condition.is_empty() {
116            attrs.push(format!("condition={:?}", edge.condition));
117        }
118        if edge.weight != 0 {
119            attrs.push(format!("weight={}", edge.weight));
120        }
121
122        if attrs.is_empty() {
123            out.push_str(&format!("    {} -> {}\n", from.id, to.id));
124        } else {
125            out.push_str(&format!(
126                "    {} -> {} [{}]\n",
127                from.id,
128                to.id,
129                attrs.join(", ")
130            ));
131        }
132    }
133
134    out.push_str("}\n");
135    out
136}