use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use super::learned_component::{
LearnedComponent, LearnedDepGraph, LearnedExploration, LearnedStrategy,
};
use super::session_group::LearningPhase;
use crate::validation::ValidationResult;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ScenarioProfileId(pub String);
impl ScenarioProfileId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl std::fmt::Display for ScenarioProfileId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProfileState {
#[default]
Draft,
Bootstrapping,
Validating,
Active,
Optimizing,
Failed,
}
impl std::fmt::Display for ProfileState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Draft => write!(f, "draft"),
Self::Bootstrapping => write!(f, "bootstrapping"),
Self::Validating => write!(f, "validating"),
Self::Active => write!(f, "active"),
Self::Optimizing => write!(f, "optimizing"),
Self::Failed => write!(f, "failed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ScenarioSource {
File { path: PathBuf },
Inline { content: String },
}
impl ScenarioSource {
pub fn from_path(path: impl AsRef<Path>) -> Self {
Self::File {
path: path.as_ref().to_path_buf(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootstrapData {
pub completed_at: u64,
pub session_count: usize,
pub success_rate: f64,
pub source_variant: String,
pub phase: LearningPhase,
}
impl BootstrapData {
pub fn new(session_count: usize, success_rate: f64, source_variant: impl Into<String>) -> Self {
Self {
completed_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
session_count,
success_rate,
source_variant: source_variant.into(),
phase: LearningPhase::Bootstrap,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProfileStats {
pub total_runs: usize,
pub success_rate: f64,
pub avg_duration_ms: u64,
pub last_run_at: Option<u64>,
}
impl ProfileStats {
pub fn record_run(&mut self, success: bool, duration_ms: u64) {
let prev_total = self.total_runs as f64;
let prev_success = self.success_rate * prev_total;
self.total_runs += 1;
let new_success = if success {
prev_success + 1.0
} else {
prev_success
};
self.success_rate = new_success / self.total_runs as f64;
self.avg_duration_ms = ((self.avg_duration_ms as f64 * prev_total + duration_ms as f64)
/ self.total_runs as f64) as u64;
self.last_run_at = Some(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioProfile {
pub id: ScenarioProfileId,
pub scenario_source: ScenarioSource,
pub state: ProfileState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dep_graph: Option<LearnedDepGraph>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exploration: Option<LearnedExploration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strategy: Option<LearnedStrategy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bootstrap: Option<BootstrapData>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validation: Option<ValidationResult>,
#[serde(default)]
pub stats: ProfileStats,
pub created_at: u64,
pub updated_at: u64,
}
impl ScenarioProfile {
pub fn new(id: impl Into<String>, source: ScenarioSource) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
Self {
id: ScenarioProfileId::new(id),
scenario_source: source,
state: ProfileState::Draft,
dep_graph: None,
exploration: None,
strategy: None,
bootstrap: None,
validation: None,
stats: ProfileStats::default(),
created_at: now,
updated_at: now,
}
}
pub fn from_file(id: impl Into<String>, path: impl AsRef<Path>) -> Self {
Self::new(id, ScenarioSource::from_path(path))
}
pub fn start_bootstrap(&mut self) {
self.state = ProfileState::Bootstrapping;
self.touch();
}
pub fn complete_bootstrap(&mut self, data: BootstrapData) {
self.bootstrap = Some(data);
self.state = ProfileState::Validating;
self.touch();
}
pub fn apply_validation(&mut self, result: ValidationResult) {
if result.passed {
self.state = ProfileState::Active;
} else {
self.state = ProfileState::Failed;
}
self.validation = Some(result);
self.touch();
}
pub fn skip_validation(&mut self) {
if self.state == ProfileState::Validating {
self.state = ProfileState::Active;
self.touch();
}
}
pub fn retry(&mut self) {
if self.state == ProfileState::Failed {
self.state = ProfileState::Draft;
self.validation = None;
self.touch();
}
}
pub fn start_optimizing(&mut self) {
if self.state == ProfileState::Active {
self.state = ProfileState::Optimizing;
self.touch();
}
}
pub fn finish_optimizing(&mut self) {
if self.state == ProfileState::Optimizing {
self.state = ProfileState::Active;
self.touch();
}
}
pub fn is_usable(&self) -> bool {
matches!(self.state, ProfileState::Active | ProfileState::Optimizing)
}
pub fn update_dep_graph(&mut self, dep_graph: LearnedDepGraph) {
if let Some(existing) = &mut self.dep_graph {
existing.merge(&dep_graph);
} else {
self.dep_graph = Some(dep_graph);
}
self.touch();
}
pub fn update_exploration(&mut self, exploration: LearnedExploration) {
if let Some(existing) = &mut self.exploration {
existing.merge(&exploration);
} else {
self.exploration = Some(exploration);
}
self.touch();
}
pub fn update_strategy(&mut self, strategy: LearnedStrategy) {
if let Some(existing) = &mut self.strategy {
existing.merge(&strategy);
} else {
self.strategy = Some(strategy);
}
self.touch();
}
pub fn record_run(&mut self, success: bool, duration_ms: u64) {
self.stats.record_run(success, duration_ms);
self.touch();
}
pub fn min_confidence(&self) -> f64 {
[
self.dep_graph.as_ref().map(|c| c.confidence()),
self.exploration.as_ref().map(|c| c.confidence()),
self.strategy.as_ref().map(|c| c.confidence()),
]
.into_iter()
.flatten()
.fold(1.0, f64::min)
}
pub fn avg_confidence(&self) -> f64 {
let confidences: Vec<f64> = [
self.dep_graph.as_ref().map(|c| c.confidence()),
self.exploration.as_ref().map(|c| c.confidence()),
self.strategy.as_ref().map(|c| c.confidence()),
]
.into_iter()
.flatten()
.collect();
if confidences.is_empty() {
0.0
} else {
confidences.iter().sum::<f64>() / confidences.len() as f64
}
}
fn touch(&mut self) {
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profile_creation() {
let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
assert_eq!(profile.id.0, "test");
assert_eq!(profile.state, ProfileState::Draft);
assert!(profile.dep_graph.is_none());
assert!(!profile.is_usable());
}
#[test]
fn test_profile_lifecycle() {
use crate::validation::ValidationResult;
let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
profile.start_bootstrap();
assert_eq!(profile.state, ProfileState::Bootstrapping);
assert!(!profile.is_usable());
let bootstrap_data = BootstrapData::new(10, 0.9, "with_graph");
profile.complete_bootstrap(bootstrap_data);
assert_eq!(profile.state, ProfileState::Validating);
assert!(!profile.is_usable());
let result = ValidationResult::pass(0.8, 0.9, "no_regression", 20);
profile.apply_validation(result);
assert_eq!(profile.state, ProfileState::Active);
assert!(profile.is_usable());
assert!(profile.validation.is_some());
profile.start_optimizing();
assert_eq!(profile.state, ProfileState::Optimizing);
assert!(profile.is_usable());
profile.finish_optimizing();
assert_eq!(profile.state, ProfileState::Active);
}
#[test]
fn test_profile_validation_failed() {
use crate::validation::ValidationResult;
let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
profile.start_bootstrap();
profile.complete_bootstrap(BootstrapData::new(10, 0.7, "with_graph"));
assert_eq!(profile.state, ProfileState::Validating);
let result = ValidationResult::fail(0.7, 0.6, "no_regression", "regression detected", 20);
profile.apply_validation(result);
assert_eq!(profile.state, ProfileState::Failed);
assert!(!profile.is_usable());
profile.retry();
assert_eq!(profile.state, ProfileState::Draft);
assert!(profile.validation.is_none());
}
#[test]
fn test_profile_skip_validation() {
let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
profile.start_bootstrap();
profile.complete_bootstrap(BootstrapData::new(10, 0.9, "with_graph"));
assert_eq!(profile.state, ProfileState::Validating);
profile.skip_validation();
assert_eq!(profile.state, ProfileState::Active);
assert!(profile.is_usable());
}
#[test]
fn test_profile_stats() {
let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
profile.record_run(true, 100);
profile.record_run(true, 200);
profile.record_run(false, 150);
assert_eq!(profile.stats.total_runs, 3);
assert!((profile.stats.success_rate - 2.0 / 3.0).abs() < 0.001);
assert_eq!(profile.stats.avg_duration_ms, 150);
}
#[test]
fn test_component_update() {
use crate::exploration::DependencyGraph;
let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
let dep_graph = LearnedDepGraph::new(DependencyGraph::new(), vec!["A".to_string()])
.with_confidence(0.8);
profile.update_dep_graph(dep_graph);
assert!(profile.dep_graph.is_some());
assert_eq!(profile.min_confidence(), 0.8);
}
#[test]
fn test_serialization() {
let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
let json = serde_json::to_string(&profile).unwrap();
let restored: ScenarioProfile = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id.0, "test");
}
}