ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! AgentState: Shared state for agent decision-making
//!
//! This represents the "world" as seen by agents - files, errors, etc.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

/// State of the agent's world
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentState {
    /// Files that have been loaded/modified
    pub files: HashMap<PathBuf, FileState>,

    /// Current errors (compile, lint, etc.)
    pub errors: Vec<ErrorInfo>,

    /// Files currently being investigated by other agents
    pub occupied_files: HashMap<PathBuf, u32>,

    /// Completed investigation targets
    pub investigated: HashMap<PathBuf, u32>,

    /// Custom metrics
    pub metrics: HashMap<String, f64>,

    /// Last progress tick (for stall detection)
    pub last_progress_tick: u64,

    /// Total changes made
    pub total_changes: usize,
}

impl AgentState {
    /// Create a new empty state
    pub fn new() -> Self {
        Self::default()
    }

    /// Add or update a file's state
    pub fn update_file(&mut self, path: PathBuf, state: FileState) {
        self.files.insert(path, state);
    }

    /// Get a file's state
    pub fn get_file(&self, path: &PathBuf) -> Option<&FileState> {
        self.files.get(path)
    }

    /// Add an error
    pub fn add_error(&mut self, error: ErrorInfo) {
        self.errors.push(error);
    }

    /// Clear all errors
    pub fn clear_errors(&mut self) {
        self.errors.clear();
    }

    /// Check if there are any errors
    pub fn has_errors(&self) -> bool {
        !self.errors.is_empty()
    }

    /// Get the number of errors
    pub fn error_count(&self) -> usize {
        self.errors.len()
    }

    /// Get the file with the most errors
    pub fn most_errored_file(&self) -> Option<&PathBuf> {
        let mut counts: HashMap<&PathBuf, usize> = HashMap::new();
        for error in &self.errors {
            *counts.entry(&error.file).or_insert(0) += 1;
        }
        counts.into_iter().max_by_key(|(_, c)| *c).map(|(f, _)| f)
    }

    /// Mark a file as occupied by an agent
    pub fn occupy_file(&mut self, path: PathBuf, agent_id: u32) {
        self.occupied_files.insert(path, agent_id);
    }

    /// Release a file
    pub fn release_file(&mut self, path: &PathBuf) {
        self.occupied_files.remove(path);
    }

    /// Check if a file is occupied (by another agent)
    pub fn is_occupied(&self, path: &PathBuf, exclude_agent: u32) -> bool {
        self.occupied_files
            .get(path)
            .map(|&id| id != exclude_agent)
            .unwrap_or(false)
    }

    /// Mark a file as investigated
    pub fn mark_investigated(&mut self, path: PathBuf, agent_id: u32) {
        self.investigated.insert(path, agent_id);
    }

    /// Check if a file has been investigated
    pub fn is_investigated(&self, path: &PathBuf) -> bool {
        self.investigated.contains_key(path)
    }

    /// Get uninvestigated files
    pub fn uninvestigated_files(&self) -> Vec<&PathBuf> {
        self.files
            .keys()
            .filter(|p| !self.investigated.contains_key(*p))
            .collect()
    }

    /// Get available files for an agent (not occupied, not investigated)
    pub fn available_files(&self, agent_id: u32) -> Vec<&PathBuf> {
        self.files
            .keys()
            .filter(|p| !self.investigated.contains_key(*p) && !self.is_occupied(p, agent_id))
            .collect()
    }

    /// Set a metric
    pub fn set_metric(&mut self, key: impl Into<String>, value: f64) {
        self.metrics.insert(key.into(), value);
    }

    /// Get a metric
    pub fn get_metric(&self, key: &str) -> Option<f64> {
        self.metrics.get(key).copied()
    }

    /// Record progress at current tick
    pub fn record_progress(&mut self, tick: u64, changes: usize) {
        if changes > 0 {
            self.last_progress_tick = tick;
            self.total_changes += changes;
        }
    }

    /// Check if progress is stalled
    pub fn is_stalled(&self, current_tick: u64, threshold: u64) -> bool {
        current_tick.saturating_sub(self.last_progress_tick) >= threshold
    }

