use crate::calibrate::profile::{MetricKind, StyleProfile};
const MAX_CEILING_MULTIPLIER: f64 = 5.0;
#[derive(Debug, Clone, Default)]
pub struct ThresholdResolver {
profile: Option<StyleProfile>,
}
impl ThresholdResolver {
pub fn new(profile: Option<StyleProfile>) -> Self {
Self { profile }
}
fn clamp(value: f64, default: f64) -> f64 {
let ceiling = default * MAX_CEILING_MULTIPLIER;
value.max(default).min(ceiling)
}
pub fn warn(&self, kind: MetricKind, default: f64) -> f64 {
match &self.profile {
Some(p) => Self::clamp(p.threshold_warn(kind, default), default),
None => default,
}
}
pub fn high(&self, kind: MetricKind, default: f64) -> f64 {
match &self.profile {
Some(p) => Self::clamp(p.threshold_high(kind, default), default),
None => default,
}
}
pub fn warn_usize(&self, kind: MetricKind, default: usize) -> usize {
self.warn(kind, default as f64) as usize
}
pub fn high_usize(&self, kind: MetricKind, default: usize) -> usize {
self.high(kind, default as f64) as usize
}
pub fn is_adaptive(&self) -> bool {
self.profile.is_some()
}
pub fn source(&self, kind: MetricKind) -> &'static str {
let is_adaptive = self.profile.as_ref()
.and_then(|p| p.get(kind))
.is_some_and(|d| d.confident);
if is_adaptive { "adaptive" } else { "default" }
}
pub fn explain(
&self,
kind: MetricKind,
actual_value: f64,
default_threshold: f64,
) -> ThresholdExplanation {
let source = self.source(kind);
let effective = self.warn(kind, default_threshold);
let percentile = match &self.profile {
Some(p) => p.get(kind).map(|d| {
format!(
"p90={:.0}, p95={:.0}, mean={:.1}, n={}",
d.p90, d.p95, d.mean, d.count
)
}),
None => None,
};
ThresholdExplanation {
threshold_source: source,
effective_threshold: effective,
default_threshold,
actual_value,
percentile_info: percentile,
}
}
}
#[derive(Debug, Clone)]
pub struct ThresholdExplanation {
pub threshold_source: &'static str,
pub effective_threshold: f64,
pub default_threshold: f64,
pub actual_value: f64,
pub percentile_info: Option<String>,
}
impl ThresholdExplanation {
pub fn to_note(&self) -> String {
let mut parts = vec![
format!(
"Threshold: {:.0} ({})",
self.effective_threshold, self.threshold_source
),
format!("Actual: {:.0}", self.actual_value),
];
if self.threshold_source == "adaptive" {
parts.push(format!("Default would be: {:.0}", self.default_threshold));
if let Some(ref pinfo) = self.percentile_info {
parts.push(format!("Profile: {}", pinfo));
}
}
parts.join(" | ")
}
pub fn to_metadata(&self) -> Vec<(String, String)> {
let mut meta = vec![
(
"threshold_source".to_string(),
self.threshold_source.to_string(),
),
(
"effective_threshold".to_string(),
format!("{:.0}", self.effective_threshold),
),
(
"actual_value".to_string(),
format!("{:.0}", self.actual_value),
),
];
if self.threshold_source == "adaptive" {
meta.push((
"default_threshold".to_string(),
format!("{:.0}", self.default_threshold),
));
}
meta
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calibrate::profile::{MetricDistribution, MetricKind, StyleProfile};
use std::collections::HashMap;
fn profile_with_metric(kind: MetricKind, values: &mut [f64]) -> StyleProfile {
let dist = MetricDistribution::from_values(values);
let mut metrics = HashMap::new();
metrics.insert(kind, dist);
StyleProfile {
version: StyleProfile::VERSION,
generated_at: String::new(),
commit_sha: None,
total_files: 0,
total_functions: values.len(),
metrics,
}
}
#[test]
fn test_no_profile_returns_defaults() {
let resolver = ThresholdResolver::new(None);
assert_eq!(resolver.warn(MetricKind::Complexity, 10.0), 10.0);
assert_eq!(resolver.high(MetricKind::Complexity, 20.0), 20.0);
assert!(!resolver.is_adaptive());
assert_eq!(resolver.source(MetricKind::Complexity), "default");
}
#[test]
fn test_no_profile_usize_helpers() {
let resolver = ThresholdResolver::new(None);
assert_eq!(resolver.warn_usize(MetricKind::FunctionLength, 50), 50);
assert_eq!(resolver.high_usize(MetricKind::NestingDepth, 5), 5);
}
#[test]
fn test_adaptive_uses_p90_when_higher_than_default() {
let mut values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
let profile = profile_with_metric(MetricKind::Complexity, &mut values);
let resolver = ThresholdResolver::new(Some(profile));
let warn = resolver.warn(MetricKind::Complexity, 10.0);
assert!(
(warn - 50.0).abs() < 0.01,
"expected ceiling-clamped 50, got {warn}"
);
assert!(resolver.is_adaptive());
assert_eq!(resolver.source(MetricKind::Complexity), "adaptive");
}
#[test]
fn test_adaptive_floor_never_below_default() {
let mut values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
let profile = profile_with_metric(MetricKind::FunctionLength, &mut values);
let resolver = ThresholdResolver::new(Some(profile));
let warn = resolver.warn(MetricKind::FunctionLength, 200.0);
assert!(
(warn - 200.0).abs() < 0.01,
"floor should clamp to default 200, got {warn}"
);
}
#[test]
fn test_adaptive_ceiling_clamps_at_5x_default() {
let mut values: Vec<f64> = (1..=100).map(|i| i as f64 * 10.0).collect();
let profile = profile_with_metric(MetricKind::Complexity, &mut values);
let resolver = ThresholdResolver::new(Some(profile));
let warn = resolver.warn(MetricKind::Complexity, 10.0);
assert!(
(warn - 50.0).abs() < 0.01,
"ceiling should clamp to 5*10=50, got {warn}"
);
let high = resolver.high(MetricKind::Complexity, 10.0);
assert!(
(high - 50.0).abs() < 0.01,
"high ceiling should also clamp to 50, got {high}"
);
}
#[test]
fn test_small_sample_falls_back_to_default() {
let mut values = vec![1.0, 2.0, 3.0]; let profile = profile_with_metric(MetricKind::NestingDepth, &mut values);
let resolver = ThresholdResolver::new(Some(profile));
assert_eq!(resolver.source(MetricKind::NestingDepth), "default");
assert_eq!(resolver.warn(MetricKind::NestingDepth, 5.0), 5.0);
assert_eq!(resolver.high(MetricKind::NestingDepth, 8.0), 8.0);
}
#[test]
fn test_missing_metric_returns_default() {
let mut values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
let profile = profile_with_metric(MetricKind::Complexity, &mut values);
let resolver = ThresholdResolver::new(Some(profile));
assert_eq!(resolver.warn(MetricKind::FanOut, 7.0), 7.0);
assert_eq!(resolver.high(MetricKind::FanOut, 12.0), 12.0);
}
#[test]
fn test_explain_default_source() {
let resolver = ThresholdResolver::new(None);
let explanation = resolver.explain(MetricKind::Complexity, 15.0, 10.0);
assert_eq!(explanation.threshold_source, "default");
assert!((explanation.effective_threshold - 10.0).abs() < 0.01);
assert!((explanation.actual_value - 15.0).abs() < 0.01);
assert!(explanation.percentile_info.is_none());
}
#[test]
fn test_explain_adaptive_includes_percentile_info() {
let mut values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
let profile = profile_with_metric(MetricKind::Complexity, &mut values);
let resolver = ThresholdResolver::new(Some(profile));
let explanation = resolver.explain(MetricKind::Complexity, 55.0, 10.0);
assert_eq!(explanation.threshold_source, "adaptive");
assert!(explanation.percentile_info.is_some());
let info = explanation.percentile_info.expect("percentile info present");
assert!(info.contains("p90="));
assert!(info.contains("p95="));
assert!(info.contains("mean="));
assert!(info.contains("n=100"));
}
#[test]
fn test_to_note_default_format() {
let explanation = ThresholdExplanation {
threshold_source: "default",
effective_threshold: 10.0,
default_threshold: 10.0,
actual_value: 15.0,
percentile_info: None,
};
let note = explanation.to_note();
assert!(note.contains("Threshold: 10 (default)"));
assert!(note.contains("Actual: 15"));
assert!(!note.contains("Default would be"));
}
#[test]
fn test_to_note_adaptive_format() {
let explanation = ThresholdExplanation {
threshold_source: "adaptive",
effective_threshold: 50.0,
default_threshold: 10.0,
actual_value: 55.0,
percentile_info: Some("p90=45, p95=48, mean=25.0, n=200".to_string()),
};
let note = explanation.to_note();
assert!(note.contains("Threshold: 50 (adaptive)"));
assert!(note.contains("Default would be: 10"));
assert!(note.contains("Profile: p90=45"));
}
#[test]
fn test_to_metadata_default_has_three_entries() {
let explanation = ThresholdExplanation {
threshold_source: "default",
effective_threshold: 10.0,
default_threshold: 10.0,
actual_value: 15.0,
percentile_info: None,
};
let meta = explanation.to_metadata();
assert_eq!(meta.len(), 3);
}
#[test]
fn test_to_metadata_adaptive_has_four_entries() {
let explanation = ThresholdExplanation {
threshold_source: "adaptive",
effective_threshold: 50.0,
default_threshold: 10.0,
actual_value: 55.0,
percentile_info: Some("info".to_string()),
};
let meta = explanation.to_metadata();
assert_eq!(meta.len(), 4);
assert!(meta.iter().any(|(k, _)| k == "default_threshold"));
}
}