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