Skip to main content

fastskill_core/core/
reconciliation.rs

1//! Reconciliation types for comparing installed skills with project/lock files
2//!
3//! These types are used by both the CLI and tests to reconcile installed skills
4//! against skills-project.toml and skills-lock.toml
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9/// Installed skill information
10#[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/// Reconciliation status for installed skills
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum ReconciliationStatus {
26    Ok,
27    Missing,    // In project.toml but not installed
28    Extraneous, // Installed but not in project.toml
29    Mismatch,   // Version mismatch with lock.toml
30}
31
32/// Reconciliation report
33#[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/// Desired entry from skills-project.toml
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct DesiredEntry {
44    pub id: String,
45    pub version: Option<String>, // Version constraint
46}
47
48/// Version mismatch information
49#[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
60/// Build reconciliation report
61pub 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    // Build map of installed skills by ID
73    let installed_map: HashMap<String, &SkillDefinition> = installed_skills
74        .iter()
75        .map(|s| (s.id.to_string(), s))
76        .collect();
77
78    // Check for missing dependencies (in project but not installed)
79    for id in project_deps.keys() {
80        if !installed_map.contains_key(id) {
81            missing.push(DesiredEntry {
82                id: id.clone(),
83                version: None, // Version constraint not used for missing detection
84            });
85        }
86    }
87
88    // Process installed skills
89    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        // Determine status
95        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        // Collect extraneous and mismatches
108        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        // Add to installed list
134        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}