agileplus-domain 0.3.0

Typed intent graphs for engineering work — convert prompts into validated DAGs of intents, features, tasks, tests, and edges, then store, query, and serve them from Rust, HTTP, or MCP.
Documentation
//! Node-related types for the intent graph: identifiers, taxonomy enums,
//! and the concrete `Node` / `Edge` records that populate the graph.

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

use super::metadata::{CanonicalMap, Meta};

// ---------------------------------------------------------------------------
// NodeId
// ---------------------------------------------------------------------------

/// Globally unique node ID.
/// Format: `<Type>#<slug>`. Example: `Feature#auth-oauth2`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NodeId(pub String);

impl NodeId {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }
}

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

// ---------------------------------------------------------------------------
// NodeType
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum NodeType {
    Intent,
    Plan,
    Feature,
    Story,
    Task,
    Spec,
    Commit,
    Test,
    PR,
    Bug,
    Artifact,
}

impl fmt::Display for NodeType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            NodeType::Intent => "Intent",
            NodeType::Plan => "Plan",
            NodeType::Feature => "Feature",
            NodeType::Story => "Story",
            NodeType::Task => "Task",
            NodeType::Spec => "Spec",
            NodeType::Commit => "Commit",
            NodeType::Test => "Test",
            NodeType::PR => "PR",
            NodeType::Bug => "Bug",
            NodeType::Artifact => "Artifact",
        };
        write!(f, "{}", s)
    }
}

impl FromStr for NodeType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "Intent" => Ok(NodeType::Intent),
            "Plan" => Ok(NodeType::Plan),
            "Feature" => Ok(NodeType::Feature),
            "Story" => Ok(NodeType::Story),
            "Task" => Ok(NodeType::Task),
            "Spec" => Ok(NodeType::Spec),
            "Commit" => Ok(NodeType::Commit),
            "Test" => Ok(NodeType::Test),
            "PR" => Ok(NodeType::PR),
            "Bug" => Ok(NodeType::Bug),
            "Artifact" => Ok(NodeType::Artifact),
            _ => Err(format!("Unknown NodeType: {}", s)),
        }
    }
}

// ---------------------------------------------------------------------------
// DagStage
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DagStage {
    Intent,
    Plan,
    Feature,
    Story,
    Task,
    Spec,
    Commit,
    Test,
    Pr,
    Bug,
    Artifact,
}

impl fmt::Display for DagStage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            DagStage::Intent => "intent",
            DagStage::Plan => "plan",
            DagStage::Feature => "feature",
            DagStage::Story => "story",
            DagStage::Task => "task",
            DagStage::Spec => "spec",
            DagStage::Commit => "commit",
            DagStage::Test => "test",
            DagStage::Pr => "pr",
            DagStage::Bug => "bug",
            DagStage::Artifact => "artifact",
        };
        write!(f, "{}", s)
    }
}

impl FromStr for DagStage {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "intent" => Ok(DagStage::Intent),
            "plan" => Ok(DagStage::Plan),
            "feature" => Ok(DagStage::Feature),
            "story" => Ok(DagStage::Story),
            "task" => Ok(DagStage::Task),
            "spec" => Ok(DagStage::Spec),
            "commit" => Ok(DagStage::Commit),
            "test" => Ok(DagStage::Test),
            "pr" => Ok(DagStage::Pr),
            "bug" => Ok(DagStage::Bug),
            "artifact" => Ok(DagStage::Artifact),
            _ => Err(format!("Unknown DagStage: {}", s)),
        }
    }
}

// ---------------------------------------------------------------------------
// Status
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Status {
    Draft,
    Active,
    Completed,
    Deprecated,
    Rejected,
    Open,
    InProgress,
    Blocked,
    Deferred,
    Cancelled,
}

impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Status::Draft => "draft",
            Status::Active => "active",
            Status::Completed => "completed",
            Status::Deprecated => "deprecated",
            Status::Rejected => "rejected",
            Status::Open => "open",
            Status::InProgress => "in_progress",
            Status::Blocked => "blocked",
            Status::Deferred => "deferred",
            Status::Cancelled => "cancelled",
        };
        write!(f, "{}", s)
    }
}

impl FromStr for Status {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "draft" => Ok(Status::Draft),
            "active" => Ok(Status::Active),
            "completed" => Ok(Status::Completed),
            "deprecated" => Ok(Status::Deprecated),
            "rejected" => Ok(Status::Rejected),
            "open" => Ok(Status::Open),
            "in_progress" => Ok(Status::InProgress),
            "blocked" => Ok(Status::Blocked),
            "deferred" => Ok(Status::Deferred),
            "cancelled" => Ok(Status::Cancelled),
            _ => Err(format!("Unknown Status: {}", s)),
        }
    }
}

// ---------------------------------------------------------------------------
// RelationshipType
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RelationshipType {
    Implements,
    Tests,
    Covers,
    TracesTo,
    DerivesFrom,
    Resolves,
    Blocks,
    DependsOn,
}

impl fmt::Display for RelationshipType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            RelationshipType::Implements => "implements",
            RelationshipType::Tests => "tests",
            RelationshipType::Covers => "covers",
            RelationshipType::TracesTo => "traces-to",
            RelationshipType::DerivesFrom => "derives-from",
            RelationshipType::Resolves => "resolves",
            RelationshipType::Blocks => "blocks",
            RelationshipType::DependsOn => "depends-on",
        };
        write!(f, "{}", s)
    }
}

impl FromStr for RelationshipType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "implements" => Ok(RelationshipType::Implements),
            "tests" => Ok(RelationshipType::Tests),
            "covers" => Ok(RelationshipType::Covers),
            "traces-to" => Ok(RelationshipType::TracesTo),
            "derives-from" => Ok(RelationshipType::DerivesFrom),
            "resolves" => Ok(RelationshipType::Resolves),
            "blocks" => Ok(RelationshipType::Blocks),
            "depends-on" => Ok(RelationshipType::DependsOn),
            _ => Err(format!("Unknown RelationshipType: {}", s)),
        }
    }
}

// ---------------------------------------------------------------------------
// Node
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Node {
    pub id: NodeId,
    pub node_type: NodeType,
    pub dag_stage: DagStage,
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    pub status: Status,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    pub meta: Meta,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub properties: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub table_ref: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub table_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub updated_at: Option<String>,
}

// ---------------------------------------------------------------------------
// Edge
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Edge {
    pub id: String,
    pub source: NodeId,
    pub target: NodeId,
    pub relationship_type: RelationshipType,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub canonical_map: Option<CanonicalMap>,
    pub meta: Meta,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub properties: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
}