Skip to main content

vtcode_skills/
validation_report.rs

1//! Comprehensive validation report for Agent Skills
2//!
3//! Provides detailed validation feedback with multiple error levels and suggestions.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::path::PathBuf;
8
9/// Severity level for validation issues
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ValidationLevel {
12    /// Critical error that prevents skill from working
13    Error,
14    /// Warning that may cause issues but doesn't prevent usage
15    Warning,
16    /// Suggestion for improvement
17    Info,
18}
19
20/// Single validation issue
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ValidationIssue {
23    level: ValidationLevel,
24    pub(crate) field: Option<String>,
25    message: String,
26    suggestion: Option<String>,
27}
28
29impl fmt::Display for ValidationIssue {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(f, "{}", self.message)?;
32        if let Some(field) = &self.field {
33            write!(f, " (field: {field})")?;
34        }
35        if let Some(suggestion) = &self.suggestion {
36            write!(f, " — {suggestion}")?;
37        }
38        Ok(())
39    }
40}
41
42/// Comprehensive skill validation report
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SkillValidationReport {
45    skill_name: String,
46    skill_path: PathBuf,
47    pub is_valid: bool,
48    errors: Vec<ValidationIssue>,
49    warnings: Vec<ValidationIssue>,
50    pub(crate) suggestions: Vec<ValidationIssue>,
51    pub stats: ValidationStats,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ValidationStats {
56    total_issues: usize,
57    pub error_count: usize,
58    warning_count: usize,
59    suggestion_count: usize,
60    token_estimate: usize,
61}
62
63impl SkillValidationReport {
64    pub(crate) fn new(skill_name: String, skill_path: PathBuf) -> Self {
65        Self {
66            skill_name,
67            skill_path,
68            is_valid: true,
69            errors: Vec::new(),
70            warnings: Vec::new(),
71            suggestions: Vec::new(),
72            stats: ValidationStats {
73                total_issues: 0,
74                error_count: 0,
75                warning_count: 0,
76                suggestion_count: 0,
77                token_estimate: 0,
78            },
79        }
80    }
81
82    pub(crate) fn add_error(&mut self, field: Option<String>, message: String, suggestion: Option<String>) {
83        self.errors.push(ValidationIssue {
84            level: ValidationLevel::Error,
85            field,
86            message,
87            suggestion,
88        });
89        self.is_valid = false;
90    }
91
92    pub(crate) fn add_warning(&mut self, field: Option<String>, message: String, suggestion: Option<String>) {
93        self.warnings.push(ValidationIssue {
94            level: ValidationLevel::Warning,
95            field,
96            message,
97            suggestion,
98        });
99    }
100
101    pub(crate) fn add_suggestion(&mut self, field: Option<String>, message: String) {
102        self.suggestions.push(ValidationIssue {
103            level: ValidationLevel::Info,
104            field,
105            message,
106            suggestion: None,
107        });
108    }
109
110    pub fn finalize(&mut self) {
111        self.stats.error_count = self.errors.len();
112        self.stats.warning_count = self.warnings.len();
113        self.stats.suggestion_count = self.suggestions.len();
114        self.stats.total_issues = self.stats.error_count + self.stats.warning_count + self.stats.suggestion_count;
115    }
116
117    pub fn generate_summary(&self) -> String {
118        let mut summary = String::new();
119
120        summary.push_str(&format!("Skill: {}\n", self.skill_name));
121        summary.push_str(&format!("Path: {}\n", self.skill_path.display()));
122        summary.push_str(&format!("Status: {}\n", if self.is_valid { "v Valid" } else { "x Invalid" }));
123        summary.push_str("\nIssues found:\n");
124        summary.push_str(&format!("  Errors: {}\n", self.stats.error_count));
125        summary.push_str(&format!("  Warnings: {}\n", self.stats.warning_count));
126        summary.push_str(&format!("  Suggestions: {}\n", self.stats.suggestion_count));
127
128        if !self.errors.is_empty() {
129            summary.push_str("\nx Errors:\n");
130            for error in &self.errors {
131                summary.push_str(&format!("  - {}", error.message));
132                if let Some(field) = &error.field {
133                    summary.push_str(&format!(" [{field}]"));
134                }
135                summary.push('\n');
136                if let Some(suggestion) = &error.suggestion {
137                    summary.push_str(&format!("    šŸ’” Suggestion: {suggestion}\n"));
138                }
139            }
140        }
141
142        if !self.warnings.is_empty() {
143            summary.push_str("\n[!]  Warnings:\n");
144            for warning in &self.warnings {
145                summary.push_str(&format!("  - {}", warning.message));
146                if let Some(field) = &warning.field {
147                    summary.push_str(&format!(" [{field}]"));
148                }
149                summary.push('\n');
150            }
151        }
152
153        if !self.suggestions.is_empty() {
154            summary.push_str("\nšŸ’” Suggestions:\n");
155            for suggestion in &self.suggestions {
156                summary.push_str(&format!("  - {}", suggestion.message));
157                if let Some(field) = &suggestion.field {
158                    summary.push_str(&format!(" [{field}]"));
159                }
160                summary.push('\n');
161            }
162        }
163
164        summary
165    }
166
167    pub fn to_json(&self) -> Result<String, serde_json::Error> {
168        serde_json::to_string_pretty(self)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use std::path::PathBuf;
176
177    #[test]
178    fn test_validation_report() {
179        let mut report = SkillValidationReport::new("test-skill".to_string(), PathBuf::from("/tmp/test-skill"));
180
181        report.add_error(
182            Some("name".to_string()),
183            "Invalid characters in name".to_string(),
184            Some("Use only lowercase letters, numbers, and hyphens".to_string()),
185        );
186
187        report.add_warning(
188            None,
189            "Description is very short".to_string(),
190            Some("Consider adding more detail about when to use this skill".to_string()),
191        );
192
193        report.finalize();
194
195        assert!(!report.is_valid);
196        assert_eq!(report.stats.error_count, 1);
197        assert_eq!(report.stats.warning_count, 1);
198        assert!(report.generate_summary().contains("Invalid characters"));
199    }
200}