rusty-beads 0.1.0

Git-backed graph issue tracker for AI coding agents - a Rust implementation with context store, dependency tracking, and semantic compaction
Documentation
//! Issue status definitions.

use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

/// The workflow state of an issue.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Status {
    /// Default state for new issues - work not yet started.
    #[default]
    Open,
    /// Work is actively underway.
    InProgress,
    /// Issue is halted by dependencies.
    Blocked,
    /// Intentionally postponed.
    Deferred,
    /// Work completed.
    Closed,
    /// Soft-deleted issue (preserved for sync).
    Tombstone,
    /// Persistent bead remaining indefinitely open.
    Pinned,
    /// Work attached to an agent's hook.
    Hooked,
}

impl Status {
    /// Returns true if this status represents active work.
    pub fn is_active(&self) -> bool {
        matches!(self, Status::Open | Status::InProgress | Status::Blocked)
    }

    /// Returns true if this status represents completed or removed work.
    pub fn is_terminal(&self) -> bool {
        matches!(self, Status::Closed | Status::Tombstone)
    }

    /// Returns true if this status blocks dependent issues.
    pub fn blocks_dependents(&self) -> bool {
        !matches!(self, Status::Closed | Status::Tombstone)
    }

    /// All valid status values.
    pub fn all() -> &'static [Status] {
        &[
            Status::Open,
            Status::InProgress,
            Status::Blocked,
            Status::Deferred,
            Status::Closed,
            Status::Tombstone,
            Status::Pinned,
            Status::Hooked,
        ]
    }

    /// Returns the string representation for database storage.
    pub fn as_str(&self) -> &'static str {
        match self {
            Status::Open => "open",
            Status::InProgress => "in_progress",
            Status::Blocked => "blocked",
            Status::Deferred => "deferred",
            Status::Closed => "closed",
            Status::Tombstone => "tombstone",
            Status::Pinned => "pinned",
            Status::Hooked => "hooked",
        }
    }
}

impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl FromStr for Status {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "open" => Ok(Status::Open),
            "in_progress" | "in-progress" | "inprogress" => Ok(Status::InProgress),
            "blocked" => Ok(Status::Blocked),
            "deferred" => Ok(Status::Deferred),
            "closed" => Ok(Status::Closed),
            "tombstone" => Ok(Status::Tombstone),
            "pinned" => Ok(Status::Pinned),
            "hooked" => Ok(Status::Hooked),
            _ => Err(format!("unknown status: {}", s)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_status_roundtrip() {
        for status in Status::all() {
            let s = status.as_str();
            let parsed: Status = s.parse().unwrap();
            assert_eq!(*status, parsed);
        }
    }

    #[test]
    fn test_status_serde() {
        let status = Status::InProgress;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, "\"in_progress\"");
        let parsed: Status = serde_json::from_str(&json).unwrap();
        assert_eq!(status, parsed);
    }
}