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;
52use crate::validation::ValidationResult;
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub struct ScenarioProfileId(pub String);
61
62impl ScenarioProfileId {
63 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum ProfileState {
89 #[default]
91 Draft,
92 Bootstrapping,
94 Validating,
96 Active,
98 Optimizing,
100 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#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(tag = "type", rename_all = "snake_case")]
124pub enum ScenarioSource {
125 File { path: PathBuf },
127 Inline { content: String },
129}
130
131impl ScenarioSource {
132 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#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct BootstrapData {
147 pub completed_at: u64,
149 pub session_count: usize,
151 pub success_rate: f64,
153 pub source_variant: String,
155 pub phase: LearningPhase,
157}
158
159impl BootstrapData {
160 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181pub struct ProfileStats {
182 pub total_runs: usize,
184 pub success_rate: f64,
186 pub avg_duration_ms: u64,
188 pub last_run_at: Option<u64>,
190}
191
192impl ProfileStats {
193 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ScenarioProfile {
231 pub id: ScenarioProfileId,
236
237 pub scenario_source: ScenarioSource,
239
240 pub state: ProfileState,
242
243 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub dep_graph: Option<LearnedDepGraph>,
249
250 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub exploration: Option<LearnedExploration>,
253
254 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub strategy: Option<LearnedStrategy>,
257
258 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub bootstrap: Option<BootstrapData>,
264
265 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub validation: Option<ValidationResult>,
271
272 #[serde(default)]
277 pub stats: ProfileStats,
278
279 pub created_at: u64,
281
282 pub updated_at: u64,
284}
285
286impl ScenarioProfile {
287 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 pub fn from_file(id: impl Into<String>, path: impl AsRef<Path>) -> Self {
311 Self::new(id, ScenarioSource::from_path(path))
312 }
313
314 pub fn start_bootstrap(&mut self) {
320 self.state = ProfileState::Bootstrapping;
321 self.touch();
322 }
323
324 pub fn complete_bootstrap(&mut self, data: BootstrapData) {
326 self.bootstrap = Some(data);
327 self.state = ProfileState::Validating;
328 self.touch();
329 }
330
331 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 pub fn skip_validation(&mut self) {
348 if self.state == ProfileState::Validating {
349 self.state = ProfileState::Active;
350 self.touch();
351 }
352 }
353
354 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 pub fn start_optimizing(&mut self) {
365 if self.state == ProfileState::Active {
366 self.state = ProfileState::Optimizing;
367 self.touch();
368 }
369 }
370
371 pub fn finish_optimizing(&mut self) {
373 if self.state == ProfileState::Optimizing {
374 self.state = ProfileState::Active;
375 self.touch();
376 }
377 }
378
379 pub fn is_usable(&self) -> bool {
381 matches!(self.state, ProfileState::Active | ProfileState::Optimizing)
382 }
383
384 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 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 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 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 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 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 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#[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 profile.start_bootstrap();
496 assert_eq!(profile.state, ProfileState::Bootstrapping);
497 assert!(!profile.is_usable());
498
499 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 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 profile.start_optimizing();
514 assert_eq!(profile.state, ProfileState::Optimizing);
515 assert!(profile.is_usable());
516
517 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 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 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 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}