use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::domain::{AivcsError, Result};
use oxidized_state::storage_traits::ContentDigest;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QualityCheck {
Fmt,
Lint,
Test,
Verification,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QualitySeverity {
Info,
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckFinding {
pub severity: QualitySeverity,
pub message: String,
pub file_path: Option<String>,
pub line: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckResult {
pub check: QualityCheck,
pub passed: bool,
pub findings: Vec<CheckFinding>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReleaseAction {
Promote,
Publish,
}
impl ReleaseAction {
fn is_high_risk(self) -> bool {
matches!(self, Self::Publish)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuardrailPolicyProfile {
pub name: &'static str,
pub required_checks: Vec<QualityCheck>,
pub block_on_severity: QualitySeverity,
}
impl GuardrailPolicyProfile {
pub fn standard() -> Self {
Self {
name: "standard",
required_checks: vec![QualityCheck::Fmt, QualityCheck::Lint, QualityCheck::Test],
block_on_severity: QualitySeverity::High,
}
}
pub fn strict() -> Self {
Self {
name: "strict",
required_checks: vec![
QualityCheck::Fmt,
QualityCheck::Lint,
QualityCheck::Test,
QualityCheck::Verification,
],
block_on_severity: QualitySeverity::Medium,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuardrailCoverage {
pub required_checks: usize,
pub executed_required_checks: usize,
pub passed_required_checks: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuardrailVerdict {
pub passed: bool,
pub blocked_checks: Vec<QualityCheck>,
pub missing_required_checks: Vec<QualityCheck>,
pub blocking_findings: Vec<CheckFinding>,
pub requires_approval: bool,
pub coverage: GuardrailCoverage,
pub evaluated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuardrailArtifact {
pub run_id: String,
pub profile_name: String,
pub check_results: Vec<CheckResult>,
pub verdict: GuardrailVerdict,
}
pub fn evaluate_quality_guardrails(
profile: &GuardrailPolicyProfile,
results: &[CheckResult],
action: ReleaseAction,
explicit_approval: bool,
) -> GuardrailVerdict {
let mut by_check: HashMap<QualityCheck, &CheckResult> = HashMap::new();
for r in results {
by_check.insert(r.check, r);
}
let mut blocked_checks = Vec::new();
let mut missing_required_checks = Vec::new();
let mut blocking_findings = Vec::new();
let mut executed_required_checks = 0usize;
let mut passed_required_checks = 0usize;
for required in &profile.required_checks {
match by_check.get(required) {
None => missing_required_checks.push(*required),
Some(result) => {
executed_required_checks += 1;
if result.passed {
passed_required_checks += 1;
} else {
blocked_checks.push(*required);
}
for f in &result.findings {
if f.severity >= profile.block_on_severity {
blocking_findings.push(f.clone());
}
}
}
}
}
let mut seen = HashSet::new();
blocked_checks.retain(|c| seen.insert(*c));
let requires_approval = action.is_high_risk() && !explicit_approval;
let passed = blocked_checks.is_empty()
&& missing_required_checks.is_empty()
&& blocking_findings.is_empty()
&& !requires_approval;
GuardrailVerdict {
passed,
blocked_checks,
missing_required_checks,
blocking_findings,
requires_approval,
coverage: GuardrailCoverage {
required_checks: profile.required_checks.len(),
executed_required_checks,
passed_required_checks,
},
evaluated_at: Utc::now(),
}
}
pub fn release_block_reason(verdict: &GuardrailVerdict) -> Option<String> {
if verdict.passed {
return None;
}
if verdict.requires_approval {
return Some("high-risk action requires explicit approval".to_string());
}
if !verdict.missing_required_checks.is_empty() {
return Some("required checks missing".to_string());
}
if !verdict.blocked_checks.is_empty() {
return Some("required checks failed".to_string());
}
if !verdict.blocking_findings.is_empty() {
return Some("blocking findings present".to_string());
}
Some("quality guardrail blocked".to_string())
}
pub fn write_guardrail_artifact(artifact: &GuardrailArtifact, dir: &Path) -> Result<PathBuf> {
let run_dir = dir.join(&artifact.run_id);
std::fs::create_dir_all(&run_dir)?;
let path = run_dir.join("guardrails.json");
let digest_path = run_dir.join("guardrails.digest");
let json = serde_json::to_vec_pretty(artifact)?;
let digest = ContentDigest::from_bytes(&json).as_str().to_string();
std::fs::write(&path, &json)?;
std::fs::write(&digest_path, digest.as_bytes())?;
Ok(path)
}
pub fn read_guardrail_artifact(run_id: &str, dir: &Path) -> Result<GuardrailArtifact> {
let run_dir = dir.join(run_id);
let path = run_dir.join("guardrails.json");
let digest_path = run_dir.join("guardrails.digest");
let json = std::fs::read(&path)?;
let digest = std::fs::read_to_string(&digest_path)?;
let actual = ContentDigest::from_bytes(&json).as_str().to_string();
if digest.trim() != actual {
return Err(AivcsError::DigestMismatch {
expected: digest.trim().to_string(),
actual,
});
}
let artifact: GuardrailArtifact = serde_json::from_slice(&json)?;
Ok(artifact)
}