provable_contracts/scoring/
codebase.rs1use std::collections::{BTreeSet, HashMap};
4
5use crate::binding::{BindingRegistry, ImplStatus, normalize_contract_id};
6use crate::schema::{Contract, LeanStatus};
7
8use super::score_contract;
9use super::types::{CodebaseScore, Grade, ScoringGap};
10
11#[allow(clippy::cast_precision_loss)]
22pub fn score_codebase(
23 contracts: &[(String, &Contract)],
24 binding: &BindingRegistry,
25) -> CodebaseScore {
26 score_codebase_with_pagerank(contracts, binding, None)
27}
28
29#[allow(clippy::cast_precision_loss, clippy::implicit_hasher)]
35pub fn score_codebase_with_pagerank(
36 contracts: &[(String, &Contract)],
37 binding: &BindingRegistry,
38 pagerank: Option<&HashMap<String, f64>>,
39) -> CodebaseScore {
40 score_codebase_full(contracts, binding, pagerank, None)
41}
42
43#[allow(clippy::cast_precision_loss, clippy::implicit_hasher)]
45pub fn score_codebase_full(
46 contracts: &[(String, &Contract)],
47 binding: &BindingRegistry,
48 pagerank: Option<&HashMap<String, f64>>,
49 drift_override: Option<f64>,
50) -> CodebaseScore {
51 let bound_stems: BTreeSet<_> = binding
52 .bindings
53 .iter()
54 .map(|b| b.contract.as_str())
55 .collect();
56
57 let unique_declared: BTreeSet<_> = binding
61 .bindings
62 .iter()
63 .map(|b| normalize_contract_id(&b.contract))
64 .collect();
65 let declared_count = unique_declared.len();
66
67 let contract_coverage = if declared_count == 0 {
68 0.0
69 } else {
70 let resolved = unique_declared
72 .iter()
73 .filter(|stem| {
74 contracts
75 .iter()
76 .any(|(s, _)| normalize_contract_id(s) == **stem)
77 })
78 .count();
79 resolved as f64 / declared_count as f64
80 };
81
82 let total_bindings = binding.bindings.len();
84 let implemented_bindings: f64 = binding
85 .bindings
86 .iter()
87 .map(|b| match b.status {
88 ImplStatus::Implemented => 1.0,
89 ImplStatus::Partial => 0.5,
90 ImplStatus::NotImplemented | ImplStatus::Pending => 0.0,
91 })
92 .sum();
93 let binding_completeness = if total_bindings == 0 {
94 0.0
95 } else {
96 implemented_bindings / total_bindings as f64
97 };
98
99 let critical_path_coverage = if binding.critical_path.is_empty() {
103 binding_completeness } else {
105 let covered = binding
106 .critical_path
107 .iter()
108 .filter(|cp| {
109 binding.bindings.iter().any(|b| {
110 b.function
111 .as_deref()
112 .is_some_and(|f| f.contains(cp.as_str()))
113 })
114 })
115 .count();
116 #[allow(clippy::cast_precision_loss)]
117 let ratio = covered as f64 / binding.critical_path.len() as f64;
118 ratio
119 };
120
121 let bound_scores: Vec<f64> = contracts
123 .iter()
124 .filter(|(stem, _)| bound_stems.contains(stem.as_str()))
125 .map(|(stem, c)| score_contract(c, Some(binding), stem).composite)
126 .collect();
127 let mean_contract_score = if bound_scores.is_empty() {
128 0.0
129 } else {
130 bound_scores.iter().sum::<f64>() / bound_scores.len() as f64
131 };
132
133 let proof_depth_dist = compute_proof_depth(contracts, &bound_stems);
135
136 let drift = drift_override.unwrap_or(1.0);
138
139 let composite = contract_coverage * 0.25
140 + critical_path_coverage * 0.20
141 + mean_contract_score * 0.20
142 + proof_depth_dist * 0.15
143 + drift * 0.20;
144
145 let top_gaps = compute_gaps(contracts, binding, &bound_stems, pagerank);
146
147 CodebaseScore {
148 path: "codebase".to_string(),
149 contract_coverage,
150 binding_completeness: critical_path_coverage,
151 mean_contract_score,
152 proof_depth_dist,
153 drift,
154 reverse_coverage: 0.0,
157 mutation_testing: 1.0,
158 ci_pipeline_depth: 1.0,
159 proof_freshness: 1.0,
160 defect_patterns: 1.0,
161 composite,
162 grade: Grade::from_score(composite),
163 top_gaps,
164 }
165}
166
167#[allow(clippy::cast_precision_loss)]
168fn compute_proof_depth(contracts: &[(String, &Contract)], bound_stems: &BTreeSet<&str>) -> f64 {
169 let mut total_obligations = 0usize;
170 let mut weighted_sum = 0.0;
171
172 for (stem, contract) in contracts {
173 if !bound_stems.contains(stem.as_str()) {
174 continue;
175 }
176 for ob in &contract.proof_obligations {
177 total_obligations += 1;
178 weighted_sum += 0.1; if !contract.falsification_tests.is_empty() {
180 weighted_sum += 0.3; }
182 if !contract.kani_harnesses.is_empty() {
183 weighted_sum += 0.4; }
185 if ob
186 .lean
187 .as_ref()
188 .is_some_and(|l| l.status == LeanStatus::Proved)
189 {
190 weighted_sum += 0.2; }
192 }
193 }
194
195 if total_obligations == 0 {
196 return 0.0;
197 }
198 (weighted_sum / total_obligations as f64).min(1.0)
199}
200
201#[allow(clippy::cast_precision_loss)]
207fn compute_gaps(
208 contracts: &[(String, &Contract)],
209 binding: &BindingRegistry,
210 bound_stems: &BTreeSet<&str>,
211 pagerank: Option<&HashMap<String, f64>>,
212) -> Vec<ScoringGap> {
213 let mut gaps = Vec::new();
214
215 let rev_dep_counts = compute_reverse_dep_counts(contracts);
217
218 for (stem, contract) in contracts {
219 if !bound_stems.contains(stem.as_str()) {
220 continue;
221 }
222 let ob_count = contract.proof_obligations.len();
223 let kani_count = contract.kani_harnesses.len();
224 let ft_count = contract.falsification_tests.len();
225 let fanout = dependency_fanout(stem, pagerank, &rev_dep_counts);
226
227 if ob_count > 0 && kani_count < ob_count {
228 let coverage = kani_count as f64 / ob_count as f64;
229 gaps.push(ScoringGap {
230 contract: stem.clone(),
231 dimension: "kani_coverage".into(),
232 current: coverage,
233 target: 1.0,
234 impact: (1.0 - coverage) * fanout,
235 action: "Write #[kani::proof] harnesses".into(),
236 });
237 }
238
239 if ob_count > 0 && ft_count < ob_count {
240 let coverage = ft_count as f64 / ob_count as f64;
241 gaps.push(ScoringGap {
242 contract: stem.clone(),
243 dimension: "falsification_coverage".into(),
244 current: coverage,
245 target: 1.0,
246 impact: (1.0 - coverage) * fanout,
247 action: "Write probar property tests".into(),
248 });
249 }
250
251 let partial_count = binding
252 .bindings_for(stem)
253 .iter()
254 .filter(|b| b.status == ImplStatus::Partial)
255 .count();
256 if partial_count > 0 {
257 gaps.push(ScoringGap {
258 contract: stem.clone(),
259 dimension: "binding_partial".into(),
260 current: 0.5,
261 target: 1.0,
262 impact: 0.5 * fanout,
263 action: "Complete partial implementations".into(),
264 });
265 }
266
267 let unimpl_count = binding
268 .bindings_for(stem)
269 .iter()
270 .filter(|b| b.status == ImplStatus::NotImplemented)
271 .count();
272 if unimpl_count > 0 {
273 gaps.push(ScoringGap {
274 contract: stem.clone(),
275 dimension: "binding_coverage".into(),
276 current: 0.0,
277 target: 1.0,
278 impact: 1.0 * fanout,
279 action: "Implement bound equations".into(),
280 });
281 }
282 }
283
284 gaps.sort_by(|a, b| {
285 b.impact
286 .partial_cmp(&a.impact)
287 .unwrap_or(std::cmp::Ordering::Equal)
288 });
289 gaps.truncate(10);
290 gaps
291}
292
293#[allow(clippy::cast_precision_loss)]
298fn dependency_fanout(
299 stem: &str,
300 pagerank: Option<&HashMap<String, f64>>,
301 rev_dep_counts: &HashMap<&str, usize>,
302) -> f64 {
303 if let Some(pr) = pagerank {
304 if let Some(&score) = pr.get(stem) {
305 let max_pr = pr.values().copied().fold(f64::NEG_INFINITY, f64::max);
306 let min_pr = pr.values().copied().fold(f64::INFINITY, f64::min);
307 let range = max_pr - min_pr;
308 if range > 1e-12 {
309 return 1.0 + 9.0 * (score - min_pr) / range;
311 }
312 }
313 }
314 (rev_dep_counts.get(stem).copied().unwrap_or(0) + 1) as f64
316}
317
318fn compute_reverse_dep_counts<'a>(contracts: &'a [(String, &Contract)]) -> HashMap<&'a str, usize> {
320 let mut counts: HashMap<&str, usize> = HashMap::new();
321 for (_, contract) in contracts {
322 for dep in &contract.metadata.depends_on {
323 for (stem, _) in contracts {
325 if stem == dep || stem.strip_suffix(".yaml").is_some_and(|s| s == dep) {
326 *counts.entry(stem.as_str()).or_default() += 1;
327 }
328 }
329 }
330 }
331 counts
332}
333
334#[cfg(test)]
335#[path = "codebase_tests.rs"]
336mod tests;