1use std::fmt;
4
5use super::diagnostic::Severity;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum ErrorCategory {
9 Parse,
10 Validation,
11 Import,
12 Runtime,
13 Lint,
14}
15
16impl ErrorCategory {
17 #[must_use]
18 pub fn prefix(self) -> char {
19 match self {
20 Self::Parse => 'P',
21 Self::Validation => 'V',
22 Self::Import => 'I',
23 Self::Runtime => 'R',
24 Self::Lint => 'L',
25 }
26 }
27}
28
29impl fmt::Display for ErrorCategory {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 let name = match self {
32 Self::Parse => "parse",
33 Self::Validation => "validation",
34 Self::Import => "import",
35 Self::Runtime => "runtime",
36 Self::Lint => "lint",
37 };
38 write!(f, "{name}")
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum ErrorCode {
44 P001,
45 P002,
46 P003,
47 P004,
48 P005,
49 P006,
50 P007,
51 P008,
52 P009,
53 P010,
54 V001,
55 V002,
56 V003,
57 V004,
58 V005,
59 V006,
60 V007,
61 V008,
62 V009,
63 V010,
64 V011,
65 V012,
66 V013,
67 V014,
68 V015,
69 V016,
70 V017,
71 V018,
72 V019,
73 V020,
74 V021,
75 V022,
76 V023,
77 V024,
78 V025,
79 V026,
80 V027,
81 I001,
82 I002,
83 I003,
84 I004,
85 I005,
86 R001,
87 R002,
88 R003,
89 R004,
90 R005,
91 R006,
92 R007,
93 R008,
94 L001,
96 L002,
98 L003,
100}
101
102impl ErrorCode {
103 #[must_use]
104 pub fn category(self) -> ErrorCategory {
105 match self {
106 Self::P001
107 | Self::P002
108 | Self::P003
109 | Self::P004
110 | Self::P005
111 | Self::P006
112 | Self::P007
113 | Self::P008
114 | Self::P009
115 | Self::P010 => ErrorCategory::Parse,
116 Self::V001
117 | Self::V002
118 | Self::V003
119 | Self::V004
120 | Self::V005
121 | Self::V006
122 | Self::V007
123 | Self::V008
124 | Self::V009
125 | Self::V010
126 | Self::V011
127 | Self::V012
128 | Self::V013
129 | Self::V014
130 | Self::V015
131 | Self::V016
132 | Self::V017
133 | Self::V018
134 | Self::V019
135 | Self::V020
136 | Self::V021
137 | Self::V022
138 | Self::V023
139 | Self::V024
140 | Self::V025
141 | Self::V026
142 | Self::V027 => ErrorCategory::Validation,
143 Self::I001 | Self::I002 | Self::I003 | Self::I004 | Self::I005 => ErrorCategory::Import,
144 Self::R001
145 | Self::R002
146 | Self::R003
147 | Self::R004
148 | Self::R005
149 | Self::R006
150 | Self::R007
151 | Self::R008 => ErrorCategory::Runtime,
152 Self::L001 | Self::L002 | Self::L003 => ErrorCategory::Lint,
153 }
154 }
155
156 #[must_use]
157 pub fn number(self) -> u16 {
158 match self {
159 Self::P001 | Self::V001 | Self::I001 | Self::R001 | Self::L001 => 1,
160 Self::P002 | Self::V002 | Self::I002 | Self::R002 | Self::L002 => 2,
161 Self::P003 | Self::V003 | Self::I003 | Self::R003 | Self::L003 => 3,
162 Self::P004 | Self::V004 | Self::I004 | Self::R004 => 4,
163 Self::P005 | Self::V005 | Self::I005 | Self::R005 => 5,
164 Self::P006 | Self::V006 | Self::R006 => 6,
165 Self::P007 | Self::V007 | Self::R007 => 7,
166 Self::P008 | Self::V008 | Self::R008 => 8,
167 Self::P009 | Self::V009 => 9,
168 Self::P010 | Self::V010 => 10,
169 Self::V011 => 11,
170 Self::V012 => 12,
171 Self::V013 => 13,
172 Self::V014 => 14,
173 Self::V015 => 15,
174 Self::V016 => 16,
175 Self::V017 => 17,
176 Self::V018 => 18,
177 Self::V019 => 19,
178 Self::V020 => 20,
179 Self::V021 => 21,
180 Self::V022 => 22,
181 Self::V023 => 23,
182 Self::V024 => 24,
183 Self::V025 => 25,
184 Self::V026 => 26,
185 Self::V027 => 27,
186 }
187 }
188
189 #[must_use]
190 pub fn default_severity(self) -> Severity {
191 match self {
192 Self::P009 => Severity::Warning,
193 Self::P010 => Severity::Info,
194 Self::P001
195 | Self::P002
196 | Self::P003
197 | Self::P004
198 | Self::P005
199 | Self::P006
200 | Self::P007
201 | Self::P008 => Severity::Error,
202 Self::V010 | Self::V011 | Self::V012 | Self::V013 | Self::V014 | Self::V017 => {
203 Severity::Warning
204 }
205 Self::V001
206 | Self::V002
207 | Self::V003
208 | Self::V004
209 | Self::V005
210 | Self::V006
211 | Self::V007
212 | Self::V008
213 | Self::V009
214 | Self::V015
215 | Self::V016
216 | Self::V018
217 | Self::V019
218 | Self::V020
219 | Self::V021
220 | Self::V022
221 | Self::V023
222 | Self::V024
223 | Self::V025
224 | Self::V027 => Severity::Error,
225 Self::V026 => Severity::Warning,
226 Self::I005 => Severity::Warning,
227 Self::I001 | Self::I002 | Self::I003 | Self::I004 => Severity::Error,
228 Self::R007 => Severity::Warning,
229 Self::R001
230 | Self::R002
231 | Self::R003
232 | Self::R004
233 | Self::R005
234 | Self::R006
235 | Self::R008 => Severity::Error,
236 Self::L001 | Self::L002 | Self::L003 => Severity::Info,
237 }
238 }
239
240 #[must_use]
241 pub fn message_template(self) -> &'static str {
242 match self {
243 Self::P001 => "Missing required header field: `{field}`",
244 Self::P002 => "Invalid node declaration syntax",
245 Self::P003 => "Unexpected indentation",
246 Self::P004 => "Tab character in indentation (spaces required)",
247 Self::P005 => "Unterminated block field",
248 Self::P006 => "Duplicate field `{field}` in node `{node}`",
249 Self::P007 => "Invalid inline list syntax",
250 Self::P008 => "Empty file (no nodes)",
251 Self::P009 => "Unknown field `{field}` in node `{node}`",
252 Self::P010 => {
253 "File spec version `{file_version}` newer than parser version `{parser_version}`"
254 }
255 Self::V001 => "Node `{node}` missing required field: `type`",
256 Self::V002 => "Node `{node}` missing required field: `summary`",
257 Self::V003 => "Duplicate node ID: `{node}`",
258 Self::V004 => "Unresolved reference `{ref}` in `{field}` of node `{node}`",
259 Self::V005 => "Cycle detected in `depends`: `{cycle_path}`",
260 Self::V006 => "Invalid `execution_status` value: `{value}`",
261 Self::V007 => "`valid_from` is after `valid_until` in node `{node}`",
262 Self::V008 => "Code block missing required field: `{field}`",
263 Self::V009 => "Verify entry missing required field: `{field}`",
264 Self::V010 => "Node type `{type}` typically includes field `{field}` (missing)",
265 Self::V011 => "Summary is empty in node `{node}`",
266 Self::V012 => "Summary exceeds 200 characters in node `{node}`",
267 Self::V013 => "Conflicting active nodes co-loaded: `{node_a}` and `{node_b}`",
268 Self::V014 => "Deprecated node `{node}` missing `replaces` or `superseded_by`",
269 Self::V015 => "`target` path is absolute or contains traversal: `{path}`",
270 Self::V016 => "Disallowed field `{field}` on node type `{type}` (strict mode)",
271 Self::V017 => "Disallowed field `{field}` on node type `{type}` (standard mode)",
272 Self::V018 => "Orchestration group `{group}` references non-existent node `{node}`",
273 Self::V019 => "Cycle in orchestration `requires`: `{cycle_path}`",
274 Self::V020 => "Invalid `execution_status` transition: `{from}` -> `{to}`",
275 Self::V021 => "Node ID does not match required pattern: `{node}`",
276 Self::V022 => "Memory key does not match required pattern: `{key}`",
277 Self::V023 => "Invalid memory action: `{action}`",
278 Self::V024 => "Node `{node}` of type `{type}` missing required schema field: `{field}`",
279 Self::V025 => "Memory topic does not match required pattern: `{topic}`",
280 Self::V026 => {
281 "Unresolved memory topic `{topic}` in `agent_context.load_memory` of node `{node}`"
282 }
283 Self::V027 => "Memory value exceeds maximum size limit (32 KiB) for key `{key}`",
284 Self::I001 => "Unresolved import: `{package}`",
285 Self::I002 => {
286 "Import version constraint not satisfied: `{package}@{constraint}` (found `{actual}`)"
287 }
288 Self::I003 => "Circular import detected: `{cycle_path}`",
289 Self::I004 => "Cross-package reference to non-existent node: `{ref}`",
290 Self::I005 => "Import `{package}` is deprecated",
291 Self::R001 => "Code block target file not found: `{path}`",
292 Self::R002 => "Code block anchor/old pattern not found in target: `{pattern}`",
293 Self::R003 => "Code block anchor/old matches multiple locations in target",
294 Self::R004 => "Verification check failed: `{check_description}`",
295 Self::R005 => "Agent context file not found: `{path}`",
296 Self::R006 => "Memory operation failed: `{action}` on key `{key}`",
297 Self::R007 => "Node `{node}` retry count exceeded threshold: `{count}`",
298 Self::R008 => "Execution timeout for node `{node}`",
299 Self::L001 => "Summary of node `{node}` exceeds 100 characters (quality hint)",
300 Self::L002 => "Nodes `{node_a}` and `{node_b}` have identical summaries",
301 Self::L003 => "Node `{node}` has too many populated fields; consider splitting",
302 }
303 }
304
305 #[must_use]
306 pub fn display_code(self) -> String {
307 format!("AGM-{}{:03}", self.category().prefix(), self.number())
308 }
309}
310
311impl fmt::Display for ErrorCode {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 write!(f, "AGM-{}{:03}", self.category().prefix(), self.number())
314 }
315}
316
317impl serde::Serialize for ErrorCode {
318 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
319 serializer.serialize_str(&self.to_string())
320 }
321}
322
323impl<'de> serde::Deserialize<'de> for ErrorCode {
324 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
325 let s = String::deserialize(deserializer)?;
326 s.parse().map_err(serde::de::Error::custom)
327 }
328}
329
330impl std::str::FromStr for ErrorCode {
331 type Err = String;
332
333 fn from_str(s: &str) -> Result<Self, Self::Err> {
334 match s {
335 "AGM-P001" => Ok(Self::P001),
336 "AGM-P002" => Ok(Self::P002),
337 "AGM-P003" => Ok(Self::P003),
338 "AGM-P004" => Ok(Self::P004),
339 "AGM-P005" => Ok(Self::P005),
340 "AGM-P006" => Ok(Self::P006),
341 "AGM-P007" => Ok(Self::P007),
342 "AGM-P008" => Ok(Self::P008),
343 "AGM-P009" => Ok(Self::P009),
344 "AGM-P010" => Ok(Self::P010),
345 "AGM-V001" => Ok(Self::V001),
346 "AGM-V002" => Ok(Self::V002),
347 "AGM-V003" => Ok(Self::V003),
348 "AGM-V004" => Ok(Self::V004),
349 "AGM-V005" => Ok(Self::V005),
350 "AGM-V006" => Ok(Self::V006),
351 "AGM-V007" => Ok(Self::V007),
352 "AGM-V008" => Ok(Self::V008),
353 "AGM-V009" => Ok(Self::V009),
354 "AGM-V010" => Ok(Self::V010),
355 "AGM-V011" => Ok(Self::V011),
356 "AGM-V012" => Ok(Self::V012),
357 "AGM-V013" => Ok(Self::V013),
358 "AGM-V014" => Ok(Self::V014),
359 "AGM-V015" => Ok(Self::V015),
360 "AGM-V016" => Ok(Self::V016),
361 "AGM-V017" => Ok(Self::V017),
362 "AGM-V018" => Ok(Self::V018),
363 "AGM-V019" => Ok(Self::V019),
364 "AGM-V020" => Ok(Self::V020),
365 "AGM-V021" => Ok(Self::V021),
366 "AGM-V022" => Ok(Self::V022),
367 "AGM-V023" => Ok(Self::V023),
368 "AGM-V024" => Ok(Self::V024),
369 "AGM-V025" => Ok(Self::V025),
370 "AGM-V026" => Ok(Self::V026),
371 "AGM-V027" => Ok(Self::V027),
372 "AGM-I001" => Ok(Self::I001),
373 "AGM-I002" => Ok(Self::I002),
374 "AGM-I003" => Ok(Self::I003),
375 "AGM-I004" => Ok(Self::I004),
376 "AGM-I005" => Ok(Self::I005),
377 "AGM-R001" => Ok(Self::R001),
378 "AGM-R002" => Ok(Self::R002),
379 "AGM-R003" => Ok(Self::R003),
380 "AGM-R004" => Ok(Self::R004),
381 "AGM-R005" => Ok(Self::R005),
382 "AGM-R006" => Ok(Self::R006),
383 "AGM-R007" => Ok(Self::R007),
384 "AGM-R008" => Ok(Self::R008),
385 "AGM-L001" => Ok(Self::L001),
386 "AGM-L002" => Ok(Self::L002),
387 "AGM-L003" => Ok(Self::L003),
388 _ => Err(format!("unknown error code: {s}")),
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_error_code_display_parse_formats_correctly() {
399 assert_eq!(ErrorCode::P001.to_string(), "AGM-P001");
400 assert_eq!(ErrorCode::P010.to_string(), "AGM-P010");
401 }
402
403 #[test]
404 fn test_error_code_display_validation_formats_correctly() {
405 assert_eq!(ErrorCode::V001.to_string(), "AGM-V001");
406 assert_eq!(ErrorCode::V023.to_string(), "AGM-V023");
407 }
408
409 #[test]
410 fn test_error_code_display_import_formats_correctly() {
411 assert_eq!(ErrorCode::I001.to_string(), "AGM-I001");
412 assert_eq!(ErrorCode::I005.to_string(), "AGM-I005");
413 }
414
415 #[test]
416 fn test_error_code_display_runtime_formats_correctly() {
417 assert_eq!(ErrorCode::R001.to_string(), "AGM-R001");
418 assert_eq!(ErrorCode::R008.to_string(), "AGM-R008");
419 }
420
421 #[test]
422 fn test_error_code_category_returns_correct_category() {
423 assert_eq!(ErrorCode::P005.category(), ErrorCategory::Parse);
424 assert_eq!(ErrorCode::V012.category(), ErrorCategory::Validation);
425 assert_eq!(ErrorCode::I003.category(), ErrorCategory::Import);
426 assert_eq!(ErrorCode::R006.category(), ErrorCategory::Runtime);
427 }
428
429 #[test]
430 fn test_error_code_default_severity_error_codes() {
431 assert_eq!(ErrorCode::P001.default_severity(), Severity::Error);
432 assert_eq!(ErrorCode::V003.default_severity(), Severity::Error);
433 assert_eq!(ErrorCode::I001.default_severity(), Severity::Error);
434 assert_eq!(ErrorCode::R001.default_severity(), Severity::Error);
435 }
436
437 #[test]
438 fn test_error_code_default_severity_warning_codes() {
439 assert_eq!(ErrorCode::P009.default_severity(), Severity::Warning);
440 assert_eq!(ErrorCode::V010.default_severity(), Severity::Warning);
441 assert_eq!(ErrorCode::V011.default_severity(), Severity::Warning);
442 assert_eq!(ErrorCode::V012.default_severity(), Severity::Warning);
443 assert_eq!(ErrorCode::V013.default_severity(), Severity::Warning);
444 assert_eq!(ErrorCode::V014.default_severity(), Severity::Warning);
445 assert_eq!(ErrorCode::V017.default_severity(), Severity::Warning);
446 assert_eq!(ErrorCode::I005.default_severity(), Severity::Warning);
447 assert_eq!(ErrorCode::R007.default_severity(), Severity::Warning);
448 }
449
450 #[test]
451 fn test_error_code_default_severity_info_codes() {
452 assert_eq!(ErrorCode::P010.default_severity(), Severity::Info);
453 }
454
455 #[test]
456 fn test_error_code_message_template_not_empty() {
457 assert!(ErrorCode::P001.message_template().contains("header"));
458 assert!(ErrorCode::V003.message_template().contains("Duplicate"));
459 assert!(ErrorCode::I002.message_template().contains("constraint"));
460 assert!(ErrorCode::R004.message_template().contains("Verification"));
461 }
462
463 #[test]
464 fn test_error_code_total_count_is_53() {
465 let all_codes: Vec<ErrorCode> = vec![
466 ErrorCode::P001,
467 ErrorCode::P002,
468 ErrorCode::P003,
469 ErrorCode::P004,
470 ErrorCode::P005,
471 ErrorCode::P006,
472 ErrorCode::P007,
473 ErrorCode::P008,
474 ErrorCode::P009,
475 ErrorCode::P010,
476 ErrorCode::V001,
477 ErrorCode::V002,
478 ErrorCode::V003,
479 ErrorCode::V004,
480 ErrorCode::V005,
481 ErrorCode::V006,
482 ErrorCode::V007,
483 ErrorCode::V008,
484 ErrorCode::V009,
485 ErrorCode::V010,
486 ErrorCode::V011,
487 ErrorCode::V012,
488 ErrorCode::V013,
489 ErrorCode::V014,
490 ErrorCode::V015,
491 ErrorCode::V016,
492 ErrorCode::V017,
493 ErrorCode::V018,
494 ErrorCode::V019,
495 ErrorCode::V020,
496 ErrorCode::V021,
497 ErrorCode::V022,
498 ErrorCode::V023,
499 ErrorCode::V024,
500 ErrorCode::V025,
501 ErrorCode::V026,
502 ErrorCode::V027,
503 ErrorCode::I001,
504 ErrorCode::I002,
505 ErrorCode::I003,
506 ErrorCode::I004,
507 ErrorCode::I005,
508 ErrorCode::R001,
509 ErrorCode::R002,
510 ErrorCode::R003,
511 ErrorCode::R004,
512 ErrorCode::R005,
513 ErrorCode::R006,
514 ErrorCode::R007,
515 ErrorCode::R008,
516 ErrorCode::L001,
517 ErrorCode::L002,
518 ErrorCode::L003,
519 ];
520 assert_eq!(all_codes.len(), 53);
521 }
522
523 #[test]
524 fn test_error_code_from_str_roundtrip() {
525 let code = ErrorCode::V003;
526 let s = code.to_string();
527 let parsed: ErrorCode = s.parse().unwrap();
528 assert_eq!(parsed, code);
529 }
530
531 #[test]
532 fn test_error_code_from_str_invalid_returns_err() {
533 let result = "AGM-X999".parse::<ErrorCode>();
534 assert!(result.is_err());
535 }
536
537 #[test]
538 fn test_error_code_serde_roundtrip() {
539 let code = ErrorCode::P003;
540 let json = serde_json::to_string(&code).unwrap();
541 assert_eq!(json, "\"AGM-P003\"");
542 let deserialized: ErrorCode = serde_json::from_str(&json).unwrap();
543 assert_eq!(deserialized, code);
544 }
545
546 #[test]
547 fn test_error_category_prefix_chars() {
548 assert_eq!(ErrorCategory::Parse.prefix(), 'P');
549 assert_eq!(ErrorCategory::Validation.prefix(), 'V');
550 assert_eq!(ErrorCategory::Import.prefix(), 'I');
551 assert_eq!(ErrorCategory::Runtime.prefix(), 'R');
552 assert_eq!(ErrorCategory::Lint.prefix(), 'L');
553 }
554
555 #[test]
556 fn test_lint_codes_display_correctly() {
557 assert_eq!(ErrorCode::L001.to_string(), "AGM-L001");
558 assert_eq!(ErrorCode::L002.to_string(), "AGM-L002");
559 assert_eq!(ErrorCode::L003.to_string(), "AGM-L003");
560 }
561
562 #[test]
563 fn test_lint_codes_default_severity_is_info() {
564 assert_eq!(ErrorCode::L001.default_severity(), Severity::Info);
565 assert_eq!(ErrorCode::L002.default_severity(), Severity::Info);
566 assert_eq!(ErrorCode::L003.default_severity(), Severity::Info);
567 }
568
569 #[test]
570 fn test_lint_codes_from_str_roundtrip() {
571 for code in [ErrorCode::L001, ErrorCode::L002, ErrorCode::L003] {
572 let s = code.to_string();
573 let parsed: ErrorCode = s.parse().unwrap();
574 assert_eq!(parsed, code);
575 }
576 }
577}