1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use chrono::Utc;
9
10#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
12pub struct ErrorRecord {
13 pub id: String,
15
16 pub error_type: String,
18
19 pub message: String,
21
22 pub context: String,
24
25 pub timestamp: String,
27
28 pub occurrence_count: usize,
30
31 pub solution: Option<String>,
33
34 pub resolved: bool,
36
37 pub tags: Vec<String>,
39
40 pub related_files: Vec<String>,
42}
43
44impl ErrorRecord {
45 pub fn new(
47 error_type: &str,
48 message: &str,
49 context: &str,
50 ) -> Self {
51 let message_hash = message.chars().fold(0u32, |acc, c| {
53 acc.wrapping_mul(31).wrapping_add(c as u32)
54 });
55
56 let id = format!("{}-{}",
57 error_type,
58 message_hash
59 );
60
61 Self {
62 id,
63 error_type: error_type.to_string(),
64 message: message.to_string(),
65 context: context.to_string(),
66 timestamp: Utc::now().to_rfc3339(),
67 occurrence_count: 1,
68 solution: None,
69 resolved: false,
70 tags: vec![],
71 related_files: vec![],
72 }
73 }
74
75 pub fn with_tag(mut self, tag: &str) -> Self {
77 if !self.tags.contains(&tag.to_string()) {
78 self.tags.push(tag.to_string());
79 }
80 self
81 }
82
83 pub fn with_file(mut self, file: &str) -> Self {
85 if !self.related_files.contains(&file.to_string()) {
86 self.related_files.push(file.to_string());
87 }
88 self
89 }
90
91 pub fn with_solution(mut self, solution: &str) -> Self {
93 self.solution = Some(solution.to_string());
94 self
95 }
96
97 pub fn resolve(mut self) -> Self {
99 self.resolved = true;
100 self
101 }
102}
103
104pub struct ErrorKnowledgeBase {
106 errors: HashMap<String, ErrorRecord>,
108
109 error_index: HashMap<String, Vec<String>>, tag_index: HashMap<String, Vec<String>>, }
115
116impl ErrorKnowledgeBase {
117 pub fn new() -> Self {
119 Self {
120 errors: HashMap::new(),
121 error_index: HashMap::new(),
122 tag_index: HashMap::new(),
123 }
124 }
125
126 pub fn record(&mut self, error: ErrorRecord) {
128 let error_id = error.id.clone();
129 let error_type = error.error_type.clone();
130
131 if let Some(existing) = self.errors.get_mut(&error_id) {
133 existing.occurrence_count += 1;
134 return;
135 }
136
137 self.error_index
139 .entry(error_type)
140 .or_default()
141 .push(error_id.clone());
142
143 for tag in &error.tags {
145 self.tag_index
146 .entry(tag.clone())
147 .or_default()
148 .push(error_id.clone());
149 }
150
151 self.errors.insert(error_id, error);
153 }
154
155 pub fn find_similar(&self, error_type: &str, keywords: &[&str]) -> Vec<ErrorRecord> {
157 let mut matches = vec![];
158
159 if let Some(error_ids) = self.error_index.get(error_type) {
161 for error_id in error_ids {
162 if let Some(error) = self.errors.get(error_id) {
163 let matches_keywords = keywords.iter().any(|kw| {
165 error.message.contains(kw)
166 || error.context.contains(kw)
167 || error.tags.iter().any(|t| t.contains(kw))
168 });
169
170 if matches_keywords || keywords.is_empty() {
171 matches.push(error.clone());
172 }
173 }
174 }
175 }
176
177 matches.sort_by(|a, b| b.occurrence_count.cmp(&a.occurrence_count));
179 matches
180 }
181
182 pub fn find_by_tag(&self, tag: &str) -> Vec<ErrorRecord> {
184 if let Some(error_ids) = self.tag_index.get(tag) {
185 error_ids
186 .iter()
187 .filter_map(|id| self.errors.get(id).cloned())
188 .collect()
189 } else {
190 vec![]
191 }
192 }
193
194 pub fn unresolved(&self) -> Vec<ErrorRecord> {
196 self.errors
197 .values()
198 .filter(|e| !e.resolved)
199 .cloned()
200 .collect()
201 }
202
203 pub fn most_frequent(&self, limit: usize) -> Vec<ErrorRecord> {
205 let mut errors: Vec<_> = self.errors.values().cloned().collect();
206 errors.sort_by(|a, b| b.occurrence_count.cmp(&a.occurrence_count));
207 errors.into_iter().take(limit).collect()
208 }
209
210 pub fn export_json(&self) -> serde_json::Result<String> {
212 serde_json::to_string_pretty(&self.errors)
213 }
214
215 pub fn stats(&self) -> ErrorStats {
217 let total_errors = self.errors.len();
218 let total_occurrences = self.errors.values().map(|e| e.occurrence_count).sum();
219 let unresolved_count = self.errors.values().filter(|e| !e.resolved).count();
220 let with_solutions = self.errors.values().filter(|e| e.solution.is_some()).count();
221
222 ErrorStats {
223 total_unique_errors: total_errors,
224 total_occurrences,
225 unresolved_count,
226 with_solutions,
227 avg_occurrences: if total_errors > 0 { total_occurrences / total_errors } else { 0 },
228 }
229 }
230}
231
232impl Default for ErrorKnowledgeBase {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238#[derive(Clone, Debug, Serialize, Deserialize)]
240pub struct ErrorStats {
241 pub total_unique_errors: usize,
242 pub total_occurrences: usize,
243 pub unresolved_count: usize,
244 pub with_solutions: usize,
245 pub avg_occurrences: usize,
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_error_record_creation() {
254 let error = ErrorRecord::new("MCP", "Client not initialized", "runtime.rs:332")
255 .with_tag("initialization")
256 .with_file("runtime.rs")
257 .with_solution("Call client.initialize() after creation");
258
259 assert_eq!(error.error_type, "MCP");
260 assert_eq!(error.message, "Client not initialized");
261 assert!(error.tags.contains(&"initialization".to_string()));
262 assert!(error.solution.is_some());
263 }
264
265 #[test]
266 fn test_knowledge_base_recording() {
267 let mut kb = ErrorKnowledgeBase::new();
268
269 let error1 = ErrorRecord::new("MCP", "Client not initialized", "context1")
270 .with_tag("initialization");
271 let error2 = ErrorRecord::new("Tool", "Tool not found", "context2")
272 .with_tag("execution");
273
274 kb.record(error1);
275 kb.record(error2);
276
277 assert_eq!(kb.errors.len(), 2);
278 }
279
280 #[test]
281 fn test_find_by_tag() {
282 let mut kb = ErrorKnowledgeBase::new();
283
284 kb.record(ErrorRecord::new("MCP", "Error 1", "ctx").with_tag("init"));
285 kb.record(ErrorRecord::new("MCP", "Error 2", "ctx").with_tag("execution"));
286 kb.record(ErrorRecord::new("MCP", "Error 3", "ctx").with_tag("init"));
287
288 let init_errors = kb.find_by_tag("init");
289 assert_eq!(init_errors.len(), 2);
290 }
291
292 #[test]
293 fn test_find_similar() {
294 let mut kb = ErrorKnowledgeBase::new();
295
296 kb.record(
297 ErrorRecord::new("MCP", "Client not initialized", "runtime").with_tag("init")
298 );
299 kb.record(
300 ErrorRecord::new("Tool", "Tool execution failed", "executor").with_tag("execution")
301 );
302
303 let similar = kb.find_similar("MCP", &["initialized"]);
304 assert_eq!(similar.len(), 1);
305 assert!(similar[0].message.contains("initialized"));
306 }
307}