1use std::collections::{HashMap, HashSet};
4use std::path::{Path, PathBuf};
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::domain::{AivcsError, Result};
10use oxidized_state::storage_traits::ContentDigest;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum QualityCheck {
16 Fmt,
17 Lint,
18 Test,
19 Verification,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum QualitySeverity {
26 Info,
27 Low,
28 Medium,
29 High,
30 Critical,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct CheckFinding {
36 pub severity: QualitySeverity,
37 pub message: String,
38 pub file_path: Option<String>,
39 pub line: Option<u32>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct CheckResult {
45 pub check: QualityCheck,
46 pub passed: bool,
47 pub findings: Vec<CheckFinding>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ReleaseAction {
54 Promote,
55 Publish,
56}
57
58impl ReleaseAction {
59 fn is_high_risk(self) -> bool {
60 matches!(self, Self::Publish)
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct GuardrailPolicyProfile {
67 pub name: &'static str,
68 pub required_checks: Vec<QualityCheck>,
69 pub block_on_severity: QualitySeverity,
70}
71
72impl GuardrailPolicyProfile {
73 pub fn standard() -> Self {
74 Self {
75 name: "standard",
76 required_checks: vec![QualityCheck::Fmt, QualityCheck::Lint, QualityCheck::Test],
77 block_on_severity: QualitySeverity::High,
78 }
79 }
80
81 pub fn strict() -> Self {
82 Self {
83 name: "strict",
84 required_checks: vec![
85 QualityCheck::Fmt,
86 QualityCheck::Lint,
87 QualityCheck::Test,
88 QualityCheck::Verification,
89 ],
90 block_on_severity: QualitySeverity::Medium,
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct GuardrailCoverage {
98 pub required_checks: usize,
99 pub executed_required_checks: usize,
100 pub passed_required_checks: usize,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct GuardrailVerdict {
106 pub passed: bool,
107 pub blocked_checks: Vec<QualityCheck>,
108 pub missing_required_checks: Vec<QualityCheck>,
109 pub blocking_findings: Vec<CheckFinding>,
110 pub requires_approval: bool,
111 pub coverage: GuardrailCoverage,
112 pub evaluated_at: DateTime<Utc>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct GuardrailArtifact {
118 pub run_id: String,
119 pub profile_name: String,
120 pub check_results: Vec<CheckResult>,
121 pub verdict: GuardrailVerdict,
122}
123
124pub fn evaluate_quality_guardrails(
126 profile: &GuardrailPolicyProfile,
127 results: &[CheckResult],
128 action: ReleaseAction,
129 explicit_approval: bool,
130) -> GuardrailVerdict {
131 let mut by_check: HashMap<QualityCheck, &CheckResult> = HashMap::new();
132 for r in results {
133 by_check.insert(r.check, r);
134 }
135
136 let mut blocked_checks = Vec::new();
137 let mut missing_required_checks = Vec::new();
138 let mut blocking_findings = Vec::new();
139 let mut executed_required_checks = 0usize;
140 let mut passed_required_checks = 0usize;
141
142 for required in &profile.required_checks {
143 match by_check.get(required) {
144 None => missing_required_checks.push(*required),
145 Some(result) => {
146 executed_required_checks += 1;
147 if result.passed {
148 passed_required_checks += 1;
149 } else {
150 blocked_checks.push(*required);
151 }
152
153 for f in &result.findings {
154 if f.severity >= profile.block_on_severity {
155 blocking_findings.push(f.clone());
156 }
157 }
158 }
159 }
160 }
161
162 let mut seen = HashSet::new();
164 blocked_checks.retain(|c| seen.insert(*c));
165
166 let requires_approval = action.is_high_risk() && !explicit_approval;
167 let passed = blocked_checks.is_empty()
168 && missing_required_checks.is_empty()
169 && blocking_findings.is_empty()
170 && !requires_approval;
171
172 GuardrailVerdict {
173 passed,
174 blocked_checks,
175 missing_required_checks,
176 blocking_findings,
177 requires_approval,
178 coverage: GuardrailCoverage {
179 required_checks: profile.required_checks.len(),
180 executed_required_checks,
181 passed_required_checks,
182 },
183 evaluated_at: Utc::now(),
184 }
185}
186
187pub fn release_block_reason(verdict: &GuardrailVerdict) -> Option<String> {
189 if verdict.passed {
190 return None;
191 }
192 if verdict.requires_approval {
193 return Some("high-risk action requires explicit approval".to_string());
194 }
195 if !verdict.missing_required_checks.is_empty() {
196 return Some("required checks missing".to_string());
197 }
198 if !verdict.blocked_checks.is_empty() {
199 return Some("required checks failed".to_string());
200 }
201 if !verdict.blocking_findings.is_empty() {
202 return Some("blocking findings present".to_string());
203 }
204 Some("quality guardrail blocked".to_string())
205}
206
207pub fn write_guardrail_artifact(artifact: &GuardrailArtifact, dir: &Path) -> Result<PathBuf> {
209 let run_dir = dir.join(&artifact.run_id);
210 std::fs::create_dir_all(&run_dir)?;
211
212 let path = run_dir.join("guardrails.json");
213 let digest_path = run_dir.join("guardrails.digest");
214 let json = serde_json::to_vec_pretty(artifact)?;
215 let digest = ContentDigest::from_bytes(&json).as_str().to_string();
216
217 std::fs::write(&path, &json)?;
218 std::fs::write(&digest_path, digest.as_bytes())?;
219
220 Ok(path)
221}
222
223pub fn read_guardrail_artifact(run_id: &str, dir: &Path) -> Result<GuardrailArtifact> {
225 let run_dir = dir.join(run_id);
226 let path = run_dir.join("guardrails.json");
227 let digest_path = run_dir.join("guardrails.digest");
228
229 let json = std::fs::read(&path)?;
230 let digest = std::fs::read_to_string(&digest_path)?;
231 let actual = ContentDigest::from_bytes(&json).as_str().to_string();
232 if digest.trim() != actual {
233 return Err(AivcsError::DigestMismatch {
234 expected: digest.trim().to_string(),
235 actual,
236 });
237 }
238 let artifact: GuardrailArtifact = serde_json::from_slice(&json)?;
239 Ok(artifact)
240}