reputation_core/calculator/
utils.rs

1//! Utility methods for reputation score analysis and predictions
2//! 
3//! Provides helpful methods for understanding scores, planning interactions,
4//! and comparing agents.
5
6use crate::{Calculator, Result, CalculationError};
7use reputation_types::{AgentData, ScoreComponents};
8use serde::{Serialize, Deserialize};
9
10/// Detailed explanation of how a score was calculated
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ScoreExplanation {
13    /// The final calculated score
14    pub final_score: f64,
15    /// The confidence level (0-1)
16    pub confidence: f64,
17    /// Human-readable explanation
18    pub explanation: String,
19    /// Detailed score breakdown
20    pub breakdown: ScoreComponents,
21}
22
23/// Prediction of score changes from additional reviews
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ScorePrediction {
26    /// Current reputation score
27    pub current_score: f64,
28    /// Predicted score after new reviews
29    pub predicted_score: f64,
30    /// Change in score (can be negative)
31    pub score_change: f64,
32    /// Change in confidence level
33    pub confidence_change: f64,
34    /// Number of reviews to be added
35    pub reviews_added: u32,
36    /// Average rating of new reviews
37    pub rating_used: f64,
38}
39
40/// Comparison between two agents' reputation scores
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct AgentComparison {
43    /// DID of first agent
44    pub agent_a_id: String,
45    /// DID of second agent
46    pub agent_b_id: String,
47    /// Score of first agent
48    pub score_a: f64,
49    /// Score of second agent
50    pub score_b: f64,
51    /// Difference in scores (A - B)
52    pub score_difference: f64,
53    /// Confidence level of first agent
54    pub confidence_a: f64,
55    /// Confidence level of second agent
56    pub confidence_b: f64,
57    /// DID of agent with higher score
58    pub higher_score_agent: String,
59    /// DID of agent with higher confidence (more reliable)
60    pub more_reliable_agent: String,
61}
62
63impl Calculator {
64    /// Calculate how many additional interactions are needed to reach a target confidence level
65    /// 
66    /// # Arguments
67    /// 
68    /// * `current` - Current number of interactions
69    /// * `target` - Target confidence level (0.0 to 1.0)
70    /// 
71    /// # Returns
72    /// 
73    /// Number of additional interactions needed
74    /// 
75    /// # Example
76    /// 
77    /// ```
78    /// use reputation_core::Calculator;
79    /// 
80    /// let calc = Calculator::default();
81    /// 
82    /// // How many more interactions to reach 90% confidence?
83    /// let needed = calc.interactions_for_confidence(50, 0.9).unwrap();
84    /// println!("Need {} more interactions for 90% confidence", needed);
85    /// ```
86    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        // Handle edge cases
92        if target == 0.0 {
93            return Ok(0);
94        }
95        if target == 1.0 {
96            // Can never reach exactly 1.0
97            return Err(CalculationError::InvalidConfidence(target).into());
98        }
99        
100        // Solve for n: target = n / (n + k)
101        // Rearranging: n = (target * k) / (1 - target)
102        let required_total = (target * self.confidence_k) / (1.0 - target);
103        let required_total = required_total.ceil() as u32;
104        
105        // Return additional interactions needed
106        Ok(required_total.saturating_sub(current))
107    }
108    
109    /// Calculate what the confidence level will be after additional interactions
110    /// 
111    /// # Arguments
112    /// 
113    /// * `current` - Current number of interactions
114    /// * `additional` - Additional interactions to add
115    /// 
116    /// # Returns
117    /// 
118    /// The confidence level after the additional interactions
119    /// 
120    /// # Example
121    /// 
122    /// ```
123    /// use reputation_core::Calculator;
124    /// 
125    /// let calc = Calculator::default();
126    /// 
127    /// // What will confidence be after 50 more interactions?
128    /// let future_confidence = calc.confidence_after_interactions(100, 50);
129    /// println!("Confidence will be {:.1}%", future_confidence * 100.0);
130    /// ```
131    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    /// Explain how a reputation score was calculated in human-readable format
137    /// 
138    /// Provides a detailed breakdown of all components that went into the score.
139    /// 
140    /// # Example
141    /// 
142    /// ```
143    /// use reputation_core::Calculator;
144    /// use reputation_types::AgentDataBuilder;
145    /// 
146    /// let calc = Calculator::default();
147    /// let agent = AgentDataBuilder::new("did:example:123")
148    ///     .with_reviews(50, 4.2)
149    ///     .total_interactions(100)
150    ///     .mcp_level(2)
151    ///     .identity_verified(true)
152    ///     .build()
153    ///     .unwrap();
154    /// 
155    /// let explanation = calc.explain_score(&agent).unwrap();
156    /// println!("{}", explanation.explanation);
157    /// ```
158    pub fn explain_score(&self, agent: &AgentData) -> Result<ScoreExplanation> {
159        let score = self.calculate(agent)?;
160        
161        // Build detailed explanation
162        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        // Add prior breakdown details
170        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        // Add empirical score details
189        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        // Add confidence details
201        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        // Add weighting details
216        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    /// Predict how the score will change with additional reviews
230    /// 
231    /// # Arguments
232    /// 
233    /// * `agent` - Current agent data
234    /// * `new_reviews` - Number of new reviews to add
235    /// * `new_rating` - Average rating of new reviews (1.0 to 5.0)
236    /// 
237    /// # Example
238    /// 
239    /// ```
240    /// use reputation_core::Calculator;
241    /// use reputation_types::AgentDataBuilder;
242    /// 
243    /// let calc = Calculator::default();
244    /// let agent = AgentDataBuilder::new("did:example:123")
245    ///     .with_reviews(50, 4.0)
246    ///     .total_interactions(50)
247    ///     .build()
248    ///     .unwrap();
249    /// 
250    /// // What if they get 10 more 5-star reviews?
251    /// let prediction = calc.predict_score_change(&agent, 10, 5.0).unwrap();
252    /// println!("Score would change by {:.1} points", prediction.score_change);
253    /// ```
254    pub fn predict_score_change(
255        &self,
256        agent: &AgentData,
257        new_reviews: u32,
258        new_rating: f64,
259    ) -> Result<ScorePrediction> {
260        // Validate rating
261        if new_rating < 1.0 || new_rating > 5.0 {
262            return Err(crate::ValidationError::InvalidRating(new_rating).into());
263        }
264        
265        // Calculate current score
266        let current_score = self.calculate(agent)?;
267        
268        // Clone and modify agent data for prediction
269        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        // Recalculate average rating
274        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            // First reviews
284            future_agent.average_rating = Some(new_rating);
285        }
286        
287        // Update positive/negative review counts (approximate)
288        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        // Calculate future score
296        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    /// Compare two agents' reputation scores
309    /// 
310    /// Provides a detailed comparison including scores, confidence levels,
311    /// and which agent is more reliable.
312    /// 
313    /// # Example
314    /// 
315    /// ```
316    /// use reputation_core::Calculator;
317    /// use reputation_types::AgentDataBuilder;
318    /// 
319    /// let calc = Calculator::default();
320    /// 
321    /// let agent_a = AgentDataBuilder::new("did:example:alice")
322    ///     .with_reviews(100, 4.5)
323    ///     .total_interactions(150)
324    ///     .build()
325    ///     .unwrap();
326    /// 
327    /// let agent_b = AgentDataBuilder::new("did:example:bob")
328    ///     .with_reviews(20, 4.8)
329    ///     .total_interactions(25)
330    ///     .build()
331    ///     .unwrap();
332    /// 
333    /// let comparison = calc.compare_agents(&agent_a, &agent_b).unwrap();
334    /// println!("Higher score: {}", comparison.higher_score_agent);
335    /// println!("More reliable: {}", comparison.more_reliable_agent);
336    /// ```
337    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        // Test basic calculation
377        let needed = calc.interactions_for_confidence(0, 0.5).unwrap();
378        assert_eq!(needed, 15); // k / (1 - 0.5) = 15
379        
380        let needed = calc.interactions_for_confidence(10, 0.5).unwrap();
381        assert_eq!(needed, 5); // 15 - 10 = 5
382        
383        // Test high confidence target
384        let needed = calc.interactions_for_confidence(0, 0.9).unwrap();
385        assert_eq!(needed, 136); // ceil((0.9 * 15) / 0.1) = 136 due to floating point precision
386        
387        // Test invalid targets
388        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()); // Can't reach 1.0
391    }
392    
393    #[test]
394    fn test_confidence_after_interactions() {
395        let calc = Calculator::default();
396        
397        // Test basic cases
398        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        // Predict improvement with good reviews
443        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        // Predict decline with bad reviews
450        let prediction = calc.predict_score_change(&agent, 10, 1.0).unwrap();
451        assert!(prediction.score_change < 0.0);
452        
453        // Test invalid rating
454        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        // Agent A has higher confidence, Agent B has slightly higher empirical but lower final score
476        // Agent A: 100 interactions, 4.5 rating, high confidence
477        // Agent B: 20 interactions, 4.8 rating, lower confidence
478        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        // Test with zero reviews
489        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        // Test score prediction with no current reviews
497        let prediction = calc.predict_score_change(&agent, 5, 4.0).unwrap();
498        assert!(prediction.score_change != 0.0);
499    }
500}