Skip to main content

swarm_engine_core/learn/
scenario_profile.rs

1//! ScenarioProfile - 永続的に改善されるシナリオの実体
2//!
3//! ## 設計思想
4//!
5//! Scenario(静的定義)に対して、学習結果を蓄積・管理する Entity。
6//! Bootstrap で活性化し、Runtime で継続的に最適化される。
7//!
8//! ## Lifecycle
9//!
10//! ```text
11//! 1. Register
12//!    swarm profile add scenarios/deep_search.toml
13//!    → state: Draft
14//!
15//! 2. Bootstrap
16//!    swarm profile bootstrap deep_search
17//!    → with_graph で N 回実行
18//!    → DepGraph 学習
19//!    → state: Active
20//!
21//! 3. Use
22//!    swarm run --profile deep_search "タスク"
23//!    → Profile から DepGraph/Params 適用
24//!    → stats 更新
25//!
26//! 4. Optimize (自動)
27//!    → パフォーマンス監視
28//!    → 閾値割れで再チューニング
29//!    → state: Optimizing → Active
30//! ```
31//!
32//! ## Storage
33//!
34//! ```text
35//! ~/.swarm-engine/profiles/troubleshooting/
36//! ├── profile.json           # ScenarioProfile (metadata)
37//! ├── dep_graph.json         # LearnedDepGraph
38//! ├── exploration.json       # LearnedExploration
39//! ├── strategy.json          # LearnedStrategy
40//! └── sessions/              # 学習セッションログ
41//! ```
42
43use std::path::{Path, PathBuf};
44use std::time::{SystemTime, UNIX_EPOCH};
45
46use serde::{Deserialize, Serialize};
47
48use super::learned_component::{
49    LearnedComponent, LearnedDepGraph, LearnedExploration, LearnedStrategy,
50};
51use super::session_group::LearningPhase;
52use crate::validation::ValidationResult;
53
54// ============================================================================
55// ScenarioProfileId
56// ============================================================================
57
58/// ScenarioProfile 識別子
59#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub struct ScenarioProfileId(pub String);
61
62impl ScenarioProfileId {
63    /// 新規作成
64    pub fn new(id: impl Into<String>) -> Self {
65        Self(id.into())
66    }
67}
68
69impl std::fmt::Display for ScenarioProfileId {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        write!(f, "{}", self.0)
72    }
73}
74
75// ============================================================================
76// ProfileState
77// ============================================================================
78
79/// Profile のライフサイクル状態
80///
81/// ```text
82/// Draft → Bootstrapping → Validating → Active → Optimizing
83///                              ↓
84///                           Failed → (retry) → Draft
85/// ```
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum ProfileState {
89    /// 定義のみ、未 Bootstrap
90    #[default]
91    Draft,
92    /// Bootstrap 進行中
93    Bootstrapping,
94    /// 検証中(Bootstrap 完了後)
95    Validating,
96    /// 使用可能
97    Active,
98    /// Active + 継続チューニング中
99    Optimizing,
100    /// 検証失敗
101    Failed,
102}
103
104impl std::fmt::Display for ProfileState {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        match self {
107            Self::Draft => write!(f, "draft"),
108            Self::Bootstrapping => write!(f, "bootstrapping"),
109            Self::Validating => write!(f, "validating"),
110            Self::Active => write!(f, "active"),
111            Self::Optimizing => write!(f, "optimizing"),
112            Self::Failed => write!(f, "failed"),
113        }
114    }
115}
116
117// ============================================================================
118// ScenarioSource
119// ============================================================================
120
121/// シナリオのソース参照
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(tag = "type", rename_all = "snake_case")]
124pub enum ScenarioSource {
125    /// ファイルパス参照
126    File { path: PathBuf },
127    /// インライン定義(将来用)
128    Inline { content: String },
129}
130
131impl ScenarioSource {
132    /// ファイルパスから作成
133    pub fn from_path(path: impl AsRef<Path>) -> Self {
134        Self::File {
135            path: path.as_ref().to_path_buf(),
136        }
137    }
138}
139
140// ============================================================================
141// BootstrapData
142// ============================================================================
143
144/// Bootstrap 完了時のデータ
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct BootstrapData {
147    /// 完了日時
148    pub completed_at: u64,
149    /// 実行セッション数
150    pub session_count: usize,
151    /// 成功率
152    pub success_rate: f64,
153    /// 使用した variant(例: "with_graph")
154    pub source_variant: String,
155    /// Bootstrap フェーズ
156    pub phase: LearningPhase,
157}
158
159impl BootstrapData {
160    /// 新規作成
161    pub fn new(session_count: usize, success_rate: f64, source_variant: impl Into<String>) -> Self {
162        Self {
163            completed_at: SystemTime::now()
164                .duration_since(UNIX_EPOCH)
165                .map(|d| d.as_secs())
166                .unwrap_or(0),
167            session_count,
168            success_rate,
169            source_variant: source_variant.into(),
170            phase: LearningPhase::Bootstrap,
171        }
172    }
173}
174
175// ============================================================================
176// ProfileStats
177// ============================================================================
178
179/// Profile の統計情報
180#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181pub struct ProfileStats {
182    /// 総実行回数
183    pub total_runs: usize,
184    /// 成功率
185    pub success_rate: f64,
186    /// 平均実行時間(ミリ秒)
187    pub avg_duration_ms: u64,
188    /// 最終実行日時
189    pub last_run_at: Option<u64>,
190}
191
192impl ProfileStats {
193    /// 実行結果を記録
194    pub fn record_run(&mut self, success: bool, duration_ms: u64) {
195        let prev_total = self.total_runs as f64;
196        let prev_success = self.success_rate * prev_total;
197
198        self.total_runs += 1;
199
200        // 成功率を更新
201        let new_success = if success {
202            prev_success + 1.0
203        } else {
204            prev_success
205        };
206        self.success_rate = new_success / self.total_runs as f64;
207
208        // 平均実行時間を更新(移動平均)
209        self.avg_duration_ms = ((self.avg_duration_ms as f64 * prev_total + duration_ms as f64)
210            / self.total_runs as f64) as u64;
211
212        self.last_run_at = Some(
213            SystemTime::now()
214                .duration_since(UNIX_EPOCH)
215                .map(|d| d.as_secs())
216                .unwrap_or(0),
217        );
218    }
219}
220
221// ============================================================================
222// ScenarioProfile
223// ============================================================================
224
225/// 永続的に改善されるシナリオの実体
226///
227/// Scenario(静的定義)に対する学習結果を管理する Entity。
228/// 各学習コンポーネントは独立して更新可能。
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ScenarioProfile {
231    // ============================================
232    // Identity
233    // ============================================
234    /// Profile ID
235    pub id: ScenarioProfileId,
236
237    /// シナリオソース参照
238    pub scenario_source: ScenarioSource,
239
240    /// ライフサイクル状態
241    pub state: ProfileState,
242
243    // ============================================
244    // Learned Components (全て明示的に型付け)
245    // ============================================
246    /// 学習済み依存グラフ
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub dep_graph: Option<LearnedDepGraph>,
249
250    /// 学習済み探索パラメータ
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub exploration: Option<LearnedExploration>,
253
254    /// 学習済み戦略設定
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub strategy: Option<LearnedStrategy>,
257
258    // ============================================
259    // Bootstrap Data
260    // ============================================
261    /// Bootstrap 完了データ
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub bootstrap: Option<BootstrapData>,
264
265    // ============================================
266    // Validation Data
267    // ============================================
268    /// 検証結果(Validator の出力)
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub validation: Option<ValidationResult>,
271
272    // ============================================
273    // Metadata
274    // ============================================
275    /// 統計情報
276    #[serde(default)]
277    pub stats: ProfileStats,
278
279    /// 作成日時
280    pub created_at: u64,
281
282    /// 更新日時
283    pub updated_at: u64,
284}
285
286impl ScenarioProfile {
287    /// 新規作成(Draft 状態)
288    pub fn new(id: impl Into<String>, source: ScenarioSource) -> Self {
289        let now = SystemTime::now()
290            .duration_since(UNIX_EPOCH)
291            .map(|d| d.as_secs())
292            .unwrap_or(0);
293
294        Self {
295            id: ScenarioProfileId::new(id),
296            scenario_source: source,
297            state: ProfileState::Draft,
298            dep_graph: None,
299            exploration: None,
300            strategy: None,
301            bootstrap: None,
302            validation: None,
303            stats: ProfileStats::default(),
304            created_at: now,
305            updated_at: now,
306        }
307    }
308
309    /// ファイルパスから作成
310    pub fn from_file(id: impl Into<String>, path: impl AsRef<Path>) -> Self {
311        Self::new(id, ScenarioSource::from_path(path))
312    }
313
314    // ============================================
315    // State Management
316    // ============================================
317
318    /// Bootstrap 開始
319    pub fn start_bootstrap(&mut self) {
320        self.state = ProfileState::Bootstrapping;
321        self.touch();
322    }
323
324    /// Bootstrap 完了(→ Validating)
325    pub fn complete_bootstrap(&mut self, data: BootstrapData) {
326        self.bootstrap = Some(data);
327        self.state = ProfileState::Validating;
328        self.touch();
329    }
330
331    /// 検証結果を適用
332    ///
333    /// Validator の出力を受け取り、状態を遷移させる。
334    /// - passed → Active
335    /// - failed → Failed
336    pub fn apply_validation(&mut self, result: ValidationResult) {
337        if result.passed {
338            self.state = ProfileState::Active;
339        } else {
340            self.state = ProfileState::Failed;
341        }
342        self.validation = Some(result);
343        self.touch();
344    }
345
346    /// 検証をスキップして Active に遷移(Bootstrap のみで使用可能にする場合)
347    pub fn skip_validation(&mut self) {
348        if self.state == ProfileState::Validating {
349            self.state = ProfileState::Active;
350            self.touch();
351        }
352    }
353
354    /// Failed から Draft に戻す(リトライ用)
355    pub fn retry(&mut self) {
356        if self.state == ProfileState::Failed {
357            self.state = ProfileState::Draft;
358            self.validation = None;
359            self.touch();
360        }
361    }
362
363    /// Optimizing 開始
364    pub fn start_optimizing(&mut self) {
365        if self.state == ProfileState::Active {
366            self.state = ProfileState::Optimizing;
367            self.touch();
368        }
369    }
370
371    /// Optimizing 完了
372    pub fn finish_optimizing(&mut self) {
373        if self.state == ProfileState::Optimizing {
374            self.state = ProfileState::Active;
375            self.touch();
376        }
377    }
378
379    /// 使用可能か
380    pub fn is_usable(&self) -> bool {
381        matches!(self.state, ProfileState::Active | ProfileState::Optimizing)
382    }
383
384    // ============================================
385    // Component Management
386    // ============================================
387
388    /// DepGraph を更新
389    pub fn update_dep_graph(&mut self, dep_graph: LearnedDepGraph) {
390        if let Some(existing) = &mut self.dep_graph {
391            existing.merge(&dep_graph);
392        } else {
393            self.dep_graph = Some(dep_graph);
394        }
395        self.touch();
396    }
397
398    /// Exploration を更新
399    pub fn update_exploration(&mut self, exploration: LearnedExploration) {
400        if let Some(existing) = &mut self.exploration {
401            existing.merge(&exploration);
402        } else {
403            self.exploration = Some(exploration);
404        }
405        self.touch();
406    }
407
408    /// Strategy を更新
409    pub fn update_strategy(&mut self, strategy: LearnedStrategy) {
410        if let Some(existing) = &mut self.strategy {
411            existing.merge(&strategy);
412        } else {
413            self.strategy = Some(strategy);
414        }
415        self.touch();
416    }
417
418    // ============================================
419    // Stats
420    // ============================================
421
422    /// 実行結果を記録
423    pub fn record_run(&mut self, success: bool, duration_ms: u64) {
424        self.stats.record_run(success, duration_ms);
425        self.touch();
426    }
427
428    /// 全コンポーネントの最小信頼度
429    pub fn min_confidence(&self) -> f64 {
430        [
431            self.dep_graph.as_ref().map(|c| c.confidence()),
432            self.exploration.as_ref().map(|c| c.confidence()),
433            self.strategy.as_ref().map(|c| c.confidence()),
434        ]
435        .into_iter()
436        .flatten()
437        .fold(1.0, f64::min)
438    }
439
440    /// 全コンポーネントの平均信頼度
441    pub fn avg_confidence(&self) -> f64 {
442        let confidences: Vec<f64> = [
443            self.dep_graph.as_ref().map(|c| c.confidence()),
444            self.exploration.as_ref().map(|c| c.confidence()),
445            self.strategy.as_ref().map(|c| c.confidence()),
446        ]
447        .into_iter()
448        .flatten()
449        .collect();
450
451        if confidences.is_empty() {
452            0.0
453        } else {
454            confidences.iter().sum::<f64>() / confidences.len() as f64
455        }
456    }
457
458    // ============================================
459    // Internal
460    // ============================================
461
462    fn touch(&mut self) {
463        self.updated_at = SystemTime::now()
464            .duration_since(UNIX_EPOCH)
465            .map(|d| d.as_secs())
466            .unwrap_or(0);
467    }
468}
469
470// ============================================================================
471// Tests
472// ============================================================================
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn test_profile_creation() {
480        let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
481
482        assert_eq!(profile.id.0, "test");
483        assert_eq!(profile.state, ProfileState::Draft);
484        assert!(profile.dep_graph.is_none());
485        assert!(!profile.is_usable());
486    }
487
488    #[test]
489    fn test_profile_lifecycle() {
490        use crate::validation::ValidationResult;
491
492        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
493
494        // Draft -> Bootstrapping
495        profile.start_bootstrap();
496        assert_eq!(profile.state, ProfileState::Bootstrapping);
497        assert!(!profile.is_usable());
498
499        // Bootstrapping -> Validating
500        let bootstrap_data = BootstrapData::new(10, 0.9, "with_graph");
501        profile.complete_bootstrap(bootstrap_data);
502        assert_eq!(profile.state, ProfileState::Validating);
503        assert!(!profile.is_usable());
504
505        // Validating -> Active (validation passed)
506        let result = ValidationResult::pass(0.8, 0.9, "no_regression", 20);
507        profile.apply_validation(result);
508        assert_eq!(profile.state, ProfileState::Active);
509        assert!(profile.is_usable());
510        assert!(profile.validation.is_some());
511
512        // Active -> Optimizing
513        profile.start_optimizing();
514        assert_eq!(profile.state, ProfileState::Optimizing);
515        assert!(profile.is_usable());
516
517        // Optimizing -> Active
518        profile.finish_optimizing();
519        assert_eq!(profile.state, ProfileState::Active);
520    }
521
522    #[test]
523    fn test_profile_validation_failed() {
524        use crate::validation::ValidationResult;
525
526        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
527
528        profile.start_bootstrap();
529        profile.complete_bootstrap(BootstrapData::new(10, 0.7, "with_graph"));
530        assert_eq!(profile.state, ProfileState::Validating);
531
532        // Validating -> Failed
533        let result = ValidationResult::fail(0.7, 0.6, "no_regression", "regression detected", 20);
534        profile.apply_validation(result);
535        assert_eq!(profile.state, ProfileState::Failed);
536        assert!(!profile.is_usable());
537
538        // Failed -> Draft (retry)
539        profile.retry();
540        assert_eq!(profile.state, ProfileState::Draft);
541        assert!(profile.validation.is_none());
542    }
543
544    #[test]
545    fn test_profile_skip_validation() {
546        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
547
548        profile.start_bootstrap();
549        profile.complete_bootstrap(BootstrapData::new(10, 0.9, "with_graph"));
550        assert_eq!(profile.state, ProfileState::Validating);
551
552        // Skip validation -> Active
553        profile.skip_validation();
554        assert_eq!(profile.state, ProfileState::Active);
555        assert!(profile.is_usable());
556    }
557
558    #[test]
559    fn test_profile_stats() {
560        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
561
562        profile.record_run(true, 100);
563        profile.record_run(true, 200);
564        profile.record_run(false, 150);
565
566        assert_eq!(profile.stats.total_runs, 3);
567        assert!((profile.stats.success_rate - 2.0 / 3.0).abs() < 0.001);
568        assert_eq!(profile.stats.avg_duration_ms, 150);
569    }
570
571    #[test]
572    fn test_component_update() {
573        use crate::exploration::DependencyGraph;
574
575        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
576
577        let dep_graph = LearnedDepGraph::new(DependencyGraph::new(), vec!["A".to_string()])
578            .with_confidence(0.8);
579
580        profile.update_dep_graph(dep_graph);
581        assert!(profile.dep_graph.is_some());
582        assert_eq!(profile.min_confidence(), 0.8);
583    }
584
585    #[test]
586    fn test_serialization() {
587        let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
588        let json = serde_json::to_string(&profile).unwrap();
589        let restored: ScenarioProfile = serde_json::from_str(&json).unwrap();
590        assert_eq!(restored.id.0, "test");
591    }
592}