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
//! The `IntentGraph` aggregate, the DAG flow ontology, edge constraints,
//! and the bundled `Ontology` with a default 1.0.0 instance.

use serde::{Deserialize, Serialize};

use super::metadata::GraphMetadata;
use super::nodes::{Edge, Node, NodeId, NodeType};

// ---------------------------------------------------------------------------
// Intent Graph
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct IntentGraph {
    pub nodes: Vec<Node>,
    pub edges: Vec<Edge>,
    pub metadata: GraphMetadata,
}

impl IntentGraph {
    pub fn new(metadata: GraphMetadata) -> Self {
        Self {
            nodes: Vec::new(),
            edges: Vec::new(),
            metadata,
        }
    }

    pub fn add_node(&mut self, node: Node) {
        self.nodes.push(node);
    }

    pub fn add_edge(&mut self, edge: Edge) {
        self.edges.push(edge);
    }

    pub fn node_by_id(&self, id: &NodeId) -> Option<&Node> {
        self.nodes.iter().find(|n| n.id == *id)
    }

    pub fn edges_from(&self, id: &NodeId) -> Vec<&Edge> {
        self.edges.iter().filter(|e| e.source == *id).collect()
    }

    pub fn edges_to(&self, id: &NodeId) -> Vec<&Edge> {
        self.edges.iter().filter(|e| e.target == *id).collect()
    }

    pub fn update_counts(&mut self) {
        self.metadata.node_count = Some(self.nodes.len());
        self.metadata.edge_count = Some(self.edges.len());
    }
}

