use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentState {
pub version: String,
pub last_updated: DateTime<Utc>,
pub monitored_projects: HashMap<String, ProjectState>,
pub quality_history: Vec<QualitySnapshot>,
pub config_overrides: HashMap<String, serde_json::Value>,
pub statistics: AgentStatistics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectState {
pub id: String,
pub path: PathBuf,
pub started_at: DateTime<Utc>,
pub last_analyzed: Option<DateTime<Utc>>,
pub current_metrics: QualityMetrics,
pub watch_patterns: Vec<String>,
pub thresholds: QualityThresholds,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct QualityMetrics {
pub avg_complexity: f64,
pub max_complexity: u32,
pub satd_count: usize,
pub dead_code_percentage: f64,
pub quality_score: f64,
pub files_analyzed: usize,
pub total_violations: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityThresholds {
pub max_complexity: u32,
pub satd_tolerance: usize,
pub dead_code_max_percentage: f64,
pub min_quality_score: f64,
}
impl Default for QualityThresholds {
fn default() -> Self {
Self {
max_complexity: 20, satd_tolerance: 0, dead_code_max_percentage: 10.0,
min_quality_score: 80.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualitySnapshot {
pub timestamp: DateTime<Utc>,
pub project_id: String,
pub metrics: QualityMetrics,
pub violations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AgentStatistics {
pub sessions_count: u64,
pub analyses_performed: u64,
pub violations_detected: u64,
pub refactorings_suggested: u64,
pub total_uptime_seconds: u64,
}
pub struct StatePersistence {
state_file: PathBuf,
state: Arc<RwLock<AgentState>>,
auto_save_interval: u64,
}
impl StatePersistence {
pub fn new(state_dir: impl AsRef<Path>) -> Result<Self> {
let state_file = state_dir.as_ref().join("agent_state.json");
let state = if state_file.exists() {
Self::load_from_file(&state_file)?
} else {
AgentState::default()
};
Ok(Self {
state_file,
state: Arc::new(RwLock::new(state)),
auto_save_interval: 60, })
}
fn load_from_file(path: &Path) -> Result<AgentState> {
let contents = std::fs::read_to_string(path).context("Failed to read state file")?;
serde_json::from_str(&contents).context("Failed to deserialize state")
}
pub async fn save(&self) -> Result<()> {
let state = self.state.read().await;
let json = serde_json::to_string_pretty(&*state)?;
if let Some(parent) = self.state_file.parent() {
fs::create_dir_all(parent).await?;
}
let temp_file = self.state_file.with_extension("tmp");
fs::write(&temp_file, json).await?;
fs::rename(&temp_file, &self.state_file).await?;
Ok(())
}
pub async fn start_auto_save(&self) {
let state_file = self.state_file.clone();
let state = self.state.clone();
let interval = self.auto_save_interval;
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval));
loop {
interval.tick().await;
if let Ok(state) = state.read().await.to_json() {
if let Err(e) = fs::write(&state_file, state).await {
tracing::error!("Failed to auto-save state: {}", e);
} else {
tracing::debug!("State auto-saved");
}
}
}
});
}
pub async fn add_project(&self, project: ProjectState) -> Result<()> {
let mut state = self.state.write().await;
state.monitored_projects.insert(project.id.clone(), project);
state.last_updated = Utc::now();
Ok(())
}
pub async fn remove_project(&self, project_id: &str) -> Result<()> {
let mut state = self.state.write().await;
state.monitored_projects.remove(project_id);
state.last_updated = Utc::now();
Ok(())
}
pub async fn update_metrics(&self, project_id: &str, metrics: QualityMetrics) -> Result<()> {
let mut state = self.state.write().await;
if let Some(project) = state.monitored_projects.get_mut(project_id) {
project.current_metrics = metrics.clone();
project.last_analyzed = Some(Utc::now());
state.quality_history.push(QualitySnapshot {
timestamp: Utc::now(),
project_id: project_id.to_string(),
metrics,
violations: Vec::new(),
});
if state.quality_history.len() > 1000 {
let drain_count = state.quality_history.len() - 1000;
state.quality_history.drain(0..drain_count);
}
state.last_updated = Utc::now();
}
Ok(())
}
pub async fn get_state(&self) -> AgentState {
self.state.read().await.clone()
}
pub async fn update_statistics<F>(&self, updater: F) -> Result<()>
where
F: FnOnce(&mut AgentStatistics),
{
let mut state = self.state.write().await;
updater(&mut state.statistics);
state.last_updated = Utc::now();
Ok(())
}
}
impl Default for AgentState {
fn default() -> Self {
Self {
version: "1.0.0".to_string(),
last_updated: Utc::now(),
monitored_projects: HashMap::new(),
quality_history: Vec::new(),
config_overrides: HashMap::new(),
statistics: AgentStatistics::default(),
}
}
}
impl AgentState {
fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).context("Failed to serialize state")
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_state_persistence() {
let temp_dir = TempDir::new().unwrap();
let persistence = StatePersistence::new(temp_dir.path()).unwrap();
let project = ProjectState {
id: "test_project".to_string(),
path: PathBuf::from("/test/path"),
started_at: Utc::now(),
last_analyzed: None,
current_metrics: QualityMetrics::default(),
watch_patterns: vec!["*.rs".to_string()],
thresholds: QualityThresholds::default(),
};
persistence.add_project(project).await.unwrap();
persistence.save().await.unwrap();
let loaded = StatePersistence::new(temp_dir.path()).unwrap();
let state = loaded.get_state().await;
assert!(state.monitored_projects.contains_key("test_project"));
}
#[tokio::test]
async fn test_metrics_update() {
let temp_dir = TempDir::new().unwrap();
let persistence = StatePersistence::new(temp_dir.path()).unwrap();
let project = ProjectState {
id: "test".to_string(),
path: PathBuf::from("/test"),
started_at: Utc::now(),
last_analyzed: None,
current_metrics: QualityMetrics::default(),
watch_patterns: vec![],
thresholds: QualityThresholds::default(),
};
persistence.add_project(project).await.unwrap();
let metrics = QualityMetrics {
avg_complexity: 5.5,
max_complexity: 15,
satd_count: 0,
dead_code_percentage: 2.5,
quality_score: 92.0,
files_analyzed: 100,
total_violations: 0,
};
persistence.update_metrics("test", metrics).await.unwrap();
let state = persistence.get_state().await;
assert_eq!(state.quality_history.len(), 1);
assert_eq!(state.quality_history[0].project_id, "test");
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}