Skip to main content

provable_contracts/audit/
mod.rs

1//! Audit trail generator — traceability chain.
2//!
3//! Traces the full chain from paper reference → equation →
4//! proof obligation → falsification test → Kani harness.
5//! Detects orphaned obligations and untested equations.
6
7use crate::binding::{BindingRegistry, ImplStatus};
8use crate::error::{Severity, Violation};
9use crate::schema::Contract;
10
11/// Audit result summarizing traceability coverage.
12#[derive(Debug, Clone)]
13pub struct AuditReport {
14    /// Total equations in the contract.
15    pub equations: usize,
16    /// Total proof obligations.
17    pub obligations: usize,
18    /// Total falsification tests.
19    pub falsification_tests: usize,
20    /// Total Kani harnesses.
21    pub kani_harnesses: usize,
22    /// Violations found during audit.
23    pub violations: Vec<Violation>,
24}
25
26/// Run a traceability audit on a contract.
27///
28/// Checks that every proof obligation is covered by at least
29/// one falsification test or Kani harness, and that no
30/// harnesses reference non-existent obligations.
31pub fn audit_contract(contract: &Contract) -> AuditReport {
32    let mut violations = Vec::new();
33
34    // Kernel-only audit checks: skip for registries, model-family schemas,
35    // pattern contracts, and reference documents that are exempt from
36    // the provability invariant.
37    let is_kernel_kind = contract.requires_proofs();
38
39    // Check: contract has at least one falsification test (kernel only)
40    if is_kernel_kind && contract.falsification_tests.is_empty() {
41        violations.push(Violation {
42            severity: Severity::Warning,
43            rule: "AUDIT-001".to_string(),
44            message: "No falsification tests — contract is \
45                      not falsifiable"
46                .to_string(),
47            location: Some("falsification_tests".to_string()),
48        });
49    }
50
51    // Check: every Kani harness references a valid obligation ID
52    let obligation_props: Vec<&str> = contract
53        .proof_obligations
54        .iter()
55        .map(|o| o.property.as_str())
56        .collect();
57
58    for harness in &contract.kani_harnesses {
59        // The harness.obligation field should reference an
60        // obligation. We do a soft check (warn, not error)
61        // since obligation IDs are free-form in current schema.
62        if harness.obligation.is_empty() {
63            violations.push(Violation {
64                severity: Severity::Error,
65                rule: "AUDIT-002".to_string(),
66                message: format!(
67                    "Kani harness {} has empty obligation \
68                     reference",
69                    harness.id
70                ),
71                location: Some(format!("kani_harnesses.{}.obligation", harness.id)),
72            });
73        }
74    }
75
76    // Check: contract has proof obligations if it has equations (kernel only)
77    if is_kernel_kind && !contract.equations.is_empty() && contract.proof_obligations.is_empty() {
78        violations.push(Violation {
79            severity: Severity::Warning,
80            rule: "AUDIT-003".to_string(),
81            message: "Equations defined but no proof obligations \
82                      — obligations should be derived from equations"
83                .to_string(),
84            location: Some("proof_obligations".to_string()),
85        });
86    }
87
88    // Suppress unused variable warning
89    let _ = obligation_props;
90
91    AuditReport {
92        equations: contract.equations.len(),
93        obligations: contract.proof_obligations.len(),
94        falsification_tests: contract.falsification_tests.len(),
95        kani_harnesses: contract.kani_harnesses.len(),
96        violations,
97    }
98}
99
100/// Binding audit result — cross-references contracts with implementations.
101#[derive(Debug, Clone)]
102pub struct BindingAuditReport {
103    /// Total equations across all contracts.
104    pub total_equations: usize,
105    /// Equations with a binding entry.
106    pub bound_equations: usize,
107    /// Bindings with status = implemented.
108    pub implemented: usize,
109    /// Bindings with status = partial.
110    pub partial: usize,
111    /// Bindings with status = `not_implemented`.
112    pub not_implemented: usize,
113    /// Total proof obligations across matched contracts.
114    pub total_obligations: usize,
115    /// Obligations covered by implemented bindings.
116    pub covered_obligations: usize,
117    /// Violations found during binding audit.
118    pub violations: Vec<Violation>,
119}
120
121/// Audit the binding registry against a set of contracts.
122///
123/// For each contract, checks whether its equations have a
124/// corresponding binding entry and reports coverage gaps.
125pub fn audit_binding(
126    contracts: &[(&str, &Contract)],
127    binding: &BindingRegistry,
128) -> BindingAuditReport {
129    let mut violations = Vec::new();
130    let mut total_equations = 0usize;
131    let mut bound_equations = 0usize;
132    let mut implemented = 0usize;
133    let mut partial = 0usize;
134    let mut not_implemented = 0usize;
135    let mut total_obligations = 0usize;
136    let mut covered_obligations = 0usize;
137
138    for &(contract_file, contract) in contracts {
139        let eq_count = contract.equations.len();
140        total_equations += eq_count;
141        total_obligations += contract.proof_obligations.len();
142
143        for eq_name in contract.equations.keys() {
144            let matching = binding.find_binding(contract_file, eq_name);
145
146            match matching {
147                Some(b) => {
148                    bound_equations += 1;
149                    match b.status {
150                        ImplStatus::Implemented => {
151                            implemented += 1;
152                            // All obligations for this equation
153                            // are considered covered.
154                            covered_obligations += obligations_for_equation(contract);
155                        }
156                        ImplStatus::Partial => {
157                            partial += 1;
158                            violations.push(Violation {
159                                severity: Severity::Warning,
160                                rule: "BIND-002".to_string(),
161                                message: format!(
162                                    "Equation '{eq_name}' in \
163                                     {contract_file} is partially \
164                                     implemented"
165                                ),
166                                location: Some(format!("bindings.{eq_name}")),
167                            });
168                        }
169                        ImplStatus::NotImplemented => {
170                            not_implemented += 1;
171                            violations.push(Violation {
172                                severity: Severity::Warning,
173                                rule: "BIND-003".to_string(),
174                                message: format!(
175                                    "Equation '{eq_name}' in \
176                                     {contract_file} has no \
177                                     implementation"
178                                ),
179                                location: Some(format!("bindings.{eq_name}")),
180                            });
181                        }
182                        ImplStatus::Pending => {
183                            not_implemented += 1;
184                            violations.push(Violation {
185                                severity: Severity::Warning,
186                                rule: "BIND-004".to_string(),
187                                message: format!(
188                                    "Equation '{eq_name}' in \
189                                     {contract_file} is pending \
190                                     implementation"
191                                ),
192                                location: Some(format!("bindings.{eq_name}")),
193                            });
194                        }
195                    }
196                }
197                None => {
198                    violations.push(Violation {
199                        severity: Severity::Error,
200                        rule: "BIND-001".to_string(),
201                        message: format!(
202                            "Equation '{eq_name}' in {contract_file} \
203                             has no binding entry"
204                        ),
205                        location: Some(format!("{contract_file}.equations.{eq_name}")),
206                    });
207                }
208            }
209        }
210    }
211
212    BindingAuditReport {
213        total_equations,
214        bound_equations,
215        implemented,
216        partial,
217        not_implemented,
218        total_obligations,
219        covered_obligations,
220        violations,
221    }
222}
223
224/// Count obligations that are not SIMD-specific for a contract.
225///
226/// When an equation is implemented, we consider all non-SIMD
227/// obligations as covered (SIMD equivalence requires a separate
228/// SIMD implementation which is trueno's domain).
229fn obligations_for_equation(contract: &Contract) -> usize {
230    contract
231        .proof_obligations
232        .iter()
233        .filter(|o| o.applies_to != Some(crate::schema::AppliesTo::Simd))
234        .count()
235}
236
237#[cfg(test)]
238mod tests {
239    include!("mod_tests.rs");
240}