mockforge_core/
fidelity.rs

1//! Fidelity Score Calculator
2//!
3//! Computes a fidelity score that quantifies how close a workspace is to its real upstream
4//! based on schema and sample comparisons.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11/// Fidelity score for a workspace
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FidelityScore {
14    /// Overall fidelity score (0.0 to 1.0, where 1.0 = perfect match)
15    pub overall: f64,
16    /// Schema similarity score (0.0 to 1.0)
17    pub schema_similarity: f64,
18    /// Sample similarity score (0.0 to 1.0)
19    pub sample_similarity: f64,
20    /// Response time similarity score (0.0 to 1.0)
21    pub response_time_similarity: f64,
22    /// Error pattern similarity score (0.0 to 1.0)
23    pub error_pattern_similarity: f64,
24    /// When the score was computed
25    pub computed_at: DateTime<Utc>,
26    /// Additional metadata
27    #[serde(default)]
28    pub metadata: HashMap<String, Value>,
29}
30
31/// Schema comparator for comparing mock and real schemas
32pub struct SchemaComparator;
33
34impl SchemaComparator {
35    /// Compare two schemas and compute similarity score
36    ///
37    /// # Arguments
38    /// * `mock_schema` - Mock/expected schema
39    /// * `real_schema` - Real/actual schema
40    ///
41    /// # Returns
42    /// Similarity score (0.0 to 1.0)
43    pub fn compare(&self, mock_schema: &Value, real_schema: &Value) -> f64 {
44        // Use existing schema diff functionality
45        let errors = crate::schema_diff::diff(mock_schema, real_schema);
46
47        if errors.is_empty() {
48            return 1.0;
49        }
50
51        // Calculate field coverage
52        let mock_fields = Self::count_fields(mock_schema);
53        let real_fields = Self::count_fields(real_schema);
54        let total_fields = mock_fields.max(real_fields);
55
56        if total_fields == 0 {
57            return 1.0;
58        }
59
60        // Score based on error count and field coverage
61        let error_penalty = errors.len() as f64 / total_fields as f64;
62        let coverage_score = if mock_fields > 0 && real_fields > 0 {
63            let common_fields = total_fields - errors.len();
64            common_fields as f64 / total_fields as f64
65        } else {
66            0.0
67        };
68
69        // Combine scores
70        (coverage_score * 0.7 + (1.0 - error_penalty.min(1.0)) * 0.3).max(0.0).min(1.0)
71    }
72
73    /// Count the number of fields in a schema
74    fn count_fields(schema: &Value) -> usize {
75        match schema {
76            Value::Object(map) => {
77                map.len() + map.values().map(|v| Self::count_fields(v)).sum::<usize>()
78            }
79            Value::Array(arr) => arr.iter().map(|v| Self::count_fields(v)).sum(),
80            _ => 1,
81        }
82    }
83}
84
85/// Sample comparator for comparing mock and real sample responses
86pub struct SampleComparator;
87
88impl SampleComparator {
89    /// Compare sample responses and compute similarity score
90    ///
91    /// # Arguments
92    /// * `mock_samples` - Vector of mock sample responses
93    /// * `real_samples` - Vector of real sample responses
94    ///
95    /// # Returns
96    /// Similarity score (0.0 to 1.0)
97    pub fn compare(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
98        if mock_samples.is_empty() || real_samples.is_empty() {
99            return 0.0;
100        }
101
102        // Compare structure similarity
103        let structure_score = self.compare_structures(mock_samples, real_samples);
104
105        // Compare value distributions (simplified)
106        let distribution_score = self.compare_distributions(mock_samples, real_samples);
107
108        // Combine scores
109        (structure_score * 0.6 + distribution_score * 0.4).max(0.0).min(1.0)
110    }
111
112    /// Compare response structures
113    fn compare_structures(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
114        // Get structure from first sample of each
115        let mock_structure = Self::extract_structure(mock_samples.first().unwrap());
116        let real_structure = Self::extract_structure(real_samples.first().unwrap());
117
118        // Compare structures
119        let mock_fields: std::collections::HashSet<String> =
120            mock_structure.keys().cloned().collect();
121        let real_fields: std::collections::HashSet<String> =
122            real_structure.keys().cloned().collect();
123
124        let intersection = mock_fields.intersection(&real_fields).count();
125        let union = mock_fields.union(&real_fields).count();
126
127        if union == 0 {
128            return 1.0;
129        }
130
131        intersection as f64 / union as f64
132    }
133
134    /// Extract structure from a JSON value
135    fn extract_structure(value: &Value) -> HashMap<String, String> {
136        let mut structure = HashMap::new();
137        Self::extract_structure_recursive(value, "", &mut structure);
138        structure
139    }
140
141    /// Recursive helper for structure extraction
142    fn extract_structure_recursive(
143        value: &Value,
144        prefix: &str,
145        structure: &mut HashMap<String, String>,
146    ) {
147        match value {
148            Value::Object(map) => {
149                for (key, val) in map {
150                    let path = if prefix.is_empty() {
151                        key.clone()
152                    } else {
153                        format!("{}.{}", prefix, key)
154                    };
155                    structure.insert(path.clone(), Self::type_of(val));
156                    Self::extract_structure_recursive(val, &path, structure);
157                }
158            }
159            Value::Array(arr) => {
160                if let Some(first) = arr.first() {
161                    Self::extract_structure_recursive(first, prefix, structure);
162                }
163            }
164            _ => {
165                if !prefix.is_empty() {
166                    structure.insert(prefix.to_string(), Self::type_of(value));
167                }
168            }
169        }
170    }
171
172    /// Get type string for a value
173    fn type_of(value: &Value) -> String {
174        match value {
175            Value::Null => "null".to_string(),
176            Value::Bool(_) => "bool".to_string(),
177            Value::Number(n) => {
178                if n.is_i64() {
179                    "integer".to_string()
180                } else {
181                    "number".to_string()
182                }
183            }
184            Value::String(_) => "string".to_string(),
185            Value::Array(_) => "array".to_string(),
186            Value::Object(_) => "object".to_string(),
187        }
188    }
189
190    /// Compare value distributions (simplified)
191    fn compare_distributions(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
192        // Simplified distribution comparison
193        // In a real implementation, this would compare statistical distributions
194        // For now, just check if value types match
195        let mock_types: std::collections::HashSet<String> =
196            mock_samples.iter().map(|v| Self::type_of(v)).collect();
197        let real_types: std::collections::HashSet<String> =
198            real_samples.iter().map(|v| Self::type_of(v)).collect();
199
200        let intersection = mock_types.intersection(&real_types).count();
201        let union = mock_types.union(&real_types).count();
202
203        if union == 0 {
204            return 1.0;
205        }
206
207        intersection as f64 / union as f64
208    }
209}
210
211/// Fidelity calculator
212pub struct FidelityCalculator {
213    schema_comparator: SchemaComparator,
214    sample_comparator: SampleComparator,
215}
216
217impl FidelityCalculator {
218    /// Create a new fidelity calculator
219    pub fn new() -> Self {
220        Self {
221            schema_comparator: SchemaComparator,
222            sample_comparator: SampleComparator,
223        }
224    }
225
226    /// Calculate fidelity score for a workspace
227    ///
228    /// # Arguments
229    /// * `mock_schema` - Mock/expected schema
230    /// * `real_schema` - Real/actual schema
231    /// * `mock_samples` - Mock sample responses
232    /// * `real_samples` - Real sample responses
233    /// * `mock_response_times` - Mock response times (optional)
234    /// * `real_response_times` - Real response times (optional)
235    /// * `mock_error_patterns` - Mock error patterns (optional)
236    /// * `real_error_patterns` - Real error patterns (optional)
237    ///
238    /// # Returns
239    /// Fidelity score
240    pub fn calculate(
241        &self,
242        mock_schema: &Value,
243        real_schema: &Value,
244        mock_samples: &[Value],
245        real_samples: &[Value],
246        mock_response_times: Option<&[u64]>,
247        real_response_times: Option<&[u64]>,
248        mock_error_patterns: Option<&HashMap<String, usize>>,
249        real_error_patterns: Option<&HashMap<String, usize>>,
250    ) -> FidelityScore {
251        // Calculate schema similarity (40% weight)
252        let schema_similarity = self.schema_comparator.compare(mock_schema, real_schema);
253
254        // Calculate sample similarity (40% weight)
255        let sample_similarity = self.sample_comparator.compare(mock_samples, real_samples);
256
257        // Calculate response time similarity (10% weight)
258        let response_time_similarity = self.compare_response_times(
259            mock_response_times.unwrap_or(&[]),
260            real_response_times.unwrap_or(&[]),
261        );
262
263        // Calculate error pattern similarity (10% weight)
264        let error_pattern_similarity =
265            self.compare_error_patterns(mock_error_patterns, real_error_patterns);
266
267        // Calculate overall score with weights
268        let overall = (schema_similarity * 0.4
269            + sample_similarity * 0.4
270            + response_time_similarity * 0.1
271            + error_pattern_similarity * 0.1)
272            .max(0.0)
273            .min(1.0);
274
275        FidelityScore {
276            overall,
277            schema_similarity,
278            sample_similarity,
279            response_time_similarity,
280            error_pattern_similarity,
281            computed_at: Utc::now(),
282            metadata: HashMap::new(),
283        }
284    }
285
286    /// Compare response times
287    fn compare_response_times(&self, mock_times: &[u64], real_times: &[u64]) -> f64 {
288        if mock_times.is_empty() || real_times.is_empty() {
289            return 0.5; // Neutral score if no data
290        }
291
292        let mock_avg = mock_times.iter().sum::<u64>() as f64 / mock_times.len() as f64;
293        let real_avg = real_times.iter().sum::<u64>() as f64 / real_times.len() as f64;
294
295        if real_avg == 0.0 {
296            return if mock_avg == 0.0 { 1.0 } else { 0.0 };
297        }
298
299        // Calculate similarity based on ratio
300        let ratio = mock_avg / real_avg;
301        // Score is highest when ratio is close to 1.0
302        (1.0 - (ratio - 1.0).abs()).max(0.0).min(1.0)
303    }
304
305    /// Compare error patterns
306    fn compare_error_patterns(
307        &self,
308        mock_patterns: Option<&HashMap<String, usize>>,
309        real_patterns: Option<&HashMap<String, usize>>,
310    ) -> f64 {
311        match (mock_patterns, real_patterns) {
312            (Some(mock), Some(real)) => {
313                if mock.is_empty() && real.is_empty() {
314                    return 1.0;
315                }
316
317                let mock_keys: std::collections::HashSet<&String> = mock.keys().collect();
318                let real_keys: std::collections::HashSet<&String> = real.keys().collect();
319
320                let intersection = mock_keys.intersection(&real_keys).count();
321                let union = mock_keys.union(&real_keys).count();
322
323                if union == 0 {
324                    return 1.0;
325                }
326
327                intersection as f64 / union as f64
328            }
329            _ => 0.5, // Neutral score if no data
330        }
331    }
332}
333
334impl Default for FidelityCalculator {
335    fn default() -> Self {
336        Self::new()
337    }
338}