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