canic-cli 0.110.32

Operator CLI for Canic fleet setup, builds, evidence, catalog, backup, and restore workflows
Documentation
//! Module: canic_cli::fleet::startup_funding
//!
//! Responsibility: render the no-effect startup funding scenario beside generated desired state.
//! Does not own: forecasting policy, admission or durable funding authority.
//! Boundary: report assumptions and shortfalls without presenting them as approved spending.

use super::format_cycles;
use canic_host::fleet_ensure::view::startup_funding::{
    StartupChildLocalDemand, StartupCoordinatorUsage, StartupDemandUnavailable,
    StartupFundingForecast, StartupNativeBalance, StartupRootFunding, StartupUsageUnavailable,
};
use std::fmt::Write as _;

pub(super) fn render(forecast: &StartupFundingForecast) -> String {
    let mut text = String::from(
        "startup_funding_scenario: pool readiness floor; fresh child grant ledgers; no execution burn\n\
         startup_funding_authority: estimate only; not included in approved funding\n",
    );
    writeln!(
        text,
        "  coordinator_balance: {}",
        balance(forecast.coordinator_balance)
    )
    .unwrap();
    writeln!(
        text,
        "  coordinator_reserve: {}",
        format_cycles(forecast.coordinator_reserve_cycles)
    )
    .unwrap();
    writeln!(
        text,
        "  coordinator_spendable_before_grant_limits: {}",
        format_cycles(forecast.coordinator_spendable_cycles)
    )
    .unwrap();
    render_usage(&mut text, &forecast.coordinator_usage);
    for root in &forecast.roots {
        render_root(&mut text, root);
    }
    text
}

fn render_usage(text: &mut String, usage: &StartupCoordinatorUsage) {
    match usage {
        StartupCoordinatorUsage::Unavailable(reason) => {
            let reason = match reason {
                StartupUsageUnavailable::NotObserved => "not observed",
                StartupUsageUnavailable::NotWorkload => "asset is not an active Workload",
                StartupUsageUnavailable::ParentRelayRequired => {
                    "funding parent requires an update relay"
                }
                StartupUsageUnavailable::ParentNotObserved => "funding parent was not observed",
                StartupUsageUnavailable::SelectedBuildNotInstalled => {
                    "selected build not installed"
                }
                StartupUsageUnavailable::ObservationFailed => "protected query failed",
                StartupUsageUnavailable::AuthorityMismatch => "authority mismatch",
                StartupUsageUnavailable::PolicyTransition => {
                    "policy differs or rotation is in progress"
                }
                StartupUsageUnavailable::InvalidAccounting => "invalid accounting",
            };
            writeln!(
                text,
                "  coordinator_grant_usage: unavailable ({reason}); no unused allowance assumed"
            )
            .unwrap();
        }
        StartupCoordinatorUsage::Observed(value) => {
            writeln!(
                text,
                "  coordinator_grant_usage: observed; generation {}; funding_enabled={}",
                value.policy_generation, value.funding_enabled
            )
            .unwrap();
            writeln!(
                text,
                "    automatic_grants: {}/{}; automatic_cycles: {}/{}; pending_roots: {}",
                value.automatic_grants,
                value.maximum_automatic_grants,
                value.automatic_cycles,
                value.maximum_automatic_cycles,
                value.pending_roots
            )
            .unwrap();
            writeln!(text, "    funding_window: start={}s; spent={} cycles; reserved={} cycles; remaining={} cycles", value.window_start_secs, value.spent_cycles, value.reserved_cycles, value.window_remaining_cycles).unwrap();
            writeln!(text, "    allowance_scope: current window only; not native balance or approval; Root limits and pending operations still apply").unwrap();
        }
    }
}

