Skip to main content

agileplus_domain/intent_graph/
graph.rs

1//! The `IntentGraph` aggregate, the DAG flow ontology, edge constraints,
2//! and the bundled `Ontology` with a default 1.0.0 instance.
3
4use serde::{Deserialize, Serialize};
5
6use super::metadata::GraphMetadata;
7use super::nodes::{Edge, Node, NodeId, NodeType};
8
9// ---------------------------------------------------------------------------
10// Intent Graph
11// ---------------------------------------------------------------------------
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub struct IntentGraph {
16    pub nodes: Vec<Node>,
17    pub edges: Vec<Edge>,
18    pub metadata: GraphMetadata,
19}
20
21impl IntentGraph {
22    pub fn new(metadata: GraphMetadata) -> Self {
23        Self {
24            nodes: Vec::new(),
25            edges: Vec::new(),
26            metadata,
27        }
28    }
29
30    pub fn add_node(&mut self, node: Node) {
31        self.nodes.push(node);
32    }
33
34    pub fn add_edge(&mut self, edge: Edge) {
35        self.edges.push(edge);
36    }
37
38    pub fn node_by_id(&self, id: &NodeId) -> Option<&Node> {
39        self.nodes.iter().find(|n| n.id == *id)
40    }
41
42    pub fn edges_from(&self, id: &NodeId) -> Vec<&Edge> {
43        self.edges.iter().filter(|e| e.source == *id).collect()
44    }
45
46    pub fn edges_to(&self, id: &NodeId) -> Vec<&Edge> {
47        self.edges.iter().filter(|e| e.target == *id).collect()
48    }
49
50    pub fn update_counts(&mut self) {
51        self.metadata.node_count = Some(self.nodes.len());
52        self.metadata.edge_count = Some(self.edges.len());
53    }
54}
55
56// ---------------------------------------------------------------------------
57// DAG Flow Stage
58// ---------------------------------------------------------------------------
59
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub struct DagFlowStage {
63    pub stage: String,
64    pub node_types: Vec<NodeType>,
65    pub produces: Vec<NodeType>,
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub struct DagFlow {
71    pub stages: Vec<DagFlowStage>,
72    pub validation_rules: DagValidationRules,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub struct DagValidationRules {
78    pub no_cycles: bool,
79    pub root_nodes_must_be: Vec<NodeType>,
80    pub leaf_nodes_prefer: Vec<NodeType>,
81}
82
83// ---------------------------------------------------------------------------
84// Edge Constraints
85// ---------------------------------------------------------------------------
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88#[serde(rename_all = "kebab-case")]
89pub struct EdgeConstraints {
90    pub implements: EdgeConstraint,
91    pub tests: EdgeConstraint,
92    pub covers: EdgeConstraint,
93    #[serde(rename = "traces-to")]
94    pub traces_to: EdgeConstraint,
95    #[serde(rename = "derives-from")]
96    pub derives_from: EdgeConstraint,
97    pub resolves: EdgeConstraint,
98    pub blocks: EdgeConstraint,
99    #[serde(rename = "depends-on")]
100    pub depends_on: EdgeConstraint,
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub struct EdgeConstraint {
106    pub description: String,
107    pub allowed: Vec<EdgeConstraintEntry>,
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub struct EdgeConstraintEntry {
113    pub from: String,
114    pub to: String,
115}
116
117// ---------------------------------------------------------------------------
118// Ontology
119// ---------------------------------------------------------------------------
120
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub struct Ontology {
124    pub version: String,
125    pub schema_uri: String,
126    pub dag_flow: DagFlow,
127    pub edge_constraints: EdgeConstraints,
128}
129
130impl Default for Ontology {
131    fn default() -> Self {
132        Self {
133            version: "1.0.0".to_string(),
134            schema_uri: "https://phenotype.dev/schemas/agileplus-intent-ontology/v1.json".to_string(),
135            dag_flow: DagFlow {
136                stages: vec![
137                    DagFlowStage { stage: "intake".to_string(), node_types: vec![NodeType::Intent], produces: vec![NodeType::Plan] },
138                    DagFlowStage { stage: "planning".to_string(), node_types: vec![NodeType::Plan], produces: vec![NodeType::Feature, NodeType::Story] },
139                    DagFlowStage { stage: "breakdown".to_string(), node_types: vec![NodeType::Feature, NodeType::Story], produces: vec![NodeType::Task] },
140                    DagFlowStage { stage: "specification".to_string(), node_types: vec![NodeType::Task], produces: vec![NodeType::Spec] },
141                    DagFlowStage { stage: "implementation".to_string(), node_types: vec![NodeType::Task], produces: vec![NodeType::Commit] },
142                    DagFlowStage { stage: "verification".to_string(), node_types: vec![NodeType::Commit, NodeType::PR], produces: vec![NodeType::Test, NodeType::Artifact] },
143                    DagFlowStage { stage: "integration".to_string(), node_types: vec![NodeType::PR], produces: vec![NodeType::Artifact] },
144                ],
145                validation_rules: DagValidationRules {
146                    no_cycles: true,
147                    root_nodes_must_be: vec![NodeType::Intent],
148                    leaf_nodes_prefer: vec![NodeType::Test, NodeType::Artifact],
149                },
150            },
151            edge_constraints: EdgeConstraints {
152                implements: EdgeConstraint {
153                    description: "Target node implements the source node's requirement.".to_string(),
154                    allowed: vec![
155                        EdgeConstraintEntry { from: "Intent".to_string(), to: "Feature".to_string() },
156                        EdgeConstraintEntry { from: "Intent".to_string(), to: "Story".to_string() },
157                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Task".to_string() },
158                        EdgeConstraintEntry { from: "Story".to_string(), to: "Task".to_string() },
159                        EdgeConstraintEntry { from: "Task".to_string(), to: "Commit".to_string() },
160                        EdgeConstraintEntry { from: "Spec".to_string(), to: "Feature".to_string() },
161                        EdgeConstraintEntry { from: "Spec".to_string(), to: "Task".to_string() },
162                    ],
163                },
164                tests: EdgeConstraint {
165                    description: "Target node tests the source node's behavior.".to_string(),
166                    allowed: vec![
167                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Test".to_string() },
168                        EdgeConstraintEntry { from: "Task".to_string(), to: "Test".to_string() },
169                        EdgeConstraintEntry { from: "Commit".to_string(), to: "Test".to_string() },
170                        EdgeConstraintEntry { from: "PR".to_string(), to: "Test".to_string() },
171                        EdgeConstraintEntry { from: "Bug".to_string(), to: "Test".to_string() },
172                    ],
173                },
174                covers: EdgeConstraint {
175                    description: "Target node provides coverage or evidence for the source node.".to_string(),
176                    allowed: vec![
177                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Test".to_string() },
178                        EdgeConstraintEntry { from: "Task".to_string(), to: "Test".to_string() },
179                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Artifact".to_string() },
180                        EdgeConstraintEntry { from: "Task".to_string(), to: "Artifact".to_string() },
181                        EdgeConstraintEntry { from: "Spec".to_string(), to: "Feature".to_string() },
182                    ],
183                },
184                traces_to: EdgeConstraint {
185                    description: "Generic traceability link for cross-cutting concerns.".to_string(),
186                    allowed: vec![
187                        EdgeConstraintEntry { from: "*".to_string(), to: "*".to_string() },
188                    ],
189                },
190                derives_from: EdgeConstraint {
191                    description: "Target is the parent or origin of the source.".to_string(),
192                    allowed: vec![
193                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Intent".to_string() },
194                        EdgeConstraintEntry { from: "Story".to_string(), to: "Intent".to_string() },
195                        EdgeConstraintEntry { from: "Story".to_string(), to: "Feature".to_string() },
196                        EdgeConstraintEntry { from: "Task".to_string(), to: "Feature".to_string() },
197                        EdgeConstraintEntry { from: "Task".to_string(), to: "Story".to_string() },
198                        EdgeConstraintEntry { from: "Task".to_string(), to: "Bug".to_string() },
199                    ],
200                },
201                resolves: EdgeConstraint {
202                    description: "Target resolves or fixes the source issue.".to_string(),
203                    allowed: vec![
204                        EdgeConstraintEntry { from: "Bug".to_string(), to: "Commit".to_string() },
205                        EdgeConstraintEntry { from: "Bug".to_string(), to: "PR".to_string() },
206                        EdgeConstraintEntry { from: "Bug".to_string(), to: "Task".to_string() },
207                    ],
208                },
209                blocks: EdgeConstraint {
210                    description: "Target is blocked by the source.".to_string(),
211                    allowed: vec![
212                        EdgeConstraintEntry { from: "Task".to_string(), to: "Task".to_string() },
213                        EdgeConstraintEntry { from: "Bug".to_string(), to: "Task".to_string() },
214                        EdgeConstraintEntry { from: "Bug".to_string(), to: "PR".to_string() },
215                        EdgeConstraintEntry { from: "Task".to_string(), to: "PR".to_string() },
216                        EdgeConstraintEntry { from: "PR".to_string(), to: "Feature".to_string() },
217                    ],
218                },
219                depends_on: EdgeConstraint {
220                    description: "Source depends on target for completion.".to_string(),
221                    allowed: vec![
222                        EdgeConstraintEntry { from: "Task".to_string(), to: "Task".to_string() },
223                        EdgeConstraintEntry { from: "Feature".to_string(), to: "Feature".to_string() },
224                        EdgeConstraintEntry { from: "Story".to_string(), to: "Story".to_string() },
225                        EdgeConstraintEntry { from: "PR".to_string(), to: "PR".to_string() },
226                        EdgeConstraintEntry { from: "Task".to_string(), to: "Artifact".to_string() },
227                    ],
228                },
229            },
230        }
231    }
232}
233
234// ---------------------------------------------------------------------------
235// Validation helpers
236// ---------------------------------------------------------------------------
237
238impl IntentGraph {
239    pub fn validate_id_format(&self) -> Vec<String> {
240        let mut errors = Vec::new();
241        let re = regex::Regex::new(r"^[A-Z][a-z]+#[a-z0-9\-]+$").unwrap();
242        for node in &self.nodes {
243            if !re.is_match(&node.id.0) {
244                errors.push(format!("Invalid node ID format: {}", node.id));
245            }
246        }
247        errors
248    }
249}