af-workflow 0.4.0

Spec-driven workflow chassis: typed node expressions composed into a branched DAG. Port of agent_core/workflow.
Documentation
//! Infra-agnostic workflow run and step persistence seam.

use std::sync::Arc;

use async_trait::async_trait;
use serde_json::Value;

/// Recorded status of a step.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepStatus {
    /// Succeeded.
    Ok,
    /// Filtered or skipped.
    Skipped,
    /// Failed.
    Error,
}

impl StepStatus {
    /// Stable lowercase name.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Ok => "ok",
            Self::Skipped => "skipped",
            Self::Error => "error",
        }
    }
}

/// Recorded status of a run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunStatus {
    /// Succeeded.
    Ok,
    /// Filtered or skipped.
    Skipped,
    /// Failed.
    Error,
}

impl RunStatus {
    /// Stable lowercase name.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Ok => "ok",
            Self::Skipped => "skipped",
            Self::Error => "error",
        }
    }
}

/// Open run being recorded.
#[async_trait]
pub trait RunHandle: Send {
    /// Record one step outcome.
    async fn record_step(
        &mut self,
        node_id: &str,
        node_type: &str,
        status: StepStatus,
        exit_reason: Option<&str>,
        detail: Value,
    );

    /// Record routine negative evidence without materializing a full run.
    async fn mark_filtered(&mut self, node_id: &str, node_type: &str, reason: &str);

    /// Close the run.
    async fn end(self: Box<Self>, status: RunStatus, exit_reason: Option<&str>);
}

/// Sink for run and step facts (audit, UI, metrics).
#[async_trait]
pub trait RunRecorder: Send + Sync {
    /// Open a run for a trigger.
    async fn start(&self, trigger_kind: &str, trigger: Value) -> Box<dyn RunHandle>;
}

/// Recorder that discards everything.
pub struct NoopRecorder;

#[async_trait]
impl RunRecorder for NoopRecorder {
    async fn start(&self, _trigger_kind: &str, _trigger: Value) -> Box<dyn RunHandle> {
        Box::new(NoopHandle)
    }
}

struct NoopHandle;

#[async_trait]
impl RunHandle for NoopHandle {
    async fn record_step(&mut self, _: &str, _: &str, _: StepStatus, _: Option<&str>, _: Value) {}
    async fn mark_filtered(&mut self, _: &str, _: &str, _: &str) {}
    async fn end(self: Box<Self>, _: RunStatus, _: Option<&str>) {}
}

/// Shared [`NoopRecorder`].
pub fn noop_recorder() -> Arc<dyn RunRecorder> {
    Arc::new(NoopRecorder)
}