1use crate::errors::LitError;
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::Path;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum TrustEvent {
14 Commit { hash: String },
16 Review { target_hash: String },
18 Merge { branch: String },
20 DelegationCompleted { task_id: String },
22 DelegationFailed { task_id: String },
24 Violation { description: String },
26 TokenRevoked { reason: String },
28 PeerVouch { voucher_did: String },
30}
31
32impl TrustEvent {
33 fn score_delta(&self) -> f64 {
35 match self {
36 TrustEvent::Commit { .. } => 1.0,
37 TrustEvent::Review { .. } => 2.0,
38 TrustEvent::Merge { .. } => 1.5,
39 TrustEvent::DelegationCompleted { .. } => 3.0,
40 TrustEvent::DelegationFailed { .. } => -2.0,
41 TrustEvent::Violation { .. } => -10.0,
42 TrustEvent::TokenRevoked { .. } => -5.0,
43 TrustEvent::PeerVouch { .. } => 2.5,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct TrustScore {
51 pub did: String,
53 pub score: f64,
55 pub total_events: u64,
57 pub level: TrustLevel,
59 pub events: Vec<TrustEventRecord>,
61 pub updated: String,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TrustEventRecord {
68 pub event: TrustEvent,
69 pub timestamp: String,
70 pub score_delta: f64,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub enum TrustLevel {
76 Untrusted,
77 Newcomer,
78 Contributor,
79 Trusted,
80 Maintainer,
81}
82
83impl std::fmt::Display for TrustLevel {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 TrustLevel::Untrusted => write!(f, "Untrusted"),
87 TrustLevel::Newcomer => write!(f, "Newcomer"),
88 TrustLevel::Contributor => write!(f, "Contributor"),
89 TrustLevel::Trusted => write!(f, "Trusted"),
90 TrustLevel::Maintainer => write!(f, "Maintainer"),
91 }
92 }
93}
94
95fn level_for_score(score: f64) -> TrustLevel {
96 if score < 10.0 {
97 TrustLevel::Untrusted
98 } else if score < 30.0 {
99 TrustLevel::Newcomer
100 } else if score < 60.0 {
101 TrustLevel::Contributor
102 } else if score < 85.0 {
103 TrustLevel::Trusted
104 } else {
105 TrustLevel::Maintainer
106 }
107}
108
109pub struct TrustEngine {
111 repo_root: std::path::PathBuf,
112}
113
114impl TrustEngine {
115 pub fn new(repo_root: &Path) -> Self {
116 TrustEngine {
117 repo_root: repo_root.to_path_buf(),
118 }
119 }
120
121 fn trust_dir(&self) -> std::path::PathBuf {
122 self.repo_root.join(".lit").join("trust")
123 }
124
125 fn score_path(&self, did: &str) -> std::path::PathBuf {
126 let safe_name: String = did
128 .chars()
129 .map(|c| if c.is_alphanumeric() { c } else { '_' })
130 .collect();
131 self.trust_dir().join(format!("{}.json", safe_name))
132 }
133
134 pub fn get_score(&self, did: &str) -> Result<TrustScore, LitError> {
136 let path = self.score_path(did);
137 if path.exists() {
138 let json = fs::read_to_string(&path)
139 .map_err(|e| LitError::io(format!("Failed to read trust score: {}", e)))?;
140 serde_json::from_str(&json)
141 .map_err(|e| LitError::general(format!("Failed to parse trust score: {}", e)))
142 } else {
143 Ok(TrustScore {
144 did: did.to_string(),
145 score: 25.0, total_events: 0,
147 level: TrustLevel::Newcomer,
148 events: Vec::new(),
149 updated: chrono::Utc::now().to_rfc3339(),
150 })
151 }
152 }
153
154 pub fn record_event(&self, did: &str, event: TrustEvent) -> Result<TrustScore, LitError> {
156 let mut score = self.get_score(did)?;
157 let delta = event.score_delta();
158
159 score.events.push(TrustEventRecord {
160 event,
161 timestamp: chrono::Utc::now().to_rfc3339(),
162 score_delta: delta,
163 });
164
165 score.score = (score.score + delta).clamp(0.0, 100.0);
166 score.total_events += 1;
167 score.level = level_for_score(score.score);
168 score.updated = chrono::Utc::now().to_rfc3339();
169
170 self.save_score(&score)?;
171 Ok(score)
172 }
173
174 pub fn list_agents(&self) -> Result<Vec<TrustScore>, LitError> {
176 let dir = self.trust_dir();
177 if !dir.exists() {
178 return Ok(Vec::new());
179 }
180
181 let mut scores = Vec::new();
182 for entry in fs::read_dir(&dir).map_err(|e| LitError::io(format!("IO error: {}", e)))? {
183 let entry = entry.map_err(|e| LitError::io(format!("IO error: {}", e)))?;
184 if entry.path().extension().is_some_and(|e| e == "json") {
185 if let Ok(json) = fs::read_to_string(entry.path()) {
186 if let Ok(score) = serde_json::from_str::<TrustScore>(&json) {
187 scores.push(score);
188 }
189 }
190 }
191 }
192
193 scores.sort_by(|a, b| {
194 b.score
195 .partial_cmp(&a.score)
196 .unwrap_or(std::cmp::Ordering::Equal)
197 });
198 Ok(scores)
199 }
200
201 fn save_score(&self, score: &TrustScore) -> Result<(), LitError> {
202 let dir = self.trust_dir();
203 fs::create_dir_all(&dir)
204 .map_err(|e| LitError::io(format!("Failed to create trust dir: {}", e)))?;
205
206 let path = self.score_path(&score.did);
207 let json = serde_json::to_string_pretty(score)
208 .map_err(|e| LitError::general(format!("Failed to serialize trust score: {}", e)))?;
209 fs::write(&path, json)
210 .map_err(|e| LitError::io(format!("Failed to write trust score: {}", e)))?;
211 Ok(())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use std::path::PathBuf;
219 use std::sync::atomic::{AtomicU32, Ordering};
220
221 static COUNTER: AtomicU32 = AtomicU32::new(0);
222
223 fn tmp_dir() -> PathBuf {
229 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
230 let dir = std::env::temp_dir().join(format!("lit_trust_test_{}_{}", std::process::id(), n));
231 let _ = fs::remove_dir_all(&dir);
232 fs::create_dir_all(&dir).unwrap();
233 dir
234 }
235
236 #[test]
237 fn test_trust_score_default() {
238 let dir = tmp_dir();
239 let engine = TrustEngine::new(&dir);
240 let score = engine.get_score("did:lit:agent1").unwrap();
241 assert_eq!(score.score, 25.0);
242 assert_eq!(score.level, TrustLevel::Newcomer);
243 let _ = fs::remove_dir_all(&dir);
244 }
245
246 #[test]
247 fn test_trust_score_events() {
248 let dir = tmp_dir();
249 let engine = TrustEngine::new(&dir);
250
251 let score = engine
252 .record_event("did:lit:x", TrustEvent::Commit { hash: "abc".into() })
253 .unwrap();
254 assert_eq!(score.score, 26.0);
255
256 let score = engine
257 .record_event(
258 "did:lit:x",
259 TrustEvent::Violation {
260 description: "force-push".into(),
261 },
262 )
263 .unwrap();
264 assert_eq!(score.score, 16.0);
265 assert_eq!(score.level, TrustLevel::Newcomer);
266
267 let _ = fs::remove_dir_all(&dir);
268 }
269
270 #[test]
271 fn test_trust_level_boundaries() {
272 assert_eq!(level_for_score(0.0), TrustLevel::Untrusted);
273 assert_eq!(level_for_score(25.0), TrustLevel::Newcomer);
274 assert_eq!(level_for_score(50.0), TrustLevel::Contributor);
275 assert_eq!(level_for_score(75.0), TrustLevel::Trusted);
276 assert_eq!(level_for_score(95.0), TrustLevel::Maintainer);
277 }
278}