reputation_core/calculator/
utils.rs1use crate::{Calculator, Result, CalculationError};
7use reputation_types::{AgentData, ScoreComponents};
8use serde::{Serialize, Deserialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ScoreExplanation {
13 pub final_score: f64,
15 pub confidence: f64,
17 pub explanation: String,
19 pub breakdown: ScoreComponents,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ScorePrediction {
26 pub current_score: f64,
28 pub predicted_score: f64,
30 pub score_change: f64,
32 pub confidence_change: f64,
34 pub reviews_added: u32,
36 pub rating_used: f64,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct AgentComparison {
43 pub agent_a_id: String,
45 pub agent_b_id: String,
47 pub score_a: f64,
49 pub score_b: f64,
51 pub score_difference: f64,
53 pub confidence_a: f64,
55 pub confidence_b: f64,
57 pub higher_score_agent: String,
59 pub more_reliable_agent: String,
61}
62
63impl Calculator {
64 pub fn interactions_for_confidence(&self, current: u32, target: f64) -> Result<u32> {
87 if target < 0.0 || target > 1.0 {
88 return Err(CalculationError::InvalidConfidence(target).into());
89 }
90
91 if target == 0.0 {
93 return Ok(0);
94 }
95 if target == 1.0 {
96 return Err(CalculationError::InvalidConfidence(target).into());
98 }
99
100 let required_total = (target * self.confidence_k) / (1.0 - target);
103 let required_total = required_total.ceil() as u32;
104
105 Ok(required_total.saturating_sub(current))
107 }
108
109 pub fn confidence_after_interactions(&self, current: u32, additional: u32) -> f64 {
132 let total = current.saturating_add(additional) as f64;
133 total / (total + self.confidence_k)
134 }
135
136 pub fn explain_score(&self, agent: &AgentData) -> Result<ScoreExplanation> {
159 let score = self.calculate(agent)?;
160
161 let mut explanation_parts = vec![
163 format!("Reputation score of {:.1} calculated from:", score.score),
164 format!(""),
165 format!("📊 Score Components:"),
166 format!("• Prior score: {:.1} points", score.components.prior_score),
167 ];
168
169 let prior = &score.components.prior_breakdown;
171 explanation_parts.push(format!(" - Base score: {:.1}", prior.base_score));
172 if prior.mcp_bonus > 0.0 {
173 explanation_parts.push(format!(" - MCP bonus: +{:.1}", prior.mcp_bonus));
174 }
175 if prior.identity_bonus > 0.0 {
176 explanation_parts.push(format!(" - Identity verified: +{:.1}", prior.identity_bonus));
177 }
178 if prior.security_audit_bonus > 0.0 {
179 explanation_parts.push(format!(" - Security audit: +{:.1}", prior.security_audit_bonus));
180 }
181 if prior.open_source_bonus > 0.0 {
182 explanation_parts.push(format!(" - Open source: +{:.1}", prior.open_source_bonus));
183 }
184 if prior.age_bonus > 0.0 {
185 explanation_parts.push(format!(" - Age bonus: +{:.1}", prior.age_bonus));
186 }
187
188 explanation_parts.push(format!(""));
190 explanation_parts.push(format!("• Empirical score: {:.1} points", score.components.empirical_score));
191 if agent.total_reviews > 0 {
192 explanation_parts.push(format!(" - From {} reviews", agent.total_reviews));
193 if let Some(rating) = agent.average_rating {
194 explanation_parts.push(format!(" - Average rating: {:.1}/5.0", rating));
195 }
196 } else {
197 explanation_parts.push(format!(" - No reviews yet"));
198 }
199
200 explanation_parts.push(format!(""));
202 explanation_parts.push(format!("🎯 Confidence Level: {:.1}% ({})",
203 score.confidence * 100.0,
204 match score.level {
205 reputation_types::ConfidenceLevel::Low => "Low",
206 reputation_types::ConfidenceLevel::Medium => "Medium",
207 reputation_types::ConfidenceLevel::High => "High",
208 }
209 ));
210 explanation_parts.push(format!(" - Based on {} interactions", agent.total_interactions));
211 if score.is_provisional {
212 explanation_parts.push(format!(" - ⚠️ Provisional score (low confidence)"));
213 }
214
215 explanation_parts.push(format!(""));
217 explanation_parts.push(format!("⚖️ Weighting:"));
218 explanation_parts.push(format!(" - Prior weight: {:.1}%", (1.0 - score.confidence) * 100.0));
219 explanation_parts.push(format!(" - Empirical weight: {:.1}%", score.confidence * 100.0));
220
221 Ok(ScoreExplanation {
222 final_score: score.score,
223 confidence: score.confidence,
224 explanation: explanation_parts.join("\n"),
225 breakdown: score.components,
226 })
227 }
228
229 pub fn predict_score_change(
255 &self,
256 agent: &AgentData,
257 new_reviews: u32,
258 new_rating: f64,
259 ) -> Result<ScorePrediction> {
260 if new_rating < 1.0 || new_rating > 5.0 {
262 return Err(crate::ValidationError::InvalidRating(new_rating).into());
263 }
264
265 let current_score = self.calculate(agent)?;
267
268 let mut future_agent = agent.clone();
270 future_agent.total_reviews = future_agent.total_reviews.saturating_add(new_reviews);
271 future_agent.total_interactions = future_agent.total_interactions.saturating_add(new_reviews);
272
273 if let Some(current_rating) = agent.average_rating {
275 let current_weight = agent.total_reviews as f64;
276 let new_weight = new_reviews as f64;
277 let total_weight = current_weight + new_weight;
278
279 future_agent.average_rating = Some(
280 ((current_rating * current_weight) + (new_rating * new_weight)) / total_weight
281 );
282 } else {
283 future_agent.average_rating = Some(new_rating);
285 }
286
287 let positive_ratio = new_rating / 5.0;
289 let new_positive = (new_reviews as f64 * positive_ratio).round() as u32;
290 let new_negative = new_reviews.saturating_sub(new_positive);
291
292 future_agent.positive_reviews = future_agent.positive_reviews.saturating_add(new_positive);
293 future_agent.negative_reviews = future_agent.negative_reviews.saturating_add(new_negative);
294
295 let future_score = self.calculate(&future_agent)?;
297
298 Ok(ScorePrediction {
299 current_score: current_score.score,
300 predicted_score: future_score.score,
301 score_change: future_score.score - current_score.score,
302 confidence_change: future_score.confidence - current_score.confidence,
303 reviews_added: new_reviews,
304 rating_used: new_rating,
305 })
306 }
307
308 pub fn compare_agents(
338 &self,
339 agent_a: &AgentData,
340 agent_b: &AgentData,
341 ) -> Result<AgentComparison> {
342 let score_a = self.calculate(agent_a)?;
343 let score_b = self.calculate(agent_b)?;
344
345 Ok(AgentComparison {
346 agent_a_id: agent_a.did.clone(),
347 agent_b_id: agent_b.did.clone(),
348 score_a: score_a.score,
349 score_b: score_b.score,
350 score_difference: score_a.score - score_b.score,
351 confidence_a: score_a.confidence,
352 confidence_b: score_b.confidence,
353 higher_score_agent: if score_a.score >= score_b.score {
354 agent_a.did.clone()
355 } else {
356 agent_b.did.clone()
357 },
358 more_reliable_agent: if score_a.confidence >= score_b.confidence {
359 agent_a.did.clone()
360 } else {
361 agent_b.did.clone()
362 },
363 })
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use reputation_types::AgentDataBuilder;
371
372 #[test]
373 fn test_interactions_for_confidence() {
374 let calc = Calculator::default();
375
376 let needed = calc.interactions_for_confidence(0, 0.5).unwrap();
378 assert_eq!(needed, 15); let needed = calc.interactions_for_confidence(10, 0.5).unwrap();
381 assert_eq!(needed, 5); let needed = calc.interactions_for_confidence(0, 0.9).unwrap();
385 assert_eq!(needed, 136); assert!(calc.interactions_for_confidence(0, -0.1).is_err());
389 assert!(calc.interactions_for_confidence(0, 1.1).is_err());
390 assert!(calc.interactions_for_confidence(0, 1.0).is_err()); }
392
393 #[test]
394 fn test_confidence_after_interactions() {
395 let calc = Calculator::default();
396
397 let conf = calc.confidence_after_interactions(0, 15);
399 assert!((conf - 0.5).abs() < 0.001);
400
401 let conf = calc.confidence_after_interactions(15, 15);
402 assert!((conf - 0.667).abs() < 0.001);
403
404 let conf = calc.confidence_after_interactions(135, 0);
405 assert!((conf - 0.9).abs() < 0.001);
406 }
407
408 #[test]
409 fn test_explain_score() {
410 let calc = Calculator::default();
411
412 let agent = AgentDataBuilder::new("did:test:explain")
413 .with_reviews(50, 4.2)
414 .total_interactions(100)
415 .mcp_level(2)
416 .identity_verified(true)
417 .build()
418 .unwrap();
419
420 let explanation = calc.explain_score(&agent).unwrap();
421
422 assert!(explanation.explanation.contains("Reputation score"));
423 assert!(explanation.explanation.contains("Prior score"));
424 assert!(explanation.explanation.contains("Empirical score"));
425 assert!(explanation.explanation.contains("Confidence Level"));
426 assert!(explanation.explanation.contains("MCP bonus"));
427 assert!(explanation.explanation.contains("Identity verified"));
428 assert_eq!(explanation.final_score, explanation.breakdown.prior_score * (1.0 - explanation.confidence)
429 + explanation.breakdown.empirical_score * explanation.confidence);
430 }
431
432 #[test]
433 fn test_predict_score_change() {
434 let calc = Calculator::default();
435
436 let agent = AgentDataBuilder::new("did:test:predict")
437 .with_reviews(50, 4.0)
438 .total_interactions(50)
439 .build()
440 .unwrap();
441
442 let prediction = calc.predict_score_change(&agent, 10, 5.0).unwrap();
444 assert!(prediction.score_change > 0.0);
445 assert!(prediction.confidence_change > 0.0);
446 assert_eq!(prediction.reviews_added, 10);
447 assert_eq!(prediction.rating_used, 5.0);
448
449 let prediction = calc.predict_score_change(&agent, 10, 1.0).unwrap();
451 assert!(prediction.score_change < 0.0);
452
453 assert!(calc.predict_score_change(&agent, 10, 6.0).is_err());
455 }
456
457 #[test]
458 fn test_compare_agents() {
459 let calc = Calculator::default();
460
461 let agent_a = AgentDataBuilder::new("did:test:alice")
462 .with_reviews(100, 4.5)
463 .total_interactions(150)
464 .build()
465 .unwrap();
466
467 let agent_b = AgentDataBuilder::new("did:test:bob")
468 .with_reviews(20, 4.8)
469 .total_interactions(25)
470 .build()
471 .unwrap();
472
473 let comparison = calc.compare_agents(&agent_a, &agent_b).unwrap();
474
475 assert!(comparison.score_a > comparison.score_b);
479 assert_eq!(comparison.higher_score_agent, "did:test:alice");
480 assert!(comparison.confidence_a > comparison.confidence_b);
481 assert_eq!(comparison.more_reliable_agent, "did:test:alice");
482 }
483
484 #[test]
485 fn test_edge_cases() {
486 let calc = Calculator::default();
487
488 let agent = AgentDataBuilder::new("did:test:new")
490 .build()
491 .unwrap();
492
493 let explanation = calc.explain_score(&agent).unwrap();
494 assert!(explanation.explanation.contains("No reviews yet"));
495
496 let prediction = calc.predict_score_change(&agent, 5, 4.0).unwrap();
498 assert!(prediction.score_change != 0.0);
499 }
500}