af-workflow 0.2.0

Spec-driven workflow chassis: typed node expressions composed into a branched DAG. Port of agent_core/workflow/v2.
Documentation
//! Pure ingress planning helpers for durable workflow hosts.

use chrono::{DateTime, Utc};

use crate::{NodeRegistry, Spec};

pub fn next_cron_at(expression: &str, after: DateTime<Utc>) -> Result<DateTime<Utc>, String> {
    expression
        .parse::<cron::Schedule>()
        .map_err(|error| format!("invalid cron '{expression}': {error}"))?
        .after(&after)
        .next()
        .ok_or_else(|| format!("cron '{expression}' has no future occurrence"))
}

#[derive(Debug, Clone, PartialEq)]
pub struct BranchIngressPlan {
    pub branch_id: String,
    pub ingress_type: String,
    pub ingress_config: serde_json::Value,
}

pub fn ingress_plans(spec: &Spec, registry: &NodeRegistry) -> Vec<BranchIngressPlan> {
    spec.branches
        .iter()
        .filter_map(|branch| {
            let ingress = branch
                .nodes
                .iter()
                .find(|node| registry.is_ingress(&node.node_type))?;
            Some(BranchIngressPlan {
                branch_id: branch.branch_id.clone(),
                ingress_type: ingress.node_type.clone(),
                ingress_config: ingress.config.clone(),
            })
        })
        .collect()
}