Skip to main content

scud/commands/attractor/
validate.rs

1//! `scud attractor validate` — Validate an Attractor pipeline without executing it.
2
3use 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
14/// Validate a pipeline file (.scg or .dot).
15pub 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    // Print nodes
51    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}