Skip to main content

canic_host/deployment_truth/report/
mod.rs

1use super::*;
2use std::collections::{BTreeMap, BTreeSet};
3
4mod artifacts;
5mod canisters;
6mod config_digests;
7mod controllers;
8mod module_hashes;
9mod pools;
10mod receipt_resume;
11mod root_subnet;
12mod safety;
13mod verifier_readiness;
14
15use artifacts::compare_artifacts;
16#[cfg(test)]
17pub(super) use artifacts::{
18    ARTIFACT_DUPLICATE_DIFF_CATEGORY, ARTIFACT_FILE_DIGEST_MISMATCH_CODE,
19    ARTIFACT_FILE_SHA256_DIFF_CATEGORY, ARTIFACT_ROLE_CONFLICT_DIFF_CATEGORY,
20    DUPLICATE_ARTIFACT_OBSERVED_CODE, DUPLICATE_PLANNED_ARTIFACT_ROLE_CODE,
21    PLANNED_ARTIFACT_DUPLICATE_DIFF_CATEGORY, PLANNED_ARTIFACT_ROLE_CONFLICT_DIFF_CATEGORY,
22};
23pub(in crate::deployment_truth) use artifacts::{
24    ARTIFACT_MISSING_CODE, is_artifact_role_failure_code,
25};
26pub(super) use artifacts::{ARTIFACT_ROLE_CONFLICT_CODE, PLANNED_ARTIFACT_ROLE_CONFLICT_CODE};
27#[cfg(test)]
28pub(super) use canisters::{
29    CANISTER_DUPLICATE_DIFF_CATEGORY, CANISTER_EXTRA_DIFF_CATEGORY,
30    CANISTER_ID_ROLE_CONFLICT_DIFF_CATEGORY, CANISTER_ROLE_AMBIGUOUS_CODE,
31    CANISTER_ROLE_AMBIGUOUS_DIFF_CATEGORY, CANISTER_ROLE_MISMATCH_CODE, CANISTER_UNOBSERVED_CODE,
32    COMPONENT_REGISTRY_ROLE_MISSING_CODE, DUPLICATE_CANISTER_OBSERVED_CODE,
33    DUPLICATE_PLANNED_CANISTER_ROLE_CODE, EXTRA_CANISTER_OBSERVED_CODE,
34    PLANNED_CANISTER_DUPLICATE_DIFF_CATEGORY, PLANNED_CANISTER_ID_CONFLICT_DIFF_CATEGORY,
35    PLANNED_CANISTER_ROLE_CONFLICT_DIFF_CATEGORY, ROLE_MISMATCH_DIFF_CATEGORY,
36    UNSAFE_CONTROL_CLASS_CODE,
37};
38pub(super) use canisters::{
39    CANISTER_ID_ROLE_CONFLICT_CODE, PLANNED_CANISTER_ID_CONFLICT_CODE,
40    PLANNED_CANISTER_ROLE_CONFLICT_CODE,
41};
42use canisters::{compare_canisters, compare_observed_canister_id_conflicts};
43#[cfg(test)]
44pub(super) use config_digests::{RAW_CONFIG_DIGEST_MISMATCH_CODE, RAW_CONFIG_SHA256_DIFF_CATEGORY};
45use config_digests::{compare_embedded_config, compare_raw_config};
46use controllers::compare_authority_profile;
47#[cfg(test)]
48pub(super) use controllers::{
49    CONTROLLER_AUTHORITY_OVERLAP_CODE, CONTROLLER_EXTRA_DIFF_CATEGORY,
50    CONTROLLER_MISSING_DIFF_CATEGORY, CONTROLLERS_UNOBSERVED_CODE,
51    EXPECTED_CONTROLLER_MISSING_CODE, EXTRA_CONTROLLER_OBSERVED_CODE,
52};
53use module_hashes::compare_module_hashes;
54#[cfg(test)]
55pub(super) use module_hashes::{
56    INSTALLED_MODULE_HASH_AMBIGUOUS_CODE, INSTALLED_MODULE_HASH_AMBIGUOUS_DIFF_CATEGORY,
57    INSTALLED_MODULE_HASH_DIFF_CATEGORY, INSTALLED_MODULE_HASH_MISMATCH_CODE,
58};
59pub(super) use pools::{
60    CANISTER_POOL_ROLE_CONFLICT_CODE, PLANNED_POOL_CONFLICT_CODE, PLANNED_POOL_ID_CONFLICT_CODE,
61    POOL_CANISTER_ID_CONFLICT_CODE,
62};
63#[cfg(test)]
64pub(super) use pools::{
65    CANISTER_POOL_ROLE_CONFLICT_DIFF_CATEGORY, DUPLICATE_PLANNED_POOL_CODE,
66    DUPLICATE_POOL_CANISTER_OBSERVED_CODE, EXTRA_POOL_CANISTER_OBSERVED_CODE,
67    PLANNED_POOL_CONFLICT_DIFF_CATEGORY, PLANNED_POOL_DUPLICATE_DIFF_CATEGORY,
68    PLANNED_POOL_ID_CONFLICT_DIFF_CATEGORY, POOL_CANISTER_DIFF_CATEGORY,
69    POOL_CANISTER_DUPLICATE_DIFF_CATEGORY, POOL_CANISTER_ID_CONFLICT_DIFF_CATEGORY,
70    POOL_CANISTER_ID_DIFF_CATEGORY, POOL_CANISTER_ID_MISMATCH_CODE, POOL_CANISTER_MISSING_CODE,
71    POOL_CONTROL_CLASS_DIFF_CATEGORY, POOL_EXTRA_DIFF_CATEGORY, UNSAFE_POOL_CONTROL_CLASS_CODE,
72};
73use pools::{compare_observed_canister_pool_role_conflicts, compare_pools};
74pub use receipt_resume::compare_plan_inventory_and_receipt;
75#[cfg(test)]
76pub(super) use receipt_resume::{
77    DUPLICATE_RECEIPT_PHASE_CODE, DUPLICATE_RECEIPT_ROLE_PHASE_CODE,
78    RECEIPT_EXECUTION_STATUS_MISMATCH_CODE, RECEIPT_PLAN_MISMATCH_CODE,
79    RECEIPT_POSTCONDITION_UNVERIFIED_CODE,
80};
81pub(super) use receipt_resume::{RECEIPT_PHASE_CONFLICT_CODE, RECEIPT_ROLE_PHASE_CONFLICT_CODE};
82#[cfg(test)]
83pub(super) use root_subnet::ROOT_AUTH_CLOUD_ENGINE_SUBNET_CODE;
84pub(super) use root_subnet::apply_root_auth_signer_subnet_check;
85#[cfg(test)]
86pub(super) use root_subnet::{
87    RootSubnetEvidence, RootSubnetEvidenceSource, apply_root_auth_signer_subnet_check_with_source,
88};
89pub use safety::safety_report_from_diff;
90pub(in crate::deployment_truth::report) use safety::{resume_safety_reasons, safety_status};
91use verifier_readiness::compare_verifier_readiness;
92#[cfg(test)]
93pub(super) use verifier_readiness::{
94    DUPLICATE_PLANNED_VERIFIER_ROLE_EPOCH_CODE, DUPLICATE_VERIFIER_ROLE_EPOCH_OBSERVED_CODE,
95    PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_DIFF_CATEGORY,
96    PLANNED_VERIFIER_ROLE_EPOCH_DUPLICATE_DIFF_CATEGORY, VERIFIER_NOT_OBSERVED_LABEL,
97    VERIFIER_ROLE_EPOCH_CONFLICT_DIFF_CATEGORY, VERIFIER_ROLE_EPOCH_DIFF_CATEGORY,
98    VERIFIER_ROLE_EPOCH_DUPLICATE_DIFF_CATEGORY, VERIFIER_ROLE_EPOCH_STALE_CODE,
99    VERIFIER_ROLE_EPOCH_UNOBSERVED_CODE,
100};
101pub(super) use verifier_readiness::{
102    PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_CODE, VERIFIER_ROLE_EPOCH_CONFLICT_CODE,
103};
104
105pub(in crate::deployment_truth) const DEPLOYMENT_MANIFEST_MISMATCH_CODE: &str =
106    "deployment_manifest_mismatch";
107pub(in crate::deployment_truth) const OBSERVATION_GAP_CODE: &str = "observation_gap";
108pub(in crate::deployment_truth) const PLAN_ASSUMPTION_CODE: &str = "plan_assumption";
109const IDENTITY_UNOBSERVED_CODE: &str = "identity_unobserved";
110const ENVIRONMENT_MISMATCH_CODE: &str = "environment_mismatch";
111pub(in crate::deployment_truth) const FLEET_IDENTITY_MISMATCH_CODE: &str =
112    "fleet_identity_mismatch";
113const ROOT_TRUST_ANCHOR_MISMATCH_CODE: &str = "root_trust_anchor_mismatch";
114const DEPLOYMENT_MANIFEST_UNOBSERVED_CODE: &str = "deployment_manifest_unobserved";
115
116#[must_use]
117pub fn is_evidence_conflict_finding_code(code: &str) -> bool {
118    matches!(
119        code,
120        PLANNED_ARTIFACT_ROLE_CONFLICT_CODE
121            | ARTIFACT_ROLE_CONFLICT_CODE
122            | CANISTER_ID_ROLE_CONFLICT_CODE
123            | PLANNED_CANISTER_ROLE_CONFLICT_CODE
124            | PLANNED_CANISTER_ID_CONFLICT_CODE
125            | CANISTER_POOL_ROLE_CONFLICT_CODE
126            | PLANNED_POOL_CONFLICT_CODE
127            | PLANNED_POOL_ID_CONFLICT_CODE
128            | POOL_CANISTER_ID_CONFLICT_CODE
129            | RECEIPT_PHASE_CONFLICT_CODE
130            | RECEIPT_ROLE_PHASE_CONFLICT_CODE
131            | PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_CODE
132            | VERIFIER_ROLE_EPOCH_CONFLICT_CODE
133    )
134}
135
136///
137/// DuplicateEvidenceGroup
138///
139struct DuplicateEvidenceGroup {
140    subject: String,
141    count: usize,
142    evidence_label: String,
143    is_conflict: bool,
144}
145
146///
147/// LocalDeploymentCheckRequest
148///
149#[derive(Clone, Debug, Eq, PartialEq)]
150pub struct LocalDeploymentCheckRequest {
151    pub fleet_name: String,
152    pub app: String,
153    pub environment: String,
154    pub artifact_environment: String,
155    pub workspace_root: std::path::PathBuf,
156    pub icp_root: std::path::PathBuf,
157    pub config_path: Option<std::path::PathBuf>,
158    pub observed_at: String,
159    pub runtime_variant: String,
160    pub build_profile: String,
161}
162
163/// Build local plan and inventory, then return the passive safety check bundle.
164pub fn check_local_deployment(
165    request: &LocalDeploymentCheckRequest,
166) -> Result<DeploymentCheckV1, DeploymentTruthError> {
167    let artifact_root =
168        crate::release_set::artifact_root_path(&request.icp_root, &request.artifact_environment);
169    check_local_deployment_at_root(request, &artifact_root)
170}
171
172pub fn check_local_deployment_at_root(
173    request: &LocalDeploymentCheckRequest,
174    artifact_root: &std::path::Path,
175) -> Result<DeploymentCheckV1, DeploymentTruthError> {
176    let plan = build_local_deployment_plan_at_root(
177        &LocalDeploymentPlanRequest {
178            fleet_name: request.fleet_name.clone(),
179            app: request.app.clone(),
180            environment: request.environment.clone(),
181            artifact_environment: request.artifact_environment.clone(),
182            workspace_root: request.workspace_root.clone(),
183            icp_root: request.icp_root.clone(),
184            config_path: request.config_path.clone(),
185            runtime_variant: request.runtime_variant.clone(),
186            build_profile: request.build_profile.clone(),
187        },
188        artifact_root,
189    );
190    let inventory = collect_local_deployment_inventory_at_root(
191        &LocalInventoryRequest {
192            fleet_name: request.fleet_name.clone(),
193            environment: request.environment.clone(),
194            artifact_environment: request.artifact_environment.clone(),
195            workspace_root: request.workspace_root.clone(),
196            icp_root: request.icp_root.clone(),
197            config_path: request.config_path.clone(),
198            observed_at: request.observed_at.clone(),
199        },
200        artifact_root,
201    )?;
202    let mut diff = compare_plan_to_inventory(&plan, &inventory);
203    apply_root_auth_signer_subnet_check(
204        &mut diff,
205        &inventory,
206        &request.environment,
207        &request.icp_root,
208    );
209    let report = safety_report_from_diff(
210        format!(
211            "local:{}:{}:report",
212            request.environment, request.fleet_name
213        ),
214        Some(format!(
215            "local:{}:{}:diff",
216            request.environment, request.fleet_name
217        )),
218        &diff,
219    );
220
221    Ok(DeploymentCheckV1 {
222        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
223        check_id: format!("local:{}:{}:check", request.environment, request.fleet_name),
224        plan,
225        inventory,
226        diff,
227        report,
228    })
229}
230
231fn refresh_resume_safety(diff: &mut DeploymentDiffV1) {
232    diff.resume_safety.status = safety_status(&diff.hard_failures, &diff.warnings);
233    diff.resume_safety.reasons = resume_safety_reasons(&diff.hard_failures, &diff.warnings);
234}
235
236/// Compare intended deployment state with observed inventory into a machine diff.
237#[must_use]
238pub fn compare_plan_to_inventory(
239    plan: &DeploymentPlanV1,
240    inventory: &DeploymentInventoryV1,
241) -> DeploymentDiffV1 {
242    let mut artifact_diff = Vec::new();
243    let mut controller_diff = Vec::new();
244    let mut pool_diff = Vec::new();
245    let mut embedded_config_diff = Vec::new();
246    let mut module_hash_diff = Vec::new();
247    let mut verifier_readiness_diff = Vec::new();
248    let mut hard_failures = Vec::new();
249    let mut warnings = Vec::new();
250
251    compare_identity(plan, inventory, &mut hard_failures);
252    compare_authority_profile(plan, &mut controller_diff, &mut hard_failures);
253    compare_artifacts(
254        plan,
255        inventory,
256        &mut artifact_diff,
257        &mut hard_failures,
258        &mut warnings,
259    );
260    compare_observed_canister_id_conflicts(
261        inventory,
262        &mut controller_diff,
263        &mut hard_failures,
264        &mut warnings,
265    );
266    compare_observed_canister_pool_role_conflicts(inventory, &mut pool_diff, &mut hard_failures);
267    compare_canisters(
268        plan,
269        inventory,
270        &mut controller_diff,
271        &mut hard_failures,
272        &mut warnings,
273    );
274    compare_pools(
275        plan,
276        inventory,
277        &mut pool_diff,
278        &mut hard_failures,
279        &mut warnings,
280    );
281    compare_module_hashes(
282        plan,
283        inventory,
284        &mut module_hash_diff,
285        &mut hard_failures,
286        &mut warnings,
287    );
288    compare_raw_config(
289        plan,
290        inventory,
291        &mut embedded_config_diff,
292        &mut hard_failures,
293    );
294    compare_embedded_config(
295        plan,
296        inventory,
297        &mut embedded_config_diff,
298        &mut hard_failures,
299        &mut warnings,
300    );
301    compare_verifier_readiness(
302        plan,
303        inventory,
304        &mut verifier_readiness_diff,
305        &mut hard_failures,
306        &mut warnings,
307    );
308    record_plan_assumptions(plan, &mut warnings);
309    for gap in &inventory.unresolved_observations {
310        warnings.push(SafetyFindingV1 {
311            code: OBSERVATION_GAP_CODE.to_string(),
312            message: gap.description.clone(),
313            severity: SafetySeverityV1::Warning,
314            subject: Some(gap.key.clone()),
315        });
316    }
317
318    let status = safety_status(&hard_failures, &warnings);
319    DeploymentDiffV1 {
320        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
321        plan_identity: plan.deployment_identity.clone(),
322        observed_identity: inventory.observed_identity.clone(),
323        artifact_diff,
324        controller_diff,
325        pool_diff,
326        embedded_config_diff,
327        module_hash_diff,
328        verifier_readiness_diff,
329        resume_safety: ResumeSafetyV1 {
330            status,
331            reasons: resume_safety_reasons(&hard_failures, &warnings),
332        },
333        hard_failures,
334        warnings,
335        resumable_phases: Vec::new(),
336    }
337}
338
339fn record_plan_assumptions(plan: &DeploymentPlanV1, warnings: &mut Vec<SafetyFindingV1>) {
340    for assumption in &plan.unresolved_assumptions {
341        warnings.push(SafetyFindingV1 {
342            code: PLAN_ASSUMPTION_CODE.to_string(),
343            message: assumption.description.clone(),
344            severity: SafetySeverityV1::Warning,
345            subject: Some(assumption.key.clone()),
346        });
347    }
348}
349
350fn compare_identity(
351    plan: &DeploymentPlanV1,
352    inventory: &DeploymentInventoryV1,
353    hard_failures: &mut Vec<SafetyFindingV1>,
354) {
355    let Some(observed) = &inventory.observed_identity else {
356        hard_failures.push(finding(
357            IDENTITY_UNOBSERVED_CODE,
358            "deployment identity was not observed",
359            SafetySeverityV1::HardFailure,
360            None,
361        ));
362        return;
363    };
364
365    if observed.environment != plan.deployment_identity.environment {
366        hard_failures.push(finding(
367            ENVIRONMENT_MISMATCH_CODE,
368            format!(
369                "plan environment {} differs from observed environment {}",
370                plan.deployment_identity.environment, observed.environment
371            ),
372            SafetySeverityV1::HardFailure,
373            Some("deployment_identity.environment".to_string()),
374        ));
375    }
376    for (field, expected, actual) in [
377        (
378            "fleet_name",
379            Some(plan.deployment_identity.fleet_name.clone()),
380            Some(observed.fleet_name.clone()),
381        ),
382        (
383            "app",
384            Some(plan.deployment_identity.app.clone()),
385            Some(observed.app.clone()),
386        ),
387        (
388            "canonical_network_id",
389            plan.deployment_identity
390                .canonical_network_id
391                .map(|value| value.to_string()),
392            observed.canonical_network_id.map(|value| value.to_string()),
393        ),
394        (
395            "fleet_id",
396            plan.deployment_identity
397                .fleet_id
398                .map(|value| value.to_string()),
399            observed.fleet_id.map(|value| value.to_string()),
400        ),
401    ] {
402        match (expected, actual) {
403            (Some(expected), Some(actual)) if expected != actual => {
404                hard_failures.push(finding(
405                    FLEET_IDENTITY_MISMATCH_CODE,
406                    format!("plan {field} {expected} differs from observed {field} {actual}"),
407                    SafetySeverityV1::HardFailure,
408                    Some(format!("deployment_identity.{field}")),
409                ));
410            }
411            (Some(expected), None) => {
412                hard_failures.push(finding(
413                    FLEET_IDENTITY_MISMATCH_CODE,
414                    format!("plan {field} {expected} was not observed"),
415                    SafetySeverityV1::HardFailure,
416                    Some(format!("deployment_identity.{field}")),
417                ));
418            }
419            _ => {}
420        }
421    }
422    if let (Some(expected), Some(actual)) = (
423        plan.deployment_identity.root_principal.as_ref(),
424        observed.root_principal.as_ref(),
425    ) && expected != actual
426    {
427        hard_failures.push(finding(
428            ROOT_TRUST_ANCHOR_MISMATCH_CODE,
429            format!("plan root {expected} differs from observed root {actual}"),
430            SafetySeverityV1::HardFailure,
431            Some("deployment_identity.root_principal".to_string()),
432        ));
433    }
434    match (
435        plan.deployment_identity.deployment_manifest_digest.as_ref(),
436        observed.deployment_manifest_digest.as_ref(),
437    ) {
438        (Some(expected), Some(actual)) if expected != actual => {
439            hard_failures.push(finding(
440                DEPLOYMENT_MANIFEST_MISMATCH_CODE,
441                "deployment manifest digest differs from the observed local config",
442                SafetySeverityV1::HardFailure,
443                Some("deployment_identity.deployment_manifest_digest".to_string()),
444            ));
445        }
446        (Some(_), None) => {
447            hard_failures.push(finding(
448                DEPLOYMENT_MANIFEST_UNOBSERVED_CODE,
449                "deployment manifest digest was not observed",
450                SafetySeverityV1::HardFailure,
451                Some("deployment_identity.deployment_manifest_digest".to_string()),
452            ));
453        }
454        _ => {}
455    }
456}
457
458fn finding(
459    code: impl Into<String>,
460    message: impl Into<String>,
461    severity: SafetySeverityV1,
462    subject: Option<String>,
463) -> SafetyFindingV1 {
464    SafetyFindingV1 {
465        code: code.into(),
466        message: message.into(),
467        severity,
468        subject,
469    }
470}
471
472fn diff_item(
473    category: impl Into<String>,
474    subject: impl Into<String>,
475    expected: Option<String>,
476    observed: Option<String>,
477    severity: SafetySeverityV1,
478) -> DiffItemV1 {
479    DiffItemV1 {
480        category: category.into(),
481        subject: subject.into(),
482        expected,
483        observed,
484        severity,
485    }
486}
487
488fn duplicate_evidence_groups<T>(
489    items: &[T],
490    subject: impl Fn(&T) -> String,
491    evidence: impl Fn(&T) -> String,
492    evidence_separator: &str,
493) -> Vec<DuplicateEvidenceGroup> {
494    let mut groups = Vec::new();
495    for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
496        if entries.len() <= 1 {
497            continue;
498        }
499        let evidence_values = entries
500            .iter()
501            .map(|entry| evidence(entry))
502            .collect::<BTreeSet<_>>();
503        groups.push(DuplicateEvidenceGroup {
504            subject,
505            count: entries.len(),
506            evidence_label: evidence_values
507                .iter()
508                .cloned()
509                .collect::<Vec<_>>()
510                .join(evidence_separator),
511            is_conflict: evidence_values.len() > 1,
512        });
513    }
514    groups
515}
516
517fn duplicate_evidence_groups_by<T, K>(
518    items: &[T],
519    subject: impl Fn(&T) -> String,
520    evidence_key: impl Fn(&T) -> K,
521    evidence_label: impl Fn(&T) -> String,
522    evidence_separator: &str,
523) -> Vec<DuplicateEvidenceGroup>
524where
525    K: Ord,
526{
527    let mut groups = Vec::new();
528    for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
529        if entries.len() <= 1 {
530            continue;
531        }
532        let evidence_values = entries
533            .iter()
534            .map(|entry| (evidence_key(entry), evidence_label(entry)))
535            .collect::<BTreeMap<_, _>>();
536        groups.push(DuplicateEvidenceGroup {
537            subject,
538            count: entries.len(),
539            evidence_label: evidence_values
540                .values()
541                .cloned()
542                .collect::<Vec<_>>()
543                .join(evidence_separator),
544            is_conflict: evidence_values.len() > 1,
545        });
546    }
547    groups
548}
549
550fn conflicting_assignment_groups<T>(
551    items: &[T],
552    subject: impl Fn(&T) -> Option<String>,
553    value: impl Fn(&T) -> String,
554    value_separator: &str,
555) -> Vec<DuplicateEvidenceGroup> {
556    let mut groups = Vec::new();
557    for (subject, entries) in group_by_subject(items, subject) {
558        if entries.len() <= 1 {
559            continue;
560        }
561        let values = entries
562            .iter()
563            .map(|entry| value(entry))
564            .collect::<BTreeSet<_>>();
565        if values.len() <= 1 {
566            continue;
567        }
568        groups.push(DuplicateEvidenceGroup {
569            subject,
570            count: entries.len(),
571            evidence_label: values
572                .iter()
573                .cloned()
574                .collect::<Vec<_>>()
575                .join(value_separator),
576            is_conflict: true,
577        });
578    }
579    groups
580}
581
582fn group_by_subject<T>(
583    items: &[T],
584    subject: impl Fn(&T) -> Option<String>,
585) -> BTreeMap<String, Vec<&T>> {
586    let mut by_subject = BTreeMap::<String, Vec<&T>>::new();
587    for item in items {
588        if let Some(subject) = subject(item) {
589            by_subject.entry(subject).or_default().push(item);
590        }
591    }
592    by_subject
593}
594
595#[cfg(test)]
596mod classification_tests {
597    use super::*;
598
599    #[test]
600    fn finding_classification_uses_exact_owner_codes() {
601        assert!(is_evidence_conflict_finding_code(
602            ARTIFACT_ROLE_CONFLICT_CODE
603        ));
604        assert!(is_evidence_conflict_finding_code(
605            RECEIPT_PHASE_CONFLICT_CODE
606        ));
607        assert!(!is_evidence_conflict_finding_code("artifact_conflict"));
608        assert!(!is_evidence_conflict_finding_code("conflict"));
609
610        assert!(is_artifact_role_failure_code(ARTIFACT_MISSING_CODE));
611        assert!(!is_artifact_role_failure_code(
612            PLANNED_ARTIFACT_ROLE_CONFLICT_CODE
613        ));
614    }
615}