canic-host 0.110.35

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Module: state_manifest::resolution
//!
//! Responsibility: resolve declared and built-in role packages into one host
//! state manifest.
//! Does not own: descriptor definitions, audit checks, report aggregation, or
//! rendering.
//! Boundary: reads passive workspace/package metadata and returns a complete
//! resolved manifest or blocking role-contract findings.

use crate::role_contract::{
    CargoFeatureSelection, PackageValidationMode, RolePackageValidation,
    materialize_state_manifest, resolve_canonical_root_contract,
    resolve_declared_role_package_contract, resolve_host_generated_fleet_coordinator_contract,
    resolve_host_generated_wasm_store_contract, validate_declared_role_package,
};
use canic_core::{
    bootstrap::parse_config_model,
    ids::CanisterRole,
    role_contract::{ResolvedRoleContract, RoleContractFinding, RoleContractResolution},
    state_contract::StateManifest,
};
use std::{
    collections::BTreeMap,
    fs,
    path::{Path, PathBuf},
};

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StateManifestResolution {
    Rejected {
        errors: Vec<RoleContractFinding>,
    },
    Resolved {
        manifest: StateManifest,
        contracts: Vec<ResolvedRoleContract>,
    },
}

#[must_use]
pub fn resolve_workspace_state_manifest(
    _workspace_root: &Path,
    config_paths: &[PathBuf],
    role_filter: Option<&str>,
) -> StateManifestResolution {
    let mut contracts = BTreeMap::<String, ResolvedRoleContract>::new();
    let mut errors = Vec::new();
    let mut matched_declared_role = false;

    let filters_to_built_in = role_filter.is_some_and(|role| {
        role == CanisterRole::FLEET_COORDINATOR.as_str()
            || role == CanisterRole::WASM_STORE.as_str()
    });
    if !filters_to_built_in {
        for config_path in config_paths {
            let Ok(source) = fs::read_to_string(config_path) else {
                errors.push(RoleContractFinding::DependencyShapeUnsupported {
                    reason: "failed to read role configuration".to_string(),
                });
                continue;
            };
            let Ok(config) = parse_config_model(&source) else {
                errors.push(RoleContractFinding::DependencyShapeUnsupported {
                    reason: "invalid role configuration".to_string(),
                });
                continue;
            };

            for role in config.roles.keys() {
                if role_filter.is_some_and(|filter| filter != role.as_str()) {
                    continue;
                }
                matched_declared_role = true;
                if role.is_root() {
                    collect_contract(
                        role,
                        resolve_canonical_root_contract(&config),
                        &mut contracts,
                        &mut errors,
                    );
                    continue;
                }
                match validate_declared_role_package(
                    config_path,
                    &config,
                    role,
                    PackageValidationMode::Passive,
                    &CargoFeatureSelection::default(),
                ) {
                    RolePackageValidation::Supported(package_evidence) => {
                        let resolution =
                            resolve_declared_role_package_contract(&config, &package_evidence);
                        collect_contract(role, resolution, &mut contracts, &mut errors);
                    }
                    RolePackageValidation::Unsupported(finding) => errors.push(finding),
                }
            }
        }
    }

    if let Some(role) = role_filter
        && !filters_to_built_in
        && !matched_declared_role
    {
        errors.push(RoleContractFinding::RoleUnknown {
            role: CanisterRole::owned(role.to_string()),
        });
    }

    if role_filter.is_none() || role_filter == Some(CanisterRole::FLEET_COORDINATOR.as_str()) {
        collect_contract(
            &CanisterRole::FLEET_COORDINATOR,
            resolve_host_generated_fleet_coordinator_contract(),
            &mut contracts,
            &mut errors,
        );
    }

    if role_filter.is_none() || role_filter == Some(CanisterRole::WASM_STORE.as_str()) {
        collect_contract(
            &CanisterRole::WASM_STORE,
            resolve_host_generated_wasm_store_contract(),
            &mut contracts,
            &mut errors,
        );
    }

    if !errors.is_empty() {
        return StateManifestResolution::Rejected { errors };
    }

    let contracts = contracts.into_values().collect::<Vec<_>>();
    match materialize_state_manifest(&contracts) {
        Ok(manifest) => StateManifestResolution::Resolved {
            manifest,
            contracts,
        },
        Err(errors) => StateManifestResolution::Rejected { errors },
    }
}

fn collect_contract(
    role: &CanisterRole,
    resolution: RoleContractResolution,
    contracts: &mut BTreeMap<String, ResolvedRoleContract>,
    errors: &mut Vec<RoleContractFinding>,
) {
    match resolution {
        RoleContractResolution::Rejected {
            errors: resolution_errors,
        } => errors.extend(resolution_errors),
        RoleContractResolution::Resolved { contract } => {
            let role_name = role.as_str().to_string();
            if contracts
                .get(&role_name)
                .is_some_and(|existing| existing != &contract)
            {
                errors.push(RoleContractFinding::PackageAmbiguous { role: role.clone() });
            } else {
                contracts.insert(role_name, contract);
            }
        }
    }
}