ryo-storage 0.1.0

Persistent storage and transaction log for RYO
Documentation
//! TxLog: Replayable Transaction Log for Ryo
//!
//! This module provides a comprehensive transaction logging system that enables:
//! - **Replay**: Re-execute all operations from a saved log
//! - **Undo/Redo**: Navigate through operation history
//! - **Debugging**: Understand exactly what happened during a session
//! - **Audit**: Track all changes for compliance or review
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │  TxLog: Replayable Command Log                              │
//! │                                                             │
//! │  ┌─────────┐    Channel    ┌──────────────┐               │
//! │  │ Caller  │ ───────────▶ │ TxLogger     │               │
//! │  │ (sync)  │   TxEntry    │ (async)      │               │
//! │  └─────────┘              └──────┬───────┘               │
//! │                                  │                        │
//! │                                  ▼                        │
//! │                          ┌──────────────┐                │
//! │                          │ InMemory Log │                │
//! │                          │ Vec<TxEntry> │                │
//! │                          └──────┬───────┘                │
//! │                                  │ dump()                │
//! │                                  ▼                        │
//! │                          ┌──────────────┐                │
//! │                          │     JSON     │                │
//! │                          └──────────────┘                │
//! └─────────────────────────────────────────────────────────────┘
//! ```
//!
//! # Usage
//!
//! ## Basic Logging
//!
//! ```rust,ignore
//! use ryo_core::txlog::{TxLogger, TxAction};
//! use std::path::PathBuf;
//!
//! // Start a logging session
//! let logger = TxLogger::start(PathBuf::from("my_project"), 100);
//!
//! // Log actions as they happen
//! logger.log_goal_set("rename function foo to bar", "refactoring", 0.95);
//! logger.log_mutation("Rename", "foo → bar", 5);
//! logger.log_file_modified("src/lib.rs", 3);
//!
//! // Finish and get the log
//! let log = logger.finish();
//!
//! // Save for later replay
//! log.dump_json(&PathBuf::from("session.txlog.json")).unwrap();
//! ```
//!
//! ## Replay
//!
//! ```rust,ignore
//! use ryo_core::txlog::TxLog;
//!
//! // Load a saved log
//! let log = TxLog::load_json(&PathBuf::from("session.txlog.json")).unwrap();
//!
//! // Iterate through all actions
//! for entry in log.iter() {
//!     println!("{:?}", entry.action);
//! }
//!
//! // Get summary statistics
//! let summary = log.summary();
//! println!("Total mutations: {}", summary.mutation_count);
//! ```
//!
//! ## Undo/Redo
//!
//! ```rust,ignore
//! use ryo_core::txlog::TxLog;
//!
//! let mut log = TxLog::new();
//!
//! // Create checkpoints at important points
//! log.checkpoint();
//! // ... do some operations ...
//! log.checkpoint();
//! // ... do more operations ...
//!
//! // Undo to previous checkpoint
//! if let Some(entries) = log.undo() {
//!     println!("Undid {} entries", entries.len());
//! }
//!
//! // Redo what we just undid
//! if let Some(entries) = log.redo() {
//!     println!("Redid {} entries", entries.len());
//! }
//! ```

mod entry;
mod log;
mod logger;
mod replay;

