nornir 0.4.25

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,
}

/// What kind of intake a funnel item is. An **idea** is a feature/krav/prompt;
/// an **error** is a pasted error report / bug. Both are first-class funnel
/// items (EPIC FUNNEL-INTAKE / FI3): an error triages → a plan node exactly like
/// an idea. Serialised lower-case (`idea` / `error`) into the `funnel_events`
/// `item_kind` column. `Idea` is the default so every pre-FI row (no column /
/// null) reads back as an idea.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ItemKind {
    /// A feature request / krav / prompt — the original funnel intake.
    #[default]
    Idea,
    /// A pasted error report / bug — captured the same way, triageable to a plan.
    Error,
}

impl ItemKind {
    /// The lower-case wire/CLI string (`idea` | `error`).
    pub fn as_str(&self) -> &'static str {
        match self {
            ItemKind::Idea => "idea",
            ItemKind::Error => "error",
        }
    }

    /// Parse the wire/CLI string; unknown / empty → `Idea` (the safe default so
    /// legacy rows with no `item_kind` replay as ideas).
    pub fn parse(s: &str) -> Self {
        match s {
            "error" => ItemKind::Error,
            _ => ItemKind::Idea,
        }
    }
}

/// 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>,
        /// `idea` (default) or `error` — an error report is a first-class funnel
        /// item triageable to a plan like any idea. `serde` default keeps
        /// pre-FI logs (no field) replaying as ideas.
        #[serde(default)]
        item_kind: ItemKind,
        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,
        }
    }
}