1use crate::config::EvaluationThresholds;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ThresholdResult {
12 pub metric_name: String,
14 pub actual_value: f64,
16 pub threshold_value: f64,
18 pub operator: ThresholdOperator,
20 pub passed: bool,
22 pub explanation: String,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum ThresholdOperator {
29 GreaterOrEqual,
31 LessOrEqual,
33 GreaterThan,
35 LessThan,
37 Equal,
39 InRange,
41}
42
43pub struct ThresholdChecker {
45 thresholds: EvaluationThresholds,
47}
48
49impl ThresholdChecker {
50 pub fn new(thresholds: EvaluationThresholds) -> Self {
52 Self { thresholds }
53 }
54
55 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 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 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 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 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 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 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 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 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}