use std::collections::BTreeMap;
use chrono::NaiveDate;
use proptest::prelude::*;
use sdivi_core::compute::thresholds::{compute_thresholds_check, THRESHOLD_EPSILON};
use sdivi_core::input::{ThresholdOverrideInput, ThresholdsInput};
use sdivi_core::null_summary;
use sdivi_snapshot::delta::DivergenceSummary;
fn arb_summary() -> impl Strategy<Value = DivergenceSummary> {
(
proptest::option::of(-5.0f64..10.0),
proptest::option::of(-5.0f64..10.0),
proptest::option::of(-1.0f64..1.0),
proptest::option::of(-10i64..10),
proptest::option::of(-5i64..20),
)
.prop_map(|(ped, cdd, cd, ccd, bvd)| DivergenceSummary {
pattern_entropy_delta: ped,
convention_drift_delta: cdd,
coupling_delta: cd,
community_count_delta: ccd,
boundary_violation_delta: bvd,
pattern_entropy_per_category_delta: None,
convention_drift_per_category_delta: None,
})
}
fn arb_thresholds() -> impl Strategy<Value = ThresholdsInput> {
(0.1f64..10.0, 0.1f64..10.0, 0.01f64..1.0, 0.1f64..10.0).prop_map(|(pe, cd, coup, bv)| {
ThresholdsInput {
pattern_entropy_rate: pe,
convention_drift_rate: cd,
coupling_delta_rate: coup,
boundary_violation_rate: bv,
..ThresholdsInput::default()
}
})
}
fn arb_override_entry() -> impl Strategy<Value = (String, ThresholdOverrideInput)> {
("[a-z]{4,8}", 0.1f64..20.0, prop::bool::ANY).prop_map(|(cat, rate, active)| {
let expires = if active { "2099-12-31" } else { "2000-01-01" };
(
cat,
ThresholdOverrideInput {
pattern_entropy_rate: Some(rate),
convention_drift_rate: None,
coupling_delta_rate: None,
boundary_violation_rate: None,
expires: expires.to_string(),
},
)
})
}
fn arb_thresholds_with_overrides() -> impl Strategy<Value = ThresholdsInput> {
(
arb_thresholds(),
prop::collection::vec(arb_override_entry(), 0..4),
)
.prop_map(|(mut cfg, entries)| {
cfg.overrides = entries.into_iter().collect();
cfg.today = NaiveDate::from_ymd_opt(2026, 5, 1).unwrap();
cfg
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(500))]
#[test]
fn prop_test_compute_thresholds_check_pure(
summary in arb_summary(),
thresholds in arb_thresholds(),
) {
let r1 = compute_thresholds_check(&summary, &thresholds);
let r2 = compute_thresholds_check(&summary, &thresholds);
prop_assert_eq!(
serde_json::to_string(&r1).unwrap(),
serde_json::to_string(&r2).unwrap(),
"compute_thresholds_check must be referentially transparent",
);
}
#[test]
fn prop_none_delta_never_breaches(thresholds in arb_thresholds()) {
let summary = DivergenceSummary {
pattern_entropy_delta: None,
convention_drift_delta: None,
coupling_delta: None,
community_count_delta: None,
boundary_violation_delta: None,
pattern_entropy_per_category_delta: None,
convention_drift_per_category_delta: None,
};
let r = compute_thresholds_check(&summary, &thresholds);
prop_assert!(!r.breached, "null summary must never breach");
}
#[test]
fn prop_thresholds_with_overrides_pure(
summary in arb_summary(),
thresholds in arb_thresholds_with_overrides(),
) {
let r1 = compute_thresholds_check(&summary, &thresholds);
let r2 = compute_thresholds_check(&summary, &thresholds);
prop_assert_eq!(
serde_json::to_string(&r1).unwrap(),
serde_json::to_string(&r2).unwrap()
);
}
#[test]
fn prop_per_category_delta_pure(
cat in "[a-z]{4,8}",
delta in -5.0f64..10.0,
thresholds in arb_thresholds_with_overrides(),
) {
let summary = DivergenceSummary {
pattern_entropy_delta: None,
convention_drift_delta: None,
coupling_delta: None,
community_count_delta: None,
boundary_violation_delta: None,
pattern_entropy_per_category_delta: Some(BTreeMap::from([(cat.clone(), delta)])),
convention_drift_per_category_delta: None,
};
let r1 = compute_thresholds_check(&summary, &thresholds);
let r2 = compute_thresholds_check(&summary, &thresholds);
prop_assert_eq!(
serde_json::to_string(&r1).unwrap(),
serde_json::to_string(&r2).unwrap()
);
let cat_breaches: Vec<_> = r1.breaches.iter()
.filter(|b| b.category.as_deref() == Some(&cat))
.collect();
prop_assert!(cat_breaches.len() <= 1,
"at most one breach per category per dimension");
}
#[test]
fn prop_convention_drift_per_category_delta_pure(
cat in "[a-z]{4,8}",
delta in -5.0f64..10.0,
thresholds in arb_thresholds_with_overrides(),
) {
let summary = DivergenceSummary {
pattern_entropy_delta: None,
convention_drift_delta: None,
coupling_delta: None,
community_count_delta: None,
boundary_violation_delta: None,
pattern_entropy_per_category_delta: None,
convention_drift_per_category_delta: Some(BTreeMap::from([(cat.clone(), delta)])),
};
let r1 = compute_thresholds_check(&summary, &thresholds);
let r2 = compute_thresholds_check(&summary, &thresholds);
prop_assert_eq!(
serde_json::to_string(&r1).unwrap(),
serde_json::to_string(&r2).unwrap(),
"convention_drift per-category check must be referentially transparent"
);
let cat_breaches: Vec<_> = r1.breaches.iter()
.filter(|b| b.dimension == "convention_drift"
&& b.category.as_deref() == Some(&cat))
.collect();
prop_assert!(
cat_breaches.len() <= 1,
"at most one convention_drift breach per category"
);
if delta <= 0.0 {
prop_assert!(
cat_breaches.is_empty(),
"negative convention_drift delta must never breach"
);
}
}
#[test]
fn prop_breach_equals_delta_gt_limit_plus_epsilon(
limit in 0.1f64..10.0,
delta in -5.0f64..15.0,
) {
let cfg = ThresholdsInput { pattern_entropy_rate: limit, ..ThresholdsInput::default() };
let mut summary = null_summary();
summary.pattern_entropy_delta = Some(delta);
let r = compute_thresholds_check(&summary, &cfg);
let expected = delta > limit + THRESHOLD_EPSILON;
prop_assert_eq!(
r.breached,
expected,
"breached({}, {}) must equal delta > limit + THRESHOLD_EPSILON ({})",
delta, limit, expected
);
}
#[test]
fn prop_breach_equals_delta_gt_limit_plus_epsilon_convention_drift(
limit in 0.1f64..10.0,
delta in -5.0f64..15.0,
) {
let cfg = ThresholdsInput { convention_drift_rate: limit, ..ThresholdsInput::default() };
let mut summary = null_summary();
summary.convention_drift_delta = Some(delta);
let r = compute_thresholds_check(&summary, &cfg);
let expected = delta > limit + THRESHOLD_EPSILON;
prop_assert_eq!(
r.breached,
expected,
"convention_drift: breached({}, {}) must equal delta > limit + THRESHOLD_EPSILON ({})",
delta, limit, expected
);
}
#[test]
fn prop_breach_equals_delta_gt_limit_plus_epsilon_coupling_delta(
limit in 0.01f64..1.0,
delta in -1.0f64..2.0,
) {
let cfg = ThresholdsInput { coupling_delta_rate: limit, ..ThresholdsInput::default() };
let mut summary = null_summary();
summary.coupling_delta = Some(delta);
let r = compute_thresholds_check(&summary, &cfg);
let expected = delta > limit + THRESHOLD_EPSILON;
prop_assert_eq!(
r.breached,
expected,
"coupling_delta: breached({}, {}) must equal delta > limit + THRESHOLD_EPSILON ({})",
delta, limit, expected
);
}
#[test]
fn prop_breach_equals_delta_gt_limit_plus_epsilon_boundary_violation(
limit in 0.1f64..10.0,
delta in -10i64..20,
) {
let cfg = ThresholdsInput { boundary_violation_rate: limit, ..ThresholdsInput::default() };
let mut summary = null_summary();
summary.boundary_violation_delta = Some(delta);
let r = compute_thresholds_check(&summary, &cfg);
let delta_f = delta as f64;
let expected = delta_f > limit + THRESHOLD_EPSILON;
prop_assert_eq!(
r.breached,
expected,
"boundary_violation: breached({}, {}) must equal delta_f > limit + THRESHOLD_EPSILON ({})",
delta_f, limit, expected
);
}
}