scud/commands/attractor/
export.rs1use 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
12pub 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 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 source
30 }
31 }
32 "scg" => {
33 if is_scg {
34 source
36 } else {
37 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
62fn pipeline_to_dot(pipeline: &PipelineGraph) -> String {
64 let mut out = String::new();
65 out.push_str(&format!("digraph {} {{\n", pipeline.name));
66
67 if let Some(ref goal) = pipeline.graph_attrs.goal {
69 out.push_str(&format!(" graph [goal={:?}]\n", goal));
70 }
71
72 out.push_str(" node [style=filled, fillcolor=white]\n\n");
74
75 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(""); 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 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}