Skip to main content

composio_sdk/wizard/
validator.rs

1//! Instruction validator for wizard instructions
2//!
3//! This module validates generated wizard instructions against Composio Skills
4//! best practices, checking for anti-patterns and missing critical rules.
5
6use super::skills::{Impact, Rule, SkillsExtractor, SkillsError};
7
8/// Result of instruction validation
9#[derive(Debug, Clone, Default)]
10pub struct ValidationResult {
11    /// Validation errors (must be fixed)
12    pub errors: Vec<String>,
13    /// Validation warnings (should be addressed)
14    pub warnings: Vec<String>,
15}
16
17impl ValidationResult {
18    /// Create a new empty validation result
19    pub fn new() -> Self {
20        Self {
21            errors: Vec::new(),
22            warnings: Vec::new(),
23        }
24    }
25
26    /// Add an error to the validation result
27    ///
28    /// # Arguments
29    ///
30    /// * `error` - Error message to add
31    pub fn add_error(&mut self, error: impl Into<String>) {
32        self.errors.push(error.into());
33    }
34
35    /// Add a warning to the validation result
36    ///
37    /// # Arguments
38    ///
39    /// * `warning` - Warning message to add
40    pub fn add_warning(&mut self, warning: impl Into<String>) {
41        self.warnings.push(warning.into());
42    }
43
44    /// Check if validation passed (no errors)
45    pub fn is_valid(&self) -> bool {
46        self.errors.is_empty()
47    }
48
49    /// Check if there are any warnings
50    pub fn has_warnings(&self) -> bool {
51        !self.warnings.is_empty()
52    }
53
54    /// Get total number of issues (errors + warnings)
55    pub fn total_issues(&self) -> usize {
56        self.errors.len() + self.warnings.len()
57    }
58
59    /// Format validation result as a human-readable string
60    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/// Validator for wizard instructions
88#[derive(Debug, Clone)]
89pub struct InstructionValidator {
90    skills: SkillsExtractor,
91}
92
93impl InstructionValidator {
94    /// Create a new instruction validator
95    ///
96    /// # Arguments
97    ///
98    /// * `skills` - SkillsExtractor instance for accessing Skills content
99    ///
100    /// # Example
101    ///
102    /// ```no_run
103    /// use composio_sdk::wizard::{SkillsExtractor, InstructionValidator};
104    ///
105    /// let skills_path = concat!(env!("CARGO_MANIFEST_DIR"), "/skills");
106    /// let skills = SkillsExtractor::new(skills_path);
107    /// let validator = InstructionValidator::new(skills);
108    /// ```
109    pub fn new(skills: SkillsExtractor) -> Self {
110        Self { skills }
111    }
112
113    /// Validate wizard instructions against Skills best practices
114    ///
115    /// Checks for:
116    /// - Anti-patterns (❌ examples) in instructions
117    /// - Missing critical rules
118    /// - Missing high-priority rules (warnings)
119    ///
120    /// # Arguments
121    ///
122    /// * `instructions` - The wizard instructions to validate
123    ///
124    /// # Returns
125    ///
126    /// A ValidationResult containing errors and warnings
127    ///
128    /// # Example
129    ///
130    /// ```no_run
131    /// use composio_sdk::wizard::{SkillsExtractor, InstructionValidator};
132    ///
133    /// let skills_path = concat!(env!("CARGO_MANIFEST_DIR"), "/skills");
134    /// let skills = SkillsExtractor::new(skills_path);
135    /// let validator = InstructionValidator::new(skills);
136    ///
137    /// let instructions = "let session = client.create_session(\"default\");";
138    /// let result = validator.validate(instructions).unwrap();
139    ///
140    /// if !result.is_valid() {
141    ///     println!("{}", result.format());
142    /// }
143    /// ```
144    pub fn validate(&self, instructions: &str) -> Result<ValidationResult, SkillsError> {
145        let mut result = ValidationResult::new();
146
147        // Get all rules from Skills
148        let all_rules = self.skills.get_all_rules()?;
149
150        // Check for anti-patterns
151        self.check_anti_patterns(instructions, &all_rules, &mut result);
152
153        // Check for missing critical rules
154        self.check_missing_critical_rules(instructions, &all_rules, &mut result);
155
156        // Check for missing high-priority rules (warnings)
157        self.check_missing_high_priority_rules(instructions, &all_rules, &mut result);
158
159        Ok(result)
160    }
161
162    /// Check for anti-patterns (❌ examples) in instructions
163    fn check_anti_patterns(
164        &self,
165        instructions: &str,
166        rules: &[Rule],
167        result: &mut ValidationResult,
168    ) {
169        for rule in rules {
170            // Check each incorrect example
171            for incorrect_example in &rule.incorrect_examples {
172                // Normalize whitespace for comparison
173                let normalized_example = Self::normalize_code(incorrect_example);
174                let normalized_instructions = Self::normalize_code(instructions);
175
176                // Check if the anti-pattern appears in instructions
177                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            // Check for common anti-pattern keywords
187            self.check_keyword_anti_patterns(instructions, rule, result);
188        }
189    }
190
191    /// Check for keyword-based anti-patterns
192    fn check_keyword_anti_patterns(
193        &self,
194        instructions: &str,
195        rule: &Rule,
196        result: &mut ValidationResult,
197    ) {
198        // Common anti-patterns to check
199        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                // Only report if it's in a critical rule context
208                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    /// Check for missing critical rules
219    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            // Check if the rule title or key concepts are mentioned
232            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    /// Check for missing high-priority rules (warnings)
244    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    /// Check if a rule is mentioned in instructions
268    fn is_rule_mentioned(instructions: &str, rule: &Rule) -> bool {
269        let instructions_lower = instructions.to_lowercase();
270
271        // Check if title is mentioned
272        if instructions_lower.contains(&rule.title.to_lowercase()) {
273            return true;
274        }
275
276        // Check if any correct examples are present
277        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        // Check if key tags are mentioned
287        for tag in &rule.tags {
288            if instructions_lower.contains(&tag.to_lowercase()) {
289                return true;
290            }
291        }
292
293        false
294    }
295
296    /// Normalize code by removing extra whitespace and comments
297    fn normalize_code(code: &str) -> String {
298        code.lines()
299            .map(|line| {
300                // Remove comments
301                let line = if let Some(pos) = line.find("//") {
302                    &line[..pos]
303                } else {
304                    line
305                };
306
307                // Trim and normalize whitespace
308                line.split_whitespace().collect::<Vec<_>>().join(" ")
309            })
310            .filter(|line| !line.is_empty())
311            .collect::<Vec<_>>()
312            .join("\n")
313    }
314
315    /// Check if instructions contain a pattern (fuzzy matching)
316    fn contains_pattern(instructions: &str, pattern: &str) -> bool {
317        // Simple substring matching for now
318        // Could be enhanced with more sophisticated pattern matching
319        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()); // Still valid with only warnings
357        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] // Requires Skills repository to be present
424    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            // Should detect the "default" anti-pattern
436            assert!(!validation.is_valid() || validation.has_warnings());
437        }
438    }
439
440    #[test]
441    #[ignore] // Requires Skills repository to be present
442    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            // Should pass validation or have minimal warnings
465            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        // Should detect anti-patterns
543        assert!(!result.errors.is_empty());
544    }
545}