swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! ProfileStore - ScenarioProfile の永続化
//!
//! ## Storage 構造
//!
//! ```text
//! ~/.swarm-engine/profiles/
//! ├── registry.json              # ScenarioRegistry
//! └── {profile_id}/
//!     ├── profile.json           # ScenarioProfile metadata
//!     ├── dep_graph.json         # LearnedDepGraph
//!     ├── exploration.json       # LearnedExploration
//!     ├── strategy.json          # LearnedStrategy
//!     └── sessions/              # 学習セッションログ
//! ```

use std::fs;
use std::path::{Path, PathBuf};

use super::learned_component::{
    LearnedComponent, LearnedDepGraph, LearnedExploration, LearnedStrategy,
};
use super::scenario_profile::{ScenarioProfile, ScenarioProfileId};

/// ProfileStore のエラー型
#[derive(Debug, thiserror::Error)]
pub enum ProfileStoreError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    #[error("Profile not found: {0}")]
    NotFound(String),

    #[error("Profile already exists: {0}")]
    AlreadyExists(String),
}

/// ScenarioProfile の永続化ストア
pub struct ProfileStore {
    /// ベースディレクトリ
    base_dir: PathBuf,
}

impl ProfileStore {
    /// 新規作成
    pub fn new(base_dir: impl AsRef<Path>) -> Self {
        Self {
            base_dir: base_dir.as_ref().to_path_buf(),
        }
    }

    /// デフォルトパスで作成
    pub fn default_path() -> Option<Self> {
        dirs::home_dir().map(|home| Self::new(home.join(".swarm-engine/profiles")))
    }

    /// ベースディレクトリを取得
    pub fn base_dir(&self) -> &Path {
        &self.base_dir
    }

    /// Profile ディレクトリを取得
    fn profile_dir(&self, id: &ScenarioProfileId) -> PathBuf {
        self.base_dir.join(&id.0)
    }

    /// Profile を保存
    pub fn save(&self, profile: &ScenarioProfile) -> Result<(), ProfileStoreError> {
        let dir = self.profile_dir(&profile.id);
        fs::create_dir_all(&dir)?;

        // Profile metadata
        let profile_path = dir.join("profile.json");
        let json = serde_json::to_string_pretty(profile)?;
        fs::write(profile_path, json)?;

        // Components (個別ファイル)
        if let Some(dep_graph) = &profile.dep_graph {
            self.save_component(&profile.id, dep_graph)?;
        }
        if let Some(exploration) = &profile.exploration {
            self.save_component(&profile.id, exploration)?;
        }
        if let Some(strategy) = &profile.strategy {
            self.save_component(&profile.id, strategy)?;
        }

        Ok(())
    }

    /// Profile を読み込み
    pub fn load(&self, id: &ScenarioProfileId) -> Result<ScenarioProfile, ProfileStoreError> {
        let dir = self.profile_dir(id);
        let profile_path = dir.join("profile.json");

        if !profile_path.exists() {
            return Err(ProfileStoreError::NotFound(id.0.clone()));
        }

        let json = fs::read_to_string(profile_path)?;
        let mut profile: ScenarioProfile = serde_json::from_str(&json)?;

        // Components を読み込み
        if let Ok(dep_graph) = self.load_component::<LearnedDepGraph>(id) {
            profile.dep_graph = Some(dep_graph);
        }
        if let Ok(exploration) = self.load_component::<LearnedExploration>(id) {
            profile.exploration = Some(exploration);
        }
        if let Ok(strategy) = self.load_component::<LearnedStrategy>(id) {
            profile.strategy = Some(strategy);
        }

        Ok(profile)
    }

    /// Profile を削除
    pub fn delete(&self, id: &ScenarioProfileId) -> Result<(), ProfileStoreError> {
        let dir = self.profile_dir(id);
        if dir.exists() {
            fs::remove_dir_all(dir)?;
        }
        Ok(())
    }

    /// Profile が存在するか
    pub fn exists(&self, id: &ScenarioProfileId) -> bool {
        self.profile_dir(id).join("profile.json").exists()
    }

    /// 全 Profile ID を列挙
    pub fn list_ids(&self) -> Result<Vec<ScenarioProfileId>, ProfileStoreError> {
        if !self.base_dir.exists() {
            return Ok(Vec::new());
        }

        let mut ids = Vec::new();
        for entry in fs::read_dir(&self.base_dir)? {
            let entry = entry?;
            if entry.file_type()?.is_dir() {
                let profile_json = entry.path().join("profile.json");
                if profile_json.exists() {
                    if let Some(name) = entry.file_name().to_str() {
                        ids.push(ScenarioProfileId::new(name));
                    }
                }
            }
        }

        Ok(ids)
    }

