1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ChangeDirection {
12 Improved,
14 Regressed,
16 Unchanged,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum ChangeSeverity {
23 Critical,
25 Notable,
27 Minor,
29 Negligible,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct MetricChange {
36 pub metric_name: String,
38 pub category: String,
40 pub baseline_value: f64,
42 pub current_value: f64,
44 pub absolute_change: f64,
46 pub percent_change: f64,
48 pub direction: ChangeDirection,
50 pub severity: ChangeSeverity,
52 pub higher_is_better: bool,
54}
55
56impl MetricChange {
57 pub fn new(
59 metric_name: impl Into<String>,
60 category: impl Into<String>,
61 baseline_value: f64,
62 current_value: f64,
63 higher_is_better: bool,
64 ) -> Self {
65 let absolute_change = current_value - baseline_value;
66 let percent_change = if baseline_value.abs() > 1e-10 {
67 (absolute_change / baseline_value) * 100.0
68 } else if current_value.abs() > 1e-10 {
69 100.0 } else {
71 0.0 };
73
74 let direction = if absolute_change.abs() < 1e-6 {
76 ChangeDirection::Unchanged
77 } else if (absolute_change > 0.0) == higher_is_better {
78 ChangeDirection::Improved
79 } else {
80 ChangeDirection::Regressed
81 };
82
83 let severity = match percent_change.abs() {
85 x if x >= 20.0 => ChangeSeverity::Critical,
86 x if x >= 10.0 => ChangeSeverity::Notable,
87 x if x >= 2.0 => ChangeSeverity::Minor,
88 _ => ChangeSeverity::Negligible,
89 };
90
91 Self {
92 metric_name: metric_name.into(),
93 category: category.into(),
94 baseline_value,
95 current_value,
96 absolute_change,
97 percent_change,
98 direction,
99 severity,
100 higher_is_better,
101 }
102 }
103
104 pub fn is_regression(&self) -> bool {
106 self.direction == ChangeDirection::Regressed
107 }
108
109 pub fn is_improvement(&self) -> bool {
111 self.direction == ChangeDirection::Improved
112 }
113
114 pub fn is_significant(&self) -> bool {
116 matches!(
117 self.severity,
118 ChangeSeverity::Critical | ChangeSeverity::Notable
119 )
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ComparisonResult {
126 pub metric_changes: Vec<MetricChange>,
128 pub improvements: usize,
130 pub regressions: usize,
132 pub unchanged: usize,
134 pub critical_regressions: usize,
136 pub summary: ComparisonSummary,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142pub enum ComparisonSummary {
143 Improved,
145 Regressed,
147 Mixed,
149 Stable,
151}
152
153impl ComparisonResult {
154 pub fn from_changes(metric_changes: Vec<MetricChange>) -> Self {
156 let improvements = metric_changes.iter().filter(|c| c.is_improvement()).count();
157 let regressions = metric_changes.iter().filter(|c| c.is_regression()).count();
158 let unchanged = metric_changes.len() - improvements - regressions;
159 let critical_regressions = metric_changes
160 .iter()
161 .filter(|c| c.is_regression() && c.severity == ChangeSeverity::Critical)
162 .count();
163
164 let summary = if critical_regressions > 0 {
165 ComparisonSummary::Regressed
166 } else if regressions == 0 && improvements > 0 {
167 ComparisonSummary::Improved
168 } else if regressions > 0 && improvements > 0 {
169 ComparisonSummary::Mixed
170 } else {
171 ComparisonSummary::Stable
172 };
173
174 Self {
175 metric_changes,
176 improvements,
177 regressions,
178 unchanged,
179 critical_regressions,
180 summary,
181 }
182 }
183
184 pub fn get_regressions(&self) -> Vec<&MetricChange> {
186 self.metric_changes
187 .iter()
188 .filter(|c| c.is_regression())
189 .collect()
190 }
191
192 pub fn get_improvements(&self) -> Vec<&MetricChange> {
194 self.metric_changes
195 .iter()
196 .filter(|c| c.is_improvement())
197 .collect()
198 }
199
200 pub fn get_significant_changes(&self) -> Vec<&MetricChange> {
202 self.metric_changes
203 .iter()
204 .filter(|c| c.is_significant())
205 .collect()
206 }
207
208 pub fn get_by_category(&self, category: &str) -> Vec<&MetricChange> {
210 self.metric_changes
211 .iter()
212 .filter(|c| c.category == category)
213 .collect()
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct BaselineComparison {
220 pub baseline_source: String,
222 pub baseline_timestamp: String,
224 pub comparison: ComparisonResult,
226}
227
228impl BaselineComparison {
229 pub fn new(
231 baseline_source: impl Into<String>,
232 baseline_timestamp: impl Into<String>,
233 comparison: ComparisonResult,
234 ) -> Self {
235 Self {
236 baseline_source: baseline_source.into(),
237 baseline_timestamp: baseline_timestamp.into(),
238 comparison,
239 }
240 }
241}
242
243#[allow(dead_code)] pub struct BaselineComparer {
246 metric_definitions: HashMap<String, MetricDefinition>,
248 significance_threshold: f64,
250}
251
252#[allow(dead_code)]
253#[derive(Clone)]
254struct MetricDefinition {
255 category: String,
256 higher_is_better: bool,
257}
258
259#[allow(dead_code)]
260impl BaselineComparer {
261 pub fn new() -> Self {
263 let mut definitions = HashMap::new();
264
265 definitions.insert(
267 "benford_p_value".to_string(),
268 MetricDefinition {
269 category: "statistical".to_string(),
270 higher_is_better: true,
271 },
272 );
273 definitions.insert(
274 "benford_mad".to_string(),
275 MetricDefinition {
276 category: "statistical".to_string(),
277 higher_is_better: false, },
279 );
280 definitions.insert(
281 "amount_ks_p_value".to_string(),
282 MetricDefinition {
283 category: "statistical".to_string(),
284 higher_is_better: true,
285 },
286 );
287 definitions.insert(
288 "temporal_correlation".to_string(),
289 MetricDefinition {
290 category: "statistical".to_string(),
291 higher_is_better: true,
292 },
293 );
294
295 definitions.insert(
297 "balance_sheet_balanced".to_string(),
298 MetricDefinition {
299 category: "coherence".to_string(),
300 higher_is_better: true,
301 },
302 );
303 definitions.insert(
304 "subledger_reconciliation".to_string(),
305 MetricDefinition {
306 category: "coherence".to_string(),
307 higher_is_better: true,
308 },
309 );
310 definitions.insert(
311 "document_chain_completion".to_string(),
312 MetricDefinition {
313 category: "coherence".to_string(),
314 higher_is_better: true,
315 },
316 );
317 definitions.insert(
318 "ic_match_rate".to_string(),
319 MetricDefinition {
320 category: "coherence".to_string(),
321 higher_is_better: true,
322 },
323 );
324
325 definitions.insert(
327 "duplicate_rate".to_string(),
328 MetricDefinition {
329 category: "quality".to_string(),
330 higher_is_better: false, },
332 );
333 definitions.insert(
334 "completeness".to_string(),
335 MetricDefinition {
336 category: "quality".to_string(),
337 higher_is_better: true,
338 },
339 );
340 definitions.insert(
341 "format_consistency".to_string(),
342 MetricDefinition {
343 category: "quality".to_string(),
344 higher_is_better: true,
345 },
346 );
347
348 definitions.insert(
350 "anomaly_rate".to_string(),
351 MetricDefinition {
352 category: "ml".to_string(),
353 higher_is_better: true, },
355 );
356 definitions.insert(
357 "label_coverage".to_string(),
358 MetricDefinition {
359 category: "ml".to_string(),
360 higher_is_better: true,
361 },
362 );
363 definitions.insert(
364 "graph_connectivity".to_string(),
365 MetricDefinition {
366 category: "ml".to_string(),
367 higher_is_better: true,
368 },
369 );
370
371 Self {
372 metric_definitions: definitions,
373 significance_threshold: 2.0, }
375 }
376
377 pub fn with_significance_threshold(mut self, threshold: f64) -> Self {
379 self.significance_threshold = threshold;
380 self
381 }
382
383 pub fn add_metric(
385 &mut self,
386 name: impl Into<String>,
387 category: impl Into<String>,
388 higher_is_better: bool,
389 ) {
390 self.metric_definitions.insert(
391 name.into(),
392 MetricDefinition {
393 category: category.into(),
394 higher_is_better,
395 },
396 );
397 }
398
399 pub fn compare(
401 &self,
402 baseline: &HashMap<String, f64>,
403 current: &HashMap<String, f64>,
404 ) -> ComparisonResult {
405 let mut changes = Vec::new();
406
407 for (metric_name, ¤t_value) in current {
408 if let Some(&baseline_value) = baseline.get(metric_name) {
409 let (category, higher_is_better) = self
410 .metric_definitions
411 .get(metric_name)
412 .map(|d| (d.category.clone(), d.higher_is_better))
413 .unwrap_or(("unknown".to_string(), true));
414
415 changes.push(MetricChange::new(
416 metric_name.clone(),
417 category,
418 baseline_value,
419 current_value,
420 higher_is_better,
421 ));
422 }
423 }
424
425 ComparisonResult::from_changes(changes)
426 }
427
428 pub fn create_comparison(
430 &self,
431 baseline_source: impl Into<String>,
432 baseline_timestamp: impl Into<String>,
433 baseline_metrics: &HashMap<String, f64>,
434 current_metrics: &HashMap<String, f64>,
435 ) -> BaselineComparison {
436 let comparison = self.compare(baseline_metrics, current_metrics);
437 BaselineComparison::new(baseline_source, baseline_timestamp, comparison)
438 }
439}
440
441impl Default for BaselineComparer {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn test_metric_change_improvement() {
453 let change = MetricChange::new(
454 "completeness",
455 "quality",
456 0.90,
457 0.95,
458 true, );
460
461 assert!(change.is_improvement());
462 assert!(!change.is_regression());
463 assert_eq!(change.direction, ChangeDirection::Improved);
464 }
465
466 #[test]
467 fn test_metric_change_regression() {
468 let change = MetricChange::new(
469 "completeness",
470 "quality",
471 0.95,
472 0.90,
473 true, );
475
476 assert!(change.is_regression());
477 assert!(!change.is_improvement());
478 assert_eq!(change.direction, ChangeDirection::Regressed);
479 }
480
481 #[test]
482 fn test_metric_change_lower_is_better() {
483 let change = MetricChange::new(
484 "duplicate_rate",
485 "quality",
486 0.05,
487 0.02,
488 false, );
490
491 assert!(change.is_improvement());
492 assert_eq!(change.direction, ChangeDirection::Improved);
493 }
494
495 #[test]
496 fn test_comparison_result() {
497 let changes = vec![
498 MetricChange::new("metric1", "cat1", 0.80, 0.90, true),
499 MetricChange::new("metric2", "cat1", 0.90, 0.85, true),
500 MetricChange::new("metric3", "cat2", 0.95, 0.95, true),
501 ];
502
503 let result = ComparisonResult::from_changes(changes);
504
505 assert_eq!(result.improvements, 1);
506 assert_eq!(result.regressions, 1);
507 assert_eq!(result.unchanged, 1);
508 assert_eq!(result.summary, ComparisonSummary::Mixed);
509 }
510
511 #[test]
512 fn test_baseline_comparer() {
513 let comparer = BaselineComparer::new();
514
515 let mut baseline = HashMap::new();
516 baseline.insert("completeness".to_string(), 0.90);
517 baseline.insert("duplicate_rate".to_string(), 0.05);
518
519 let mut current = HashMap::new();
520 current.insert("completeness".to_string(), 0.95);
521 current.insert("duplicate_rate".to_string(), 0.03);
522
523 let result = comparer.compare(&baseline, ¤t);
524
525 assert_eq!(result.improvements, 2);
526 assert_eq!(result.regressions, 0);
527 }
528
529 #[test]
530 fn test_critical_severity() {
531 let change = MetricChange::new("metric", "category", 0.50, 0.70, true);
532
533 assert_eq!(change.severity, ChangeSeverity::Critical);
534 assert!(change.percent_change >= 20.0);
535 }
536}