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.metric);
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 metric: &QualityMetric,
347 ) -> (Option<f64>, String) {
348 match 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::warn!("CorrelationPreservation metric always returns None — not yet wired into evaluation pipeline");
376 (
377 None,
378 "correlation preservation metric not available".to_string(),
379 )
380 }
381 QualityMetric::TemporalConsistency => {
382 let rate = evaluation
383 .statistical
384 .temporal
385 .as_ref()
386 .map(|t| t.pattern_correlation);
387 (rate, "temporal analysis not available".to_string())
388 }
389 QualityMetric::PrivacyMiaAuc => {
390 let auc = evaluation
391 .privacy
392 .as_ref()
393 .and_then(|p| p.membership_inference.as_ref())
394 .map(|m| m.auc_roc);
395 (auc, "privacy MIA evaluation not available".to_string())
396 }
397 QualityMetric::CompletionRate => {
398 let rate = evaluation
399 .quality
400 .completeness
401 .as_ref()
402 .map(|c| c.overall_completeness);
403 (rate, "completeness analysis not available".to_string())
404 }
405 QualityMetric::DuplicateRate => {
406 let rate = evaluation
407 .quality
408 .uniqueness
409 .as_ref()
410 .map(|u| u.duplicate_rate);
411 (rate, "uniqueness analysis not available".to_string())
412 }
413 QualityMetric::ReferentialIntegrity => {
414 let rate = evaluation
415 .coherence
416 .referential
417 .as_ref()
418 .map(|r| r.overall_integrity_score);
419 (
420 rate,
421 "referential integrity evaluation not available".to_string(),
422 )
423 }
424 QualityMetric::IcMatchRate => {
425 let rate = evaluation
426 .coherence
427 .intercompany
428 .as_ref()
429 .map(|ic| ic.match_rate);
430 (rate, "IC matching evaluation not available".to_string())
431 }
432 QualityMetric::S2CChainCompletion => {
433 let rate = evaluation
434 .coherence
435 .sourcing
436 .as_ref()
437 .map(|s| s.rfx_completion_rate);
438 (rate, "sourcing evaluation not available".to_string())
439 }
440 QualityMetric::PayrollAccuracy => {
441 let rate = evaluation
442 .coherence
443 .hr_payroll
444 .as_ref()
445 .map(|h| h.gross_to_net_accuracy);
446 (rate, "HR/payroll evaluation not available".to_string())
447 }
448 QualityMetric::ManufacturingYield => {
449 let rate = evaluation
450 .coherence
451 .manufacturing
452 .as_ref()
453 .map(|m| m.yield_rate_consistency);
454 (rate, "manufacturing evaluation not available".to_string())
455 }
456 QualityMetric::BankReconciliationBalance => {
457 let rate = evaluation
458 .coherence
459 .bank_reconciliation
460 .as_ref()
461 .map(|b| b.balance_accuracy);
462 (
463 rate,
464 "bank reconciliation evaluation not available".to_string(),
465 )
466 }
467 QualityMetric::FinancialReportingTieBack => {
468 let rate = evaluation
469 .coherence
470 .financial_reporting
471 .as_ref()
472 .map(|fr| fr.statement_tb_tie_rate);
473 (
474 rate,
475 "financial reporting evaluation not available".to_string(),
476 )
477 }
478 QualityMetric::AmlDetectability => {
479 let rate = evaluation
480 .banking
481 .as_ref()
482 .and_then(|b| b.aml.as_ref())
483 .map(|a| a.typology_coverage);
484 (
485 rate,
486 "AML detectability evaluation not available".to_string(),
487 )
488 }
489 QualityMetric::ProcessMiningCoverage => {
490 let rate = evaluation
491 .process_mining
492 .as_ref()
493 .and_then(|pm| pm.event_sequence.as_ref())
494 .map(|es| es.timestamp_monotonicity);
495 (rate, "process mining evaluation not available".to_string())
496 }
497 QualityMetric::AuditEvidenceCoverage => {
498 let rate = evaluation
499 .coherence
500 .audit
501 .as_ref()
502 .map(|a| a.evidence_to_finding_rate);
503 (rate, "audit evaluation not available".to_string())
504 }
505 QualityMetric::AnomalySeparability => {
506 let score = evaluation
507 .ml_readiness
508 .anomaly_scoring
509 .as_ref()
510 .map(|a| a.anomaly_separability);
511 (
512 score,
513 "anomaly scoring evaluation not available".to_string(),
514 )
515 }
516 QualityMetric::FeatureQualityScore => {
517 let score = evaluation
518 .ml_readiness
519 .feature_quality
520 .as_ref()
521 .map(|f| f.feature_quality_score);
522 (
523 score,
524 "feature quality evaluation not available".to_string(),
525 )
526 }
527 QualityMetric::GnnReadinessScore => {
528 let score = evaluation
529 .ml_readiness
530 .gnn_readiness
531 .as_ref()
532 .map(|g| g.gnn_readiness_score);
533 (score, "GNN readiness evaluation not available".to_string())
534 }
535 QualityMetric::DomainGapScore => {
536 let score = evaluation
537 .ml_readiness
538 .domain_gap
539 .as_ref()
540 .map(|d| d.domain_gap_score);
541 (score, "domain gap evaluation not available".to_string())
542 }
543 QualityMetric::Custom(name) => {
544 tracing::warn!("Custom metric '{}' always returns None — custom metric evaluation not implemented", name);
545 (
546 None,
547 format!(
548 "custom metric '{}' not available in standard evaluation",
549 name
550 ),
551 )
552 }
553 }
554 }
555}
556
557#[cfg(test)]
558#[allow(clippy::unwrap_used)]
559mod tests {
560 use super::*;
561
562 fn sample_profile() -> GateProfile {
563 GateProfile::new(
564 "test",
565 vec![
566 QualityGate::lte("benford_compliance", QualityMetric::BenfordMad, 0.015),
567 QualityGate::gte("completeness", QualityMetric::CompletionRate, 0.95),
568 ],
569 )
570 }
571
572 #[test]
573 fn test_gate_check_gte() {
574 let gate = QualityGate::gte("test", QualityMetric::CompletionRate, 0.95);
575 assert!(gate.check(0.96));
576 assert!(gate.check(0.95));
577 assert!(!gate.check(0.94));
578 }
579
580 #[test]
581 fn test_gate_check_lte() {
582 let gate = QualityGate::lte("test", QualityMetric::BenfordMad, 0.015);
583 assert!(gate.check(0.01));
584 assert!(gate.check(0.015));
585 assert!(!gate.check(0.016));
586 }
587
588 #[test]
589 fn test_gate_check_between() {
590 let gate = QualityGate::between("test", QualityMetric::DuplicateRate, 0.0, 0.05);
591 assert!(gate.check(0.0));
592 assert!(gate.check(0.03));
593 assert!(gate.check(0.05));
594 assert!(!gate.check(0.06));
595 }
596
597 #[test]
598 fn test_gate_check_eq() {
599 let gate = QualityGate::new("test", QualityMetric::BalanceCoherence, 1.0, Comparison::Eq);
600 assert!(gate.check(1.0));
601 assert!(!gate.check(0.99));
602 }
603
604 #[test]
605 fn test_evaluate_empty_evaluation() {
606 let evaluation = ComprehensiveEvaluation::new();
607 let profile = sample_profile();
608 let result = GateEngine::evaluate(&evaluation, &profile);
609 assert!(result.passed);
611 assert_eq!(result.gates_total, 2);
612 }
613
614 #[test]
615 fn test_fail_fast_stops_on_first_failure() {
616 let evaluation = ComprehensiveEvaluation::new();
617 let profile = GateProfile::new(
618 "strict",
619 vec![
620 QualityGate::gte(
624 "custom_gate",
625 QualityMetric::Custom("nonexistent".to_string()),
626 0.99,
627 ),
628 QualityGate::gte(
629 "another",
630 QualityMetric::Custom("also_nonexistent".to_string()),
631 0.99,
632 ),
633 ],
634 )
635 .with_fail_strategy(FailStrategy::FailFast);
636
637 let result = GateEngine::evaluate(&evaluation, &profile);
638 assert!(result.passed);
640 }
641
642 #[test]
643 fn test_collect_all_reports_all_failures() {
644 let evaluation = ComprehensiveEvaluation::new();
645 let profile = GateProfile::new(
646 "test",
647 vec![
648 QualityGate::lte("mad", QualityMetric::BenfordMad, 0.015),
649 QualityGate::gte("completion", QualityMetric::CompletionRate, 0.95),
650 ],
651 )
652 .with_fail_strategy(FailStrategy::CollectAll);
653
654 let result = GateEngine::evaluate(&evaluation, &profile);
655 assert_eq!(result.results.len(), 2);
656 }
657
658 #[test]
659 fn test_gate_result_summary() {
660 let evaluation = ComprehensiveEvaluation::new();
661 let profile = sample_profile();
662 let result = GateEngine::evaluate(&evaluation, &profile);
663 assert!(result.summary.contains("test"));
664 }
665
666 #[test]
667 fn test_quality_metric_display() {
668 assert_eq!(QualityMetric::BenfordMad.to_string(), "benford_mad");
669 assert_eq!(
670 QualityMetric::BalanceCoherence.to_string(),
671 "balance_coherence"
672 );
673 assert_eq!(
674 QualityMetric::Custom("my_metric".to_string()).to_string(),
675 "custom:my_metric"
676 );
677 }
678
679 #[test]
680 fn test_gate_profile_serialization() {
681 let profile = sample_profile();
682 let json = serde_json::to_string(&profile).expect("serialize");
683 let deserialized: GateProfile = serde_json::from_str(&json).expect("deserialize");
684 assert_eq!(deserialized.name, "test");
685 assert_eq!(deserialized.gates.len(), 2);
686 }
687}