fastskill_core/core/
reconciliation.rs1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct InstalledSkillInfo {
12 pub id: String,
13 pub name: String,
14 pub version: String,
15 pub description: String,
16 pub source: Option<String>,
17 pub installed_path: PathBuf,
18 pub installed_at: Option<chrono::DateTime<chrono::Utc>>,
19 pub status: ReconciliationStatus,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum ReconciliationStatus {
26 Ok,
27 Missing, Extraneous, Mismatch, }
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ReconciliationReport {
35 pub installed: Vec<InstalledSkillInfo>,
36 pub missing: Vec<DesiredEntry>,
37 pub extraneous: Vec<InstalledSkillInfo>,
38 pub version_mismatches: Vec<VersionMismatch>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct DesiredEntry {
44 pub id: String,
45 pub version: Option<String>, }
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct VersionMismatch {
51 pub id: String,
52 pub installed_version: String,
53 pub locked_version: String,
54}
55
56use crate::core::skill_manager::SkillDefinition;
57use std::collections::HashMap;
58use std::path::Path;
59
60pub fn build_reconciliation_report(
62 installed_skills: &[SkillDefinition],
63 project_deps: &HashMap<String, Option<String>>,
64 lock_deps: &HashMap<String, String>,
65 _skills_dir: &Path,
66) -> Result<ReconciliationReport, crate::core::service::ServiceError> {
67 let mut installed_info = Vec::new();
68 let mut missing = Vec::new();
69 let mut extraneous = Vec::new();
70 let mut version_mismatches = Vec::new();
71
72 let installed_map: HashMap<String, &SkillDefinition> = installed_skills
74 .iter()
75 .map(|s| (s.id.to_string(), s))
76 .collect();
77
78 for id in project_deps.keys() {
80 if !installed_map.contains_key(id) {
81 missing.push(DesiredEntry {
82 id: id.clone(),
83 version: None, });
85 }
86 }
87
88 for skill in installed_skills {
90 let skill_id = skill.id.to_string();
91 let is_in_project = project_deps.contains_key(&skill_id);
92 let locked_version = lock_deps.get(&skill_id);
93
94 let status = if !is_in_project {
96 ReconciliationStatus::Extraneous
97 } else if let Some(locked_ver) = locked_version {
98 if skill.version != *locked_ver {
99 ReconciliationStatus::Mismatch
100 } else {
101 ReconciliationStatus::Ok
102 }
103 } else {
104 ReconciliationStatus::Ok
105 };
106
107 match &status {
109 ReconciliationStatus::Extraneous => {
110 extraneous.push(InstalledSkillInfo {
111 id: skill_id.clone(),
112 name: skill.name.clone(),
113 version: skill.version.clone(),
114 description: skill.description.clone(),
115 source: skill.source_url.clone(),
116 installed_path: skill.skill_file.clone(),
117 installed_at: Some(skill.updated_at),
118 status: status.clone(),
119 });
120 }
121 ReconciliationStatus::Mismatch => {
122 if let Some(locked_ver) = locked_version {
123 version_mismatches.push(VersionMismatch {
124 id: skill_id.clone(),
125 installed_version: skill.version.clone(),
126 locked_version: locked_ver.clone(),
127 });
128 }
129 }
130 _ => {}
131 }
132
133 installed_info.push(InstalledSkillInfo {
135 id: skill_id,
136 name: skill.name.clone(),
137 version: skill.version.clone(),
138 description: skill.description.clone(),
139 source: skill.source_url.clone(),
140 installed_path: skill.skill_file.clone(),
141 installed_at: Some(skill.updated_at),
142 status,
143 });
144 }
145
146 Ok(ReconciliationReport {
147 installed: installed_info,
148 missing,
149 extraneous,
150 version_mismatches,
151 })
152}