use super::super::super::{
digest::promotion_readiness_digest,
ensure::{ensure_readiness_field, ensure_readiness_optional_sha256, ensure_readiness_sha256},
error::PromotionReadinessError,
};
use crate::deployment_truth::{
DEPLOYMENT_TRUTH_SCHEMA_VERSION, PromotionReadinessStatusV1, PromotionReadinessV1,
RolePromotionReadinessV1, SafetyFindingV1, SafetySeverityV1,
};
pub fn validate_promotion_readiness(
readiness: &PromotionReadinessV1,
) -> Result<(), PromotionReadinessError> {
if readiness.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
return Err(PromotionReadinessError::SchemaVersionMismatch {
expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
found: readiness.schema_version,
});
}
ensure_readiness_field("readiness_id", &readiness.readiness_id)?;
ensure_readiness_sha256(
"promotion_readiness_digest",
&readiness.promotion_readiness_digest,
)?;
ensure_readiness_field("target_plan_id", &readiness.target_plan_id)?;
ensure_readiness_status_matches_blockers(readiness)?;
ensure_unique_readiness_roles(&readiness.roles)?;
for role in &readiness.roles {
validate_role_readiness(role)?;
}
validate_readiness_findings(
"blockers",
&readiness.blockers,
SafetySeverityV1::HardFailure,
)?;
validate_readiness_findings("warnings", &readiness.warnings, SafetySeverityV1::Warning)?;
if readiness.promotion_readiness_digest != promotion_readiness_digest(readiness) {
return Err(PromotionReadinessError::LinkageMismatch {
field: "promotion_readiness_digest",
});
}
Ok(())
}
fn validate_role_readiness(role: &RolePromotionReadinessV1) -> Result<(), PromotionReadinessError> {
ensure_readiness_field("role", &role.role)?;
ensure_readiness_optional_sha256("source_wasm_sha256", role.source_wasm_sha256.as_deref())?;
ensure_readiness_optional_sha256(
"source_wasm_gz_sha256",
role.source_wasm_gz_sha256.as_deref(),
)?;
ensure_readiness_optional_sha256("target_wasm_sha256", role.target_wasm_sha256.as_deref())?;
ensure_readiness_optional_sha256(
"target_wasm_gz_sha256",
role.target_wasm_gz_sha256.as_deref(),
)?;
ensure_readiness_optional_sha256(
"source_canonical_embedded_config_sha256",
role.source_canonical_embedded_config_sha256.as_deref(),
)?;
ensure_readiness_optional_sha256(
"target_canonical_embedded_config_sha256",
role.target_canonical_embedded_config_sha256.as_deref(),
)?;
if role.restage_required != (role.target_store_has_artifact == Some(false)) {
return Err(PromotionReadinessError::RestageStateMismatch {
role: role.role.clone(),
});
}
Ok(())
}
const fn ensure_readiness_status_matches_blockers(
readiness: &PromotionReadinessV1,
) -> Result<(), PromotionReadinessError> {
match (readiness.status, readiness.blockers.is_empty()) {
(PromotionReadinessStatusV1::Ready, false)
| (PromotionReadinessStatusV1::Blocked, true) => {
Err(PromotionReadinessError::StatusBlockerMismatch {
status: readiness.status,
blocker_count: readiness.blockers.len(),
})
}
_ => Ok(()),
}
}
fn ensure_unique_readiness_roles(
roles: &[RolePromotionReadinessV1],
) -> Result<(), PromotionReadinessError> {
let mut seen = std::collections::BTreeSet::new();
for role in roles {
if !seen.insert(role.role.as_str()) {
return Err(PromotionReadinessError::DuplicateRole {
role: role.role.clone(),
});
}
}
Ok(())
}
fn validate_readiness_findings(
field: &'static str,
findings: &[SafetyFindingV1],
expected_severity: SafetySeverityV1,
) -> Result<(), PromotionReadinessError> {
for finding in findings {
ensure_readiness_field("finding.code", &finding.code)?;
ensure_readiness_field("finding.message", &finding.message)?;
if finding.severity != expected_severity {
return Err(PromotionReadinessError::FindingSeverityMismatch {
field,
severity: finding.severity,
});
}
}
Ok(())
}