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) => map.len() + map.values().map(Self::count_fields).sum::<usize>(),
77            Value::Array(arr) => arr.iter().map(Self::count_fields).sum(),
78            _ => 1,
79        }
80    }
81}
82
83/// Sample comparator for comparing mock and real sample responses
84pub struct SampleComparator;
85
86impl SampleComparator {
87    /// Compare sample responses and compute similarity score
88    ///
89    /// # Arguments
90    /// * `mock_samples` - Vector of mock sample responses
91    /// * `real_samples` - Vector of real sample responses
92    ///
93    /// # Returns
94    /// Similarity score (0.0 to 1.0)
95    pub fn compare(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
96        if mock_samples.is_empty() || real_samples.is_empty() {
97            return 0.0;
98        }
99
100        // Compare structure similarity
101        let structure_score = self.compare_structures(mock_samples, real_samples);
102
103        // Compare value distributions (simplified)
104        let distribution_score = self.compare_distributions(mock_samples, real_samples);
105
106        // Combine scores
107        (structure_score * 0.6 + distribution_score * 0.4).max(0.0).min(1.0)
108    }
109
110    /// Compare response structures
111    fn compare_structures(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
112        // Get structure from first sample of each
113        let mock_structure = Self::extract_structure(mock_samples.first().unwrap());
114        let real_structure = Self::extract_structure(real_samples.first().unwrap());
115
116        // Compare structures
117        let mock_fields: std::collections::HashSet<String> =
118            mock_structure.keys().cloned().collect();
119        let real_fields: std::collections::HashSet<String> =
120            real_structure.keys().cloned().collect();
121
122        let intersection = mock_fields.intersection(&real_fields).count();
123        let union = mock_fields.union(&real_fields).count();
124
125        if union == 0 {
126            return 1.0;
127        }
128
129        intersection as f64 / union as f64
130    }
131
132    /// Extract structure from a JSON value
133    fn extract_structure(value: &Value) -> HashMap<String, String> {
134        let mut structure = HashMap::new();
135        Self::extract_structure_recursive(value, "", &mut structure);
136        structure
137    }
138
139    /// Recursive helper for structure extraction
140    fn extract_structure_recursive(
141        value: &Value,
142        prefix: &str,
143        structure: &mut HashMap<String, String>,
144    ) {
145        match value {
146            Value::Object(map) => {
147                for (key, val) in map {
148                    let path = if prefix.is_empty() {
149                        key.clone()
150                    } else {
151                        format!("{}.{}", prefix, key)
152                    };
153                    structure.insert(path.clone(), Self::type_of(val));
154                    Self::extract_structure_recursive(val, &path, structure);
155                }
156            }
157            Value::Array(arr) => {
158                if let Some(first) = arr.first() {
159                    Self::extract_structure_recursive(first, prefix, structure);
160                }
161            }
162            _ => {
163                if !prefix.is_empty() {
164                    structure.insert(prefix.to_string(), Self::type_of(value));
165                }
166            }
167        }
168    }
169
170    /// Get type string for a value
171    fn type_of(value: &Value) -> String {
172        match value {
173            Value::Null => "null".to_string(),
174            Value::Bool(_) => "bool".to_string(),
175            Value::Number(n) => {
176                if n.is_i64() {
177                    "integer".to_string()
178                } else {
179                    "number".to_string()
180                }
181            }
182            Value::String(_) => "string".to_string(),
183            Value::Array(_) => "array".to_string(),
184            Value::Object(_) => "object".to_string(),
185        }
186    }
187
188    /// Compare value distributions (simplified)
189    fn compare_distributions(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
190        // Simplified distribution comparison
191        // In a real implementation, this would compare statistical distributions
192        // For now, just check if value types match
193        let mock_types: std::collections::HashSet<String> =
194            mock_samples.iter().map(Self::type_of).collect();
195        let real_types: std::collections::HashSet<String> =
196            real_samples.iter().map(Self::type_of).collect();
197
198        let intersection = mock_types.intersection(&real_types).count();
199        let union = mock_types.union(&real_types).count();
200
201        if union == 0 {
202            return 1.0;
203        }
204
205        intersection as f64 / union as f64
206    }
207}
208
209/// Fidelity calculator
210pub struct FidelityCalculator {
211    schema_comparator: SchemaComparator,
212    sample_comparator: SampleComparator,
213}
214
215impl FidelityCalculator {
216    /// Create a new fidelity calculator
217    pub fn new() -> Self {
218        Self {
219            schema_comparator: SchemaComparator,
220            sample_comparator: SampleComparator,
221        }
222    }
223
224    /// Calculate fidelity score for a workspace
225    ///
226    /// # Arguments
227    /// * `mock_schema` - Mock/expected schema
228    /// * `real_schema` - Real/actual schema
229    /// * `mock_samples` - Mock sample responses
230    /// * `real_samples` - Real sample responses
231    /// * `mock_response_times` - Mock response times (optional)
232    /// * `real_response_times` - Real response times (optional)
233    /// * `mock_error_patterns` - Mock error patterns (optional)
234    /// * `real_error_patterns` - Real error patterns (optional)
235    ///
236    /// # Returns
237    /// Fidelity score
238    pub fn calculate(
239        &self,
240        mock_schema: &Value,
241        real_schema: &Value,
242        mock_samples: &[Value],
243        real_samples: &[Value],
244        mock_response_times: Option<&[u64]>,
245        real_response_times: Option<&[u64]>,
246        mock_error_patterns: Option<&HashMap<String, usize>>,
247        real_error_patterns: Option<&HashMap<String, usize>>,
248    ) -> FidelityScore {
249        // Calculate schema similarity (40% weight)
250        let schema_similarity = self.schema_comparator.compare(mock_schema, real_schema);
251
252        // Calculate sample similarity (40% weight)
253        let sample_similarity = self.sample_comparator.compare(mock_samples, real_samples);
254
255        // Calculate response time similarity (10% weight)
256        let response_time_similarity = self.compare_response_times(
257            mock_response_times.unwrap_or(&[]),
258            real_response_times.unwrap_or(&[]),
259        );
260
261        // Calculate error pattern similarity (10% weight)
262        let error_pattern_similarity =
263            self.compare_error_patterns(mock_error_patterns, real_error_patterns);
264
265        // Calculate overall score with weights
266        let overall = (schema_similarity * 0.4
267            + sample_similarity * 0.4
268            + response_time_similarity * 0.1
269            + error_pattern_similarity * 0.1)
270            .max(0.0)
271            .min(1.0);
272
273        FidelityScore {
274            overall,
275            schema_similarity,
276            sample_similarity,
277            response_time_similarity,
278            error_pattern_similarity,
279            computed_at: Utc::now(),
280            metadata: HashMap::new(),
281        }
282    }
283
284    /// Compare response times
285    fn compare_response_times(&self, mock_times: &[u64], real_times: &[u64]) -> f64 {
286        if mock_times.is_empty() || real_times.is_empty() {
287            return 0.5; // Neutral score if no data
288        }
289
290        let mock_avg = mock_times.iter().sum::<u64>() as f64 / mock_times.len() as f64;
291        let real_avg = real_times.iter().sum::<u64>() as f64 / real_times.len() as f64;
292
293        if real_avg == 0.0 {
294            return if mock_avg == 0.0 { 1.0 } else { 0.0 };
295        }
296
297        // Calculate similarity based on ratio
298        let ratio = mock_avg / real_avg;
299        // Score is highest when ratio is close to 1.0
300        (1.0 - (ratio - 1.0).abs()).max(0.0).min(1.0)
301    }
302
303    /// Compare error patterns
304    fn compare_error_patterns(
305        &self,
306        mock_patterns: Option<&HashMap<String, usize>>,
307        real_patterns: Option<&HashMap<String, usize>>,
308    ) -> f64 {
309        match (mock_patterns, real_patterns) {
310            (Some(mock), Some(real)) => {
311                if mock.is_empty() && real.is_empty() {
312                    return 1.0;
313                }
314
315                let mock_keys: std::collections::HashSet<&String> = mock.keys().collect();
316                let real_keys: std::collections::HashSet<&String> = real.keys().collect();
317
318                let intersection = mock_keys.intersection(&real_keys).count();
319                let union = mock_keys.union(&real_keys).count();
320
321                if union == 0 {
322                    return 1.0;
323                }
324
325                intersection as f64 / union as f64
326            }
327            _ => 0.5, // Neutral score if no data
328        }
329    }
330}
331
332impl Default for FidelityCalculator {
333    fn default() -> Self {
334        Self::new()
335    }
336}