    /// Generate a summary for logging
    pub fn summary(&self) -> String {
        format!(
            "Files: {}, Errors: {}, Investigated: {}/{}, Changes: {}",
            self.files.len(),
            self.errors.len(),
            self.investigated.len(),
            self.files.len(),
            self.total_changes,
        )
    }
}

/// State of a single file
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileState {
    /// Number of lines
    pub lines: usize,

    /// File size in bytes
    pub size_bytes: usize,

    /// Last modification tick
    pub last_modified_tick: u64,

    /// Number of changes made
    pub changes: usize,

    /// Whether the file has been read
    pub read: bool,

    /// Whether the file has been modified
    pub modified: bool,
}

impl FileState {
    /// Create a new file state
    pub fn new(lines: usize, size_bytes: usize) -> Self {
        Self {
            lines,
            size_bytes,
            ..Default::default()
        }
    }

    /// Mark as read
    pub fn mark_read(&mut self) {
        self.read = true;
    }

    /// Mark as modified
    pub fn mark_modified(&mut self, tick: u64, changes: usize) {
        self.modified = true;
        self.last_modified_tick = tick;
        self.changes += changes;
    }
}

/// Error information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorInfo {
    /// File containing the error
    pub file: PathBuf,

    /// Line number
    pub line: usize,

    /// Column number
    pub column: Option<usize>,

    /// Error message
    pub message: String,

    /// Error severity
    pub severity: ErrorSeverity,
}

impl ErrorInfo {
    /// Create a new error
    pub fn new(file: PathBuf, line: usize, message: impl Into<String>) -> Self {
        Self {
            file,
            line,
            column: None,
            message: message.into(),
            severity: ErrorSeverity::Error,
        }
    }

    /// Set the column
    pub fn with_column(mut self, column: usize) -> Self {
        self.column = Some(column);
        self
    }

    /// Set the severity
    pub fn with_severity(mut self, severity: ErrorSeverity) -> Self {
        self.severity = severity;
        self
    }
}

/// Error severity
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorSeverity {
    Warning,
    Error,
    Critical,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_agent_state() {
        let mut state = AgentState::new();

        // Add files
        state.update_file(PathBuf::from("a.rs"), FileState::new(100, 2000));
        state.update_file(PathBuf::from("b.rs"), FileState::new(50, 1000));

        assert_eq!(state.files.len(), 2);

        // Add errors
        state.add_error(ErrorInfo::new(PathBuf::from("a.rs"), 10, "error 1"));
        state.add_error(ErrorInfo::new(PathBuf::from("a.rs"), 20, "error 2"));
        state.add_error(ErrorInfo::new(PathBuf::from("b.rs"), 5, "error 3"));

        assert!(state.has_errors());
        assert_eq!(state.error_count(), 3);
        assert_eq!(state.most_errored_file(), Some(&PathBuf::from("a.rs")));
    }

    #[test]
    fn test_occupation() {
        let mut state = AgentState::new();
        state.update_file(PathBuf::from("a.rs"), FileState::new(100, 2000));

        let path = PathBuf::from("a.rs");

        // Agent 0 occupies the file
        state.occupy_file(path.clone(), 0);
        assert!(!state.is_occupied(&path, 0)); // Not occupied for agent 0
        assert!(state.is_occupied(&path, 1)); // Occupied for agent 1

        // Release
        state.release_file(&path);
        assert!(!state.is_occupied(&path, 1));
    }

    #[test]
    fn test_investigation() {
        let mut state = AgentState::new();
        state.update_file(PathBuf::from("a.rs"), FileState::new(100, 2000));
        state.update_file(PathBuf::from("b.rs"), FileState::new(50, 1000));

        let path_a = PathBuf::from("a.rs");

        // Mark a.rs as investigated
        state.mark_investigated(path_a.clone(), 0);

        assert!(state.is_investigated(&path_a));
        assert!(!state.is_investigated(&PathBuf::from("b.rs")));

        // Only b.rs should be uninvestigated
        let uninvestigated = state.uninvestigated_files();
        assert_eq!(uninvestigated.len(), 1);
    }
}