1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use super::code::CodeBlock;
8use super::context::AgentContext;
9use super::execution::ExecutionStatus;
10use super::fields::{
11 Confidence, FieldValue, NodeStatus, NodeType, Priority, SddPhase, Span, Stability, TicketAction,
12};
13use super::memory::MemoryEntry;
14use super::orchestration::ParallelGroup;
15use super::verify::VerifyCheck;
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct Node {
19 #[serde(rename = "node")]
20 pub id: String,
21 #[serde(rename = "type")]
22 pub node_type: NodeType,
23 pub summary: String,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub priority: Option<Priority>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub stability: Option<Stability>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub confidence: Option<Confidence>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub status: Option<NodeStatus>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub depends: Option<Vec<String>>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub related_to: Option<Vec<String>>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub replaces: Option<Vec<String>>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub conflicts: Option<Vec<String>>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub see_also: Option<Vec<String>>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub items: Option<Vec<String>>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub steps: Option<Vec<String>>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub fields: Option<Vec<String>>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub input: Option<Vec<String>>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub output: Option<Vec<String>>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub detail: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub rationale: Option<Vec<String>>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub tradeoffs: Option<Vec<String>>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub resolution: Option<Vec<String>>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub examples: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub notes: Option<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub code: Option<CodeBlock>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub code_blocks: Option<Vec<CodeBlock>>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub verify: Option<Vec<VerifyCheck>>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub agent_context: Option<AgentContext>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub target: Option<String>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub execution_status: Option<ExecutionStatus>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub executed_by: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub executed_at: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub execution_log: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub retry_count: Option<u32>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub parallel_groups: Option<Vec<ParallelGroup>>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub memory: Option<Vec<MemoryEntry>>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub scope: Option<Vec<String>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub applies_when: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub valid_from: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub valid_until: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub tags: Option<Vec<String>>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub aliases: Option<Vec<String>>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub keywords: Option<Vec<String>>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub title: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub description: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub action: Option<TicketAction>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub sdd_phase: Option<SddPhase>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub prompt: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub assignee: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub labels: Option<Vec<String>>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub ticket_id: Option<String>,
138
139 #[serde(flatten)]
141 pub extra_fields: BTreeMap<String, FieldValue>,
142
143 #[serde(skip)]
145 pub span: Span,
146}
147
148impl Default for Node {
149 fn default() -> Self {
150 Self {
151 id: String::new(),
152 node_type: NodeType::Facts,
153 summary: String::new(),
154 priority: None,
155 stability: None,
156 confidence: None,
157 status: None,
158 depends: None,
159 related_to: None,
160 replaces: None,
161 conflicts: None,
162 see_also: None,
163 items: None,
164 steps: None,
165 fields: None,
166 input: None,
167 output: None,
168 detail: None,
169 rationale: None,
170 tradeoffs: None,
171 resolution: None,
172 examples: None,
173 notes: None,
174 code: None,
175 code_blocks: None,
176 verify: None,
177 agent_context: None,
178 target: None,
179 execution_status: None,
180 executed_by: None,
181 executed_at: None,
182 execution_log: None,
183 retry_count: None,
184 parallel_groups: None,
185 memory: None,
186 scope: None,
187 applies_when: None,
188 valid_from: None,
189 valid_until: None,
190 tags: None,
191 aliases: None,
192 keywords: None,
193 title: None,
194 description: None,
195 action: None,
196 sdd_phase: None,
197 prompt: None,
198 assignee: None,
199 labels: None,
200 ticket_id: None,
201 extra_fields: BTreeMap::new(),
202 span: Span::new(0, 0),
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn minimal_node() -> Node {
212 Node {
213 id: "test.node".to_owned(),
214 node_type: NodeType::Facts,
215 summary: "a test node".to_owned(),
216 ..Default::default()
217 }
218 }
219
220 #[test]
221 fn test_node_minimal_serde_roundtrip() {
222 let node = minimal_node();
223 let json = serde_json::to_string(&node).unwrap();
224 let back: Node = serde_json::from_str(&json).unwrap();
225 assert_eq!(node.id, back.id);
226 assert_eq!(node.node_type, back.node_type);
227 assert_eq!(node.summary, back.summary);
228 }
229
230 #[test]
231 fn test_node_json_uses_spec_field_names() {
232 let node = minimal_node();
233 let json = serde_json::to_string(&node).unwrap();
234 assert!(json.contains("\"node\""));
235 assert!(json.contains("\"type\""));
236 assert!(!json.contains("\"id\""));
237 assert!(!json.contains("\"node_type\""));
238 }
239
240 #[test]
241 fn test_node_optional_fields_absent_in_json() {
242 let node = minimal_node();
243 let json = serde_json::to_string(&node).unwrap();
244 assert!(!json.contains("priority"));
245 assert!(!json.contains("depends"));
246 assert!(!json.contains("steps"));
247 assert!(!json.contains("code"));
248 assert!(!json.contains("execution_status"));
249 assert!(!json.contains("memory"));
250 }
251
252 #[test]
253 fn test_node_with_relationships_serde() {
254 let mut node = minimal_node();
255 node.depends = Some(vec!["auth.constraints".to_owned()]);
256 node.related_to = Some(vec!["auth.session".to_owned()]);
257 let json = serde_json::to_string(&node).unwrap();
258 let back: Node = serde_json::from_str(&json).unwrap();
259 assert_eq!(node.depends, back.depends);
260 assert_eq!(node.related_to, back.related_to);
261 }
262
263 #[test]
264 fn test_node_with_execution_state_serde() {
265 let mut node = minimal_node();
266 node.execution_status = Some(ExecutionStatus::Completed);
267 node.executed_by = Some("agent-01".to_owned());
268 node.executed_at = Some("2026-04-03T14:32:00Z".to_owned());
269 node.retry_count = Some(0);
270 let json = serde_json::to_string(&node).unwrap();
271 let back: Node = serde_json::from_str(&json).unwrap();
272 assert_eq!(node.execution_status, back.execution_status);
273 assert_eq!(node.executed_by, back.executed_by);
274 }
275
276 #[test]
277 fn test_node_extra_fields_preserved() {
278 let mut node = minimal_node();
279 node.extra_fields.insert(
280 "custom_field".to_owned(),
281 FieldValue::Scalar("value".to_owned()),
282 );
283 let json = serde_json::to_string(&node).unwrap();
284 assert!(json.contains("custom_field"));
285 let back: Node = serde_json::from_str(&json).unwrap();
286 assert_eq!(
287 back.extra_fields.get("custom_field"),
288 Some(&FieldValue::Scalar("value".to_owned()))
289 );
290 }
291
292 #[test]
293 fn test_node_span_not_serialized() {
294 let mut node = minimal_node();
295 node.span = Span::new(5, 20);
296 let json = serde_json::to_string(&node).unwrap();
297 assert!(!json.contains("start_line"));
298 assert!(!json.contains("end_line"));
299 assert!(!json.contains("span"));
300 }
301
302 #[test]
303 fn test_node_deserialize_from_spec_appendix_b() {
304 let json = r#"{
305 "node": "auth.login",
306 "type": "workflow",
307 "stability": "medium",
308 "priority": "critical",
309 "depends": ["auth.constraints", "auth.session"],
310 "input": ["host", "return_url"],
311 "output": ["redirect_url", "sid_cookie"],
312 "summary": "resolve tenant -> redirect -> callback -> create sid",
313 "steps": ["resolve tenant", "redirect to provider"],
314 "execution_status": "pending",
315 "retry_count": 0
316 }"#;
317 let node: Node = serde_json::from_str(json).unwrap();
318 assert_eq!(node.id, "auth.login");
319 assert_eq!(node.node_type, NodeType::Workflow);
320 assert_eq!(node.priority, Some(Priority::Critical));
321 assert_eq!(node.stability, Some(Stability::Medium));
322 assert_eq!(node.depends.as_ref().unwrap().len(), 2);
323 assert_eq!(node.steps.as_ref().unwrap().len(), 2);
324 assert_eq!(node.execution_status, Some(ExecutionStatus::Pending));
325 assert_eq!(node.retry_count, Some(0));
326 }
327}