1use super::skills::{Impact, Rule, SkillsExtractor, SkillsError};
7
8#[derive(Debug, Clone, Default)]
10pub struct ValidationResult {
11 pub errors: Vec<String>,
13 pub warnings: Vec<String>,
15}
16
17impl ValidationResult {
18 pub fn new() -> Self {
20 Self {
21 errors: Vec::new(),
22 warnings: Vec::new(),
23 }
24 }
25
26 pub fn add_error(&mut self, error: impl Into<String>) {
32 self.errors.push(error.into());
33 }
34
35 pub fn add_warning(&mut self, warning: impl Into<String>) {
41 self.warnings.push(warning.into());
42 }
43
44 pub fn is_valid(&self) -> bool {
46 self.errors.is_empty()
47 }
48
49 pub fn has_warnings(&self) -> bool {
51 !self.warnings.is_empty()
52 }
53
54 pub fn total_issues(&self) -> usize {
56 self.errors.len() + self.warnings.len()
57 }
58
59 pub fn format(&self) -> String {
61 let mut output = String::new();
62
63 if !self.errors.is_empty() {
64 output.push_str(&format!("❌ {} Error(s):\n", self.errors.len()));
65 for (i, error) in self.errors.iter().enumerate() {
66 output.push_str(&format!(" {}. {}\n", i + 1, error));
67 }
68 output.push('\n');
69 }
70
71 if !self.warnings.is_empty() {
72 output.push_str(&format!("⚠️ {} Warning(s):\n", self.warnings.len()));
73 for (i, warning) in self.warnings.iter().enumerate() {
74 output.push_str(&format!(" {}. {}\n", i + 1, warning));
75 }
76 output.push('\n');
77 }
78
79 if self.is_valid() && !self.has_warnings() {
80 output.push_str("✅ Validation passed with no issues\n");
81 }
82
83 output
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct InstructionValidator {
90 skills: SkillsExtractor,
91}
92
93impl InstructionValidator {
94 pub fn new(skills: SkillsExtractor) -> Self {
109 Self { skills }
110 }
111
112 pub fn validate(&self, instructions: &str) -> Result<ValidationResult, SkillsError> {
143 let mut result = ValidationResult::new();
144
145 let all_rules = self.skills.get_all_rules()?;
147
148 self.check_anti_patterns(instructions, &all_rules, &mut result);
150
151 self.check_missing_critical_rules(instructions, &all_rules, &mut result);
153
154 self.check_missing_high_priority_rules(instructions, &all_rules, &mut result);
156
157 Ok(result)
158 }
159
160 fn check_anti_patterns(
162 &self,
163 instructions: &str,
164 rules: &[Rule],
165 result: &mut ValidationResult,
166 ) {
167 for rule in rules {
168 for incorrect_example in &rule.incorrect_examples {
170 let normalized_example = Self::normalize_code(incorrect_example);
172 let normalized_instructions = Self::normalize_code(instructions);
173
174 if Self::contains_pattern(&normalized_instructions, &normalized_example) {
176 result.add_error(format!(
177 "Anti-pattern detected from rule '{}' ({}): Found code matching incorrect example",
178 rule.title,
179 rule.impact.as_str()
180 ));
181 }
182 }
183
184 self.check_keyword_anti_patterns(instructions, rule, result);
186 }
187 }
188
189 fn check_keyword_anti_patterns(
191 &self,
192 instructions: &str,
193 rule: &Rule,
194 result: &mut ValidationResult,
195 ) {
196 let anti_patterns = [
198 ("\"default\"", "Using 'default' as user_id"),
199 ("entity_id", "Using deprecated 'entity_id' instead of 'user_id'"),
200 ("actions", "Using deprecated 'actions' instead of 'tools'"),
201 ];
202
203 for (pattern, description) in &anti_patterns {
204 if instructions.contains(pattern) && rule.impact == Impact::Critical {
205 if rule.content.contains(pattern) || rule.incorrect_examples.iter().any(|ex| ex.contains(pattern)) {
207 result.add_error(format!(
208 "Anti-pattern detected: {} (from rule '{}')",
209 description, rule.title
210 ));
211 }
212 }
213 }
214 }
215
216 fn check_missing_critical_rules(
218 &self,
219 instructions: &str,
220 rules: &[Rule],
221 result: &mut ValidationResult,
222 ) {
223 let critical_rules: Vec<&Rule> = rules
224 .iter()
225 .filter(|r| r.impact == Impact::Critical)
226 .collect();
227
228 for rule in critical_rules {
229 let is_mentioned = Self::is_rule_mentioned(instructions, rule);
231
232 if !is_mentioned {
233 result.add_error(format!(
234 "Missing critical rule: '{}' - {}",
235 rule.title, rule.description
236 ));
237 }
238 }
239 }
240
241 fn check_missing_high_priority_rules(
243 &self,
244 instructions: &str,
245 rules: &[Rule],
246 result: &mut ValidationResult,
247 ) {
248 let high_priority_rules: Vec<&Rule> = rules
249 .iter()
250 .filter(|r| r.impact == Impact::High)
251 .collect();
252
253 for rule in high_priority_rules {
254 let is_mentioned = Self::is_rule_mentioned(instructions, rule);
255
256 if !is_mentioned {
257 result.add_warning(format!(
258 "Missing high-priority rule: '{}' - {}",
259 rule.title, rule.description
260 ));
261 }
262 }
263 }
264
265 fn is_rule_mentioned(instructions: &str, rule: &Rule) -> bool {
267 let instructions_lower = instructions.to_lowercase();
268
269 if instructions_lower.contains(&rule.title.to_lowercase()) {
271 return true;
272 }
273
274 for correct_example in &rule.correct_examples {
276 let normalized_example = Self::normalize_code(correct_example);
277 let normalized_instructions = Self::normalize_code(instructions);
278
279 if Self::contains_pattern(&normalized_instructions, &normalized_example) {
280 return true;
281 }
282 }
283
284 for tag in &rule.tags {
286 if instructions_lower.contains(&tag.to_lowercase()) {
287 return true;
288 }
289 }
290
291 false
292 }
293
294 fn normalize_code(code: &str) -> String {
296 code.lines()
297 .map(|line| {
298 let line = if let Some(pos) = line.find("//") {
300 &line[..pos]
301 } else {
302 line
303 };
304
305 line.split_whitespace().collect::<Vec<_>>().join(" ")
307 })
308 .filter(|line| !line.is_empty())
309 .collect::<Vec<_>>()
310 .join("\n")
311 }
312
313 fn contains_pattern(instructions: &str, pattern: &str) -> bool {
315 instructions.contains(pattern)
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn create_test_validator() -> InstructionValidator {
326 let skills = SkillsExtractor::new("vendor/skills/skills/composio");
327 InstructionValidator::new(skills)
328 }
329
330 #[test]
331 fn test_validation_result_new() {
332 let result = ValidationResult::new();
333 assert!(result.is_valid());
334 assert!(!result.has_warnings());
335 assert_eq!(result.total_issues(), 0);
336 }
337
338 #[test]
339 fn test_validation_result_add_error() {
340 let mut result = ValidationResult::new();
341 result.add_error("Test error");
342
343 assert!(!result.is_valid());
344 assert_eq!(result.errors.len(), 1);
345 assert_eq!(result.errors[0], "Test error");
346 }
347
348 #[test]
349 fn test_validation_result_add_warning() {
350 let mut result = ValidationResult::new();
351 result.add_warning("Test warning");
352
353 assert!(result.is_valid()); assert!(result.has_warnings());
355 assert_eq!(result.warnings.len(), 1);
356 assert_eq!(result.warnings[0], "Test warning");
357 }
358
359 #[test]
360 fn test_validation_result_format() {
361 let mut result = ValidationResult::new();
362 result.add_error("Error 1");
363 result.add_error("Error 2");
364 result.add_warning("Warning 1");
365
366 let formatted = result.format();
367
368 assert!(formatted.contains("❌ 2 Error(s)"));
369 assert!(formatted.contains("Error 1"));
370 assert!(formatted.contains("Error 2"));
371 assert!(formatted.contains("⚠️ 1 Warning(s)"));
372 assert!(formatted.contains("Warning 1"));
373 }
374
375 #[test]
376 fn test_validation_result_format_success() {
377 let result = ValidationResult::new();
378 let formatted = result.format();
379
380 assert!(formatted.contains("✅ Validation passed with no issues"));
381 }
382
383 #[test]
384 fn test_normalize_code() {
385 let code = r#"
386 let session = client.create_session("user_123"); // Create session
387 let tools = session.tools();
388 "#;
389
390 let normalized = InstructionValidator::normalize_code(code);
391
392 assert!(!normalized.contains("//"));
393 assert!(normalized.contains("create_session"));
394 assert!(normalized.contains("user_123"));
395 }
396
397 #[test]
398 fn test_contains_pattern() {
399 let instructions = "let session = client.create_session(\"user_123\");";
400 let pattern = "create_session(\"user_123\")";
401
402 assert!(InstructionValidator::contains_pattern(instructions, pattern));
403 }
404
405 #[test]
406 fn test_contains_pattern_not_found() {
407 let instructions = "let session = client.create_session(\"user_123\");";
408 let pattern = "create_session(\"default\")";
409
410 assert!(!InstructionValidator::contains_pattern(instructions, pattern));
411 }
412
413 #[test]
414 fn test_validator_creation() {
415 let validator = create_test_validator();
416 assert!(std::mem::size_of_val(&validator) > 0);
417 }
418
419 #[test]
420 #[ignore] fn test_validate_with_anti_pattern() {
422 let validator = create_test_validator();
423
424 let instructions = r#"
425 // Bad example - using "default" as user_id
426 let session = client.create_session("default");
427 "#;
428
429 let result = validator.validate(instructions);
430
431 if let Ok(validation) = result {
432 assert!(!validation.is_valid() || validation.has_warnings());
434 }
435 }
436
437 #[test]
438 #[ignore] fn test_validate_correct_pattern() {
440 let validator = create_test_validator();
441
442 let instructions = r#"
443 # Composio Wizard Instructions
444
445 ## Session Management
446
447 Always create sessions with a valid user_id:
448
449 ```rust
450 let session = client.create_session("user_123");
451 ```
452
453 ## Authentication
454
455 Use in-chat authentication for dynamic auth flows.
456 "#;
457
458 let result = validator.validate(instructions);
459
460 if let Ok(validation) = result {
461 println!("{}", validation.format());
463 }
464 }
465
466 #[test]
467 fn test_is_rule_mentioned_by_title() {
468 let instructions = "This document covers Session Management best practices.";
469
470 let rule = Rule {
471 title: "Session Management".to_string(),
472 impact: Impact::Critical,
473 description: "Best practices".to_string(),
474 tags: vec![],
475 content: String::new(),
476 correct_examples: vec![],
477 incorrect_examples: vec![],
478 };
479
480 assert!(InstructionValidator::is_rule_mentioned(instructions, &rule));
481 }
482
483 #[test]
484 fn test_is_rule_mentioned_by_tag() {
485 let instructions = "This document covers sessions and authentication.";
486
487 let rule = Rule {
488 title: "Some Rule".to_string(),
489 impact: Impact::High,
490 description: "Description".to_string(),
491 tags: vec!["sessions".to_string()],
492 content: String::new(),
493 correct_examples: vec![],
494 incorrect_examples: vec![],
495 };
496
497 assert!(InstructionValidator::is_rule_mentioned(instructions, &rule));
498 }
499
500 #[test]
501 fn test_is_rule_not_mentioned() {
502 let instructions = "This document covers something else entirely.";
503
504 let rule = Rule {
505 title: "Session Management".to_string(),
506 impact: Impact::Critical,
507 description: "Best practices".to_string(),
508 tags: vec!["sessions".to_string()],
509 content: String::new(),
510 correct_examples: vec![],
511 incorrect_examples: vec![],
512 };
513
514 assert!(!InstructionValidator::is_rule_mentioned(instructions, &rule));
515 }
516
517 #[test]
518 fn test_check_keyword_anti_patterns() {
519 let validator = create_test_validator();
520 let mut result = ValidationResult::new();
521
522 let instructions = r#"
523 let session = client.create_session("default");
524 let entity_id = "user_123";
525 "#;
526
527 let rule = Rule {
528 title: "User ID Best Practices".to_string(),
529 impact: Impact::Critical,
530 description: "Never use default".to_string(),
531 tags: vec![],
532 content: "Never use \"default\" as user_id".to_string(),
533 correct_examples: vec![],
534 incorrect_examples: vec!["create_session(\"default\")".to_string()],
535 };
536
537 validator.check_keyword_anti_patterns(instructions, &rule, &mut result);
538
539 assert!(!result.errors.is_empty());
541 }
542}