Skip to main content

datasynth_eval/report/
thresholds.rs

1//! Threshold checking for pass/fail determination.
2//!
3//! Validates metrics against configured thresholds and generates
4//! pass/fail results with detailed feedback.
5
6use crate::config::EvaluationThresholds;
7use serde::{Deserialize, Serialize};
8
9/// Result of threshold checking.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ThresholdResult {
12    /// Metric name.
13    pub metric_name: String,
14    /// Actual value.
15    pub actual_value: f64,
16    /// Threshold value.
17    pub threshold_value: f64,
18    /// Comparison operator.
19    pub operator: ThresholdOperator,
20    /// Whether threshold was met.
21    pub passed: bool,
22    /// Human-readable explanation.
23    pub explanation: String,
24}
25
26/// Threshold comparison operator.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum ThresholdOperator {
29    /// Greater than or equal.
30    GreaterOrEqual,
31    /// Less than or equal.
32    LessOrEqual,
33    /// Greater than.
34    GreaterThan,
35    /// Less than.
36    LessThan,
37    /// Equal (with tolerance).
38    Equal,
39    /// Within range.
40    InRange,
41}
42
43/// Checker for threshold validation.
44pub struct ThresholdChecker {
45    /// Thresholds to check against.
46    thresholds: EvaluationThresholds,
47}
48
49impl ThresholdChecker {
50    /// Create a new checker with the specified thresholds.
51    pub fn new(thresholds: EvaluationThresholds) -> Self {
52        Self { thresholds }
53    }
54
55    /// Check a single metric against a minimum threshold.
56    pub fn check_min(&self, name: &str, actual: f64, threshold: f64) -> ThresholdResult {
57        let passed = actual >= threshold;
58        ThresholdResult {
59            metric_name: name.to_string(),
60            actual_value: actual,
61            threshold_value: threshold,
62            operator: ThresholdOperator::GreaterOrEqual,
63            passed,
64            explanation: if passed {
65                format!("{name} ({actual:.4}) >= {threshold} (threshold)")
66            } else {
67                format!("{name} ({actual:.4}) < {threshold} (threshold) - FAILED")
68            },
69        }
70    }
71
72    /// Check a single metric against a maximum threshold.
73    pub fn check_max(&self, name: &str, actual: f64, threshold: f64) -> ThresholdResult {
74        let passed = actual <= threshold;
75        ThresholdResult {
76            metric_name: name.to_string(),
77            actual_value: actual,
78            threshold_value: threshold,
79            operator: ThresholdOperator::LessOrEqual,
80            passed,
81            explanation: if passed {
82                format!("{name} ({actual:.4}) <= {threshold} (threshold)")
83            } else {
84                format!("{name} ({actual:.4}) > {threshold} (threshold) - FAILED")
85            },
86        }
87    }
88
89    /// Check a metric is within a range.
90    pub fn check_range(&self, name: &str, actual: f64, min: f64, max: f64) -> ThresholdResult {
91        let passed = actual >= min && actual <= max;
92        ThresholdResult {
93            metric_name: name.to_string(),
94            actual_value: actual,
95            threshold_value: (min + max) / 2.0,
96            operator: ThresholdOperator::InRange,
97            passed,
98            explanation: if passed {
99                format!("{name} ({actual:.4}) in range [{min}, {max}]")
100            } else {
101                format!("{name} ({actual:.4}) outside range [{min}, {max}] - FAILED")
102            },
103        }
104    }
105
106    /// Check all statistical thresholds.
107    pub fn check_statistical(
108        &self,
109        benford_p: Option<f64>,
110        benford_mad: Option<f64>,
111        temporal_corr: Option<f64>,
112    ) -> Vec<ThresholdResult> {
113        let mut results = Vec::new();
114
115        if let Some(p) = benford_p {
116            results.push(self.check_min("benford_p_value", p, self.thresholds.benford_p_value_min));
117        }
118
119        if let Some(mad) = benford_mad {
120            results.push(self.check_max("benford_mad", mad, self.thresholds.benford_mad_max));
121        }
122
123        if let Some(corr) = temporal_corr {
124            results.push(self.check_min(
125                "temporal_correlation",
126                corr,
127                self.thresholds.temporal_correlation_min,
128            ));
129        }
130
131        results
132    }
133
134    /// Check all coherence thresholds.
135    pub fn check_coherence(
136        &self,
137        balance_imbalance: Option<f64>,
138        subledger_rate: Option<f64>,
139        doc_chain_rate: Option<f64>,
140        ic_match_rate: Option<f64>,
141    ) -> Vec<ThresholdResult> {
142        let mut results = Vec::new();
143
144        if let Some(imb) = balance_imbalance {
145            let tolerance = self
146                .thresholds
147                .balance_tolerance
148                .to_string()
149                .parse::<f64>()
150                .unwrap_or(0.01);
151            results.push(self.check_max("balance_imbalance", imb, tolerance));
152        }
153
154        if let Some(rate) = subledger_rate {
155            results.push(self.check_min(
156                "subledger_reconciliation",
157                rate,
158                self.thresholds.subledger_reconciliation_rate_min,
159            ));
160        }
161
162        if let Some(rate) = doc_chain_rate {
163            results.push(self.check_min(
164                "document_chain_completion",
165                rate,
166                self.thresholds.document_chain_completion_min,
167            ));
168        }
169
170        if let Some(rate) = ic_match_rate {
171            results.push(self.check_min("ic_match_rate", rate, self.thresholds.ic_match_rate_min));
172        }
173
174        results
175    }
176
177    /// Check all quality thresholds.
178    pub fn check_quality(
179        &self,
180        duplicate_rate: Option<f64>,
181        completeness: Option<f64>,
182        format_consistency: Option<f64>,
183    ) -> Vec<ThresholdResult> {
184        let mut results = Vec::new();
185
186        if let Some(rate) = duplicate_rate {
187            results.push(self.check_max(
188                "duplicate_rate",
189                rate,
190                self.thresholds.duplicate_rate_max,
191            ));
192        }
193
194        if let Some(comp) = completeness {
195            results.push(self.check_min(
196                "completeness",
197                comp,
198                self.thresholds.completeness_rate_min,
199            ));
200        }
201
202        if let Some(fmt) = format_consistency {
203            results.push(self.check_min(
204                "format_consistency",
205                fmt,
206                self.thresholds.format_consistency_min,
207            ));
208        }
209
210        results
211    }
212
213    /// Check all ML thresholds.
214    pub fn check_ml(
215        &self,
216        anomaly_rate: Option<f64>,
217        label_coverage: Option<f64>,
218        graph_connectivity: Option<f64>,
219    ) -> Vec<ThresholdResult> {
220        let mut results = Vec::new();
221
222        if let Some(rate) = anomaly_rate {
223            results.push(self.check_range(
224                "anomaly_rate",
225                rate,
226                self.thresholds.anomaly_rate_min,
227                self.thresholds.anomaly_rate_max,
228            ));
229        }
230
231        if let Some(cov) = label_coverage {
232            results.push(self.check_min("label_coverage", cov, self.thresholds.label_coverage_min));
233        }
234
235        if let Some(conn) = graph_connectivity {
236            results.push(self.check_min(
237                "graph_connectivity",
238                conn,
239                self.thresholds.graph_connectivity_min,
240            ));
241        }
242
243        results
244    }
245
246    /// Get all threshold results.
247    pub fn check_all(
248        &self,
249        benford_p: Option<f64>,
250        benford_mad: Option<f64>,
251        temporal_corr: Option<f64>,
252        balance_imbalance: Option<f64>,
253        subledger_rate: Option<f64>,
254        doc_chain_rate: Option<f64>,
255        ic_match_rate: Option<f64>,
256        duplicate_rate: Option<f64>,
257        completeness: Option<f64>,
258        format_consistency: Option<f64>,
259        anomaly_rate: Option<f64>,
260        label_coverage: Option<f64>,
261        graph_connectivity: Option<f64>,
262    ) -> Vec<ThresholdResult> {
263        let mut all = Vec::new();
264        all.extend(self.check_statistical(benford_p, benford_mad, temporal_corr));
265        all.extend(self.check_coherence(
266            balance_imbalance,
267            subledger_rate,
268            doc_chain_rate,
269            ic_match_rate,
270        ));
271        all.extend(self.check_quality(duplicate_rate, completeness, format_consistency));
272        all.extend(self.check_ml(anomaly_rate, label_coverage, graph_connectivity));
273        all
274    }
275
276    /// Check if all results pass.
277    pub fn all_pass(results: &[ThresholdResult]) -> bool {
278        results.iter().all(|r| r.passed)
279    }
280}
281
282impl Default for ThresholdChecker {
283    fn default() -> Self {
284        Self::new(EvaluationThresholds::default())
285    }
286}
287
288#[cfg(test)]
289#[allow(clippy::unwrap_used)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_check_min() {
295        let checker = ThresholdChecker::default();
296        let result = checker.check_min("test_metric", 0.95, 0.90);
297        assert!(result.passed);
298    }
299
300    #[test]
301    fn test_check_min_fail() {
302        let checker = ThresholdChecker::default();
303        let result = checker.check_min("test_metric", 0.85, 0.90);
304        assert!(!result.passed);
305    }
306
307    #[test]
308    fn test_check_max() {
309        let checker = ThresholdChecker::default();
310        let result = checker.check_max("test_metric", 0.05, 0.10);
311        assert!(result.passed);
312    }
313
314    #[test]
315    fn test_check_range() {
316        let checker = ThresholdChecker::default();
317        let result = checker.check_range("test_metric", 0.10, 0.05, 0.15);
318        assert!(result.passed);
319
320        let result2 = checker.check_range("test_metric", 0.20, 0.05, 0.15);
321        assert!(!result2.passed);
322    }
323}