Skip to main content

provable_contracts/
obligation_matrix.rs

1//! Per-obligation verification matrix.
2//!
3//! Shows L2/L3/L4 status for each proof obligation across all contracts.
4
5use serde::{Deserialize, Serialize};
6
7use crate::proof_status::ProofLevel;
8use crate::schema::Contract;
9
10/// Verification status for a single obligation across all proof levels.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ObligationStatus {
13    /// Human-readable obligation property name
14    pub property: String,
15    /// Obligation type (invariant, bound, equivalence, etc.)
16    pub obligation_type: String,
17    /// Whether at least one falsification test covers this obligation
18    pub l2_tested: bool,
19    /// Whether at least one Kani harness covers this obligation
20    pub l3_kani: bool,
21    /// Whether the obligation has a Lean proof with status "proved"
22    pub l4_lean: bool,
23    /// Highest achieved level for this specific obligation
24    pub max_level: ProofLevel,
25}
26
27/// Per-contract obligation verification matrix.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ContractObligationMatrix {
30    /// Contract file stem (e.g. "softmax-kernel-v1")
31    pub stem: String,
32    /// Per-obligation verification status entries
33    pub obligations: Vec<ObligationStatus>,
34}
35
36/// Build per-obligation verification matrices for a list of contracts.
37///
38/// For each contract, determines per-obligation coverage:
39/// - **L2**: A falsification test covers this obligation (index-based or rule match)
40/// - **L3**: A Kani harness references this obligation (property match)
41/// - **L4**: The obligation has a `lean` field with `status: proved`
42pub fn obligation_matrix(contracts: &[(String, &Contract)]) -> Vec<ContractObligationMatrix> {
43    contracts
44        .iter()
45        .map(|(stem, contract)| {
46            let obligations = contract
47                .proof_obligations
48                .iter()
49                .enumerate()
50                .map(|(idx, ob)| {
51                    let prop_lower = ob.property.to_lowercase();
52
53                    // L2: check if any falsification test covers this obligation.
54                    // Strategy: index-based (obligation i covered if i < test count),
55                    // plus rule-text matching against the property name.
56                    let l2_tested = idx < contract.falsification_tests.len()
57                        || contract.falsification_tests.iter().any(|ft| {
58                            let rule_lower = ft.rule.to_lowercase();
59                            prop_lower.contains(&rule_lower) || rule_lower.contains(&prop_lower)
60                        });
61
62                    // L3: check if any Kani harness covers this obligation.
63                    // Match by property text overlap or by harness property containing
64                    // key words from the obligation property.
65                    let l3_kani = contract.kani_harnesses.iter().any(|kh| {
66                        if let Some(ref kh_prop) = kh.property {
67                            let kh_lower = kh_prop.to_lowercase();
68                            property_words_match(&prop_lower, &kh_lower)
69                        } else {
70                            false
71                        }
72                    });
73
74                    // L4: obligation has lean proof with status proved
75                    let l4_lean = ob
76                        .lean
77                        .as_ref()
78                        .is_some_and(|lp| lp.status == crate::schema::LeanStatus::Proved);
79
80                    let max_level = if l4_lean {
81                        ProofLevel::L4
82                    } else if l3_kani {
83                        ProofLevel::L3
84                    } else if l2_tested {
85                        ProofLevel::L2
86                    } else {
87                        ProofLevel::L1
88                    };
89
90                    ObligationStatus {
91                        property: ob.property.clone(),
92                        obligation_type: ob.obligation_type.to_string(),
93                        l2_tested,
94                        l3_kani,
95                        l4_lean,
96                        max_level,
97                    }
98                })
99                .collect();
100
101            ContractObligationMatrix {
102                stem: stem.clone(),
103                obligations,
104            }
105        })
106        .collect()
107}
108
109/// Format per-obligation matrices as a human-readable table.
110pub fn format_obligation_table(matrices: &[ContractObligationMatrix]) -> String {
111    let mut out = String::new();
112
113    out.push_str("\nObligation Status Matrix\n");
114    out.push_str("========================\n");
115
116    for matrix in matrices {
117        if matrix.obligations.is_empty() {
118            continue;
119        }
120
121        // Compute max property width (capped at 40 for readability)
122        let max_prop_width = matrix
123            .obligations
124            .iter()
125            .map(|o| o.property.len())
126            .max()
127            .unwrap_or(20)
128            .min(40);
129
130        out.push_str(&format!("Contract: {}\n", matrix.stem));
131        out.push_str(&format!(
132            "  {:<width$} | L2 Test | L3 Kani | L4 Lean | Status\n",
133            "Obligation",
134            width = max_prop_width
135        ));
136        out.push_str(&format!("  {}\n", "-".repeat(max_prop_width + 39)));
137
138        for ob in &matrix.obligations {
139            let check = "\u{2713}";
140            let cross = "\u{2717}";
141            let l2 = if ob.l2_tested { check } else { cross };
142            let l3 = if ob.l3_kani { check } else { cross };
143            let l4 = if ob.l4_lean { check } else { cross };
144            let prop_display = truncate(&ob.property, max_prop_width);
145            out.push_str(&format!(
146                "  {:<width$} |    {}    |    {}    |    {}    | {}\n",
147                prop_display,
148                l2,
149                l3,
150                l4,
151                ob.max_level,
152                width = max_prop_width
153            ));
154        }
155
156        out.push('\n');
157    }
158
159    out
160}
161
162/// Check whether two property descriptions share significant words.
163///
164/// Splits both strings into words (>= 3 chars, excluding stop words) and
165pub fn truncate(s: &str, max: usize) -> &str {
166    if s.len() <= max {
167        s
168    } else {
169        &s[..max]
170    }
171}
172
173/// returns true if at least one non-trivial word overlaps.
174pub fn property_words_match(a: &str, b: &str) -> bool {
175    let stop_words: &[&str] = &[
176        "the", "for", "and", "all", "are", "with", "from", "that", "this", "has", "not", "any",
177        "each", "into",
178    ];
179
180    let words_a: Vec<&str> = a
181        .split(|c: char| !c.is_alphanumeric())
182        .filter(|w| w.len() >= 3 && !stop_words.contains(w))
183        .collect();
184
185    words_a.iter().any(|wa| {
186        b.split(|c: char| !c.is_alphanumeric())
187            .filter(|w| w.len() >= 3 && !stop_words.contains(w))
188            .any(|wb| wa == &wb)
189    })
190}
191
192// Tests for obligation_matrix are in proof_status_tests.rs
193// (included via proof_status.rs #[path] directive)