ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! Content-Based Modified Files Detection
//!
//! This module implements a Symbol-centric approach to detecting which files
//! were modified during mutation execution.
//!
//! # Design Philosophy
//!
//! ## Web Service Analogy
//!
//! Consider the difference between two web application designs:
//!
//! ```text
//! ❌ Anti-pattern (Raw HTML storage):
//!    DB: { user_id: 1, html: "<div>Name: John</div>" }
//!    Update: html.replace("John", "Jane")
//!    Problem: Data and presentation are conflated
//!
//! ✅ Proper design (Entity + Template):
//!    DB: { user_id: 1, name: "John" }
//!    View: template.render(user)
//!    Update: user.name = "Jane" → re-render
//! ```
//!
//! ## Application to Ryo
//!
//! The same principle applies to Ryo's architecture:
//!
//! ```text
//! ❌ FileSpan-based (current problematic approach):
//!    SymbolRegistry: SymbolId → FileSpan (file position)
//!    Problem: Symbol (data) mixed with File position (presentation)
//!
//! ✅ Content-based (this module's approach):
//!    Entity: SymbolPath → PureItem (AST content)
//!    View: SymbolPath → FilePath (deterministic derivation)
//!    Update: PureItem change → regenerate affected files only
//! ```
//!
//! # Core Principles
//!
//! 1. **Symbol = Entity**: `SymbolId → PureItem` is the true data
//! 2. **File = View**: FilePath is deterministically derived from SymbolPath
//! 3. **FileSpan = Unnecessary**: A parsing artifact, meaningless post-mutation
//! 4. **Change Tracking = Symbol-level**: Track SymbolId via MutationEvent
//!
//! # SymbolPath → FilePath Derivation Rules
//!
//! ## Library Crates (lib.rs)
//!
//! ```text
//! SymbolPath                          FilePath
//! ────────────────────────────────────────────────────────
//! my_crate                         →  src/lib.rs
//! my_crate::Item                   →  src/lib.rs
//! my_crate::module::Item           →  src/module.rs
//! my_crate::module::sub::Item      →  src/module/sub.rs
//! my_crate::<impl Foo>             →  src/lib.rs
//! my_crate::module::<impl Foo>     →  src/module.rs
//! ```
//!
//! ## Binary Crates (main.rs) - Uses `main::` Prefix
//!
//! Binary symbols use the `main::` prefix to distinguish from library symbols:
//!
//! ```text
//! SymbolPath                          FilePath
//! ────────────────────────────────────────────────────────
//! main::my_app                     →  src/main.rs
//! main::my_app::Item               →  src/main.rs
//! main::my_app::cli::Args          →  src/cli.rs
//! main::my_app::cli::cmd::Run      →  src/cli/cmd.rs
//! main::my_app::<impl Config>      →  src/main.rs
//! ```
//!
//! This distinction is critical for bin-only crates (no lib.rs) where all symbols
//! must resolve to main.rs and its sub-modules.
//!
//! ## Workspace Crates (crates/xxx/)
//!
//! ```text
//! SymbolPath                          FilePath
//! ────────────────────────────────────────────────────────
//! my_crate                         →  crates/my-crate/src/lib.rs
//! my_crate::models::User           →  crates/my-crate/src/models.rs
//! main::my_app                     →  crates/my-app/src/main.rs
//! main::my_app::cli                →  crates/my-app/src/cli.rs
//! ```
//!
//! # Data Flow
//!
//! ```text
//! execute_v2()
//!//! MutationEvent emitted
//!     ├─ SymbolAdded { path }
//!     ├─ SymbolModified { id }
//!     └─ SymbolRemoved { path }
//!//! collect_modified_symbols(events, registry) → Vec<SymbolId>
//!//! symbols_to_files(symbols, registry, workspace_root)
//!     → HashSet<WorkspaceFilePath>  (derived, NOT from FileSpan)
//!//! FileDumper::dump_files(affected_files)  ← only changed files
//!//! modified_files (accurate)
//! ```
//!
//! # Edge Cases
//!
//! ## Binary Entry (main.rs) and Bin-Only Crates
//!
//! ### The Problem
//!
//! In a mixed crate (both lib.rs and main.rs), how do we distinguish between:
//! - `my_crate::Config` in lib.rs
//! - `my_crate::Config` in main.rs
//!
//! ### The Solution: `main::` Prefix
//!
//! Binary symbols use the `main::` prefix in their SymbolPath:
//!
//! ```text
//! Library symbol:  my_crate::Config     → src/lib.rs
//! Binary symbol:   main::my_crate::Config → src/main.rs
//! ```
//!
//! This prefix is applied during initial file loading by `SymbolPath::module_path_str()`:
//!
//! ```text
//! WorkspaceFilePath("src/lib.rs")  → SymbolPath("my_crate")
//! WorkspaceFilePath("src/main.rs") → SymbolPath("main::my_crate")
//! ```
//!
//! ### Bin-Only Crates (No lib.rs)
//!
//! Bin-only crates have ONLY main.rs, no lib.rs. All symbols get the `main::` prefix:
//!
//! ```text
//! // File: src/main.rs
//! pub enum Status { Active, Inactive }
//! fn main() { ... }
//!
//! // SymbolPaths:
//! main::my_app                  (crate root)
//! main::my_app::Status          (enum)
//! main::my_app::Status::Active  (variant)
//! main::my_app::main            (function)
//! ```
//!
//! The file resolution chain correctly handles this:
//! 1. `symbol_path_to_file()` extracts actual crate name: "my_app"
//! 2. Looks up CrateInfo from CargoMetadataProvider
//! 3. `resolve_candidates_with_crate_info()` checks entry_points
//! 4. Finds only Bin target (no Lib target) → returns ["src/main.rs"]
//!
//! ### Why This Works Without Adhoc Logic
//!
//! The system is fully declarative:
//! - **File → Symbol**: `module_path_str()` applies `main::` for main.rs
//! - **Symbol → File**: `resolve_candidates_with_crate_info()` uses CargoMetadataProvider
//! - **No path guessing**: All decisions based on Cargo.toml metadata
//!
//! No adhoc bin-only detection or lib.rs → main.rs path mapping is needed.
//!
//! ## New Symbols (AddItem)
//!
//! Problem: New symbols have no FileSpan
//! Solution: Derive FilePath from SymbolPath (FileSpan not needed)
//!
//! ```text
//! AddItem { target: "crate::models", content: "pub struct User {}" }
//!   → SymbolPath: crate::models::User
//!   → FilePath: src/models.rs (derived)
//! ```
//!
//! ## Symbol Move (RenameIdent across modules)
//!
//! Solution:
//! 1. Derive old_file from old_path → regenerate
//! 2. Derive new_file from new_path → regenerate