fn render_root(text: &mut String, root: &StartupRootFunding) {
    writeln!(text, "  root: {}", root.root).unwrap();
    for child in &root.child_usage {
        if let Some(binding) = &child.binding {
            writeln!(
                text,
                "    child_funding_parent: {}; child={}; role={}; component_spec={}",
                binding.parent, child.child, binding.role, binding.component.component_spec
            )
            .unwrap();
        }
        match &child.usage {
            Ok(usage) => {
                writeln!(text, "    child_grant_usage: {} {}; accounted={} cycles; pending_operations={}; reserved_cycles={}",
                    child.name, child.child, usage.accounted_cycles, usage.pending_operations,
                    usage.reserved_cycles.map_or_else(|| "unknown".into(), |cycles| cycles.to_string())).unwrap();
            }
            Err(reason) => {
                writeln!(text, "    child_grant_usage: {} {}; unavailable ({reason:?}); no unused allowance assumed", child.name, child.child).unwrap();
            }
        }
        match &child.allowance {
            Ok(allowance) => {
                writeln!(text, "    child_grant_allowance: lifetime_limit={} cycles; remaining_after_charges={} cycles; cooldown_remaining={}s; next_request_policy_cap_cycles={}",
                    allowance.maximum_per_child_cycles, allowance.remaining_after_charges_cycles,
                    allowance.cooldown_remaining_secs,
                    allowance.next_request_policy_cap_cycles.map_or_else(|| "unknown".into(), |amount| amount.to_string())).unwrap();
            }
            Err(reason) => {
                writeln!(text, "    child_grant_allowance: unavailable ({reason:?}); no unused allowance assumed").unwrap();
            }
        }
        render_local_demand(text, &child.local_demand);
    }
    if !root.child_usage.is_empty() {
        writeln!(text, "    child_usage_scope: allocation-qualified Root-funded Workloads only; descendant usage requires parent relay; charged totals may include pending grants; recovery quote remains separate").unwrap();
        writeln!(text, "    child_allowance_scope: policy cap only; funding enablement, parent reserves and window eligibility still apply; not remaining demand or approved spending").unwrap();
        writeln!(text, "    child_local_demand_scope: own threshold only; excludes descendant transfers and burn; balance and ledger are separate observations; next request still requires parent admission; not a recovery quote").unwrap();
    }
    writeln!(text, "    native_balance: {}", balance(root.balance)).unwrap();
    writeln!(
        text,
        "    initial_child_grants: {}",
        format_cycles(root.child_grants_cycles)
    )
    .unwrap();
    writeln!(
        text,
        "    native_requirement_before_burn: {} ({} cycles)",
        format_cycles(root.minimum_native_cycles),
        root.minimum_native_cycles
    )
    .unwrap();
    writeln!(
        text,
        "    native_shortfall_before_burn: {} ({} cycles)",
        format_cycles(root.shortfall_cycles),
        root.shortfall_cycles
    )
    .unwrap();
    writeln!(
        text,
        "    grants_exceed_configured_window: {} ({} / {}s)",
        root.exceeds_window_budget,
        format_cycles(root.funding_budget.maximum_cycles.to_u128()),
        root.funding_budget.window_secs
    )
    .unwrap();
    for component in &root.components {
        writeln!(
            text,
            "    component: {} {}/{}",
            component.component_spec, component.deployment, component.ordinal
        )
        .unwrap();
        writeln!(
            text,
            "      descendant_grants: {} (included through parent demand)",
            format_cycles(component.descendant_grants_cycles)
        )
        .unwrap();
        writeln!(
            text,
            "      grants_exceed_configured_window: {} ({} / {}s)",
            component.exceeds_window_budget,
            format_cycles(component.funding_budget.maximum_cycles.to_u128()),
            component.funding_budget.window_secs
        )
        .unwrap();
        for role in &component.roles {
            writeln!(text, "      role: {} instances={} initial={} threshold={} grant_cap={} lifetime_limit={} grants_per_instance={} parent_funding_per_instance={} unfunded_per_instance_cycles={} cooldown={}s",
                    role.role, role.instances, format_cycles(role.initial_balance_cycles),
                    role.threshold_cycles.map_or_else(|| "disabled".to_owned(), format_cycles),
                    format_cycles(role.effective_grant_cycles), format_cycles(role.maximum_per_child_cycles),
                    role.grants_per_instance, format_cycles(role.parent_grants_per_instance_cycles),
                    role.unfunded_per_instance_cycles, role.cooldown_secs).unwrap();
        }
    }
}

