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
//! Dependency type definitions.

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

/// A relationship between two issues.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
    /// The issue that depends on another.
    pub issue_id: String,
    /// The issue being depended upon.
    pub depends_on_id: String,
    /// The type of dependency relationship.
    #[serde(rename = "type")]
    pub dep_type: DependencyType,
    /// When the dependency was created.
    pub created_at: DateTime<Utc>,
    /// Who created the dependency.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub created_by: Option<String>,
    /// Type-specific metadata (JSON).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<String>,
    /// Thread ID for conversation threading.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<String>,
}

impl Dependency {
    /// Create a new blocking dependency.
    pub fn blocks(issue_id: impl Into<String>, depends_on_id: impl Into<String>) -> Self {
        Self {
            issue_id: issue_id.into(),
            depends_on_id: depends_on_id.into(),
            dep_type: DependencyType::Blocks,
            created_at: Utc::now(),
            created_by: None,
            metadata: None,
            thread_id: None,
        }
    }

    /// Create a parent-child relationship.
    pub fn parent_child(child_id: impl Into<String>, parent_id: impl Into<String>) -> Self {
        Self {
            issue_id: child_id.into(),
            depends_on_id: parent_id.into(),
            dep_type: DependencyType::ParentChild,
            created_at: Utc::now(),
            created_by: None,
            metadata: None,
            thread_id: None,
        }
    }

    /// Set the creator of this dependency.
    pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
        self.created_by = Some(creator.into());
        self
    }

    /// Set metadata for this dependency.
    pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
        self.metadata = Some(metadata.into());
        self
    }
}

/// The type of relationship between issues.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum DependencyType {
    // Workflow types (affect ready work calculation)
    /// Blocks downstream work.
    #[default]
    Blocks,
    /// Hierarchical parent-child relationship.
    ParentChild,
    /// Conditional blocking.
    ConditionalBlocks,
    /// Async coordination wait.
    WaitsFor,

    // Association types (non-blocking)
    /// Generally related issues.
    Related,
    /// Source of discovery.
    DiscoveredFrom,

    // Graph link types
    /// Comment thread connection.
    RepliesTo,
    /// Topical relationship.
    RelatesTo,
    /// Duplicate issues.
    Duplicates,
    /// Replacement relationship.
    Supersedes,

    // Entity types (HOP foundation)
    /// Creator attribution.
    AuthoredBy,
    /// Work assignment.
    AssignedTo,
    /// Approval chain.
    ApprovedBy,
    /// Skill attestation.
    Attests,

    // Cross-project references
    /// Tracks external issue.
    Tracks,
    /// Depends until condition.
    Until,
    /// Caused by external event.
    CausedBy,
    /// Validates work.
    Validates,
    /// Delegated from another issue.
    DelegatedFrom,
}

impl DependencyType {
    /// All valid dependency types.
    pub fn all() -> &'static [DependencyType] {
        &[
            DependencyType::Blocks,
            DependencyType::ParentChild,
            DependencyType::ConditionalBlocks,
            DependencyType::WaitsFor,
            DependencyType::Related,
            DependencyType::DiscoveredFrom,
            DependencyType::RepliesTo,
            DependencyType::RelatesTo,
            DependencyType::Duplicates,
            DependencyType::Supersedes,
            DependencyType::AuthoredBy,
            DependencyType::AssignedTo,
            DependencyType::ApprovedBy,
            DependencyType::Attests,
            DependencyType::Tracks,
            DependencyType::Until,
            DependencyType::CausedBy,
            DependencyType::Validates,
            DependencyType::DelegatedFrom,
        ]
    }

    /// Returns true if this dependency type blocks work.
    pub fn is_blocking(&self) -> bool {
        matches!(
            self,
            DependencyType::Blocks
                | DependencyType::ParentChild
                | DependencyType::ConditionalBlocks
                | DependencyType::WaitsFor
        )
    }

    /// Returns true if cycles should be checked for this type.
    pub fn check_cycles(&self) -> bool {
        // RelatesTo is symmetric so cycles don't matter
        !matches!(self, DependencyType::RelatesTo)
    }

    /// Returns the string representation for database storage.
    pub fn as_str(&self) -> &'static str {
        match self {
            DependencyType::Blocks => "blocks",
            DependencyType::ParentChild => "parent_child",
            DependencyType::ConditionalBlocks => "conditional_blocks",
            DependencyType::WaitsFor => "waits_for",
            DependencyType::Related => "related",
            DependencyType::DiscoveredFrom => "discovered_from",
            DependencyType::RepliesTo => "replies_to",
            DependencyType::RelatesTo => "relates_to",
            DependencyType::Duplicates => "duplicates",
            DependencyType::Supersedes => "supersedes",
            DependencyType::AuthoredBy => "authored_by",
            DependencyType::AssignedTo => "assigned_to",
            DependencyType::ApprovedBy => "approved_by",
            DependencyType::Attests => "attests",
            DependencyType::Tracks => "tracks",
            DependencyType::Until => "until",
            DependencyType::CausedBy => "caused_by",
            DependencyType::Validates => "validates",
            DependencyType::DelegatedFrom => "delegated_from",
        }
    }
}

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

impl FromStr for DependencyType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().replace('-', "_").as_str() {
            "blocks" => Ok(DependencyType::Blocks),
            "parent_child" => Ok(DependencyType::ParentChild),
            "conditional_blocks" => Ok(DependencyType::ConditionalBlocks),
            "waits_for" => Ok(DependencyType::WaitsFor),
            "related" => Ok(DependencyType::Related),
            "discovered_from" => Ok(DependencyType::DiscoveredFrom),
            "replies_to" => Ok(DependencyType::RepliesTo),
            "relates_to" => Ok(DependencyType::RelatesTo),
            "duplicates" => Ok(DependencyType::Duplicates),
            "supersedes" => Ok(DependencyType::Supersedes),
            "authored_by" => Ok(DependencyType::AuthoredBy),
            "assigned_to" => Ok(DependencyType::AssignedTo),
            "approved_by" => Ok(DependencyType::ApprovedBy),
            "attests" => Ok(DependencyType::Attests),
            "tracks" => Ok(DependencyType::Tracks),
            "until" => Ok(DependencyType::Until),
            "caused_by" => Ok(DependencyType::CausedBy),
            "validates" => Ok(DependencyType::Validates),
            "delegated_from" => Ok(DependencyType::DelegatedFrom),
            _ => Err(format!("unknown dependency type: {}", s)),
        }
    }
}

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

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

    #[test]
    fn test_blocking_types() {
        assert!(DependencyType::Blocks.is_blocking());
        assert!(DependencyType::ParentChild.is_blocking());
        assert!(!DependencyType::RelatesTo.is_blocking());
        assert!(!DependencyType::Related.is_blocking());
    }
}