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    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    /// Lint: summary > 100 chars (quality hint)
94    L001,
95    /// Lint: duplicate summaries across nodes
96    L002,
97    /// Lint: node has too many fields (granularity hint)
98    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}