swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! ScenarioRegistry - マルチシナリオ管理
//!
//! ## 概要
//!
//! 複数の ScenarioProfile を統合管理し、Task → Scenario のルーティングを行う。
//!
//! ## 機能
//!
//! - Profile の登録・取得・削除
//! - Task パターンに基づく Scenario 選択
//! - Ensemble 投票による Scenario 選択(将来)
//!
//! ## 使用例
//!
//! ```ignore
//! let mut registry = ScenarioRegistry::new(store);
//!
//! // Profile 登録
//! registry.register(profile)?;
//!
//! // Task → Scenario ルーティング
//! let profile = registry.select_for_task("ログ調査して原因特定")?;
//! ```

use std::collections::HashMap;

use super::profile_store::{ProfileStore, ProfileStoreError};
use super::scenario_profile::{ProfileState, ScenarioProfile, ScenarioProfileId};

/// ScenarioRegistry のエラー型
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
    #[error("Store error: {0}")]
    Store(#[from] ProfileStoreError),

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

    #[error("No usable profile found")]
    NoUsableProfile,

    #[error("Profile not active: {0}")]
    NotActive(String),
}

/// Task → Scenario マッチング設定
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TaskMatcher {
    /// パターン(キーワードベース)
    pub keywords: Vec<String>,
    /// 対象 Profile ID
    pub profile_id: ScenarioProfileId,
    /// 優先度(高いほど優先)
    pub priority: i32,
}

impl TaskMatcher {
    /// 新規作成
    pub fn new(profile_id: ScenarioProfileId, keywords: Vec<String>) -> Self {
        Self {
            keywords,
            profile_id,
            priority: 0,
        }
    }

    /// 優先度を設定
    pub fn with_priority(mut self, priority: i32) -> Self {
        self.priority = priority;
        self
    }

    /// Task がマッチするか判定
    pub fn matches(&self, task: &str) -> bool {
        let task_lower = task.to_lowercase();
        self.keywords
            .iter()
            .any(|kw| task_lower.contains(&kw.to_lowercase()))
    }
}

/// マルチシナリオ管理
pub struct ScenarioRegistry {
    /// Profile ストア
    store: ProfileStore,
    /// キャッシュされた Profile
    profiles: HashMap<ScenarioProfileId, ScenarioProfile>,
    /// Task マッチャー
    matchers: Vec<TaskMatcher>,
}

impl ScenarioRegistry {
    /// 新規作成
    pub fn new(store: ProfileStore) -> Self {
        Self {
            store,
            profiles: HashMap::new(),
            matchers: Vec::new(),
        }
    }

    /// ストアから全 Profile を読み込み
    pub fn load_all(&mut self) -> Result<(), RegistryError> {
        let profiles = self.store.load_all()?;
        for profile in profiles {
            self.profiles.insert(profile.id.clone(), profile);
        }
        Ok(())
    }

    // ========================================
    // Profile 操作
    // ========================================

    /// Profile を登録
    pub fn register(&mut self, profile: ScenarioProfile) -> Result<(), RegistryError> {
        self.store.save(&profile)?;
        self.profiles.insert(profile.id.clone(), profile);
        Ok(())
    }

    /// Profile を取得
    pub fn get(&self, id: &ScenarioProfileId) -> Option<&ScenarioProfile> {
        self.profiles.get(id)
    }

    /// Profile を取得(可変)
    pub fn get_mut(&mut self, id: &ScenarioProfileId) -> Option<&mut ScenarioProfile> {
        self.profiles.get_mut(id)
    }

    /// Profile を削除
    pub fn remove(&mut self, id: &ScenarioProfileId) -> Result<(), RegistryError> {
        self.store.delete(id)?;
        self.profiles.remove(id);
        // 関連する matcher も削除
        self.matchers.retain(|m| &m.profile_id != id);
        Ok(())
    }

    /// 全 Profile を取得
    pub fn all_profiles(&self) -> impl Iterator<Item = &ScenarioProfile> {
        self.profiles.values()
    }

    /// 使用可能な Profile を取得
    pub fn usable_profiles(&self) -> impl Iterator<Item = &ScenarioProfile> {
        self.profiles.values().filter(|p| p.is_usable())
    }

    /// Profile を保存(更新後)
    pub fn save(&self, id: &ScenarioProfileId) -> Result<(), RegistryError> {
        if let Some(profile) = self.profiles.get(id) {
            self.store.save(profile)?;
        }
        Ok(())
    }

    // ========================================
    // Task Matching
    // ========================================

    /// TaskMatcher を追加
    pub fn add_matcher(&mut self, matcher: TaskMatcher) {
        self.matchers.push(matcher);
        // 優先度でソート
        self.matchers.sort_by(|a, b| b.priority.cmp(&a.priority));
    }

