use crate::quality::{CodeQualityMetrics, QualityViolation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricCorrelation {
pub metric_a: String,
pub metric_b: String,
pub pearson_coefficient: f64,
pub sample_count: usize,
pub standard_error: f64,
}
impl MetricCorrelation {
pub fn is_significant(&self) -> bool {
self.pearson_coefficient.abs() > 0.6
}
pub fn strength(&self) -> &str {
let abs_r = self.pearson_coefficient.abs();
match abs_r {
r if r > 0.9 => "Very Strong",
r if r > 0.7 => "Strong",
r if r > 0.5 => "Moderate",
r if r > 0.3 => "Weak",
_ => "Very Weak",
}
}
pub fn direction(&self) -> &str {
if self.pearson_coefficient > 0.0 {
"Positive"
} else if self.pearson_coefficient < 0.0 {
"Negative"
} else {
"None"
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimultaneousViolation {
pub timestamps: Vec<u64>,
pub metric_names: Vec<String>,
pub severities: Vec<String>,
pub time_window_secs: u64,
pub violation_count: usize,
}
impl SimultaneousViolation {
pub fn new(
metric_names: Vec<String>,
severities: Vec<String>,
timestamps: Vec<u64>,
time_window_secs: u64,
) -> Self {
let violation_count = metric_names.len();
Self {
timestamps,
metric_names,
severities,
time_window_secs,
violation_count,
}
}
pub fn max_severity(&self) -> &str {
self.severities
.iter()
.max_by_key(|s| severity_rank(s))
.map(|s| s.as_str())
.unwrap_or("UNKNOWN")
}
pub fn is_compound(&self) -> bool {
self.violation_count >= 3
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootCauseHypothesis {
pub causal_metric: String,
pub affected_metric: String,
pub correlation: f64,
pub confidence: f64,
pub lag_seconds: u64,
pub evidence: String,
}
impl RootCauseHypothesis {
pub fn is_actionable(&self) -> bool {
self.confidence > 0.7
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorrelationAnalysis {
pub metric_correlations: Vec<MetricCorrelation>,
pub simultaneous_violations: Vec<SimultaneousViolation>,
pub root_causes: Vec<RootCauseHypothesis>,
pub amplified_severities: HashMap<String, f64>,
pub timestamp: u64,
}
fn pearson_correlation(a: &[f64], b: &[f64]) -> (f64, f64) {
if a.len() < 2 || a.len() != b.len() {
return (0.0, 0.0);
}
let n = a.len() as f64;
let mean_a = a.iter().sum::<f64>() / n;
let mean_b = b.iter().sum::<f64>() / n;
let mut sum_ab = 0.0;
let mut sum_aa = 0.0;
let mut sum_bb = 0.0;
for i in 0..a.len() {
let da = a[i] - mean_a;
let db = b[i] - mean_b;
sum_ab += da * db;
sum_aa += da * da;
sum_bb += db * db;
}
let denominator = (sum_aa * sum_bb).sqrt();
let r = if denominator > 0.0 {
sum_ab / denominator
} else {
0.0
};
let se = if n > 2.0 {
((1.0 - r * r) / (n - 2.0)).sqrt()
} else {
0.0
};
(r, se)
}
pub fn compute_metric_correlations(history: &[CodeQualityMetrics]) -> Vec<MetricCorrelation> {
if history.len() < 2 {
return Vec::new();
}
let stub_ratios: Vec<f64> = history.iter().map(|m| m.stub_ratio).collect();
let type_coverages: Vec<f64> = history.iter().map(|m| m.type_coverage).collect();
let churns: Vec<f64> = history.iter().map(|m| m.churn as f64).collect();
let complexities: Vec<f64> = history.iter().map(|m| m.cyclomatic_complexity).collect();
let clippy_warnings: Vec<f64> = history.iter().map(|m| m.clippy_warnings as f64).collect();
let test_coverage: Vec<f64> = history.iter().map(|m| m.test_coverage).collect();
let doc_coverage: Vec<f64> = history.iter().map(|m| m.doc_coverage).collect();
let metrics = vec![
("stub_ratio", stub_ratios),
("type_coverage", type_coverages),
("churn", churns),
("cyclomatic_complexity", complexities),
("clippy_warnings", clippy_warnings),
("test_coverage", test_coverage),
("doc_coverage", doc_coverage),
];
let mut correlations = Vec::new();
for i in 0..metrics.len() {
for j in (i + 1)..metrics.len() {
let (metric_name_a, values_a) = &metrics[i];
let (metric_name_b, values_b) = &metrics[j];
let (r, se) = pearson_correlation(values_a, values_b);
correlations.push(MetricCorrelation {
metric_a: metric_name_a.to_string(),
metric_b: metric_name_b.to_string(),
pearson_coefficient: r,
sample_count: history.len(),
standard_error: se,
});
}
}
correlations
}
pub fn detect_simultaneous_violations(
violations: &[QualityViolation],
) -> Vec<SimultaneousViolation> {
if violations.len() < 2 {
return Vec::new();
}
let time_window_secs = 300; let mut simultaneous = Vec::new();
let mut processed = vec![false; violations.len()];
for i in 0..violations.len() {
if processed[i] {
continue;
}
let mut group_metrics = vec![violations[i].metric().to_string()];
let mut group_severities = vec![violations[i].severity().to_string()];
let mut group_timestamps = vec![get_violation_timestamp(&violations[i])];
for j in (i + 1)..violations.len() {
if processed[j] {
continue;
}
let time_diff = (get_violation_timestamp(&violations[j]) as i64
- get_violation_timestamp(&violations[i]) as i64)
.abs() as u64;
if time_diff <= time_window_secs {
group_metrics.push(violations[j].metric().to_string());
group_severities.push(violations[j].severity().to_string());
group_timestamps.push(get_violation_timestamp(&violations[j]));
processed[j] = true;
}
}
if group_metrics.len() >= 2 {
simultaneous.push(SimultaneousViolation::new(
group_metrics,
group_severities,
group_timestamps,
time_window_secs,
));
}
processed[i] = true;
}
simultaneous
}
fn get_violation_timestamp(violation: &QualityViolation) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
violation.metric().hash(&mut hasher);
(hasher.finish() % 1000000000) as u64
}
pub fn infer_root_cause(
metrics: &[CodeQualityMetrics],
violation: &QualityViolation,
) -> RootCauseHypothesis {
let affected_metric = violation.metric().to_string();
let affected_values: Vec<f64> = match affected_metric.as_str() {
"stub_ratio" => metrics.iter().map(|m| m.stub_ratio).collect(),
"type_coverage" => metrics.iter().map(|m| m.type_coverage).collect(),
"churn" => metrics.iter().map(|m| m.churn as f64).collect(),
"cyclomatic_complexity" => metrics.iter().map(|m| m.cyclomatic_complexity).collect(),
"clippy_warnings" => metrics.iter().map(|m| m.clippy_warnings as f64).collect(),
"test_coverage" => metrics.iter().map(|m| m.test_coverage).collect(),
"doc_coverage" => metrics.iter().map(|m| m.doc_coverage).collect(),
_ => return default_hypothesis(&affected_metric),
};
let candidates = vec![
(
"stub_ratio",
metrics.iter().map(|m| m.stub_ratio).collect::<Vec<_>>(),
),
(
"type_coverage",
metrics.iter().map(|m| m.type_coverage).collect::<Vec<_>>(),
),
(
"churn",
metrics.iter().map(|m| m.churn as f64).collect::<Vec<_>>(),
),
(
"cyclomatic_complexity",
metrics
.iter()
.map(|m| m.cyclomatic_complexity)
.collect::<Vec<_>>(),
),
];
let mut best_hypothesis = default_hypothesis(&affected_metric);
for (candidate_name, candidate_values) in candidates {
if candidate_name == affected_metric.as_str() {
continue;
}
let (r, _se) = pearson_correlation(&candidate_values, &affected_values);
let confidence = (r.abs() * 0.8) + 0.2;
if r.abs() > 0.7 && confidence > best_hypothesis.confidence {
best_hypothesis = RootCauseHypothesis {
causal_metric: candidate_name.to_string(),
affected_metric: affected_metric.clone(),
correlation: r,
confidence,
lag_seconds: estimate_lag(&candidate_values, &affected_values),
evidence: format!(
"{} shows {} correlation (r={:.2}) with {}",
candidate_name,
if r > 0.0 { "positive" } else { "negative" },
r,
affected_metric
),
};
}
}
best_hypothesis
}
fn default_hypothesis(affected_metric: &str) -> RootCauseHypothesis {
RootCauseHypothesis {
causal_metric: "unknown".to_string(),
affected_metric: affected_metric.to_string(),
correlation: 0.0,
confidence: 0.0,
lag_seconds: 0,
evidence: "Insufficient data to determine root cause".to_string(),
}
}
fn estimate_lag(causal: &[f64], affected: &[f64]) -> u64 {
if causal.len() < 2 || affected.len() < 2 {
return 0;
}
let mut max_correlation = 0.0;
let mut best_lag = 0;
for lag in 0..=std::cmp::min(5, causal.len() - 1) {
let shifted_causal: Vec<f64> = causal.iter().skip(lag).copied().collect();
let truncated_affected: Vec<f64> = affected
.iter()
.take(affected.len() - lag)
.copied()
.collect();
let (r, _se) = pearson_correlation(&shifted_causal, &truncated_affected);
if r.abs() > max_correlation {
max_correlation = r.abs();
best_lag = lag as u64;
}
}
best_lag * 60 }
fn severity_rank(severity: &str) -> usize {
match severity {
"CRITICAL" => 3,
"HIGH" => 2,
"MEDIUM" => 1,
_ => 0,
}
}
pub fn amplify_severity_for_correlated_violations(
violations: &[QualityViolation],
correlations: &[MetricCorrelation],
) -> HashMap<String, f64> {
let mut amplified = HashMap::new();
let base_severity = |sev: &str| -> f64 {
match sev {
"CRITICAL" => 9.0,
"HIGH" => 7.0,
"MEDIUM" => 5.0,
"LOW" => 3.0,
_ => 1.0,
}
};
for violation in violations {
let metric = violation.metric().to_string();
let base = base_severity(violation.severity());
amplified.insert(metric, base);
}
for (metric_a, severity_a) in amplified.clone().iter() {
let mut amplification_factor = 1.0;
for violation in violations {
let metric_b = violation.metric();
if metric_a == &metric_b.to_string() {
continue;
}
for corr in correlations {
if (corr.metric_a == *metric_a && corr.metric_b == metric_b)
|| (corr.metric_a == metric_b && corr.metric_b == *metric_a)
{
if corr.is_significant() {
amplification_factor *= 1.0 + (corr.pearson_coefficient.abs() * 0.5);
}
}
}
}
amplification_factor = amplification_factor.min(2.5);
let amplified_score = (severity_a * amplification_factor).min(10.0);
amplified.insert(metric_a.clone(), amplified_score);
}
amplified
}
pub fn analyze_correlations(
history: &[CodeQualityMetrics],
violations: &[QualityViolation],
) -> CorrelationAnalysis {
let metric_correlations = compute_metric_correlations(history);
let simultaneous_violations = detect_simultaneous_violations(violations);
let mut root_causes = Vec::new();
for violation in violations {
root_causes.push(infer_root_cause(history, violation));
}
let amplified_severities =
amplify_severity_for_correlated_violations(violations, &metric_correlations);
CorrelationAnalysis {
metric_correlations,
simultaneous_violations,
root_causes,
amplified_severities,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pearson_perfect_positive_correlation() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let b = vec![2.0, 4.0, 6.0, 8.0, 10.0]; let (r, _se) = pearson_correlation(&a, &b);
assert!(
(r - 1.0).abs() < 0.01,
"Perfect positive correlation should be 1.0"
);
}
#[test]
fn test_pearson_perfect_negative_correlation() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let b = vec![5.0, 4.0, 3.0, 2.0, 1.0]; let (r, _se) = pearson_correlation(&a, &b);
assert!(
(r - (-1.0)).abs() < 0.01,
"Perfect negative correlation should be -1.0"
);
}
#[test]
fn test_pearson_no_correlation() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let b = vec![5.0, 3.0, 1.0, 4.0, 2.0]; let (r, _se) = pearson_correlation(&a, &b);
assert!(r.abs() <= 1.0, "Correlation should be in [-1, 1] range");
}
#[test]
fn test_pearson_insufficient_data() {
let a = vec![1.0];
let b = vec![2.0];
let (r, se) = pearson_correlation(&a, &b);
assert_eq!(r, 0.0);
assert_eq!(se, 0.0);
}
#[test]
fn test_correlation_significance() {
let corr_strong = MetricCorrelation {
metric_a: "stub_ratio".to_string(),
metric_b: "type_coverage".to_string(),
pearson_coefficient: 0.85,
sample_count: 30,
standard_error: 0.1,
};
assert!(corr_strong.is_significant());
let corr_weak = MetricCorrelation {
metric_a: "churn".to_string(),
metric_b: "complexity".to_string(),
pearson_coefficient: 0.3,
sample_count: 30,
standard_error: 0.15,
};
assert!(!corr_weak.is_significant());
}
#[test]
fn test_correlation_strength_interpretation() {
let tests = vec![
(0.95, "Very Strong"),
(0.75, "Strong"),
(0.55, "Moderate"),
(0.35, "Weak"),
(0.15, "Very Weak"),
];
for (coefficient, expected_strength) in tests {
let corr = MetricCorrelation {
metric_a: "a".to_string(),
metric_b: "b".to_string(),
pearson_coefficient: coefficient,
sample_count: 30,
standard_error: 0.1,
};
assert_eq!(corr.strength(), expected_strength);
}
}
#[test]
fn test_correlation_direction() {
let pos = MetricCorrelation {
metric_a: "a".to_string(),
metric_b: "b".to_string(),
pearson_coefficient: 0.7,
sample_count: 30,
standard_error: 0.1,
};
assert_eq!(pos.direction(), "Positive");
let neg = MetricCorrelation {
metric_a: "a".to_string(),
metric_b: "b".to_string(),
pearson_coefficient: -0.7,
sample_count: 30,
standard_error: 0.1,
};
assert_eq!(neg.direction(), "Negative");
let none = MetricCorrelation {
metric_a: "a".to_string(),
metric_b: "b".to_string(),
pearson_coefficient: 0.0,
sample_count: 30,
standard_error: 0.1,
};
assert_eq!(none.direction(), "None");
}
#[test]
fn test_simultaneous_violation_creation() {
let sim = SimultaneousViolation::new(
vec!["stub_ratio".to_string(), "type_coverage".to_string()],
vec!["HIGH".to_string(), "MEDIUM".to_string()],
vec![1000, 1050],
300,
);
assert_eq!(sim.violation_count, 2);
assert_eq!(sim.metric_names.len(), 2);
}
#[test]
fn test_simultaneous_violation_max_severity() {
let sim = SimultaneousViolation::new(
vec!["a".to_string(), "b".to_string(), "c".to_string()],
vec![
"MEDIUM".to_string(),
"CRITICAL".to_string(),
"HIGH".to_string(),
],
vec![1000, 1050, 1100],
300,
);
assert_eq!(sim.max_severity(), "CRITICAL");
}
#[test]
fn test_simultaneous_violation_is_compound() {
let compound = SimultaneousViolation::new(
vec!["a".to_string(), "b".to_string(), "c".to_string()],
vec!["HIGH".to_string(), "HIGH".to_string(), "HIGH".to_string()],
vec![1000, 1050, 1100],
300,
);
assert!(compound.is_compound());
let not_compound = SimultaneousViolation::new(
vec!["a".to_string(), "b".to_string()],
vec!["HIGH".to_string(), "HIGH".to_string()],
vec![1000, 1050],
300,
);
assert!(!not_compound.is_compound());
}
#[test]
fn test_root_cause_actionability() {
let high_confidence = RootCauseHypothesis {
causal_metric: "stub_ratio".to_string(),
affected_metric: "type_coverage".to_string(),
correlation: 0.85,
confidence: 0.85,
lag_seconds: 60,
evidence: "test".to_string(),
};
assert!(high_confidence.is_actionable());
let low_confidence = RootCauseHypothesis {
causal_metric: "churn".to_string(),
affected_metric: "complexity".to_string(),
correlation: 0.3,
confidence: 0.5,
lag_seconds: 60,
evidence: "test".to_string(),
};
assert!(!low_confidence.is_actionable());
}
#[test]
fn test_compute_metric_correlations_single_history() {
let history = vec![CodeQualityMetrics::default()];
let correlations = compute_metric_correlations(&history);
assert_eq!(
correlations.len(),
0,
"Single measurement cannot have correlations"
);
}
#[test]
fn test_compute_metric_correlations_multiple_history() {
let mut history = Vec::new();
for i in 0..10 {
let mut metrics = CodeQualityMetrics::default();
metrics.stub_ratio = 0.1 + (i as f64 * 0.05);
metrics.type_coverage = 0.9 - (i as f64 * 0.05); history.push(metrics);
}
let correlations = compute_metric_correlations(&history);
assert!(!correlations.is_empty());
let stub_type_corr = correlations.iter().find(|c| {
(c.metric_a == "stub_ratio" && c.metric_b == "type_coverage")
|| (c.metric_a == "type_coverage" && c.metric_b == "stub_ratio")
});
assert!(stub_type_corr.is_some());
if let Some(corr) = stub_type_corr {
assert!(
corr.pearson_coefficient < -0.5,
"Should detect negative correlation"
);
}
}
#[test]
fn test_detect_simultaneous_violations_empty() {
let violations: Vec<QualityViolation> = Vec::new();
let simultaneous = detect_simultaneous_violations(&violations);
assert_eq!(simultaneous.len(), 0);
}
#[test]
fn test_detect_simultaneous_violations_single() {
let violations = vec![QualityViolation::Rule1Sigma {
metric: "stub_ratio".to_string(),
value: 0.5,
threshold: 0.3,
z_score: 3.5,
severity: "CRITICAL".to_string(),
}];
let simultaneous = detect_simultaneous_violations(&violations);
assert_eq!(
simultaneous.len(),
0,
"Single violation is not simultaneous"
);
}
#[test]
fn test_detect_simultaneous_violations_multiple() {
let violations = vec![
QualityViolation::Rule1Sigma {
metric: "stub_ratio".to_string(),
value: 0.5,
threshold: 0.3,
z_score: 3.5,
severity: "CRITICAL".to_string(),
},
QualityViolation::Rule1Sigma {
metric: "type_coverage".to_string(),
value: 0.3,
threshold: 0.7,
z_score: -4.0,
severity: "HIGH".to_string(),
},
];
let _simultaneous = detect_simultaneous_violations(&violations);
assert!(true, "Function should complete without panic");
}
#[test]
fn test_infer_root_cause_insufficient_history() {
let history = vec![CodeQualityMetrics::default()];
let violation = QualityViolation::Rule1Sigma {
metric: "stub_ratio".to_string(),
value: 0.5,
threshold: 0.3,
z_score: 3.5,
severity: "CRITICAL".to_string(),
};
let hypothesis = infer_root_cause(&history, &violation);
assert_eq!(hypothesis.confidence, 0.0);
}
#[test]
fn test_infer_root_cause_with_correlation() {
let mut history = Vec::new();
for i in 0..10 {
let mut metrics = CodeQualityMetrics::default();
metrics.stub_ratio = 0.1 + (i as f64 * 0.05);
metrics.churn = 10 + (i * 15);
history.push(metrics);
}
let violation = QualityViolation::Rule1Sigma {
metric: "stub_ratio".to_string(),
value: 0.5,
threshold: 0.3,
z_score: 3.5,
severity: "CRITICAL".to_string(),
};
let hypothesis = infer_root_cause(&history, &violation);
assert!(hypothesis.confidence > 0.0 || hypothesis.causal_metric == "unknown");
}
#[test]
fn test_amplify_severity_no_violations() {
let violations: Vec<QualityViolation> = Vec::new();
let correlations: Vec<MetricCorrelation> = Vec::new();
let amplified = amplify_severity_for_correlated_violations(&violations, &correlations);
assert!(amplified.is_empty());
}
#[test]
fn test_amplify_severity_single_violation() {
let violations = vec![QualityViolation::Rule1Sigma {
metric: "stub_ratio".to_string(),
value: 0.5,
threshold: 0.3,
z_score: 3.5,
severity: "CRITICAL".to_string(),
}];
let correlations: Vec<MetricCorrelation> = Vec::new();
let amplified = amplify_severity_for_correlated_violations(&violations, &correlations);
assert!(!amplified.is_empty());
let stub_severity = amplified.get("stub_ratio").unwrap();
assert!(*stub_severity >= 9.0 && *stub_severity <= 10.0); }
#[test]
fn test_amplify_severity_with_correlated_violations() {
let violations = vec![
QualityViolation::Rule1Sigma {
metric: "stub_ratio".to_string(),
value: 0.5,
threshold: 0.3,
z_score: 3.5,
severity: "CRITICAL".to_string(),
},
QualityViolation::Rule1Sigma {
metric: "type_coverage".to_string(),
value: 0.3,
threshold: 0.7,
z_score: -4.0,
severity: "HIGH".to_string(),
},
];
let correlations = vec![MetricCorrelation {
metric_a: "stub_ratio".to_string(),
metric_b: "type_coverage".to_string(),
pearson_coefficient: 0.85,
sample_count: 30,
standard_error: 0.1,
}];
let amplified = amplify_severity_for_correlated_violations(&violations, &correlations);
assert_eq!(amplified.len(), 2);
let stub_severity = amplified.get("stub_ratio").unwrap();
assert!(*stub_severity > 9.0);
}
#[test]
fn test_amplify_severity_caps_at_10() {
let violations = vec![
QualityViolation::Rule1Sigma {
metric: "stub_ratio".to_string(),
value: 0.5,
threshold: 0.3,
z_score: 3.5,
severity: "CRITICAL".to_string(),
},
QualityViolation::Rule1Sigma {
metric: "type_coverage".to_string(),
value: 0.3,
threshold: 0.7,
z_score: -4.0,
severity: "CRITICAL".to_string(),
},
QualityViolation::Rule1Sigma {
metric: "churn".to_string(),
value: 500.0,
threshold: 100.0,
z_score: 4.0,
severity: "CRITICAL".to_string(),
},
];
let correlations = vec![
MetricCorrelation {
metric_a: "stub_ratio".to_string(),
metric_b: "type_coverage".to_string(),
pearson_coefficient: 0.9,
sample_count: 30,
standard_error: 0.05,
},
MetricCorrelation {
metric_a: "stub_ratio".to_string(),
metric_b: "churn".to_string(),
pearson_coefficient: 0.85,
sample_count: 30,
standard_error: 0.1,
},
];
let amplified = amplify_severity_for_correlated_violations(&violations, &correlations);
for (_, severity) in amplified.iter() {
assert!(*severity <= 10.0, "Severity should be capped at 10.0");
}
}
#[test]
fn test_analyze_correlations_full_workflow() {
let mut history = Vec::new();
for i in 0..10 {
let mut metrics = CodeQualityMetrics::default();
metrics.stub_ratio = 0.1 + (i as f64 * 0.02);
metrics.type_coverage = 0.95 - (i as f64 * 0.02);
metrics.churn = 5 + (i * 3);
history.push(metrics);
}
let violations = vec![
QualityViolation::Rule1Sigma {
metric: "stub_ratio".to_string(),
value: 0.25,
threshold: 0.3,
z_score: 3.5,
severity: "CRITICAL".to_string(),
},
QualityViolation::Rule1Sigma {
metric: "type_coverage".to_string(),
value: 0.75,
threshold: 0.7,
z_score: -3.0,
severity: "HIGH".to_string(),
},
];
let analysis = analyze_correlations(&history, &violations);
assert!(
!analysis.metric_correlations.is_empty(),
"Should compute pairwise correlations"
);
assert!(
!analysis.root_causes.is_empty(),
"Should generate root cause hypotheses"
);
assert!(
!analysis.amplified_severities.is_empty(),
"Should amplify severities"
);
assert!(analysis.timestamp > 0, "Timestamp should be valid");
}
}