fn render_local_demand(
    text: &mut String,
    demand: &Result<StartupChildLocalDemand, StartupDemandUnavailable>,
) {
    match demand {
        Ok(demand) => {
            writeln!(text, "    child_local_demand: balance={} cycles; threshold={}; shortfall={} cycles; beyond_lifetime_allowance={} cycles; next_request_policy_cycles={}",
                demand.observed_balance_cycles,
                demand.threshold_cycles.map_or_else(|| "disabled".into(), |amount| amount.to_string()),
                demand.shortfall_cycles, demand.shortfall_beyond_lifetime_allowance_cycles,
                demand.next_request_policy_cycles).unwrap();
        }
        Err(reason) => {
            writeln!(
                text,
                "    child_local_demand: unavailable ({reason:?}); no zero demand assumed"
            )
            .unwrap();
        }
    }
}

fn balance(value: StartupNativeBalance) -> String {
    match value {
        StartupNativeBalance::ConfiguredCreation(cycles) => {
            format!("{} (configured creation)", format_cycles(cycles))
        }
        StartupNativeBalance::Observed(cycles) => format!("{} (observed)", format_cycles(cycles)),
    }
}

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use canic_core::{cdk::types::Cycles, ids::CyclesFundingBudget};

    fn forecast() -> StartupFundingForecast {
        StartupFundingForecast {
            coordinator_usage: StartupCoordinatorUsage::Unavailable(
                StartupUsageUnavailable::NotObserved,
            ),
            coordinator_balance: StartupNativeBalance::Observed(0),
            coordinator_reserve_cycles: 1,
            coordinator_spendable_cycles: 0,
            roots: vec![StartupRootFunding {
                child_usage: Vec::new(),
                balance: StartupNativeBalance::ConfiguredCreation(10_000_000_000_000),
                child_grants_cycles: 0,
                components: vec![],
                exceeds_window_budget: false,
                funding_budget: CyclesFundingBudget {
                    window_secs: 60,
                    maximum_cycles: Cycles::new(1_000_000_000_000),
                },
                minimum_native_cycles: 10_000_000_000_001,
                request_threshold_cycles: 10_000_000_000_000,
                root: "root-0".to_owned(),
                shortfall_cycles: 1,
            }],
        }
    }

    fn child_binding() -> canic_host::fleet_ensure::view::startup_funding::StartupChildFundingBinding
    {
        use canic_core::ids::*;
        let principal = |byte| candid::Principal::from_slice(&[byte]);
        canic_host::fleet_ensure::view::startup_funding::StartupChildFundingBinding {
            release_set: FleetSubnetRootReleaseSet {
                release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
                    [2; 32],
                )),
                manifest_digest: ReleaseSetDigest::from_bytes([3; 32]),
            },
            component: ComponentBinding {
                authority: FleetRegistryAuthority {
                    binding: FleetCoordinatorBinding {
                        fleet: FleetBinding {
                            fleet: FleetKey {
                                canonical_network_id: CanonicalNetworkId::ic_mainnet(),
                                fleet_id: FleetId::from_generated_bytes([4; 32]),
                            },
                            app: AppId::from("test"),
                        },
                        coordinator_subnet: SubnetId::from_principal(principal(1)),
                        coordinator: principal(2),
                    },
                    epoch: 1,
                },
                component: ComponentInstanceId::from_generated_bytes([5; 32]),
                component_spec: "hubs".parse().unwrap(),
                spec_hash: [1; 32],
                role: "hub".into(),
                placement_subnet: SubnetId::from_principal(principal(1)),
                fleet_subnet_root: principal(3),
                canister_id: principal(4),
            },
            parent: principal(4),
            canister_id: principal(5),
            parent_role: Some("hub".into()),
            role: "shard".into(),
        }
    }

    #[test]
    fn startup_report_distinguishes_evidence_and_retains_sub_display_unit_shortfall() {
        let mut forecast = forecast();
        let report = render(&forecast);
        assert!(report.contains("coordinator_balance: 0.000B (observed)"));
        assert!(report.contains("native_balance: 10.000T (configured creation)"));
        assert!(report.contains("native_shortfall_before_burn: 0.000B (1 cycles)"));
        assert!(report.contains("startup_funding_authority: estimate only"));
        assert!(report.contains("coordinator_grant_usage: unavailable (not observed)"));
        forecast.coordinator_usage = StartupCoordinatorUsage::Observed(
            canic_host::fleet_ensure::view::startup_funding::StartupCoordinatorAccounting {
                policy_generation: 1,
                funding_enabled: false,
                window_start_secs: 60,
                spent_cycles: 30,
                reserved_cycles: 25,
                window_remaining_cycles: 45,
                automatic_grants: 2,
                maximum_automatic_grants: 4,
                automatic_cycles: 60,
                maximum_automatic_cycles: 200,
                pending_roots: 1,
            },
        );
        let report = render(&forecast);
        assert!(report.contains("funding_enabled=false"));
        assert!(
            report.contains("automatic_grants: 2/4; automatic_cycles: 60/200; pending_roots: 1")
        );
        assert!(report.contains("spent=30 cycles; reserved=25 cycles; remaining=45 cycles"));
        forecast.roots[0].child_usage.push(
            canic_host::fleet_ensure::view::startup_funding::StartupChildFundingUsage {
                observed_balance_cycles: Some(10),
                local_demand: Err(canic_host::fleet_ensure::view::startup_funding::StartupDemandUnavailable::PendingGrant),
                allowance: Ok(
                    canic_host::fleet_ensure::view::startup_funding::StartupChildFundingAllowance {
                        maximum_per_child_cycles: 200,
                        remaining_after_charges_cycles: 70,
                        cooldown_remaining_secs: 5,
                        next_request_policy_cap_cycles: None,
                    },
                ),
                binding: None,
                name: "pool-0".into(),
                child: "child".into(),
                usage: Ok(
                    canic_host::fleet_ensure::view::startup_funding::StartupChildAccounting {
                        observed_at_ns: 1_000_000_000,
                        accounted_cycles: 130,
                        last_accounted_at_secs: 1,
                        pending_operations: 1,
                        reserved_cycles: None,
                    },
                ),
            },
        );
        let report = render(&forecast);
        assert!(
            report.contains("accounted=130 cycles; pending_operations=1; reserved_cycles=unknown")
        );
        assert!(report.contains("allocation-qualified Root-funded Workloads only"));
        assert!(report.contains("child_local_demand: unavailable (PendingGrant)"));
        assert!(report.contains("remaining_after_charges=70 cycles; cooldown_remaining=5s; next_request_policy_cap_cycles=unknown"));
        forecast.roots[0].child_usage[0].binding = Some(child_binding());
        forecast.roots[0].child_usage[0].usage = Err(
            canic_host::fleet_ensure::view::startup_funding::StartupUsageUnavailable::ParentRelayRequired,
        );
        let report = render(&forecast);
        assert!(report.contains(&format!(
            "parent: {}; child=child; role=shard; component_spec=hubs",
            child_binding().parent
        )));
        assert!(report.contains("ParentRelayRequired"));
    }

    #[test]
    fn local_demand_report_preserves_exact_deficits() {
        let mut report = String::new();
        render_local_demand(
            &mut report,
            &Ok(StartupChildLocalDemand {
                observed_balance_cycles: 10,
                threshold_cycles: Some(20),
                shortfall_cycles: 11,
                shortfall_beyond_lifetime_allowance_cycles: 6,
                next_request_policy_cycles: 5,
            }),
        );
        assert!(report.contains("balance=10 cycles; threshold=20; shortfall=11 cycles; beyond_lifetime_allowance=6 cycles; next_request_policy_cycles=5"));
    }
}