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, StmtInsertPosition};
42use crate::engine::{collect_affected_ids, MutationEvent};
43use ryo_analysis::{AnalysisContext, RegistryUpdateBatch, SymbolPath};
44use ryo_mutations::MutationResult;
45use ryo_source::pure::ToSynError;
46use ryo_symbol::{MetadataError, SymbolId, WorkspaceFilePath};
47use std::collections::HashSet;
48use std::sync::Arc;
49use tracing::{debug, info, instrument, warn};
50
51/// Error during file sync after blueprint execution.
52#[derive(Debug, thiserror::Error)]
53pub enum SyncError {
54    #[error("cargo metadata unavailable: {0}")]
55    Metadata(#[from] MetadataError),
56    #[error("source generation failed: {0}")]
57    SourceGeneration(#[from] ToSynError),
58}
59
60/// Execution strategy for BlueprintExecutor
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum ExecutionStrategy {
63    /// Execute mutations one by one (safe, debuggable)
64    #[default]
65    Sequential,
66
67    /// Execute independent mutations in parallel within each wave
68    /// Uses Collect-then-Apply pattern for thread safety
69    Wavefront,
70}
71
72/// Suggest the best execution strategy based on blueprint characteristics
73pub fn suggest_strategy(blueprint: &ParallelBlueprint) -> ExecutionStrategy {
74    let parallelism = blueprint.parallelism();
75    let mutation_count = blueprint.mutations.len();
76
77    // Heuristics for strategy selection:
78    // - Low parallelism (≤1.2) → Sequential (no benefit from parallel)
79    // - Very few mutations (≤2) → Sequential (overhead not worth it)
80    // - Otherwise → Wavefront
81    if parallelism <= 1.2 || mutation_count <= 2 {
82        ExecutionStrategy::Sequential
83    } else {
84        ExecutionStrategy::Wavefront
85    }
86}
87
88/// Result of executing a blueprint
89#[derive(Debug, Clone)]
90pub struct BlueprintResult {
91    /// Results for each mutation spec
92    pub results: Vec<SpecResult>,
93
94    /// Total changes across all mutations
95    pub total_changes: usize,
96
97    /// Files that were modified
98    pub modified_files: Vec<WorkspaceFilePath>,
99
100    /// Whether execution completed successfully
101    pub success: bool,
102
103    /// Error message if failed
104    pub error: Option<String>,
105
106    /// Registry updates to apply (for Context-centric design)
107    ///
108    /// These updates should be applied to the SymbolRegistry after
109    /// execution to keep it in sync with the AST changes.
110    pub registry_updates: RegistryUpdateBatch,
111}
112
113/// Result of executing a single MutationSpec
114#[derive(Debug, Clone)]
115pub struct SpecResult {
116    /// Index in the blueprint
117    pub index: usize,
118
119    /// The spec that was executed
120    pub spec_type: String,
121
122    /// Number of changes made
123    pub changes: usize,
124
125    /// Files affected
126    pub affected_files: Vec<WorkspaceFilePath>,
127
128    /// Symbols affected (for history tracking)
129    pub affected_symbols: Vec<SymbolPath>,
130
131    /// Whether this spec succeeded
132    pub success: bool,
133
134    /// Error message if failed
135    pub error: Option<String>,
136
137    /// Registry updates from this spec
138    pub registry_updates: RegistryUpdateBatch,
139
140    /// Mutation events emitted during execution (for incremental updates)
141    pub events: Vec<MutationEvent>,
142}
143
144impl BlueprintResult {
145    pub fn success(results: Vec<SpecResult>, modified_files: Vec<WorkspaceFilePath>) -> Self {
146        let total_changes = results.iter().map(|r| r.changes).sum();
147        // Collect all registry updates from spec results
148        let mut all_updates = RegistryUpdateBatch::new();
149        for result in &results {
150            for update in &result.registry_updates {
151                all_updates.push(update.clone());
152            }
153        }
154        Self {
155            results,
156            total_changes,
157            modified_files,
158            success: true,
159            error: None,
160            registry_updates: all_updates,
161        }
162    }
163
164    pub fn failure(error: impl Into<String>) -> Self {
165        Self {
166            results: vec![],
167            total_changes: 0,
168            modified_files: vec![],
169            success: false,
170            error: Some(error.into()),
171            registry_updates: RegistryUpdateBatch::new(),
172        }
173    }
174}
175
176/// Resolve a `MutationTargetSymbol` to a `SymbolId` inside an `execute_spec_v2`
177/// arm. On failure, build a `SpecResult` describing the lookup error and early
178/// return from the enclosing function so callers see a structured failure
179/// instead of a panic.
180macro_rules! try_resolve {
181    ($self:expr, $target:expr, $ctx:expr, $index:expr, $spec_type:expr, $affected_symbols:expr, $msg:expr $(,)?) => {
182        match $self.resolve_target_symbol_simple($target, $ctx) {
183            Ok(id) => id,
184            Err(e) => {
185                return SpecResult {
186                    index: $index,
187                    spec_type: $spec_type.clone(),
188                    success: false,
189                    changes: 0,
190                    affected_files: vec![],
191                    affected_symbols: $affected_symbols.clone(),
192                    error: Some(format!("{}: {}", $msg, e)),
193                    registry_updates: RegistryUpdateBatch::new(),
194                    events: vec![],
195                };
196            }
197        }
198    };
199}
200
201/// Executor for ParallelBlueprint
202#[derive(Debug)]
203pub struct BlueprintExecutor {
204    /// Registry for MutationSpec → Mutation conversion
205    registry: MutationRegistry,
206
207    /// Execution strategy
208    pub strategy: ExecutionStrategy,
209
210    /// Whether to verify compile after each mutation
211    pub verify_after_each: bool,
212
213    /// Whether to stop on first error
214    pub stop_on_error: bool,
215
216    /// Whether to ignore conflicts and process sequentially
217    /// Default: true (conflicts are ignored, specs processed in order)
218    pub ignore_conflicts: bool,
219}
220
221impl Default for BlueprintExecutor {
222    fn default() -> Self {
223        Self {
224            registry: MutationRegistry::default(),
225            strategy: ExecutionStrategy::default(),
226            verify_after_each: false,
227            stop_on_error: true,
228            ignore_conflicts: true,
229        }
230    }
231}
232
233impl BlueprintExecutor {
234    pub fn new() -> Self {
235        Self::default()
236    }
237
238    /// Resolve MutationTargetSymbol to SymbolId (helper for Legacy fallback)
239    ///
240    /// This is a simplified version of ResolveTargetSymbol trait for use in
241    /// legacy fallback code paths that haven't been migrated to convert_v2() yet.
242    fn resolve_target_symbol_simple(
243        &self,
244        target: &super::spec::MutationTargetSymbol,
245        ctx: &AnalysisContext,
246    ) -> Result<SymbolId, String> {
247        use super::spec::MutationTargetSymbol;
248
249        match target {
250            MutationTargetSymbol::ById(id) => {
251                // Already resolved, verify it exists
252                if ctx.registry.resolve(*id).is_none() {
253                    return Err(format!("Symbol {:?} not found in registry", id));
254                }
255                Ok(*id)
256            }
257            MutationTargetSymbol::ByPath(path) => {
258                // Lazy resolution by path - search registry
259                ctx.registry
260                    .iter()
261                    .find(|(_, p)| *p == &**path)
262                    .map(|(id, _)| id)
263                    .ok_or_else(|| format!("Symbol at path '{}' not found", path))
264            }
265            MutationTargetSymbol::ByKindAndName(kind, name) => {
266                // Lazy resolution by kind + name - search registry
267                // For impl blocks with generics, normalize both names before comparison
268                let normalized_name = normalize_generic_name(name);
269                let matches: Vec<_> = ctx
270                    .registry
271                    .iter()
272                    .filter(|(_, path)| normalize_generic_name(path.name()) == normalized_name)
273                    .collect();
274
275                match matches.len() {
276                    0 => Err(format!("Symbol not found: kind={:?}, name={}", kind, name)),
277                    1 => Ok(matches[0].0),
278                    _ => Err(format!(
279                        "Multiple symbols found: kind={:?}, name={} (found {} matches)",
280                        kind,
281                        name,
282                        matches.len()
283                    )),
284                }
285            }
286            MutationTargetSymbol::ByAffectedId {
287                parent_id,
288                kind,
289                name,
290            } => {
291                // Derived from parent mutation
292                let parent_path = ctx
293                    .registry
294                    .resolve(*parent_id)
295                    .ok_or_else(|| format!("Parent SymbolId {:?} not found", parent_id))?;
296
297                if let Some(child_name) = name {
298                    // Construct child path
299                    parent_path
300                        .child(child_name)
301                        .map_err(|e| format!("Invalid child path: {}", e))
302                        .and_then(|child_path| {
303                            // Lookup child in registry
304                            ctx.registry
305                                .iter()
306                                .find(|(_, p)| p == &&child_path)
307                                .map(|(id, _)| id)
308                                .ok_or_else(|| {
309                                    format!(
310                                        "Child symbol not found: parent={:?}, kind={:?}, name={}",
311                                        parent_id, kind, child_name
312                                    )
313                                })
314                        })
315                } else {
316                    // Anonymous child - not yet supported
317                    Err(format!(
318                        "Anonymous child symbols not yet supported: parent={:?}, kind={:?}",
319                        parent_id, kind
320                    ))
321                }
322            }
323        }
324    }
325
326    // ========================================================================
327    // V2 API: ASTRegistry-centric execution (new path)
328    // ========================================================================
329
330    /// Execute blueprint using ASTRegistry-centric path.
331    ///
332    /// This is the new execution path that:
333    /// 1. Executes mutations directly on ASTRegistry (no file I/O)
334    /// 2. Returns MutationEvents for incremental updates
335    ///
336    /// **Important**: This method only updates ASTRegistry. Callers must:
337    /// - Call `ctx.sync_files_and_rebuild()` for full sync (files + graphs)
338    /// - Or skip sync for lightweight precheck scenarios
339    ///
340    /// Unimplemented MutationSpecs will panic - this is intentional
341    /// as we're building towards complete migration.
342    #[instrument(skip(self, blueprint, ctx), fields(mutations = blueprint.mutations.len()))]
343    pub fn execute_v2(
344        &self,
345        blueprint: &ParallelBlueprint,
346        ctx: &mut AnalysisContext,
347    ) -> BlueprintResult {
348        debug!(
349            "Starting blueprint execution with {} mutations",
350            blueprint.mutations.len()
351        );
352
353        // Check for conflicts (skip if ignore_conflicts is true)
354        if !self.ignore_conflicts && blueprint.needs_escalation() {
355            warn!(
356                "Blueprint has {} conflicts requiring escalation",
357                blueprint.conflicts.len()
358            );
359            return BlueprintResult::failure(format!(
360                "Blueprint has {} conflicts requiring escalation",
361                blueprint.conflicts.len()
362            ));
363        }
364
365        let mut results: Vec<SpecResult> = Vec::new();
366        let mut completed: HashSet<usize> = HashSet::new();
367
368        // Execute in topological order
369        while completed.len() < blueprint.mutations.len() {
370            let ready = blueprint
371                .deps
372                .ready_set(blueprint.mutations.len(), &completed);
373
374            if ready.is_empty() {
375                if completed.len() < blueprint.mutations.len() {
376                    warn!("Dependency cycle detected");
377                    return BlueprintResult::failure("Dependency cycle detected");
378                }
379                break;
380            }
381
382            debug!("Ready to execute {} specs", ready.len());
383
384            for idx in ready {
385                let spec = &blueprint.mutations[idx];
386                debug!("Executing spec {}: {:?}", idx, spec);
387                let result = self.execute_spec_v2(idx, spec, ctx);
388
389                if let Some(ref error) = result.error {
390                    warn!(
391                        "Spec {} completed with error: success={}, changes={}, error={}",
392                        idx, result.success, result.changes, error
393                    );
394                } else {
395                    debug!(
396                        "Spec {} completed: success={}, changes={}, events={}",
397                        idx,
398                        result.success,
399                        result.changes,
400                        result.events.len()
401                    );
402                }
403
404                let success = result.success;
405                results.push(result);
406                completed.insert(idx);
407
408                if !success && self.stop_on_error {
409                    let total_changes = results.iter().map(|r| r.changes).sum();
410
411                    // Get the error from the failed spec result
412                    let spec_error = results
413                        .last()
414                        .and_then(|r| r.error.as_ref())
415                        .map(|e| format!("Spec {} failed: {}", idx, e))
416                        .unwrap_or_else(|| format!("Stopped at spec {} (no error message)", idx));
417
418                    warn!("Stopping on error at spec {}: {}", idx, spec_error);
419
420                    return BlueprintResult {
421                        results,
422                        total_changes,
423                        modified_files: Vec::new(),
424                        success: false,
425                        error: Some(spec_error),
426                        registry_updates: RegistryUpdateBatch::new(),
427                    };
428                }
429            }
430        }
431
432        let total_changes: usize = results.iter().map(|r| r.changes).sum();
433        info!(
434            "Blueprint execution completed: {} results, {} total changes",
435            results.len(),
436            total_changes
437        );
438
439        BlueprintResult::success(results, Vec::new())
440    }
441
442    /// Synchronize files and rebuild analysis graphs after execute_v2.
443    ///
444    /// This method should be called after `execute_v2()` when you need:
445    /// - Updated source files (for disk write or cargo check)
446    /// - Updated analysis graphs (code_graph, typeflow, dataflow, detail_store)
447    ///
448    /// For lightweight precheck scenarios, skip this call entirely.
449    ///
450    /// # File Path Resolution
451    ///
452    /// This method handles the conversion from `RegistryGenerator`'s output
453    /// (crate-relative paths like `"src/lib.rs"`) to workspace-relative paths
454    /// (like `"crates/core/src/lib.rs"`).
455    ///
456    /// The conversion works as follows:
457    ///
458    /// 1. **Extract crate roots from existing files**: For each crate, find its
459    ///    root directory by looking at existing `WorkspaceFilePath`s in `ctx.files`.
460    ///    For example, `"crates/core/src/lib.rs"` → crate_root = `"crates/core"`.
461    ///
462    /// 2. **Combine crate_root + crate-relative path**: The generator outputs
463    ///    `"src/lib.rs"`, and we prepend the crate_root to get the full
464    ///    workspace-relative path: `"crates/core" + "src/lib.rs"` → `"crates/core/src/lib.rs"`.
465    ///
466    /// This approach keeps `RegistryGenerator` focused on pure SymbolPath-based
467    /// generation, while the caller (this function) handles workspace layout concerns.
468    ///
469    /// # Arguments
470    /// - `result`: The BlueprintResult from execute_v2
471    /// - `ctx`: The AnalysisContext that was mutated
472    ///
473    /// # Returns
474    /// List of modified file paths (as `WorkspaceFilePath`)
475    #[instrument(skip(result, ctx), fields(total_changes = result.total_changes))]
476    pub fn sync_files_and_rebuild(
477        result: &BlueprintResult,
478        ctx: &mut AnalysisContext,
479    ) -> Result<Vec<WorkspaceFilePath>, SyncError> {
480        use crate::engine::{collect_modified_symbols, RegistryGenerator};
481        use ryo_symbol::CargoMetadataProvider;
482
483        debug!(
484            "Starting sync_files_and_rebuild with {} results",
485            result.results.len()
486        );
487
488        // Step 1: Collect all events from execution results
489        let all_events: Vec<MutationEvent> = result
490            .results
491            .iter()
492            .flat_map(|r| r.events.clone())
493            .collect();
494
495        debug!(
496            "Collected {} mutation events from results",
497            all_events.len()
498        );
499        // INVARIANT: No mutation events → no files changed → nothing to sync.
500        // This prevents the expensive generate path from running on 0-mutation results.
501        if all_events.is_empty() {
502            debug!("No mutation events — skipping file sync entirely");
503            return Ok(Vec::new());
504        }
505
506        // Step 2: Determine modified files from events
507        let modified_symbol_ids = collect_modified_symbols(&all_events, ctx.registry());
508        debug!("Found {} modified symbols", modified_symbol_ids.len());
509        // Step 3: Generator determines affected files and generates ONLY those files.
510        //
511        // DESIGN RULE: Full-workspace generation (generate/dump_all) is PROHIBITED here.
512        // Only files containing modified symbols may be regenerated.
513        // This is critical because:
514        //   1. to_source() (prettyplease) is expensive per file
515        //   2. Regenerating unmodified files causes format drift (vec![] → vec!(), shorthand expansion)
516        //   3. Writing unmodified files wastes I/O and triggers unnecessary re-indexing
517        let metadata = CargoMetadataProvider::from_directory(&ctx.workspace_root)?;
518
519        let generator = RegistryGenerator::multi_file();
520        let workspace = generator.generate_affected(
521            &ctx.ast_registry,
522            ctx.registry(),
523            &modified_symbol_ids,
524            &metadata,
525        )?;
526
527        debug!(
528            "Generator produced {} crates with {} total files",
529            workspace.crates.len(),
530            workspace.total_files()
531        );
532
533        // Step 4: Convert generated files to WorkspaceFilePath and update context
534        // Generator returns crate-relative paths (e.g., "src/lib.rs")
535        // We need to resolve these to workspace-relative paths using CrateLayout
536        let mut modified_files = Vec::new();
537
538        for generated_crate in workspace.crates.values() {
539            debug!(
540                "Processing crate {} with {} files",
541                generated_crate.crate_name,
542                generated_crate.files.len()
543            );
544
545            // Get CrateLayout for this crate to convert paths correctly
546            use ryo_symbol::{CrateName, WorkspacePathResolver};
547            let crate_name = CrateName::new(&generated_crate.crate_name).expect(
548                "generator-emitted crate names must already satisfy CrateName validation; \
549                 reaching this expect means the generator is producing invalid names",
550            );
551            let layout = metadata.crate_layout(&crate_name);
552
553            for (crate_relative_path, generated_file) in &generated_crate.files {
554                // Convert crate-relative path to workspace-relative path using CrateLayout
555                let workspace_relative = match &layout {
556                    Some(layout) => layout.to_workspace_relative(crate_relative_path),
557                    None => {
558                        // Fallback: use crate-relative path as-is (single-crate workspace)
559                        std::path::PathBuf::from(crate_relative_path.as_str())
560                    }
561                };
562
563                // Find corresponding WorkspaceFilePath from context
564                // Match by crate_name and workspace-relative path
565                let workspace_file = ctx
566                    .files()
567                    .keys()
568                    .find(|wfp| {
569                        wfp.crate_name().as_str() == generated_crate.crate_name
570                            && wfp.as_relative() == workspace_relative.as_path()
571                    })
572                    .cloned();
573
574                let wfp = if let Some(existing) = workspace_file {
575                    debug!(
576                        "Updating existing file: {} ({})",
577                        crate_relative_path,
578                        existing.as_relative().display()
579                    );
580                    existing
581                } else {
582                    // New file: construct WorkspaceFilePath with correct workspace-relative path
583                    debug!(
584                        "Creating new file: {} -> {} for crate {}",
585                        crate_relative_path,
586                        workspace_relative.display(),
587                        generated_crate.crate_name
588                    );
589                    let resolver =
590                        WorkspacePathResolver::new(ctx.workspace_root.as_ref().to_path_buf());
591                    resolver.resolve_relative_with_crate(&workspace_relative, crate_name.clone())
592                };
593
594                let parsed = ryo_source::pure::PureFile::from_source(&generated_file.source);
595                let pure_file = parsed.unwrap_or_else(|_e| ryo_source::pure::PureFile::new());
596                ctx.files_mut().insert(wfp.clone(), Arc::new(pure_file));
597                modified_files.push(wfp);
598            }
599        }
600
601        debug!(
602            "Updated {} files in context from generator output",
603            modified_files.len()
604        );
605
606        // Step 5: Rebuild analysis graphs using symbol-based incremental update
607        if !all_events.is_empty() {
608            let affected_ids = collect_affected_ids(&all_events, ctx.registry());
609            debug!("Rebuilding with {} affected symbol IDs", affected_ids.len());
610            ctx.rebuild_after_mutation_by_symbols(&affected_ids);
611        } else if !modified_files.is_empty() {
612            // Fallback to file-based update if no events were collected
613            debug!(
614                "Rebuilding with {} modified files (fallback)",
615                modified_files.len()
616            );
617            ctx.rebuild_after_mutation(&modified_files);
618        }
619
620        info!(
621            "sync_files_and_rebuild completed: {} files modified",
622            modified_files.len()
623        );
624        Ok(modified_files)
625    }
626
627    /// Execute a single MutationSpec via ASTRegistry path.
628    ///
629    /// Uses convert_v2() when available, falls back to direct construction
630    /// for specs that haven't been migrated yet.
631    fn execute_spec_v2(
632        &self,
633        index: usize,
634        spec: &MutationSpec,
635        ctx: &mut AnalysisContext,
636    ) -> SpecResult {
637        use crate::engine::{ASTMutationEngine, ExecutionResult};
638        use crate::executor::registry::ConvertError;
639        use ryo_mutations::basic::{
640            AddDeriveMutation, AddFieldMutation, RemoveDeriveMutation, RemoveFieldMutation,
641            RemoveModMutation,
642        };
643
644        let spec_type = spec_type_name(spec);
645        // Compute affected_symbols from spec targets
646        let affected_symbols: Vec<SymbolPath> = spec
647            .get_targets()
648            .iter()
649            .filter_map(|target| target.to_path(ctx.registry()))
650            .collect();
651
652        // Try V2 path first (convert_v2 → execute_ast_reg_batch_dyn)
653        match self.registry.convert_v2(spec, ctx) {
654            Ok(mutations) => {
655                let exec_result = ASTMutationEngine::execute_ast_reg_batch_dyn(mutations, ctx);
656
657                // For AddItem: register the newly added symbol for deferred resolution
658                // This allows later intents (AddDerive, AddVariant) to reference it by name
659                if let MutationSpec::AddItem {
660                    target, content, ..
661                } = spec
662                {
663                    // Only register if target is already resolved to a SymbolPath
664                    if let MutationTargetSymbol::ByPath(path) = target {
665                        register_item_from_content(ctx, path, content);
666                    } else if let MutationTargetSymbol::ById(id) = target {
667                        // Resolve SymbolId to SymbolPath and clone it
668                        if let Some(path) = ctx.registry().resolve(*id).cloned() {
669                            register_item_from_content(ctx, &path, content);
670                        }
671                    }
672                }
673
674                return SpecResult {
675                    index,
676                    spec_type,
677                    success: true,
678                    changes: exec_result.result.changes,
679                    affected_files: vec![], // TODO: Extract from events
680                    affected_symbols,
681                    error: None,
682                    registry_updates: RegistryUpdateBatch::new(),
683                    events: exec_result.events,
684                };
685            }
686            Err(ConvertError::V2NotSupported) => {
687                // Fall through to legacy match-based construction
688            }
689            Err(e) => {
690                // Other errors (UnknownSpec, etc.) are real failures
691                return SpecResult {
692                    index,
693                    spec_type,
694                    success: false,
695                    changes: 0,
696                    affected_files: vec![],
697                    affected_symbols,
698                    error: Some(format!("convert_v2 failed: {}", e)),
699                    registry_updates: RegistryUpdateBatch::new(),
700                    events: vec![],
701                };
702            }
703        }
704
705        // Legacy fallback: direct construction for specs not yet migrated to convert_v2
706        let exec_result = match spec {
707            MutationSpec::AddField {
708                target,
709                field_name,
710                field_type,
711                visibility,
712                ..
713            } => {
714                // Resolve target to SymbolId
715                let symbol_id = try_resolve!(
716                    self,
717                    target,
718                    ctx,
719                    index,
720                    spec_type,
721                    affected_symbols,
722                    "Failed to resolve target symbol"
723                );
724                let mut mutation = AddFieldMutation::new(symbol_id, field_name, field_type);
725                if matches!(visibility, super::spec::Visibility::Pub) {
726                    mutation = mutation.public();
727                }
728                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
729            }
730
731            MutationSpec::RemoveField {
732                target, field_name, ..
733            } => {
734                // Resolve target to SymbolId
735                let symbol_id = try_resolve!(
736                    self,
737                    target,
738                    ctx,
739                    index,
740                    spec_type,
741                    affected_symbols,
742                    "Failed to resolve target symbol"
743                );
744                let mutation = RemoveFieldMutation::new(symbol_id, field_name);
745                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
746            }
747
748            MutationSpec::RemoveMod {
749                target, mod_name, ..
750            } => {
751                use ryo_symbol::SymbolKind;
752
753                // Resolve parent module
754                let parent_id = try_resolve!(
755                    self,
756                    target,
757                    ctx,
758                    index,
759                    spec_type,
760                    affected_symbols,
761                    "Failed to resolve parent module"
762                );
763
764                // Find child module within parent
765                let parent_path = match ctx.registry.resolve(parent_id) {
766                    Some(p) => p,
767                    None => {
768                        return SpecResult {
769                            index,
770                            spec_type: spec_type.clone(),
771                            success: false,
772                            changes: 0,
773                            affected_files: vec![],
774                            affected_symbols: affected_symbols.clone(),
775                            error: Some(format!(
776                                "Parent module path not found for SymbolId {:?}",
777                                parent_id
778                            )),
779                            registry_updates: RegistryUpdateBatch::new(),
780                            events: vec![],
781                        };
782                    }
783                };
784
785                let mod_path = match parent_path.child(mod_name) {
786                    Ok(p) => p,
787                    Err(e) => {
788                        return SpecResult {
789                            index,
790                            spec_type: spec_type.clone(),
791                            success: false,
792                            changes: 0,
793                            affected_files: vec![],
794                            affected_symbols: affected_symbols.clone(),
795                            error: Some(format!(
796                                "Failed to build module path for '{}': {}",
797                                mod_name, e
798                            )),
799                            registry_updates: RegistryUpdateBatch::new(),
800                            events: vec![],
801                        };
802                    }
803                };
804
805                let module_id = match ctx.registry.lookup(&mod_path) {
806                    Some(id) => id,
807                    None => {
808                        return SpecResult {
809                            index,
810                            spec_type: spec_type.clone(),
811                            success: false,
812                            changes: 0,
813                            affected_files: vec![],
814                            affected_symbols: affected_symbols.clone(),
815                            error: Some(format!(
816                                "Module '{}' not found in {}",
817                                mod_name, parent_path
818                            )),
819                            registry_updates: RegistryUpdateBatch::new(),
820                            events: vec![],
821                        };
822                    }
823                };
824
825                // Verify it's a module
826                if ctx.registry.kind(module_id) != Some(SymbolKind::Mod) {
827                    return SpecResult {
828                        index,
829                        spec_type: spec_type.clone(),
830                        success: false,
831                        changes: 0,
832                        affected_files: vec![],
833                        affected_symbols: affected_symbols.clone(),
834                        error: Some(format!("Symbol {} is not a module", module_id)),
835                        registry_updates: RegistryUpdateBatch::new(),
836                        events: vec![],
837                    };
838                }
839
840                let mutation = RemoveModMutation::new(module_id);
841                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
842            }
843
844            MutationSpec::AddDerive {
845                target, derives, ..
846            } => {
847                // Resolve target to SymbolId
848                let symbol_id = try_resolve!(
849                    self,
850                    target,
851                    ctx,
852                    index,
853                    spec_type,
854                    affected_symbols,
855                    "Failed to resolve target symbol"
856                );
857                let mutation = AddDeriveMutation::new(symbol_id, derives.clone());
858                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
859            }
860
861            MutationSpec::RemoveDerive {
862                target, derives, ..
863            } => {
864                // Resolve target to SymbolId
865                let symbol_id = try_resolve!(
866                    self,
867                    target,
868                    ctx,
869                    index,
870                    spec_type,
871                    affected_symbols,
872                    "Failed to resolve target symbol"
873                );
874                let mutation = RemoveDeriveMutation::new(symbol_id, derives.clone());
875                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
876            }
877
878            MutationSpec::AddVariant {
879                target,
880                variant_name,
881                variant_kind,
882                ..
883            } => {
884                use ryo_mutations::basic::AddVariantMutation;
885                use ryo_source::pure::{PureField, PureFields, PureType, PureVis};
886
887                // Resolve target to SymbolId
888                let symbol_id = try_resolve!(
889                    self,
890                    target,
891                    ctx,
892                    index,
893                    spec_type,
894                    affected_symbols,
895                    "Failed to resolve target symbol"
896                );
897
898                let fields = match variant_kind {
899                    super::spec::VariantKind::Unit => PureFields::Unit,
900                    super::spec::VariantKind::Tuple { types } => {
901                        PureFields::Tuple(types.iter().map(|t| PureType::Path(t.clone())).collect())
902                    }
903                    super::spec::VariantKind::Struct { fields } => {
904                        let pure_fields: Vec<PureField> = fields
905                            .iter()
906                            .map(|(n, t)| PureField {
907                                attrs: Vec::new(),
908                                vis: PureVis::Private,
909                                name: n.clone(),
910                                ty: PureType::Path(t.clone()),
911                            })
912                            .collect();
913                        PureFields::Named(pure_fields)
914                    }
915                };
916                let mutation = AddVariantMutation::new(symbol_id, variant_name, fields);
917                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
918            }
919
920            MutationSpec::RemoveVariant {
921                target,
922                variant_name,
923                ..
924            } => {
925                use ryo_mutations::basic::RemoveVariantMutation;
926                // Resolve target to SymbolId
927                let symbol_id = try_resolve!(
928                    self,
929                    target,
930                    ctx,
931                    index,
932                    spec_type,
933                    affected_symbols,
934                    "Failed to resolve target symbol"
935                );
936                let mutation = RemoveVariantMutation::new(symbol_id, variant_name);
937                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
938            }
939
940            MutationSpec::ChangeVisibility {
941                target, visibility, ..
942            } => {
943                use ryo_mutations::basic::ChangeVisibilityMutation;
944                use ryo_source::pure::PureVis;
945
946                // Resolve target to SymbolId
947                let symbol_id = try_resolve!(
948                    self,
949                    target,
950                    ctx,
951                    index,
952                    spec_type,
953                    affected_symbols,
954                    "Failed to resolve target symbol"
955                );
956
957                let pure_vis = match visibility {
958                    super::spec::Visibility::Private => PureVis::Private,
959                    super::spec::Visibility::Pub => PureVis::Public,
960                    super::spec::Visibility::PubCrate => PureVis::Crate,
961                    super::spec::Visibility::PubSuper => PureVis::Super,
962                    super::spec::Visibility::PubIn(_) => PureVis::Public, // Simplified
963                };
964
965                let mutation = ChangeVisibilityMutation::new(symbol_id, pure_vis);
966                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
967            }
968
969            MutationSpec::Rename { target, to, .. } => {
970                use ryo_mutations::basic::RenameMutation;
971                // Resolve target to SymbolId
972                let symbol_id = try_resolve!(
973                    self,
974                    target,
975                    ctx,
976                    index,
977                    spec_type,
978                    affected_symbols,
979                    "Failed to resolve target symbol"
980                );
981                let mutation = RenameMutation::new(symbol_id, to);
982                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
983            }
984
985            // AddItem, RemoveItem, CreateMod, AddMethod, RemoveMethod
986            // all go through convert_v2() → ASTRegApply path
987            MutationSpec::AddMatchArm {
988                target,
989                enum_name,
990                pattern,
991                body,
992            } => {
993                use ryo_mutations::basic::AddMatchArmMutation;
994
995                let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
996                    Ok(id) => id,
997                    Err(e) => {
998                        return SpecResult {
999                            index,
1000                            spec_type,
1001                            changes: 0,
1002                            affected_files: vec![],
1003                            affected_symbols: vec![],
1004                            success: false,
1005                            error: Some(e),
1006                            registry_updates: RegistryUpdateBatch::default(),
1007                            events: vec![],
1008                        };
1009                    }
1010                };
1011
1012                let mutation = AddMatchArmMutation::new(fn_id, enum_name, pattern, body);
1013                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1014            }
1015
1016            MutationSpec::RemoveMatchArm {
1017                target,
1018                enum_name,
1019                pattern,
1020            } => {
1021                use ryo_mutations::basic::RemoveMatchArmMutation;
1022
1023                let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
1024                    Ok(id) => id,
1025                    Err(e) => {
1026                        return SpecResult {
1027                            index,
1028                            spec_type,
1029                            changes: 0,
1030                            affected_files: vec![],
1031                            affected_symbols: vec![],
1032                            success: false,
1033                            error: Some(e),
1034                            registry_updates: RegistryUpdateBatch::default(),
1035                            events: vec![],
1036                        };
1037                    }
1038                };
1039
1040                let mutation = RemoveMatchArmMutation::new(fn_id, enum_name, pattern);
1041                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1042            }
1043
1044            MutationSpec::ReplaceMatchArm {
1045                target,
1046                enum_name,
1047                old_pattern,
1048                new_pattern,
1049                new_body,
1050            } => {
1051                use ryo_mutations::basic::ReplaceMatchArmMutation;
1052
1053                let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
1054                    Ok(id) => id,
1055                    Err(e) => {
1056                        return SpecResult {
1057                            index,
1058                            spec_type,
1059                            changes: 0,
1060                            affected_files: vec![],
1061                            affected_symbols: vec![],
1062                            success: false,
1063                            error: Some(e),
1064                            registry_updates: RegistryUpdateBatch::default(),
1065                            events: vec![],
1066                        };
1067                    }
1068                };
1069
1070                let mutation = ReplaceMatchArmMutation::new(
1071                    fn_id,
1072                    enum_name,
1073                    old_pattern,
1074                    new_pattern,
1075                    new_body,
1076                );
1077                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1078            }
1079
1080            MutationSpec::AddStructLiteralField {
1081                target,
1082                field_name,
1083                value,
1084                ..
1085            } => {
1086                use ryo_mutations::basic::AddStructLiteralFieldMutation;
1087                // Resolve target to SymbolId
1088                let symbol_id = try_resolve!(
1089                    self,
1090                    target,
1091                    ctx,
1092                    index,
1093                    spec_type,
1094                    affected_symbols,
1095                    "Failed to resolve target symbol"
1096                );
1097                let mutation = AddStructLiteralFieldMutation::new(symbol_id, field_name, value);
1098                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1099            }
1100
1101            MutationSpec::RemoveStructLiteralField {
1102                target, field_name, ..
1103            } => {
1104                use ryo_mutations::basic::RemoveStructLiteralFieldMutation;
1105                // Resolve target to SymbolId
1106                let symbol_id = try_resolve!(
1107                    self,
1108                    target,
1109                    ctx,
1110                    index,
1111                    spec_type,
1112                    affected_symbols,
1113                    "Failed to resolve target symbol"
1114                );
1115                let mutation = RemoveStructLiteralFieldMutation::new(symbol_id, field_name);
1116                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1117            }
1118
1119            MutationSpec::OrganizeImports {
1120                deduplicate,
1121                merge_groups,
1122                ..
1123            } => {
1124                use ryo_mutations::idiom::OrganizeImportsMutation;
1125                let mutation = OrganizeImportsMutation::new()
1126                    .with_deduplicate(*deduplicate)
1127                    .with_merge_groups(*merge_groups);
1128                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1129            }
1130
1131            MutationSpec::AssignOp { .. } => {
1132                use ryo_mutations::idiom::AssignOpMutation;
1133                let mutation = AssignOpMutation::new();
1134                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1135            }
1136
1137            MutationSpec::BoolSimplify { .. } => {
1138                use ryo_mutations::idiom::BoolSimplifyMutation;
1139                let mutation = BoolSimplifyMutation::new();
1140                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1141            }
1142
1143            MutationSpec::ComparisonToMethod { .. } => {
1144                use ryo_mutations::idiom::ComparisonToMethodMutation;
1145                let mutation = ComparisonToMethodMutation::new();
1146                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1147            }
1148
1149            MutationSpec::CollapsibleIf { .. } => {
1150                use ryo_mutations::idiom::CollapsibleIfMutation;
1151                let mutation = CollapsibleIfMutation::new();
1152                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1153            }
1154
1155            MutationSpec::RedundantClosure { .. } => {
1156                use ryo_mutations::idiom::RedundantClosureMutation;
1157                let mutation = RedundantClosureMutation::new();
1158                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1159            }
1160
1161            MutationSpec::FilterNext { .. } => {
1162                use ryo_mutations::idiom::FilterNextMutation;
1163                let mutation = FilterNextMutation::new();
1164                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1165            }
1166
1167            MutationSpec::MapUnwrapOr { .. } => {
1168                use ryo_mutations::idiom::MapUnwrapOrMutation;
1169                let mutation = MapUnwrapOrMutation::new();
1170                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1171            }
1172
1173            MutationSpec::CloneOnCopy { .. } => {
1174                use ryo_mutations::idiom::CloneOnCopyMutation;
1175                let mutation = CloneOnCopyMutation::new();
1176                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1177            }
1178
1179            MutationSpec::LoopToIterator { .. } => {
1180                use ryo_mutations::idiom::LoopToIteratorMutation;
1181                let mutation = LoopToIteratorMutation::new();
1182                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1183            }
1184
1185            MutationSpec::UnwrapToQuestion { .. } => {
1186                use ryo_mutations::idiom::UnwrapToQuestionMutation;
1187                let mutation = UnwrapToQuestionMutation::new();
1188                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1189            }
1190
1191            MutationSpec::ManualMap { .. } => {
1192                use ryo_mutations::idiom::ManualMapMutation;
1193                let mutation = ManualMapMutation::new();
1194                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1195            }
1196
1197            MutationSpec::MatchToIfLet { .. } => {
1198                use ryo_mutations::idiom::MatchToIfLetMutation;
1199                let mutation = MatchToIfLetMutation::new();
1200                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1201            }
1202
1203            // Stub implementations - return pending status
1204            // See crates/ryo-executor/src/engine/impls/ for Quality Policy
1205            MutationSpec::IntroduceVariable { expr, var_name, .. } => {
1206                use ryo_mutations::idiom::IntroduceVariableMutation;
1207                use ryo_source::ToPure;
1208                // Parse expr string to PureExpr via syn
1209                let pure_expr = syn::parse_str::<syn::Expr>(expr)
1210                    .map(|e| e.to_pure())
1211                    .unwrap_or_else(|_| ryo_source::pure::PureExpr::Path(expr.clone()));
1212                let mutation = IntroduceVariableMutation::new(pure_expr, var_name);
1213                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1214            }
1215
1216            MutationSpec::ExtractTrait {
1217                target,
1218                ref trait_name,
1219                ref methods,
1220                ..
1221            } => {
1222                use ryo_mutations::basic::ExtractTraitMutation;
1223                // Resolve target to SymbolId
1224                let symbol_id = try_resolve!(
1225                    self,
1226                    target,
1227                    ctx,
1228                    index,
1229                    spec_type,
1230                    affected_symbols,
1231                    "Failed to resolve target symbol"
1232                );
1233                let mut mutation = ExtractTraitMutation::new(symbol_id, trait_name.clone());
1234                if let Some(ref m) = methods {
1235                    mutation = mutation.with_methods(m.clone());
1236                }
1237                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1238            }
1239
1240            MutationSpec::InlineTrait {
1241                target,
1242                ref struct_name,
1243                remove_trait,
1244                ..
1245            } => {
1246                use ryo_mutations::basic::InlineTraitMutation;
1247                // Resolve target to SymbolId
1248                let symbol_id = try_resolve!(
1249                    self,
1250                    target,
1251                    ctx,
1252                    index,
1253                    spec_type,
1254                    affected_symbols,
1255                    "Failed to resolve target symbol"
1256                );
1257                let mut mutation = InlineTraitMutation::new(symbol_id, struct_name.clone());
1258                if !remove_trait {
1259                    mutation = mutation.keep_trait();
1260                }
1261                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1262            }
1263
1264            MutationSpec::ReplaceExpr {
1265                fn_id,
1266                old_expr,
1267                new_expr,
1268                replace_all,
1269                ..
1270            } => {
1271                use ryo_mutations::basic::stmt::ReplaceExprMutation;
1272                use ryo_source::pure::{PureExpr, ToPure};
1273
1274                // V1 blueprint executor requires fn_id to be specified
1275                let target_fn = match fn_id {
1276                    Some(id) => *id,
1277                    None => {
1278                        return SpecResult {
1279                            index,
1280                            spec_type,
1281                            changes: 0,
1282                            affected_files: vec![],
1283                            affected_symbols: vec![],
1284                            success: false,
1285                            error: Some(
1286                                "ReplaceExpr requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
1287                            ),
1288                            registry_updates: RegistryUpdateBatch::default(),
1289                            events: vec![],
1290                        };
1291                    }
1292                };
1293
1294                // Parse expressions from string, fallback to Path if parse fails
1295                let old_pure = syn::parse_str::<syn::Expr>(old_expr)
1296                    .map(|e| e.to_pure())
1297                    .unwrap_or_else(|_| PureExpr::Path(old_expr.clone()));
1298                let new_pure = syn::parse_str::<syn::Expr>(new_expr)
1299                    .map(|e| e.to_pure())
1300                    .unwrap_or_else(|_| PureExpr::Path(new_expr.clone()));
1301
1302                let mut mutation = ReplaceExprMutation::new(old_pure, new_pure, target_fn);
1303                if !replace_all {
1304                    mutation = mutation.first_only();
1305                }
1306                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1307            }
1308
1309            MutationSpec::RemoveStatement {
1310                fn_id,
1311                ref pattern,
1312                remove_all,
1313                ..
1314            } => {
1315                use crate::executor::registry::converters::StmtConverter;
1316                use ryo_mutations::basic::stmt::RemoveStatementMutation;
1317
1318                // V1 blueprint executor requires fn_id to be specified
1319                let target_fn = match fn_id {
1320                    Some(id) => *id,
1321                    None => {
1322                        return SpecResult {
1323                            index,
1324                            spec_type,
1325                            changes: 0,
1326                            affected_files: vec![],
1327                            affected_symbols: vec![],
1328                            success: false,
1329                            error: Some(
1330                                "RemoveStatement requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
1331                            ),
1332                            registry_updates: RegistryUpdateBatch::default(),
1333                            events: vec![],
1334                        };
1335                    }
1336                };
1337
1338                let target_stmt = match StmtConverter::parse_stmt(pattern) {
1339                    Ok(s) => s,
1340                    Err(e) => {
1341                        return SpecResult {
1342                            index,
1343                            spec_type,
1344                            changes: 0,
1345                            affected_files: vec![],
1346                            affected_symbols: vec![],
1347                            success: false,
1348                            error: Some(format!("Failed to parse statement pattern: {}", e)),
1349                            registry_updates: RegistryUpdateBatch::default(),
1350                            events: vec![],
1351                        };
1352                    }
1353                };
1354
1355                let mut mutation =
1356                    RemoveStatementMutation::new(target_stmt, pattern.clone(), target_fn);
1357                if !*remove_all {
1358                    mutation = mutation.first_only();
1359                }
1360                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1361            }
1362
1363            MutationSpec::InsertStatement {
1364                fn_id,
1365                ref stmt,
1366                ref position,
1367                ref reference_pattern,
1368                ..
1369            } => {
1370                use crate::executor::registry::converters::StmtConverter;
1371                use ryo_mutations::basic::stmt::InsertStatementMutation;
1372
1373                let pure_stmt = match StmtConverter::parse_stmt(stmt) {
1374                    Ok(s) => s,
1375                    Err(e) => {
1376                        return SpecResult {
1377                            index,
1378                            spec_type,
1379                            changes: 0,
1380                            affected_files: vec![],
1381                            affected_symbols: vec![],
1382                            success: false,
1383                            error: Some(format!("Failed to parse statement: {}", e)),
1384                            registry_updates: RegistryUpdateBatch::default(),
1385                            events: vec![],
1386                        };
1387                    }
1388                };
1389
1390                let mut mutation = InsertStatementMutation::new(pure_stmt, *fn_id);
1391                mutation = match position {
1392                    StmtInsertPosition::Start => mutation.at_start(),
1393                    StmtInsertPosition::End => mutation.at_end(),
1394                    StmtInsertPosition::BeforePattern => {
1395                        if let Some(ref p) = reference_pattern {
1396                            let reference_stmt = match StmtConverter::parse_stmt(p) {
1397                                Ok(s) => s,
1398                                Err(e) => {
1399                                    return SpecResult {
1400                                        index,
1401                                        spec_type,
1402                                        changes: 0,
1403                                        affected_files: vec![],
1404                                        affected_symbols: vec![],
1405                                        success: false,
1406                                        error: Some(format!(
1407                                            "Failed to parse reference pattern: {}",
1408                                            e
1409                                        )),
1410                                        registry_updates: RegistryUpdateBatch::default(),
1411                                        events: vec![],
1412                                    };
1413                                }
1414                            };
1415                            mutation.before(reference_stmt)
1416                        } else {
1417                            mutation
1418                        }
1419                    }
1420                    StmtInsertPosition::AfterPattern => {
1421                        if let Some(ref p) = reference_pattern {
1422                            let reference_stmt = match StmtConverter::parse_stmt(p) {
1423                                Ok(s) => s,
1424                                Err(e) => {
1425                                    return SpecResult {
1426                                        index,
1427                                        spec_type,
1428                                        changes: 0,
1429                                        affected_files: vec![],
1430                                        affected_symbols: vec![],
1431                                        success: false,
1432                                        error: Some(format!(
1433                                            "Failed to parse reference pattern: {}",
1434                                            e
1435                                        )),
1436                                        registry_updates: RegistryUpdateBatch::default(),
1437                                        events: vec![],
1438                                    };
1439                                }
1440                            };
1441                            mutation.after(reference_stmt)
1442                        } else {
1443                            mutation
1444                        }
1445                    }
1446                };
1447                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1448            }
1449
1450            MutationSpec::ReplaceStatement {
1451                old_stmt,
1452                new_stmt,
1453                fn_id,
1454                ..
1455            } => {
1456                use crate::executor::registry::converters::StmtConverter;
1457                use ryo_mutations::basic::stmt::ReplaceStatementMutation;
1458
1459                let old_pure = match StmtConverter::parse_stmt(old_stmt) {
1460                    Ok(s) => s,
1461                    Err(e) => {
1462                        return SpecResult {
1463                            index,
1464                            spec_type,
1465                            changes: 0,
1466                            affected_files: vec![],
1467                            affected_symbols: vec![],
1468                            success: false,
1469                            error: Some(format!("Failed to parse old statement: {}", e)),
1470                            registry_updates: RegistryUpdateBatch::default(),
1471                            events: vec![],
1472                        };
1473                    }
1474                };
1475                let new_pure = match StmtConverter::parse_stmt(new_stmt) {
1476                    Ok(s) => s,
1477                    Err(e) => {
1478                        return SpecResult {
1479                            index,
1480                            spec_type,
1481                            changes: 0,
1482                            affected_files: vec![],
1483                            affected_symbols: vec![],
1484                            success: false,
1485                            error: Some(format!("Failed to parse new statement: {}", e)),
1486                            registry_updates: RegistryUpdateBatch::default(),
1487                            events: vec![],
1488                        };
1489                    }
1490                };
1491
1492                // V1 blueprint executor requires fn_id to be specified
1493                let target_fn = match fn_id {
1494                    Some(id) => *id,
1495                    None => {
1496                        return SpecResult {
1497                            index,
1498                            spec_type,
1499                            changes: 0,
1500                            affected_files: vec![],
1501                            affected_symbols: vec![],
1502                            success: false,
1503                            error: Some(
1504                                "ReplaceStatement requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
1505                            ),
1506                            registry_updates: RegistryUpdateBatch::default(),
1507                            events: vec![],
1508                        };
1509                    }
1510                };
1511
1512                let mutation = ReplaceStatementMutation::new(old_pure, new_pure, target_fn);
1513                ASTMutationEngine::execute_ast_reg(&mutation, ctx)
1514            }
1515
1516            MutationSpec::DuplicateFunction { .. }
1517            | MutationSpec::DuplicateStruct { .. }
1518            | MutationSpec::DuplicateEnum { .. }
1519            | MutationSpec::DuplicateModTree { .. } => {
1520                // Duplicate* specs are V2-only — they must be handled by
1521                // DuplicateConverter::convert_v2() in the V2 path above. If a
1522                // V1 fall-through reaches this arm it indicates the V2 path
1523                // returned V2NotSupported for a Duplicate spec, which is a
1524                // converter bug rather than a runtime error in the user's input.
1525                return SpecResult {
1526                    index,
1527                    spec_type,
1528                    success: false,
1529                    changes: 0,
1530                    affected_files: vec![],
1531                    affected_symbols,
1532                    error: Some(
1533                        "Duplicate mutations must be routed through \
1534                         DuplicateConverter::convert_v2(); the V1 executor path \
1535                         has been removed."
1536                            .to_string(),
1537                    ),
1538                    registry_updates: RegistryUpdateBatch::new(),
1539                    events: vec![],
1540                };
1541            }
1542
1543            // Blueprint composition - not individual mutations
1544            MutationSpec::AddSpec { .. } => {
1545                // TODO: Implement as AddTypeAlias composition
1546                ExecutionResult::new(
1547                    MutationResult {
1548                        mutation_type: "AddSpec".to_string(),
1549                        changes: 0,
1550                        description: "V2 pending - implement as AddTypeAlias composition"
1551                            .to_string(),
1552                    },
1553                    vec![],
1554                )
1555            }
1556
1557            // MoveItem is now handled via V2 path (MoveConverter → MoveItemMutation)
1558            MutationSpec::PluginTransform { .. } => {
1559                // WASM runtime out of scope for V2 core
1560                ExecutionResult::new(
1561                    MutationResult {
1562                        mutation_type: "PluginTransform".to_string(),
1563                        changes: 0,
1564                        description: "WASM plugin runtime not implemented in V2".to_string(),
1565                    },
1566                    vec![],
1567                )
1568            }
1569
1570            _ => {
1571                return SpecResult {
1572                    index,
1573                    spec_type: spec_type.clone(),
1574                    success: false,
1575                    changes: 0,
1576                    affected_files: vec![],
1577                    affected_symbols,
1578                    error: Some(format!(
1579                        "MutationSpec::{} is not implemented in the V2 AST path \
1580                         (no converter or fallback covers this variant). \
1581                         If you need this spec, add a converter to \
1582                         MutationRegistry::convert_v2 or extend execute_spec_v2.",
1583                        spec_type
1584                    )),
1585                    registry_updates: RegistryUpdateBatch::new(),
1586                    events: vec![],
1587                };
1588            }
1589        };
1590
1591        SpecResult {
1592            index,
1593            spec_type,
1594            changes: exec_result.result.changes,
1595            affected_files: vec![], // Will be determined by FileDumper at end
1596            affected_symbols,
1597            success: exec_result.has_changes() || exec_result.result.changes == 0,
1598            error: None,
1599            registry_updates: RegistryUpdateBatch::new(),
1600            events: exec_result.events,
1601        }
1602    }
1603
1604    /// Set execution strategy
1605    pub fn with_strategy(mut self, strategy: ExecutionStrategy) -> Self {
1606        self.strategy = strategy;
1607        self
1608    }
1609
1610    /// Enable compile verification after each mutation
1611    pub fn with_verify(mut self, verify: bool) -> Self {
1612        self.verify_after_each = verify;
1613        self
1614    }
1615
1616    /// Stop execution on first error
1617    pub fn with_stop_on_error(mut self, stop: bool) -> Self {
1618        self.stop_on_error = stop;
1619        self
1620    }
1621}
1622
1623/// Normalize generic type names for comparison.
1624///
1625/// The registry stores generic types with spaces due to `to_token_stream().to_string()`
1626/// behavior (e.g., "Generic < T , U >"), but DSL specifies them without spaces
1627/// (e.g., "Generic<T, U>"). This function normalizes both formats for comparison.
1628fn normalize_generic_name(name: &str) -> String {
1629    name.replace(" < ", "<")
1630        .replace(" > ", ">")
1631        .replace("< ", "<")
1632        .replace(" >", ">")
1633        .replace(", ", ",")
1634        .replace(" ,", ",")
1635}
1636
1637/// Register a symbol from AddItem content for deferred resolution.
1638///
1639/// This allows later intents in a batch (e.g., AddDerive, AddVariant) to reference
1640/// symbols created by earlier AddItem intents.
1641fn register_item_from_content(
1642    ctx: &mut AnalysisContext,
1643    target: &ryo_symbol::SymbolPath,
1644    content: &str,
1645) {
1646    use ryo_symbol::SymbolKind;
1647
1648    let trimmed = content.trim();
1649
1650    // Skip attributes and find the item declaration
1651    let mut lines = trimmed.lines();
1652    let mut decl_line = "";
1653    for line in lines.by_ref() {
1654        let line = line.trim();
1655        if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
1656            decl_line = line;
1657            break;
1658        }
1659    }
1660
1661    // Parse: pub? (struct|enum|fn|type|const|static|trait|mod) NAME
1662    let tokens: Vec<&str> = decl_line.split_whitespace().collect();
1663    if tokens.is_empty() {
1664        return;
1665    }
1666
1667    let mut idx = 0;
1668
1669    // Skip visibility
1670    if tokens.get(idx) == Some(&"pub") {
1671        idx += 1;
1672        // Skip pub(crate), pub(super), etc.
1673        if let Some(t) = tokens.get(idx) {
1674            if t.starts_with('(') {
1675                idx += 1;
1676            }
1677        }
1678    }
1679
1680    // Get keyword
1681    let Some(keyword) = tokens.get(idx) else {
1682        return;
1683    };
1684    idx += 1;
1685
1686    let kind = match *keyword {
1687        "struct" => SymbolKind::Struct,
1688        "enum" => SymbolKind::Enum,
1689        "fn" => SymbolKind::Function,
1690        "type" => SymbolKind::TypeAlias,
1691        "const" => SymbolKind::Const,
1692        "static" => SymbolKind::Static,
1693        "trait" => SymbolKind::Trait,
1694        "mod" => SymbolKind::Mod,
1695        "impl" => return, // Skip impl blocks - they don't create new symbols
1696        _ => return,
1697    };
1698
1699    // Get name (may include generics like "Foo<T>")
1700    let Some(name_token) = tokens.get(idx) else {
1701        return;
1702    };
1703    let name = name_token
1704        .split(['<', '{', '(', ':'])
1705        .next()
1706        .unwrap_or(name_token);
1707
1708    // Build full symbol path
1709    let target_str = target.to_string();
1710    let is_crate_root = target_str == "crate";
1711    let full_path = if is_crate_root {
1712        ryo_symbol::SymbolPath::parse(&format!("crate::{}", name))
1713    } else {
1714        ryo_symbol::SymbolPath::parse(&format!("{}::{}", target_str, name))
1715    };
1716
1717    let Ok(path) = full_path else {
1718        return;
1719    };
1720
1721    // Register in registry (ignore errors - symbol may already exist)
1722    let _ = ctx.registry_mut().register(path, kind);
1723}
1724
1725/// Get a short type name for a MutationSpec
1726fn spec_type_name(spec: &MutationSpec) -> String {
1727    match spec {
1728        MutationSpec::Rename { .. } => "Rename".to_string(),
1729        MutationSpec::AddField { .. } => "AddField".to_string(),
1730        MutationSpec::RemoveField { .. } => "RemoveField".to_string(),
1731        MutationSpec::ChangeVisibility { .. } => "ChangeVisibility".to_string(),
1732        MutationSpec::AddDerive { .. } => "AddDerive".to_string(),
1733        MutationSpec::RemoveDerive { .. } => "RemoveDerive".to_string(),
1734        MutationSpec::AddVariant { .. } => "AddVariant".to_string(),
1735        MutationSpec::RemoveVariant { .. } => "RemoveVariant".to_string(),
1736        MutationSpec::AddMatchArm { .. } => "AddMatchArm".to_string(),
1737        MutationSpec::RemoveMatchArm { .. } => "RemoveMatchArm".to_string(),
1738        MutationSpec::ReplaceMatchArm { .. } => "ReplaceMatchArm".to_string(),
1739        MutationSpec::AddStructLiteralField { .. } => "AddStructLiteralField".to_string(),
1740        MutationSpec::RemoveStructLiteralField { .. } => "RemoveStructLiteralField".to_string(),
1741        MutationSpec::AddItem { .. } => "AddItem".to_string(),
1742        MutationSpec::RemoveItem { .. } => "RemoveItem".to_string(),
1743        MutationSpec::AddMethod { .. } => "AddMethod".to_string(),
1744        MutationSpec::RemoveMethod { .. } => "RemoveMethod".to_string(),
1745        MutationSpec::RemoveMod { .. } => "RemoveMod".to_string(),
1746        MutationSpec::CreateMod { .. } => "CreateMod".to_string(),
1747        MutationSpec::OrganizeImports { .. } => "OrganizeImports".to_string(),
1748        MutationSpec::LoopToIterator { .. } => "LoopToIterator".to_string(),
1749        MutationSpec::UnwrapToQuestion { .. } => "UnwrapToQuestion".to_string(),
1750        MutationSpec::AddSpec { .. } => "AddSpec".to_string(),
1751        MutationSpec::RemoveSpec { .. } => "RemoveSpec".to_string(),
1752        MutationSpec::ValidateSpec { .. } => "ValidateSpec".to_string(),
1753        MutationSpec::ExtractTrait { .. } => "ExtractTrait".to_string(),
1754        MutationSpec::InlineTrait { .. } => "InlineTrait".to_string(),
1755        MutationSpec::ReplaceType { .. } => "ReplaceType".to_string(),
1756        MutationSpec::EnumToTrait { .. } => "EnumToTrait".to_string(),
1757        MutationSpec::MoveItem { .. } => "MoveItem".to_string(),
1758        MutationSpec::AssignOp { .. } => "AssignOp".to_string(),
1759        MutationSpec::BoolSimplify { .. } => "BoolSimplify".to_string(),
1760        MutationSpec::CloneOnCopy { .. } => "CloneOnCopy".to_string(),
1761        MutationSpec::CollapsibleIf { .. } => "CollapsibleIf".to_string(),
1762        MutationSpec::NoOpArmToTodo { .. } => "NoOpArmToTodo".to_string(),
1763        MutationSpec::ComparisonToMethod { .. } => "ComparisonToMethod".to_string(),
1764        MutationSpec::RedundantClosure { .. } => "RedundantClosure".to_string(),
1765        MutationSpec::IntroduceVariable { .. } => "IntroduceVariable".to_string(),
1766        MutationSpec::ManualMap { .. } => "ManualMap".to_string(),
1767        MutationSpec::MatchToIfLet { .. } => "MatchToIfLet".to_string(),
1768        MutationSpec::FilterNext { .. } => "FilterNext".to_string(),
1769        MutationSpec::MapUnwrapOr { .. } => "MapUnwrapOr".to_string(),
1770        MutationSpec::ReplaceExpr { .. } => "ReplaceExpr".to_string(),
1771        MutationSpec::RemoveStatement { .. } => "RemoveStatement".to_string(),
1772        MutationSpec::InsertStatement { .. } => "InsertStatement".to_string(),
1773        MutationSpec::ReplaceStatement { .. } => "ReplaceStatement".to_string(),
1774        MutationSpec::PluginTransform { .. } => "PluginTransform".to_string(),
1775        MutationSpec::DuplicateFunction { .. } => "DuplicateFunction".to_string(),
1776        MutationSpec::DuplicateStruct { .. } => "DuplicateStruct".to_string(),
1777        MutationSpec::DuplicateEnum { .. } => "DuplicateEnum".to_string(),
1778        MutationSpec::DuplicateModTree { .. } => "DuplicateModTree".to_string(),
1779    }
1780}
1781
1782#[cfg(test)]
1783mod tests {
1784    use super::*;
1785    use crate::executor::spec::{Scope, SelfParam, SymbolPath, Visibility};
1786    use ryo_analysis::testing::{ContextBuilder, ContextTestExt};
1787    use ryo_symbol::SymbolId;
1788
1789    /// Create a dummy SymbolId for testing
1790    fn dummy_id(index: u32) -> SymbolId {
1791        SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
1792    }
1793
1794    /// Helper function that executes a blueprint and syncs files.
1795    /// This is the standard pattern for tests that need to read from ctx.files() after execution.
1796    fn execute_and_sync(
1797        executor: &BlueprintExecutor,
1798        blueprint: &ParallelBlueprint,
1799        ctx: &mut AnalysisContext,
1800    ) -> BlueprintResult {
1801        let result = executor.execute_v2(blueprint, ctx);
1802        if result.success {
1803            BlueprintExecutor::sync_files_and_rebuild(&result, ctx).unwrap();
1804        }
1805        result
1806    }
1807
1808    fn create_test_context() -> AnalysisContext {
1809        let code = r#"
1810struct Config {
1811    name: String,
1812}
1813
1814impl Config {
1815    fn new() -> Self {
1816        Self { name: String::new() }
1817    }
1818}
1819"#;
1820        ContextBuilder::new()
1821            .with_file("src/config.rs", code)
1822            .build()
1823    }
1824
1825    #[test]
1826    fn test_blueprint_executor_rename() {
1827        let mut ctx = create_test_context();
1828
1829        // Look up the symbol_id for Config from the context's registry
1830        let symbol_id = ctx
1831            .registry()
1832            .lookup_by_name("Config")
1833            .expect("Config should exist in registry");
1834
1835        let specs = vec![MutationSpec::Rename {
1836            target: MutationTargetSymbol::ById(symbol_id),
1837            to: "AppConfig".to_string(),
1838            scope: Scope::Project,
1839        }];
1840        let blueprint = ParallelBlueprint::from_mutations(specs);
1841
1842        let executor = BlueprintExecutor::new();
1843        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1844
1845        assert!(exec_result.success);
1846        assert!(exec_result.total_changes > 0);
1847
1848        // Verify the rename happened
1849        let file = ctx.test_file("src/config.rs").unwrap();
1850        let source = file.to_source().unwrap();
1851        assert!(source.contains("AppConfig"));
1852        assert!(!source.contains("struct Config"));
1853    }
1854
1855    #[test]
1856    fn test_blueprint_executor_add_derive() {
1857        let mut ctx = create_test_context();
1858
1859        // Lookup the actual SymbolId (file is src/config.rs, so path is crate::config::Config)
1860        let path = SymbolPath::parse("test_crate::config::Config").unwrap();
1861        let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
1862
1863        let specs = vec![MutationSpec::AddDerive {
1864            target: MutationTargetSymbol::ById(symbol_id),
1865            derives: vec!["Debug".to_string(), "Clone".to_string()],
1866        }];
1867        let blueprint = ParallelBlueprint::from_mutations(specs);
1868
1869        let executor = BlueprintExecutor::new();
1870        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1871
1872        assert!(exec_result.success);
1873
1874        let file = ctx.test_file("src/config.rs").unwrap();
1875        let source = file.to_source().unwrap();
1876        assert!(source.contains("derive"), "Expected derive in: {}", source);
1877        assert!(source.contains("Debug"));
1878    }
1879
1880    #[test]
1881    fn test_blueprint_executor_organize_imports() {
1882        let code = r#"
1883use std::collections::HashMap;
1884use std::io::Write;
1885use std::collections::HashSet;
1886use std::io::Read;
1887
1888fn main() {}
1889"#;
1890        let mut ctx = ContextBuilder::new().with_file("src/main.rs", code).build();
1891
1892        let specs = vec![MutationSpec::OrganizeImports {
1893            module_id: None,
1894            deduplicate: true,
1895            merge_groups: true,
1896        }];
1897        let blueprint = ParallelBlueprint::from_mutations(specs);
1898
1899        let executor = BlueprintExecutor::new();
1900        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1901
1902        assert!(exec_result.success);
1903    }
1904
1905    #[test]
1906    fn test_blueprint_with_conflicts_fails_when_not_ignored() {
1907        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
1908
1909        let mut ctx = create_test_context();
1910
1911        // Create dummy symbol IDs for testing actual conflicts
1912        let mut symbol_registry = SymbolRegistry::new();
1913        let path_a = SymbolPath::parse("test_crate::A").unwrap();
1914        let symbol_a = symbol_registry
1915            .register(path_a, SymbolKind::Struct)
1916            .unwrap();
1917
1918        // Create blueprint with actual conflicts (same target renamed twice)
1919        let specs = vec![
1920            MutationSpec::Rename {
1921                target: MutationTargetSymbol::ById(symbol_a),
1922                to: "B".to_string(),
1923                scope: Scope::Project,
1924            },
1925            MutationSpec::Rename {
1926                target: MutationTargetSymbol::ById(symbol_a),
1927                to: "C".to_string(),
1928                scope: Scope::Project,
1929            },
1930        ];
1931
1932        let blueprint = ParallelBlueprint::from_mutations(specs);
1933
1934        // Verify blueprint has conflicts
1935        assert!(
1936            !blueprint.conflicts.is_empty(),
1937            "Blueprint should have conflicts"
1938        );
1939
1940        // With ignore_conflicts = false, conflicts should fail
1941        let mut executor = BlueprintExecutor::new();
1942        executor.ignore_conflicts = false;
1943        let result = execute_and_sync(&executor, &blueprint, &mut ctx);
1944
1945        // Should fail because of conflicts
1946        assert!(
1947            !result.success,
1948            "Execution should fail when conflicts are not ignored"
1949        );
1950        assert!(result.error.is_some(), "Should have error message");
1951        assert!(
1952            result.error.unwrap().contains("conflict"),
1953            "Error should mention conflicts"
1954        );
1955    }
1956
1957    // Note: Intent-based tests have been removed as they use deprecated ryo_core::Intent.
1958    // Converter-level tests in registry/converters/ provide comprehensive coverage.
1959    // TODO: Add MutationSpec-based integration tests if needed.
1960
1961    // === AddItem Tests ===
1962
1963    #[test]
1964    fn test_blueprint_executor_add_item_struct() {
1965        let mut ctx = ContextBuilder::new()
1966            .with_file("src/lib.rs", "// empty file\n")
1967            .build();
1968
1969        let spec = MutationSpec::AddItem {
1970            target: MutationTargetSymbol::ByPath(Box::new(
1971                SymbolPath::parse("test_crate").unwrap(),
1972            )),
1973            content: "pub struct Config {}".to_string(),
1974            position: super::super::spec::InsertPosition::Bottom,
1975        };
1976
1977        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1978        let executor = BlueprintExecutor::new();
1979        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1980
1981        assert!(
1982            exec_result.success,
1983            "AddItem struct failed: {:?}",
1984            exec_result.error
1985        );
1986
1987        let file = ctx.test_file("src/lib.rs").unwrap();
1988        let source = file.to_source().unwrap();
1989        assert!(
1990            source.contains("pub struct Config"),
1991            "Struct not added: {}",
1992            source
1993        );
1994    }
1995
1996    #[test]
1997    fn test_blueprint_executor_add_item_fn() {
1998        let mut ctx = ContextBuilder::new()
1999            .with_file("src/lib.rs", "// empty file\n")
2000            .build();
2001
2002        let spec = MutationSpec::AddItem {
2003            target: MutationTargetSymbol::ByPath(Box::new(
2004                SymbolPath::parse("test_crate").unwrap(),
2005            )),
2006            content: "fn helper() {}".to_string(),
2007            position: super::super::spec::InsertPosition::Bottom,
2008        };
2009
2010        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2011        let executor = BlueprintExecutor::new();
2012        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2013
2014        assert!(
2015            exec_result.success,
2016            "AddItem fn failed: {:?}",
2017            exec_result.error
2018        );
2019
2020        let file = ctx.test_file("src/lib.rs").unwrap();
2021        let source = file.to_source().unwrap();
2022        assert!(
2023            source.contains("fn helper"),
2024            "Function not added: {}",
2025            source
2026        );
2027    }
2028
2029    #[test]
2030    fn test_blueprint_executor_add_item_use() {
2031        let mut ctx = ContextBuilder::new()
2032            .with_file("src/lib.rs", "pub struct Dummy;\n")
2033            .build();
2034
2035        let spec = MutationSpec::AddItem {
2036            target: MutationTargetSymbol::ByPath(Box::new(
2037                SymbolPath::parse("test_crate").unwrap(),
2038            )),
2039            content: "use HashMap;".to_string(),
2040            position: super::super::spec::InsertPosition::Top,
2041        };
2042
2043        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2044        let executor = BlueprintExecutor::new();
2045        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2046
2047        assert!(
2048            exec_result.success,
2049            "AddItem use failed: {:?}",
2050            exec_result.error
2051        );
2052
2053        let file = ctx.test_file("src/lib.rs").unwrap();
2054        let source = file.to_source().unwrap();
2055        assert!(source.contains("use HashMap"), "Use not added: {}", source);
2056    }
2057
2058    #[test]
2059    fn test_blueprint_executor_add_item_impl() {
2060        let mut ctx = ContextBuilder::new()
2061            .with_file("src/lib.rs", "struct Foo {}\n")
2062            .build();
2063
2064        let spec = MutationSpec::AddItem {
2065            target: MutationTargetSymbol::ByPath(Box::new(
2066                SymbolPath::parse("test_crate").unwrap(),
2067            )),
2068            content: "impl Foo {}".to_string(),
2069            position: super::super::spec::InsertPosition::Bottom,
2070        };
2071
2072        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2073        let executor = BlueprintExecutor::new();
2074        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2075
2076        assert!(
2077            exec_result.success,
2078            "AddItem impl failed: {:?}",
2079            exec_result.error
2080        );
2081
2082        let file = ctx.test_file("src/lib.rs").unwrap();
2083        let source = file.to_source().unwrap();
2084        assert!(source.contains("impl Foo"), "Impl not added: {}", source);
2085    }
2086
2087    #[test]
2088    fn test_blueprint_executor_add_item_enum() {
2089        let mut ctx = ContextBuilder::new()
2090            .with_file("src/lib.rs", "// empty file\n")
2091            .build();
2092
2093        let spec = MutationSpec::AddItem {
2094            target: MutationTargetSymbol::ByPath(Box::new(
2095                SymbolPath::parse("test_crate").unwrap(),
2096            )),
2097            content: "pub enum Status { Pending, Active, Completed }".to_string(),
2098            position: super::super::spec::InsertPosition::Bottom,
2099        };
2100
2101        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2102        let executor = BlueprintExecutor::new();
2103        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2104
2105        assert!(
2106            exec_result.success,
2107            "AddItem enum failed: {:?}",
2108            exec_result.error
2109        );
2110
2111        let file = ctx.test_file("src/lib.rs").unwrap();
2112        let source = file.to_source().unwrap();
2113        assert!(
2114            source.contains("pub enum Status"),
2115            "Enum not added: {}",
2116            source
2117        );
2118    }
2119
2120    // === AddMethod Tests ===
2121
2122    #[test]
2123    fn test_blueprint_executor_add_method_basic() {
2124        let mut ctx = ContextBuilder::new()
2125            .with_file("src/lib.rs", "pub struct Config {}\n\nimpl Config {}\n")
2126            .build();
2127
2128        let spec = MutationSpec::AddMethod {
2129            target: MutationTargetSymbol::ByKindAndName(
2130                crate::executor::ItemKind::Impl,
2131                "Config".to_string(),
2132            ),
2133            method_name: "new".to_string(),
2134            params: vec![],
2135            return_type: Some("Self".to_string()),
2136            body: "Self {}".to_string(),
2137            is_pub: true,
2138            self_param: None,
2139        };
2140
2141        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2142        let executor = BlueprintExecutor::new();
2143        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2144
2145        assert!(
2146            exec_result.success,
2147            "AddMethod basic failed: {:?}",
2148            exec_result.error
2149        );
2150
2151        let file = ctx.test_file("src/lib.rs").unwrap();
2152        let source = file.to_source().unwrap();
2153        assert!(
2154            source.contains("pub fn new"),
2155            "Method not added: {}",
2156            source
2157        );
2158        assert!(
2159            source.contains("-> Self"),
2160            "Return type not added: {}",
2161            source
2162        );
2163    }
2164
2165    #[test]
2166    fn test_blueprint_executor_add_method_with_self() {
2167        let mut ctx = ContextBuilder::new()
2168            .with_file(
2169                "src/lib.rs",
2170                "pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
2171            )
2172            .build();
2173
2174        let spec = MutationSpec::AddMethod {
2175            target: MutationTargetSymbol::ByKindAndName(
2176                crate::executor::ItemKind::Impl,
2177                "Counter".to_string(),
2178            ),
2179            method_name: "get".to_string(),
2180            params: vec![],
2181            return_type: Some("u32".to_string()),
2182            body: "self.value".to_string(),
2183            is_pub: true,
2184            self_param: Some(SelfParam::Ref),
2185        };
2186
2187        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2188        let executor = BlueprintExecutor::new();
2189        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2190
2191        assert!(
2192            exec_result.success,
2193            "AddMethod with_self failed: {:?}",
2194            exec_result.error
2195        );
2196
2197        let file = ctx.test_file("src/lib.rs").unwrap();
2198        let source = file.to_source().unwrap();
2199        assert!(source.contains("&self"), "Self param not added: {}", source);
2200        assert!(source.contains("fn get"), "Method not added: {}", source);
2201    }
2202
2203    #[test]
2204    fn test_blueprint_executor_add_method_with_mut_self() {
2205        let mut ctx = ContextBuilder::new()
2206            .with_file(
2207                "src/lib.rs",
2208                "pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
2209            )
2210            .build();
2211
2212        let spec = MutationSpec::AddMethod {
2213            target: MutationTargetSymbol::ByKindAndName(
2214                crate::executor::ItemKind::Impl,
2215                "Counter".to_string(),
2216            ),
2217            method_name: "increment".to_string(),
2218            params: vec![],
2219            return_type: None,
2220            body: "self.value += 1".to_string(),
2221            is_pub: true,
2222            self_param: Some(SelfParam::Mut),
2223        };
2224
2225        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2226        let executor = BlueprintExecutor::new();
2227        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2228
2229        assert!(
2230            exec_result.success,
2231            "AddMethod with_mut_self failed: {:?}",
2232            exec_result.error
2233        );
2234
2235        let file = ctx.test_file("src/lib.rs").unwrap();
2236        let source = file.to_source().unwrap();
2237        assert!(
2238            source.contains("&mut self"),
2239            "Mut self param not added: {}",
2240            source
2241        );
2242        assert!(
2243            source.contains("fn increment"),
2244            "Method not added: {}",
2245            source
2246        );
2247    }
2248
2249    #[test]
2250    fn test_blueprint_executor_add_method_with_params() {
2251        let mut ctx = ContextBuilder::new()
2252            .with_file(
2253                "src/lib.rs",
2254                "pub struct Calculator {}\n\nimpl Calculator {}\n",
2255            )
2256            .build();
2257
2258        let spec = MutationSpec::AddMethod {
2259            target: MutationTargetSymbol::ByKindAndName(
2260                crate::executor::ItemKind::Impl,
2261                "Calculator".to_string(),
2262            ),
2263            method_name: "add".to_string(),
2264            params: vec![
2265                ("a".to_string(), "i32".to_string()),
2266                ("b".to_string(), "i32".to_string()),
2267            ],
2268            return_type: Some("i32".to_string()),
2269            body: "a + b".to_string(),
2270            is_pub: true,
2271            self_param: None,
2272        };
2273
2274        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2275        let executor = BlueprintExecutor::new();
2276        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2277
2278        assert!(
2279            exec_result.success,
2280            "AddMethod with_params failed: {:?}",
2281            exec_result.error
2282        );
2283
2284        let file = ctx.test_file("src/lib.rs").unwrap();
2285        let source = file.to_source().unwrap();
2286        assert!(source.contains("fn add"), "Method not added: {}", source);
2287        assert!(source.contains("a: i32"), "Param a not added: {}", source);
2288        assert!(source.contains("b: i32"), "Param b not added: {}", source);
2289    }
2290
2291    // === Multi-file Tests ===
2292
2293    #[test]
2294    fn test_blueprint_executor_multi_file_rename() {
2295        let mut ctx = ContextBuilder::new()
2296            .with_file(
2297                "src/lib.rs",
2298                r#"mod models;
2299
2300fn process(task: Task) -> Task {
2301    task
2302}
2303"#,
2304            )
2305            .with_file(
2306                "src/models.rs",
2307                r#"pub struct Task {
2308    pub id: u32,
2309    pub name: String,
2310}
2311"#,
2312            )
2313            .build();
2314
2315        // Look up the symbol_id for Task from the context's registry
2316        let symbol_id = ctx
2317            .registry()
2318            .lookup_by_name("Task")
2319            .expect("Task should exist in registry");
2320
2321        // Rename Task to TodoItem across all files
2322        let spec = MutationSpec::Rename {
2323            target: MutationTargetSymbol::ById(symbol_id),
2324            to: "TodoItem".to_string(),
2325            scope: Scope::Project,
2326        };
2327
2328        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2329        let executor = BlueprintExecutor::new();
2330        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2331
2332        assert!(
2333            exec_result.success,
2334            "Multi-file rename failed: {:?}",
2335            exec_result.error
2336        );
2337
2338        // Check lib.rs type annotations were renamed
2339        let lib = ctx.test_file("src/lib.rs").unwrap();
2340        let lib_source = lib.to_source().unwrap();
2341        assert!(
2342            lib_source.contains("task: TodoItem"),
2343            "lib.rs type not renamed: {}",
2344            lib_source
2345        );
2346        assert!(
2347            lib_source.contains("-> TodoItem"),
2348            "lib.rs return type not renamed: {}",
2349            lib_source
2350        );
2351
2352        // Check models.rs struct was renamed
2353        let models = ctx.test_file("src/models.rs").unwrap();
2354        let models_source = models.to_source().unwrap();
2355        assert!(
2356            models_source.contains("struct TodoItem"),
2357            "models.rs struct not renamed: {}",
2358            models_source
2359        );
2360        assert!(
2361            !models_source.contains("struct Task"),
2362            "models.rs still has struct Task: {}",
2363            models_source
2364        );
2365    }
2366
2367    #[test]
2368    fn test_blueprint_executor_multi_file_add_items() {
2369        let mut ctx = ContextBuilder::new()
2370            .with_file("src/lib.rs", "pub mod models;\n")
2371            .with_file("src/models.rs", "pub struct Placeholder;\n")
2372            .build();
2373
2374        // Add struct to models.rs
2375        let spec1 = MutationSpec::AddItem {
2376            target: MutationTargetSymbol::ByPath(Box::new(
2377                SymbolPath::parse("test_crate::models").unwrap(),
2378            )),
2379            content: "pub struct User { id: u32 }".to_string(),
2380            position: super::super::spec::InsertPosition::Bottom,
2381        };
2382
2383        // Add use to lib.rs
2384        let spec2 = MutationSpec::AddItem {
2385            target: MutationTargetSymbol::ByPath(Box::new(
2386                SymbolPath::parse("test_crate").unwrap(),
2387            )),
2388            content: "use models::User;".to_string(),
2389            position: super::super::spec::InsertPosition::Bottom,
2390        };
2391
2392        let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
2393        let executor = BlueprintExecutor::new();
2394        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2395
2396        assert!(
2397            exec_result.success,
2398            "Multi-file add items failed: {:?}",
2399            exec_result.error
2400        );
2401
2402        // Check models.rs has User struct
2403        let models = ctx.test_file("src/models.rs").unwrap();
2404        let models_source = models.to_source().unwrap();
2405        assert!(
2406            models_source.contains("pub struct User"),
2407            "User not added to models.rs: {}",
2408            models_source
2409        );
2410
2411        // Check lib.rs has use statement
2412        let lib = ctx.test_file("src/lib.rs").unwrap();
2413        let lib_source = lib.to_source().unwrap();
2414        assert!(
2415            lib_source.contains("use models::User"),
2416            "Use not added to lib.rs: {}",
2417            lib_source
2418        );
2419    }
2420
2421    // === Generic and Async Tests ===
2422
2423    #[test]
2424    fn test_blueprint_executor_add_item_generic_struct() {
2425        let mut ctx = ContextBuilder::new()
2426            .with_file("src/lib.rs", "// empty file\n")
2427            .build();
2428
2429        let spec = MutationSpec::AddItem {
2430            target: MutationTargetSymbol::ByPath(Box::new(
2431                SymbolPath::parse("test_crate").unwrap(),
2432            )),
2433            content: "pub struct Container<T> { value: T }".to_string(),
2434            position: super::super::spec::InsertPosition::Bottom,
2435        };
2436
2437        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2438        let executor = BlueprintExecutor::new();
2439        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2440
2441        assert!(
2442            exec_result.success,
2443            "AddItem generic struct failed: {:?}",
2444            exec_result.error
2445        );
2446
2447        let file = ctx.test_file("src/lib.rs").unwrap();
2448        let source = file.to_source().unwrap();
2449        assert!(
2450            source.contains("struct Container"),
2451            "Generic struct not added: {}",
2452            source
2453        );
2454    }
2455
2456    #[test]
2457    fn test_blueprint_executor_add_item_async_fn() {
2458        let mut ctx = ContextBuilder::new()
2459            .with_file("src/lib.rs", "// empty file\n")
2460            .build();
2461
2462        let spec = MutationSpec::AddItem {
2463            target: MutationTargetSymbol::ByPath(Box::new(
2464                SymbolPath::parse("test_crate").unwrap(),
2465            )),
2466            content: "pub async fn fetch_data() -> String { String::new() }".to_string(),
2467            position: super::super::spec::InsertPosition::Bottom,
2468        };
2469
2470        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2471        let executor = BlueprintExecutor::new();
2472        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2473
2474        assert!(
2475            exec_result.success,
2476            "AddItem async fn failed: {:?}",
2477            exec_result.error
2478        );
2479
2480        let file = ctx.test_file("src/lib.rs").unwrap();
2481        let source = file.to_source().unwrap();
2482        assert!(
2483            source.contains("fn fetch_data"),
2484            "Async fn not added: {}",
2485            source
2486        );
2487    }
2488
2489    #[test]
2490    fn test_blueprint_executor_add_field_to_generic_struct() {
2491        use ryo_analysis::SymbolKind;
2492
2493        let mut ctx = ContextBuilder::new()
2494            .with_file("src/lib.rs", "pub struct Wrapper<T> { inner: T }\n")
2495            .build();
2496
2497        // Get symbol_id for Wrapper struct
2498        let symbol_id = ctx
2499            .registry
2500            .iter()
2501            .find(|(id, path)| {
2502                path.name() == "Wrapper" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2503            })
2504            .map(|(id, _)| id)
2505            .expect("Wrapper struct not found in registry");
2506
2507        let spec = MutationSpec::AddField {
2508            target: MutationTargetSymbol::ById(symbol_id),
2509            field_name: "count".to_string(),
2510            field_type: "usize".to_string(),
2511            visibility: Visibility::Pub,
2512        };
2513
2514        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2515        let executor = BlueprintExecutor::new();
2516        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2517
2518        assert!(
2519            exec_result.success,
2520            "AddField to generic struct failed: {:?}",
2521            exec_result.error
2522        );
2523
2524        let file = ctx.test_file("src/lib.rs").unwrap();
2525        let source = file.to_source().unwrap();
2526        assert!(
2527            source.contains("pub count: usize"),
2528            "Field not added to generic struct: {}",
2529            source
2530        );
2531    }
2532
2533    #[test]
2534    fn test_blueprint_executor_add_derive_to_generic_struct() {
2535        let mut ctx = ContextBuilder::new()
2536            .with_file(
2537                "src/lib.rs",
2538                "pub struct Pair<T, U> { first: T, second: U }\n",
2539            )
2540            .build();
2541
2542        // Lookup the actual SymbolId
2543        let path = SymbolPath::parse("test_crate::Pair").unwrap();
2544        let symbol_id = ctx.registry().lookup(&path).expect("Pair should exist");
2545
2546        let spec = MutationSpec::AddDerive {
2547            target: MutationTargetSymbol::ById(symbol_id),
2548            derives: vec!["Debug".to_string(), "Clone".to_string()],
2549        };
2550
2551        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2552        let executor = BlueprintExecutor::new();
2553        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2554
2555        assert!(
2556            exec_result.success,
2557            "AddDerive to generic struct failed: {:?}",
2558            exec_result.error
2559        );
2560
2561        let file = ctx.test_file("src/lib.rs").unwrap();
2562        let source = file.to_source().unwrap();
2563        assert!(
2564            source.contains("Debug"),
2565            "Debug derive not added: {}",
2566            source
2567        );
2568        assert!(
2569            source.contains("Clone"),
2570            "Clone derive not added: {}",
2571            source
2572        );
2573    }
2574
2575    // === Module Tests ===
2576    // Note: AddMod was consolidated into CreateMod. These tests use CreateMod with empty content
2577    // to test the mod declaration functionality.
2578
2579    #[test]
2580    fn test_blueprint_executor_create_mod_declaration() {
2581        let mut ctx = ContextBuilder::new()
2582            .with_file("src/lib.rs", "use std::io;\n\nfn main() {}\n")
2583            .build();
2584
2585        let spec = MutationSpec::CreateMod {
2586            target: MutationTargetSymbol::ByPath(Box::new(
2587                SymbolPath::parse("test_crate").unwrap(),
2588            )),
2589            mod_name: "models".to_string(),
2590            content: String::new(),
2591            is_pub: false,
2592        };
2593
2594        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2595        let executor = BlueprintExecutor::new();
2596        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2597
2598        assert!(
2599            exec_result.success,
2600            "CreateMod failed: {:?}",
2601            exec_result.error
2602        );
2603
2604        let file = ctx.test_file("src/lib.rs").unwrap();
2605        let source = file.to_source().unwrap();
2606        assert!(source.contains("mod models;"), "Mod not added: {}", source);
2607    }
2608
2609    #[test]
2610    fn test_blueprint_executor_create_pub_mod_declaration() {
2611        let mut ctx = ContextBuilder::new()
2612            .with_file("src/lib.rs", "fn main() {}\n")
2613            .build();
2614
2615        let spec = MutationSpec::CreateMod {
2616            target: MutationTargetSymbol::ByPath(Box::new(
2617                SymbolPath::parse("test_crate").unwrap(),
2618            )),
2619            mod_name: "api".to_string(),
2620            content: String::new(),
2621            is_pub: true,
2622        };
2623
2624        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2625        let executor = BlueprintExecutor::new();
2626        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2627
2628        assert!(
2629            exec_result.success,
2630            "CreateMod pub failed: {:?}",
2631            exec_result.error
2632        );
2633
2634        let file = ctx.test_file("src/lib.rs").unwrap();
2635        let source = file.to_source().unwrap();
2636        assert!(
2637            source.contains("pub mod api;"),
2638            "Pub mod not added: {}",
2639            source
2640        );
2641    }
2642
2643    #[test]
2644    fn test_blueprint_executor_create_file() {
2645        // NOTE: Don't pre-declare "mod models;" - CreateMod will add both the declaration and content
2646        let mut ctx = ContextBuilder::new()
2647            .with_file("src/lib.rs", "// lib.rs\n")
2648            .build();
2649
2650        let spec = MutationSpec::CreateMod {
2651            target: MutationTargetSymbol::ByPath(Box::new(
2652                SymbolPath::parse("test_crate").unwrap(),
2653            )),
2654            mod_name: "models".to_string(),
2655            content: "pub struct Model { id: u32 }".to_string(),
2656            is_pub: true,
2657        };
2658
2659        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2660        let executor = BlueprintExecutor::new();
2661        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2662
2663        assert!(
2664            exec_result.success,
2665            "CreateFile failed: {:?}",
2666            exec_result.error
2667        );
2668
2669        // Check file was created (file style: src/models.rs - Ryo uses file-style modules by default)
2670        assert!(ctx.test_file("src/models.rs").is_some(), "File not created");
2671
2672        let file = ctx.test_file("src/models.rs").unwrap();
2673        let source = file.to_source().unwrap();
2674        assert!(
2675            source.contains("struct Model"),
2676            "Content not correct: {}",
2677            source
2678        );
2679    }
2680
2681    #[test]
2682    fn test_blueprint_executor_create_module_workflow() {
2683        let mut ctx = ContextBuilder::new()
2684            .with_file("src/lib.rs", "fn main() {}\n")
2685            .build();
2686
2687        // CreateMod handles both mod declaration and file creation
2688        let spec = MutationSpec::CreateMod {
2689            target: MutationTargetSymbol::ByPath(Box::new(
2690                SymbolPath::parse("test_crate").unwrap(),
2691            )),
2692            mod_name: "utils".to_string(),
2693            content: "pub fn helper() {}".to_string(),
2694            is_pub: true,
2695        };
2696
2697        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2698        let executor = BlueprintExecutor::new();
2699        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2700
2701        assert!(
2702            exec_result.success,
2703            "Module workflow failed: {:?}",
2704            exec_result.error
2705        );
2706
2707        // Check lib.rs has mod declaration
2708        let lib = ctx.test_file("src/lib.rs").unwrap();
2709        let lib_source = lib.to_source().unwrap();
2710        assert!(
2711            lib_source.contains("pub mod utils;"),
2712            "Mod not added to lib: {}",
2713            lib_source
2714        );
2715
2716        // Check utils file exists and has function (file style: src/utils.rs)
2717        let utils = ctx.test_file("src/utils.rs").unwrap();
2718        let utils_source = utils.to_source().unwrap();
2719        assert!(
2720            utils_source.contains("fn helper"),
2721            "Function not in utils: {}",
2722            utils_source
2723        );
2724    }
2725
2726    // === Wavefront Execution Tests ===
2727
2728    #[test]
2729    fn test_wavefront_execution_basic() {
2730        let mut ctx = create_test_context();
2731
2732        // Lookup the actual SymbolId (file is src/config.rs, so path is crate::config::Config)
2733        let path = SymbolPath::parse("test_crate::config::Config").unwrap();
2734        let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
2735
2736        let specs = vec![MutationSpec::AddDerive {
2737            target: MutationTargetSymbol::ById(symbol_id),
2738            derives: vec!["Debug".to_string()],
2739        }];
2740        let blueprint = ParallelBlueprint::from_mutations(specs);
2741
2742        let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
2743        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2744
2745        assert!(
2746            exec_result.success,
2747            "Wavefront basic failed: {:?}",
2748            exec_result.error
2749        );
2750        assert!(exec_result.total_changes > 0);
2751
2752        let file = ctx.test_file("src/config.rs").unwrap();
2753        let source = file.to_source().unwrap();
2754        assert!(source.contains("Debug"), "Derive not added: {}", source);
2755    }
2756
2757    #[test]
2758    fn test_wavefront_execution_multi_file() {
2759        let mut ctx = ContextBuilder::new()
2760            .with_file("src/lib.rs", "// lib\n")
2761            .with_file("src/models.rs", "// models\n")
2762            .build();
2763
2764        // Add items to different files (can run in parallel)
2765        let spec1 = MutationSpec::AddItem {
2766            target: MutationTargetSymbol::ByPath(Box::new(
2767                SymbolPath::parse("test_crate::models").unwrap(),
2768            )),
2769            content: "pub struct User { id: u32 }".to_string(),
2770            position: super::super::spec::InsertPosition::Bottom,
2771        };
2772
2773        let spec2 = MutationSpec::AddItem {
2774            target: MutationTargetSymbol::ByPath(Box::new(
2775                SymbolPath::parse("test_crate").unwrap(),
2776            )),
2777            content: "fn main() {}".to_string(),
2778            position: super::super::spec::InsertPosition::Bottom,
2779        };
2780
2781        let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
2782        let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
2783        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2784
2785        assert!(
2786            exec_result.success,
2787            "Wavefront multi-file failed: {:?}",
2788            exec_result.error
2789        );
2790
2791        let models = ctx.test_file("src/models.rs").unwrap();
2792        assert!(
2793            models.to_source().unwrap().contains("pub struct User"),
2794            "User not added to models.rs"
2795        );
2796
2797        let lib = ctx.test_file("src/lib.rs").unwrap();
2798        assert!(
2799            lib.to_source().unwrap().contains("fn main"),
2800            "main not added to lib.rs"
2801        );
2802    }
2803
2804    #[test]
2805    fn test_wavefront_execution_with_dependencies() {
2806        let mut ctx = ContextBuilder::new()
2807            .with_file("src/lib.rs", "fn main() {}\n")
2808            .build();
2809
2810        // CreateMod handles both mod declaration and file creation
2811        let spec = MutationSpec::CreateMod {
2812            target: MutationTargetSymbol::ByPath(Box::new(
2813                SymbolPath::parse("test_crate").unwrap(),
2814            )),
2815            mod_name: "api".to_string(),
2816            content: "pub fn endpoint() {}".to_string(),
2817            is_pub: true,
2818        };
2819
2820        let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
2821        let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
2822        let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
2823
2824        assert!(
2825            exec_result.success,
2826            "Wavefront with deps failed: {:?}",
2827            exec_result.error
2828        );
2829
2830        // Verify dependency order was respected
2831        let lib = ctx.test_file("src/lib.rs").unwrap();
2832        assert!(
2833            lib.to_source().unwrap().contains("pub mod api;"),
2834            "Mod not added to lib"
2835        );
2836
2837        // Check api file (file style: src/api.rs)
2838        let api = ctx.test_file("src/api.rs").unwrap();
2839        assert!(
2840            api.to_source().unwrap().contains("fn endpoint"),
2841            "Function not in api"
2842        );
2843    }
2844
2845    #[test]
2846    fn test_suggest_strategy() {
2847        // Single mutation → Sequential
2848        let specs = vec![MutationSpec::AddDerive {
2849            target: MutationTargetSymbol::ById(dummy_id(1)),
2850            derives: vec!["Debug".to_string()],
2851        }];
2852        let blueprint = ParallelBlueprint::from_mutations(specs);
2853        assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
2854
2855        // Two mutations → Sequential (too few)
2856        let specs = vec![
2857            MutationSpec::AddDerive {
2858                target: MutationTargetSymbol::ById(dummy_id(1)),
2859                derives: vec!["Debug".to_string()],
2860            },
2861            MutationSpec::AddDerive {
2862                target: MutationTargetSymbol::ById(dummy_id(1)),
2863                derives: vec!["Clone".to_string()],
2864            },
2865        ];
2866        let blueprint = ParallelBlueprint::from_mutations(specs);
2867        assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
2868
2869        // Multiple independent mutations → Wavefront
2870        let specs = vec![
2871            MutationSpec::AddDerive {
2872                target: MutationTargetSymbol::ById(dummy_id(1)),
2873                derives: vec!["Debug".to_string()],
2874            },
2875            MutationSpec::AddDerive {
2876                target: MutationTargetSymbol::ById(dummy_id(1)),
2877                derives: vec!["Clone".to_string()],
2878            },
2879            MutationSpec::AddDerive {
2880                target: MutationTargetSymbol::ById(dummy_id(1)),
2881                derives: vec!["Default".to_string()],
2882            },
2883            MutationSpec::AddDerive {
2884                target: MutationTargetSymbol::ById(dummy_id(1)),
2885                derives: vec!["Hash".to_string()],
2886            },
2887        ];
2888        let blueprint = ParallelBlueprint::from_mutations(specs);
2889        assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Wavefront);
2890    }
2891
2892    #[test]
2893    #[ignore = "V1 path disabled - needs V2 migration"]
2894    fn test_sequential_and_wavefront_produce_same_result() {
2895        let code = r#"
2896struct A {}
2897struct B {}
2898struct C {}
2899"#;
2900
2901        let specs = vec![
2902            MutationSpec::AddDerive {
2903                target: MutationTargetSymbol::ById(dummy_id(1)),
2904                derives: vec!["Debug".to_string()],
2905            },
2906            MutationSpec::AddDerive {
2907                target: MutationTargetSymbol::ById(dummy_id(1)),
2908                derives: vec!["Clone".to_string()],
2909            },
2910            MutationSpec::AddDerive {
2911                target: MutationTargetSymbol::ById(dummy_id(1)),
2912                derives: vec!["Default".to_string()],
2913            },
2914        ];
2915        let blueprint = ParallelBlueprint::from_mutations(specs.clone());
2916
2917        // Execute with Sequential
2918        let mut ctx_seq = ContextBuilder::new().with_file("src/lib.rs", code).build();
2919        let executor_seq = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Sequential);
2920        let result_seq = execute_and_sync(&executor_seq, &blueprint, &mut ctx_seq);
2921
2922        // Execute with Wavefront
2923        let mut ctx_wave = ContextBuilder::new().with_file("src/lib.rs", code).build();
2924        let executor_wave = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
2925        let result_wave = execute_and_sync(&executor_wave, &blueprint, &mut ctx_wave);
2926
2927        // Both should succeed
2928        assert!(result_seq.success, "Sequential failed");
2929        assert!(result_wave.success, "Wavefront failed");
2930
2931        // Both should produce the same output
2932        let source_seq = ctx_seq
2933            .test_file("src/lib.rs")
2934            .unwrap()
2935            .to_source()
2936            .unwrap();
2937        let source_wave = ctx_wave
2938            .test_file("src/lib.rs")
2939            .unwrap()
2940            .to_source()
2941            .unwrap();
2942
2943        assert!(source_seq.contains("Debug"), "Sequential missing Debug");
2944        assert!(source_seq.contains("Clone"), "Sequential missing Clone");
2945        assert!(source_seq.contains("Default"), "Sequential missing Default");
2946        assert!(source_wave.contains("Debug"), "Wavefront missing Debug");
2947        assert!(source_wave.contains("Clone"), "Wavefront missing Clone");
2948        assert!(source_wave.contains("Default"), "Wavefront missing Default");
2949    }
2950
2951    #[test]
2952    #[ignore = "flaky: µs-level timing comparison depends on CPU load"]
2953    fn test_execute_v2_without_sync_is_faster() {
2954        use ryo_analysis::SymbolKind;
2955        use std::time::Instant;
2956
2957        // Create a larger context for more realistic benchmark
2958        let code = r#"
2959pub struct Config { name: String, value: i32 }
2960pub struct User { id: u64, name: String, email: String }
2961pub struct Order { id: u64, user_id: u64, total: f64 }
2962pub enum Status { Pending, Active, Completed, Failed }
2963pub trait Processor { fn process(&self); }
2964impl Processor for Config { fn process(&self) {} }
2965impl Processor for User { fn process(&self) {} }
2966"#;
2967
2968        // Helper to create specs with resolved symbol_ids
2969        fn create_specs(ctx: &ryo_analysis::AnalysisContext) -> Vec<MutationSpec> {
2970            let config_id = ctx
2971                .registry
2972                .iter()
2973                .find(|(id, path)| {
2974                    path.name() == "Config" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2975                })
2976                .map(|(id, _)| id)
2977                .expect("Config not found");
2978
2979            let user_id = ctx
2980                .registry
2981                .iter()
2982                .find(|(id, path)| {
2983                    path.name() == "User" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2984                })
2985                .map(|(id, _)| id)
2986                .expect("User not found");
2987
2988            vec![
2989                MutationSpec::AddField {
2990                    target: MutationTargetSymbol::ById(config_id),
2991                    field_name: "enabled".to_string(),
2992                    field_type: "bool".to_string(),
2993                    visibility: Visibility::Pub,
2994                },
2995                MutationSpec::AddDerive {
2996                    target: MutationTargetSymbol::ById(user_id),
2997                    derives: vec!["Debug".to_string(), "Clone".to_string()],
2998                },
2999            ]
3000        }
3001
3002        // Benchmark execute_v2 only (no sync)
3003        let iterations = 10;
3004        let mut execute_only_times = Vec::with_capacity(iterations);
3005
3006        for _ in 0..iterations {
3007            let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
3008            let specs = create_specs(&ctx);
3009            let blueprint = ParallelBlueprint::from_mutations(specs);
3010            let executor = BlueprintExecutor::new();
3011
3012            let t0 = Instant::now();
3013            let result = executor.execute_v2(&blueprint, &mut ctx);
3014            execute_only_times.push(t0.elapsed());
3015
3016            assert!(result.success);
3017        }
3018
3019        // Benchmark execute_v2 + sync
3020        let mut execute_with_sync_times = Vec::with_capacity(iterations);
3021
3022        for _ in 0..iterations {
3023            let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
3024            let specs = create_specs(&ctx);
3025            let blueprint = ParallelBlueprint::from_mutations(specs);
3026            let executor = BlueprintExecutor::new();
3027
3028            let t0 = Instant::now();
3029            let result = executor.execute_v2(&blueprint, &mut ctx);
3030            BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
3031            execute_with_sync_times.push(t0.elapsed());
3032
3033            assert!(result.success);
3034        }
3035
3036        let avg_execute_only: u128 = execute_only_times
3037            .iter()
3038            .map(|d| d.as_micros())
3039            .sum::<u128>()
3040            / iterations as u128;
3041        let avg_with_sync: u128 = execute_with_sync_times
3042            .iter()
3043            .map(|d| d.as_micros())
3044            .sum::<u128>()
3045            / iterations as u128;
3046
3047        eprintln!(
3048            "execute_v2 only: {}µs avg, execute_v2 + sync: {}µs avg, sync overhead: {:.1}x",
3049            avg_execute_only,
3050            avg_with_sync,
3051            avg_with_sync as f64 / avg_execute_only as f64
3052        );
3053
3054        // execute_v2 without sync should be faster
3055        assert!(
3056            avg_execute_only < avg_with_sync,
3057            "execute_v2 only ({}µs) should be faster than with sync ({}µs)",
3058            avg_execute_only,
3059            avg_with_sync
3060        );
3061    }
3062}