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