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