use serde::{Deserialize, Serialize};
use crate::ComprehensiveEvaluation;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum QualityMetric {
BenfordMad,
BalanceCoherence,
DocumentChainIntegrity,
CorrelationPreservation,
TemporalConsistency,
PrivacyMiaAuc,
CompletionRate,
DuplicateRate,
ReferentialIntegrity,
IcMatchRate,
S2CChainCompletion,
PayrollAccuracy,
ManufacturingYield,
BankReconciliationBalance,
FinancialReportingTieBack,
AmlDetectability,
ProcessMiningCoverage,
AuditEvidenceCoverage,
AnomalySeparability,
FeatureQualityScore,
GnnReadinessScore,
DomainGapScore,
Custom(String),
}
impl std::fmt::Display for QualityMetric {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BenfordMad => write!(f, "benford_mad"),
Self::BalanceCoherence => write!(f, "balance_coherence"),
Self::DocumentChainIntegrity => write!(f, "document_chain_integrity"),
Self::CorrelationPreservation => write!(f, "correlation_preservation"),
Self::TemporalConsistency => write!(f, "temporal_consistency"),
Self::PrivacyMiaAuc => write!(f, "privacy_mia_auc"),
Self::CompletionRate => write!(f, "completion_rate"),
Self::DuplicateRate => write!(f, "duplicate_rate"),
Self::ReferentialIntegrity => write!(f, "referential_integrity"),
Self::IcMatchRate => write!(f, "ic_match_rate"),
Self::S2CChainCompletion => write!(f, "s2c_chain_completion"),
Self::PayrollAccuracy => write!(f, "payroll_accuracy"),
Self::ManufacturingYield => write!(f, "manufacturing_yield"),
Self::BankReconciliationBalance => write!(f, "bank_reconciliation_balance"),
Self::FinancialReportingTieBack => write!(f, "financial_reporting_tie_back"),
Self::AmlDetectability => write!(f, "aml_detectability"),
Self::ProcessMiningCoverage => write!(f, "process_mining_coverage"),
Self::AuditEvidenceCoverage => write!(f, "audit_evidence_coverage"),
Self::AnomalySeparability => write!(f, "anomaly_separability"),
Self::FeatureQualityScore => write!(f, "feature_quality_score"),
Self::GnnReadinessScore => write!(f, "gnn_readiness_score"),
Self::DomainGapScore => write!(f, "domain_gap_score"),
Self::Custom(name) => write!(f, "custom:{name}"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Comparison {
Gte,
Lte,
Eq,
Between,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum FailStrategy {
FailFast,
#[default]
CollectAll,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGate {
pub name: String,
pub metric: QualityMetric,
pub threshold: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upper_threshold: Option<f64>,
pub comparison: Comparison,
}
impl QualityGate {
pub fn new(
name: impl Into<String>,
metric: QualityMetric,
threshold: f64,
comparison: Comparison,
) -> Self {
Self {
name: name.into(),
metric,
threshold,
upper_threshold: None,
comparison,
}
}
pub fn gte(name: impl Into<String>, metric: QualityMetric, threshold: f64) -> Self {
Self::new(name, metric, threshold, Comparison::Gte)
}
pub fn lte(name: impl Into<String>, metric: QualityMetric, threshold: f64) -> Self {
Self::new(name, metric, threshold, Comparison::Lte)
}
pub fn between(name: impl Into<String>, metric: QualityMetric, lower: f64, upper: f64) -> Self {
Self {
name: name.into(),
metric,
threshold: lower,
upper_threshold: Some(upper),
comparison: Comparison::Between,
}
}
pub fn check(&self, actual: f64) -> bool {
match self.comparison {
Comparison::Gte => actual >= self.threshold,
Comparison::Lte => actual <= self.threshold,
Comparison::Eq => (actual - self.threshold).abs() < 1e-9,
Comparison::Between => {
let upper = self.upper_threshold.unwrap_or(self.threshold);
actual >= self.threshold && actual <= upper
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateProfile {
pub name: String,
pub gates: Vec<QualityGate>,
#[serde(default)]
pub fail_strategy: FailStrategy,
}
impl GateProfile {
pub fn new(name: impl Into<String>, gates: Vec<QualityGate>) -> Self {
Self {
name: name.into(),
gates,
fail_strategy: FailStrategy::default(),
}
}
pub fn with_fail_strategy(mut self, strategy: FailStrategy) -> Self {
self.fail_strategy = strategy;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateCheckResult {
pub gate_name: String,
pub metric: QualityMetric,
pub passed: bool,
pub actual_value: Option<f64>,
pub threshold: f64,
pub comparison: Comparison,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateResult {
pub passed: bool,
pub profile_name: String,
pub results: Vec<GateCheckResult>,
pub summary: String,
pub gates_passed: usize,
pub gates_total: usize,
}
pub struct GateEngine;
impl GateEngine {
pub fn evaluate(evaluation: &ComprehensiveEvaluation, profile: &GateProfile) -> GateResult {
let mut results = Vec::new();
let mut all_passed = true;
for gate in &profile.gates {
let (actual_value, message) = Self::extract_metric(evaluation, gate);
let check_result = match actual_value {
Some(value) => {
let passed = gate.check(value);
if !passed {
all_passed = false;
}
GateCheckResult {
gate_name: gate.name.clone(),
metric: gate.metric.clone(),
passed,
actual_value: Some(value),
threshold: gate.threshold,
comparison: gate.comparison.clone(),
message: if passed {
format!(
"{}: {:.4} passes {:?} {:.4}",
gate.name, value, gate.comparison, gate.threshold
)
} else {
format!(
"{}: {:.4} fails {:?} {:.4}",
gate.name, value, gate.comparison, gate.threshold
)
},
}
}
None => {
GateCheckResult {
gate_name: gate.name.clone(),
metric: gate.metric.clone(),
passed: true,
actual_value: None,
threshold: gate.threshold,
comparison: gate.comparison.clone(),
message: format!("{}: metric not available ({})", gate.name, message),
}
}
};
let failed = !check_result.passed;
results.push(check_result);
if failed && profile.fail_strategy == FailStrategy::FailFast {
break;
}
}
let gates_passed = results.iter().filter(|r| r.passed).count();
let gates_total = results.len();
let summary = if all_passed {
format!(
"All {}/{} quality gates passed (profile: {})",
gates_passed, gates_total, profile.name
)
} else {
let failed_names: Vec<_> = results
.iter()
.filter(|r| !r.passed)
.map(|r| r.gate_name.as_str())
.collect();
format!(
"{}/{} quality gates passed, {} failed: {} (profile: {})",
gates_passed,
gates_total,
gates_total - gates_passed,
failed_names.join(", "),
profile.name
)
};
GateResult {
passed: all_passed,
profile_name: profile.name.clone(),
results,
summary,
gates_passed,
gates_total,
}
}
fn extract_metric(
evaluation: &ComprehensiveEvaluation,
gate: &QualityGate,
) -> (Option<f64>, String) {
match &gate.metric {
QualityMetric::BenfordMad => {
let mad = evaluation.statistical.benford.as_ref().map(|b| b.mad);
(mad, "benford analysis not available".to_string())
}
QualityMetric::BalanceCoherence => {
let rate = evaluation.coherence.balance.as_ref().map(|b| {
if b.periods_evaluated == 0 {
0.0
} else {
(b.periods_evaluated - b.periods_imbalanced) as f64
/ b.periods_evaluated as f64
}
});
(rate, "balance sheet evaluation not available".to_string())
}
QualityMetric::DocumentChainIntegrity => {
let rate = evaluation
.coherence
.document_chain
.as_ref()
.map(|d| d.p2p_completion_rate);
(rate, "document chain evaluation not available".to_string())
}
QualityMetric::CorrelationPreservation => {
let rate = evaluation.statistical.correlation.as_ref().map(|c| {
let total = c.checks_passed + c.checks_failed;
if total > 0 {
c.checks_passed as f64 / total as f64
} else {
1.0 }
});
(rate, "correlation analysis not available".to_string())
}
QualityMetric::TemporalConsistency => {
let rate = evaluation
.statistical
.temporal
.as_ref()
.map(|t| t.pattern_correlation);
(rate, "temporal analysis not available".to_string())
}
QualityMetric::PrivacyMiaAuc => {
let auc = evaluation
.privacy
.as_ref()
.and_then(|p| p.membership_inference.as_ref())
.map(|m| m.auc_roc);
(auc, "privacy MIA evaluation not available".to_string())
}
QualityMetric::CompletionRate => {
let rate = evaluation
.quality
.completeness
.as_ref()
.map(|c| c.overall_completeness);
(rate, "completeness analysis not available".to_string())
}
QualityMetric::DuplicateRate => {
let rate = evaluation
.quality
.uniqueness
.as_ref()
.map(|u| u.duplicate_rate);
(rate, "uniqueness analysis not available".to_string())
}
QualityMetric::ReferentialIntegrity => {
let rate = evaluation
.coherence
.referential
.as_ref()
.map(|r| r.overall_integrity_score);
(
rate,
"referential integrity evaluation not available".to_string(),
)
}
QualityMetric::IcMatchRate => {
let rate = evaluation
.coherence
.intercompany
.as_ref()
.map(|ic| ic.match_rate);
(rate, "IC matching evaluation not available".to_string())
}
QualityMetric::S2CChainCompletion => {
let rate = evaluation
.coherence
.sourcing
.as_ref()
.map(|s| s.rfx_completion_rate);
(rate, "sourcing evaluation not available".to_string())
}
QualityMetric::PayrollAccuracy => {
let rate = evaluation
.coherence
.hr_payroll
.as_ref()
.map(|h| h.gross_to_net_accuracy);
(rate, "HR/payroll evaluation not available".to_string())
}
QualityMetric::ManufacturingYield => {
let rate = evaluation
.coherence
.manufacturing
.as_ref()
.map(|m| m.yield_rate_consistency);
(rate, "manufacturing evaluation not available".to_string())
}
QualityMetric::BankReconciliationBalance => {
let rate = evaluation
.coherence
.bank_reconciliation
.as_ref()
.map(|b| b.balance_accuracy);
(
rate,
"bank reconciliation evaluation not available".to_string(),
)
}
QualityMetric::FinancialReportingTieBack => {
let rate = evaluation
.coherence
.financial_reporting
.as_ref()
.map(|fr| fr.statement_tb_tie_rate);
(
rate,
"financial reporting evaluation not available".to_string(),
)
}
QualityMetric::AmlDetectability => {
let rate = evaluation
.banking
.as_ref()
.and_then(|b| b.aml.as_ref())
.map(|a| a.typology_coverage);
(
rate,
"AML detectability evaluation not available".to_string(),
)
}
QualityMetric::ProcessMiningCoverage => {
let rate = evaluation
.process_mining
.as_ref()
.and_then(|pm| pm.event_sequence.as_ref())
.map(|es| es.timestamp_monotonicity);
(rate, "process mining evaluation not available".to_string())
}
QualityMetric::AuditEvidenceCoverage => {
let rate = evaluation
.coherence
.audit
.as_ref()
.map(|a| a.evidence_to_finding_rate);
(rate, "audit evaluation not available".to_string())
}
QualityMetric::AnomalySeparability => {
let score = evaluation
.ml_readiness
.anomaly_scoring
.as_ref()
.map(|a| a.anomaly_separability);
(
score,
"anomaly scoring evaluation not available".to_string(),
)
}
QualityMetric::FeatureQualityScore => {
let score = evaluation
.ml_readiness
.feature_quality
.as_ref()
.map(|f| f.feature_quality_score);
(
score,
"feature quality evaluation not available".to_string(),
)
}
QualityMetric::GnnReadinessScore => {
let score = evaluation
.ml_readiness
.gnn_readiness
.as_ref()
.map(|g| g.gnn_readiness_score);
(score, "GNN readiness evaluation not available".to_string())
}
QualityMetric::DomainGapScore => {
let score = evaluation
.ml_readiness
.domain_gap
.as_ref()
.map(|d| d.domain_gap_score);
(score, "domain gap evaluation not available".to_string())
}
QualityMetric::Custom(name) => {
tracing::warn!(
"Custom metric '{}' in gate '{}' has no registered evaluator — \
gate skipped; implement a custom GateEvaluator to enable evaluation",
name,
gate.name
);
(
None,
format!(
"custom metric '{name}' not implemented — gate skipped; \
register a custom GateEvaluator for real evaluation"
),
)
}
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn sample_profile() -> GateProfile {
GateProfile::new(
"test",
vec![
QualityGate::lte("benford_compliance", QualityMetric::BenfordMad, 0.015),
QualityGate::gte("completeness", QualityMetric::CompletionRate, 0.95),
],
)
}
#[test]
fn test_gate_check_gte() {
let gate = QualityGate::gte("test", QualityMetric::CompletionRate, 0.95);
assert!(gate.check(0.96));
assert!(gate.check(0.95));
assert!(!gate.check(0.94));
}
#[test]
fn test_gate_check_lte() {
let gate = QualityGate::lte("test", QualityMetric::BenfordMad, 0.015);
assert!(gate.check(0.01));
assert!(gate.check(0.015));
assert!(!gate.check(0.016));
}
#[test]
fn test_gate_check_between() {
let gate = QualityGate::between("test", QualityMetric::DuplicateRate, 0.0, 0.05);
assert!(gate.check(0.0));
assert!(gate.check(0.03));
assert!(gate.check(0.05));
assert!(!gate.check(0.06));
}
#[test]
fn test_gate_check_eq() {
let gate = QualityGate::new("test", QualityMetric::BalanceCoherence, 1.0, Comparison::Eq);
assert!(gate.check(1.0));
assert!(!gate.check(0.99));
}
#[test]
fn test_evaluate_empty_evaluation() {
let evaluation = ComprehensiveEvaluation::new();
let profile = sample_profile();
let result = GateEngine::evaluate(&evaluation, &profile);
assert!(result.passed);
assert_eq!(result.gates_total, 2);
}
#[test]
fn test_fail_fast_stops_on_first_failure() {
let evaluation = ComprehensiveEvaluation::new();
let profile = GateProfile::new(
"strict",
vec![
QualityGate::gte(
"custom_gate",
QualityMetric::Custom("nonexistent".to_string()),
0.99,
),
QualityGate::gte(
"another",
QualityMetric::Custom("also_nonexistent".to_string()),
0.99,
),
],
)
.with_fail_strategy(FailStrategy::FailFast);
let result = GateEngine::evaluate(&evaluation, &profile);
assert!(result.passed);
}
#[test]
fn test_collect_all_reports_all_failures() {
let evaluation = ComprehensiveEvaluation::new();
let profile = GateProfile::new(
"test",
vec![
QualityGate::lte("mad", QualityMetric::BenfordMad, 0.015),
QualityGate::gte("completion", QualityMetric::CompletionRate, 0.95),
],
)
.with_fail_strategy(FailStrategy::CollectAll);
let result = GateEngine::evaluate(&evaluation, &profile);
assert_eq!(result.results.len(), 2);
}
#[test]
fn test_gate_result_summary() {
let evaluation = ComprehensiveEvaluation::new();
let profile = sample_profile();
let result = GateEngine::evaluate(&evaluation, &profile);
assert!(result.summary.contains("test"));
}
#[test]
fn test_quality_metric_display() {
assert_eq!(QualityMetric::BenfordMad.to_string(), "benford_mad");
assert_eq!(
QualityMetric::BalanceCoherence.to_string(),
"balance_coherence"
);
assert_eq!(
QualityMetric::Custom("my_metric".to_string()).to_string(),
"custom:my_metric"
);
}
#[test]
fn test_gate_profile_serialization() {
let profile = sample_profile();
let json = serde_json::to_string(&profile).expect("serialize");
let deserialized: GateProfile = serde_json::from_str(&json).expect("deserialize");
assert_eq!(deserialized.name, "test");
assert_eq!(deserialized.gates.len(), 2);
}
}