Skip to main content

aprender_contracts_cli/commands/
verify_structure.rs

1//! `pv verify-structure` — verify model architecture matches contracts.
2//!
3//! Two modes:
4//! 1. Contract-only (no model file): enumerate expected tensors from
5//!    apr-architecture-schema-v1 equations for a given config.
6//! 2. With model file (future): compare expected vs actual tensors.
7//!
8//! Spec: docs/specifications/sub/model-layout-provability.md (§36, P0-4)
9
10use std::path::Path;
11
12use super::certify::analyze_config;
13
14/// Run the verify-structure command.
15pub fn run(
16    contract_dir: &Path,
17    config_json: Option<&Path>,
18    model_file: Option<&Path>,
19) -> Result<(), Box<dyn std::error::Error>> {
20    println!("pv verify-structure — Architecture Structure Verification");
21    println!("=========================================================");
22    println!();
23
24    // Load architecture schema contract
25    let arch_path = find_contract(contract_dir, "apr-architecture-schema-v1");
26    let config_path = find_contract(contract_dir, "model-config-algebra-v1");
27    let shape_path = find_contract(contract_dir, "tensor-shape-flow-v1");
28
29    let mut found = 0;
30    let mut missing = Vec::new();
31
32    for (name, path) in [
33        ("apr-architecture-schema-v1", &arch_path),
34        ("model-config-algebra-v1", &config_path),
35        ("tensor-shape-flow-v1", &shape_path),
36    ] {
37        if let Some(p) = path {
38            let contract = provable_contracts::schema::parse_contract(p)?;
39            let eq_count = contract.equations.len();
40            let has_assumes = contract
41                .equations
42                .values()
43                .filter(|e| e.assumes.is_some())
44                .count();
45            let has_guarantees = contract
46                .equations
47                .values()
48                .filter(|e| e.guarantees.is_some())
49                .count();
50            println!(
51                "  ✓ {name}: {eq_count} equations, {has_assumes} assumes, {has_guarantees} guarantees"
52            );
53            found += 1;
54        } else {
55            println!("  ✗ {name}: not found");
56            missing.push(name);
57        }
58    }
59    println!();
60
61    // Config analysis
62    if let Some(cfg) = config_json {
63        if cfg.exists() {
64            analyze_config(cfg)?;
65        } else {
66            println!("Config file not found: {}", cfg.display());
67        }
68    } else {
69        println!("No --config provided. Use --config path/to/config.json for structural analysis.");
70    }
71
72    // Model file analysis (future)
73    if let Some(mf) = model_file {
74        println!("Model file: {}", mf.display());
75        println!("  ⚠ Model file parsing not yet implemented (P0-4 phase 2)");
76        println!("  Planned: enumerate actual tensors, compare shapes to arch-schema");
77    }
78
79    println!();
80    if missing.is_empty() && found == 3 {
81        println!("Result: PASS — all 3 architecture contracts found with composition data");
82    } else {
83        println!(
84            "Result: PARTIAL — {found}/3 contracts found, {} missing",
85            missing.len()
86        );
87    }
88
89    Ok(())
90}
91
92fn find_contract(dir: &Path, stem: &str) -> Option<std::path::PathBuf> {
93    let direct = dir.join(format!("{stem}.yaml"));
94    if direct.exists() {
95        return Some(direct);
96    }
97    // Recursively search subdirectories so contracts nested deeper than one
98    // level (e.g. contracts/aprender/foo/bar-v1.yaml) are still located.
99    let entries = std::fs::read_dir(dir).ok()?;
100    for entry in entries.flatten() {
101        let path = entry.path();
102        if path.is_dir() {
103            if let Some(found) = find_contract(&path, stem) {
104                return Some(found);
105            }
106        }
107    }
108    None
109}