use crate::engine::events::MutationEvent;
use ryo_analysis::{SymbolId, SymbolKind, SymbolRegistry};

/// Collect modified SymbolIds from mutation events.
///
/// This function extracts all symbol IDs that were affected by mutations,
/// which will be used to determine which files need to be regenerated.
///
/// # Arguments
///
/// * `events` - The mutation events emitted during execution
/// * `registry` - The symbol registry to look up paths
///
/// # Returns
///
/// A deduplicated vector of affected SymbolIds
pub fn collect_modified_symbols(
    events: &[MutationEvent],
    registry: &SymbolRegistry,
) -> Vec<SymbolId> {
    let mut ids = Vec::new();

    for event in events {
        match event {
            MutationEvent::SymbolAdded { path, kind } => {
                if let Some(id) = registry.lookup(path) {
                    ids.push(id);
                }
                // Propagate to parent ONLY when adding a Module.
                // Adding `mod name;` requires the parent file to be regenerated.
                // Non-module symbols (functions, structs, etc.) only affect their own file.
                if matches!(kind, SymbolKind::Mod) {
                    if let Some(parent) = path.parent() {
                        if let Some(parent_id) = registry.lookup(&parent) {
                            ids.push(parent_id);
                        }
                    }
                }
            }
            MutationEvent::SymbolModified { id, .. } => {
                ids.push(*id);
            }
            MutationEvent::SymbolRemoved { path } => {
                // For removed symbols, the parent module is affected
                if let Some(parent) = path.parent() {
                    if let Some(id) = registry.lookup(&parent) {
                        ids.push(id);
                    }
                }
            }
            MutationEvent::SymbolRenamed { old_path, new_path } => {
                // Both old and new locations are affected
                if let Some(parent) = old_path.parent() {
                    if let Some(id) = registry.lookup(&parent) {
                        ids.push(id);
                    }
                }
                if let Some(id) = registry.lookup(new_path) {
                    ids.push(id);
                }
                if let Some(parent) = new_path.parent() {
                    if let Some(id) = registry.lookup(&parent) {
                        ids.push(id);
                    }
                }
            }
        }
    }

    ids.sort_unstable();
    ids.dedup();
    ids
}

