use std::collections::BTreeMap;
use chrono::NaiveDate;
use crate::input::{ThresholdOverrideInput, ThresholdsInput};
use sdivi_snapshot::delta::DivergenceSummary;
pub use super::threshold_types::{
AppliedOverrideInfo, ThresholdBreachInfo, ThresholdCheckResult, THRESHOLD_EPSILON,
};
pub fn compute_thresholds_check(
summary: &DivergenceSummary,
cfg: &ThresholdsInput,
) -> ThresholdCheckResult {
let (applied_overrides, active_overrides) = resolve_overrides(cfg);
let mut breaches: Vec<ThresholdBreachInfo> = Vec::new();
if let Some(delta) = summary.pattern_entropy_delta {
let limit = cfg.pattern_entropy_rate;
if delta > limit + THRESHOLD_EPSILON {
breaches.push(ThresholdBreachInfo {
dimension: "pattern_entropy".to_string(),
category: None,
actual: delta,
limit,
});
}
}
if let Some(delta) = summary.convention_drift_delta {
let limit = cfg.convention_drift_rate;
if delta > limit + THRESHOLD_EPSILON {
breaches.push(ThresholdBreachInfo {
dimension: "convention_drift".to_string(),
category: None,
actual: delta,
limit,
});
}
}
if let Some(delta) = summary.coupling_delta {
let limit = cfg.coupling_delta_rate;
if delta > limit + THRESHOLD_EPSILON {
breaches.push(ThresholdBreachInfo {
dimension: "coupling_delta".to_string(),
category: None,
actual: delta,
limit,
});
}
}
if let Some(delta) = summary.boundary_violation_delta {
let limit = cfg.boundary_violation_rate;
let delta_f = delta as f64;
if delta_f > limit + THRESHOLD_EPSILON {
breaches.push(ThresholdBreachInfo {
dimension: "boundary_violations".to_string(),
category: None,
actual: delta_f,
limit,
});
}
}
if let Some(per_cat) = &summary.pattern_entropy_per_category_delta {
for (cat, &delta) in per_cat {
let limit = active_overrides
.get(cat)
.and_then(|ov| ov.pattern_entropy_rate)
.unwrap_or(cfg.pattern_entropy_rate);
if delta > limit + THRESHOLD_EPSILON {
breaches.push(ThresholdBreachInfo {
dimension: "pattern_entropy".to_string(),
category: Some(cat.clone()),
actual: delta,
limit,
});
}
}
}
if let Some(per_cat) = &summary.convention_drift_per_category_delta {
for (cat, &delta) in per_cat {
let limit = active_overrides
.get(cat)
.and_then(|ov| ov.convention_drift_rate)
.unwrap_or(cfg.convention_drift_rate);
if delta > limit + THRESHOLD_EPSILON {
breaches.push(ThresholdBreachInfo {
dimension: "convention_drift".to_string(),
category: Some(cat.clone()),
actual: delta,
limit,
});
}
}
}
let breached = !breaches.is_empty();
ThresholdCheckResult {
breached,
breaches,
applied_overrides,
}
}
fn resolve_overrides(
cfg: &ThresholdsInput,
) -> (
BTreeMap<String, AppliedOverrideInfo>,
BTreeMap<String, &ThresholdOverrideInput>,
) {
let mut diagnostics: BTreeMap<String, AppliedOverrideInfo> = BTreeMap::new();
let mut active: BTreeMap<String, &ThresholdOverrideInput> = BTreeMap::new();
for (cat, ov) in &cfg.overrides {
match NaiveDate::parse_from_str(&ov.expires, "%Y-%m-%d") {
Err(e) => {
diagnostics.insert(
cat.clone(),
AppliedOverrideInfo {
active: false,
expires: NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
expired_reason: Some(format!("failed to parse expires date: {e}")),
},
);
}
Ok(expires) => {
let is_active = cfg.today <= expires;
let expired_reason = if is_active {
None
} else {
Some(format!("expired on {expires}"))
};
diagnostics.insert(
cat.clone(),
AppliedOverrideInfo {
active: is_active,
expires,
expired_reason,
},
);
if is_active {
active.insert(cat.clone(), ov);
}
}
}
}
(diagnostics, active)
}