use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChangeDirection {
Improved,
Regressed,
Unchanged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChangeSeverity {
Critical,
Notable,
Minor,
Negligible,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricChange {
pub metric_name: String,
pub category: String,
pub baseline_value: f64,
pub current_value: f64,
pub absolute_change: f64,
pub percent_change: f64,
pub direction: ChangeDirection,
pub severity: ChangeSeverity,
pub higher_is_better: bool,
}
impl MetricChange {
pub fn new(
metric_name: impl Into<String>,
category: impl Into<String>,
baseline_value: f64,
current_value: f64,
higher_is_better: bool,
) -> Self {
let absolute_change = current_value - baseline_value;
let percent_change = if baseline_value.abs() > 1e-10 {
(absolute_change / baseline_value) * 100.0
} else if current_value.abs() > 1e-10 {
100.0 } else {
0.0 };
let direction = if absolute_change.abs() < 1e-6 {
ChangeDirection::Unchanged
} else if (absolute_change > 0.0) == higher_is_better {
ChangeDirection::Improved
} else {
ChangeDirection::Regressed
};
let severity = match percent_change.abs() {
x if x >= 20.0 => ChangeSeverity::Critical,
x if x >= 10.0 => ChangeSeverity::Notable,
x if x >= 2.0 => ChangeSeverity::Minor,
_ => ChangeSeverity::Negligible,
};
Self {
metric_name: metric_name.into(),
category: category.into(),
baseline_value,
current_value,
absolute_change,
percent_change,
direction,
severity,
higher_is_better,
}
}
pub fn is_regression(&self) -> bool {
self.direction == ChangeDirection::Regressed
}
pub fn is_improvement(&self) -> bool {
self.direction == ChangeDirection::Improved
}
pub fn is_significant(&self) -> bool {
matches!(
self.severity,
ChangeSeverity::Critical | ChangeSeverity::Notable
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComparisonResult {
pub metric_changes: Vec<MetricChange>,
pub improvements: usize,
pub regressions: usize,
pub unchanged: usize,
pub critical_regressions: usize,
pub summary: ComparisonSummary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComparisonSummary {
Improved,
Regressed,
Mixed,
Stable,
}
impl ComparisonResult {
pub fn from_changes(metric_changes: Vec<MetricChange>) -> Self {
let improvements = metric_changes.iter().filter(|c| c.is_improvement()).count();
let regressions = metric_changes.iter().filter(|c| c.is_regression()).count();
let unchanged = metric_changes.len() - improvements - regressions;
let critical_regressions = metric_changes
.iter()
.filter(|c| c.is_regression() && c.severity == ChangeSeverity::Critical)
.count();
let summary = if critical_regressions > 0 {
ComparisonSummary::Regressed
} else if regressions == 0 && improvements > 0 {
ComparisonSummary::Improved
} else if regressions > 0 && improvements > 0 {
ComparisonSummary::Mixed
} else {
ComparisonSummary::Stable
};
Self {
metric_changes,
improvements,
regressions,
unchanged,
critical_regressions,
summary,
}
}
pub fn get_regressions(&self) -> Vec<&MetricChange> {
self.metric_changes
.iter()
.filter(|c| c.is_regression())
.collect()
}
pub fn get_improvements(&self) -> Vec<&MetricChange> {
self.metric_changes
.iter()
.filter(|c| c.is_improvement())
.collect()
}
pub fn get_significant_changes(&self) -> Vec<&MetricChange> {
self.metric_changes
.iter()
.filter(|c| c.is_significant())
.collect()
}
pub fn get_by_category(&self, category: &str) -> Vec<&MetricChange> {
self.metric_changes
.iter()
.filter(|c| c.category == category)
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaselineComparison {
pub baseline_source: String,
pub baseline_timestamp: String,
pub comparison: ComparisonResult,
}
impl BaselineComparison {
pub fn new(
baseline_source: impl Into<String>,
baseline_timestamp: impl Into<String>,
comparison: ComparisonResult,
) -> Self {
Self {
baseline_source: baseline_source.into(),
baseline_timestamp: baseline_timestamp.into(),
comparison,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_metric_change_improvement() {
let change = MetricChange::new(
"completeness",
"quality",
0.90,
0.95,
true, );
assert!(change.is_improvement());
assert!(!change.is_regression());
assert_eq!(change.direction, ChangeDirection::Improved);
}
#[test]
fn test_metric_change_regression() {
let change = MetricChange::new(
"completeness",
"quality",
0.95,
0.90,
true, );
assert!(change.is_regression());
assert!(!change.is_improvement());
assert_eq!(change.direction, ChangeDirection::Regressed);
}
#[test]
fn test_metric_change_lower_is_better() {
let change = MetricChange::new(
"duplicate_rate",
"quality",
0.05,
0.02,
false, );
assert!(change.is_improvement());
assert_eq!(change.direction, ChangeDirection::Improved);
}
#[test]
fn test_comparison_result() {
let changes = vec![
MetricChange::new("metric1", "cat1", 0.80, 0.90, true),
MetricChange::new("metric2", "cat1", 0.90, 0.85, true),
MetricChange::new("metric3", "cat2", 0.95, 0.95, true),
];
let result = ComparisonResult::from_changes(changes);
assert_eq!(result.improvements, 1);
assert_eq!(result.regressions, 1);
assert_eq!(result.unchanged, 1);
assert_eq!(result.summary, ComparisonSummary::Mixed);
}
#[test]
fn test_critical_severity() {
let change = MetricChange::new("metric", "category", 0.50, 0.70, true);
assert_eq!(change.severity, ChangeSeverity::Critical);
assert!(change.percent_change >= 20.0);
}
}