Skip to main content

aprender_contracts_cli/commands/
pipeline.rs

1use std::path::Path;
2
3use provable_contracts::pipeline::{self, IssueSeverity};
4use serde_json::Value;
5
6use crate::json_obj::obj;
7
8/// Run the `pv pipeline` command: validate a pipeline contract.
9pub fn run(path: &Path, format: &str) -> Result<(), Box<dyn std::error::Error>> {
10    let pipeline = pipeline::parse_pipeline(path)?;
11    let issues = pipeline::validate_pipeline(&pipeline);
12
13    let errors: Vec<_> = issues
14        .iter()
15        .filter(|i| i.severity == IssueSeverity::Error)
16        .collect();
17    let warnings: Vec<_> = issues
18        .iter()
19        .filter(|i| i.severity == IssueSeverity::Warning)
20        .collect();
21
22    match format {
23        "json" => print_json(&pipeline, &issues),
24        _ => print_text(&pipeline, &errors, &warnings),
25    }
26
27    if !errors.is_empty() {
28        return Err(format!("{} error(s) in pipeline", errors.len()).into());
29    }
30
31    Ok(())
32}
33
34fn print_text(
35    pipeline: &pipeline::PipelineContract,
36    errors: &[&pipeline::PipelineIssue],
37    warnings: &[&pipeline::PipelineIssue],
38) {
39    println!(
40        "Pipeline: {} (v{})",
41        pipeline.metadata.description, pipeline.metadata.version
42    );
43    println!();
44
45    // Stages
46    let stage_names = collect_stage_display(&pipeline.stages, 0);
47    println!("Stages ({}):", stage_names.len());
48    for (indent, name, repo, contract) in &stage_names {
49        let prefix = "  ".repeat(*indent + 1);
50        let repo_str = repo.as_deref().unwrap_or("—");
51        let contract_str = contract.as_deref().unwrap_or("—");
52        println!("{prefix}{name} [{repo_str}] → {contract_str}");
53    }
54    println!();
55
56    // Cross-boundary obligations
57    println!(
58        "Cross-boundary obligations ({}):",
59        pipeline.cross_boundary_obligations.len()
60    );
61    for ob in &pipeline.cross_boundary_obligations {
62        println!(
63            "  {} {} → {}: {}",
64            ob.id, ob.from_stage, ob.to_stage, ob.property
65        );
66    }
67    println!();
68
69    // Performance contract
70    if let Some(ref perf) = pipeline.performance_contract {
71        println!("Performance:");
72        if let Some(ref r) = perf.roofline {
73            println!("  Roofline: {r}");
74        }
75        if let Some(ref p) = perf.prefill_bound {
76            println!("  Prefill: {p}");
77        }
78        if let Some(ref d) = perf.decode_bound {
79            println!("  Decode: {d}");
80        }
81        println!();
82    }
83
84    // Issues
85    if errors.is_empty() && warnings.is_empty() {
86        println!("Validation: PASS (0 errors, 0 warnings)");
87    } else {
88        for e in errors {
89            println!("  [ERROR] {}", e.message);
90        }
91        for w in warnings {
92            println!("  [WARN]  {}", w.message);
93        }
94        println!(
95            "\nValidation: {} ({} error(s), {} warning(s))",
96            if errors.is_empty() { "PASS" } else { "FAIL" },
97            errors.len(),
98            warnings.len()
99        );
100    }
101}
102
103fn print_json(pipeline: &pipeline::PipelineContract, issues: &[pipeline::PipelineIssue]) {
104    let error_count = issues
105        .iter()
106        .filter(|i| i.severity == IssueSeverity::Error)
107        .count();
108    let warning_count = issues
109        .iter()
110        .filter(|i| i.severity == IssueSeverity::Warning)
111        .count();
112    let issue_arr: Vec<Value> = issues
113        .iter()
114        .map(|i| {
115            obj([
116                (
117                    "severity",
118                    Value::from(match i.severity {
119                        IssueSeverity::Error => "error",
120                        IssueSeverity::Warning => "warning",
121                    }),
122                ),
123                ("message", Value::from(i.message.clone())),
124            ])
125        })
126        .collect();
127
128    let output = obj([
129        (
130            "description",
131            Value::from(pipeline.metadata.description.clone()),
132        ),
133        ("version", Value::from(pipeline.metadata.version.clone())),
134        ("stage_count", Value::from(pipeline.stages.len())),
135        (
136            "obligation_count",
137            Value::from(pipeline.cross_boundary_obligations.len()),
138        ),
139        ("error_count", Value::from(error_count)),
140        ("warning_count", Value::from(warning_count)),
141        ("issues", Value::Array(issue_arr)),
142    ]);
143
144    println!(
145        "{}",
146        serde_json::to_string_pretty(&output).unwrap_or_default()
147    );
148}
149
150fn collect_stage_display(
151    stages: &[pipeline::PipelineStage],
152    depth: usize,
153) -> Vec<(usize, String, Option<String>, Option<String>)> {
154    let mut result = Vec::new();
155    for s in stages {
156        result.push((depth, s.name.clone(), s.repo.clone(), s.contract.clone()));
157        if !s.substages.is_empty() {
158            result.extend(collect_stage_display(&s.substages, depth + 1));
159        }
160    }
161    result
162}