scud/commands/attractor/
validate.rs1use anyhow::{Context, Result};
4use colored::Colorize;
5use std::path::Path;
6
7use crate::attractor::dot_parser::parse_dot;
8use crate::attractor::graph::PipelineGraph;
9use crate::attractor::scg_bridge;
10use crate::attractor::transforms::apply_transforms;
11use crate::attractor::validator::{self, Severity};
12use crate::formats::parse_scg_result;
13
14pub fn run(file: &Path) -> Result<()> {
16 let source =
17 std::fs::read_to_string(file).context(format!("Failed to read: {}", file.display()))?;
18
19 let is_scg = file.extension().and_then(|e| e.to_str()) == Some("scg");
20 let mut pipeline = if is_scg {
21 let result = parse_scg_result(&source).context("Failed to parse SCG file")?;
22 scg_bridge::pipeline_from_scg(&result).context("Failed to build pipeline graph from SCG")?
23 } else {
24 let dot_graph = parse_dot(&source).context("Failed to parse DOT file")?;
25 PipelineGraph::from_dot(&dot_graph).context("Failed to build pipeline graph")?
26 };
27
28 apply_transforms(&mut pipeline);
29
30 let issues = validator::validate(&pipeline);
31
32 let errors: Vec<_> = issues
33 .iter()
34 .filter(|i| i.severity == Severity::Error)
35 .collect();
36 let warnings: Vec<_> = issues
37 .iter()
38 .filter(|i| i.severity == Severity::Warning)
39 .collect();
40
41 println!(
42 "{}: {} ({} nodes, {} edges)",
43 "Pipeline".bold(),
44 pipeline.name.cyan(),
45 pipeline.graph.node_count(),
46 pipeline.graph.edge_count()
47 );
48 println!();
49
50 println!("{}", "Nodes:".bold());
52 for idx in pipeline.graph.node_indices() {
53 let node = &pipeline.graph[idx];
54 println!(
55 " {} ({}) {}",
56 node.id,
57 node.handler_type.dimmed(),
58 if node.prompt.is_empty() {
59 String::new()
60 } else {
61 format!("- {}", truncate(&node.prompt, 60))
62 }
63 );
64 }
65 println!();
66
67 if !warnings.is_empty() {
68 println!("{} ({}):", "Warnings".yellow().bold(), warnings.len());
69 for issue in &warnings {
70 println!(" {} [{}] {}", "WARN".yellow(), issue.rule, issue.message);
71 }
72 println!();
73 }
74
75 if !errors.is_empty() {
76 println!("{} ({}):", "Errors".red().bold(), errors.len());
77 for issue in &errors {
78 println!(" {} [{}] {}", "ERROR".red(), issue.rule, issue.message);
79 }
80 println!();
81 anyhow::bail!("Validation failed with {} error(s)", errors.len());
82 }
83
84 println!("{}", "✓ Pipeline is valid".green().bold());
85 Ok(())
86}
87
88fn truncate(s: &str, max: usize) -> String {
89 if s.len() > max {
90 format!("{}...", &s[..max - 3])
91 } else {
92 s.to_string()
93 }
94}