    /// Task に対応する Profile を選択
    pub fn select_for_task(&self, task: &str) -> Result<&ScenarioProfile, RegistryError> {
        // マッチするものを探す
        for matcher in &self.matchers {
            if matcher.matches(task) {
                if let Some(profile) = self.profiles.get(&matcher.profile_id) {
                    if profile.is_usable() {
                        return Ok(profile);
                    }
                }
            }
        }

        // マッチしない場合、使用可能な最初の Profile を返す
        self.usable_profiles()
            .next()
            .ok_or(RegistryError::NoUsableProfile)
    }

    /// Task に対応する全候補 Profile を取得
    pub fn candidates_for_task(&self, task: &str) -> Vec<&ScenarioProfile> {
        let mut candidates: Vec<_> = self
            .matchers
            .iter()
            .filter(|m| m.matches(task))
            .filter_map(|m| self.profiles.get(&m.profile_id))
            .filter(|p| p.is_usable())
            .collect();

        // 重複除去
        candidates.dedup_by_key(|p| &p.id);
        candidates
    }

    // ========================================
    // Stats
    // ========================================

    /// 登録されている Profile 数
    pub fn profile_count(&self) -> usize {
        self.profiles.len()
    }

    /// 使用可能な Profile 数
    pub fn usable_count(&self) -> usize {
        self.profiles.values().filter(|p| p.is_usable()).count()
    }

    /// Matcher 数
    pub fn matcher_count(&self) -> usize {
        self.matchers.len()
    }

    /// 状態別 Profile 数
    pub fn count_by_state(&self) -> HashMap<ProfileState, usize> {
        let mut counts = HashMap::new();
        for profile in self.profiles.values() {
            *counts.entry(profile.state).or_insert(0) += 1;
        }
        counts
    }
}

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

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

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

    #[test]
    fn test_register_and_get() {
        let (mut registry, _temp) = create_test_registry();

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

        let loaded = registry.get(&ScenarioProfileId::new("test"));
        assert!(loaded.is_some());
    }

    #[test]
    fn test_task_matcher() {
        let matcher = TaskMatcher::new(
            ScenarioProfileId::new("troubleshooting"),
            vec!["ログ".to_string(), "調査".to_string(), "エラー".to_string()],
        );

        assert!(matcher.matches("ログを調査してください"));
        assert!(matcher.matches("エラーの原因を特定"));
        assert!(!matcher.matches("新機能を追加"));
    }

    #[test]
    fn test_select_for_task() {
        let (mut registry, _temp) = create_test_registry();

        // Active な Profile を作成
        let mut profile1 = ScenarioProfile::from_file("troubleshooting", "/path/to/1.toml");
        profile1.state = ProfileState::Active;

        let mut profile2 = ScenarioProfile::from_file("deep_search", "/path/to/2.toml");
        profile2.state = ProfileState::Active;

        registry.register(profile1).unwrap();
        registry.register(profile2).unwrap();

        // Matcher を追加
        registry.add_matcher(TaskMatcher::new(
            ScenarioProfileId::new("troubleshooting"),
            vec!["ログ".to_string(), "エラー".to_string()],
        ));
        registry.add_matcher(TaskMatcher::new(
            ScenarioProfileId::new("deep_search"),
            vec!["検索".to_string(), "探索".to_string()],
        ));

        // マッチング
        let selected = registry.select_for_task("ログを調査").unwrap();
        assert_eq!(selected.id.0, "troubleshooting");

        let selected = registry.select_for_task("ファイルを検索").unwrap();
        assert_eq!(selected.id.0, "deep_search");
    }

    #[test]
    fn test_usable_profiles() {
        let (mut registry, _temp) = create_test_registry();

        let mut active = ScenarioProfile::from_file("active", "/path/to/1.toml");
        active.state = ProfileState::Active;

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

        registry.register(active).unwrap();
        registry.register(draft).unwrap();

        assert_eq!(registry.profile_count(), 2);
        assert_eq!(registry.usable_count(), 1);
    }

    #[test]
    fn test_count_by_state() {
        let (mut registry, _temp) = create_test_registry();

        let mut active1 = ScenarioProfile::from_file("active1", "/path/to/1.toml");
        active1.state = ProfileState::Active;

        let mut active2 = ScenarioProfile::from_file("active2", "/path/to/2.toml");
        active2.state = ProfileState::Active;

        let draft = ScenarioProfile::from_file("draft", "/path/to/3.toml");

        registry.register(active1).unwrap();
        registry.register(active2).unwrap();
        registry.register(draft).unwrap();

        let counts = registry.count_by_state();
        assert_eq!(counts.get(&ProfileState::Active), Some(&2));
        assert_eq!(counts.get(&ProfileState::Draft), Some(&1));
    }
}