1use serde::{Deserialize, Serialize};
6
7use crate::ComprehensiveEvaluation;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[serde(rename_all = "snake_case")]
12pub enum QualityMetric {
13 BenfordMad,
15 BalanceCoherence,
17 DocumentChainIntegrity,
19 CorrelationPreservation,
21 TemporalConsistency,
23 PrivacyMiaAuc,
25 CompletionRate,
27 DuplicateRate,
29 ReferentialIntegrity,
31 IcMatchRate,
33 S2CChainCompletion,
35 PayrollAccuracy,
37 ManufacturingYield,
39 BankReconciliationBalance,
41 FinancialReportingTieBack,
43 AmlDetectability,
45 ProcessMiningCoverage,
47 AuditEvidenceCoverage,
49 AnomalySeparability,
51 FeatureQualityScore,
53 GnnReadinessScore,
55 DomainGapScore,
57 Custom(String),
59}
60
61impl std::fmt::Display for QualityMetric {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 Self::BenfordMad => write!(f, "benford_mad"),
65 Self::BalanceCoherence => write!(f, "balance_coherence"),
66 Self::DocumentChainIntegrity => write!(f, "document_chain_integrity"),
67 Self::CorrelationPreservation => write!(f, "correlation_preservation"),
68 Self::TemporalConsistency => write!(f, "temporal_consistency"),
69 Self::PrivacyMiaAuc => write!(f, "privacy_mia_auc"),
70 Self::CompletionRate => write!(f, "completion_rate"),
71 Self::DuplicateRate => write!(f, "duplicate_rate"),
72 Self::ReferentialIntegrity => write!(f, "referential_integrity"),
73 Self::IcMatchRate => write!(f, "ic_match_rate"),
74 Self::S2CChainCompletion => write!(f, "s2c_chain_completion"),
75 Self::PayrollAccuracy => write!(f, "payroll_accuracy"),
76 Self::ManufacturingYield => write!(f, "manufacturing_yield"),
77 Self::BankReconciliationBalance => write!(f, "bank_reconciliation_balance"),
78 Self::FinancialReportingTieBack => write!(f, "financial_reporting_tie_back"),
79 Self::AmlDetectability => write!(f, "aml_detectability"),
80 Self::ProcessMiningCoverage => write!(f, "process_mining_coverage"),
81 Self::AuditEvidenceCoverage => write!(f, "audit_evidence_coverage"),
82 Self::AnomalySeparability => write!(f, "anomaly_separability"),
83 Self::FeatureQualityScore => write!(f, "feature_quality_score"),
84 Self::GnnReadinessScore => write!(f, "gnn_readiness_score"),
85 Self::DomainGapScore => write!(f, "domain_gap_score"),
86 Self::Custom(name) => write!(f, "custom:{}", name),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(rename_all = "snake_case")]
94pub enum Comparison {
95 Gte,
97 Lte,
99 Eq,
101 Between,
103}
104
105#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
107#[serde(rename_all = "snake_case")]
108pub enum FailStrategy {
109 FailFast,
111 #[default]
113 CollectAll,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct QualityGate {
119 pub name: String,
121 pub metric: QualityMetric,
123 pub threshold: f64,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub upper_threshold: Option<f64>,
128 pub comparison: Comparison,
130}
131
132impl QualityGate {
133 pub fn new(
135 name: impl Into<String>,
136 metric: QualityMetric,
137 threshold: f64,
138 comparison: Comparison,
139 ) -> Self {
140 Self {
141 name: name.into(),
142 metric,
143 threshold,
144 upper_threshold: None,
145 comparison,
146 }
147 }
148
149 pub fn gte(name: impl Into<String>, metric: QualityMetric, threshold: f64) -> Self {
151 Self::new(name, metric, threshold, Comparison::Gte)
152 }
153
154 pub fn lte(name: impl Into<String>, metric: QualityMetric, threshold: f64) -> Self {
156 Self::new(name, metric, threshold, Comparison::Lte)
157 }
158
159 pub fn between(name: impl Into<String>, metric: QualityMetric, lower: f64, upper: f64) -> Self {
161 Self {
162 name: name.into(),
163 metric,
164 threshold: lower,
165 upper_threshold: Some(upper),
166 comparison: Comparison::Between,
167 }
168 }
169
170 pub fn check(&self, actual: f64) -> bool {
172 match self.comparison {
173 Comparison::Gte => actual >= self.threshold,
174 Comparison::Lte => actual <= self.threshold,
175 Comparison::Eq => (actual - self.threshold).abs() < 1e-9,
176 Comparison::Between => {
177 let upper = self.upper_threshold.unwrap_or(self.threshold);
178 actual >= self.threshold && actual <= upper
179 }
180 }
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct GateProfile {
187 pub name: String,
189 pub gates: Vec<QualityGate>,
191 #[serde(default)]
193 pub fail_strategy: FailStrategy,
194}
195
196impl GateProfile {
197 pub fn new(name: impl Into<String>, gates: Vec<QualityGate>) -> Self {
199 Self {
200 name: name.into(),
201 gates,
202 fail_strategy: FailStrategy::default(),
203 }
204 }
205
206 pub fn with_fail_strategy(mut self, strategy: FailStrategy) -> Self {
208 self.fail_strategy = strategy;
209 self
210 }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct GateCheckResult {
216 pub gate_name: String,
218 pub metric: QualityMetric,
220 pub passed: bool,
222 pub actual_value: Option<f64>,
224 pub threshold: f64,
226 pub comparison: Comparison,
228 pub message: String,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct GateResult {
235 pub passed: bool,
237 pub profile_name: String,
239 pub results: Vec<GateCheckResult>,
241 pub summary: String,
243 pub gates_passed: usize,
245 pub gates_total: usize,
247}
248
249pub struct GateEngine;
251
252impl GateEngine {
253 pub fn evaluate(evaluation: &ComprehensiveEvaluation, profile: &GateProfile) -> GateResult {
255 let mut results = Vec::new();
256 let mut all_passed = true;
257
258 for gate in &profile.gates {
259 let (actual_value, message) = Self::extract_metric(evaluation, gate);
260
261 let check_result = match actual_value {
262 Some(value) => {
263 let passed = gate.check(value);
264 if !passed {
265 all_passed = false;
266 }
267 GateCheckResult {
268 gate_name: gate.name.clone(),
269 metric: gate.metric.clone(),
270 passed,
271 actual_value: Some(value),
272 threshold: gate.threshold,
273 comparison: gate.comparison.clone(),
274 message: if passed {
275 format!(
276 "{}: {:.4} passes {:?} {:.4}",
277 gate.name, value, gate.comparison, gate.threshold
278 )
279 } else {
280 format!(
281 "{}: {:.4} fails {:?} {:.4}",
282 gate.name, value, gate.comparison, gate.threshold
283 )
284 },
285 }
286 }
287 None => {
288 GateCheckResult {
290 gate_name: gate.name.clone(),
291 metric: gate.metric.clone(),
292 passed: true,
293 actual_value: None,
294 threshold: gate.threshold,
295 comparison: gate.comparison.clone(),
296 message: format!("{}: metric not available ({})", gate.name, message),
297 }
298 }
299 };
300
301 let failed = !check_result.passed;
302 results.push(check_result);
303
304 if failed && profile.fail_strategy == FailStrategy::FailFast {
305 break;
306 }
307 }
308
309 let gates_passed = results.iter().filter(|r| r.passed).count();
310 let gates_total = results.len();
311
312 let summary = if all_passed {
313 format!(
314 "All {}/{} quality gates passed (profile: {})",
315 gates_passed, gates_total, profile.name
316 )
317 } else {
318 let failed_names: Vec<_> = results
319 .iter()
320 .filter(|r| !r.passed)
321 .map(|r| r.gate_name.as_str())
322 .collect();
323 format!(
324 "{}/{} quality gates passed, {} failed: {} (profile: {})",
325 gates_passed,
326 gates_total,
327 gates_total - gates_passed,
328 failed_names.join(", "),
329 profile.name
330 )
331 };
332
333 GateResult {
334 passed: all_passed,
335 profile_name: profile.name.clone(),
336 results,
337 summary,
338 gates_passed,
339 gates_total,
340 }
341 }
342
343 fn extract_metric(
345 evaluation: &ComprehensiveEvaluation,
346 gate: &QualityGate,
347 ) -> (Option<f64>, String) {
348 match &gate.metric {
349 QualityMetric::BenfordMad => {
350 let mad = evaluation.statistical.benford.as_ref().map(|b| b.mad);
351 (mad, "benford analysis not available".to_string())
352 }
353 QualityMetric::BalanceCoherence => {
354 let rate = evaluation.coherence.balance.as_ref().map(|b| {
355 if b.periods_evaluated == 0 {
356 0.0
357 } else {
358 (b.periods_evaluated - b.periods_imbalanced) as f64
359 / b.periods_evaluated as f64
360 }
361 });
362 (rate, "balance sheet evaluation not available".to_string())
363 }
364 QualityMetric::DocumentChainIntegrity => {
365 let rate = evaluation
366 .coherence
367 .document_chain
368 .as_ref()
369 .map(|d| d.p2p_completion_rate);
370 (rate, "document chain evaluation not available".to_string())
371 }
372 QualityMetric::CorrelationPreservation => {
373 tracing::error!(
376 "CorrelationPreservation gate '{}' cannot be evaluated — metric not implemented",
377 gate.name
378 );
379 (
380 None,
381 "correlation preservation metric not implemented — gate cannot be evaluated"
382 .to_string(),
383 )
384 }
385 QualityMetric::TemporalConsistency => {
386 let rate = evaluation
387 .statistical
388 .temporal
389 .as_ref()
390 .map(|t| t.pattern_correlation);
391 (rate, "temporal analysis not available".to_string())
392 }
393 QualityMetric::PrivacyMiaAuc => {
394 let auc = evaluation
395 .privacy
396 .as_ref()
397 .and_then(|p| p.membership_inference.as_ref())
398 .map(|m| m.auc_roc);
399 (auc, "privacy MIA evaluation not available".to_string())
400 }
401 QualityMetric::CompletionRate => {
402 let rate = evaluation
403 .quality
404 .completeness
405 .as_ref()
406 .map(|c| c.overall_completeness);
407 (rate, "completeness analysis not available".to_string())
408 }
409 QualityMetric::DuplicateRate => {
410 let rate = evaluation
411 .quality
412 .uniqueness
413 .as_ref()
414 .map(|u| u.duplicate_rate);
415 (rate, "uniqueness analysis not available".to_string())
416 }
417 QualityMetric::ReferentialIntegrity => {
418 let rate = evaluation
419 .coherence
420 .referential
421 .as_ref()
422 .map(|r| r.overall_integrity_score);
423 (
424 rate,
425 "referential integrity evaluation not available".to_string(),
426 )
427 }
428 QualityMetric::IcMatchRate => {
429 let rate = evaluation
430 .coherence
431 .intercompany
432 .as_ref()
433 .map(|ic| ic.match_rate);
434 (rate, "IC matching evaluation not available".to_string())
435 }
436 QualityMetric::S2CChainCompletion => {
437 let rate = evaluation
438 .coherence
439 .sourcing
440 .as_ref()
441 .map(|s| s.rfx_completion_rate);
442 (rate, "sourcing evaluation not available".to_string())
443 }
444 QualityMetric::PayrollAccuracy => {
445 let rate = evaluation
446 .coherence
447 .hr_payroll
448 .as_ref()
449 .map(|h| h.gross_to_net_accuracy);
450 (rate, "HR/payroll evaluation not available".to_string())
451 }
452 QualityMetric::ManufacturingYield => {
453 let rate = evaluation
454 .coherence
455 .manufacturing
456 .as_ref()
457 .map(|m| m.yield_rate_consistency);
458 (rate, "manufacturing evaluation not available".to_string())
459 }
460 QualityMetric::BankReconciliationBalance => {
461 let rate = evaluation
462 .coherence
463 .bank_reconciliation
464 .as_ref()
465 .map(|b| b.balance_accuracy);
466 (
467 rate,
468 "bank reconciliation evaluation not available".to_string(),
469 )
470 }
471 QualityMetric::FinancialReportingTieBack => {
472 let rate = evaluation
473 .coherence
474 .financial_reporting
475 .as_ref()
476 .map(|fr| fr.statement_tb_tie_rate);
477 (
478 rate,
479 "financial reporting evaluation not available".to_string(),
480 )
481 }
482 QualityMetric::AmlDetectability => {
483 let rate = evaluation
484 .banking
485 .as_ref()
486 .and_then(|b| b.aml.as_ref())
487 .map(|a| a.typology_coverage);
488 (
489 rate,
490 "AML detectability evaluation not available".to_string(),
491 )
492 }
493 QualityMetric::ProcessMiningCoverage => {
494 let rate = evaluation
495 .process_mining
496 .as_ref()
497 .and_then(|pm| pm.event_sequence.as_ref())
498 .map(|es| es.timestamp_monotonicity);
499 (rate, "process mining evaluation not available".to_string())
500 }
501 QualityMetric::AuditEvidenceCoverage => {
502 let rate = evaluation
503 .coherence
504 .audit
505 .as_ref()
506 .map(|a| a.evidence_to_finding_rate);
507 (rate, "audit evaluation not available".to_string())
508 }
509 QualityMetric::AnomalySeparability => {
510 let score = evaluation
511 .ml_readiness
512 .anomaly_scoring
513 .as_ref()
514 .map(|a| a.anomaly_separability);
515 (
516 score,
517 "anomaly scoring evaluation not available".to_string(),
518 )
519 }
520 QualityMetric::FeatureQualityScore => {
521 let score = evaluation
522 .ml_readiness
523 .feature_quality
524 .as_ref()
525 .map(|f| f.feature_quality_score);
526 (
527 score,
528 "feature quality evaluation not available".to_string(),
529 )
530 }
531 QualityMetric::GnnReadinessScore => {
532 let score = evaluation
533 .ml_readiness
534 .gnn_readiness
535 .as_ref()
536 .map(|g| g.gnn_readiness_score);
537 (score, "GNN readiness evaluation not available".to_string())
538 }
539 QualityMetric::DomainGapScore => {
540 let score = evaluation
541 .ml_readiness
542 .domain_gap
543 .as_ref()
544 .map(|d| d.domain_gap_score);
545 (score, "domain gap evaluation not available".to_string())
546 }
547 QualityMetric::Custom(name) => {
548 tracing::error!(
549 "Custom metric '{}' gate '{}' cannot be evaluated — custom metrics not implemented",
550 name, gate.name
551 );
552 (
553 None,
554 format!(
555 "custom metric '{}' not implemented — gate cannot be evaluated",
556 name
557 ),
558 )
559 }
560 }
561 }
562}
563
564#[cfg(test)]
565#[allow(clippy::unwrap_used)]
566mod tests {
567 use super::*;
568
569 fn sample_profile() -> GateProfile {
570 GateProfile::new(
571 "test",
572 vec![
573 QualityGate::lte("benford_compliance", QualityMetric::BenfordMad, 0.015),
574 QualityGate::gte("completeness", QualityMetric::CompletionRate, 0.95),
575 ],
576 )
577 }
578
579 #[test]
580 fn test_gate_check_gte() {
581 let gate = QualityGate::gte("test", QualityMetric::CompletionRate, 0.95);
582 assert!(gate.check(0.96));
583 assert!(gate.check(0.95));
584 assert!(!gate.check(0.94));
585 }
586
587 #[test]
588 fn test_gate_check_lte() {
589 let gate = QualityGate::lte("test", QualityMetric::BenfordMad, 0.015);
590 assert!(gate.check(0.01));
591 assert!(gate.check(0.015));
592 assert!(!gate.check(0.016));
593 }
594
595 #[test]
596 fn test_gate_check_between() {
597 let gate = QualityGate::between("test", QualityMetric::DuplicateRate, 0.0, 0.05);
598 assert!(gate.check(0.0));
599 assert!(gate.check(0.03));
600 assert!(gate.check(0.05));
601 assert!(!gate.check(0.06));
602 }
603
604 #[test]
605 fn test_gate_check_eq() {
606 let gate = QualityGate::new("test", QualityMetric::BalanceCoherence, 1.0, Comparison::Eq);
607 assert!(gate.check(1.0));
608 assert!(!gate.check(0.99));
609 }
610
611 #[test]
612 fn test_evaluate_empty_evaluation() {
613 let evaluation = ComprehensiveEvaluation::new();
614 let profile = sample_profile();
615 let result = GateEngine::evaluate(&evaluation, &profile);
616 assert!(result.passed);
618 assert_eq!(result.gates_total, 2);
619 }
620
621 #[test]
622 fn test_fail_fast_stops_on_first_failure() {
623 let evaluation = ComprehensiveEvaluation::new();
624 let profile = GateProfile::new(
625 "strict",
626 vec![
627 QualityGate::gte(
631 "custom_gate",
632 QualityMetric::Custom("nonexistent".to_string()),
633 0.99,
634 ),
635 QualityGate::gte(
636 "another",
637 QualityMetric::Custom("also_nonexistent".to_string()),
638 0.99,
639 ),
640 ],
641 )
642 .with_fail_strategy(FailStrategy::FailFast);
643
644 let result = GateEngine::evaluate(&evaluation, &profile);
645 assert!(result.passed);
647 }
648
649 #[test]
650 fn test_collect_all_reports_all_failures() {
651 let evaluation = ComprehensiveEvaluation::new();
652 let profile = GateProfile::new(
653 "test",
654 vec![
655 QualityGate::lte("mad", QualityMetric::BenfordMad, 0.015),
656 QualityGate::gte("completion", QualityMetric::CompletionRate, 0.95),
657 ],
658 )
659 .with_fail_strategy(FailStrategy::CollectAll);
660
661 let result = GateEngine::evaluate(&evaluation, &profile);
662 assert_eq!(result.results.len(), 2);
663 }
664
665 #[test]
666 fn test_gate_result_summary() {
667 let evaluation = ComprehensiveEvaluation::new();
668 let profile = sample_profile();
669 let result = GateEngine::evaluate(&evaluation, &profile);
670 assert!(result.summary.contains("test"));
671 }
672
673 #[test]
674 fn test_quality_metric_display() {
675 assert_eq!(QualityMetric::BenfordMad.to_string(), "benford_mad");
676 assert_eq!(
677 QualityMetric::BalanceCoherence.to_string(),
678 "balance_coherence"
679 );
680 assert_eq!(
681 QualityMetric::Custom("my_metric".to_string()).to_string(),
682 "custom:my_metric"
683 );
684 }
685
686 #[test]
687 fn test_gate_profile_serialization() {
688 let profile = sample_profile();
689 let json = serde_json::to_string(&profile).expect("serialize");
690 let deserialized: GateProfile = serde_json::from_str(&json).expect("deserialize");
691 assert_eq!(deserialized.name, "test");
692 assert_eq!(deserialized.gates.len(), 2);
693 }
694}