1use std::sync::LazyLock;
4
5use regex::Regex;
6
7use crate::error::{AgmError, ErrorCode, ErrorLocation};
8use crate::model::execution::ExecutionStatus;
9use crate::model::fields::{
10 Confidence, FieldValue, NodeStatus, Priority, SddPhase, Span, Stability, TicketAction,
11};
12use crate::model::node::Node;
13
14use super::fields::{
15 FieldTracker, collect_structured_raw, is_structured_field, parse_block, parse_indented_list,
16 skip_field_body,
17};
18use super::lexer::{Line, LineKind};
19use super::structured::{
20 parse_agent_context, parse_code_block, parse_code_blocks, parse_memory, parse_parallel_groups,
21 parse_verify,
22};
23
24static NODE_ID_RE: LazyLock<Regex> =
29 LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9_]*([.\-][a-z][a-z0-9_]*)*$").unwrap());
30
31fn default_node(id: String, start_line: usize) -> Node {
40 Node {
41 id,
42 span: Span::new(start_line, start_line),
43 ..Default::default()
44 }
45}
46
47pub fn parse_node(lines: &[Line], pos: &mut usize, errors: &mut Vec<AgmError>) -> Node {
55 let (id, declaration_line) = match &lines[*pos].kind {
57 LineKind::NodeDeclaration(id) => (id.clone(), lines[*pos].number),
58 _ => {
59 return default_node(String::new(), lines[*pos].number);
61 }
62 };
63
64 if id.is_empty() {
66 errors.push(AgmError::new(
67 ErrorCode::P002,
68 "Node declaration missing ID",
69 ErrorLocation::new(None, Some(declaration_line), None),
70 ));
71 } else if !NODE_ID_RE.is_match(&id) {
72 errors.push(AgmError::new(
73 ErrorCode::P002,
74 format!("Invalid node ID: {id:?} (must match [a-z][a-z0-9]*([.\\-][a-z][a-z0-9]*)*)"),
75 ErrorLocation::new(None, Some(declaration_line), None),
76 ));
77 }
78
79 let mut node = default_node(id, declaration_line);
80 *pos += 1; let mut tracker = FieldTracker::new();
83 let mut last_line = declaration_line;
84
85 while *pos < lines.len() {
86 match &lines[*pos].kind.clone() {
87 LineKind::NodeDeclaration(_) => break,
88
89 LineKind::Blank | LineKind::Comment | LineKind::TestExpectHeader(_) => {
90 last_line = lines[*pos].number;
91 *pos += 1;
92 }
93
94 LineKind::ScalarField(key, value) => {
95 let key = key.clone();
96 let value = value.clone();
97 let line_number = lines[*pos].number;
98 last_line = line_number;
99
100 if tracker.track(&key) {
101 errors.push(AgmError::new(
102 ErrorCode::P006,
103 format!("Duplicate field `{key}` in node `{}`", node.id),
104 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
105 ));
106 *pos += 1;
107 continue;
108 }
109
110 assign_scalar_field(&mut node, &key, &value, line_number, errors);
111 *pos += 1;
112 }
113
114 LineKind::InlineListField(key, items) => {
115 let key = key.clone();
116 let items = items.clone();
117 let line_number = lines[*pos].number;
118 last_line = line_number;
119
120 if tracker.track(&key) {
121 errors.push(AgmError::new(
122 ErrorCode::P006,
123 format!("Duplicate field `{key}` in node `{}`", node.id),
124 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
125 ));
126 *pos += 1;
127 continue;
128 }
129
130 assign_list_field(&mut node, &key, items);
131 *pos += 1;
132 }
133
134 LineKind::FieldStart(key) => {
135 let key = key.clone();
136 let line_number = lines[*pos].number;
137 last_line = line_number;
138
139 if tracker.track(&key) {
140 errors.push(AgmError::new(
141 ErrorCode::P006,
142 format!("Duplicate field `{key}` in node `{}`", node.id),
143 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
144 ));
145 *pos += 1;
146 skip_field_body(lines, pos);
147 continue;
148 }
149
150 *pos += 1; if is_structured_field(&key) {
153 match key.as_str() {
154 "code" => {
155 node.code = Some(parse_code_block(lines, pos, errors));
156 }
157 "code_blocks" => {
158 node.code_blocks = Some(parse_code_blocks(lines, pos, errors));
159 }
160 "verify" => {
161 node.verify = Some(parse_verify(lines, pos, errors));
162 }
163 "agent_context" => {
164 node.agent_context = Some(parse_agent_context(lines, pos, errors));
165 }
166 "parallel_groups" => {
167 node.parallel_groups = Some(parse_parallel_groups(lines, pos, errors));
168 }
169 "memory" => {
170 node.memory = Some(parse_memory(lines, pos, errors));
171 }
172 _ => {
173 let raw = collect_structured_raw(lines, pos);
175 node.extra_fields.insert(key, FieldValue::Block(raw));
176 }
177 }
178 } else {
179 let peek_pos = {
181 let mut p = *pos;
182 while p < lines.len()
183 && matches!(
184 &lines[p].kind,
185 LineKind::Comment | LineKind::TestExpectHeader(_)
186 )
187 {
188 p += 1;
189 }
190 p
191 };
192 let is_list = peek_pos < lines.len()
193 && matches!(&lines[peek_pos].kind, LineKind::ListItem(_));
194
195 if is_list {
196 let items = parse_indented_list(lines, pos);
197 assign_list_field(&mut node, &key, items);
198 } else {
199 let text = parse_block(lines, pos);
200 assign_block_field(&mut node, &key, text);
201 }
202 }
203 }
204
205 LineKind::BodyMarker => {
206 let line_number = lines[*pos].number;
207 last_line = line_number;
208
209 let is_dup = tracker.track("body");
210 *pos += 1; let text = parse_block(lines, pos);
213
214 if is_dup {
215 node.extra_fields
217 .insert("body".to_owned(), FieldValue::Block(text));
218 } else {
219 if node.detail.is_none() {
221 node.detail = Some(text);
222 } else {
223 node.extra_fields
224 .insert("body".to_owned(), FieldValue::Block(text));
225 }
226 }
227 }
228
229 LineKind::ListItem(_) | LineKind::IndentedLine(_) => {
230 errors.push(AgmError::new(
231 ErrorCode::P003,
232 format!(
233 "Unexpected indentation at line {} in node `{}`",
234 lines[*pos].number, node.id
235 ),
236 ErrorLocation::new(None, Some(lines[*pos].number), Some(node.id.clone())),
237 ));
238 last_line = lines[*pos].number;
239 *pos += 1;
240 }
241 }
242 }
243
244 node.span.end_line = last_line;
245 node
246}
247
248fn assign_scalar_field(
253 node: &mut Node,
254 key: &str,
255 value: &str,
256 line_number: usize,
257 errors: &mut Vec<AgmError>,
258) {
259 match key {
260 "type" => {
261 node.node_type = value.parse().unwrap(); }
263 "summary" => {
264 node.summary = value.to_owned();
265 }
266 "priority" => match value.parse::<Priority>() {
267 Ok(p) => node.priority = Some(p),
268 Err(_) => {
269 errors.push(AgmError::new(
270 ErrorCode::P003,
271 format!("Invalid `priority` value: {value:?}"),
272 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
273 ));
274 }
275 },
276 "stability" => match value.parse::<Stability>() {
277 Ok(s) => node.stability = Some(s),
278 Err(_) => {
279 errors.push(AgmError::new(
280 ErrorCode::P003,
281 format!("Invalid `stability` value: {value:?}"),
282 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
283 ));
284 }
285 },
286 "confidence" => match value.parse::<Confidence>() {
287 Ok(c) => node.confidence = Some(c),
288 Err(_) => {
289 errors.push(AgmError::new(
290 ErrorCode::P003,
291 format!("Invalid `confidence` value: {value:?}"),
292 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
293 ));
294 }
295 },
296 "status" => match value.parse::<NodeStatus>() {
297 Ok(s) => node.status = Some(s),
298 Err(_) => {
299 errors.push(AgmError::new(
300 ErrorCode::P003,
301 format!("Invalid `status` value: {value:?}"),
302 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
303 ));
304 }
305 },
306 "detail" => node.detail = Some(value.to_owned()),
307 "examples" => node.examples = Some(value.to_owned()),
308 "notes" => node.notes = Some(value.to_owned()),
309 "target" => node.target = Some(value.to_owned()),
310 "applies_when" => node.applies_when = Some(value.to_owned()),
311 "valid_from" => node.valid_from = Some(value.to_owned()),
312 "valid_until" => node.valid_until = Some(value.to_owned()),
313 "title" => node.title = Some(value.to_owned()),
315 "description" => node.description = Some(value.to_owned()),
316 "action" => match value.parse::<TicketAction>() {
317 Ok(a) => node.action = Some(a),
318 Err(_) => errors.push(AgmError::new(
319 ErrorCode::V029,
320 format!("Invalid `action` value: {value:?}"),
321 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
322 )),
323 },
324 "sdd_phase" => match value.parse::<SddPhase>() {
325 Ok(p) => node.sdd_phase = Some(p),
326 Err(_) => errors.push(AgmError::new(
327 ErrorCode::V030,
328 format!("Invalid `sdd_phase` value: {value:?}"),
329 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
330 )),
331 },
332 "prompt" => node.prompt = Some(value.to_owned()),
333 "assignee" => node.assignee = Some(value.to_owned()),
334 "ticket_id" => node.ticket_id = Some(value.to_owned()),
335 "execution_status" => match value.parse::<ExecutionStatus>() {
336 Ok(es) => node.execution_status = Some(es),
337 Err(_) => {
338 errors.push(AgmError::new(
339 ErrorCode::P003,
340 format!("Invalid `execution_status` value: {value:?}"),
341 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
342 ));
343 }
344 },
345 "executed_by" => node.executed_by = Some(value.to_owned()),
346 "executed_at" => node.executed_at = Some(value.to_owned()),
347 "execution_log" => node.execution_log = Some(value.to_owned()),
348 "retry_count" => match value.parse::<u32>() {
349 Ok(n) => node.retry_count = Some(n),
350 Err(_) => {
351 errors.push(AgmError::new(
352 ErrorCode::P003,
353 format!("Invalid `retry_count` value: {value:?}"),
354 ErrorLocation::new(None, Some(line_number), Some(node.id.clone())),
355 ));
356 }
357 },
358 _ => {
359 node.extra_fields
360 .insert(key.to_owned(), FieldValue::Scalar(value.to_owned()));
361 }
362 }
363}
364
365fn assign_list_field(node: &mut Node, key: &str, items: Vec<String>) {
370 match key {
371 "depends" => node.depends = Some(items),
372 "related_to" => node.related_to = Some(items),
373 "replaces" => node.replaces = Some(items),
374 "conflicts" => node.conflicts = Some(items),
375 "see_also" => node.see_also = Some(items),
376 "items" => node.items = Some(items),
377 "steps" => node.steps = Some(items),
378 "fields" => node.fields = Some(items),
379 "input" => node.input = Some(items),
380 "output" => node.output = Some(items),
381 "rationale" => node.rationale = Some(items),
382 "tradeoffs" => node.tradeoffs = Some(items),
383 "resolution" => node.resolution = Some(items),
384 "scope" => node.scope = Some(items),
385 "tags" => node.tags = Some(items),
386 "aliases" => node.aliases = Some(items),
387 "keywords" => node.keywords = Some(items),
388 "labels" => node.labels = Some(items),
390 _ => {
391 node.extra_fields
392 .insert(key.to_owned(), FieldValue::List(items));
393 }
394 }
395}
396
397fn assign_block_field(node: &mut Node, key: &str, text: String) {
402 match key {
403 "detail" => node.detail = Some(text),
404 "examples" => node.examples = Some(text),
405 "notes" => node.notes = Some(text),
406 "execution_log" => node.execution_log = Some(text),
407 "applies_when" => node.applies_when = Some(text),
408 "title" => node.title = Some(text),
410 "description" => node.description = Some(text),
411 "prompt" => node.prompt = Some(text),
412 _ => {
413 node.extra_fields
414 .insert(key.to_owned(), FieldValue::Block(text));
415 }
416 }
417}