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 plan = build_local_deployment_plan(&LocalDeploymentPlanRequest {
168        fleet_name: request.fleet_name.clone(),
169        app: request.app.clone(),
170        environment: request.environment.clone(),
171        artifact_environment: request.artifact_environment.clone(),
172        workspace_root: request.workspace_root.clone(),
173        icp_root: request.icp_root.clone(),
174        config_path: request.config_path.clone(),
175        runtime_variant: request.runtime_variant.clone(),
176        build_profile: request.build_profile.clone(),
177    });
178    let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
179        fleet_name: request.fleet_name.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        observed_at: request.observed_at.clone(),
186    })?;
187    let mut diff = compare_plan_to_inventory(&plan, &inventory);
188    apply_root_auth_signer_subnet_check(
189        &mut diff,
190        &inventory,
191        &request.environment,
192        &request.icp_root,
193    );
194    let report = safety_report_from_diff(
195        format!(
196            "local:{}:{}:report",
197            request.environment, request.fleet_name
198        ),
199        Some(format!(
200            "local:{}:{}:diff",
201            request.environment, request.fleet_name
202        )),
203        &diff,
204    );
205
206    Ok(DeploymentCheckV1 {
207        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
208        check_id: format!("local:{}:{}:check", request.environment, request.fleet_name),
209        plan,
210        inventory,
211        diff,
212        report,
213    })
214}
215
216fn refresh_resume_safety(diff: &mut DeploymentDiffV1) {
217    diff.resume_safety.status = safety_status(&diff.hard_failures, &diff.warnings);
218    diff.resume_safety.reasons = resume_safety_reasons(&diff.hard_failures, &diff.warnings);
219}
220
221/// Compare intended deployment state with observed inventory into a machine diff.
222#[must_use]
223pub fn compare_plan_to_inventory(
224    plan: &DeploymentPlanV1,
225    inventory: &DeploymentInventoryV1,
226) -> DeploymentDiffV1 {
227    let mut artifact_diff = Vec::new();
228    let mut controller_diff = Vec::new();
229    let mut pool_diff = Vec::new();
230    let mut embedded_config_diff = Vec::new();
231    let mut module_hash_diff = Vec::new();
232    let mut verifier_readiness_diff = Vec::new();
233    let mut hard_failures = Vec::new();
234    let mut warnings = Vec::new();
235
236    compare_identity(plan, inventory, &mut hard_failures);
237    compare_authority_profile(plan, &mut controller_diff, &mut hard_failures);
238    compare_artifacts(
239        plan,
240        inventory,
241        &mut artifact_diff,
242        &mut hard_failures,
243        &mut warnings,
244    );
245    compare_observed_canister_id_conflicts(
246        inventory,
247        &mut controller_diff,
248        &mut hard_failures,
249        &mut warnings,
250    );
251    compare_observed_canister_pool_role_conflicts(inventory, &mut pool_diff, &mut hard_failures);
252    compare_canisters(
253        plan,
254        inventory,
255        &mut controller_diff,
256        &mut hard_failures,
257        &mut warnings,
258    );
259    compare_pools(
260        plan,
261        inventory,
262        &mut pool_diff,
263        &mut hard_failures,
264        &mut warnings,
265    );
266    compare_module_hashes(
267        plan,
268        inventory,
269        &mut module_hash_diff,
270        &mut hard_failures,
271        &mut warnings,
272    );
273    compare_raw_config(
274        plan,
275        inventory,
276        &mut embedded_config_diff,
277        &mut hard_failures,
278    );
279    compare_embedded_config(
280        plan,
281        inventory,
282        &mut embedded_config_diff,
283        &mut hard_failures,
284        &mut warnings,
285    );
286    compare_verifier_readiness(
287        plan,
288        inventory,
289        &mut verifier_readiness_diff,
290        &mut hard_failures,
291        &mut warnings,
292    );
293    record_plan_assumptions(plan, &mut warnings);
294    for gap in &inventory.unresolved_observations {
295        warnings.push(SafetyFindingV1 {
296            code: OBSERVATION_GAP_CODE.to_string(),
297            message: gap.description.clone(),
298            severity: SafetySeverityV1::Warning,
299            subject: Some(gap.key.clone()),
300        });
301    }
302
303    let status = safety_status(&hard_failures, &warnings);
304    DeploymentDiffV1 {
305        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
306        plan_identity: plan.deployment_identity.clone(),
307        observed_identity: inventory.observed_identity.clone(),
308        artifact_diff,
309        controller_diff,
310        pool_diff,
311        embedded_config_diff,
312        module_hash_diff,
313        verifier_readiness_diff,
314        resume_safety: ResumeSafetyV1 {
315            status,
316            reasons: resume_safety_reasons(&hard_failures, &warnings),
317        },
318        hard_failures,
319        warnings,
320        resumable_phases: Vec::new(),
321    }
322}
323
324fn record_plan_assumptions(plan: &DeploymentPlanV1, warnings: &mut Vec<SafetyFindingV1>) {
325    for assumption in &plan.unresolved_assumptions {
326        warnings.push(SafetyFindingV1 {
327            code: PLAN_ASSUMPTION_CODE.to_string(),
328            message: assumption.description.clone(),
329            severity: SafetySeverityV1::Warning,
330            subject: Some(assumption.key.clone()),
331        });
332    }
333}
334
335fn compare_identity(
336    plan: &DeploymentPlanV1,
337    inventory: &DeploymentInventoryV1,
338    hard_failures: &mut Vec<SafetyFindingV1>,
339) {
340    let Some(observed) = &inventory.observed_identity else {
341        hard_failures.push(finding(
342            IDENTITY_UNOBSERVED_CODE,
343            "deployment identity was not observed",
344            SafetySeverityV1::HardFailure,
345            None,
346        ));
347        return;
348    };
349
350    if observed.environment != plan.deployment_identity.environment {
351        hard_failures.push(finding(
352            ENVIRONMENT_MISMATCH_CODE,
353            format!(
354                "plan environment {} differs from observed environment {}",
355                plan.deployment_identity.environment, observed.environment
356            ),
357            SafetySeverityV1::HardFailure,
358            Some("deployment_identity.environment".to_string()),
359        ));
360    }
361    for (field, expected, actual) in [
362        (
363            "fleet_name",
364            Some(plan.deployment_identity.fleet_name.clone()),
365            Some(observed.fleet_name.clone()),
366        ),
367        (
368            "app",
369            Some(plan.deployment_identity.app.clone()),
370            Some(observed.app.clone()),
371        ),
372        (
373            "canonical_network_id",
374            plan.deployment_identity
375                .canonical_network_id
376                .map(|value| value.to_string()),
377            observed.canonical_network_id.map(|value| value.to_string()),
378        ),
379        (
380            "fleet_id",
381            plan.deployment_identity
382                .fleet_id
383                .map(|value| value.to_string()),
384            observed.fleet_id.map(|value| value.to_string()),
385        ),
386    ] {
387        match (expected, actual) {
388            (Some(expected), Some(actual)) if expected != actual => {
389                hard_failures.push(finding(
390                    FLEET_IDENTITY_MISMATCH_CODE,
391                    format!("plan {field} {expected} differs from observed {field} {actual}"),
392                    SafetySeverityV1::HardFailure,
393                    Some(format!("deployment_identity.{field}")),
394                ));
395            }
396            (Some(expected), None) => {
397                hard_failures.push(finding(
398                    FLEET_IDENTITY_MISMATCH_CODE,
399                    format!("plan {field} {expected} was not observed"),
400                    SafetySeverityV1::HardFailure,
401                    Some(format!("deployment_identity.{field}")),
402                ));
403            }
404            _ => {}
405        }
406    }
407    if let (Some(expected), Some(actual)) = (
408        plan.deployment_identity.root_principal.as_ref(),
409        observed.root_principal.as_ref(),
410    ) && expected != actual
411    {
412        hard_failures.push(finding(
413            ROOT_TRUST_ANCHOR_MISMATCH_CODE,
414            format!("plan root {expected} differs from observed root {actual}"),
415            SafetySeverityV1::HardFailure,
416            Some("deployment_identity.root_principal".to_string()),
417        ));
418    }
419    match (
420        plan.deployment_identity.deployment_manifest_digest.as_ref(),
421        observed.deployment_manifest_digest.as_ref(),
422    ) {
423        (Some(expected), Some(actual)) if expected != actual => {
424            hard_failures.push(finding(
425                DEPLOYMENT_MANIFEST_MISMATCH_CODE,
426                "deployment manifest digest differs from the observed local config",
427                SafetySeverityV1::HardFailure,
428                Some("deployment_identity.deployment_manifest_digest".to_string()),
429            ));
430        }
431        (Some(_), None) => {
432            hard_failures.push(finding(
433                DEPLOYMENT_MANIFEST_UNOBSERVED_CODE,
434                "deployment manifest digest was not observed",
435                SafetySeverityV1::HardFailure,
436                Some("deployment_identity.deployment_manifest_digest".to_string()),
437            ));
438        }
439        _ => {}
440    }
441}
442
443fn finding(
444    code: impl Into<String>,
445    message: impl Into<String>,
446    severity: SafetySeverityV1,
447    subject: Option<String>,
448) -> SafetyFindingV1 {
449    SafetyFindingV1 {
450        code: code.into(),
451        message: message.into(),
452        severity,
453        subject,
454    }
455}
456
457fn diff_item(
458    category: impl Into<String>,
459    subject: impl Into<String>,
460    expected: Option<String>,
461    observed: Option<String>,
462    severity: SafetySeverityV1,
463) -> DiffItemV1 {
464    DiffItemV1 {
465        category: category.into(),
466        subject: subject.into(),
467        expected,
468        observed,
469        severity,
470    }
471}
472
473fn duplicate_evidence_groups<T>(
474    items: &[T],
475    subject: impl Fn(&T) -> String,
476    evidence: impl Fn(&T) -> String,
477    evidence_separator: &str,
478) -> Vec<DuplicateEvidenceGroup> {
479    let mut groups = Vec::new();
480    for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
481        if entries.len() <= 1 {
482            continue;
483        }
484        let evidence_values = entries
485            .iter()
486            .map(|entry| evidence(entry))
487            .collect::<BTreeSet<_>>();
488        groups.push(DuplicateEvidenceGroup {
489            subject,
490            count: entries.len(),
491            evidence_label: evidence_values
492                .iter()
493                .cloned()
494                .collect::<Vec<_>>()
495                .join(evidence_separator),
496            is_conflict: evidence_values.len() > 1,
497        });
498    }
499    groups
500}
501
502fn duplicate_evidence_groups_by<T, K>(
503    items: &[T],
504    subject: impl Fn(&T) -> String,
505    evidence_key: impl Fn(&T) -> K,
506    evidence_label: impl Fn(&T) -> String,
507    evidence_separator: &str,
508) -> Vec<DuplicateEvidenceGroup>
509where
510    K: Ord,
511{
512    let mut groups = Vec::new();
513    for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
514        if entries.len() <= 1 {
515            continue;
516        }
517        let evidence_values = entries
518            .iter()
519            .map(|entry| (evidence_key(entry), evidence_label(entry)))
520            .collect::<BTreeMap<_, _>>();
521        groups.push(DuplicateEvidenceGroup {
522            subject,
523            count: entries.len(),
524            evidence_label: evidence_values
525                .values()
526                .cloned()
527                .collect::<Vec<_>>()
528                .join(evidence_separator),
529            is_conflict: evidence_values.len() > 1,
530        });
531    }
532    groups
533}
534
535fn conflicting_assignment_groups<T>(
536    items: &[T],
537    subject: impl Fn(&T) -> Option<String>,
538    value: impl Fn(&T) -> String,
539    value_separator: &str,
540) -> Vec<DuplicateEvidenceGroup> {
541    let mut groups = Vec::new();
542    for (subject, entries) in group_by_subject(items, subject) {
543        if entries.len() <= 1 {
544            continue;
545        }
546        let values = entries
547            .iter()
548            .map(|entry| value(entry))
549            .collect::<BTreeSet<_>>();
550        if values.len() <= 1 {
551            continue;
552        }
553        groups.push(DuplicateEvidenceGroup {
554            subject,
555            count: entries.len(),
556            evidence_label: values
557                .iter()
558                .cloned()
559                .collect::<Vec<_>>()
560                .join(value_separator),
561            is_conflict: true,
562        });
563    }
564    groups
565}
566
567fn group_by_subject<T>(
568    items: &[T],
569    subject: impl Fn(&T) -> Option<String>,
570) -> BTreeMap<String, Vec<&T>> {
571    let mut by_subject = BTreeMap::<String, Vec<&T>>::new();
572    for item in items {
573        if let Some(subject) = subject(item) {
574            by_subject.entry(subject).or_default().push(item);
575        }
576    }
577    by_subject
578}
579
580#[cfg(test)]
581mod classification_tests {
582    use super::*;
583
584    #[test]
585    fn finding_classification_uses_exact_owner_codes() {
586        assert!(is_evidence_conflict_finding_code(
587            ARTIFACT_ROLE_CONFLICT_CODE
588        ));
589        assert!(is_evidence_conflict_finding_code(
590            RECEIPT_PHASE_CONFLICT_CODE
591        ));
592        assert!(!is_evidence_conflict_finding_code("artifact_conflict"));
593        assert!(!is_evidence_conflict_finding_code("conflict"));
594
595        assert!(is_artifact_role_failure_code(ARTIFACT_MISSING_CODE));
596        assert!(!is_artifact_role_failure_code(
597            PLANNED_ARTIFACT_ROLE_CONFLICT_CODE
598        ));
599    }
600}