terraphim_session_analyzer/patterns/
loader.rs1use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ToolPattern {
12 pub name: String,
14
15 pub patterns: Vec<String>,
17
18 pub metadata: ToolMetadata,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ToolMetadata {
25 pub category: String,
27
28 pub description: Option<String>,
30
31 #[serde(default = "default_confidence")]
33 pub confidence: f32,
34}
35
36fn default_confidence() -> f32 {
37 0.9
38}
39
40#[derive(Debug, Deserialize)]
42struct ToolPatternsConfig {
43 tools: Vec<ToolPattern>,
44}
45
46pub fn load_patterns() -> Result<Vec<ToolPattern>> {
52 let toml_content = include_str!("../patterns.toml");
53 load_patterns_from_str(toml_content)
54}
55
56pub fn load_patterns_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<ToolPattern>> {
62 let content = std::fs::read_to_string(path.as_ref())
63 .with_context(|| format!("Failed to read patterns from {}", path.as_ref().display()))?;
64
65 load_patterns_from_str(&content)
66}
67
68pub fn load_patterns_from_str(toml_str: &str) -> Result<Vec<ToolPattern>> {
74 let config: ToolPatternsConfig =
75 toml::from_str(toml_str).context("Failed to parse tool patterns TOML")?;
76
77 for tool in &config.tools {
79 if tool.patterns.is_empty() {
80 anyhow::bail!("Tool '{}' has no patterns defined", tool.name);
81 }
82
83 if tool.metadata.confidence < 0.0 || tool.metadata.confidence > 1.0 {
84 anyhow::bail!(
85 "Tool '{}' has invalid confidence score: {}",
86 tool.name,
87 tool.metadata.confidence
88 );
89 }
90 }
91
92 Ok(config.tools)
93}
94
95pub fn load_user_patterns() -> Result<Vec<ToolPattern>> {
101 let home = home::home_dir().context("No home directory")?;
102 let config_path = home
103 .join(".config")
104 .join("claude-log-analyzer")
105 .join("tools.toml");
106
107 if !config_path.exists() {
108 return Ok(Vec::new());
109 }
110
111 load_patterns_from_file(config_path)
112}
113
114pub fn load_all_patterns() -> Result<Vec<ToolPattern>> {
122 let builtin = load_patterns()?;
123 let user = load_user_patterns()?;
124
125 merge_patterns(builtin, user)
126}
127
128fn merge_patterns(builtin: Vec<ToolPattern>, user: Vec<ToolPattern>) -> Result<Vec<ToolPattern>> {
137 use std::collections::HashMap;
138
139 let mut pattern_map: HashMap<String, ToolPattern> = HashMap::new();
141
142 for pattern in builtin {
144 pattern_map.insert(pattern.name.clone(), pattern);
145 }
146
147 for pattern in user {
149 pattern_map.insert(pattern.name.clone(), pattern);
150 }
151
152 let mut merged: Vec<ToolPattern> = pattern_map.into_values().collect();
154
155 merged.sort_by(|a, b| a.name.cmp(&b.name));
157
158 for tool in &merged {
160 if tool.patterns.is_empty() {
161 anyhow::bail!("Tool '{}' has no patterns defined", tool.name);
162 }
163
164 if tool.metadata.confidence < 0.0 || tool.metadata.confidence > 1.0 {
165 anyhow::bail!(
166 "Tool '{}' has invalid confidence score: {}",
167 tool.name,
168 tool.metadata.confidence
169 );
170 }
171 }
172
173 Ok(merged)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_load_patterns_from_str() {
182 let toml = r#"
183[[tools]]
184name = "wrangler"
185patterns = ["npx wrangler", "bunx wrangler"]
186
187[tools.metadata]
188category = "cloudflare"
189description = "Cloudflare Workers CLI"
190confidence = 0.95
191
192[[tools]]
193name = "npm"
194patterns = ["npm "]
195
196[tools.metadata]
197category = "package-manager"
198description = "Node package manager"
199confidence = 0.9
200"#;
201
202 let patterns = load_patterns_from_str(toml).unwrap();
203 assert_eq!(patterns.len(), 2);
204
205 assert_eq!(patterns[0].name, "wrangler");
206 assert_eq!(patterns[0].patterns.len(), 2);
207 assert_eq!(patterns[0].metadata.category, "cloudflare");
208 assert_eq!(patterns[0].metadata.confidence, 0.95);
209
210 assert_eq!(patterns[1].name, "npm");
211 assert_eq!(patterns[1].patterns.len(), 1);
212 assert_eq!(patterns[1].metadata.category, "package-manager");
213 }
214
215 #[test]
216 fn test_default_confidence() {
217 let toml = r#"
218[[tools]]
219name = "test"
220patterns = ["test"]
221
222[tools.metadata]
223category = "test"
224"#;
225
226 let patterns = load_patterns_from_str(toml).unwrap();
227 assert_eq!(patterns[0].metadata.confidence, 0.9);
228 }
229
230 #[test]
231 fn test_empty_patterns_validation() {
232 let toml = r#"
233[[tools]]
234name = "empty"
235patterns = []
236
237[tools.metadata]
238category = "test"
239"#;
240
241 let result = load_patterns_from_str(toml);
242 assert!(result.is_err());
243 assert!(result.unwrap_err().to_string().contains("no patterns"));
244 }
245
246 #[test]
247 fn test_invalid_confidence_validation() {
248 let toml = r#"
249[[tools]]
250name = "invalid"
251patterns = ["test"]
252
253[tools.metadata]
254category = "test"
255confidence = 1.5
256"#;
257
258 let result = load_patterns_from_str(toml);
259 assert!(result.is_err());
260 assert!(result
261 .unwrap_err()
262 .to_string()
263 .contains("invalid confidence"));
264 }
265
266 #[test]
267 fn test_load_built_in_patterns() {
268 let result = load_patterns();
270 assert!(
271 result.is_ok(),
272 "Failed to load built-in patterns: {:?}",
273 result.err()
274 );
275
276 let patterns = result.unwrap();
277 assert!(
278 !patterns.is_empty(),
279 "Built-in patterns should not be empty"
280 );
281
282 let tool_names: Vec<&str> = patterns.iter().map(|p| p.name.as_str()).collect();
284 assert!(
285 tool_names.contains(&"wrangler"),
286 "Expected wrangler pattern"
287 );
288 assert!(tool_names.contains(&"npm"), "Expected npm pattern");
289 }
290
291 #[test]
292 fn test_merge_patterns_unique() {
293 let builtin = vec![
294 ToolPattern {
295 name: "npm".to_string(),
296 patterns: vec!["npm ".to_string()],
297 metadata: ToolMetadata {
298 category: "package-manager".to_string(),
299 description: Some("Node package manager".to_string()),
300 confidence: 0.9,
301 },
302 },
303 ToolPattern {
304 name: "cargo".to_string(),
305 patterns: vec!["cargo ".to_string()],
306 metadata: ToolMetadata {
307 category: "rust-toolchain".to_string(),
308 description: Some("Rust package manager".to_string()),
309 confidence: 0.95,
310 },
311 },
312 ];
313
314 let user = vec![ToolPattern {
315 name: "custom".to_string(),
316 patterns: vec!["custom ".to_string()],
317 metadata: ToolMetadata {
318 category: "custom".to_string(),
319 description: Some("Custom tool".to_string()),
320 confidence: 0.8,
321 },
322 }];
323
324 let merged = merge_patterns(builtin, user).unwrap();
325 assert_eq!(merged.len(), 3);
326
327 let tool_names: Vec<&str> = merged.iter().map(|p| p.name.as_str()).collect();
328 assert!(tool_names.contains(&"npm"));
329 assert!(tool_names.contains(&"cargo"));
330 assert!(tool_names.contains(&"custom"));
331 }
332
333 #[test]
334 fn test_merge_patterns_override() {
335 let builtin = vec![ToolPattern {
336 name: "npm".to_string(),
337 patterns: vec!["npm ".to_string()],
338 metadata: ToolMetadata {
339 category: "package-manager".to_string(),
340 description: Some("Node package manager".to_string()),
341 confidence: 0.9,
342 },
343 }];
344
345 let user = vec![ToolPattern {
346 name: "npm".to_string(),
347 patterns: vec!["npm install".to_string(), "npm run".to_string()],
348 metadata: ToolMetadata {
349 category: "package-manager".to_string(),
350 description: Some("Custom npm config".to_string()),
351 confidence: 0.95,
352 },
353 }];
354
355 let merged = merge_patterns(builtin, user).unwrap();
356 assert_eq!(merged.len(), 1);
357
358 let npm = merged.iter().find(|p| p.name == "npm").unwrap();
359 assert_eq!(npm.patterns.len(), 2);
360 assert_eq!(
361 npm.metadata.description.as_deref(),
362 Some("Custom npm config")
363 );
364 assert_eq!(npm.metadata.confidence, 0.95);
365 }
366
367 #[test]
368 fn test_merge_patterns_validation_fails() {
369 let builtin = vec![];
370
371 let user = vec![ToolPattern {
372 name: "invalid".to_string(),
373 patterns: vec![],
374 metadata: ToolMetadata {
375 category: "test".to_string(),
376 description: None,
377 confidence: 0.9,
378 },
379 }];
380
381 let result = merge_patterns(builtin, user);
382 assert!(result.is_err());
383 assert!(result.unwrap_err().to_string().contains("no patterns"));
384 }
385
386 #[test]
387 fn test_load_user_patterns_no_file() {
388 let result = load_user_patterns();
390 assert!(result.is_ok());
391 }
392
393 #[test]
394 fn test_load_all_patterns() {
395 let result = load_all_patterns();
397 assert!(result.is_ok());
398
399 let patterns = result.unwrap();
400 assert!(
401 !patterns.is_empty(),
402 "Should have at least built-in patterns"
403 );
404 }
405}