reputation_core/calculator/
mod.rs

1//! Calculator module for reputation score calculation
2//! 
3//! This module provides the main Calculator struct and coordinates the
4//! calculation of reputation scores using submodules for prior, empirical,
5//! and confidence calculations.
6//! 
7//! ## Architecture
8//! 
9//! The calculator is organized into specialized submodules:
10//! - `prior`: Calculates base reputation from agent credentials (50-80 points)
11//! - `empirical`: Derives performance score from reviews (0-100 points)
12//! - `confidence`: Determines confidence level based on interaction volume
13//! - `builder`: Provides fluent API for calculator configuration
14//! - `utils`: Utility methods for score analysis and predictions
15//! 
16//! ## Calculation Flow
17//! 
18//! 1. **Validation**: Agent data is validated for consistency
19//! 2. **Prior Score**: Base score calculated from credentials
20//! 3. **Empirical Score**: Performance score from reviews
21//! 4. **Confidence**: Weight based on data volume
22//! 5. **Final Score**: Weighted combination of prior and empirical
23//! 
24//! ## Thread Safety
25//! 
26//! The Calculator is thread-safe and can be shared across threads using Arc:
27//! ```no_run
28//! use std::sync::Arc;
29//! use reputation_core::Calculator;
30//! 
31//! let calculator = Arc::new(Calculator::default());
32//! let calc_clone = Arc::clone(&calculator);
33//! 
34//! std::thread::spawn(move || {
35//!     // Use calc_clone in another thread
36//! });
37//! ```
38
39use chrono::Utc;
40use reputation_types::{AgentData, ReputationScore, ConfidenceLevel, ScoreComponents};
41use crate::error::{BuilderError, CalculationError, Result};
42use crate::validation;
43use crate::config::CalculatorConfig;
44use crate::ALGORITHM_VERSION;
45use rayon::prelude::*;
46use std::time::{Duration, Instant};
47
48mod prior;
49mod empirical;
50mod confidence;
51pub mod builder;
52pub mod utils;
53
54// Re-export for internal use
55pub(crate) use self::prior::calculate_prior_detailed;
56pub(crate) use self::empirical::calculate_empirical;
57pub(crate) use self::confidence::calculate_confidence;
58
59/// Calculates reputation scores for MCP agents
60/// 
61/// The Calculator implements a hybrid scoring system that combines
62/// prior reputation (based on credentials) with empirical performance
63/// (based on reviews and interactions).
64/// 
65/// # Example
66/// ```
67/// use reputation_core::Calculator;
68/// use reputation_types::AgentDataBuilder;
69/// 
70/// let calculator = Calculator::default();
71/// let agent = AgentDataBuilder::new("did:example:test")
72///     .with_reviews(50, 4.0)
73///     .total_interactions(100)  // Must be >= total_reviews
74///     .build()
75///     .unwrap();
76/// 
77/// let score = calculator.calculate(&agent).unwrap();
78/// assert!(score.score >= 0.0 && score.score <= 100.0);
79/// ```
80#[derive(Debug)]
81pub struct Calculator {
82    /// Confidence growth parameter (k in the formula)
83    /// Higher values = slower confidence growth
84    pub(crate) confidence_k: f64,
85    
86    /// Base prior score (starting reputation)
87    pub(crate) prior_base: f64,
88    
89    /// Maximum prior score (cap for credential bonuses)
90    pub(crate) prior_max: f64,
91}
92
93impl Default for Calculator {
94    fn default() -> Self {
95        Self {
96            confidence_k: 15.0,
97            prior_base: 50.0,
98            prior_max: 80.0,
99        }
100    }
101}
102
103impl Calculator {
104    /// Get the confidence growth parameter
105    pub fn confidence_k(&self) -> f64 {
106        self.confidence_k
107    }
108    
109    /// Get the base prior score
110    pub fn prior_base(&self) -> f64 {
111        self.prior_base
112    }
113    
114    /// Get the maximum prior score
115    pub fn prior_max(&self) -> f64 {
116        self.prior_max
117    }
118    /// Creates a builder for constructing a Calculator with custom configuration
119    /// 
120    /// # Example
121    /// 
122    /// ```
123    /// use reputation_core::Calculator;
124    /// 
125    /// let calculator = Calculator::builder()
126    ///     .confidence_k(20.0)
127    ///     .prior_base(60.0)
128    ///     .build()
129    ///     .unwrap();
130    /// ```
131    pub fn builder() -> builder::CalculatorBuilder {
132        builder::CalculatorBuilder::new()
133    }
134    
135    /// Creates a Calculator from a configuration
136    /// 
137    /// # Example
138    /// 
139    /// ```
140    /// use reputation_core::{Calculator, CalculatorConfig};
141    /// 
142    /// let config = CalculatorConfig {
143    ///     confidence_k: 20.0,
144    ///     prior_base: 60.0,
145    ///     prior_max: 85.0,
146    /// };
147    /// let calculator = Calculator::from_config(config).unwrap();
148    /// ```
149    pub fn from_config(config: CalculatorConfig) -> Result<Self> {
150        Self::new(config.confidence_k, config.prior_base, config.prior_max)
151    }
152    
153    /// Creates a new Calculator with custom parameters
154    /// 
155    /// # Parameters
156    /// 
157    /// - `confidence_k`: Controls confidence growth rate (must be positive)
158    /// - `prior_base`: Base prior score (must be 0-100)
159    /// - `prior_max`: Maximum prior score cap (must be between prior_base and 100)
160    /// 
161    /// # Errors
162    /// 
163    /// Returns an error if parameters are out of valid ranges
164    /// 
165    /// # Example
166    /// 
167    /// ```
168    /// use reputation_core::Calculator;
169    /// 
170    /// let calculator = Calculator::new(20.0, 60.0, 90.0).unwrap();
171    /// ```
172    pub fn new(confidence_k: f64, prior_base: f64, prior_max: f64) -> Result<Self> {
173        if confidence_k <= 0.0 {
174            return Err(BuilderError::InvalidConfig("confidence_k must be positive".to_string()).into());
175        }
176        if prior_base < 0.0 || prior_base > 100.0 {
177            return Err(BuilderError::InvalidConfig("prior_base must be between 0 and 100".to_string()).into());
178        }
179        if prior_max < prior_base || prior_max > 100.0 {
180            return Err(BuilderError::InvalidConfig("prior_max must be between prior_base and 100".to_string()).into());
181        }
182
183        Ok(Self {
184            confidence_k,
185            prior_base,
186            prior_max,
187        })
188    }
189
190    /// Calculates the reputation score for an agent
191    /// 
192    /// # Algorithm Details
193    /// 
194    /// 1. **Prior Score Calculation** (50-80 points by default):
195    ///    - Base: 50 points
196    ///    - MCP Level bonus: 0-15 points (5 per level)
197    ///    - Identity verified: +5 points
198    ///    - Security audit: +7 points
199    ///    - Open source: +3 points
200    ///    - Age > 365 days: +5 points
201    /// 
202    /// 2. **Empirical Score** (0-100 points):
203    ///    - Based on average rating (1-5 stars)
204    ///    - Converted to 0-100 scale
205    /// 
206    /// 3. **Confidence Calculation**:
207    ///    - Based on total interactions
208    ///    - Approaches 1.0 asymptotically
209    /// 
210    /// # Returns
211    /// 
212    /// A `ReputationScore` containing:
213    /// - `score`: Final reputation (0-100)
214    /// - `confidence`: Confidence in the score (0-1)
215    /// - `algorithm_version`: Version of the algorithm used
216    /// - `calculated_at`: Timestamp of calculation
217    /// 
218    /// # Errors
219    /// 
220    /// Returns an error if:
221    /// - Agent data validation fails
222    /// - Calculation produces NaN or out-of-bounds values
223    /// 
224    /// # Example
225    /// 
226    /// ```
227    /// use reputation_core::Calculator;
228    /// use reputation_types::AgentDataBuilder;
229    /// 
230    /// let calculator = Calculator::default();
231    /// let agent = AgentDataBuilder::new("did:example:123")
232    ///     .with_reviews(100, 4.5)
233    ///     .total_interactions(200)  // Must be >= total_reviews
234    ///     .identity_verified(true)
235    ///     .build()
236    ///     .unwrap();
237    /// 
238    /// let score = calculator.calculate(&agent).unwrap();
239    /// match score.confidence {
240    ///     c if c < 0.3 => println!("Low confidence - needs more data"),
241    ///     c if c < 0.7 => println!("Moderate confidence"),
242    ///     _ => println!("High confidence in score"),
243    /// }
244    /// ```
245    pub fn calculate(&self, agent: &AgentData) -> Result<ReputationScore> {
246        // Validate agent data
247        validation::validate_agent_data(agent)?;
248
249        // Calculate components with detailed breakdown
250        let prior_breakdown = calculate_prior_detailed(agent, self.prior_base, self.prior_max);
251        let prior_score = prior_breakdown.total;
252        let empirical_score = calculate_empirical(agent);
253        let confidence_value = calculate_confidence(agent.total_interactions, self.confidence_k)?;
254        
255        // Determine confidence level
256        let confidence_level = ConfidenceLevel::from_confidence(confidence_value);
257        
258        // Calculate weights
259        let prior_weight = 1.0 - confidence_value;
260        let empirical_weight = confidence_value;
261
262        // Blend scores
263        let final_score = prior_weight * prior_score + empirical_weight * empirical_score;
264
265        // Validate final score
266        if final_score.is_nan() {
267            return Err(CalculationError::NaNResult.into());
268        }
269        if final_score < 0.0 || final_score > 100.0 {
270            return Err(CalculationError::ScoreOutOfBounds(final_score).into());
271        }
272
273        // Create score components
274        let components = ScoreComponents {
275            prior_score,
276            prior_breakdown,
277            empirical_score,
278            confidence_value,
279            confidence_level,
280            prior_weight,
281            empirical_weight,
282        };
283
284        // Calculate total data points
285        let data_points = agent.total_interactions.saturating_add(agent.total_reviews);
286        
287        // Determine if provisional
288        let is_provisional = confidence_value < 0.2;
289
290        Ok(ReputationScore {
291            score: final_score.clamp(0.0, 100.0),
292            confidence: confidence_value,
293            level: confidence_level,
294            components,
295            is_provisional,
296            data_points,
297            algorithm_version: ALGORITHM_VERSION.to_string(),
298            calculated_at: Utc::now(),
299        })
300    }
301
302    /// Calculate reputation scores for multiple agents in parallel
303    /// 
304    /// This method uses rayon to process agents in parallel, providing
305    /// significant performance improvements for large batches.
306    /// 
307    /// # Returns
308    /// 
309    /// A vector of results in the same order as the input agents.
310    /// Each result contains either a ReputationScore or an error.
311    /// 
312    /// # Example
313    /// 
314    /// ```
315    /// use reputation_core::Calculator;
316    /// use reputation_types::AgentDataBuilder;
317    /// 
318    /// let calculator = Calculator::default();
319    /// let agents: Vec<_> = (0..100).map(|i| {
320    ///     AgentDataBuilder::new(&format!("did:example:agent{}", i))
321    ///         .with_reviews(50, 4.0)
322    ///         .total_interactions(100)
323    ///         .build()
324    ///         .unwrap()
325    /// }).collect();
326    /// 
327    /// let results = calculator.calculate_batch(&agents);
328    /// assert_eq!(results.len(), agents.len());
329    /// ```
330    pub fn calculate_batch(&self, agents: &[AgentData]) -> Vec<Result<ReputationScore>> {
331        agents
332            .par_iter()
333            .map(|agent| self.calculate(agent))
334            .collect()
335    }
336
337    /// Calculate reputation scores with batch processing options
338    /// 
339    /// This method provides more control over batch processing with options
340    /// for chunk size, progress tracking, and detailed timing information.
341    /// 
342    /// # Arguments
343    /// 
344    /// * `agents` - Slice of agents to calculate scores for
345    /// * `options` - Configuration options for batch processing
346    /// 
347    /// # Returns
348    /// 
349    /// A `BatchResult` containing:
350    /// - Individual calculation results with timing
351    /// - Total processing duration
352    /// - Success/failure counts
353    /// 
354    /// # Example
355    /// 
356    /// ```
357    /// use reputation_core::{Calculator, BatchOptions};
358    /// use reputation_types::AgentDataBuilder;
359    /// 
360    /// let calculator = Calculator::default();
361    /// let agents: Vec<_> = (0..1000).map(|i| {
362    ///     AgentDataBuilder::new(&format!("did:example:agent{}", i))
363    ///         .with_reviews(50, 4.0)
364    ///         .total_interactions(100)
365    ///         .build()
366    ///         .unwrap()
367    /// }).collect();
368    /// 
369    /// let options = BatchOptions {
370    ///     chunk_size: Some(100),
371    ///     fail_fast: false,
372    ///     progress_callback: None,
373    /// };
374    /// 
375    /// let result = calculator.calculate_batch_with_options(&agents, options);
376    /// println!("Processed {} agents in {:?}", agents.len(), result.total_duration);
377    /// println!("Success: {}, Failed: {}", result.successful_count, result.failed_count);
378    /// ```
379    pub fn calculate_batch_with_options(
380        &self,
381        agents: &[AgentData],
382        options: BatchOptions,
383    ) -> BatchResult {
384        let batch_start = Instant::now();
385        let chunk_size = options.chunk_size.unwrap_or(100);
386        
387        let calculations: Vec<_> = if let Some(ref callback) = options.progress_callback {
388            let processed = std::sync::atomic::AtomicUsize::new(0);
389            let total = agents.len();
390            
391            agents
392                .par_chunks(chunk_size)
393                .flat_map(|chunk| {
394                    chunk.par_iter().map(|agent| {
395                        let start = Instant::now();
396                        let result = self.calculate(agent);
397                        let duration = start.elapsed();
398                        
399                        if options.fail_fast && result.is_err() {
400                            // In fail_fast mode, we'd need a more complex implementation
401                            // to actually stop all threads. For now, we just continue.
402                        }
403                        
404                        let calc = BatchCalculation {
405                            agent_id: agent.did.clone(),
406                            result,
407                            duration,
408                        };
409                        
410                        // Update progress
411                        let current = processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
412                        callback(current, total);
413                        
414                        calc
415                    })
416                })
417                .collect()
418        } else {
419            agents
420                .par_chunks(chunk_size)
421                .flat_map(|chunk| {
422                    chunk.par_iter().map(|agent| {
423                        let start = Instant::now();
424                        let result = self.calculate(agent);
425                        let duration = start.elapsed();
426                        
427                        BatchCalculation {
428                            agent_id: agent.did.clone(),
429                            result,
430                            duration,
431                        }
432                    })
433                })
434                .collect()
435        };
436        
437        let successful_count = calculations.iter().filter(|c| c.result.is_ok()).count();
438        let failed_count = calculations.len() - successful_count;
439        
440        BatchResult {
441            calculations,
442            total_duration: batch_start.elapsed(),
443            successful_count,
444            failed_count,
445        }
446    }
447}
448
449/// Options for batch processing
450pub struct BatchOptions {
451    /// Size of chunks for parallel processing
452    pub chunk_size: Option<usize>,
453    /// Whether to stop on first error (not fully implemented)
454    pub fail_fast: bool,
455    /// Optional callback for progress updates
456    pub progress_callback: Option<Box<dyn Fn(usize, usize) + Send + Sync>>,
457}
458
459impl Default for BatchOptions {
460    fn default() -> Self {
461        Self {
462            chunk_size: None,
463            fail_fast: false,
464            progress_callback: None,
465        }
466    }
467}
468
469impl std::fmt::Debug for BatchOptions {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        f.debug_struct("BatchOptions")
472            .field("chunk_size", &self.chunk_size)
473            .field("fail_fast", &self.fail_fast)
474            .field("progress_callback", &self.progress_callback.is_some())
475            .finish()
476    }
477}
478
479/// Result of batch processing
480#[derive(Debug)]
481pub struct BatchResult {
482    /// Individual calculation results
483    pub calculations: Vec<BatchCalculation>,
484    /// Total time taken for batch processing
485    pub total_duration: Duration,
486    /// Number of successful calculations
487    pub successful_count: usize,
488    /// Number of failed calculations
489    pub failed_count: usize,
490}
491
492/// Individual calculation result in a batch
493#[derive(Debug)]
494pub struct BatchCalculation {
495    /// Agent DID
496    pub agent_id: String,
497    /// Calculation result
498    pub result: Result<ReputationScore>,
499    /// Time taken for this calculation
500    pub duration: Duration,
501}