agtrace_engine/diagnostics/
validator.rs

1use std::collections::HashMap;
2
3/// Result of diagnosing log file parsing health for a provider.
4///
5/// Contains success/failure statistics and categorized failure examples
6/// for identifying systematic parsing issues.
7#[derive(Debug)]
8pub struct DiagnoseResult {
9    /// Provider being diagnosed (claude, codex, gemini).
10    pub provider_name: String,
11    /// Total number of log files checked.
12    pub total_files: usize,
13    /// Number of files successfully parsed.
14    pub successful: usize,
15    /// Failed files grouped by failure type.
16    pub failures: HashMap<FailureType, Vec<FailureExample>>,
17}
18
19/// Category of log file parsing failure.
20///
21/// Used to group similar failures together for easier diagnosis.
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub enum FailureType {
24    /// Required field is missing from the log entry.
25    MissingField(String),
26    /// Field has unexpected type or format.
27    TypeMismatch(String),
28    /// Generic parsing error not categorized further.
29    ParseError,
30}
31
32impl std::fmt::Display for FailureType {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            FailureType::MissingField(field) => write!(f, "missing_field ({})", field),
36            FailureType::TypeMismatch(field) => write!(f, "type_mismatch ({})", field),
37            FailureType::ParseError => write!(f, "parse_error"),
38        }
39    }
40}
41
42/// Example of a specific file parsing failure.
43///
44/// Provides the file path and error reason for investigation.
45#[derive(Debug, Clone)]
46pub struct FailureExample {
47    /// Absolute path to the file that failed to parse.
48    pub path: String,
49    /// Human-readable description of the failure.
50    pub reason: String,
51}
52
53/// Categorize a parse error message into a structured failure type.
54///
55/// Analyzes error text to extract field names and classify the failure
56/// as missing field, type mismatch, or generic parse error.
57pub fn categorize_parse_error(error_msg: &str) -> (FailureType, String) {
58    if error_msg.contains("missing field") {
59        if let Some(field) = extract_field_name(error_msg) {
60            (
61                FailureType::MissingField(field.clone()),
62                format!("Missing required field: {}", field),
63            )
64        } else {
65            (FailureType::ParseError, error_msg.to_string())
66        }
67    } else if error_msg.contains("expected") || error_msg.contains("invalid type") {
68        if let Some(field) = extract_field_name(error_msg) {
69            (
70                FailureType::TypeMismatch(field.clone()),
71                format!("Type mismatch for field: {}", field),
72            )
73        } else {
74            (FailureType::ParseError, error_msg.to_string())
75        }
76    } else {
77        (FailureType::ParseError, error_msg.to_string())
78    }
79}
80
81fn extract_field_name(error_msg: &str) -> Option<String> {
82    if let Some(start) = error_msg.find("field `") {
83        let rest = &error_msg[start + 7..];
84        if let Some(end) = rest.find('`') {
85            return Some(rest[..end].to_string());
86        }
87    }
88    None
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_categorize_missing_field() {
97        let error = "missing field `source` at line 1 column 2";
98        let (failure_type, reason) = categorize_parse_error(error);
99        assert_eq!(
100            failure_type,
101            FailureType::MissingField("source".to_string())
102        );
103        assert_eq!(reason, "Missing required field: source");
104    }
105
106    #[test]
107    fn test_categorize_type_mismatch() {
108        let error = "invalid type for field `timestamp`: expected string, got number";
109        let (failure_type, reason) = categorize_parse_error(error);
110        assert_eq!(
111            failure_type,
112            FailureType::TypeMismatch("timestamp".to_string())
113        );
114        assert_eq!(reason, "Type mismatch for field: timestamp");
115    }
116
117    #[test]
118    fn test_categorize_generic_parse_error() {
119        let error = "unexpected character at line 1";
120        let (failure_type, _reason) = categorize_parse_error(error);
121        assert_eq!(failure_type, FailureType::ParseError);
122    }
123
124    #[test]
125    fn test_extract_field_name() {
126        assert_eq!(
127            extract_field_name("missing field `source`"),
128            Some("source".to_string())
129        );
130        assert_eq!(
131            extract_field_name("field `timestamp` has wrong type"),
132            Some("timestamp".to_string())
133        );
134        assert_eq!(extract_field_name("no field here"), None);
135    }
136}