use crate::storage::StateRef;
use ryo_analysis::SymbolPath;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxEntry {
pub id: u64,
pub timestamp_ms: u64,
pub action: TxAction,
pub duration_us: Option<u64>,
}
impl TxEntry {
pub fn new(id: u64, timestamp_ms: u64, action: TxAction) -> Self {
Self {
id,
timestamp_ms,
action,
duration_us: None,
}
}
pub fn with_duration(mut self, duration_us: u64) -> Self {
self.duration_us = Some(duration_us);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TxAction {
SessionStart {
project_path: PathBuf,
file_count: usize,
},
SessionEnd {
total_changes: usize,
files_modified: usize,
},
GoalSet {
query: String,
intent_type: String,
confidence: f64,
},
MutationApplied {
mutation_type: String,
target: String,
changes: usize,
mutation_data: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
file_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pre_state: Option<StateRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
post_state: Option<StateRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
affected_symbols: Vec<SymbolPath>,
},
MutationBatch {
mutations: Vec<MutationRecord>,
total_changes: usize,
},
FileLoaded {
path: PathBuf,
size_bytes: usize,
},
FileModified {
path: PathBuf,
changes: usize,
},
FileWritten {
path: PathBuf,
},
CompileCheck {
success: bool,
error_count: usize,
errors: Vec<String>,
},
Checkpoint {
name: String,
},
Undo {
target_id: u64,
},
Redo {
target_id: u64,
},
Custom {
name: String,
data: serde_json::Value,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationRecord {
pub mutation_type: String,
pub target: String,
pub changes: usize,
pub file_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub affected_symbols: Vec<SymbolPath>,
}
impl TxAction {
pub fn describe(&self) -> String {
match self {
TxAction::SessionStart {
project_path,
file_count,
} => {
format!(
"Session started: {} ({} files)",
project_path.display(),
file_count
)
}
TxAction::SessionEnd {
total_changes,
files_modified,
} => {
format!(
"Session ended: {} changes in {} files",
total_changes, files_modified
)
}
TxAction::GoalSet {
query, intent_type, ..
} => {
format!("Goal: {} ({})", query, intent_type)
}
TxAction::MutationApplied {
mutation_type,
target,
changes,
..
} => {
format!("{}: {} ({} changes)", mutation_type, target, changes)
}
TxAction::MutationBatch {
mutations,
total_changes,
} => {
format!(
"Batch: {} mutations ({} changes)",
mutations.len(),
total_changes
)
}
TxAction::FileLoaded { path, .. } => {
format!("Loaded: {}", path.display())
}
TxAction::FileModified { path, changes } => {
format!("Modified: {} ({} changes)", path.display(), changes)
}
TxAction::FileWritten { path } => {
format!("Written: {}", path.display())
}
TxAction::CompileCheck {
success,
error_count,
..
} => {
if *success {
"Compile check: OK".to_string()
} else {
format!("Compile check: {} errors", error_count)
}
}
TxAction::Checkpoint { name } => {
format!("Checkpoint: {}", name)
}
TxAction::Undo { target_id } => {
format!("Undo: entry #{}", target_id)
}
TxAction::Redo { target_id } => {
format!("Redo: entry #{}", target_id)
}
TxAction::Custom { name, .. } => {
format!("Custom: {}", name)
}
}
}
pub fn is_replayable(&self) -> bool {
matches!(
self,
TxAction::MutationApplied { .. }
| TxAction::MutationBatch { .. }
| TxAction::FileWritten { .. }
)
}
pub fn is_mutation(&self) -> bool {
matches!(
self,
TxAction::MutationApplied { .. } | TxAction::MutationBatch { .. }
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entry_serialization() {
let entry = TxEntry::new(
1,
100,
TxAction::MutationApplied {
mutation_type: "Rename".to_string(),
target: "old_name -> new_name".to_string(),
changes: 5,
mutation_data: None,
file_path: None,
pre_state: None,
post_state: None,
affected_symbols: vec![],
},
);
let json = serde_json::to_string(&entry).unwrap();
let deserialized: TxEntry = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, 1);
assert_eq!(deserialized.timestamp_ms, 100);
}
#[test]
fn test_action_describe() {
let action = TxAction::GoalSet {
query: "rename foo to bar".to_string(),
intent_type: "RenameIdent".to_string(),
confidence: 0.95,
};
assert!(action.describe().contains("rename foo to bar"));
}
}