aprender_contracts_cli/commands/
verify_pipeline.rs1use std::collections::BTreeMap;
10use std::path::Path;
11
12use provable_contracts::graph::dependency_graph;
13use provable_contracts::schema::Contract;
14use serde_json::Value;
15
16use crate::contract_walk::collect_contracts;
17use crate::json_obj::obj;
18
19pub fn run(contract_dir: &Path, format: &str) {
21 let mut contracts = Vec::new();
23 collect_contracts(contract_dir, &mut contracts);
24 contracts.sort_by(|a, b| a.0.cmp(&b.0));
25
26 if contracts.is_empty() {
27 eprintln!("No contracts found in {}", contract_dir.display());
28 return;
29 }
30
31 let refs: Vec<(String, &Contract)> = contracts.iter().map(|(s, c)| (s.clone(), c)).collect();
33 let graph = dependency_graph(&refs);
34
35 if !graph.cycles.is_empty() {
36 eprintln!(
37 "ERROR: {} cycle(s) detected in dependency graph — cannot verify pipeline",
38 graph.cycles.len()
39 );
40 for cycle in &graph.cycles {
41 eprintln!(" cycle: {}", cycle.join(" → "));
42 }
43 std::process::exit(1);
44 }
45
46 let index: BTreeMap<&str, &Contract> = contracts.iter().map(|(s, c)| (s.as_str(), c)).collect();
48 let (chains, edges_total, edges_satisfied, edges_broken) =
49 walk_composition_edges(&graph.topo_order, &index);
50
51 if format == "json" {
53 print_json(&chains, edges_total, edges_satisfied, &edges_broken, &graph);
54 } else {
55 print_text(&chains, edges_total, edges_satisfied, &edges_broken, &graph);
56 }
57
58 if !edges_broken.is_empty() {
59 std::process::exit(1);
60 }
61}
62
63fn walk_composition_edges(
64 topo_order: &[String],
65 index: &BTreeMap<&str, &Contract>,
66) -> (Vec<CompositionEdge>, usize, usize, Vec<CompositionEdge>) {
67 let mut edges_total = 0usize;
68 let mut edges_satisfied = 0usize;
69 let mut edges_broken = Vec::new();
70 let mut chains: Vec<CompositionEdge> = Vec::new();
71
72 for stem in topo_order {
73 let Some(contract) = index.get(stem.as_str()) else {
74 continue;
75 };
76 for (eq_name, equation) in &contract.equations {
77 let Some(assumes) = &equation.assumes else {
78 continue;
79 };
80 let Some(from_contract) = &assumes.from_contract else {
81 continue;
82 };
83
84 edges_total += 1;
85 let from_eq = assumes.from_equation.as_deref();
86
87 let edge = CompositionEdge {
88 downstream: format!("{stem}.{eq_name}"),
89 upstream: format!(
90 "{from_contract}{}",
91 from_eq.map_or(String::new(), |e| format!(".{e}"))
92 ),
93 assumed_shapes: assumes.shapes.keys().cloned().collect(),
94 status: EdgeStatus::Unknown,
95 };
96
97 let Some(upstream_contract) = index.get(from_contract.as_str()) else {
99 let mut e = edge;
100 e.status = EdgeStatus::Broken("upstream contract not found".into());
101 edges_broken.push(e.clone());
102 chains.push(e);
103 continue;
104 };
105
106 if let Some(upstream_eq_name) = from_eq {
107 let Some(upstream_eq) = upstream_contract.equations.get(upstream_eq_name) else {
108 let mut e = edge;
109 e.status = EdgeStatus::Broken("upstream equation not found".into());
110 edges_broken.push(e.clone());
111 chains.push(e);
112 continue;
113 };
114
115 let Some(guarantees) = upstream_eq.guarantees.as_ref() else {
119 let mut e = edge;
120 e.status = EdgeStatus::Broken("upstream has no guarantees".into());
121 edges_broken.push(e.clone());
122 chains.push(e);
123 continue;
124 };
125
126 let mut e = edge;
128 let guaranteed_shapes: Vec<String> = guarantees.shapes.keys().cloned().collect();
129 e.status = EdgeStatus::Satisfied(guaranteed_shapes);
130 edges_satisfied += 1;
131 chains.push(e);
132 } else {
133 let has_guarantees = upstream_contract
135 .equations
136 .values()
137 .any(|eq| eq.guarantees.is_some());
138 let mut e = edge;
139 if has_guarantees {
140 e.status = EdgeStatus::Satisfied(vec![]);
141 edges_satisfied += 1;
142 } else {
143 e.status = EdgeStatus::Broken("no equations with guarantees".into());
144 edges_broken.push(e.clone());
145 }
146 chains.push(e);
147 }
148 }
149 }
150
151 (chains, edges_total, edges_satisfied, edges_broken)
152}
153
154#[derive(Debug, Clone)]
155struct CompositionEdge {
156 downstream: String,
157 upstream: String,
158 assumed_shapes: Vec<String>,
159 status: EdgeStatus,
160}
161
162#[derive(Debug, Clone)]
163enum EdgeStatus {
164 Unknown,
165 Satisfied(Vec<String>),
166 Broken(String),
167}
168
169fn print_text(
170 chains: &[CompositionEdge],
171 total: usize,
172 satisfied: usize,
173 broken: &[CompositionEdge],
174 graph: &provable_contracts::graph::DependencyGraph,
175) {
176 println!("pv verify-pipeline — Compositional Shape Verification");
177 println!("=====================================================");
178 println!();
179 println!(
180 "Contracts: {} | Topo depth: {}",
181 graph.nodes.len(),
182 graph.topo_order.len()
183 );
184 println!(
185 "Edges: {} | Satisfied: {} | Broken: {}",
186 total,
187 satisfied,
188 broken.len()
189 );
190 println!();
191
192 if !chains.is_empty() {
193 println!("Composition edges:");
194 for edge in chains {
195 let icon = match &edge.status {
196 EdgeStatus::Satisfied(_) => "✓",
197 EdgeStatus::Broken(_) => "✗",
198 EdgeStatus::Unknown => "?",
199 };
200 let detail = match &edge.status {
201 EdgeStatus::Satisfied(shapes) if !shapes.is_empty() => {
202 format!(" (guarantees: {})", shapes.join(", "))
203 }
204 EdgeStatus::Broken(reason) => format!(" — {reason}"),
205 _ => String::new(),
206 };
207 println!(" {icon} {} ← {}{detail}", edge.downstream, edge.upstream);
208 }
209 println!();
210 }
211
212 if broken.is_empty() {
213 println!("Result: PASS — all composition edges satisfied");
214 } else {
215 println!("Result: FAIL — {} broken edge(s)", broken.len());
216 for edge in broken {
217 if let EdgeStatus::Broken(reason) = &edge.status {
218 println!(" ✗ {} ← {} — {reason}", edge.downstream, edge.upstream);
219 }
220 }
221 }
222}
223
224fn print_json(
225 chains: &[CompositionEdge],
226 total: usize,
227 satisfied: usize,
228 broken: &[CompositionEdge],
229 graph: &provable_contracts::graph::DependencyGraph,
230) {
231 let edges_json: Vec<Value> = chains
232 .iter()
233 .map(|e| {
234 let (status, detail) = match &e.status {
235 EdgeStatus::Satisfied(shapes) => (
236 "satisfied",
237 obj([("guaranteed_shapes", Value::from(shapes.clone()))]),
238 ),
239 EdgeStatus::Broken(reason) => {
240 ("broken", obj([("reason", Value::from(reason.clone()))]))
241 }
242 EdgeStatus::Unknown => ("unknown", obj([])),
243 };
244 obj([
245 ("downstream", Value::from(e.downstream.clone())),
246 ("upstream", Value::from(e.upstream.clone())),
247 ("assumed_shapes", Value::from(e.assumed_shapes.clone())),
248 ("status", Value::from(status)),
249 ("detail", detail),
250 ])
251 })
252 .collect();
253
254 let report = obj([
255 ("contracts", Value::from(graph.nodes.len())),
256 ("topo_depth", Value::from(graph.topo_order.len())),
257 ("edges_total", Value::from(total)),
258 ("edges_satisfied", Value::from(satisfied)),
259 ("edges_broken", Value::from(broken.len())),
260 ("passed", Value::from(broken.is_empty())),
261 ("edges", Value::Array(edges_json)),
262 ]);
263
264 println!(
265 "{}",
266 serde_json::to_string_pretty(&report)
267 .expect("a serde_json::Value of objects/arrays/strings/numbers always serializes")
268 );
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn verify_pipeline_on_real_contracts() {
277 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts");
278 if !dir.exists() {
279 return; }
281 run(&dir, "text");
283 }
284
285 #[test]
286 fn verify_pipeline_json_on_real_contracts() {
287 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts");
288 if !dir.exists() {
289 return;
290 }
291 run(&dir, "json");
292 }
293
294 #[test]
295 fn verify_pipeline_empty_dir() {
296 let tmp = tempfile::tempdir().unwrap();
297 run(tmp.path(), "text");
298 }
299}