// REMOVED: symbols_to_files() and symbol_path_to_file()
//
// These functions are replaced by RegistryGenerator.generate_affected()
// which determines file paths based on the generator's file layout strategy
// rather than inferring from SymbolPath structure.

#[cfg(test)]
mod tests {
    use super::*;
    use ryo_analysis::{SymbolKind, SymbolPath};

    fn make_registry_with_symbols() -> SymbolRegistry {
        let mut registry = SymbolRegistry::new();

        // Register some test symbols
        let _ = registry.register(SymbolPath::parse("test_crate").unwrap(), SymbolKind::Mod);
        let _ = registry.register(
            SymbolPath::parse("test_crate::Config").unwrap(),
            SymbolKind::Struct,
        );
        let _ = registry.register(
            SymbolPath::parse("test_crate::models").unwrap(),
            SymbolKind::Mod,
        );
        let _ = registry.register(
            SymbolPath::parse("test_crate::models::User").unwrap(),
            SymbolKind::Struct,
        );

        registry
    }

    #[test]
    fn test_collect_modified_symbols_added() {
        let registry = make_registry_with_symbols();
        let events = vec![MutationEvent::SymbolAdded {
            path: SymbolPath::parse("test_crate::Config").unwrap(),
            kind: SymbolKind::Struct,
        }];

        let modified = collect_modified_symbols(&events, &registry);

        // Should include both the added symbol and its parent module
        assert!(!modified.is_empty());
    }

    #[test]
    fn test_collect_modified_symbols_modified() {
        let registry = make_registry_with_symbols();
        let id = registry
            .lookup(&SymbolPath::parse("test_crate::Config").unwrap())
            .unwrap();

        let events = vec![MutationEvent::SymbolModified {
            id,
            modification: crate::engine::events::ModificationType::BodyModified,
        }];

        let modified = collect_modified_symbols(&events, &registry);
        assert!(modified.contains(&id));
    }

    /// NG-4: Adding a non-module symbol should NOT propagate to parent.
    /// Only Module additions need parent file regeneration (for `mod name;`).
    #[test]
    fn test_collect_modified_symbols_added_function_no_parent_propagation() {
        let mut reg = make_registry_with_symbols();
        let fn_id = reg
            .register(
                SymbolPath::parse("test_crate::models::process").unwrap(),
                SymbolKind::Function,
            )
            .unwrap();

        let events = vec![MutationEvent::SymbolAdded {
            path: SymbolPath::parse("test_crate::models::process").unwrap(),
            kind: SymbolKind::Function,
        }];

        let modified = collect_modified_symbols(&events, &reg);

        // Should include the function but NOT the parent module
        assert!(
            modified.contains(&fn_id),
            "Function itself should be marked"
        );
        let models_id = reg
            .lookup(&SymbolPath::parse("test_crate::models").unwrap())
            .unwrap();
        assert!(
            !modified.contains(&models_id),
            "Parent module should NOT be marked for non-module additions"
        );
    }

    /// Adding a Module SHOULD propagate to parent (parent needs `mod name;`).
    #[test]
    fn test_collect_modified_symbols_added_module_propagates_to_parent() {
        let mut reg = make_registry_with_symbols();
        let sub_id = reg
            .register(
                SymbolPath::parse("test_crate::models::sub").unwrap(),
                SymbolKind::Mod,
            )
            .unwrap();

        let events = vec![MutationEvent::SymbolAdded {
            path: SymbolPath::parse("test_crate::models::sub").unwrap(),
            kind: SymbolKind::Mod,
        }];

        let modified = collect_modified_symbols(&events, &reg);

        // Should include both the new module AND its parent
        assert!(modified.contains(&sub_id), "New module should be marked");
        let models_id = reg
            .lookup(&SymbolPath::parse("test_crate::models").unwrap())
            .unwrap();
        assert!(
            modified.contains(&models_id),
            "Parent module should be marked for module additions"
        );
    }

    // Note: These tests require MockMetadataProvider which is behind test-utils feature.
    // Integration tests verify the full behavior with real CargoMetadataProvider.
}