1use 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;
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
59pub struct ScenarioProfileId(pub String);
60
61impl ScenarioProfileId {
62 pub fn new(id: impl Into<String>) -> Self {
64 Self(id.into())
65 }
66}
67
68impl std::fmt::Display for ScenarioProfileId {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 write!(f, "{}", self.0)
71 }
72}
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
80#[serde(rename_all = "snake_case")]
81pub enum ProfileState {
82 #[default]
84 Draft,
85 Bootstrapping,
87 Active,
89 Optimizing,
91}
92
93impl std::fmt::Display for ProfileState {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::Draft => write!(f, "draft"),
97 Self::Bootstrapping => write!(f, "bootstrapping"),
98 Self::Active => write!(f, "active"),
99 Self::Optimizing => write!(f, "optimizing"),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(tag = "type", rename_all = "snake_case")]
111pub enum ScenarioSource {
112 File { path: PathBuf },
114 Inline { content: String },
116}
117
118impl ScenarioSource {
119 pub fn from_path(path: impl AsRef<Path>) -> Self {
121 Self::File {
122 path: path.as_ref().to_path_buf(),
123 }
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct BootstrapData {
134 pub completed_at: u64,
136 pub session_count: usize,
138 pub success_rate: f64,
140 pub source_variant: String,
142 pub phase: LearningPhase,
144}
145
146impl BootstrapData {
147 pub fn new(session_count: usize, success_rate: f64, source_variant: impl Into<String>) -> Self {
149 Self {
150 completed_at: SystemTime::now()
151 .duration_since(UNIX_EPOCH)
152 .map(|d| d.as_secs())
153 .unwrap_or(0),
154 session_count,
155 success_rate,
156 source_variant: source_variant.into(),
157 phase: LearningPhase::Bootstrap,
158 }
159 }
160}
161
162#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub struct ProfileStats {
169 pub total_runs: usize,
171 pub success_rate: f64,
173 pub avg_duration_ms: u64,
175 pub last_run_at: Option<u64>,
177}
178
179impl ProfileStats {
180 pub fn record_run(&mut self, success: bool, duration_ms: u64) {
182 let prev_total = self.total_runs as f64;
183 let prev_success = self.success_rate * prev_total;
184
185 self.total_runs += 1;
186
187 let new_success = if success {
189 prev_success + 1.0
190 } else {
191 prev_success
192 };
193 self.success_rate = new_success / self.total_runs as f64;
194
195 self.avg_duration_ms = ((self.avg_duration_ms as f64 * prev_total + duration_ms as f64)
197 / self.total_runs as f64) as u64;
198
199 self.last_run_at = Some(
200 SystemTime::now()
201 .duration_since(UNIX_EPOCH)
202 .map(|d| d.as_secs())
203 .unwrap_or(0),
204 );
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ScenarioProfile {
218 pub id: ScenarioProfileId,
223
224 pub scenario_source: ScenarioSource,
226
227 pub state: ProfileState,
229
230 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub dep_graph: Option<LearnedDepGraph>,
236
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub exploration: Option<LearnedExploration>,
240
241 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub strategy: Option<LearnedStrategy>,
244
245 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub bootstrap: Option<BootstrapData>,
251
252 #[serde(default)]
257 pub stats: ProfileStats,
258
259 pub created_at: u64,
261
262 pub updated_at: u64,
264}
265
266impl ScenarioProfile {
267 pub fn new(id: impl Into<String>, source: ScenarioSource) -> Self {
269 let now = SystemTime::now()
270 .duration_since(UNIX_EPOCH)
271 .map(|d| d.as_secs())
272 .unwrap_or(0);
273
274 Self {
275 id: ScenarioProfileId::new(id),
276 scenario_source: source,
277 state: ProfileState::Draft,
278 dep_graph: None,
279 exploration: None,
280 strategy: None,
281 bootstrap: None,
282 stats: ProfileStats::default(),
283 created_at: now,
284 updated_at: now,
285 }
286 }
287
288 pub fn from_file(id: impl Into<String>, path: impl AsRef<Path>) -> Self {
290 Self::new(id, ScenarioSource::from_path(path))
291 }
292
293 pub fn start_bootstrap(&mut self) {
299 self.state = ProfileState::Bootstrapping;
300 self.touch();
301 }
302
303 pub fn complete_bootstrap(&mut self, data: BootstrapData) {
305 self.bootstrap = Some(data);
306 self.state = ProfileState::Active;
307 self.touch();
308 }
309
310 pub fn start_optimizing(&mut self) {
312 if self.state == ProfileState::Active {
313 self.state = ProfileState::Optimizing;
314 self.touch();
315 }
316 }
317
318 pub fn finish_optimizing(&mut self) {
320 if self.state == ProfileState::Optimizing {
321 self.state = ProfileState::Active;
322 self.touch();
323 }
324 }
325
326 pub fn is_usable(&self) -> bool {
328 matches!(self.state, ProfileState::Active | ProfileState::Optimizing)
329 }
330
331 pub fn update_dep_graph(&mut self, dep_graph: LearnedDepGraph) {
337 if let Some(existing) = &mut self.dep_graph {
338 existing.merge(&dep_graph);
339 } else {
340 self.dep_graph = Some(dep_graph);
341 }
342 self.touch();
343 }
344
345 pub fn update_exploration(&mut self, exploration: LearnedExploration) {
347 if let Some(existing) = &mut self.exploration {
348 existing.merge(&exploration);
349 } else {
350 self.exploration = Some(exploration);
351 }
352 self.touch();
353 }
354
355 pub fn update_strategy(&mut self, strategy: LearnedStrategy) {
357 if let Some(existing) = &mut self.strategy {
358 existing.merge(&strategy);
359 } else {
360 self.strategy = Some(strategy);
361 }
362 self.touch();
363 }
364
365 pub fn record_run(&mut self, success: bool, duration_ms: u64) {
371 self.stats.record_run(success, duration_ms);
372 self.touch();
373 }
374
375 pub fn min_confidence(&self) -> f64 {
377 [
378 self.dep_graph.as_ref().map(|c| c.confidence()),
379 self.exploration.as_ref().map(|c| c.confidence()),
380 self.strategy.as_ref().map(|c| c.confidence()),
381 ]
382 .into_iter()
383 .flatten()
384 .fold(1.0, f64::min)
385 }
386
387 pub fn avg_confidence(&self) -> f64 {
389 let confidences: Vec<f64> = [
390 self.dep_graph.as_ref().map(|c| c.confidence()),
391 self.exploration.as_ref().map(|c| c.confidence()),
392 self.strategy.as_ref().map(|c| c.confidence()),
393 ]
394 .into_iter()
395 .flatten()
396 .collect();
397
398 if confidences.is_empty() {
399 0.0
400 } else {
401 confidences.iter().sum::<f64>() / confidences.len() as f64
402 }
403 }
404
405 fn touch(&mut self) {
410 self.updated_at = SystemTime::now()
411 .duration_since(UNIX_EPOCH)
412 .map(|d| d.as_secs())
413 .unwrap_or(0);
414 }
415}
416
417#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn test_profile_creation() {
427 let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
428
429 assert_eq!(profile.id.0, "test");
430 assert_eq!(profile.state, ProfileState::Draft);
431 assert!(profile.dep_graph.is_none());
432 assert!(!profile.is_usable());
433 }
434
435 #[test]
436 fn test_profile_lifecycle() {
437 let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
438
439 profile.start_bootstrap();
441 assert_eq!(profile.state, ProfileState::Bootstrapping);
442 assert!(!profile.is_usable());
443
444 let bootstrap_data = BootstrapData::new(10, 0.9, "with_graph");
446 profile.complete_bootstrap(bootstrap_data);
447 assert_eq!(profile.state, ProfileState::Active);
448 assert!(profile.is_usable());
449
450 profile.start_optimizing();
452 assert_eq!(profile.state, ProfileState::Optimizing);
453 assert!(profile.is_usable());
454
455 profile.finish_optimizing();
457 assert_eq!(profile.state, ProfileState::Active);
458 }
459
460 #[test]
461 fn test_profile_stats() {
462 let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
463
464 profile.record_run(true, 100);
465 profile.record_run(true, 200);
466 profile.record_run(false, 150);
467
468 assert_eq!(profile.stats.total_runs, 3);
469 assert!((profile.stats.success_rate - 2.0 / 3.0).abs() < 0.001);
470 assert_eq!(profile.stats.avg_duration_ms, 150);
471 }
472
473 #[test]
474 fn test_component_update() {
475 use crate::exploration::DependencyGraph;
476
477 let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
478
479 let dep_graph = LearnedDepGraph::new(DependencyGraph::new(), vec!["A".to_string()])
480 .with_confidence(0.8);
481
482 profile.update_dep_graph(dep_graph);
483 assert!(profile.dep_graph.is_some());
484 assert_eq!(profile.min_confidence(), 0.8);
485 }
486
487 #[test]
488 fn test_serialization() {
489 let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
490 let json = serde_json::to_string(&profile).unwrap();
491 let restored: ScenarioProfile = serde_json::from_str(&json).unwrap();
492 assert_eq!(restored.id.0, "test");
493 }
494}