use super::super::*;
use super::{diff_item, duplicate_evidence_groups, finding};
use std::collections::{BTreeMap, BTreeSet};
pub(super) fn compare_artifacts(
plan: &DeploymentPlanV1,
inventory: &DeploymentInventoryV1,
artifact_diff: &mut Vec<DiffItemV1>,
hard_failures: &mut Vec<SafetyFindingV1>,
warnings: &mut Vec<SafetyFindingV1>,
) {
let planned_conflicting_roles =
compare_planned_artifact_role_conflicts(plan, artifact_diff, hard_failures, warnings);
let conflicting_roles =
compare_observed_artifact_role_conflicts(inventory, artifact_diff, hard_failures, warnings);
let mut observed_by_role = BTreeMap::new();
for artifact in &inventory.observed_artifacts {
if conflicting_roles.contains(&artifact.role) {
continue;
}
observed_by_role
.entry(artifact.role.as_str())
.or_insert(artifact);
}
let mut compared_roles = BTreeSet::new();
for expected in &plan.role_artifacts {
if planned_conflicting_roles.contains(&expected.role)
|| conflicting_roles.contains(&expected.role)
|| !compared_roles.insert(expected.role.as_str())
{
continue;
}
let Some(observed) = observed_by_role.get(expected.role.as_str()) else {
record_missing_artifact(expected, artifact_diff, hard_failures);
continue;
};
compare_artifact_file_sha256(expected, observed, artifact_diff, hard_failures);
compare_artifact_payload_sha256(expected, observed, artifact_diff, hard_failures, warnings);
}
}
fn compare_planned_artifact_role_conflicts(
plan: &DeploymentPlanV1,
artifact_diff: &mut Vec<DiffItemV1>,
hard_failures: &mut Vec<SafetyFindingV1>,
warnings: &mut Vec<SafetyFindingV1>,
) -> BTreeSet<String> {
let mut conflicting_roles = BTreeSet::new();
for group in duplicate_evidence_groups(
&plan.role_artifacts,
|planned| planned.role.as_str().to_string(),
planned_artifact_evidence_label,
" | ",
) {
if group.is_conflict {
conflicting_roles.insert(group.subject.clone());
artifact_diff.push(diff_item(
"planned_artifact_role_conflict",
&group.subject,
Some("one planned artifact".to_string()),
Some(group.evidence_label.clone()),
SafetySeverityV1::HardFailure,
));
hard_failures.push(finding(
"planned_artifact_role_conflict",
format!(
"planned artifact role {} has conflicting evidence: {}",
group.subject, group.evidence_label
),
SafetySeverityV1::HardFailure,
Some(group.subject),
));
} else {
artifact_diff.push(diff_item(
"planned_artifact_duplicate",
&group.subject,
Some(group.evidence_label.clone()),
Some(group.count.to_string()),
SafetySeverityV1::Warning,
));
warnings.push(finding(
"duplicate_planned_artifact_role",
format!(
"planned artifact role {} was declared {} times with identical evidence",
group.subject, group.count
),
SafetySeverityV1::Warning,
Some(group.subject),
));
}
}
conflicting_roles
}
fn planned_artifact_evidence_label(planned: &RoleArtifactV1) -> String {
format!(
"wasm_gz_path={};wasm_gz={};file={};module={};raw_config={};canonical={}",
planned.wasm_gz_path.as_deref().unwrap_or("<none>"),
planned.wasm_gz_sha256.as_deref().unwrap_or("<none>"),
planned
.observed_wasm_gz_file_sha256
.as_deref()
.unwrap_or("<none>"),
planned.installed_module_hash.as_deref().unwrap_or("<none>"),
planned.raw_config_sha256.as_deref().unwrap_or("<none>"),
planned
.canonical_embedded_config_sha256
.as_deref()
.unwrap_or("<none>")
)
}
fn compare_observed_artifact_role_conflicts(
inventory: &DeploymentInventoryV1,
artifact_diff: &mut Vec<DiffItemV1>,
hard_failures: &mut Vec<SafetyFindingV1>,
warnings: &mut Vec<SafetyFindingV1>,
) -> BTreeSet<String> {
let mut conflicting_roles = BTreeSet::new();
for group in duplicate_evidence_groups(
&inventory.observed_artifacts,
|observed| observed.role.as_str().to_string(),
observed_artifact_evidence_label,
" | ",
) {
if group.is_conflict {
conflicting_roles.insert(group.subject.clone());
artifact_diff.push(diff_item(
"artifact_role_conflict",
&group.subject,
Some("one artifact observation".to_string()),
Some(group.evidence_label.clone()),
SafetySeverityV1::HardFailure,
));
hard_failures.push(finding(
"artifact_role_conflict",
format!(
"observed artifact role {} has conflicting evidence: {}",
group.subject, group.evidence_label
),
SafetySeverityV1::HardFailure,
Some(group.subject),
));
} else {
artifact_diff.push(diff_item(
"artifact_duplicate",
&group.subject,
Some(group.evidence_label.clone()),
Some(group.count.to_string()),
SafetySeverityV1::Warning,
));
warnings.push(finding(
"duplicate_artifact_observed",
format!(
"observed artifact role {} was reported {} times with identical evidence",
group.subject, group.count
),
SafetySeverityV1::Warning,
Some(group.subject),
));
}
}
conflicting_roles
}
fn observed_artifact_evidence_label(observed: &ObservedArtifactV1) -> String {
format!(
"path={};file={};payload={};size={};source={:?}",
observed.artifact_path,
observed.file_sha256.as_deref().unwrap_or("<none>"),
observed.payload_sha256.as_deref().unwrap_or("<none>"),
observed
.payload_size_bytes
.map_or_else(|| "<none>".to_string(), |size| size.to_string()),
observed.source
)
}
fn record_missing_artifact(
expected: &RoleArtifactV1,
artifact_diff: &mut Vec<DiffItemV1>,
hard_failures: &mut Vec<SafetyFindingV1>,
) {
artifact_diff.push(diff_item(
"artifact",
&expected.role,
expected.wasm_gz_path.clone(),
None,
SafetySeverityV1::HardFailure,
));
hard_failures.push(finding(
"artifact_missing",
format!("missing observed artifact for role {}", expected.role),
SafetySeverityV1::HardFailure,
Some(expected.role.clone()),
));
}
fn compare_artifact_file_sha256(
expected: &RoleArtifactV1,
observed: &ObservedArtifactV1,
artifact_diff: &mut Vec<DiffItemV1>,
hard_failures: &mut Vec<SafetyFindingV1>,
) {
match (
expected.observed_wasm_gz_file_sha256.as_ref(),
observed.file_sha256.as_ref(),
) {
(Some(want), Some(got)) if want != got => {
artifact_diff.push(diff_item(
"artifact_file_sha256",
&expected.role,
Some(want.clone()),
Some(got.clone()),
SafetySeverityV1::HardFailure,
));
hard_failures.push(finding(
"artifact_file_digest_mismatch",
format!(
"observed artifact file digest changed during deployment truth check for role {}",
expected.role
),
SafetySeverityV1::HardFailure,
Some(expected.role.clone()),
));
}
(_, Some(got)) => {
artifact_diff.push(diff_item(
"artifact_file_sha256",
&expected.role,
expected.observed_wasm_gz_file_sha256.clone(),
Some(got.clone()),
SafetySeverityV1::Info,
));
}
_ => {}
}
}
fn compare_artifact_payload_sha256(
expected: &RoleArtifactV1,
observed: &ObservedArtifactV1,
artifact_diff: &mut Vec<DiffItemV1>,
hard_failures: &mut Vec<SafetyFindingV1>,
warnings: &mut Vec<SafetyFindingV1>,
) {
match (
expected.wasm_gz_sha256.as_ref(),
observed.payload_sha256.as_ref(),
) {
(Some(want), Some(got)) if want != got => {
artifact_diff.push(diff_item(
"artifact_sha256",
&expected.role,
Some(want.clone()),
Some(got.clone()),
SafetySeverityV1::HardFailure,
));
hard_failures.push(finding(
"artifact_digest_mismatch",
format!("artifact digest mismatch for role {}", expected.role),
SafetySeverityV1::HardFailure,
Some(expected.role.clone()),
));
}
(Some(want), None) => warnings.push(finding(
"artifact_digest_unobserved",
format!(
"expected artifact digest {want} for role {} was not observed",
expected.role
),
SafetySeverityV1::Warning,
Some(expected.role.clone()),
)),
_ => {}
}
}