    /// 全 Profile を読み込み
    pub fn load_all(&self) -> Result<Vec<ScenarioProfile>, ProfileStoreError> {
        let ids = self.list_ids()?;
        let mut profiles = Vec::new();

        for id in ids {
            match self.load(&id) {
                Ok(profile) => profiles.push(profile),
                Err(e) => {
                    tracing::warn!("Failed to load profile {}: {}", id, e);
                }
            }
        }

        Ok(profiles)
    }

    // ========================================
    // Component 操作
    // ========================================

    /// Component を保存
    pub fn save_component<C: LearnedComponent>(
        &self,
        id: &ScenarioProfileId,
        component: &C,
    ) -> Result<(), ProfileStoreError> {
        let dir = self.profile_dir(id);
        fs::create_dir_all(&dir)?;

        let path = dir.join(format!("{}.json", C::component_id()));
        let json = serde_json::to_string_pretty(component)?;
        fs::write(path, json)?;

        Ok(())
    }

    /// Component を読み込み
    pub fn load_component<C: LearnedComponent>(
        &self,
        id: &ScenarioProfileId,
    ) -> Result<C, ProfileStoreError> {
        let path = self
            .profile_dir(id)
            .join(format!("{}.json", C::component_id()));

        if !path.exists() {
            return Err(ProfileStoreError::NotFound(format!(
                "{}/{}",
                id.0,
                C::component_id()
            )));
        }

        let json = fs::read_to_string(path)?;
        let component: C = serde_json::from_str(&json)?;
        Ok(component)
    }

    /// Component を削除
    pub fn delete_component<C: LearnedComponent>(
        &self,
        id: &ScenarioProfileId,
    ) -> Result<(), ProfileStoreError> {
        let path = self
            .profile_dir(id)
            .join(format!("{}.json", C::component_id()));
        if path.exists() {
            fs::remove_file(path)?;
        }
        Ok(())
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::exploration::DependencyGraph;
    use tempfile::TempDir;

    fn create_test_store() -> (ProfileStore, TempDir) {
        let temp_dir = TempDir::new().unwrap();
        let store = ProfileStore::new(temp_dir.path());
        (store, temp_dir)
    }

    #[test]
    fn test_save_and_load_profile() {
        let (store, _temp) = create_test_store();

        let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
        store.save(&profile).unwrap();

        let loaded = store.load(&profile.id).unwrap();
        assert_eq!(loaded.id.0, "test");
    }

    #[test]
    fn test_save_and_load_with_components() {
        let (store, _temp) = create_test_store();

        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");

        // Add components
        let dep_graph = LearnedDepGraph::new(DependencyGraph::new(), vec!["A".to_string()])
            .with_confidence(0.8);
        profile.dep_graph = Some(dep_graph);

        let exploration = LearnedExploration::new(2.0, 0.5, 1.0);
        profile.exploration = Some(exploration);

        store.save(&profile).unwrap();

        let loaded = store.load(&profile.id).unwrap();
        assert!(loaded.dep_graph.is_some());
        assert!(loaded.exploration.is_some());
        assert_eq!(loaded.dep_graph.unwrap().confidence, 0.8);
    }

    #[test]
    fn test_list_ids() {
        let (store, _temp) = create_test_store();

        let profile1 = ScenarioProfile::from_file("profile1", "/path/to/1.toml");
        let profile2 = ScenarioProfile::from_file("profile2", "/path/to/2.toml");

        store.save(&profile1).unwrap();
        store.save(&profile2).unwrap();

        let ids = store.list_ids().unwrap();
        assert_eq!(ids.len(), 2);
    }

    #[test]
    fn test_exists() {
        let (store, _temp) = create_test_store();

        let id = ScenarioProfileId::new("test");
        assert!(!store.exists(&id));

        let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
        store.save(&profile).unwrap();

        assert!(store.exists(&id));
    }

    #[test]
    fn test_delete() {
        let (store, _temp) = create_test_store();

        let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
        store.save(&profile).unwrap();
        assert!(store.exists(&profile.id));

        store.delete(&profile.id).unwrap();
        assert!(!store.exists(&profile.id));
    }
}