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