use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use anyhow::{Result, Context};
use chrono::{DateTime, Utc};
use tracing::{info, warn, error};
use crate::semantic::{
sla_bounded_evaluation::{SLABoundedEvaluationResult, GateValidationResult},
config_fingerprint::{ConfigFingerprint, PolicyReference, ConfigFingerprintSystem},
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum GateStatus {
Pass,
Fail,
Warning,
NotEvaluated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateRequirement {
pub requirement_id: String,
pub description: String,
pub status: GateStatus,
pub measured_value: Option<f32>,
pub threshold: f32,
pub threshold_operator: ThresholdOperator,
pub details: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ThresholdOperator {
GreaterThanOrEqual, LessThanOrEqual, Equal, }
#[derive(Debug, Serialize, Deserialize)]
pub struct GateCheckResult {
pub gate_id: String,
pub timestamp: DateTime<Utc>,
pub overall_status: GateStatus,
pub requirements: Vec<GateRequirement>,
pub evaluation_results: HashMap<String, SLABoundedEvaluationResult>,
pub config_fingerprints: HashMap<String, ConfigFingerprint>,
pub baseline_policy: PolicyReference,
pub candidate_policy: PolicyReference,
pub artifacts: Vec<ArtifactReference>,
pub summary: GateCheckSummary,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GateCheckSummary {
pub total_requirements: usize,
pub passed_requirements: usize,
pub failed_requirements: usize,
pub warning_requirements: usize,
pub promotion_decision: PromotionDecision,
pub next_steps: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum PromotionDecision {
Promote,
Block,
Conditional(Vec<String>),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ArtifactReference {
pub artifact_id: String,
pub artifact_type: ArtifactType,
pub path: String,
pub sha256_hash: String,
pub size_bytes: u64,
pub creation_timestamp: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum ArtifactType {
TrainedModel,
CalibrationData,
EvaluationResults,
ConfigFingerprint,
BenchmarkReport,
}
pub struct GateChecker {
fingerprint_system: ConfigFingerprintSystem,
gate_id: String,
}
impl GateChecker {
pub fn new() -> Result<Self> {
let fingerprint_system = ConfigFingerprintSystem::new()
.context("Failed to initialize fingerprint system")?;
let gate_id = format!("gate_{}", chrono::Utc::now().format("%Y%m%d_%H%M%S"));
Ok(Self {
fingerprint_system,
gate_id,
})
}
pub async fn check_gates(
&mut self,
evaluation_results: HashMap<String, SLABoundedEvaluationResult>,
config_fingerprints: HashMap<String, ConfigFingerprint>,
baseline_policy: PolicyReference,
candidate_policy: PolicyReference,
) -> Result<GateCheckResult> {
info!("Starting gate check: {}", self.gate_id);
let mut requirements = Vec::new();
let mut artifacts = Vec::new();
let ndcg_requirement = self.check_semantic_lift_gate(&evaluation_results).await?;
requirements.push(ndcg_requirement);
let ece_requirement = self.check_calibration_gate(&evaluation_results).await?;
requirements.push(ece_requirement);
let sla_requirement = self.check_sla_recall_gate(&evaluation_results).await?;
requirements.push(sla_requirement);
let significance_requirement = self.check_statistical_significance_gate(&evaluation_results).await?;
requirements.push(significance_requirement);
let attestation_requirement = self.check_config_attestation_gate(&config_fingerprints).await?;
requirements.push(attestation_requirement);
let (artifact_requirement, artifact_refs) = self.check_artifact_integrity_gate(&evaluation_results, &config_fingerprints).await?;
requirements.push(artifact_requirement);
artifacts.extend(artifact_refs);
let overall_status = self.calculate_overall_status(&requirements);
let summary = self.generate_summary(&requirements);
let result = GateCheckResult {
gate_id: self.gate_id.clone(),
timestamp: Utc::now(),
overall_status,
requirements,
evaluation_results,
config_fingerprints,
baseline_policy,
candidate_policy,
artifacts,
summary,
};
self.save_gate_check_result(&result).await?;
info!(
"Gate check complete: {} - Status: {:?}",
self.gate_id, result.overall_status
);
Ok(result)
}
async fn check_semantic_lift_gate(
&self,
evaluation_results: &HashMap<String, SLABoundedEvaluationResult>,
) -> Result<GateRequirement> {
let mut max_delta_ndcg = 0.0;
let mut details = Vec::new();
let mut found_comparison = false;
for (slice_name, result) in evaluation_results {
if let Some(baseline_comp) = &result.baseline_comparison {
found_comparison = true;
let delta_pp = baseline_comp.delta_ndcg * 100.0; max_delta_ndcg = max_delta_ndcg.max(delta_pp);
details.push(format!(
"Slice {}: ΔnDCG = +{:.2}pp (baseline: {:.4}, candidate: {:.4})",
slice_name,
delta_pp,
baseline_comp.baseline_ndcg,
baseline_comp.candidate_ndcg
));
if baseline_comp.statistical_significance {
details.push(format!(" ✓ Statistically significant (p={:.4})", baseline_comp.p_value));
} else {
details.push(format!(" ⚠ Not statistically significant (p={:.4})", baseline_comp.p_value));
}
}
}
let status = if !found_comparison {
details.push("No baseline comparisons found".to_string());
GateStatus::Fail
} else if max_delta_ndcg >= 4.0 {
GateStatus::Pass
} else {
GateStatus::Fail
};
Ok(GateRequirement {
requirement_id: "SEMANTIC_LIFT".to_string(),
description: "ΔnDCG ≥ +4.0pp semantic lift on NL slices".to_string(),
status,
measured_value: if found_comparison { Some(max_delta_ndcg) } else { None },
threshold: 4.0,
threshold_operator: ThresholdOperator::GreaterThanOrEqual,
details,
})
}
async fn check_calibration_gate(
&self,
evaluation_results: &HashMap<String, SLABoundedEvaluationResult>,
) -> Result<GateRequirement> {
let mut max_ece = 0.0;
let mut details = Vec::new();
for (slice_name, result) in evaluation_results {
let ece = result.expected_calibration_error;
max_ece = max_ece.max(ece);
details.push(format!(
"Slice {}: ECE = {:.4} (threshold: ≤0.02)",
slice_name, ece
));
for bin in &result.calibration_bins {
if bin.count > 0 {
details.push(format!(
" Bin {}: [{:.2}, {:.2}] - {} samples, confidence: {:.3}, accuracy: {:.3}, ECE: {:.4}",
bin.bin_id,
bin.confidence_range.0,
bin.confidence_range.1,
bin.count,
bin.avg_confidence,
bin.avg_accuracy,
bin.bin_ece
));
}
}
}
let status = if max_ece <= 0.02 {
GateStatus::Pass
} else {
GateStatus::Fail
};
Ok(GateRequirement {
requirement_id: "CALIBRATION_ECE".to_string(),
description: "Expected Calibration Error ≤ 0.02".to_string(),
status,
measured_value: Some(max_ece),
threshold: 0.02,
threshold_operator: ThresholdOperator::LessThanOrEqual,
details,
})
}
async fn check_sla_recall_gate(
&self,
evaluation_results: &HashMap<String, SLABoundedEvaluationResult>,
) -> Result<GateRequirement> {
let mut min_sla_recall = 1.0;
let mut details = Vec::new();
for (slice_name, result) in evaluation_results {
let sla_recall = result.sla_recall;
min_sla_recall = min_sla_recall.min(sla_recall);
details.push(format!(
"Slice {}: SLA-Recall = {:.3} ({}/{} queries within 150ms)",
slice_name,
sla_recall,
result.within_sla_queries,
result.total_queries
));
let timing = &result.execution_time_stats;
details.push(format!(
" Timing: mean={:.1}ms, p95={:.1}ms, p99={:.1}ms, timeouts={}",
timing.mean_ms,
timing.p95_ms,
timing.p99_ms,
timing.timeout_count
));
}
let status = if min_sla_recall >= 0.0 {
GateStatus::Pass
} else {
GateStatus::Fail
};
Ok(GateRequirement {
requirement_id: "SLA_RECALL".to_string(),
description: "SLA-Recall ≥ 0 (queries processed within 150ms)".to_string(),
status,
measured_value: Some(min_sla_recall),
threshold: 0.0,
threshold_operator: ThresholdOperator::GreaterThanOrEqual,
details,
})
}
async fn check_statistical_significance_gate(
&self,
evaluation_results: &HashMap<String, SLABoundedEvaluationResult>,
) -> Result<GateRequirement> {
let mut significance_count = 0;
let mut total_comparisons = 0;
let mut details = Vec::new();
for (slice_name, result) in evaluation_results {
if let Some(baseline_comp) = &result.baseline_comparison {
total_comparisons += 1;
if baseline_comp.statistical_significance {
significance_count += 1;
}
details.push(format!(
"Slice {}: p={:.4}, significant={}",
slice_name,
baseline_comp.p_value,
baseline_comp.statistical_significance
));
}
if let Some(bootstrap_ci) = &result.bootstrap_confidence_interval {
details.push(format!(
" Bootstrap CI: [{:.4}, {:.4}] (point estimate: {:.4})",
bootstrap_ci.lower_bound,
bootstrap_ci.upper_bound,
bootstrap_ci.point_estimate
));
}
}
let significance_rate = if total_comparisons > 0 {
significance_count as f32 / total_comparisons as f32
} else {
0.0
};
let status = if significance_rate >= 0.5 { GateStatus::Pass
} else if total_comparisons == 0 {
GateStatus::NotEvaluated
} else {
GateStatus::Warning
};
Ok(GateRequirement {
requirement_id: "STATISTICAL_SIGNIFICANCE".to_string(),
description: "Statistical significance via paired bootstrap (α=0.05)".to_string(),
status,
measured_value: Some(significance_rate),
threshold: 0.5,
threshold_operator: ThresholdOperator::GreaterThanOrEqual,
details,
})
}
async fn check_config_attestation_gate(
&self,
config_fingerprints: &HashMap<String, ConfigFingerprint>,
) -> Result<GateRequirement> {
let mut details = Vec::new();
let mut valid_count = 0;
let mut total_count = 0;
for (config_name, fingerprint) in config_fingerprints {
total_count += 1;
let is_valid = self.fingerprint_system.verify_fingerprint(fingerprint)
.unwrap_or(false);
if is_valid {
valid_count += 1;
}
details.push(format!(
"Config {}: fingerprint={}, valid={}, type={:?}",
config_name,
&fingerprint.fingerprint_id[..8],
is_valid,
fingerprint.config_type
));
match &fingerprint.attestation.validation_status {
crate::semantic::config_fingerprint::ValidationStatus::Valid => {
details.push(" ✓ Configuration validation: Valid".to_string());
}
crate::semantic::config_fingerprint::ValidationStatus::Warning(warnings) => {
details.push(format!(" ⚠ Configuration validation: {} warnings", warnings.len()));
for warning in warnings {
details.push(format!(" - {}", warning));
}
}
crate::semantic::config_fingerprint::ValidationStatus::Invalid(errors) => {
details.push(format!(" ❌ Configuration validation: {} errors", errors.len()));
for error in errors {
details.push(format!(" - {}", error));
}
}
}
}
let status = if valid_count == total_count && total_count > 0 {
GateStatus::Pass
} else if total_count == 0 {
GateStatus::NotEvaluated
} else {
GateStatus::Fail
};
Ok(GateRequirement {
requirement_id: "CONFIG_ATTESTATION".to_string(),
description: "Configuration fingerprint attestation and integrity".to_string(),
status,
measured_value: Some(valid_count as f32 / total_count.max(1) as f32),
threshold: 1.0,
threshold_operator: ThresholdOperator::GreaterThanOrEqual,
details,
})
}
async fn check_artifact_integrity_gate(
&self,
evaluation_results: &HashMap<String, SLABoundedEvaluationResult>,
config_fingerprints: &HashMap<String, ConfigFingerprint>,
) -> Result<(GateRequirement, Vec<ArtifactReference>)> {
let mut details = Vec::new();
let mut artifacts = Vec::new();
let mut verified_count = 0;
let mut total_count = 0;
for (slice_name, result) in evaluation_results {
total_count += 1;
let artifact_exists = self.verify_artifact_exists(&result.artifact_path).await;
if artifact_exists {
verified_count += 1;
let artifact_ref = ArtifactReference {
artifact_id: format!("eval_{}_{}", slice_name, &result.slice_name),
artifact_type: ArtifactType::EvaluationResults,
path: result.artifact_path.clone(),
sha256_hash: "placeholder".to_string(), size_bytes: 0, creation_timestamp: Utc::now(),
};
artifacts.push(artifact_ref);
}
details.push(format!(
"Evaluation artifact {}: path={}, exists={}",
slice_name, result.artifact_path, artifact_exists
));
}
for (config_name, fingerprint) in config_fingerprints {
total_count += 1;
let artifact_path = format!("artifact://config/config_{}_{}.json",
fingerprint.config_type.to_string().to_lowercase(),
&fingerprint.fingerprint_id[..8]
);
let artifact_exists = self.verify_artifact_exists(&artifact_path).await;
if artifact_exists {
verified_count += 1;
let artifact_ref = ArtifactReference {
artifact_id: format!("config_{}", config_name),
artifact_type: ArtifactType::ConfigFingerprint,
path: artifact_path.clone(),
sha256_hash: fingerprint.fingerprint_id.clone(),
size_bytes: 0,
creation_timestamp: fingerprint.creation_timestamp,
};
artifacts.push(artifact_ref);
}
details.push(format!(
"Config artifact {}: path={}, exists={}",
config_name, artifact_path, artifact_exists
));
}
let status = if verified_count == total_count && total_count > 0 {
GateStatus::Pass
} else if total_count == 0 {
GateStatus::NotEvaluated
} else {
GateStatus::Fail
};
let requirement = GateRequirement {
requirement_id: "ARTIFACT_INTEGRITY".to_string(),
description: "Artifact publishing and SHA256 verification".to_string(),
status,
measured_value: Some(verified_count as f32 / total_count.max(1) as f32),
threshold: 1.0,
threshold_operator: ThresholdOperator::GreaterThanOrEqual,
details,
};
Ok((requirement, artifacts))
}
async fn verify_artifact_exists(&self, artifact_path: &str) -> bool {
if let Some(path_part) = artifact_path.split("://").nth(1) {
let filesystem_path = std::path::Path::new("artifact").join(path_part);
tokio::fs::metadata(&filesystem_path).await.is_ok()
} else {
false
}
}
fn calculate_overall_status(&self, requirements: &[GateRequirement]) -> GateStatus {
let mut has_fail = false;
let mut has_warning = false;
let mut has_not_evaluated = false;
for req in requirements {
match req.status {
GateStatus::Fail => has_fail = true,
GateStatus::Warning => has_warning = true,
GateStatus::NotEvaluated => has_not_evaluated = true,
GateStatus::Pass => {}
}
}
if has_fail {
GateStatus::Fail
} else if has_warning || has_not_evaluated {
GateStatus::Warning
} else {
GateStatus::Pass
}
}
fn generate_summary(&self, requirements: &[GateRequirement]) -> GateCheckSummary {
let total_requirements = requirements.len();
let passed_requirements = requirements.iter().filter(|r| r.status == GateStatus::Pass).count();
let failed_requirements = requirements.iter().filter(|r| r.status == GateStatus::Fail).count();
let warning_requirements = requirements.iter().filter(|r| r.status == GateStatus::Warning).count();
let promotion_decision = if failed_requirements == 0 {
if warning_requirements == 0 {
PromotionDecision::Promote
} else {
PromotionDecision::Conditional(vec![
"Review warnings before promotion".to_string(),
"Ensure statistical significance is adequate".to_string(),
])
}
} else {
PromotionDecision::Block
};
let next_steps = match promotion_decision {
PromotionDecision::Promote => vec![
"✅ All gates passed - ready for production promotion".to_string(),
"Publish trained model artifacts".to_string(),
"Update baseline policy reference".to_string(),
],
PromotionDecision::Block => vec![
"❌ Critical requirements failed - promotion blocked".to_string(),
"Address failed requirements and rerun training".to_string(),
"Review model architecture and hyperparameters".to_string(),
],
PromotionDecision::Conditional(_) => vec![
"⚠ Some warnings present - review before promotion".to_string(),
"Consider additional validation on problem slices".to_string(),
"Monitor performance closely after deployment".to_string(),
],
};
GateCheckSummary {
total_requirements,
passed_requirements,
failed_requirements,
warning_requirements,
promotion_decision,
next_steps,
}
}
async fn save_gate_check_result(&self, result: &GateCheckResult) -> Result<()> {
let artifact_dir = std::path::Path::new("artifact").join("gates");
tokio::fs::create_dir_all(&artifact_dir).await
.context("Failed to create gates artifact directory")?;
let filename = format!("gate_check_{}.json", result.gate_id);
let filepath = artifact_dir.join(&filename);
let content = serde_json::to_string_pretty(result)
.context("Failed to serialize gate check result")?;
tokio::fs::write(&filepath, content).await
.context("Failed to write gate check result")?;
info!("Saved gate check result: {}", filepath.display());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[tokio::test]
async fn test_gate_checker_creation() {
let gate_checker = GateChecker::new().unwrap();
assert!(!gate_checker.gate_id.is_empty());
}
#[test]
fn test_overall_status_calculation() {
let gate_checker = GateChecker::new().unwrap();
let requirements = vec![
GateRequirement {
requirement_id: "test1".to_string(),
description: "Test 1".to_string(),
status: GateStatus::Pass,
measured_value: Some(1.0),
threshold: 1.0,
threshold_operator: ThresholdOperator::GreaterThanOrEqual,
details: vec![],
},
GateRequirement {
requirement_id: "test2".to_string(),
description: "Test 2".to_string(),
status: GateStatus::Fail,
measured_value: Some(0.5),
threshold: 1.0,
threshold_operator: ThresholdOperator::GreaterThanOrEqual,
details: vec![],
},
];
let overall_status = gate_checker.calculate_overall_status(&requirements);
assert_eq!(overall_status, GateStatus::Fail);
}
}