1use anyhow::{Result, anyhow};
10use chrono::{DateTime, Utc};
11use hashbrown::HashMap;
12use serde::{Deserialize, Serialize};
13use std::fmt::Write;
14use tracing::debug;
15use vtcode_commons::MultiErrors;
16
17#[cfg(test)]
18use crate::config::constants::tools;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ToolVersion {
23 pub name: String,
25 pub major: u32,
27 pub minor: u32,
29 pub patch: u32,
31 pub released: DateTime<Utc>,
33 pub description: String,
35 pub input_schema: serde_json::Value,
37 pub output_schema: serde_json::Value,
39 pub breaking_changes: Vec<BreakingChange>,
41 pub deprecations: Vec<Deprecation>,
43 pub migration_guide: Option<String>,
45}
46
47impl ToolVersion {
48 pub fn version_string(&self) -> String {
50 format!("{}.{}.{}", self.major, self.minor, self.patch)
51 }
52
53 pub fn from_string(s: &str) -> Result<(u32, u32, u32)> {
55 let parts: Vec<&str> = s.split('.').collect();
56 if parts.len() != 3 {
57 return Err(anyhow!("Invalid version format: {s}"));
58 }
59 Ok((parts[0].parse()?, parts[1].parse()?, parts[2].parse()?))
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct BreakingChange {
66 pub field: String,
68 pub old_type: String,
70 pub new_type: String,
72 pub reason: String,
74 pub migration_code: String,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Deprecation {
81 pub field: String,
83 pub replacement: Option<String>,
85 pub removed_in: String,
87 pub guidance: String,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ToolDependency {
94 pub name: String,
96 pub version: String,
98 pub usage: Vec<String>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct CompatibilityReport {
105 pub compatible: bool,
107 pub warnings: Vec<String>,
109 pub errors: MultiErrors<String>,
111 pub migrations: Vec<Migration>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Migration {
118 pub skill_name: String,
120 pub tool: String,
122 pub from_version: String,
124 pub to_version: String,
126 pub transformations: Vec<CodeTransformation>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct CodeTransformation {
133 pub line_number: usize,
135 pub old_code: String,
137 pub new_code: String,
139 pub reason: String,
141}
142
143pub enum VersionCompatibility {
145 Compatible,
147 Warning(String),
149 RequiresMigration,
151 Incompatible(String),
153}
154
155pub struct SkillCompatibilityChecker {
157 skill_name: String,
159 tool_dependencies: Vec<ToolDependency>,
160 tool_versions: HashMap<String, ToolVersion>,
162}
163
164impl SkillCompatibilityChecker {
165 pub fn new(
167 skill_name: String,
168 tool_dependencies: Vec<ToolDependency>,
169 tool_versions: HashMap<String, ToolVersion>,
170 ) -> Self {
171 Self { skill_name, tool_dependencies, tool_versions }
172 }
173
174 pub fn check_compatibility(&self) -> Result<CompatibilityReport> {
176 let mut report = CompatibilityReport {
177 compatible: true,
178 warnings: vec![],
179 errors: MultiErrors::new(),
180 migrations: vec![],
181 };
182
183 for dep in &self.tool_dependencies {
184 let current_tool = match self.tool_versions.get(&dep.name) {
185 Some(v) => v,
186 None => {
187 report.compatible = false;
188 report.errors.push(format!("Tool not found: {}", dep.name));
189 continue;
190 }
191 };
192
193 match self.check_version_compatibility(&dep.version, ¤t_tool.version_string())? {
194 VersionCompatibility::Compatible => {
195 debug!("Tool {} version {} is compatible", dep.name, dep.version);
196 }
197 VersionCompatibility::Warning(msg) => {
198 report.warnings.push(msg.clone());
199 debug!("Compatibility warning for {}: {}", dep.name, msg);
200 }
201 VersionCompatibility::RequiresMigration => {
202 report.compatible = false;
203 report.migrations.push(Migration {
205 skill_name: self.skill_name.clone(),
206 tool: dep.name.clone(),
207 from_version: dep.version.clone(),
208 to_version: current_tool.version_string(),
209 transformations: vec![],
210 });
211 debug!("Migration required for {} in {}", dep.name, self.skill_name);
212 }
213 VersionCompatibility::Incompatible(msg) => {
214 report.compatible = false;
215 report.errors.push(msg.clone());
216 debug!("Incompatibility error for {}: {}", dep.name, msg);
217 }
218 }
219 }
220
221 Ok(report)
222 }
223
224 fn check_version_compatibility(&self, required: &str, available: &str) -> Result<VersionCompatibility> {
226 let req_parts: Vec<&str> = required.split('.').collect();
230 if req_parts.is_empty() || req_parts.len() > 2 {
231 return Err(anyhow!("Invalid required version format: {required}"));
232 }
233
234 let req_major: u32 = req_parts[0].parse()?;
235 let req_minor: u32 = if req_parts.len() == 2 { req_parts[1].parse()? } else { 0 };
236
237 let (avail_major, avail_minor, _avail_patch) = ToolVersion::from_string(available)?;
238
239 let compat = match (req_major == avail_major, req_minor == avail_minor) {
240 (true, true) => {
241 VersionCompatibility::Compatible
243 }
244 (true, false) if avail_minor > req_minor => {
245 VersionCompatibility::Warning(format!(
247 "Tool available version {available} is newer than required {required}"
248 ))
249 }
250 (true, false) if avail_minor < req_minor => {
251 VersionCompatibility::RequiresMigration
253 }
254 (false, _) if avail_major > req_major => {
255 VersionCompatibility::Incompatible(format!(
257 "Tool major version changed from {req_major} to {avail_major}"
258 ))
259 }
260 _ => {
261 VersionCompatibility::Incompatible(format!(
263 "Tool version {available} not compatible with required {required}"
264 ))
265 }
266 };
267
268 Ok(compat)
269 }
270
271 pub fn detailed_report(&self) -> Result<String> {
273 let report = self.check_compatibility()?;
274
275 let mut output = format!("Skill: {}\n", self.skill_name);
276 let _ = writeln!(output, "Compatible: {}", report.compatible);
277
278 if !report.warnings.is_empty() {
279 output.push_str("\nWarnings:\n");
280 for warning in &report.warnings {
281 let _ = writeln!(output, " - {warning}");
282 }
283 }
284
285 if !report.errors.is_empty() {
286 output.push_str("\nErrors:\n");
287 for error in report.errors.iter() {
288 let _ = writeln!(output, " - {error}");
289 }
290 }
291
292 if !report.migrations.is_empty() {
293 output.push_str("\nRequired Migrations:\n");
294 for migration in &report.migrations {
295 let _ =
296 writeln!(output, " - {}: {} -> {}", migration.tool, migration.from_version, migration.to_version);
297 }
298 }
299
300 Ok(output)
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn create_test_tool(name: &str, version: &str) -> ToolVersion {
309 let (major, minor, patch) = ToolVersion::from_string(version).unwrap();
310 ToolVersion {
311 name: name.to_owned(),
312 major,
313 minor,
314 patch,
315 released: Utc::now(),
316 description: format!("Test tool {version}"),
317 input_schema: serde_json::json!({}),
318 output_schema: serde_json::json!({}),
319 breaking_changes: vec![],
320 deprecations: vec![],
321 migration_guide: None,
322 }
323 }
324
325 #[test]
326 fn test_version_parsing() {
327 let (major, minor, patch) = ToolVersion::from_string("1.2.3").unwrap();
328 assert_eq!(major, 1);
329 assert_eq!(minor, 2);
330 assert_eq!(patch, 3);
331
332 ToolVersion::from_string("1.2").unwrap_err();
334 ToolVersion::from_string("invalid").unwrap_err();
335 }
336
337 #[test]
338 fn test_exact_version_compatibility() {
339 let mut tools = HashMap::new();
340 tools.insert("read_file".to_owned(), create_test_tool("read_file", "1.2.3"));
341
342 let deps = vec![ToolDependency {
343 name: "read_file".to_owned(),
344 version: "1.2".to_owned(),
345 usage: vec!["test".to_owned()],
346 }];
347
348 let checker = SkillCompatibilityChecker::new("test_skill".to_owned(), deps, tools);
349 let report = checker.check_compatibility().unwrap();
350
351 assert!(report.compatible);
352 assert!(report.errors.is_empty());
353 }
354
355 #[test]
356 fn test_missing_tool() {
357 let tools = HashMap::new(); let deps = vec![ToolDependency {
360 name: "nonexistent_tool".to_owned(),
361 version: "1.0".to_owned(),
362 usage: vec![],
363 }];
364
365 let checker = SkillCompatibilityChecker::new("test_skill".to_owned(), deps, tools);
366 let report = checker.check_compatibility().unwrap();
367
368 assert!(!report.compatible);
369 assert!(!report.errors.is_empty());
370 }
371
372 #[test]
373 fn test_minor_version_upgrade_warning() {
374 let mut tools = HashMap::new();
375 tools.insert(tools::LIST_FILES.to_owned(), create_test_tool(tools::LIST_FILES, "1.3.0"));
377
378 let deps = vec![ToolDependency {
379 name: tools::LIST_FILES.to_owned(),
380 version: "1.2".to_owned(),
381 usage: vec![],
382 }];
383
384 let checker = SkillCompatibilityChecker::new("test_skill".to_owned(), deps, tools);
385 let report = checker.check_compatibility().unwrap();
386
387 assert!(report.compatible);
388 assert!(!report.warnings.is_empty());
389 }
390
391 #[test]
392 fn test_major_version_incompatibility() {
393 let mut tools = HashMap::new();
394 tools.insert(tools::GREP_FILE.to_owned(), create_test_tool(tools::GREP_FILE, "2.0.0"));
396
397 let deps = vec![ToolDependency {
398 name: tools::GREP_FILE.to_owned(),
399 version: "1.2".to_owned(),
400 usage: vec![],
401 }];
402
403 let checker = SkillCompatibilityChecker::new("test_skill".to_owned(), deps, tools);
404 let report = checker.check_compatibility().unwrap();
405
406 assert!(!report.compatible);
407 assert!(!report.errors.is_empty());
408 }
409
410 #[test]
411 fn test_detailed_report() {
412 let mut tools = HashMap::new();
413 tools.insert("read_file".to_owned(), create_test_tool("read_file", "1.2.3"));
414
415 let deps = vec![ToolDependency {
416 name: "read_file".to_owned(),
417 version: "1.2".to_owned(),
418 usage: vec!["main".to_owned()],
419 }];
420
421 let checker = SkillCompatibilityChecker::new("filter_skill".to_owned(), deps, tools);
422 let report = checker.detailed_report().unwrap();
423
424 assert!(report.contains("filter_skill"));
425 assert!(report.contains("Compatible: true"));
426 }
427
428 #[test]
429 fn test_skill_compatible_with_newer_patch_version() {
430 let mut tools = HashMap::new();
433 tools.insert(tools::LIST_FILES.to_owned(), create_test_tool(tools::LIST_FILES, "1.2.5"));
434
435 let deps = vec![ToolDependency {
436 name: tools::LIST_FILES.to_owned(),
437 version: "1.2".to_owned(),
438 usage: vec!["main".to_owned()],
439 }];
440
441 let checker = SkillCompatibilityChecker::new("filter_skill".to_owned(), deps, tools);
442 let report = checker.check_compatibility().unwrap();
443
444 assert!(report.compatible, "Should be compatible with patch version upgrade");
445 assert!(report.errors.is_empty());
446 }
447
448 #[test]
449 fn test_multiple_tool_dependencies() {
450 let mut tools = HashMap::new();
452 tools.insert("read_file".to_owned(), create_test_tool("read_file", "1.2.0"));
453 tools.insert("write_file".to_owned(), create_test_tool("write_file", "2.0.0"));
454 tools.insert(tools::LIST_FILES.to_owned(), create_test_tool(tools::LIST_FILES, "1.3.0"));
455
456 let deps = vec![
457 ToolDependency {
458 name: "read_file".to_owned(),
459 version: "1.2".to_owned(),
460 usage: vec!["read_input".to_owned()],
461 },
462 ToolDependency {
463 name: "write_file".to_owned(),
464 version: "1.0".to_owned(),
465 usage: vec!["write_output".to_owned()],
466 },
467 ToolDependency {
468 name: tools::LIST_FILES.to_owned(),
469 version: "1.2".to_owned(),
470 usage: vec!["scan_directory".to_owned()],
471 },
472 ];
473
474 let checker = SkillCompatibilityChecker::new("complex_skill".to_owned(), deps, tools);
475 let report = checker.check_compatibility().unwrap();
476
477 assert!(!report.compatible, "Should not be fully compatible due to write_file");
479 assert!(!report.errors.is_empty());
480 }
481}