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 {
110 Self { skills }
111 }
112
113 pub fn validate(&self, instructions: &str) -> Result<ValidationResult, SkillsError> {
145 let mut result = ValidationResult::new();
146
147 let all_rules = self.skills.get_all_rules()?;
149
150 self.check_anti_patterns(instructions, &all_rules, &mut result);
152
153 self.check_missing_critical_rules(instructions, &all_rules, &mut result);
155
156 self.check_missing_high_priority_rules(instructions, &all_rules, &mut result);
158
159 Ok(result)
160 }
161
162 fn check_anti_patterns(
164 &self,
165 instructions: &str,
166 rules: &[Rule],
167 result: &mut ValidationResult,
168 ) {
169 for rule in rules {
170 for incorrect_example in &rule.incorrect_examples {
172 let normalized_example = Self::normalize_code(incorrect_example);
174 let normalized_instructions = Self::normalize_code(instructions);
175
176 if Self::contains_pattern(&normalized_instructions, &normalized_example) {
178 result.add_error(format!(
179 "Anti-pattern detected from rule '{}' ({}): Found code matching incorrect example",
180 rule.title,
181 rule.impact.as_str()
182 ));
183 }
184 }
185
186 self.check_keyword_anti_patterns(instructions, rule, result);
188 }
189 }
190
191 fn check_keyword_anti_patterns(
193 &self,
194 instructions: &str,
195 rule: &Rule,
196 result: &mut ValidationResult,
197 ) {
198 let anti_patterns = [
200 ("\"default\"", "Using 'default' as user_id"),
201 ("entity_id", "Using deprecated 'entity_id' instead of 'user_id'"),
202 ("actions", "Using deprecated 'actions' instead of 'tools'"),
203 ];
204
205 for (pattern, description) in &anti_patterns {
206 if instructions.contains(pattern) && rule.impact == Impact::Critical {
207 if rule.content.contains(pattern) || rule.incorrect_examples.iter().any(|ex| ex.contains(pattern)) {
209 result.add_error(format!(
210 "Anti-pattern detected: {} (from rule '{}')",
211 description, rule.title
212 ));
213 }
214 }
215 }
216 }
217
218 fn check_missing_critical_rules(
220 &self,
221 instructions: &str,
222 rules: &[Rule],
223 result: &mut ValidationResult,
224 ) {
225 let critical_rules: Vec<&Rule> = rules
226 .iter()
227 .filter(|r| r.impact == Impact::Critical)
228 .collect();
229
230 for rule in critical_rules {
231 let is_mentioned = Self::is_rule_mentioned(instructions, rule);
233
234 if !is_mentioned {
235 result.add_error(format!(
236 "Missing critical rule: '{}' - {}",
237 rule.title, rule.description
238 ));
239 }
240 }
241 }
242
243 fn check_missing_high_priority_rules(
245 &self,
246 instructions: &str,
247 rules: &[Rule],
248 result: &mut ValidationResult,
249 ) {
250 let high_priority_rules: Vec<&Rule> = rules
251 .iter()
252 .filter(|r| r.impact == Impact::High)
253 .collect();
254
255 for rule in high_priority_rules {
256 let is_mentioned = Self::is_rule_mentioned(instructions, rule);
257
258 if !is_mentioned {
259 result.add_warning(format!(
260 "Missing high-priority rule: '{}' - {}",
261 rule.title, rule.description
262 ));
263 }
264 }
265 }
266
267 fn is_rule_mentioned(instructions: &str, rule: &Rule) -> bool {
269 let instructions_lower = instructions.to_lowercase();
270
271 if instructions_lower.contains(&rule.title.to_lowercase()) {
273 return true;
274 }
275
276 for correct_example in &rule.correct_examples {
278 let normalized_example = Self::normalize_code(correct_example);
279 let normalized_instructions = Self::normalize_code(instructions);
280
281 if Self::contains_pattern(&normalized_instructions, &normalized_example) {
282 return true;
283 }
284 }
285
286 for tag in &rule.tags {
288 if instructions_lower.contains(&tag.to_lowercase()) {
289 return true;
290 }
291 }
292
293 false
294 }
295
296 fn normalize_code(code: &str) -> String {
298 code.lines()
299 .map(|line| {
300 let line = if let Some(pos) = line.find("//") {
302 &line[..pos]
303 } else {
304 line
305 };
306
307 line.split_whitespace().collect::<Vec<_>>().join(" ")
309 })
310 .filter(|line| !line.is_empty())
311 .collect::<Vec<_>>()
312 .join("\n")
313 }
314
315 fn contains_pattern(instructions: &str, pattern: &str) -> bool {
317 instructions.contains(pattern)
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn create_test_validator() -> InstructionValidator {
328 let skills_path = concat!(env!("CARGO_MANIFEST_DIR"), "/skills");
329 let skills = SkillsExtractor::new(skills_path);
330 InstructionValidator::new(skills)
331 }
332
333 #[test]
334 fn test_validation_result_new() {
335 let result = ValidationResult::new();
336 assert!(result.is_valid());
337 assert!(!result.has_warnings());
338 assert_eq!(result.total_issues(), 0);
339 }
340
341 #[test]
342 fn test_validation_result_add_error() {
343 let mut result = ValidationResult::new();
344 result.add_error("Test error");
345
346 assert!(!result.is_valid());
347 assert_eq!(result.errors.len(), 1);
348 assert_eq!(result.errors[0], "Test error");
349 }
350
351 #[test]
352 fn test_validation_result_add_warning() {
353 let mut result = ValidationResult::new();
354 result.add_warning("Test warning");
355
356 assert!(result.is_valid()); assert!(result.has_warnings());
358 assert_eq!(result.warnings.len(), 1);
359 assert_eq!(result.warnings[0], "Test warning");
360 }
361
362 #[test]
363 fn test_validation_result_format() {
364 let mut result = ValidationResult::new();
365 result.add_error("Error 1");
366 result.add_error("Error 2");
367 result.add_warning("Warning 1");
368
369 let formatted = result.format();
370
371 assert!(formatted.contains("❌ 2 Error(s)"));
372 assert!(formatted.contains("Error 1"));
373 assert!(formatted.contains("Error 2"));
374 assert!(formatted.contains("⚠️ 1 Warning(s)"));
375 assert!(formatted.contains("Warning 1"));
376 }
377
378 #[test]
379 fn test_validation_result_format_success() {
380 let result = ValidationResult::new();
381 let formatted = result.format();
382
383 assert!(formatted.contains("✅ Validation passed with no issues"));
384 }
385
386 #[test]
387 fn test_normalize_code() {
388 let code = r#"
389 let session = client.create_session("user_123"); // Create session
390 let tools = session.tools();
391 "#;
392
393 let normalized = InstructionValidator::normalize_code(code);
394
395 assert!(!normalized.contains("//"));
396 assert!(normalized.contains("create_session"));
397 assert!(normalized.contains("user_123"));
398 }
399
400 #[test]
401 fn test_contains_pattern() {
402 let instructions = "let session = client.create_session(\"user_123\");";
403 let pattern = "create_session(\"user_123\")";
404
405 assert!(InstructionValidator::contains_pattern(instructions, pattern));
406 }
407
408 #[test]
409 fn test_contains_pattern_not_found() {
410 let instructions = "let session = client.create_session(\"user_123\");";
411 let pattern = "create_session(\"default\")";
412
413 assert!(!InstructionValidator::contains_pattern(instructions, pattern));
414 }
415
416 #[test]
417 fn test_validator_creation() {
418 let validator = create_test_validator();
419 assert!(std::mem::size_of_val(&validator) > 0);
420 }
421
422 #[test]
423 #[ignore] fn test_validate_with_anti_pattern() {
425 let validator = create_test_validator();
426
427 let instructions = r#"
428 // Bad example - using "default" as user_id
429 let session = client.create_session("default");
430 "#;
431
432 let result = validator.validate(instructions);
433
434 if let Ok(validation) = result {
435 assert!(!validation.is_valid() || validation.has_warnings());
437 }
438 }
439
440 #[test]
441 #[ignore] fn test_validate_correct_pattern() {
443 let validator = create_test_validator();
444
445 let instructions = r#"
446 # Composio Wizard Instructions
447
448 ## Session Management
449
450 Always create sessions with a valid user_id:
451
452 ```rust
453 let session = client.create_session("user_123");
454 ```
455
456 ## Authentication
457
458 Use in-chat authentication for dynamic auth flows.
459 "#;
460
461 let result = validator.validate(instructions);
462
463 if let Ok(validation) = result {
464 println!("{}", validation.format());
466 }
467 }
468
469 #[test]
470 fn test_is_rule_mentioned_by_title() {
471 let instructions = "This document covers Session Management best practices.";
472
473 let rule = Rule {
474 title: "Session Management".to_string(),
475 impact: Impact::Critical,
476 description: "Best practices".to_string(),
477 tags: vec![],
478 content: String::new(),
479 correct_examples: vec![],
480 incorrect_examples: vec![],
481 };
482
483 assert!(InstructionValidator::is_rule_mentioned(instructions, &rule));
484 }
485
486 #[test]
487 fn test_is_rule_mentioned_by_tag() {
488 let instructions = "This document covers sessions and authentication.";
489
490 let rule = Rule {
491 title: "Some Rule".to_string(),
492 impact: Impact::High,
493 description: "Description".to_string(),
494 tags: vec!["sessions".to_string()],
495 content: String::new(),
496 correct_examples: vec![],
497 incorrect_examples: vec![],
498 };
499
500 assert!(InstructionValidator::is_rule_mentioned(instructions, &rule));
501 }
502
503 #[test]
504 fn test_is_rule_not_mentioned() {
505 let instructions = "This document covers something else entirely.";
506
507 let rule = Rule {
508 title: "Session Management".to_string(),
509 impact: Impact::Critical,
510 description: "Best practices".to_string(),
511 tags: vec!["sessions".to_string()],
512 content: String::new(),
513 correct_examples: vec![],
514 incorrect_examples: vec![],
515 };
516
517 assert!(!InstructionValidator::is_rule_mentioned(instructions, &rule));
518 }
519
520 #[test]
521 fn test_check_keyword_anti_patterns() {
522 let validator = create_test_validator();
523 let mut result = ValidationResult::new();
524
525 let instructions = r#"
526 let session = client.create_session("default");
527 let entity_id = "user_123";
528 "#;
529
530 let rule = Rule {
531 title: "User ID Best Practices".to_string(),
532 impact: Impact::Critical,
533 description: "Never use default".to_string(),
534 tags: vec![],
535 content: "Never use \"default\" as user_id".to_string(),
536 correct_examples: vec![],
537 incorrect_examples: vec!["create_session(\"default\")".to_string()],
538 };
539
540 validator.check_keyword_anti_patterns(instructions, &rule, &mut result);
541
542 assert!(!result.errors.is_empty());
544 }
545}