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 = SkillsExtractor::new("vendor/skills/skills/composio");
106    /// let validator = InstructionValidator::new(skills);
107    /// ```
108    pub fn new(skills: SkillsExtractor) -> Self {
109        Self { skills }
110    }
111
112    /// Validate wizard instructions against Skills best practices
113    ///
114    /// Checks for:
115    /// - Anti-patterns (❌ examples) in instructions
116    /// - Missing critical rules
117    /// - Missing high-priority rules (warnings)
118    ///
119    /// # Arguments
120    ///
121    /// * `instructions` - The wizard instructions to validate
122    ///
123    /// # Returns
124    ///
125    /// A ValidationResult containing errors and warnings
126    ///
127    /// # Example
128    ///
129    /// ```no_run
130    /// use composio_sdk::wizard::{SkillsExtractor, InstructionValidator};
131    ///
132    /// let skills = SkillsExtractor::new("vendor/skills/skills/composio");
133    /// let validator = InstructionValidator::new(skills);
134    ///
135    /// let instructions = "let session = client.create_session(\"default\");";
136    /// let result = validator.validate(instructions).unwrap();
137    ///
138    /// if !result.is_valid() {
139    ///     println!("{}", result.format());
140    /// }
141    /// ```
142    pub fn validate(&self, instructions: &str) -> Result<ValidationResult, SkillsError> {
143        let mut result = ValidationResult::new();
144
145        // Get all rules from Skills
146        let all_rules = self.skills.get_all_rules()?;
147
148        // Check for anti-patterns
149        self.check_anti_patterns(instructions, &all_rules, &mut result);
150
151        // Check for missing critical rules
152        self.check_missing_critical_rules(instructions, &all_rules, &mut result);
153
154        // Check for missing high-priority rules (warnings)
155        self.check_missing_high_priority_rules(instructions, &all_rules, &mut result);
156
157        Ok(result)
158    }
159
160    /// Check for anti-patterns (❌ examples) in instructions
161    fn check_anti_patterns(
162        &self,
163        instructions: &str,
164        rules: &[Rule],
165        result: &mut ValidationResult,
166    ) {
167        for rule in rules {
168            // Check each incorrect example
169            for incorrect_example in &rule.incorrect_examples {
170                // Normalize whitespace for comparison
171                let normalized_example = Self::normalize_code(incorrect_example);
172                let normalized_instructions = Self::normalize_code(instructions);
173
174                // Check if the anti-pattern appears in instructions
175                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            // Check for common anti-pattern keywords
185            self.check_keyword_anti_patterns(instructions, rule, result);
186        }
187    }
188
189    /// Check for keyword-based anti-patterns
190    fn check_keyword_anti_patterns(
191        &self,
192        instructions: &str,
193        rule: &Rule,
194        result: &mut ValidationResult,
195    ) {
196        // Common anti-patterns to check
197        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                // Only report if it's in a critical rule context
206                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    /// Check for missing critical rules
217    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            // Check if the rule title or key concepts are mentioned
230            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    /// Check for missing high-priority rules (warnings)
242    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    /// Check if a rule is mentioned in instructions
266    fn is_rule_mentioned(instructions: &str, rule: &Rule) -> bool {
267        let instructions_lower = instructions.to_lowercase();
268
269        // Check if title is mentioned
270        if instructions_lower.contains(&rule.title.to_lowercase()) {
271            return true;
272        }
273
274        // Check if any correct examples are present
275        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        // Check if key tags are mentioned
285        for tag in &rule.tags {
286            if instructions_lower.contains(&tag.to_lowercase()) {
287                return true;
288            }
289        }
290
291        false
292    }
293
294    /// Normalize code by removing extra whitespace and comments
295    fn normalize_code(code: &str) -> String {
296        code.lines()
297            .map(|line| {
298                // Remove comments
299                let line = if let Some(pos) = line.find("//") {
300                    &line[..pos]
301                } else {
302                    line
303                };
304
305                // Trim and normalize whitespace
306                line.split_whitespace().collect::<Vec<_>>().join(" ")
307            })
308            .filter(|line| !line.is_empty())
309            .collect::<Vec<_>>()
310            .join("\n")
311    }
312
313    /// Check if instructions contain a pattern (fuzzy matching)
314    fn contains_pattern(instructions: &str, pattern: &str) -> bool {
315        // Simple substring matching for now
316        // Could be enhanced with more sophisticated pattern matching
317        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()); // Still valid with only warnings
354        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] // Requires Skills repository to be present
421    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            // Should detect the "default" anti-pattern
433            assert!(!validation.is_valid() || validation.has_warnings());
434        }
435    }
436
437    #[test]
438    #[ignore] // Requires Skills repository to be present
439    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            // Should pass validation or have minimal warnings
462            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        // Should detect anti-patterns
540        assert!(!result.errors.is_empty());
541    }
542}