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
//! `NodeBuilder` — constructs `Node` records with required field validation.

use crate::intent_graph::{DagStage, Meta, Node, NodeType, Status};

/// Builder for `Node`.
#[derive(Debug, Default)]
pub struct NodeBuilder {
    id: Option<String>,
    node_type: Option<NodeType>,
    dag_stage: Option<DagStage>,
    title: Option<String>,
    description: Option<String>,
    status: Option<Status>,
    tags: Vec<String>,
    meta: Option<Meta>,
    properties: Option<serde_json::Value>,
    table_ref: Option<String>,
    table_id: Option<String>,
    created_at: Option<String>,
    updated_at: Option<String>,
}

impl NodeBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn id(mut self, id: impl Into<String>) -> Self {
        self.id = Some(id.into());
        self
    }

    pub fn node_type(mut self, nt: NodeType) -> Self {
        self.node_type = Some(nt);
        self
    }

    pub fn dag_stage(mut self, ds: DagStage) -> Self {
        self.dag_stage = Some(ds);
        self
    }

    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    pub fn description(mut self, desc: impl Into<String>) -> Self {
        self.description = Some(desc.into());
        self
    }

    pub fn status(mut self, status: Status) -> Self {
        self.status = Some(status);
        self
    }

    pub fn tag(mut self, tag: impl Into<String>) -> Self {
        self.tags.push(tag.into());
        self
    }

    pub fn tags(mut self, tags: Vec<String>) -> Self {
        self.tags = tags;
        self
    }

    pub fn meta(mut self, meta: Meta) -> Self {
        self.meta = Some(meta);
        self
    }

    pub fn with_meta(mut self, source: impl Into<String>) -> Self {
        let now = chrono::Utc::now().to_rfc3339();
        self.meta = Some(Meta {
            confidence: None,
            source: source.into(),
            timestamp: now,
            agent_id: None,
        });
        self
    }

    pub fn properties(mut self, props: serde_json::Value) -> Self {
        self.properties = Some(props);
        self
    }

    pub fn table_ref(mut self, table_ref: impl Into<String>) -> Self {
        self.table_ref = Some(table_ref.into());
        self
    }

    pub fn table_id(mut self, table_id: impl Into<String>) -> Self {
        self.table_id = Some(table_id.into());
        self
    }

    pub fn created_at(mut self, at: impl Into<String>) -> Self {
        self.created_at = Some(at.into());
        self
    }

    pub fn updated_at(mut self, at: impl Into<String>) -> Self {
        self.updated_at = Some(at.into());
        self
    }

    pub fn build(self) -> Result<Node, String> {
        let now = chrono::Utc::now().to_rfc3339();
        Ok(Node {
            id: crate::intent_graph::NodeId::new(self.id.ok_or("id is required")?),
            node_type: self.node_type.ok_or("node_type is required")?,
            dag_stage: self.dag_stage.ok_or("dag_stage is required")?,
            title: self.title.ok_or("title is required")?,
            description: self.description,
            status: self.status.ok_or("status is required")?,
            tags: self.tags,
            meta: self.meta.ok_or("meta is required")?,
            properties: self.properties,
            table_ref: self.table_ref,
            table_id: self.table_id,
            created_at: Some(self.created_at.unwrap_or_else(|| now.clone())),
            updated_at: Some(self.updated_at.unwrap_or(now)),
        })
    }
}