1use serde::{Deserialize, Serialize};
5
6use super::metadata::GraphMetadata;
7use super::nodes::{Edge, Node, NodeId, NodeType};
8
9#[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#[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#[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#[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
234impl 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}