// ---------------------------------------------------------------------------
// DAG Flow Stage
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DagFlowStage {
    pub stage: String,
    pub node_types: Vec<NodeType>,
    pub produces: Vec<NodeType>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DagFlow {
    pub stages: Vec<DagFlowStage>,
    pub validation_rules: DagValidationRules,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DagValidationRules {
    pub no_cycles: bool,
    pub root_nodes_must_be: Vec<NodeType>,
    pub leaf_nodes_prefer: Vec<NodeType>,
}

// ---------------------------------------------------------------------------
// Edge Constraints
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct EdgeConstraints {
    pub implements: EdgeConstraint,
    pub tests: EdgeConstraint,
    pub covers: EdgeConstraint,
    #[serde(rename = "traces-to")]
    pub traces_to: EdgeConstraint,
    #[serde(rename = "derives-from")]
    pub derives_from: EdgeConstraint,
    pub resolves: EdgeConstraint,
    pub blocks: EdgeConstraint,
    #[serde(rename = "depends-on")]
    pub depends_on: EdgeConstraint,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EdgeConstraint {
    pub description: String,
    pub allowed: Vec<EdgeConstraintEntry>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EdgeConstraintEntry {
    pub from: String,
    pub to: String,
}

// ---------------------------------------------------------------------------
// Ontology
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Ontology {
    pub version: String,
    pub schema_uri: String,
    pub dag_flow: DagFlow,
    pub edge_constraints: EdgeConstraints,
}

impl Default for Ontology {
    fn default() -> Self {
        Self {
            version: "1.0.0".to_string(),
            schema_uri: "https://phenotype.dev/schemas/agileplus-intent-ontology/v1.json".to_string(),
            dag_flow: DagFlow {
                stages: vec![
                    DagFlowStage { stage: "intake".to_string(), node_types: vec![NodeType::Intent], produces: vec![NodeType::Plan] },
                    DagFlowStage { stage: "planning".to_string(), node_types: vec![NodeType::Plan], produces: vec![NodeType::Feature, NodeType::Story] },
                    DagFlowStage { stage: "breakdown".to_string(), node_types: vec![NodeType::Feature, NodeType::Story], produces: vec![NodeType::Task] },
                    DagFlowStage { stage: "specification".to_string(), node_types: vec![NodeType::Task], produces: vec![NodeType::Spec] },
                    DagFlowStage { stage: "implementation".to_string(), node_types: vec![NodeType::Task], produces: vec![NodeType::Commit] },
                    DagFlowStage { stage: "verification".to_string(), node_types: vec![NodeType::Commit, NodeType::PR], produces: vec![NodeType::Test, NodeType::Artifact] },
                    DagFlowStage { stage: "integration".to_string(), node_types: vec![NodeType::PR], produces: vec![NodeType::Artifact] },
                ],
                validation_rules: DagValidationRules {
                    no_cycles: true,
                    root_nodes_must_be: vec![NodeType::Intent],
                    leaf_nodes_prefer: vec![NodeType::Test, NodeType::Artifact],
                },
            },
            edge_constraints: EdgeConstraints {
                implements: EdgeConstraint {
                    description: "Target node implements the source node's requirement.".to_string(),
                    allowed: vec![
                        EdgeConstraintEntry { from: "Intent".to_string(), to: "Feature".to_string() },
                        EdgeConstraintEntry { from: "Intent".to_string(), to: "Story".to_string() },
                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Task".to_string() },
                        EdgeConstraintEntry { from: "Story".to_string(), to: "Task".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Commit".to_string() },
                        EdgeConstraintEntry { from: "Spec".to_string(), to: "Feature".to_string() },
                        EdgeConstraintEntry { from: "Spec".to_string(), to: "Task".to_string() },
                    ],
                },
                tests: EdgeConstraint {
                    description: "Target node tests the source node's behavior.".to_string(),
                    allowed: vec![
                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Test".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Test".to_string() },
                        EdgeConstraintEntry { from: "Commit".to_string(), to: "Test".to_string() },
                        EdgeConstraintEntry { from: "PR".to_string(), to: "Test".to_string() },
                        EdgeConstraintEntry { from: "Bug".to_string(), to: "Test".to_string() },
                    ],
                },
                covers: EdgeConstraint {
                    description: "Target node provides coverage or evidence for the source node.".to_string(),
                    allowed: vec![
                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Test".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Test".to_string() },
                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Artifact".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Artifact".to_string() },
                        EdgeConstraintEntry { from: "Spec".to_string(), to: "Feature".to_string() },
                    ],
                },
                traces_to: EdgeConstraint {
                    description: "Generic traceability link for cross-cutting concerns.".to_string(),
                    allowed: vec![
                        EdgeConstraintEntry { from: "*".to_string(), to: "*".to_string() },
                    ],
                },
                derives_from: EdgeConstraint {
                    description: "Target is the parent or origin of the source.".to_string(),
                    allowed: vec![
                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Intent".to_string() },
                        EdgeConstraintEntry { from: "Story".to_string(), to: "Intent".to_string() },
                        EdgeConstraintEntry { from: "Story".to_string(), to: "Feature".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Feature".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Story".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Bug".to_string() },
                    ],
                },
                resolves: EdgeConstraint {
                    description: "Target resolves or fixes the source issue.".to_string(),
                    allowed: vec![
                        EdgeConstraintEntry { from: "Bug".to_string(), to: "Commit".to_string() },
                        EdgeConstraintEntry { from: "Bug".to_string(), to: "PR".to_string() },
                        EdgeConstraintEntry { from: "Bug".to_string(), to: "Task".to_string() },
                    ],
                },
                blocks: EdgeConstraint {
                    description: "Target is blocked by the source.".to_string(),
                    allowed: vec![
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Task".to_string() },
                        EdgeConstraintEntry { from: "Bug".to_string(), to: "Task".to_string() },
                        EdgeConstraintEntry { from: "Bug".to_string(), to: "PR".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "PR".to_string() },
                        EdgeConstraintEntry { from: "PR".to_string(), to: "Feature".to_string() },
                    ],
                },
                depends_on: EdgeConstraint {
                    description: "Source depends on target for completion.".to_string(),
                    allowed: vec![
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Task".to_string() },
                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Feature".to_string() },
                        EdgeConstraintEntry { from: "Story".to_string(), to: "Story".to_string() },
                        EdgeConstraintEntry { from: "PR".to_string(), to: "PR".to_string() },
                        EdgeConstraintEntry { from: "Task".to_string(), to: "Artifact".to_string() },
                    ],
                },
            },
        }
    }
}

// ---------------------------------------------------------------------------
// Validation helpers
// ---------------------------------------------------------------------------

impl IntentGraph {
    pub fn validate_id_format(&self) -> Vec<String> {
        let mut errors = Vec::new();
        let re = regex::Regex::new(r"^[A-Z][a-z]+#[a-z0-9\-]+$").unwrap();
        for node in &self.nodes {
            if !re.is_match(&node.id.0) {
                errors.push(format!("Invalid node ID format: {}", node.id));
            }
        }
        errors
    }
}