runx-core 0.6.19

Pure Rust parity kernel for runx state-machine and policy decisions.
Documentation
use std::collections::BTreeSet;

use runx_contracts::JsonValue;

use super::super::types::{
    FanoutBranchResult, FanoutGate, FanoutGroupPolicy, FanoutSyncDecision, FanoutSyncOutcome,
    FanoutThresholdGate, GraphStepStatus,
};
use super::values::{json_value_as_f64, resolve_structured_field};
use super::{Counts, DecisionDetails, is_resolved, sync_decision};

pub(super) fn threshold_decision(
    policy: &FanoutGroupPolicy,
    results: &[FanoutBranchResult],
    resolved_gate_keys: Option<&BTreeSet<String>>,
    counts: Counts,
) -> Option<FanoutSyncDecision> {
    for gate in policy.threshold_gates.as_deref().unwrap_or(&[]) {
        let Some(above) = gate.above.as_f64() else {
            continue;
        };
        let Some(result) = results
            .iter()
            .find(|candidate| candidate.step_id == gate.step)
        else {
            continue;
        };
        if result.status != GraphStepStatus::Succeeded {
            continue;
        }
        let Some(value) = resolve_structured_field(result.outputs.as_ref(), &gate.field) else {
            return Some(threshold_missing_decision(policy, gate, counts));
        };
        let Some(number) = json_value_as_f64(value) else {
            return Some(threshold_non_numeric_decision(
                policy,
                gate,
                value.clone(),
                counts,
            ));
        };
        if number > above {
            let decision = threshold_above_decision(policy, gate, value.clone(), number, counts);
            if !is_resolved(&decision, resolved_gate_keys) {
                return Some(decision);
            }
        }
    }
    None
}

fn threshold_missing_decision(
    policy: &FanoutGroupPolicy,
    gate: &FanoutThresholdGate,
    counts: Counts,
) -> FanoutSyncDecision {
    sync_decision(
        policy,
        FanoutSyncOutcome::Halt,
        "threshold",
        counts,
        DecisionDetails::new(
            format!("threshold.{}.{}.missing", gate.step, gate.field),
            format!(
                "threshold field {}.{} was not produced",
                gate.step, gate.field
            ),
        )
        .with_gate(FanoutGate::Threshold {
            step_id: Some(gate.step.clone()),
            field: gate.field.clone(),
            value: None,
            compared_to: None,
            action: gate.action.clone(),
        }),
    )
}

fn threshold_non_numeric_decision(
    policy: &FanoutGroupPolicy,
    gate: &FanoutThresholdGate,
    value: JsonValue,
    counts: Counts,
) -> FanoutSyncDecision {
    sync_decision(
        policy,
        FanoutSyncOutcome::Halt,
        "threshold",
        counts,
        DecisionDetails::new(
            format!("threshold.{}.{}.non_numeric", gate.step, gate.field),
            format!(
                "threshold field {}.{} must be numeric",
                gate.step, gate.field
            ),
        )
        .with_gate(FanoutGate::Threshold {
            step_id: Some(gate.step.clone()),
            field: gate.field.clone(),
            value: Some(value),
            compared_to: None,
            action: gate.action.clone(),
        }),
    )
}

fn threshold_above_decision(
    policy: &FanoutGroupPolicy,
    gate: &FanoutThresholdGate,
    value: JsonValue,
    number: f64,
    counts: Counts,
) -> FanoutSyncDecision {
    sync_decision(
        policy,
        gate.action.clone().into(),
        "threshold",
        counts,
        DecisionDetails::new(
            format!("threshold.{}.{}.above", gate.step, gate.field),
            format!(
                "{}.{}={number} exceeded {}",
                gate.step, gate.field, gate.above
            ),
        )
        .with_gate(FanoutGate::Threshold {
            step_id: Some(gate.step.clone()),
            field: gate.field.clone(),
            value: Some(value),
            compared_to: Some(gate.above.clone()),
            action: gate.action.clone(),
        }),
    )
}