Skip to main content

agm_core/error/
codes.rs

1//! Error code registry (spec Appendix D).
2
3use 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    // Ticket-specific validation (v1.2.0)
82    /// Invalid ticket action enum value
83    V029,
84    /// Invalid sdd_phase enum value
85    V030,
86    /// Non-create action missing ticket_id
87    V031,
88    /// Ticket title exceeds 200 characters
89    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    /// Lint: summary > 100 chars (quality hint)
104    L001,
105    /// Lint: duplicate summaries across nodes
106    L002,
107    /// Lint: node has too many fields (granularity hint)
108    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}