// Re-export main types
pub use entry::{MutationRecord, TxAction, TxEntry};
pub use log::{TxLog, TxSummary};
pub use logger::TxLogger;
pub use replay::{ReplayAnalysis, ReplayEngine, ReplayError, ReplayMode, ReplayResult};

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::{Path, PathBuf};
    use std::thread;
    use std::time::Duration;

    #[test]
    fn test_basic_logging_workflow() {
        // Start logger
        let logger = TxLogger::start(PathBuf::from("/test/project"), 50);

        // Log some actions
        logger.log_goal("test goal", "test", 0.9);
        logger.log_mutation("Rename", "foo → bar", 3);
        logger.log_file_modified(Path::new("test.rs"), 2);

        // Small delay to ensure messages are processed
        thread::sleep(Duration::from_millis(10));

        // Finish and verify
        let log = logger.finish();

        assert!(log.len() >= 4); // SessionStart + 3 logged actions

        let summary = log.summary();
        assert_eq!(summary.total_mutations, 1);
        assert_eq!(summary.files_modified, 1);
    }

    #[test]
    fn test_json_serialization() {
        let mut log = TxLog::new();
        log.log(TxAction::GoalSet {
            query: "test query".to_string(),
            intent_type: "refactoring".to_string(),
            confidence: 0.95,
        });
        log.log(TxAction::MutationApplied {
            mutation_type: "Rename".to_string(),
            target: "old → new".to_string(),
            changes: 5,
            mutation_data: None,
            file_path: None,
            pre_state: None,
            post_state: None,
            affected_symbols: vec![],
        });

        // Serialize to JSON string
        let json = log.to_json().unwrap();

        // Deserialize back
        let loaded = TxLog::from_json(&json).unwrap();

        assert_eq!(loaded.len(), log.len());
    }

    #[test]
    fn test_checkpoint_based_filtering() {
        let mut log = TxLog::new();

        // Add entries with checkpoints
        log.log(TxAction::GoalSet {
            query: "goal 1".to_string(),
            intent_type: "test".to_string(),
            confidence: 0.8,
        });
        log.log(TxAction::Checkpoint {
            name: "cp1".to_string(),
        });

        log.log(TxAction::GoalSet {
            query: "goal 2".to_string(),
            intent_type: "test".to_string(),
            confidence: 0.9,
        });
        log.log(TxAction::MutationApplied {
            mutation_type: "Rename".to_string(),
            target: "foo".to_string(),
            changes: 1,
            mutation_data: None,
            file_path: None,
            pre_state: None,
            post_state: None,
            affected_symbols: vec![],
        });

        // Get entries since checkpoint
        let entries = log.entries_since_checkpoint("cp1");
        assert_eq!(entries.len(), 2); // goal 2 + mutation
    }

    #[test]
    fn test_replay_iterator() {
        let mut log = TxLog::new();

        log.log(TxAction::SessionStart {
            project_path: PathBuf::from("/test"),
            file_count: 10,
        });
        log.log(TxAction::GoalSet {
            query: "test".to_string(),
            intent_type: "test".to_string(),
            confidence: 0.9,
        });
        log.log(TxAction::SessionEnd {
            total_changes: 5,
            files_modified: 2,
        });

        let entries: Vec<_> = log.iter().collect();
        assert_eq!(entries.len(), 3);

        // Verify order
        assert!(matches!(entries[0].action, TxAction::SessionStart { .. }));
        assert!(matches!(entries[1].action, TxAction::GoalSet { .. }));
        assert!(matches!(entries[2].action, TxAction::SessionEnd { .. }));
    }

    #[test]
    fn test_filter_by_action_type() {
        let mut log = TxLog::new();

        log.log(TxAction::GoalSet {
            query: "goal 1".to_string(),
            intent_type: "test".to_string(),
            confidence: 0.8,
        });
        log.log(TxAction::MutationApplied {
            mutation_type: "Rename".to_string(),
            target: "a → b".to_string(),
            changes: 1,
            mutation_data: None,
            file_path: None,
            pre_state: None,
            post_state: None,
            affected_symbols: vec![],
        });
        log.log(TxAction::GoalSet {
            query: "goal 2".to_string(),
            intent_type: "test".to_string(),
            confidence: 0.9,
        });
        log.log(TxAction::MutationApplied {
            mutation_type: "Delete".to_string(),
            target: "c".to_string(),
            changes: 1,
            mutation_data: None,
            file_path: None,
            pre_state: None,
            post_state: None,
            affected_symbols: vec![],
        });

        // Filter only mutations
        let mutations: Vec<_> = log
            .iter()
            .filter(|e| matches!(e.action, TxAction::MutationApplied { .. }))
            .collect();
        assert_eq!(mutations.len(), 2);

        // Filter only goals
        let goals: Vec<_> = log
            .iter()
            .filter(|e| matches!(e.action, TxAction::GoalSet { .. }))
            .collect();
        assert_eq!(goals.len(), 2);
    }

    #[test]
    fn test_summary_generation() {
        let mut log = TxLog::new();

        log.log(TxAction::MutationApplied {
            mutation_type: "Rename".to_string(),
            target: "foo".to_string(),
            changes: 3,
            mutation_data: None,
            file_path: None,
            pre_state: None,
            post_state: None,
            affected_symbols: vec![],
        });
        log.log(TxAction::MutationApplied {
            mutation_type: "AddField".to_string(),
            target: "bar".to_string(),
            changes: 1,
            mutation_data: None,
            file_path: None,
            pre_state: None,
            post_state: None,
            affected_symbols: vec![],
        });
        log.log(TxAction::FileModified {
            path: PathBuf::from("test.rs"),
            changes: 2,
        });
        log.log(TxAction::Checkpoint {
            name: "done".to_string(),
        });

        let summary = log.summary();
        assert_eq!(summary.total_mutations, 2);
        assert_eq!(summary.total_changes, 6); // mutations: 3 + 1, file_modified: 2
        assert_eq!(summary.files_modified, 1);
        assert!(summary.checkpoints.contains(&"done".to_string()));
    }
}