provable_contracts/scoring/
drift.rs1use std::collections::HashSet;
9use std::path::Path;
10
11pub fn detect_stale_contracts<S: std::hash::BuildHasher>(
19 contract_dir: &Path,
20 binding_path: &Path,
21 bound_stems: &HashSet<&str, S>,
22) -> HashSet<String> {
23 let mut stale = HashSet::new();
24
25 let binding_ts = git_last_commit_ts(binding_path);
26 if binding_ts == 0 {
27 return stale;
29 }
30
31 for stem in bound_stems {
32 let contract_path = contract_dir.join(stem);
33 if !contract_path.exists() {
34 continue;
35 }
36 let contract_ts = git_last_commit_ts(&contract_path);
37 if contract_ts > binding_ts {
38 stale.insert((*stem).to_string());
39 }
40 }
41
42 stale
43}
44
45#[allow(clippy::cast_precision_loss)]
54pub fn compute_drift(stale_count: usize, total_bound: usize) -> f64 {
55 if total_bound == 0 {
56 return 1.0;
57 }
58 1.0 - (stale_count as f64 / total_bound as f64)
59}
60
61fn git_last_commit_ts(path: &Path) -> u64 {
65 let output = std::process::Command::new("git")
66 .args(["log", "-1", "--format=%ct", "--"])
67 .arg(path)
68 .output();
69
70 match output {
71 Ok(o) if o.status.success() => {
72 let s = String::from_utf8_lossy(&o.stdout);
73 s.trim().parse().unwrap_or(0)
74 }
75 _ => 0,
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn compute_drift_no_stale() {
85 assert!((compute_drift(0, 10) - 1.0).abs() < 1e-9);
86 }
87
88 #[test]
89 fn compute_drift_all_stale() {
90 assert!((compute_drift(5, 5) - 0.0).abs() < 1e-9);
91 }
92
93 #[test]
94 fn compute_drift_partial() {
95 assert!((compute_drift(2, 10) - 0.8).abs() < 1e-9);
96 }
97
98 #[test]
99 fn compute_drift_empty_total() {
100 assert!((compute_drift(0, 0) - 1.0).abs() < 1e-9);
101 }
102
103 #[test]
104 #[ignore] fn git_ts_for_real_contract() {
106 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
107 .join("../../contracts/softmax-kernel-v1.yaml");
108 let ts = git_last_commit_ts(&path);
109 assert!(ts > 0, "softmax contract should have a git timestamp");
110 }
111
112 #[test]
113 fn git_ts_nonexistent_file() {
114 let ts = git_last_commit_ts(Path::new("/nonexistent/file.yaml"));
115 assert_eq!(ts, 0);
116 }
117
118 #[test]
119 fn detect_stale_with_real_files() {
120 let contract_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts");
121 let binding_path = contract_dir.join("aprender/binding.yaml");
122
123 let mut bound = HashSet::new();
124 bound.insert("softmax-kernel-v1.yaml");
125
126 let stale = detect_stale_contracts(&contract_dir, &binding_path, &bound);
127 assert!(stale.len() <= 1);
129 }
130
131 #[test]
132 fn detect_stale_no_git_binding() {
133 let contract_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts");
135 let mut bound = HashSet::new();
136 bound.insert("softmax-kernel-v1.yaml");
137 let stale = detect_stale_contracts(
138 &contract_dir,
139 Path::new("/tmp/nonexistent-binding.yaml"),
140 &bound,
141 );
142 assert!(stale.is_empty());
143 }
144
145 #[test]
146 fn detect_stale_missing_contract_file() {
147 let contract_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts");
149 let binding_path = contract_dir.join("aprender/binding.yaml");
150 let mut bound = HashSet::new();
151 bound.insert("nonexistent-contract-v1.yaml");
152 let stale = detect_stale_contracts(&contract_dir, &binding_path, &bound);
153 assert!(stale.is_empty());
154 }
155}