use super::skills::{Impact, Rule, SkillsExtractor, SkillsError};
#[derive(Debug, Clone, Default)]
pub struct ValidationResult {
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ValidationResult {
pub fn new() -> Self {
Self {
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn add_error(&mut self, error: impl Into<String>) {
self.errors.push(error.into());
}
pub fn add_warning(&mut self, warning: impl Into<String>) {
self.warnings.push(warning.into());
}
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn total_issues(&self) -> usize {
self.errors.len() + self.warnings.len()
}
pub fn format(&self) -> String {
let mut output = String::new();
if !self.errors.is_empty() {
output.push_str(&format!("❌ {} Error(s):\n", self.errors.len()));
for (i, error) in self.errors.iter().enumerate() {
output.push_str(&format!(" {}. {}\n", i + 1, error));
}
output.push('\n');
}
if !self.warnings.is_empty() {
output.push_str(&format!("⚠️ {} Warning(s):\n", self.warnings.len()));
for (i, warning) in self.warnings.iter().enumerate() {
output.push_str(&format!(" {}. {}\n", i + 1, warning));
}
output.push('\n');
}
if self.is_valid() && !self.has_warnings() {
output.push_str("✅ Validation passed with no issues\n");
}
output
}
}
#[derive(Debug, Clone)]
pub struct InstructionValidator {
skills: SkillsExtractor,
}
impl InstructionValidator {
pub fn new(skills: SkillsExtractor) -> Self {
Self { skills }
}
pub fn validate(&self, instructions: &str) -> Result<ValidationResult, SkillsError> {
let mut result = ValidationResult::new();
let all_rules = self.skills.get_all_rules()?;
self.check_anti_patterns(instructions, &all_rules, &mut result);
self.check_missing_critical_rules(instructions, &all_rules, &mut result);
self.check_missing_high_priority_rules(instructions, &all_rules, &mut result);
Ok(result)
}
fn check_anti_patterns(
&self,
instructions: &str,
rules: &[Rule],
result: &mut ValidationResult,
) {
for rule in rules {
for incorrect_example in &rule.incorrect_examples {
let normalized_example = Self::normalize_code(incorrect_example);
let normalized_instructions = Self::normalize_code(instructions);
if Self::contains_pattern(&normalized_instructions, &normalized_example) {
result.add_error(format!(
"Anti-pattern detected from rule '{}' ({}): Found code matching incorrect example",
rule.title,
rule.impact.as_str()
));
}
}
self.check_keyword_anti_patterns(instructions, rule, result);
}
}
fn check_keyword_anti_patterns(
&self,
instructions: &str,
rule: &Rule,
result: &mut ValidationResult,
) {
let anti_patterns = [
("\"default\"", "Using 'default' as user_id"),
("entity_id", "Using deprecated 'entity_id' instead of 'user_id'"),
("actions", "Using deprecated 'actions' instead of 'tools'"),
];
for (pattern, description) in &anti_patterns {
if instructions.contains(pattern) && rule.impact == Impact::Critical {
if rule.content.contains(pattern) || rule.incorrect_examples.iter().any(|ex| ex.contains(pattern)) {
result.add_error(format!(
"Anti-pattern detected: {} (from rule '{}')",
description, rule.title
));
}
}
}
}
fn check_missing_critical_rules(
&self,
instructions: &str,
rules: &[Rule],
result: &mut ValidationResult,
) {
let critical_rules: Vec<&Rule> = rules
.iter()
.filter(|r| r.impact == Impact::Critical)
.collect();
for rule in critical_rules {
let is_mentioned = Self::is_rule_mentioned(instructions, rule);
if !is_mentioned {
result.add_error(format!(
"Missing critical rule: '{}' - {}",
rule.title, rule.description
));
}
}
}
fn check_missing_high_priority_rules(
&self,
instructions: &str,
rules: &[Rule],
result: &mut ValidationResult,
) {
let high_priority_rules: Vec<&Rule> = rules
.iter()
.filter(|r| r.impact == Impact::High)
.collect();
for rule in high_priority_rules {
let is_mentioned = Self::is_rule_mentioned(instructions, rule);
if !is_mentioned {
result.add_warning(format!(
"Missing high-priority rule: '{}' - {}",
rule.title, rule.description
));
}
}
}
fn is_rule_mentioned(instructions: &str, rule: &Rule) -> bool {
let instructions_lower = instructions.to_lowercase();
if instructions_lower.contains(&rule.title.to_lowercase()) {
return true;
}
for correct_example in &rule.correct_examples {
let normalized_example = Self::normalize_code(correct_example);
let normalized_instructions = Self::normalize_code(instructions);
if Self::contains_pattern(&normalized_instructions, &normalized_example) {
return true;
}
}
for tag in &rule.tags {
if instructions_lower.contains(&tag.to_lowercase()) {
return true;
}
}
false
}
fn normalize_code(code: &str) -> String {
code.lines()
.map(|line| {
let line = if let Some(pos) = line.find("//") {
&line[..pos]
} else {
line
};
line.split_whitespace().collect::<Vec<_>>().join(" ")
})
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
fn contains_pattern(instructions: &str, pattern: &str) -> bool {
instructions.contains(pattern)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_validator() -> InstructionValidator {
let skills_path = concat!(env!("CARGO_MANIFEST_DIR"), "/skills");
let skills = SkillsExtractor::new(skills_path);
InstructionValidator::new(skills)
}
#[test]
fn test_validation_result_new() {
let result = ValidationResult::new();
assert!(result.is_valid());
assert!(!result.has_warnings());
assert_eq!(result.total_issues(), 0);
}
#[test]
fn test_validation_result_add_error() {
let mut result = ValidationResult::new();
result.add_error("Test error");
assert!(!result.is_valid());
assert_eq!(result.errors.len(), 1);
assert_eq!(result.errors[0], "Test error");
}
#[test]
fn test_validation_result_add_warning() {
let mut result = ValidationResult::new();
result.add_warning("Test warning");
assert!(result.is_valid()); assert!(result.has_warnings());
assert_eq!(result.warnings.len(), 1);
assert_eq!(result.warnings[0], "Test warning");
}
#[test]
fn test_validation_result_format() {
let mut result = ValidationResult::new();
result.add_error("Error 1");
result.add_error("Error 2");
result.add_warning("Warning 1");
let formatted = result.format();
assert!(formatted.contains("❌ 2 Error(s)"));
assert!(formatted.contains("Error 1"));
assert!(formatted.contains("Error 2"));
assert!(formatted.contains("⚠️ 1 Warning(s)"));
assert!(formatted.contains("Warning 1"));
}
#[test]
fn test_validation_result_format_success() {
let result = ValidationResult::new();
let formatted = result.format();
assert!(formatted.contains("✅ Validation passed with no issues"));
}
#[test]
fn test_normalize_code() {
let code = r#"
let session = client.create_session("user_123"); // Create session
let tools = session.tools();
"#;
let normalized = InstructionValidator::normalize_code(code);
assert!(!normalized.contains("//"));
assert!(normalized.contains("create_session"));
assert!(normalized.contains("user_123"));
}
#[test]
fn test_contains_pattern() {
let instructions = "let session = client.create_session(\"user_123\");";
let pattern = "create_session(\"user_123\")";
assert!(InstructionValidator::contains_pattern(instructions, pattern));
}
#[test]
fn test_contains_pattern_not_found() {
let instructions = "let session = client.create_session(\"user_123\");";
let pattern = "create_session(\"default\")";
assert!(!InstructionValidator::contains_pattern(instructions, pattern));
}
#[test]
fn test_validator_creation() {
let validator = create_test_validator();
assert!(std::mem::size_of_val(&validator) > 0);
}
#[test]
#[ignore] fn test_validate_with_anti_pattern() {
let validator = create_test_validator();
let instructions = r#"
// Bad example - using "default" as user_id
let session = client.create_session("default");
"#;
let result = validator.validate(instructions);
if let Ok(validation) = result {
assert!(!validation.is_valid() || validation.has_warnings());
}
}
#[test]
#[ignore] fn test_validate_correct_pattern() {
let validator = create_test_validator();
let instructions = r#"
# Composio Wizard Instructions
## Session Management
Always create sessions with a valid user_id:
```rust
let session = client.create_session("user_123");
```
## Authentication
Use in-chat authentication for dynamic auth flows.
"#;
let result = validator.validate(instructions);
if let Ok(validation) = result {
println!("{}", validation.format());
}
}
#[test]
fn test_is_rule_mentioned_by_title() {
let instructions = "This document covers Session Management best practices.";
let rule = Rule {
title: "Session Management".to_string(),
impact: Impact::Critical,
description: "Best practices".to_string(),
tags: vec![],
content: String::new(),
correct_examples: vec![],
incorrect_examples: vec![],
};
assert!(InstructionValidator::is_rule_mentioned(instructions, &rule));
}
#[test]
fn test_is_rule_mentioned_by_tag() {
let instructions = "This document covers sessions and authentication.";
let rule = Rule {
title: "Some Rule".to_string(),
impact: Impact::High,
description: "Description".to_string(),
tags: vec!["sessions".to_string()],
content: String::new(),
correct_examples: vec![],
incorrect_examples: vec![],
};
assert!(InstructionValidator::is_rule_mentioned(instructions, &rule));
}
#[test]
fn test_is_rule_not_mentioned() {
let instructions = "This document covers something else entirely.";
let rule = Rule {
title: "Session Management".to_string(),
impact: Impact::Critical,
description: "Best practices".to_string(),
tags: vec!["sessions".to_string()],
content: String::new(),
correct_examples: vec![],
incorrect_examples: vec![],
};
assert!(!InstructionValidator::is_rule_mentioned(instructions, &rule));
}
#[test]
fn test_check_keyword_anti_patterns() {
let validator = create_test_validator();
let mut result = ValidationResult::new();
let instructions = r#"
let session = client.create_session("default");
let entity_id = "user_123";
"#;
let rule = Rule {
title: "User ID Best Practices".to_string(),
impact: Impact::Critical,
description: "Never use default".to_string(),
tags: vec![],
content: "Never use \"default\" as user_id".to_string(),
correct_examples: vec![],
incorrect_examples: vec!["create_session(\"default\")".to_string()],
};
validator.check_keyword_anti_patterns(instructions, &rule, &mut result);
assert!(!result.errors.is_empty());
}
}