provable_contracts/scoring/
mod.rs1mod codebase;
10pub mod drift;
11mod pvscore;
12mod types;
13
14pub use codebase::{score_codebase, score_codebase_full, score_codebase_with_pagerank};
15pub use pvscore::pvscore_10dim;
16pub use types::{CodebaseScore, ContractScore, Grade, ScoreProbe, ScoringGap, ScoringWeights};
17
18use crate::binding::{BindingRegistry, ImplStatus};
19use crate::schema::{Contract, KaniHarness, KaniStrategy, LeanStatus};
20
21pub fn score_contract(
30 contract: &Contract,
31 binding: Option<&BindingRegistry>,
32 stem: &str,
33) -> ContractScore {
34 score_contract_weighted(contract, binding, stem, &ScoringWeights::default())
35}
36
37pub fn score_contract_weighted(
39 contract: &Contract,
40 binding: Option<&BindingRegistry>,
41 stem: &str,
42 weights: &ScoringWeights,
43) -> ContractScore {
44 let w = weights.normalized();
45 let mut probes = Vec::new();
46
47 let spec_depth = compute_spec_depth(contract, &mut probes);
48 let falsification = compute_falsification_coverage(contract, &mut probes);
49 let kani = compute_kani_coverage(contract, &mut probes);
50 let lean = compute_lean_coverage(contract, &mut probes);
51 let binding_cov = compute_binding_coverage(contract, binding, stem, &mut probes);
52
53 let (effective_binding, effective_lean) = if contract.requires_proofs() {
58 (binding_cov, lean)
59 } else {
60 (1.0, lean.max(0.5))
61 };
62
63 let composite = spec_depth * w.spec_depth
64 + falsification * w.falsification
65 + kani * w.kani
66 + effective_lean * w.lean
67 + effective_binding * w.binding;
68
69 ContractScore {
70 stem: stem.to_string(),
71 spec_depth,
72 falsification_coverage: falsification,
73 kani_coverage: kani,
74 lean_coverage: effective_lean,
75 binding_coverage: effective_binding,
76 composite,
77 grade: Grade::from_score(composite),
78 probes,
79 }
80}
81
82#[allow(clippy::too_many_lines)]
83fn compute_spec_depth(contract: &Contract, probes: &mut Vec<ScoreProbe>) -> f64 {
84 let mut score = 0.0;
85
86 let has_equations = !contract.equations.is_empty();
88 probes.push(ScoreProbe {
89 dimension: "spec_depth".into(),
90 probe: "has_equations".into(),
91 outcome: has_equations,
92 detail: if has_equations {
93 format!("{} equation(s)", contract.equations.len())
94 } else {
95 "(no equations)".into()
96 },
97 });
98 if has_equations {
99 score += 0.30;
100 }
101
102 for (name, eq) in &contract.equations {
104 let has_domain = eq.domain.is_some();
105 probes.push(ScoreProbe {
106 dimension: "spec_depth".into(),
107 probe: format!("{name}: domain"),
108 outcome: has_domain,
109 detail: if has_domain {
110 eq.domain.as_deref().unwrap_or("").to_string()
111 } else {
112 "(no domain)".into()
113 },
114 });
115 let has_inv = !eq.invariants.is_empty();
116 probes.push(ScoreProbe {
117 dimension: "spec_depth".into(),
118 probe: format!("{name}: invariants"),
119 outcome: has_inv,
120 detail: if has_inv {
121 format!("{} invariant(s)", eq.invariants.len())
122 } else {
123 "(no invariants)".into()
124 },
125 });
126 }
127
128 let has_domains = contract.equations.values().any(|eq| eq.domain.is_some());
130 if has_domains {
131 score += 0.15;
132 }
133
134 let has_invariants = contract
136 .equations
137 .values()
138 .any(|eq| !eq.invariants.is_empty());
139 if has_invariants {
140 score += 0.15;
141 }
142
143 let has_ks = contract.kernel_structure.is_some();
145 probes.push(ScoreProbe {
146 dimension: "spec_depth".into(),
147 probe: "kernel_structure".into(),
148 outcome: has_ks,
149 detail: if has_ks {
150 "present".into()
151 } else {
152 "(no kernel_structure)".into()
153 },
154 });
155 if has_ks {
156 score += 0.15;
157 }
158
159 let has_tolerances = contract
161 .proof_obligations
162 .iter()
163 .any(|ob| ob.tolerance.is_some());
164 probes.push(ScoreProbe {
165 dimension: "spec_depth".into(),
166 probe: "has_tolerances".into(),
167 outcome: has_tolerances,
168 detail: if has_tolerances {
169 "at least one obligation has tolerance".into()
170 } else {
171 "(no tolerances)".into()
172 },
173 });
174 if has_tolerances {
175 score += 0.10;
176 }
177
178 let has_refs = !contract.metadata.references.is_empty();
180 probes.push(ScoreProbe {
181 dimension: "spec_depth".into(),
182 probe: "references".into(),
183 outcome: has_refs,
184 detail: if has_refs {
185 format!("{} reference(s)", contract.metadata.references.len())
186 } else {
187 "(no references)".into()
188 },
189 });
190 if has_refs {
191 score += 0.10;
192 }
193
194 let has_deps = !contract.metadata.depends_on.is_empty();
196 probes.push(ScoreProbe {
197 dimension: "spec_depth".into(),
198 probe: "depends_on".into(),
199 outcome: has_deps,
200 detail: if has_deps {
201 format!("{} dep(s)", contract.metadata.depends_on.len())
202 } else {
203 "(no depends_on)".into()
204 },
205 });
206 if has_deps {
207 score += 0.05;
208 }
209
210 score
211}
212
213#[allow(clippy::cast_precision_loss)]
214fn compute_falsification_coverage(contract: &Contract, probes: &mut Vec<ScoreProbe>) -> f64 {
215 let total = contract.proof_obligations.len();
216
217 for ob in &contract.proof_obligations {
219 let matching_test = contract
220 .falsification_tests
221 .iter()
222 .find(|t| t.rule == ob.property);
223 let has_test = matching_test.is_some();
224 probes.push(ScoreProbe {
225 dimension: "falsification".into(),
226 probe: ob.property.clone(),
227 outcome: has_test,
228 detail: if let Some(t) = matching_test {
229 t.id.clone()
230 } else {
231 "(no test)".into()
232 },
233 });
234 }
235
236 for t in &contract.falsification_tests {
238 let matches_obligation = contract
239 .proof_obligations
240 .iter()
241 .any(|ob| ob.property == t.rule);
242 if !matches_obligation {
243 probes.push(ScoreProbe {
244 dimension: "falsification".into(),
245 probe: t.rule.clone(),
246 outcome: true,
247 detail: format!("{} (unmatched)", t.id),
248 });
249 }
250 }
251
252 if total == 0 {
253 return if contract.falsification_tests.is_empty() {
254 0.0
255 } else {
256 1.0
257 };
258 }
259
260 let covered = contract.falsification_tests.len().min(total);
262 covered as f64 / total as f64
263}
264
265#[allow(clippy::cast_precision_loss)]
266fn compute_kani_coverage(contract: &Contract, probes: &mut Vec<ScoreProbe>) -> f64 {
267 let total = contract.proof_obligations.len();
268
269 for ob in &contract.proof_obligations {
271 let matching_harness = contract
272 .kani_harnesses
273 .iter()
274 .find(|h| h.obligation == ob.property);
275 let has_harness = matching_harness.is_some();
276 probes.push(ScoreProbe {
277 dimension: "kani".into(),
278 probe: ob.property.clone(),
279 outcome: has_harness,
280 detail: if let Some(h) = matching_harness {
281 let strategy_note = h.strategy.map(|s| format!(" [{s}]")).unwrap_or_default();
282 format!("{}{strategy_note}", h.id)
283 } else {
284 "(no harness)".into()
285 },
286 });
287 }
288
289 for h in &contract.kani_harnesses {
291 let matches_obligation = contract
292 .proof_obligations
293 .iter()
294 .any(|ob| ob.property == h.obligation);
295 if !matches_obligation {
296 probes.push(ScoreProbe {
297 dimension: "kani".into(),
298 probe: h.obligation.clone(),
299 outcome: true,
300 detail: format!("{} (unmatched)", h.id),
301 });
302 }
303 }
304
305 if total == 0 {
306 return if contract.kani_harnesses.is_empty() {
307 0.0
308 } else {
309 1.0
310 };
311 }
312
313 let strategy_weight = |h: &KaniHarness| -> f64 {
314 if h.actually_verified == Some(true) {
318 return 1.0;
319 }
320 match h.strategy.as_ref() {
321 Some(KaniStrategy::Exhaustive) => 1.0,
322 Some(KaniStrategy::BoundedInt) => 0.9,
323 Some(KaniStrategy::StubFloat) => 0.8,
324 Some(KaniStrategy::Compositional) => 0.7,
325 None => 0.5,
326 }
327 };
328
329 let weighted_sum: f64 = contract.kani_harnesses.iter().map(strategy_weight).sum();
330 (weighted_sum / total as f64).min(1.0)
331}
332
333#[allow(clippy::cast_precision_loss)]
334fn compute_lean_coverage(contract: &Contract, probes: &mut Vec<ScoreProbe>) -> f64 {
335 for ob in &contract.proof_obligations {
337 match &ob.lean {
338 Some(lean) if lean.status != LeanStatus::NotApplicable => {
339 let is_proved = lean.status == LeanStatus::Proved;
340 probes.push(ScoreProbe {
341 dimension: "lean".into(),
342 probe: ob.property.clone(),
343 outcome: is_proved,
344 detail: if is_proved {
345 format!("{} (proved)", lean.theorem)
346 } else {
347 format!("{} ({})", lean.theorem, lean.status)
348 },
349 });
350 }
351 Some(lean) => {
352 probes.push(ScoreProbe {
354 dimension: "lean".into(),
355 probe: ob.property.clone(),
356 outcome: false,
357 detail: format!("{} (not-applicable)", lean.theorem),
358 });
359 }
360 None => {
361 probes.push(ScoreProbe {
362 dimension: "lean".into(),
363 probe: ob.property.clone(),
364 outcome: false,
365 detail: "(no lean_theorem)".into(),
366 });
367 }
368 }
369 }
370
371 let applicable: Vec<_> = contract
372 .proof_obligations
373 .iter()
374 .filter(|ob| {
375 ob.lean
376 .as_ref()
377 .is_some_and(|l| l.status != LeanStatus::NotApplicable)
378 })
379 .collect();
380
381 if applicable.is_empty() {
382 return 0.0;
383 }
384
385 let proved = applicable
386 .iter()
387 .filter(|ob| {
388 ob.lean
389 .as_ref()
390 .is_some_and(|l| l.status == LeanStatus::Proved)
391 })
392 .count();
393
394 proved as f64 / applicable.len() as f64
395}
396
397#[allow(clippy::cast_precision_loss)]
398fn compute_binding_coverage(
399 _contract: &Contract,
400 binding: Option<&BindingRegistry>,
401 stem: &str,
402 probes: &mut Vec<ScoreProbe>,
403) -> f64 {
404 let Some(binding) = binding else {
405 probes.push(ScoreProbe {
406 dimension: "binding".into(),
407 probe: "(all)".into(),
408 outcome: false,
409 detail: "(no binding registry)".into(),
410 });
411 return 0.0;
412 };
413
414 let relevant = binding.bindings_for(stem);
415
416 if relevant.is_empty() {
417 probes.push(ScoreProbe {
418 dimension: "binding".into(),
419 probe: stem.into(),
420 outcome: false,
421 detail: "(no bindings for this contract)".into(),
422 });
423 return 0.0;
424 }
425
426 for b in &relevant {
428 let status_str = match b.status {
429 ImplStatus::Implemented => "implemented",
430 ImplStatus::Partial => "partial",
431 ImplStatus::NotImplemented => "not_implemented",
432 ImplStatus::Pending => "pending",
433 };
434 let is_implemented = b.status == ImplStatus::Implemented;
435 let fn_name = b
436 .function
437 .as_deref()
438 .or(b.module_path.as_deref())
439 .unwrap_or("?");
440 probes.push(ScoreProbe {
441 dimension: "binding".into(),
442 probe: b.equation.clone(),
443 outcome: is_implemented,
444 detail: format!("{fn_name} ({status_str})"),
445 });
446 }
447
448 let implemented: f64 = relevant
449 .iter()
450 .map(|b| match b.status {
451 ImplStatus::Implemented => 1.0,
452 ImplStatus::Partial => 0.5,
453 ImplStatus::NotImplemented | ImplStatus::Pending => 0.0,
454 })
455 .sum();
456
457 implemented / relevant.len() as f64
458}
459
460#[cfg(test)]
461mod tests {
462 include!("scoring_tests.rs");
463}