Skip to main content

ryo_executor/executor/
blueprint_executor.rs

1//! BlueprintExecutor: Execute ParallelBlueprint against AnalysisContext
2//!
3//! Uses MutationRegistry to delegate all MutationSpec → Mutation conversions
4//! to specialized Converter implementations.
5//!
6//! ```text
7//! ParallelBlueprint
8//!    │
9//!    ▼ execute()
10//! BlueprintExecutor
11//!    │
12//!    ├── Strategy Selection (Sequential / Wavefront)
13//!    ├── Topological sort by dependencies
14//!    ├── Delegate to MutationRegistry.convert_and_apply()
15//!    │      ├── RenameConverter
16//!    │      ├── FieldConverter
17//!    │      ├── AddItemConverter
18//!    │      ├── IdiomConverter (13 idioms)
19//!    │      ├── TraitConverter
20//!    │      ├── MoveConverter
21//!    │      ├── PluginConverter (WASM skeleton)
22//!    │      └── ... (31 converters total)
23//!    │
24//!    ▼
25//! BlueprintResult
26//! ```
27//!
28//! ## Execution Strategies
29//!
30//! - **Sequential**: Execute mutations one by one (safe, debuggable)
31//! - **Wavefront**: Execute independent mutations in parallel within each wave
32//!
33//! ```text
34//! Wave 0: [Spec_A, Spec_B, Spec_C] → parallel compute → barrier → apply
35//! Wave 1: [Spec_D]                 → parallel compute → barrier → apply
36//! Wave 2: [Spec_E, Spec_F]         → parallel compute → barrier → apply
37//! ```
38
39use super::blueprint::ParallelBlueprint;
40use super::registry::MutationRegistry;
41use super::spec::{MutationSpec, MutationTargetSymbol};
42use crate::engine::{collect_affected_ids, MutationEvent};
43use ryo_analysis::{AnalysisContext, RegistryUpdateBatch, SymbolPath};
44use ryo_source::pure::ToSynError;
45use ryo_symbol::{MetadataError, WorkspaceFilePath};
46use std::collections::HashSet;
47use std::sync::Arc;
48use tracing::{debug, info, instrument, warn};
49
50/// Error during file sync after blueprint execution.
51#[derive(Debug, thiserror::Error)]
52pub enum SyncError {
53    #[error("cargo metadata unavailable: {0}")]
54    Metadata(#[from] MetadataError),
55    #[error("source generation failed: {0}")]
56    SourceGeneration(#[from] ToSynError),
57}
58
59/// Execution strategy for BlueprintExecutor
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum ExecutionStrategy {
62    /// Execute mutations one by one (safe, debuggable)
63    #[default]
64    Sequential,
65
66    /// Execute independent mutations in parallel within each wave
67    /// Uses Collect-then-Apply pattern for thread safety
68    Wavefront,
69}
70
71/// Suggest the best execution strategy based on blueprint characteristics
72pub fn suggest_strategy(blueprint: &ParallelBlueprint) -> ExecutionStrategy {
73    let parallelism = blueprint.parallelism();
74    let mutation_count = blueprint.mutations.len();
75
76    // Heuristics for strategy selection:
77    // - Low parallelism (≤1.2) → Sequential (no benefit from parallel)
78    // - Very few mutations (≤2) → Sequential (overhead not worth it)
79    // - Otherwise → Wavefront
80    if parallelism <= 1.2 || mutation_count <= 2 {
81        ExecutionStrategy::Sequential
82    } else {
83        ExecutionStrategy::Wavefront
84    }
85}
86
87/// Result of executing a blueprint
88#[derive(Debug, Clone)]
89pub struct BlueprintResult {
90    /// Results for each mutation spec
91    pub results: Vec<SpecResult>,
92
93    /// Total changes across all mutations
94    pub total_changes: usize,
95
96    /// Files that were modified
97    pub modified_files: Vec<WorkspaceFilePath>,
98
99    /// Whether execution completed successfully
100    pub success: bool,
101
102    /// Error message if failed
103    pub error: Option<String>,
104
105    /// Registry updates to apply (for Context-centric design)
106    ///
107    /// These updates should be applied to the SymbolRegistry after
108    /// execution to keep it in sync with the AST changes.
109    pub registry_updates: RegistryUpdateBatch,
110}
111
112/// Result of executing a single MutationSpec
113#[derive(Debug, Clone)]
114pub struct SpecResult {
115    /// Index in the blueprint
116    pub index: usize,
117
118    /// The spec that was executed
119    pub spec_type: String,
120
121    /// Number of changes made
122    pub changes: usize,
123
124    /// Files affected
125    pub affected_files: Vec<WorkspaceFilePath>,
126
127    /// Symbols affected (for history tracking)
128    pub affected_symbols: Vec<SymbolPath>,
129
130    /// Whether this spec succeeded
131    pub success: bool,
132
133    /// Error message if failed
134    pub error: Option<String>,
135
136    /// Registry updates from this spec
137    pub registry_updates: RegistryUpdateBatch,
138
139    /// Mutation events emitted during execution (for incremental updates)
140    pub events: Vec<MutationEvent>,
141}
142
143impl BlueprintResult {
144    pub fn success(results: Vec<SpecResult>, modified_files: Vec<WorkspaceFilePath>) -> Self {
145        let total_changes = results.iter().map(|r| r.changes).sum();
146        // Collect all registry updates from spec results
147        let mut all_updates = RegistryUpdateBatch::new();
148        for result in &results {
149            for update in &result.registry_updates {
150                all_updates.push(update.clone());
151            }
152        }
153        Self {
154            results,
155            total_changes,
156            modified_files,
157            success: true,
158            error: None,
159            registry_updates: all_updates,
160        }
161    }
162
163    pub fn failure(error: impl Into<String>) -> Self {
164        Self {
165            results: vec![],
166            total_changes: 0,
167            modified_files: vec![],
168            success: false,
169            error: Some(error.into()),
170            registry_updates: RegistryUpdateBatch::new(),
171        }
172    }
173}
174
175/// Executor for ParallelBlueprint
176#[derive(Debug)]
177pub struct BlueprintExecutor {
178    /// Registry for MutationSpec → Mutation conversion
179    registry: MutationRegistry,
180
181    /// Execution strategy
182    pub strategy: ExecutionStrategy,
183
184    /// Whether to verify compile after each mutation
185    pub verify_after_each: bool,
186
187    /// Whether to stop on first error
188    pub stop_on_error: bool,
189
190    /// Whether to ignore conflicts and process sequentially
191    /// Default: true (conflicts are ignored, specs processed in order)
192    pub ignore_conflicts: bool,
193}
194
195impl Default for BlueprintExecutor {
196    fn default() -> Self {
197        Self {
198            registry: MutationRegistry::default(),
199            strategy: ExecutionStrategy::default(),
200            verify_after_each: false,
201            stop_on_error: true,
202            ignore_conflicts: true,
203        }
204    }
205}
206
207/// Convert a workspace-relative `WorkspaceFilePath` into the crate-relative key
208/// expected by `RegistryGenerator::with_existing_files` (e.g. `"src/storage.rs"`).
209///
210/// Returns `None` when the metadata provider cannot resolve the crate's layout
211/// or when the workspace-relative path does not live under the layout prefix.
212///
213/// Extracted from the inline closure inside [`BlueprintExecutor::sync_files_and_rebuild`]
214/// so the hyphen-named-crate strip_prefix behaviour (Cargo name `ryo-app` ↔ module
215/// name `ryo_app`) is independently testable.
216pub(crate) fn ws_file_to_crate_relative_key<P: ryo_symbol::WorkspaceMetadataProvider>(
217    wfp: &WorkspaceFilePath,
218    metadata: &P,
219) -> Option<String> {
220    use ryo_symbol::CrateName;
221    let crate_name = CrateName::new(wfp.crate_name().as_str()).ok()?;
222    let layout = metadata.crate_layout(&crate_name)?;
223    let ws_rel = wfp.as_relative();
224    let prefix = layout.to_workspace_relative("");
225    let prefix_path = prefix.as_path();
226    let stripped = if prefix_path.as_os_str().is_empty() {
227        ws_rel.to_path_buf()
228    } else {
229        ws_rel.strip_prefix(prefix_path).ok()?.to_path_buf()
230    };
231    stripped.to_str().map(str::to_string)
232}
233
234/// Convert a workspace-relative `WorkspaceFilePath` into a `(CrateName,
235/// crate_relative_key)` pair used for per-crate `existing_file_keys`
236/// construction inside [`BlueprintExecutor::sync_files_and_rebuild`].
237///
238/// Returns `None` under the same conditions as
239/// [`ws_file_to_crate_relative_key`]. The returned `CrateName` is the same
240/// `CrateName` used by `RegistryGenerator::with_existing_files_per_crate`
241/// keying, so the per-crate isolation contract is preserved end-to-end.
242pub(crate) fn ws_file_to_crate_pair<P: ryo_symbol::WorkspaceMetadataProvider>(
243    wfp: &WorkspaceFilePath,
244    metadata: &P,
245) -> Option<(ryo_symbol::CrateName, String)> {
246    use ryo_symbol::CrateName;
247    let crate_name = CrateName::new(wfp.crate_name().as_str()).ok()?;
248    let key = ws_file_to_crate_relative_key(wfp, metadata)?;
249    Some((crate_name, key))
250}
251
252impl BlueprintExecutor {
253    pub fn new() -> Self {
254        Self::default()
255    }
256
257    // ========================================================================
258    // V2 API: ASTRegistry-centric execution (new path)
259    // ========================================================================
260
261    /// Execute blueprint using ASTRegistry-centric path.
262    ///
263    /// This is the new execution path that:
264    /// 1. Executes mutations directly on ASTRegistry (no file I/O)
265    /// 2. Returns MutationEvents for incremental updates
266    ///
267    /// **Important**: This method only updates ASTRegistry. Callers must:
268    /// - Call `ctx.sync_files_and_rebuild()` for full sync (files + graphs)
269    /// - Or skip sync for lightweight precheck scenarios
270    ///
271    /// Unimplemented MutationSpecs will panic - this is intentional
272    /// as we're building towards complete migration.
273    #[instrument(skip(self, blueprint, ctx), fields(mutations = blueprint.mutations.len()))]
274    pub fn execute_v2(
275        &self,
276        blueprint: &ParallelBlueprint,
277        ctx: &mut AnalysisContext,
278    ) -> BlueprintResult {
279        debug!(
280            "Starting blueprint execution with {} mutations",
281            blueprint.mutations.len()
282        );
283
284        // Check for conflicts (skip if ignore_conflicts is true)
285        if !self.ignore_conflicts && blueprint.needs_escalation() {
286            warn!(
287                "Blueprint has {} conflicts requiring escalation",
288                blueprint.conflicts.len()
289            );
290            return BlueprintResult::failure(format!(
291                "Blueprint has {} conflicts requiring escalation",
292                blueprint.conflicts.len()
293            ));
294        }
295
296        let mut results: Vec<SpecResult> = Vec::new();
297        let mut completed: HashSet<usize> = HashSet::new();
298
299        // Execute in topological order
300        while completed.len() < blueprint.mutations.len() {
301            let ready = blueprint
302                .deps
303                .ready_set(blueprint.mutations.len(), &completed);
304
305            if ready.is_empty() {
306                if completed.len() < blueprint.mutations.len() {
307                    warn!("Dependency cycle detected");
308                    return BlueprintResult::failure("Dependency cycle detected");
309                }
310                break;
311            }
312
313            debug!("Ready to execute {} specs", ready.len());
314
315            for idx in ready {
316                let spec = &blueprint.mutations[idx];
317                debug!("Executing spec {}: {:?}", idx, spec);
318                let result = self.execute_spec_v2(idx, spec, ctx);
319
320                if let Some(ref error) = result.error {
321                    warn!(
322                        "Spec {} completed with error: success={}, changes={}, error={}",
323                        idx, result.success, result.changes, error
324                    );
325                } else {
326                    debug!(
327                        "Spec {} completed: success={}, changes={}, events={}",
328                        idx,
329                        result.success,
330                        result.changes,
331                        result.events.len()
332                    );
333                }
334
335                let success = result.success;
336                results.push(result);
337                completed.insert(idx);
338
339                if !success && self.stop_on_error {
340                    let total_changes = results.iter().map(|r| r.changes).sum();
341
342                    // Get the error from the failed spec result
343                    let spec_error = results
344                        .last()
345                        .and_then(|r| r.error.as_ref())
346                        .map(|e| format!("Spec {} failed: {}", idx, e))
347                        .unwrap_or_else(|| format!("Stopped at spec {} (no error message)", idx));
348
349                    warn!("Stopping on error at spec {}: {}", idx, spec_error);
350
351                    return BlueprintResult {
352                        results,
353                        total_changes,
354                        modified_files: Vec::new(),
355                        success: false,
356                        error: Some(spec_error),
357                        registry_updates: RegistryUpdateBatch::new(),
358                    };
359                }
360            }
361        }
362
363        let total_changes: usize = results.iter().map(|r| r.changes).sum();
364        info!(
365            "Blueprint execution completed: {} results, {} total changes",
366            results.len(),
367            total_changes
368        );
369
370        BlueprintResult::success(results, Vec::new())
371    }
372
373    /// Synchronize files and rebuild analysis graphs after execute_v2.
374    ///
375    /// This method should be called after `execute_v2()` when you need:
376    /// - Updated source files (for disk write or cargo check)
377    /// - Updated analysis graphs (code_graph, typeflow, dataflow, detail_store)
378    ///
379    /// For lightweight precheck scenarios, skip this call entirely.
380    ///
381    /// # File Path Resolution
382    ///
383    /// This method handles the conversion from `RegistryGenerator`'s output
384    /// (crate-relative paths like `"src/lib.rs"`) to workspace-relative paths
385    /// (like `"crates/core/src/lib.rs"`).
386    ///
387    /// The conversion works as follows:
388    ///
389    /// 1. **Extract crate roots from existing files**: For each crate, find its
390    ///    root directory by looking at existing `WorkspaceFilePath`s in `ctx.files`.
391    ///    For example, `"crates/core/src/lib.rs"` → crate_root = `"crates/core"`.
392    ///
393    /// 2. **Combine crate_root + crate-relative path**: The generator outputs
394    ///    `"src/lib.rs"`, and we prepend the crate_root to get the full
395    ///    workspace-relative path: `"crates/core" + "src/lib.rs"` → `"crates/core/src/lib.rs"`.
396    ///
397    /// This approach keeps `RegistryGenerator` focused on pure SymbolPath-based
398    /// generation, while the caller (this function) handles workspace layout concerns.
399    ///
400    /// # Arguments
401    /// - `result`: The BlueprintResult from execute_v2
402    /// - `ctx`: The AnalysisContext that was mutated
403    ///
404    /// # Returns
405    /// List of modified file paths (as `WorkspaceFilePath`)
406    #[instrument(skip(result, ctx), fields(total_changes = result.total_changes))]
407    pub fn sync_files_and_rebuild(
408        result: &BlueprintResult,
409        ctx: &mut AnalysisContext,
410    ) -> Result<Vec<WorkspaceFilePath>, SyncError> {
411        use crate::engine::{collect_modified_symbols, RegistryGenerator};
412        use ryo_symbol::CargoMetadataProvider;
413
414        debug!(
415            "Starting sync_files_and_rebuild with {} results",
416            result.results.len()
417        );
418
419        // Step 1: Collect all events from execution results
420        let all_events: Vec<MutationEvent> = result
421            .results
422            .iter()
423            .flat_map(|r| r.events.clone())
424            .collect();
425
426        debug!(
427            "Collected {} mutation events from results",
428            all_events.len()
429        );
430        // INVARIANT: No mutation events → no files changed → nothing to sync.
431        // This prevents the expensive generate path from running on 0-mutation results.
432        if all_events.is_empty() {
433            debug!("No mutation events — skipping file sync entirely");
434            return Ok(Vec::new());
435        }
436
437        // Step 2: Determine modified files from events
438        let modified_symbol_ids = collect_modified_symbols(&all_events, ctx.registry());
439        debug!("Found {} modified symbols", modified_symbol_ids.len());
440        // Step 3: Generator determines affected files and generates ONLY those files.
441        //
442        // DESIGN RULE: Full-workspace generation (generate/dump_all) is PROHIBITED here.
443        // Only files containing modified symbols may be regenerated.
444        // This is critical because:
445        //   1. to_source() (prettyplease) is expensive per file
446        //   2. Regenerating unmodified files causes format drift (vec![] → vec!(), shorthand expansion)
447        //   3. Writing unmodified files wastes I/O and triggers unnecessary re-indexing
448        let metadata = CargoMetadataProvider::from_directory(&ctx.workspace_root)?;
449
450        // Collect existing crate-relative file paths **per crate** so the
451        // generator preserves on-disk layout (`<mod>/mod.rs` vs flat `<mod>.rs`)
452        // **without cross-crate contamination**.
453        //
454        // Pre-fix: a single workspace-wide HashSet caused
455        // `ryo-executor/src/executor/mod.rs` to bleed into `ryo-agent`'s lookup
456        // and emit a phantom `executor/mod.rs` next to that crate's existing
457        // flat `executor.rs` — the RL061 mod-ambiguity bug captured by
458        // `crates/ryo-app/tests/sync_phantom_mod_rs_probe.rs`. Per-crate
459        // isolation keys the key set by `CrateName` so each crate sees only
460        // its own layout.
461        let mut existing_per_crate: std::collections::HashMap<
462            ryo_symbol::CrateName,
463            std::collections::HashSet<String>,
464        > = std::collections::HashMap::new();
465        for wfp in ctx.files().keys() {
466            if let Some((crate_name, key)) = ws_file_to_crate_pair(wfp, &metadata) {
467                existing_per_crate
468                    .entry(crate_name)
469                    .or_default()
470                    .insert(key);
471            }
472        }
473
474        // R2 fix: derive the set of crates that own a `src/lib.rs` on
475        // disk straight from the per-crate existing-file map. The
476        // generator uses this to decline emitting a lib-style root for
477        // binary-only crates (= those NOT in this set when their
478        // symbols carry no `main::` prefix). The previous sync-side
479        // defensive guard from `f42eb504` is replaced by this pre-flight
480        // filter so the phantom `src/lib.rs` is never generated.
481        let lib_crates: std::collections::HashSet<ryo_symbol::CrateName> = existing_per_crate
482            .iter()
483            .filter_map(|(crate_name, keys)| {
484                if keys.contains("src/lib.rs") {
485                    Some(crate_name.clone())
486                } else {
487                    None
488                }
489            })
490            .collect();
491
492        let generator = RegistryGenerator::multi_file()
493            .with_existing_files_per_crate(existing_per_crate)
494            .with_lib_crates(lib_crates);
495        let workspace = generator.generate_affected(
496            &ctx.ast_registry,
497            ctx.registry(),
498            &modified_symbol_ids,
499            &metadata,
500        )?;
501
502        debug!(
503            "Generator produced {} crates with {} total files",
504            workspace.crates.len(),
505            workspace.total_files()
506        );
507
508        // Step 4: Convert generated files to WorkspaceFilePath and update context
509        // Generator returns crate-relative paths (e.g., "src/lib.rs")
510        // We need to resolve these to workspace-relative paths using CrateLayout
511        let mut modified_files = Vec::new();
512
513        for generated_crate in workspace.crates.values() {
514            debug!(
515                "Processing crate {} with {} files",
516                generated_crate.crate_name,
517                generated_crate.files.len()
518            );
519
520            // Get CrateLayout for this crate to convert paths correctly
521            use ryo_symbol::{CrateName, WorkspacePathResolver};
522            let crate_name = CrateName::new(&generated_crate.crate_name).expect(
523                "generator-emitted crate names must already satisfy CrateName validation; \
524                 reaching this expect means the generator is producing invalid names",
525            );
526            let layout = metadata.crate_layout(&crate_name);
527
528            for (crate_relative_path, generated_file) in &generated_crate.files {
529                // (R2: the defensive `src/lib.rs` skip guard that used to
530                // live here was replaced by the `RegistryGenerator::with_lib_crates`
531                // pre-flight filter above — the generator simply never
532                // emits a lib-style root for binary-only crates, so the
533                // post-hoc guard became dead code.)
534
535                // Convert crate-relative path to workspace-relative path using CrateLayout
536                let workspace_relative = match &layout {
537                    Some(layout) => layout.to_workspace_relative(crate_relative_path),
538                    None => {
539                        // Fallback: use crate-relative path as-is (single-crate workspace)
540                        std::path::PathBuf::from(crate_relative_path.as_str())
541                    }
542                };
543
544                // Find corresponding WorkspaceFilePath from context
545                // Match by crate_name and workspace-relative path
546                let workspace_file = ctx
547                    .files()
548                    .keys()
549                    .find(|wfp| {
550                        wfp.crate_name().as_str() == generated_crate.crate_name
551                            && wfp.as_relative() == workspace_relative.as_path()
552                    })
553                    .cloned();
554
555                let wfp = if let Some(existing) = workspace_file {
556                    debug!(
557                        "Updating existing file: {} ({})",
558                        crate_relative_path,
559                        existing.as_relative().display()
560                    );
561                    existing
562                } else {
563                    // New file: construct WorkspaceFilePath with correct workspace-relative path
564                    debug!(
565                        "Creating new file: {} -> {} for crate {}",
566                        crate_relative_path,
567                        workspace_relative.display(),
568                        generated_crate.crate_name
569                    );
570                    let resolver =
571                        WorkspacePathResolver::new(ctx.workspace_root.as_ref().to_path_buf());
572                    resolver.resolve_relative_with_crate(&workspace_relative, crate_name.clone())
573                };
574
575                let parsed = ryo_source::pure::PureFile::from_source(&generated_file.source);
576                let pure_file = parsed.unwrap_or_else(|_e| ryo_source::pure::PureFile::new());
577                ctx.files_mut().insert(wfp.clone(), Arc::new(pure_file));
578                modified_files.push(wfp);
579            }
580        }
581
582        debug!(
583            "Updated {} files in context from generator output",
584            modified_files.len()
585        );
586
587        // Step 5: Rebuild analysis graphs using symbol-based incremental update
588        if !all_events.is_empty() {
589            let affected_ids = collect_affected_ids(&all_events, ctx.registry());
590            debug!("Rebuilding with {} affected symbol IDs", affected_ids.len());
591            ctx.rebuild_after_mutation_by_symbols(&affected_ids);
592        } else if !modified_files.is_empty() {
593            // Fallback to file-based update if no events were collected
594            debug!(
595                "Rebuilding with {} modified files (fallback)",
596                modified_files.len()
597            );
598            ctx.rebuild_after_mutation(&modified_files);
599        }
600
601        info!(
602            "sync_files_and_rebuild completed: {} files modified",
603            modified_files.len()
604        );
605        Ok(modified_files)
606    }
607
608    /// Execute a single MutationSpec via ASTRegistry path.
609    ///
610    /// Uses convert_v2() when available, falls back to direct construction
611    /// for specs that haven't been migrated yet.
612    fn execute_spec_v2(
613        &self,
614        index: usize,
615        spec: &MutationSpec,
616        ctx: &mut AnalysisContext,
617    ) -> SpecResult {
618        use crate::engine::ASTMutationEngine;
619
620        let spec_type = spec_type_name(spec);
621        // Compute affected_symbols from spec targets
622        let affected_symbols: Vec<SymbolPath> = spec
623            .get_targets()
624            .iter()
625            .filter_map(|target| target.to_path(ctx.registry()))
626            .collect();
627
628        // Try V2 path first (convert_v2 → execute_ast_reg_batch_dyn)
629        match self.registry.convert_v2(spec, ctx) {
630            Ok(mutations) => {
631                let exec_result = ASTMutationEngine::execute_ast_reg_batch_dyn(mutations, ctx);
632
633                // For AddItem: register the newly added symbol for deferred resolution
634                // This allows later intents (AddDerive, AddVariant) to reference it by name
635                if let MutationSpec::AddItem {
636                    target, content, ..
637                } = spec
638                {
639                    // Only register if target is already resolved to a SymbolPath
640                    if let MutationTargetSymbol::ByPath(path) = target {
641                        register_item_from_content(ctx, path, content);
642                    } else if let MutationTargetSymbol::ById(id) = target {
643                        // Resolve SymbolId to SymbolPath and clone it
644                        if let Some(path) = ctx.registry().resolve(*id).cloned() {
645                            register_item_from_content(ctx, &path, content);
646                        }
647                    }
648                }
649
650                SpecResult {
651                    index,
652                    spec_type,
653                    success: true,
654                    changes: exec_result.result.changes,
655                    affected_files: vec![], // TODO: Extract from events
656                    affected_symbols,
657                    error: None,
658                    registry_updates: RegistryUpdateBatch::new(),
659                    events: exec_result.events,
660                }
661            }
662            Err(e) => SpecResult {
663                index,
664                spec_type,
665                success: false,
666                changes: 0,
667                affected_files: vec![],
668                affected_symbols,
669                error: Some(format!("convert_v2 failed: {}", e)),
670                registry_updates: RegistryUpdateBatch::new(),
671                events: vec![],
672            },
673        }
674    }
675
676    /// Set execution strategy
677    pub fn with_strategy(mut self, strategy: ExecutionStrategy) -> Self {
678        self.strategy = strategy;
679        self
680    }
681
682    /// Enable compile verification after each mutation
683    pub fn with_verify(mut self, verify: bool) -> Self {
684        self.verify_after_each = verify;
685        self
686    }
687
688    /// Stop execution on first error
689    pub fn with_stop_on_error(mut self, stop: bool) -> Self {
690        self.stop_on_error = stop;
691        self
692    }
693}
694
695/// Register a symbol from AddItem content for deferred resolution.
696///
697/// This allows later intents in a batch (e.g., AddDerive, AddVariant) to reference
698/// symbols created by earlier AddItem intents.
699fn register_item_from_content(
700    ctx: &mut AnalysisContext,
701    target: &ryo_symbol::SymbolPath,
702    content: &str,
703) {
704    use ryo_symbol::SymbolKind;
705
706    let trimmed = content.trim();
707
708    // Skip attributes and find the item declaration
709    let mut lines = trimmed.lines();
710    let mut decl_line = "";
711    for line in lines.by_ref() {
712        let line = line.trim();
713        if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
714            decl_line = line;
715            break;
716        }
717    }
718
719    // Parse: pub? (struct|enum|fn|type|const|static|trait|mod) NAME
720    let tokens: Vec<&str> = decl_line.split_whitespace().collect();
721    if tokens.is_empty() {
722        return;
723    }
724
725    let mut idx = 0;
726
727    // Skip visibility
728    if tokens.get(idx) == Some(&"pub") {
729        idx += 1;
730        // Skip pub(crate), pub(super), etc.
731        if let Some(t) = tokens.get(idx) {
732            if t.starts_with('(') {
733                idx += 1;
734            }
735        }
736    }
737
738    // Get keyword
739    let Some(keyword) = tokens.get(idx) else {
740        return;
741    };
742    idx += 1;
743
744    let kind = match *keyword {
745        "struct" => SymbolKind::Struct,
746        "enum" => SymbolKind::Enum,
747        "fn" => SymbolKind::Function,
748        "type" => SymbolKind::TypeAlias,
749        "const" => SymbolKind::Const,
750        "static" => SymbolKind::Static,
751        "trait" => SymbolKind::Trait,
752        "mod" => SymbolKind::Mod,
753        "impl" => return, // Skip impl blocks - they don't create new symbols
754        _ => return,
755    };
756
757    // Get name (may include generics like "Foo<T>")
758    let Some(name_token) = tokens.get(idx) else {
759        return;
760    };
761    let name = name_token
762        .split(['<', '{', '(', ':'])
763        .next()
764        .unwrap_or(name_token);
765
766    // Build full symbol path
767    let target_str = target.to_string();
768    let is_crate_root = target_str == "crate";
769    let full_path = if is_crate_root {
770        ryo_symbol::SymbolPath::parse(&format!("crate::{}", name))
771    } else {
772        ryo_symbol::SymbolPath::parse(&format!("{}::{}", target_str, name))
773    };
774
775    let Ok(path) = full_path else {
776        return;
777    };
778
779    // Register in registry (ignore errors - symbol may already exist)
780    let _ = ctx.registry_mut().register(path, kind);
781}
782
783/// Get a short type name for a MutationSpec
784fn spec_type_name(spec: &MutationSpec) -> String {
785    match spec {
786        MutationSpec::Rename { .. } => "Rename".to_string(),
787        MutationSpec::AddField { .. } => "AddField".to_string(),
788        MutationSpec::RemoveField { .. } => "RemoveField".to_string(),
789        MutationSpec::ChangeVisibility { .. } => "ChangeVisibility".to_string(),
790        MutationSpec::AddDerive { .. } => "AddDerive".to_string(),
791        MutationSpec::RemoveDerive { .. } => "RemoveDerive".to_string(),
792        MutationSpec::AddVariant { .. } => "AddVariant".to_string(),
793        MutationSpec::RemoveVariant { .. } => "RemoveVariant".to_string(),
794        MutationSpec::AddMatchArm { .. } => "AddMatchArm".to_string(),
795        MutationSpec::RemoveMatchArm { .. } => "RemoveMatchArm".to_string(),
796        MutationSpec::ReplaceMatchArm { .. } => "ReplaceMatchArm".to_string(),
797        MutationSpec::AddStructLiteralField { .. } => "AddStructLiteralField".to_string(),
798        MutationSpec::RemoveStructLiteralField { .. } => "RemoveStructLiteralField".to_string(),
799        MutationSpec::AddItem { .. } => "AddItem".to_string(),
800        MutationSpec::RemoveItem { .. } => "RemoveItem".to_string(),
801        MutationSpec::ReplaceCode { .. } => "ReplaceCode".to_string(),
802        MutationSpec::AddMethod { .. } => "AddMethod".to_string(),
803        MutationSpec::RemoveMethod { .. } => "RemoveMethod".to_string(),
804        MutationSpec::RemoveMod { .. } => "RemoveMod".to_string(),
805        MutationSpec::CreateMod { .. } => "CreateMod".to_string(),
806        MutationSpec::OrganizeImports { .. } => "OrganizeImports".to_string(),
807        MutationSpec::LoopToIterator { .. } => "LoopToIterator".to_string(),
808        MutationSpec::UnwrapToQuestion { .. } => "UnwrapToQuestion".to_string(),
809        MutationSpec::AddSpec { .. } => "AddSpec".to_string(),
810        MutationSpec::RemoveSpec { .. } => "RemoveSpec".to_string(),
811        MutationSpec::ValidateSpec { .. } => "ValidateSpec".to_string(),
812        MutationSpec::ExtractTrait { .. } => "ExtractTrait".to_string(),
813        MutationSpec::InlineTrait { .. } => "InlineTrait".to_string(),
814        MutationSpec::ReplaceType { .. } => "ReplaceType".to_string(),
815        MutationSpec::EnumToTrait { .. } => "EnumToTrait".to_string(),
816        MutationSpec::MoveItem { .. } => "MoveItem".to_string(),
817        MutationSpec::AssignOp { .. } => "AssignOp".to_string(),
818        MutationSpec::BoolSimplify { .. } => "BoolSimplify".to_string(),
819        MutationSpec::CloneOnCopy { .. } => "CloneOnCopy".to_string(),
820        MutationSpec::CollapsibleIf { .. } => "CollapsibleIf".to_string(),
821        MutationSpec::NoOpArmToTodo { .. } => "NoOpArmToTodo".to_string(),
822        MutationSpec::ComparisonToMethod { .. } => "ComparisonToMethod".to_string(),
823        MutationSpec::RedundantClosure { .. } => "RedundantClosure".to_string(),
824        MutationSpec::IntroduceVariable { .. } => "IntroduceVariable".to_string(),
825        MutationSpec::ManualMap { .. } => "ManualMap".to_string(),
826        MutationSpec::MatchToIfLet { .. } => "MatchToIfLet".to_string(),
827        MutationSpec::FilterNext { .. } => "FilterNext".to_string(),
828        MutationSpec::MapUnwrapOr { .. } => "MapUnwrapOr".to_string(),
829        MutationSpec::ReplaceExpr { .. } => "ReplaceExpr".to_string(),
830        MutationSpec::RemoveStatement { .. } => "RemoveStatement".to_string(),
831        MutationSpec::InsertStatement { .. } => "InsertStatement".to_string(),
832        MutationSpec::ReplaceStatement { .. } => "ReplaceStatement".to_string(),
833        MutationSpec::PluginTransform { .. } => "PluginTransform".to_string(),
834        MutationSpec::DuplicateFunction { .. } => "DuplicateFunction".to_string(),
835        MutationSpec::DuplicateStruct { .. } => "DuplicateStruct".to_string(),
836        MutationSpec::DuplicateEnum { .. } => "DuplicateEnum".to_string(),
837        MutationSpec::DuplicateModTree { .. } => "DuplicateModTree".to_string(),
838    }
839}
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844    use crate::executor::spec::{Scope, SelfParam, SymbolPath, Visibility};
845    use ryo_analysis::testing::{ContextBuilder, ContextTestExt};
846    use ryo_symbol::SymbolId;
847
848    /// Create a dummy SymbolId for testing
849    fn dummy_id(index: u32) -> SymbolId {
850        SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
851    }
852
853    /// Helper function that executes a blueprint and syncs files.
854    /// This is the standard pattern for tests that need to read from ctx.files() after execution.
855    fn execute_and_sync(
856        executor: &BlueprintExecutor,
857        blueprint: &ParallelBlueprint,
858        ctx: &mut AnalysisContext,
859    ) -> BlueprintResult {
860        let result = executor.execute_v2(blueprint, ctx);
861        if result.success {
862            BlueprintExecutor::sync_files_and_rebuild(&result, ctx).unwrap();
863        }
864        result
865    }
866
867    fn create_test_context() -> AnalysisContext {
868        let code = r#"
869struct Config {
870    name: String,
871}
872
873impl Config {
874    fn new() -> Self {
875        Self { name: String::new() }
876    }
877}
878"#;
879        ContextBuilder::new()
880            .with_file("src/config.rs", code)
881            .build()
882    }
883
884    #[test]
885    fn test_blueprint_executor_rename() {
886        let mut ctx = create_test_context();
887
888        // Look up the symbol_id for Config from the context's registry
889        let symbol_id = ctx
890            .registry()
891            .lookup_by_name("Config")
892            .expect("Config should exist in registry");
893
894        let specs = vec![MutationSpec::Rename {
895            target: MutationTargetSymbol::ById(symbol_id),
896            to: "AppConfig".to_string(),
897            scope: Scope::Project,
898        }];
899        let blueprint = ParallelBlueprint::from_mutations(specs);
900
901        let executor = BlueprintExecutor::new();
902        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
903
904        assert!(exec_result.success);
905        assert!(exec_result.total_changes > 0);
906
907        // Verify the rename happened
908        let file = ctx.test_file("src/config.rs").unwrap();
909        let source = file.to_source().unwrap();
910        assert!(source.contains("AppConfig"));
911        assert!(!source.contains("struct Config"));
912    }
913
914    #[test]
915    fn test_blueprint_executor_add_derive() {
916        let mut ctx = create_test_context();
917
918        // Lookup the actual SymbolId (file is src/config.rs, so path is crate::config::Config)
919        let path = SymbolPath::parse("test_crate::config::Config").unwrap();
920        let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
921
922        let specs = vec![MutationSpec::AddDerive {
923            target: MutationTargetSymbol::ById(symbol_id),
924            derives: vec!["Debug".to_string(), "Clone".to_string()],
925        }];
926        let blueprint = ParallelBlueprint::from_mutations(specs);
927
928        let executor = BlueprintExecutor::new();
929        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
930
931        assert!(exec_result.success);
932
933        let file = ctx.test_file("src/config.rs").unwrap();
934        let source = file.to_source().unwrap();
935        assert!(source.contains("derive"), "Expected derive in: {}", source);
936        assert!(source.contains("Debug"));
937    }
938
939    #[test]
940    fn test_blueprint_executor_organize_imports() {
941        let code = r#"
942use std::collections::HashMap;
943use std::io::Write;
944use std::collections::HashSet;
945use std::io::Read;
946
947fn main() {}
948"#;
949        let mut ctx = ContextBuilder::new().with_file("src/main.rs", code).build();
950
951        let specs = vec![MutationSpec::OrganizeImports {
952            module_id: None,
953            deduplicate: true,
954            merge_groups: true,
955        }];
956        let blueprint = ParallelBlueprint::from_mutations(specs);
957
958        let executor = BlueprintExecutor::new();
959        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
960
961        assert!(exec_result.success);
962    }
963
964    #[test]
965    fn test_blueprint_with_conflicts_fails_when_not_ignored() {
966        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
967
968        let mut ctx = create_test_context();
969
970        // Create dummy symbol IDs for testing actual conflicts
971        let mut symbol_registry = SymbolRegistry::new();
972        let path_a = SymbolPath::parse("test_crate::A").unwrap();
973        let symbol_a = symbol_registry
974            .register(path_a, SymbolKind::Struct)
975            .unwrap();
976
977        // Create blueprint with actual conflicts (same target renamed twice)
978        let specs = vec![
979            MutationSpec::Rename {
980                target: MutationTargetSymbol::ById(symbol_a),
981                to: "B".to_string(),
982                scope: Scope::Project,
983            },
984            MutationSpec::Rename {
985                target: MutationTargetSymbol::ById(symbol_a),
986                to: "C".to_string(),
987                scope: Scope::Project,
988            },
989        ];
990
991        let blueprint = ParallelBlueprint::from_mutations(specs);
992
993        // Verify blueprint has conflicts
994        assert!(
995            !blueprint.conflicts.is_empty(),
996            "Blueprint should have conflicts"
997        );
998
999        // With ignore_conflicts = false, conflicts should fail
1000        let mut executor = BlueprintExecutor::new();
1001        executor.ignore_conflicts = false;
1002        let result = execute_and_sync(&executor, &blueprint, &mut ctx);
1003
1004        // Should fail because of conflicts
1005        assert!(
1006            !result.success,
1007            "Execution should fail when conflicts are not ignored"
1008        );
1009        assert!(result.error.is_some(), "Should have error message");
1010        assert!(
1011            result.error.unwrap().contains("conflict"),
1012            "Error should mention conflicts"
1013        );
1014    }
1015
1016    // Note: Intent-based tests have been removed as they use deprecated ryo_core::Intent.
1017    // Converter-level tests in registry/converters/ provide comprehensive coverage.
1018    // TODO: Add MutationSpec-based integration tests if needed.
1019
1020    // === AddItem Tests ===
1021
1022    #[test]
1023    fn test_blueprint_executor_add_item_struct() {
1024        let mut ctx = ContextBuilder::new()
1025            .with_file("src/lib.rs", "// empty file\n")
1026            .build();
1027
1028        let spec = MutationSpec::AddItem {
1029            target: MutationTargetSymbol::ByPath(Box::new(
1030                SymbolPath::parse("test_crate").unwrap(),
1031            )),
1032            content: "pub struct Config {}".to_string(),
1033            position: super::super::spec::InsertPosition::Bottom,
1034        };
1035
1036        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1037        let executor = BlueprintExecutor::new();
1038        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1039
1040        assert!(
1041            exec_result.success,
1042            "AddItem struct failed: {:?}",
1043            exec_result.error
1044        );
1045
1046        let file = ctx.test_file("src/lib.rs").unwrap();
1047        let source = file.to_source().unwrap();
1048        assert!(
1049            source.contains("pub struct Config"),
1050            "Struct not added: {}",
1051            source
1052        );
1053    }
1054
1055    #[test]
1056    fn test_blueprint_executor_add_item_fn() {
1057        let mut ctx = ContextBuilder::new()
1058            .with_file("src/lib.rs", "// empty file\n")
1059            .build();
1060
1061        let spec = MutationSpec::AddItem {
1062            target: MutationTargetSymbol::ByPath(Box::new(
1063                SymbolPath::parse("test_crate").unwrap(),
1064            )),
1065            content: "fn helper() {}".to_string(),
1066            position: super::super::spec::InsertPosition::Bottom,
1067        };
1068
1069        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1070        let executor = BlueprintExecutor::new();
1071        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1072
1073        assert!(
1074            exec_result.success,
1075            "AddItem fn failed: {:?}",
1076            exec_result.error
1077        );
1078
1079        let file = ctx.test_file("src/lib.rs").unwrap();
1080        let source = file.to_source().unwrap();
1081        assert!(
1082            source.contains("fn helper"),
1083            "Function not added: {}",
1084            source
1085        );
1086    }
1087
1088    #[test]
1089    fn test_blueprint_executor_add_item_use() {
1090        let mut ctx = ContextBuilder::new()
1091            .with_file("src/lib.rs", "pub struct Dummy;\n")
1092            .build();
1093
1094        let spec = MutationSpec::AddItem {
1095            target: MutationTargetSymbol::ByPath(Box::new(
1096                SymbolPath::parse("test_crate").unwrap(),
1097            )),
1098            content: "use HashMap;".to_string(),
1099            position: super::super::spec::InsertPosition::Top,
1100        };
1101
1102        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1103        let executor = BlueprintExecutor::new();
1104        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1105
1106        assert!(
1107            exec_result.success,
1108            "AddItem use failed: {:?}",
1109            exec_result.error
1110        );
1111
1112        let file = ctx.test_file("src/lib.rs").unwrap();
1113        let source = file.to_source().unwrap();
1114        assert!(source.contains("use HashMap"), "Use not added: {}", source);
1115    }
1116
1117    #[test]
1118    fn test_blueprint_executor_add_item_impl() {
1119        let mut ctx = ContextBuilder::new()
1120            .with_file("src/lib.rs", "struct Foo {}\n")
1121            .build();
1122
1123        let spec = MutationSpec::AddItem {
1124            target: MutationTargetSymbol::ByPath(Box::new(
1125                SymbolPath::parse("test_crate").unwrap(),
1126            )),
1127            content: "impl Foo {}".to_string(),
1128            position: super::super::spec::InsertPosition::Bottom,
1129        };
1130
1131        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1132        let executor = BlueprintExecutor::new();
1133        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1134
1135        assert!(
1136            exec_result.success,
1137            "AddItem impl failed: {:?}",
1138            exec_result.error
1139        );
1140
1141        let file = ctx.test_file("src/lib.rs").unwrap();
1142        let source = file.to_source().unwrap();
1143        assert!(source.contains("impl Foo"), "Impl not added: {}", source);
1144    }
1145
1146    #[test]
1147    fn test_blueprint_executor_add_item_enum() {
1148        let mut ctx = ContextBuilder::new()
1149            .with_file("src/lib.rs", "// empty file\n")
1150            .build();
1151
1152        let spec = MutationSpec::AddItem {
1153            target: MutationTargetSymbol::ByPath(Box::new(
1154                SymbolPath::parse("test_crate").unwrap(),
1155            )),
1156            content: "pub enum Status { Pending, Active, Completed }".to_string(),
1157            position: super::super::spec::InsertPosition::Bottom,
1158        };
1159
1160        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1161        let executor = BlueprintExecutor::new();
1162        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1163
1164        assert!(
1165            exec_result.success,
1166            "AddItem enum failed: {:?}",
1167            exec_result.error
1168        );
1169
1170        let file = ctx.test_file("src/lib.rs").unwrap();
1171        let source = file.to_source().unwrap();
1172        assert!(
1173            source.contains("pub enum Status"),
1174            "Enum not added: {}",
1175            source
1176        );
1177    }
1178
1179    // === AddMethod Tests ===
1180
1181    #[test]
1182    fn test_blueprint_executor_add_method_basic() {
1183        let mut ctx = ContextBuilder::new()
1184            .with_file("src/lib.rs", "pub struct Config {}\n\nimpl Config {}\n")
1185            .build();
1186
1187        let spec = MutationSpec::AddMethod {
1188            target: MutationTargetSymbol::ByKindAndName(
1189                crate::executor::ItemKind::Impl,
1190                "Config".to_string(),
1191            ),
1192            method_name: "new".to_string(),
1193            params: vec![],
1194            return_type: Some("Self".to_string()),
1195            body: "Self {}".to_string(),
1196            is_pub: true,
1197            self_param: None,
1198        };
1199
1200        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1201        let executor = BlueprintExecutor::new();
1202        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1203
1204        assert!(
1205            exec_result.success,
1206            "AddMethod basic failed: {:?}",
1207            exec_result.error
1208        );
1209
1210        let file = ctx.test_file("src/lib.rs").unwrap();
1211        let source = file.to_source().unwrap();
1212        assert!(
1213            source.contains("pub fn new"),
1214            "Method not added: {}",
1215            source
1216        );
1217        assert!(
1218            source.contains("-> Self"),
1219            "Return type not added: {}",
1220            source
1221        );
1222    }
1223
1224    #[test]
1225    fn test_blueprint_executor_add_method_with_self() {
1226        let mut ctx = ContextBuilder::new()
1227            .with_file(
1228                "src/lib.rs",
1229                "pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
1230            )
1231            .build();
1232
1233        let spec = MutationSpec::AddMethod {
1234            target: MutationTargetSymbol::ByKindAndName(
1235                crate::executor::ItemKind::Impl,
1236                "Counter".to_string(),
1237            ),
1238            method_name: "get".to_string(),
1239            params: vec![],
1240            return_type: Some("u32".to_string()),
1241            body: "self.value".to_string(),
1242            is_pub: true,
1243            self_param: Some(SelfParam::Ref),
1244        };
1245
1246        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1247        let executor = BlueprintExecutor::new();
1248        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1249
1250        assert!(
1251            exec_result.success,
1252            "AddMethod with_self failed: {:?}",
1253            exec_result.error
1254        );
1255
1256        let file = ctx.test_file("src/lib.rs").unwrap();
1257        let source = file.to_source().unwrap();
1258        assert!(source.contains("&self"), "Self param not added: {}", source);
1259        assert!(source.contains("fn get"), "Method not added: {}", source);
1260    }
1261
1262    #[test]
1263    fn test_blueprint_executor_add_method_with_mut_self() {
1264        let mut ctx = ContextBuilder::new()
1265            .with_file(
1266                "src/lib.rs",
1267                "pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
1268            )
1269            .build();
1270
1271        let spec = MutationSpec::AddMethod {
1272            target: MutationTargetSymbol::ByKindAndName(
1273                crate::executor::ItemKind::Impl,
1274                "Counter".to_string(),
1275            ),
1276            method_name: "increment".to_string(),
1277            params: vec![],
1278            return_type: None,
1279            body: "self.value += 1".to_string(),
1280            is_pub: true,
1281            self_param: Some(SelfParam::Mut),
1282        };
1283
1284        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1285        let executor = BlueprintExecutor::new();
1286        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1287
1288        assert!(
1289            exec_result.success,
1290            "AddMethod with_mut_self failed: {:?}",
1291            exec_result.error
1292        );
1293
1294        let file = ctx.test_file("src/lib.rs").unwrap();
1295        let source = file.to_source().unwrap();
1296        assert!(
1297            source.contains("&mut self"),
1298            "Mut self param not added: {}",
1299            source
1300        );
1301        assert!(
1302            source.contains("fn increment"),
1303            "Method not added: {}",
1304            source
1305        );
1306    }
1307
1308    #[test]
1309    fn test_blueprint_executor_add_method_with_params() {
1310        let mut ctx = ContextBuilder::new()
1311            .with_file(
1312                "src/lib.rs",
1313                "pub struct Calculator {}\n\nimpl Calculator {}\n",
1314            )
1315            .build();
1316
1317        let spec = MutationSpec::AddMethod {
1318            target: MutationTargetSymbol::ByKindAndName(
1319                crate::executor::ItemKind::Impl,
1320                "Calculator".to_string(),
1321            ),
1322            method_name: "add".to_string(),
1323            params: vec![
1324                ("a".to_string(), "i32".to_string()),
1325                ("b".to_string(), "i32".to_string()),
1326            ],
1327            return_type: Some("i32".to_string()),
1328            body: "a + b".to_string(),
1329            is_pub: true,
1330            self_param: None,
1331        };
1332
1333        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1334        let executor = BlueprintExecutor::new();
1335        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1336
1337        assert!(
1338            exec_result.success,
1339            "AddMethod with_params failed: {:?}",
1340            exec_result.error
1341        );
1342
1343        let file = ctx.test_file("src/lib.rs").unwrap();
1344        let source = file.to_source().unwrap();
1345        assert!(source.contains("fn add"), "Method not added: {}", source);
1346        assert!(source.contains("a: i32"), "Param a not added: {}", source);
1347        assert!(source.contains("b: i32"), "Param b not added: {}", source);
1348    }
1349
1350    // === Multi-file Tests ===
1351
1352    #[test]
1353    fn test_blueprint_executor_multi_file_rename() {
1354        let mut ctx = ContextBuilder::new()
1355            .with_file(
1356                "src/lib.rs",
1357                r#"mod models;
1358
1359fn process(task: Task) -> Task {
1360    task
1361}
1362"#,
1363            )
1364            .with_file(
1365                "src/models.rs",
1366                r#"pub struct Task {
1367    pub id: u32,
1368    pub name: String,
1369}
1370"#,
1371            )
1372            .build();
1373
1374        // Look up the symbol_id for Task from the context's registry
1375        let symbol_id = ctx
1376            .registry()
1377            .lookup_by_name("Task")
1378            .expect("Task should exist in registry");
1379
1380        // Rename Task to TodoItem across all files
1381        let spec = MutationSpec::Rename {
1382            target: MutationTargetSymbol::ById(symbol_id),
1383            to: "TodoItem".to_string(),
1384            scope: Scope::Project,
1385        };
1386
1387        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1388        let executor = BlueprintExecutor::new();
1389        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1390
1391        assert!(
1392            exec_result.success,
1393            "Multi-file rename failed: {:?}",
1394            exec_result.error
1395        );
1396
1397        // Check lib.rs type annotations were renamed
1398        let lib = ctx.test_file("src/lib.rs").unwrap();
1399        let lib_source = lib.to_source().unwrap();
1400        assert!(
1401            lib_source.contains("task: TodoItem"),
1402            "lib.rs type not renamed: {}",
1403            lib_source
1404        );
1405        assert!(
1406            lib_source.contains("-> TodoItem"),
1407            "lib.rs return type not renamed: {}",
1408            lib_source
1409        );
1410
1411        // Check models.rs struct was renamed
1412        let models = ctx.test_file("src/models.rs").unwrap();
1413        let models_source = models.to_source().unwrap();
1414        assert!(
1415            models_source.contains("struct TodoItem"),
1416            "models.rs struct not renamed: {}",
1417            models_source
1418        );
1419        assert!(
1420            !models_source.contains("struct Task"),
1421            "models.rs still has struct Task: {}",
1422            models_source
1423        );
1424    }
1425
1426    #[test]
1427    fn test_blueprint_executor_multi_file_add_items() {
1428        let mut ctx = ContextBuilder::new()
1429            .with_file("src/lib.rs", "pub mod models;\n")
1430            .with_file("src/models.rs", "pub struct Placeholder;\n")
1431            .build();
1432
1433        // Add struct to models.rs
1434        let spec1 = MutationSpec::AddItem {
1435            target: MutationTargetSymbol::ByPath(Box::new(
1436                SymbolPath::parse("test_crate::models").unwrap(),
1437            )),
1438            content: "pub struct User { id: u32 }".to_string(),
1439            position: super::super::spec::InsertPosition::Bottom,
1440        };
1441
1442        // Add use to lib.rs
1443        let spec2 = MutationSpec::AddItem {
1444            target: MutationTargetSymbol::ByPath(Box::new(
1445                SymbolPath::parse("test_crate").unwrap(),
1446            )),
1447            content: "use models::User;".to_string(),
1448            position: super::super::spec::InsertPosition::Bottom,
1449        };
1450
1451        let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
1452        let executor = BlueprintExecutor::new();
1453        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1454
1455        assert!(
1456            exec_result.success,
1457            "Multi-file add items failed: {:?}",
1458            exec_result.error
1459        );
1460
1461        // Check models.rs has User struct
1462        let models = ctx.test_file("src/models.rs").unwrap();
1463        let models_source = models.to_source().unwrap();
1464        assert!(
1465            models_source.contains("pub struct User"),
1466            "User not added to models.rs: {}",
1467            models_source
1468        );
1469
1470        // Check lib.rs has use statement
1471        let lib = ctx.test_file("src/lib.rs").unwrap();
1472        let lib_source = lib.to_source().unwrap();
1473        assert!(
1474            lib_source.contains("use models::User"),
1475            "Use not added to lib.rs: {}",
1476            lib_source
1477        );
1478    }
1479
1480    // === Generic and Async Tests ===
1481
1482    #[test]
1483    fn test_blueprint_executor_add_item_generic_struct() {
1484        let mut ctx = ContextBuilder::new()
1485            .with_file("src/lib.rs", "// empty file\n")
1486            .build();
1487
1488        let spec = MutationSpec::AddItem {
1489            target: MutationTargetSymbol::ByPath(Box::new(
1490                SymbolPath::parse("test_crate").unwrap(),
1491            )),
1492            content: "pub struct Container<T> { value: T }".to_string(),
1493            position: super::super::spec::InsertPosition::Bottom,
1494        };
1495
1496        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1497        let executor = BlueprintExecutor::new();
1498        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1499
1500        assert!(
1501            exec_result.success,
1502            "AddItem generic struct failed: {:?}",
1503            exec_result.error
1504        );
1505
1506        let file = ctx.test_file("src/lib.rs").unwrap();
1507        let source = file.to_source().unwrap();
1508        assert!(
1509            source.contains("struct Container"),
1510            "Generic struct not added: {}",
1511            source
1512        );
1513    }
1514
1515    #[test]
1516    fn test_blueprint_executor_add_item_async_fn() {
1517        let mut ctx = ContextBuilder::new()
1518            .with_file("src/lib.rs", "// empty file\n")
1519            .build();
1520
1521        let spec = MutationSpec::AddItem {
1522            target: MutationTargetSymbol::ByPath(Box::new(
1523                SymbolPath::parse("test_crate").unwrap(),
1524            )),
1525            content: "pub async fn fetch_data() -> String { String::new() }".to_string(),
1526            position: super::super::spec::InsertPosition::Bottom,
1527        };
1528
1529        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1530        let executor = BlueprintExecutor::new();
1531        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1532
1533        assert!(
1534            exec_result.success,
1535            "AddItem async fn failed: {:?}",
1536            exec_result.error
1537        );
1538
1539        let file = ctx.test_file("src/lib.rs").unwrap();
1540        let source = file.to_source().unwrap();
1541        assert!(
1542            source.contains("fn fetch_data"),
1543            "Async fn not added: {}",
1544            source
1545        );
1546    }
1547
1548    #[test]
1549    fn test_blueprint_executor_add_field_to_generic_struct() {
1550        use ryo_analysis::SymbolKind;
1551
1552        let mut ctx = ContextBuilder::new()
1553            .with_file("src/lib.rs", "pub struct Wrapper<T> { inner: T }\n")
1554            .build();
1555
1556        // Get symbol_id for Wrapper struct
1557        let symbol_id = ctx
1558            .registry
1559            .iter()
1560            .find(|(id, path)| {
1561                path.name() == "Wrapper" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
1562            })
1563            .map(|(id, _)| id)
1564            .expect("Wrapper struct not found in registry");
1565
1566        let spec = MutationSpec::AddField {
1567            target: MutationTargetSymbol::ById(symbol_id),
1568            field_name: "count".to_string(),
1569            field_type: "usize".to_string(),
1570            visibility: Visibility::Pub,
1571        };
1572
1573        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1574        let executor = BlueprintExecutor::new();
1575        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1576
1577        assert!(
1578            exec_result.success,
1579            "AddField to generic struct failed: {:?}",
1580            exec_result.error
1581        );
1582
1583        let file = ctx.test_file("src/lib.rs").unwrap();
1584        let source = file.to_source().unwrap();
1585        assert!(
1586            source.contains("pub count: usize"),
1587            "Field not added to generic struct: {}",
1588            source
1589        );
1590    }
1591
1592    #[test]
1593    fn test_blueprint_executor_add_derive_to_generic_struct() {
1594        let mut ctx = ContextBuilder::new()
1595            .with_file(
1596                "src/lib.rs",
1597                "pub struct Pair<T, U> { first: T, second: U }\n",
1598            )
1599            .build();
1600
1601        // Lookup the actual SymbolId
1602        let path = SymbolPath::parse("test_crate::Pair").unwrap();
1603        let symbol_id = ctx.registry().lookup(&path).expect("Pair should exist");
1604
1605        let spec = MutationSpec::AddDerive {
1606            target: MutationTargetSymbol::ById(symbol_id),
1607            derives: vec!["Debug".to_string(), "Clone".to_string()],
1608        };
1609
1610        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1611        let executor = BlueprintExecutor::new();
1612        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1613
1614        assert!(
1615            exec_result.success,
1616            "AddDerive to generic struct failed: {:?}",
1617            exec_result.error
1618        );
1619
1620        let file = ctx.test_file("src/lib.rs").unwrap();
1621        let source = file.to_source().unwrap();
1622        assert!(
1623            source.contains("Debug"),
1624            "Debug derive not added: {}",
1625            source
1626        );
1627        assert!(
1628            source.contains("Clone"),
1629            "Clone derive not added: {}",
1630            source
1631        );
1632    }
1633
1634    // === Module Tests ===
1635    // Note: AddMod was consolidated into CreateMod. These tests use CreateMod with empty content
1636    // to test the mod declaration functionality.
1637
1638    #[test]
1639    fn test_blueprint_executor_create_mod_declaration() {
1640        let mut ctx = ContextBuilder::new()
1641            .with_file("src/lib.rs", "use std::io;\n\nfn main() {}\n")
1642            .build();
1643
1644        let spec = MutationSpec::CreateMod {
1645            target: MutationTargetSymbol::ByPath(Box::new(
1646                SymbolPath::parse("test_crate").unwrap(),
1647            )),
1648            mod_name: "models".to_string(),
1649            content: String::new(),
1650            is_pub: false,
1651        };
1652
1653        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1654        let executor = BlueprintExecutor::new();
1655        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1656
1657        assert!(
1658            exec_result.success,
1659            "CreateMod failed: {:?}",
1660            exec_result.error
1661        );
1662
1663        let file = ctx.test_file("src/lib.rs").unwrap();
1664        let source = file.to_source().unwrap();
1665        assert!(source.contains("mod models;"), "Mod not added: {}", source);
1666    }
1667
1668    #[test]
1669    fn test_blueprint_executor_create_pub_mod_declaration() {
1670        let mut ctx = ContextBuilder::new()
1671            .with_file("src/lib.rs", "fn main() {}\n")
1672            .build();
1673
1674        let spec = MutationSpec::CreateMod {
1675            target: MutationTargetSymbol::ByPath(Box::new(
1676                SymbolPath::parse("test_crate").unwrap(),
1677            )),
1678            mod_name: "api".to_string(),
1679            content: String::new(),
1680            is_pub: true,
1681        };
1682
1683        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1684        let executor = BlueprintExecutor::new();
1685        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1686
1687        assert!(
1688            exec_result.success,
1689            "CreateMod pub failed: {:?}",
1690            exec_result.error
1691        );
1692
1693        let file = ctx.test_file("src/lib.rs").unwrap();
1694        let source = file.to_source().unwrap();
1695        assert!(
1696            source.contains("pub mod api;"),
1697            "Pub mod not added: {}",
1698            source
1699        );
1700    }
1701
1702    #[test]
1703    fn test_blueprint_executor_create_file() {
1704        // NOTE: Don't pre-declare "mod models;" - CreateMod will add both the declaration and content
1705        let mut ctx = ContextBuilder::new()
1706            .with_file("src/lib.rs", "// lib.rs\n")
1707            .build();
1708
1709        let spec = MutationSpec::CreateMod {
1710            target: MutationTargetSymbol::ByPath(Box::new(
1711                SymbolPath::parse("test_crate").unwrap(),
1712            )),
1713            mod_name: "models".to_string(),
1714            content: "pub struct Model { id: u32 }".to_string(),
1715            is_pub: true,
1716        };
1717
1718        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1719        let executor = BlueprintExecutor::new();
1720        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1721
1722        assert!(
1723            exec_result.success,
1724            "CreateFile failed: {:?}",
1725            exec_result.error
1726        );
1727
1728        // Check file was created (file style: src/models.rs - Ryo uses file-style modules by default)
1729        assert!(ctx.test_file("src/models.rs").is_some(), "File not created");
1730
1731        let file = ctx.test_file("src/models.rs").unwrap();
1732        let source = file.to_source().unwrap();
1733        assert!(
1734            source.contains("struct Model"),
1735            "Content not correct: {}",
1736            source
1737        );
1738    }
1739
1740    #[test]
1741    fn test_blueprint_executor_create_module_workflow() {
1742        let mut ctx = ContextBuilder::new()
1743            .with_file("src/lib.rs", "fn main() {}\n")
1744            .build();
1745
1746        // CreateMod handles both mod declaration and file creation
1747        let spec = MutationSpec::CreateMod {
1748            target: MutationTargetSymbol::ByPath(Box::new(
1749                SymbolPath::parse("test_crate").unwrap(),
1750            )),
1751            mod_name: "utils".to_string(),
1752            content: "pub fn helper() {}".to_string(),
1753            is_pub: true,
1754        };
1755
1756        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1757        let executor = BlueprintExecutor::new();
1758        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1759
1760        assert!(
1761            exec_result.success,
1762            "Module workflow failed: {:?}",
1763            exec_result.error
1764        );
1765
1766        // Check lib.rs has mod declaration
1767        let lib = ctx.test_file("src/lib.rs").unwrap();
1768        let lib_source = lib.to_source().unwrap();
1769        assert!(
1770            lib_source.contains("pub mod utils;"),
1771            "Mod not added to lib: {}",
1772            lib_source
1773        );
1774
1775        // Check utils file exists and has function (file style: src/utils.rs)
1776        let utils = ctx.test_file("src/utils.rs").unwrap();
1777        let utils_source = utils.to_source().unwrap();
1778        assert!(
1779            utils_source.contains("fn helper"),
1780            "Function not in utils: {}",
1781            utils_source
1782        );
1783    }
1784
1785    // === Wavefront Execution Tests ===
1786
1787    #[test]
1788    fn test_wavefront_execution_basic() {
1789        let mut ctx = create_test_context();
1790
1791        // Lookup the actual SymbolId (file is src/config.rs, so path is crate::config::Config)
1792        let path = SymbolPath::parse("test_crate::config::Config").unwrap();
1793        let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
1794
1795        let specs = vec![MutationSpec::AddDerive {
1796            target: MutationTargetSymbol::ById(symbol_id),
1797            derives: vec!["Debug".to_string()],
1798        }];
1799        let blueprint = ParallelBlueprint::from_mutations(specs);
1800
1801        let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
1802        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1803
1804        assert!(
1805            exec_result.success,
1806            "Wavefront basic failed: {:?}",
1807            exec_result.error
1808        );
1809        assert!(exec_result.total_changes > 0);
1810
1811        let file = ctx.test_file("src/config.rs").unwrap();
1812        let source = file.to_source().unwrap();
1813        assert!(source.contains("Debug"), "Derive not added: {}", source);
1814    }
1815
1816    #[test]
1817    fn test_wavefront_execution_multi_file() {
1818        // Production-realistic fixture: the parent file (`src/lib.rs`)
1819        // must declare its child module (`pub mod models;`) so the
1820        // analysis context picks `models` up with its real
1821        // visibility via `collect_from_item`. Omitting the
1822        // declaration here previously worked only because
1823        // `collect_from_file` registered the child's mod-symbol
1824        // itself — a redundant push that races against the parent's
1825        // record and drove the lib.rs `pub mod X;` → `mod X;` diff
1826        // surfaced by `rl061_tempfile_diff_observe`.
1827        let mut ctx = ContextBuilder::new()
1828            .with_file("src/lib.rs", "pub mod models;\n")
1829            .with_file("src/models.rs", "// models\n")
1830            .build();
1831
1832        // Add items to different files (can run in parallel)
1833        let spec1 = MutationSpec::AddItem {
1834            target: MutationTargetSymbol::ByPath(Box::new(
1835                SymbolPath::parse("test_crate::models").unwrap(),
1836            )),
1837            content: "pub struct User { id: u32 }".to_string(),
1838            position: super::super::spec::InsertPosition::Bottom,
1839        };
1840
1841        let spec2 = MutationSpec::AddItem {
1842            target: MutationTargetSymbol::ByPath(Box::new(
1843                SymbolPath::parse("test_crate").unwrap(),
1844            )),
1845            content: "fn main() {}".to_string(),
1846            position: super::super::spec::InsertPosition::Bottom,
1847        };
1848
1849        let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
1850        let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
1851        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1852
1853        assert!(
1854            exec_result.success,
1855            "Wavefront multi-file failed: {:?}",
1856            exec_result.error
1857        );
1858
1859        let models = ctx.test_file("src/models.rs").unwrap();
1860        assert!(
1861            models.to_source().unwrap().contains("pub struct User"),
1862            "User not added to models.rs"
1863        );
1864
1865        let lib = ctx.test_file("src/lib.rs").unwrap();
1866        assert!(
1867            lib.to_source().unwrap().contains("fn main"),
1868            "main not added to lib.rs"
1869        );
1870    }
1871
1872    #[test]
1873    fn test_wavefront_execution_with_dependencies() {
1874        let mut ctx = ContextBuilder::new()
1875            .with_file("src/lib.rs", "fn main() {}\n")
1876            .build();
1877
1878        // CreateMod handles both mod declaration and file creation
1879        let spec = MutationSpec::CreateMod {
1880            target: MutationTargetSymbol::ByPath(Box::new(
1881                SymbolPath::parse("test_crate").unwrap(),
1882            )),
1883            mod_name: "api".to_string(),
1884            content: "pub fn endpoint() {}".to_string(),
1885            is_pub: true,
1886        };
1887
1888        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1889        let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
1890        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1891
1892        assert!(
1893            exec_result.success,
1894            "Wavefront with deps failed: {:?}",
1895            exec_result.error
1896        );
1897
1898        // Verify dependency order was respected
1899        let lib = ctx.test_file("src/lib.rs").unwrap();
1900        assert!(
1901            lib.to_source().unwrap().contains("pub mod api;"),
1902            "Mod not added to lib"
1903        );
1904
1905        // Check api file (file style: src/api.rs)
1906        let api = ctx.test_file("src/api.rs").unwrap();
1907        assert!(
1908            api.to_source().unwrap().contains("fn endpoint"),
1909            "Function not in api"
1910        );
1911    }
1912
1913    #[test]
1914    fn test_suggest_strategy() {
1915        // Single mutation → Sequential
1916        let specs = vec![MutationSpec::AddDerive {
1917            target: MutationTargetSymbol::ById(dummy_id(1)),
1918            derives: vec!["Debug".to_string()],
1919        }];
1920        let blueprint = ParallelBlueprint::from_mutations(specs);
1921        assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
1922
1923        // Two mutations → Sequential (too few)
1924        let specs = vec![
1925            MutationSpec::AddDerive {
1926                target: MutationTargetSymbol::ById(dummy_id(1)),
1927                derives: vec!["Debug".to_string()],
1928            },
1929            MutationSpec::AddDerive {
1930                target: MutationTargetSymbol::ById(dummy_id(1)),
1931                derives: vec!["Clone".to_string()],
1932            },
1933        ];
1934        let blueprint = ParallelBlueprint::from_mutations(specs);
1935        assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
1936
1937        // Multiple independent mutations → Wavefront
1938        let specs = vec![
1939            MutationSpec::AddDerive {
1940                target: MutationTargetSymbol::ById(dummy_id(1)),
1941                derives: vec!["Debug".to_string()],
1942            },
1943            MutationSpec::AddDerive {
1944                target: MutationTargetSymbol::ById(dummy_id(1)),
1945                derives: vec!["Clone".to_string()],
1946            },
1947            MutationSpec::AddDerive {
1948                target: MutationTargetSymbol::ById(dummy_id(1)),
1949                derives: vec!["Default".to_string()],
1950            },
1951            MutationSpec::AddDerive {
1952                target: MutationTargetSymbol::ById(dummy_id(1)),
1953                derives: vec!["Hash".to_string()],
1954            },
1955        ];
1956        let blueprint = ParallelBlueprint::from_mutations(specs);
1957        assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Wavefront);
1958    }
1959
1960    #[test]
1961    #[ignore = "V1 path disabled - needs V2 migration"]
1962    fn test_sequential_and_wavefront_produce_same_result() {
1963        let code = r#"
1964struct A {}
1965struct B {}
1966struct C {}
1967"#;
1968
1969        let specs = vec![
1970            MutationSpec::AddDerive {
1971                target: MutationTargetSymbol::ById(dummy_id(1)),
1972                derives: vec!["Debug".to_string()],
1973            },
1974            MutationSpec::AddDerive {
1975                target: MutationTargetSymbol::ById(dummy_id(1)),
1976                derives: vec!["Clone".to_string()],
1977            },
1978            MutationSpec::AddDerive {
1979                target: MutationTargetSymbol::ById(dummy_id(1)),
1980                derives: vec!["Default".to_string()],
1981            },
1982        ];
1983        let blueprint = ParallelBlueprint::from_mutations(specs.clone());
1984
1985        // Execute with Sequential
1986        let mut ctx_seq = ContextBuilder::new().with_file("src/lib.rs", code).build();
1987        let executor_seq = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Sequential);
1988        let result_seq = execute_and_sync(&executor_seq, &blueprint, &mut ctx_seq);
1989
1990        // Execute with Wavefront
1991        let mut ctx_wave = ContextBuilder::new().with_file("src/lib.rs", code).build();
1992        let executor_wave = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
1993        let result_wave = execute_and_sync(&executor_wave, &blueprint, &mut ctx_wave);
1994
1995        // Both should succeed
1996        assert!(result_seq.success, "Sequential failed");
1997        assert!(result_wave.success, "Wavefront failed");
1998
1999        // Both should produce the same output
2000        let source_seq = ctx_seq
2001            .test_file("src/lib.rs")
2002            .unwrap()
2003            .to_source()
2004            .unwrap();
2005        let source_wave = ctx_wave
2006            .test_file("src/lib.rs")
2007            .unwrap()
2008            .to_source()
2009            .unwrap();
2010
2011        assert!(source_seq.contains("Debug"), "Sequential missing Debug");
2012        assert!(source_seq.contains("Clone"), "Sequential missing Clone");
2013        assert!(source_seq.contains("Default"), "Sequential missing Default");
2014        assert!(source_wave.contains("Debug"), "Wavefront missing Debug");
2015        assert!(source_wave.contains("Clone"), "Wavefront missing Clone");
2016        assert!(source_wave.contains("Default"), "Wavefront missing Default");
2017    }
2018
2019    #[test]
2020    #[ignore = "flaky: µs-level timing comparison depends on CPU load"]
2021    fn test_execute_v2_without_sync_is_faster() {
2022        use ryo_analysis::SymbolKind;
2023        use std::time::Instant;
2024
2025        // Create a larger context for more realistic benchmark
2026        let code = r#"
2027pub struct Config { name: String, value: i32 }
2028pub struct User { id: u64, name: String, email: String }
2029pub struct Order { id: u64, user_id: u64, total: f64 }
2030pub enum Status { Pending, Active, Completed, Failed }
2031pub trait Processor { fn process(&self); }
2032impl Processor for Config { fn process(&self) {} }
2033impl Processor for User { fn process(&self) {} }
2034"#;
2035
2036        // Helper to create specs with resolved symbol_ids
2037        fn create_specs(ctx: &ryo_analysis::AnalysisContext) -> Vec<MutationSpec> {
2038            let config_id = ctx
2039                .registry
2040                .iter()
2041                .find(|(id, path)| {
2042                    path.name() == "Config" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2043                })
2044                .map(|(id, _)| id)
2045                .expect("Config not found");
2046
2047            let user_id = ctx
2048                .registry
2049                .iter()
2050                .find(|(id, path)| {
2051                    path.name() == "User" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2052                })
2053                .map(|(id, _)| id)
2054                .expect("User not found");
2055
2056            vec![
2057                MutationSpec::AddField {
2058                    target: MutationTargetSymbol::ById(config_id),
2059                    field_name: "enabled".to_string(),
2060                    field_type: "bool".to_string(),
2061                    visibility: Visibility::Pub,
2062                },
2063                MutationSpec::AddDerive {
2064                    target: MutationTargetSymbol::ById(user_id),
2065                    derives: vec!["Debug".to_string(), "Clone".to_string()],
2066                },
2067            ]
2068        }
2069
2070        // Benchmark execute_v2 only (no sync)
2071        let iterations = 10;
2072        let mut execute_only_times = Vec::with_capacity(iterations);
2073
2074        for _ in 0..iterations {
2075            let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
2076            let specs = create_specs(&ctx);
2077            let blueprint = ParallelBlueprint::from_mutations(specs);
2078            let executor = BlueprintExecutor::new();
2079
2080            let t0 = Instant::now();
2081            let result = executor.execute_v2(&blueprint, &mut ctx);
2082            execute_only_times.push(t0.elapsed());
2083
2084            assert!(result.success);
2085        }
2086
2087        // Benchmark execute_v2 + sync
2088        let mut execute_with_sync_times = Vec::with_capacity(iterations);
2089
2090        for _ in 0..iterations {
2091            let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
2092            let specs = create_specs(&ctx);
2093            let blueprint = ParallelBlueprint::from_mutations(specs);
2094            let executor = BlueprintExecutor::new();
2095
2096            let t0 = Instant::now();
2097            let result = executor.execute_v2(&blueprint, &mut ctx);
2098            BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
2099            execute_with_sync_times.push(t0.elapsed());
2100
2101            assert!(result.success);
2102        }
2103
2104        let avg_execute_only: u128 = execute_only_times
2105            .iter()
2106            .map(|d| d.as_micros())
2107            .sum::<u128>()
2108            / iterations as u128;
2109        let avg_with_sync: u128 = execute_with_sync_times
2110            .iter()
2111            .map(|d| d.as_micros())
2112            .sum::<u128>()
2113            / iterations as u128;
2114
2115        eprintln!(
2116            "execute_v2 only: {}µs avg, execute_v2 + sync: {}µs avg, sync overhead: {:.1}x",
2117            avg_execute_only,
2118            avg_with_sync,
2119            avg_with_sync as f64 / avg_execute_only as f64
2120        );
2121
2122        // execute_v2 without sync should be faster
2123        assert!(
2124            avg_execute_only < avg_with_sync,
2125            "execute_v2 only ({}µs) should be faster than with sync ({}µs)",
2126            avg_execute_only,
2127            avg_with_sync
2128        );
2129    }
2130
2131    // ========================================================================
2132    // ws_file_to_crate_relative_key regression tests (mod ambiguity candidate #1)
2133    //
2134    // These pin the strip_prefix / CrateName layer that drives
2135    // `existing_file_keys` construction inside `sync_files_and_rebuild`.
2136    // The hypothesis under test: hyphen-named workspace crates (Cargo name
2137    // `ryo-app`, module name `ryo_app`) round-trip correctly so that
2138    // `crates/ryo-app/src/storage.rs` collapses to `"src/storage.rs"`.
2139    //
2140    // If any of these FAIL, candidate #1 from journal Decided is confirmed:
2141    // the inline closure misses files for the affected layout, leaving
2142    // `existing_file_keys` empty and letting the generator emit a duplicate
2143    // `<mod>/mod.rs` next to the existing flat `<mod>.rs`.
2144    // ========================================================================
2145
2146    use ryo_symbol::{CrateLayout, CrateName, WorkspaceFilePath, WorkspaceMetadataProvider};
2147    use std::path::{Path, PathBuf};
2148
2149    /// In-test provider that returns a single fixed `CrateLayout` for any
2150    /// `CrateName`. Only the methods used by `ws_file_to_crate_relative_key`
2151    /// are meaningfully implemented; the rest panic so accidental reliance is
2152    /// caught in the test.
2153    struct FixedLayoutProvider {
2154        layout: CrateLayout,
2155    }
2156
2157    impl WorkspaceMetadataProvider for FixedLayoutProvider {
2158        fn crate_for_file(&self, _path: &WorkspaceFilePath) -> Option<CrateName> {
2159            unreachable!("ws_file_to_crate_relative_key does not call crate_for_file");
2160        }
2161        fn all_crates(&self) -> Vec<CrateName> {
2162            vec![]
2163        }
2164        fn crate_root(&self, _crate_name: &CrateName) -> Option<PathBuf> {
2165            None
2166        }
2167        fn workspace_root(&self) -> &Path {
2168            Path::new("/workspace")
2169        }
2170        fn crate_layout(&self, _crate_name: &CrateName) -> Option<CrateLayout> {
2171            Some(self.layout.clone())
2172        }
2173    }
2174
2175    #[test]
2176    fn ws_key_root_layout_flat_file() {
2177        let wfp = WorkspaceFilePath::new_for_test("src/storage.rs", "/workspace", "test_crate");
2178        let provider = FixedLayoutProvider {
2179            layout: CrateLayout::Root,
2180        };
2181        assert_eq!(
2182            ws_file_to_crate_relative_key(&wfp, &provider).as_deref(),
2183            Some("src/storage.rs")
2184        );
2185    }
2186
2187    #[test]
2188    fn ws_key_in_crates_hyphen_name_flat_file() {
2189        // The exact scenario from journal candidate #1:
2190        // - Cargo name "ryo-app" (hyphen)
2191        // - Module name "ryo_app" (the form `wfp.crate_name()` carries)
2192        // - On-disk path crates/ryo-app/src/storage.rs
2193        // - Expected crate-relative key "src/storage.rs"
2194        let wfp = WorkspaceFilePath::new_for_test(
2195            "crates/ryo-app/src/storage.rs",
2196            "/workspace",
2197            "ryo_app",
2198        );
2199        let provider = FixedLayoutProvider {
2200            layout: CrateLayout::InCrates {
2201                crate_dir_name: "ryo-app".to_string(),
2202            },
2203        };
2204        assert_eq!(
2205            ws_file_to_crate_relative_key(&wfp, &provider).as_deref(),
2206            Some("src/storage.rs"),
2207            "Hyphen-named InCrates layout must strip 'crates/ryo-app/' prefix \
2208             so the generator's with_existing_files sees the flat src/storage.rs"
2209        );
2210    }
2211
2212    #[test]
2213    fn ws_key_in_crates_hyphen_name_nested_module() {
2214        let wfp = WorkspaceFilePath::new_for_test(
2215            "crates/ryo-app/src/discover/service.rs",
2216            "/workspace",
2217            "ryo_app",
2218        );
2219        let provider = FixedLayoutProvider {
2220            layout: CrateLayout::InCrates {
2221                crate_dir_name: "ryo-app".to_string(),
2222            },
2223        };
2224        assert_eq!(
2225            ws_file_to_crate_relative_key(&wfp, &provider).as_deref(),
2226            Some("src/discover/service.rs")
2227        );
2228    }
2229
2230    #[test]
2231    fn ws_key_custom_prefix_layout() {
2232        let wfp = WorkspaceFilePath::new_for_test("packages/core/src/lib.rs", "/workspace", "core");
2233        let provider = FixedLayoutProvider {
2234            layout: CrateLayout::Custom {
2235                prefix: PathBuf::from("packages/core"),
2236            },
2237        };
2238        assert_eq!(
2239            ws_file_to_crate_relative_key(&wfp, &provider).as_deref(),
2240            Some("src/lib.rs")
2241        );
2242    }
2243
2244    #[test]
2245    fn ws_key_layout_lookup_failure_returns_none() {
2246        // Provider that always fails to resolve the layout → caller drops the entry.
2247        struct NoLayoutProvider;
2248        impl WorkspaceMetadataProvider for NoLayoutProvider {
2249            fn crate_for_file(&self, _p: &WorkspaceFilePath) -> Option<CrateName> {
2250                None
2251            }
2252            fn all_crates(&self) -> Vec<CrateName> {
2253                vec![]
2254            }
2255            fn crate_root(&self, _c: &CrateName) -> Option<PathBuf> {
2256                None
2257            }
2258            fn workspace_root(&self) -> &Path {
2259                Path::new("/workspace")
2260            }
2261            fn crate_layout(&self, _c: &CrateName) -> Option<CrateLayout> {
2262                None
2263            }
2264        }
2265        let wfp = WorkspaceFilePath::new_for_test(
2266            "crates/ryo-app/src/storage.rs",
2267            "/workspace",
2268            "ryo_app",
2269        );
2270        assert_eq!(ws_file_to_crate_relative_key(&wfp, &NoLayoutProvider), None);
2271    }
2272}