nornir 0.4.6

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
Documentation
//! Append-only event log entries that materialise the funnel state.
//!
//! Every mutation to the funnel — submitting an idea, adding a plan
//! node, flipping a status, recording a run — is serialised as a
//! single [`Event`] appended as a row to the Iceberg `funnel_events` table
//! under `<workspace>/.nornir/warehouse` (one snapshot per write).
//! Loading the funnel = replaying every row into [`super::state::Funnel`].
//!
//! Events are versioned implicitly by serde's `#[serde(tag = "kind")]`
//! discriminator. New event kinds can be added at the bottom; old rows
//! keep replaying cleanly as long as renames go through `#[serde(rename)]`.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use super::ids::{IdeaId, NodeId, PlanId, RunId};

/// Where an idea originated. Free-form so MCP / CI integrations can
/// add their own source tags without a schema bump.
pub type Source = String;

/// File-glob style target marker. Empty means "no target".
pub type Target = String;

/// Lifecycle of a plan node.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeStatus {
    /// Created but deps not yet satisfied.
    Pending,
    /// All deps done; eligible for `topo_ready()`.
    Ready,
    /// Someone (human or agent) picked it up.
    InProgress,
    /// Successfully completed; dependents may now be `Ready`.
    Done,
    /// Cannot proceed; dependents remain `Pending` forever unless
    /// re-planned with an alternate route.
    Blocked,
    /// Attempted and failed; can be re-attempted (a new `Run` will
    /// be recorded — node stays `Failed` until explicitly flipped).
    Failed,
}

/// Lifecycle of a plan.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlanStatus {
    Draft,
    Active,
    Done,
    Abandoned,
}

/// Triage decision on an incoming idea.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TriageDecision {
    Accept,
    Drop,
}

/// Outcome of a single execution attempt.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunOutcome {
    Ok,
    Failed,
    Aborted,
}

/// Discriminated union of every mutation the funnel understands.
///
/// **Append-only.** Once a variant ships, it must keep deserialising
/// from older logs. Add new variants at the bottom; never remove or
/// reorder existing ones.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Event {
    IdeaSubmitted {
        id: IdeaId,
        source: Source,
        text: String,
        #[serde(default)]
        refs: Vec<String>,
        ts: DateTime<Utc>,
    },
    IdeaTriaged {
        idea_id: IdeaId,
        decision: TriageDecision,
        #[serde(default)]
        why: Option<String>,
        ts: DateTime<Utc>,
    },
    PlanCreated {
        id: PlanId,
        idea_id: IdeaId,
        summary: String,
        planner: String,
        ts: DateTime<Utc>,
    },
    NodeAdded {
        plan_id: PlanId,
        node_id: NodeId,
        #[serde(rename = "node_kind")]
        kind: String,
        #[serde(default)]
        params: serde_json::Map<String, serde_json::Value>,
        #[serde(default)]
        targets: Vec<Target>,
        #[serde(default)]
        prompt_excerpt: Option<String>,
        ts: DateTime<Utc>,
    },
    EdgeAdded {
        plan_id: PlanId,
        from_node: NodeId,
        to_node: NodeId,
        ts: DateTime<Utc>,
    },
    NodeStatusChanged {
        plan_id: PlanId,
        node_id: NodeId,
        status: NodeStatus,
        #[serde(default)]
        why: Option<String>,
        ts: DateTime<Utc>,
    },
    RunRecorded {
        plan_id: PlanId,
        node_id: NodeId,
        run_id: RunId,
        ran_by: String,
        outcome: RunOutcome,
        #[serde(default)]
        log_ref: Option<String>,
        #[serde(default)]
        produced_commits: Vec<CommitRef>,
        #[serde(default)]
        produced_test_runs: Vec<String>,
        ts: DateTime<Utc>,
    },
    PlanStatusChanged {
        plan_id: PlanId,
        status: PlanStatus,
        #[serde(default)]
        why: Option<String>,
        ts: DateTime<Utc>,
    },
}

/// Identifies a commit a [`RunRecorded`] event produced.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitRef {
    pub repo: String,
    pub sha: String,
}

impl Event {
    /// Timestamp this event was produced at. Used by the store for
    /// ordering when external tools replay the log out of order.
    pub fn ts(&self) -> DateTime<Utc> {
        match self {
            Event::IdeaSubmitted { ts, .. }
            | Event::IdeaTriaged { ts, .. }
            | Event::PlanCreated { ts, .. }
            | Event::NodeAdded { ts, .. }
            | Event::EdgeAdded { ts, .. }
            | Event::NodeStatusChanged { ts, .. }
            | Event::RunRecorded { ts, .. }
            | Event::PlanStatusChanged { ts, .. } => *ts,
        }
    }
}