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}