use std::path::Path;
use provable_contracts::pipeline::{self, IssueSeverity};
pub fn run(path: &Path, format: &str) -> Result<(), Box<dyn std::error::Error>> {
let pipeline = pipeline::parse_pipeline(path)?;
let issues = pipeline::validate_pipeline(&pipeline);
let errors: Vec<_> = issues
.iter()
.filter(|i| i.severity == IssueSeverity::Error)
.collect();
let warnings: Vec<_> = issues
.iter()
.filter(|i| i.severity == IssueSeverity::Warning)
.collect();
match format {
"json" => print_json(&pipeline, &issues),
_ => print_text(&pipeline, &errors, &warnings),
}
if !errors.is_empty() {
return Err(format!("{} error(s) in pipeline", errors.len()).into());
}
Ok(())
}
fn print_text(
pipeline: &pipeline::PipelineContract,
errors: &[&pipeline::PipelineIssue],
warnings: &[&pipeline::PipelineIssue],
) {
println!(
"Pipeline: {} (v{})",
pipeline.metadata.description, pipeline.metadata.version
);
println!();
let stage_names = collect_stage_display(&pipeline.stages, 0);
println!("Stages ({}):", stage_names.len());
for (indent, name, repo, contract) in &stage_names {
let prefix = " ".repeat(*indent + 1);
let repo_str = repo.as_deref().unwrap_or("—");
let contract_str = contract.as_deref().unwrap_or("—");
println!("{prefix}{name} [{repo_str}] → {contract_str}");
}
println!();
println!(
"Cross-boundary obligations ({}):",
pipeline.cross_boundary_obligations.len()
);
for ob in &pipeline.cross_boundary_obligations {
println!(
" {} {} → {}: {}",
ob.id, ob.from_stage, ob.to_stage, ob.property
);
}
println!();
if let Some(ref perf) = pipeline.performance_contract {
println!("Performance:");
if let Some(ref r) = perf.roofline {
println!(" Roofline: {r}");
}
if let Some(ref p) = perf.prefill_bound {
println!(" Prefill: {p}");
}
if let Some(ref d) = perf.decode_bound {
println!(" Decode: {d}");
}
println!();
}
if errors.is_empty() && warnings.is_empty() {
println!("Validation: PASS (0 errors, 0 warnings)");
} else {
for e in errors {
println!(" [ERROR] {}", e.message);
}
for w in warnings {
println!(" [WARN] {}", w.message);
}
println!(
"\nValidation: {} ({} error(s), {} warning(s))",
if errors.is_empty() { "PASS" } else { "FAIL" },
errors.len(),
warnings.len()
);
}
}
fn print_json(pipeline: &pipeline::PipelineContract, issues: &[pipeline::PipelineIssue]) {
let error_count = issues
.iter()
.filter(|i| i.severity == IssueSeverity::Error)
.count();
let warning_count = issues
.iter()
.filter(|i| i.severity == IssueSeverity::Warning)
.count();
let issue_arr: Vec<serde_json::Value> = issues
.iter()
.map(|i| {
serde_json::json!({
"severity": match i.severity {
IssueSeverity::Error => "error",
IssueSeverity::Warning => "warning",
},
"message": i.message,
})
})
.collect();
let output = serde_json::json!({
"description": pipeline.metadata.description,
"version": pipeline.metadata.version,
"stage_count": pipeline.stages.len(),
"obligation_count": pipeline.cross_boundary_obligations.len(),
"error_count": error_count,
"warning_count": warning_count,
"issues": issue_arr,
});
println!(
"{}",
serde_json::to_string_pretty(&output).unwrap_or_default()
);
}
fn collect_stage_display(
stages: &[pipeline::PipelineStage],
depth: usize,
) -> Vec<(usize, String, Option<String>, Option<String>)> {
let mut result = Vec::new();
for s in stages {
result.push((depth, s.name.clone(), s.repo.clone(), s.contract.clone()));
if !s.substages.is_empty() {
result.extend(collect_stage_display(&s.substages, depth + 1));
}
}
result
}