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