Skip to main content

ryo_app/
planner.rs

1//! Planner: Goal → Vec<MutationSpec> 変換
2//!
3//! IntentをMutationSpecに変換し、実行計画を生成する。
4//!
5//! ```text
6//! Goal (Intent + Scope + Constraints)
7//!     ↓ Planner::plan()
8//! Vec<MutationSpec>
9//!     ↓ ParallelBlueprint::from_mutations()
10//! ParallelBlueprint
11//!     ↓ BlueprintExecutor::execute()
12//! BlueprintResult
13//! ```
14//!
15//! # 複数Intent対応
16//!
17//! 複数Intentsを受け取り、全てのMutationSpecを生成する。
18//! 独立したIntentsは並列実行され、マイクロ秒オーダーで完了する。
19//!
20//! ## Intent順序と並列実行
21//!
22//! **設計原則**: Intent順序はユーザー指定を尊重し、Plannerは変更しない。
23//!
24//! 並列実行の最適化はMutationSpec以降で行われる:
25//!
26//! ```text
27//! Intent[0, 1, 2, 3] (ユーザー指定順序)
28//!     ↓ Planner::plan() - 順序維持
29//! MutationSpec[0, 1, 2, 3]
30//!     ↓ ParallelBlueprint - ItemRefベースの依存解析
31//! DependencyGraph + Conflict検出
32//!     ↓ topological_levels() - Waveに分割
33//! Wave実行: 同じItemRef → 順序維持、異なるItemRef → 並列化
34//! ```
35//!
36//! **例**: `0[User.email], 1[User.email], 2[Order.id], 3[Product.name]`
37//! - Wave 0: [0]
38//! - Wave 1: [1]
39//! - Wave 2: [2, 3] ← 並列実行
40//!
41//! 詳細は以下を参照:
42//! - `ryo-executor::executor::spec` - ItemRef定義、MutationSpec
43//! - `ryo-executor::executor::blueprint` - DependencyGraph、Conflict検出
44//! - `ryo-executor::executor::blueprint_executor` - Wavefront実行戦略
45
46use crate::intent::{
47    Goal, Intent, ItemKind, SelfParam as IntentSelfParam, SpecRelation as IntentSpecRelation,
48    SpecRelationKind as IntentSpecRelationKind, StmtInsertPosition as IntentStmtPosition,
49    Visibility,
50};
51use ryo_analysis::{SymbolKind, SymbolPath, SymbolRegistry};
52use ryo_executor::{
53    InsertPosition, MutationSpec, MutationTargetSymbol, SelfParam, SpecRelation, SpecRelationKind,
54    StmtInsertPosition, VariantKind,
55};
56use ryo_symbol::SymbolId;
57use std::collections::HashSet;
58
59/// Planning error
60#[derive(Debug, thiserror::Error)]
61pub enum PlanError {
62    #[error("Unsupported intent: {0}")]
63    UnsupportedIntent(String),
64
65    #[error("Invalid pattern: {0}")]
66    InvalidPattern(String),
67
68    #[error("Missing required field: {0}")]
69    MissingField(String),
70
71    #[error("Invalid target '{target}': {reason}")]
72    InvalidTarget { target: String, reason: String },
73
74    #[error("Symbol not found: {name} (kind: {kind:?})")]
75    SymbolNotFound {
76        name: String,
77        kind: Option<SymbolKind>,
78    },
79
80    #[error("Duplicate symbol: {name} (kind: {kind:?}, found {count} matches)")]
81    DuplicateSymbol {
82        name: String,
83        kind: Option<SymbolKind>,
84        count: usize,
85    },
86
87    #[error("SymbolRegistry not available to resolve '{target}'")]
88    RegistryNotAvailable { target: String },
89
90    #[error(
91        "Cannot resolve target for '{intent}'.\n\n\
92        Resolution requires one of:\n\
93        1. symbol_id   (recommended): \"symbol_id\": \"42v1\"\n\
94           → Use 'ryo discover' to find valid IDs\n\
95        2. symbol_path (canonical):   \"symbol_path\": \"my_crate::module::Type\"\n\
96           → Requires full path: crate_name::module::item\n\
97           → NG: \"main\", \"crate::xxx\", \"self::xxx\"\n\
98        3. target_xxx  (name only):   \"target_type\": \"MyStruct\"\n\
99           → Must be unique in workspace\n\n\
100        Tip: symbol_id is most reliable. Run:\n\
101          ryo discover \"Pattern*\" --format json\n\
102        to get symbol IDs for targeting."
103    )]
104    CannotResolve { intent: String },
105
106    #[error(
107        "Missing target module for '{intent}'.\n\n\
108        This intent requires 'symbol_path' to specify where to add the item.\n\n\
109        Valid symbol_path formats:\n\
110        ✓ \"my_crate\"                 (crate root / lib.rs)\n\
111        ✓ \"my_crate::module\"         (submodule)\n\
112        ✓ \"main::my_app\"             (binary crate root / main.rs)\n\
113        ✓ \"main::my_app::module\"     (binary crate submodule)\n\n\
114        Invalid formats:\n\
115        ✗ \"main\"                     (main:: requires crate name)\n\
116        ✗ \"crate::xxx\"               (use actual crate name)\n\
117        ✗ \"self::xxx\", \"super::xxx\"  (context-dependent)\n\n\
118        Example:\n\
119        {{\n\
120          \"type\": \"{intent}\",\n\
121          \"symbol_path\": \"my_crate::domain\",\n\
122          ...\n\
123        }}"
124    )]
125    MissingTargetModule { intent: String },
126
127    #[error("Invalid module path: {message}")]
128    InvalidModulePath { message: String },
129
130    #[error("SymbolRegistry required: {message}")]
131    RegistryRequired { message: String },
132
133    #[error("Unknown crate '{crate_name}' in path '{path}'. Known crates: {known_crates}")]
134    UnknownCrate {
135        path: String,
136        crate_name: String,
137        known_crates: String,
138    },
139}
140
141pub type PlanResult<T> = Result<T, PlanError>;
142
143/// Planner: Goal → Vec<MutationSpec>
144pub struct Planner;
145
146impl Planner {
147    /// Plan mutations from a Goal (supports multiple intents)
148    ///
149    /// # Arguments
150    /// * `goal` - The goal containing intents to plan
151    /// * `registry` - Optional SymbolRegistry for resolving Pattern::Direct to SymbolPath
152    ///
153    /// # Note
154    /// - Duplicate CreateMod specs are automatically deduplicated to prevent conflicts
155    ///   when multiple AddCode intents target nested modules.
156    /// - Batch intent deferred resolution: When AddItem creates a symbol that is
157    ///   referenced by a later intent (e.g., AddDerive), the symbol_id is set to None
158    ///   and resolution is deferred to execution time.
159    pub fn plan(goal: &Goal, registry: Option<&SymbolRegistry>) -> PlanResult<Vec<MutationSpec>> {
160        // Pre-scan AddItem intents to collect pending symbol names
161        let pending_symbols = Self::collect_pending_symbols(&goal.intents);
162
163        let mut all_specs = Vec::new();
164        for intent in &goal.intents {
165            let specs = Self::intent_to_specs(intent, registry, &pending_symbols)?;
166            all_specs.extend(specs);
167        }
168        // Deduplicate CreateMod specs to prevent conflicts
169        Ok(Self::deduplicate_create_mods(all_specs))
170    }
171
172    /// Collect symbol names that will be created by AddItem intents.
173    /// Used for deferred resolution in batch intents.
174    fn collect_pending_symbols(intents: &[Intent]) -> HashSet<String> {
175        let mut pending = HashSet::new();
176        for intent in intents {
177            if let Intent::AddItem { content, .. } = intent {
178                if let Some(name) = extract_item_name_from_content(content) {
179                    pending.insert(name);
180                }
181            }
182        }
183        pending
184    }
185
186    /// Remove duplicate CreateMod specs (same parent + mod_name)
187    ///
188    /// When multiple AddCode intents target nested modules, they may generate
189    /// overlapping CreateMod specs. This function keeps only the first occurrence.
190    fn deduplicate_create_mods(specs: Vec<MutationSpec>) -> Vec<MutationSpec> {
191        use std::collections::HashSet;
192
193        let mut seen_create_mods: HashSet<(String, String)> = HashSet::new();
194        let mut result = Vec::with_capacity(specs.len());
195
196        for spec in specs {
197            match &spec {
198                MutationSpec::CreateMod {
199                    target, mod_name, ..
200                } => {
201                    let key = (format!("{:?}", target), mod_name.clone());
202                    if seen_create_mods.insert(key) {
203                        // First occurrence - keep it
204                        result.push(spec);
205                    }
206                    // Duplicate - skip it
207                }
208                _ => {
209                    // Non-CreateMod specs are always kept
210                    result.push(spec);
211                }
212            }
213        }
214
215        result
216    }
217
218    /// Convert Intent to MutationSpec(s)
219    ///
220    /// The `pending_symbols` set contains names of symbols that will be created
221    /// by AddItem intents earlier in the batch. When resolving fails for a name
222    /// in this set, `symbol_id` is set to `None` for deferred resolution at execution time.
223    fn intent_to_specs(
224        intent: &Intent,
225        registry: Option<&SymbolRegistry>,
226        pending_symbols: &HashSet<String>,
227    ) -> PlanResult<Vec<MutationSpec>> {
228        match intent {
229            // === 識別子リネーム系 ===
230            Intent::RenameIdent {
231                symbol_id,
232                symbol_path,
233                target_ident,
234                to,
235                ..
236            } => {
237                let resolved_id = resolve_from_3fields(
238                    registry,
239                    symbol_id.as_deref(),
240                    symbol_path.as_deref(),
241                    target_ident.as_deref(),
242                    "RenameIdent",
243                )?;
244
245                Ok(vec![MutationSpec::Rename {
246                    target: MutationTargetSymbol::ById(resolved_id),
247                    to: to.clone(),
248                    scope: ryo_executor::Scope::default(),
249                }])
250            }
251
252            // === 構造変更系 ===
253            Intent::ChangeVisibility {
254                symbol_id,
255                symbol_path,
256                target_item,
257                to,
258            } => {
259                let resolved_id = resolve_from_3fields(
260                    registry,
261                    symbol_id.as_deref(),
262                    symbol_path.as_deref(),
263                    target_item.as_deref(),
264                    "ChangeVisibility",
265                )?;
266                Ok(vec![MutationSpec::ChangeVisibility {
267                    target: MutationTargetSymbol::ById(resolved_id),
268                    visibility: visibility_to_spec(*to),
269                }])
270            }
271
272            Intent::MoveItem {
273                symbol_id,
274                symbol_path,
275                target_item,
276                to_module,
277            } => {
278                let resolved_id = resolve_from_3fields(
279                    registry,
280                    symbol_id.as_deref(),
281                    symbol_path.as_deref(),
282                    target_item.as_deref(),
283                    "MoveItem",
284                )?;
285                // Get source path and crate name from registry
286                let resolved_path =
287                    registry.and_then(|r| r.path(resolved_id)).ok_or_else(|| {
288                        PlanError::CannotResolve {
289                            intent: "MoveItem".to_string(),
290                        }
291                    })?;
292                let _source = resolved_path
293                    .parent()
294                    .ok_or_else(|| PlanError::InvalidTarget {
295                        target: resolved_path.to_string(),
296                        reason: "Cannot get parent module of item".to_string(),
297                    })?;
298                let crate_name = resolved_path.crate_name();
299
300                // Resolve to_module path (may contain "test_crate" or bare module name)
301                let to_path_str = if to_module == "test_crate" || to_module.starts_with("crate::") {
302                    // Handle crate:: prefix
303                    to_module.replacen("test_crate", crate_name, 1)
304                } else if to_module.contains("::") {
305                    // Already has path separators (e.g., "foo::bar")
306                    to_module.to_string()
307                } else {
308                    // Bare module name (e.g., "core") - prepend crate root
309                    format!("{}::{}", crate_name, to_module)
310                };
311
312                // Parse with registry validation if available
313                let to_path = if let Some(reg) = registry {
314                    SymbolPath::parse_validated(&to_path_str, reg).map_err(|e| match e {
315                        ryo_symbol::ParseError::UnknownCrate {
316                            path,
317                            crate_name,
318                            known,
319                        } => PlanError::UnknownCrate {
320                            path,
321                            crate_name,
322                            known_crates: known,
323                        },
324                        other => PlanError::InvalidTarget {
325                            target: to_path_str.clone(),
326                            reason: format!("Invalid to_module: {}", other),
327                        },
328                    })?
329                } else {
330                    SymbolPath::parse(&to_path_str).map_err(|e| PlanError::InvalidTarget {
331                        target: to_path_str.clone(),
332                        reason: format!("Invalid to_module: {}", e),
333                    })?
334                };
335
336                Ok(vec![MutationSpec::MoveItem {
337                    source: MutationTargetSymbol::ById(resolved_id),
338                    target: MutationTargetSymbol::ByPath(Box::new(to_path)),
339                    item_name: target_item.clone().unwrap_or_default(),
340                    item_kind: ryo_executor::ItemKind::Struct,
341                    add_use: true,
342                }])
343            }
344
345            Intent::ExtractTrait {
346                symbol_id,
347                symbol_path,
348                target_type,
349                trait_name,
350                methods,
351            } => {
352                // ExtractTrait needs impl block's SymbolId, not the type's
353                let resolved_id = resolve_impl_from_3fields(
354                    registry,
355                    symbol_id.as_deref(),
356                    symbol_path.as_deref(),
357                    target_type.as_deref(),
358                    "ExtractTrait",
359                )?;
360
361                let methods_opt = if methods.is_empty() {
362                    None
363                } else {
364                    Some(methods.clone())
365                };
366                Ok(vec![MutationSpec::ExtractTrait {
367                    target: MutationTargetSymbol::ById(resolved_id),
368                    trait_name: trait_name.clone(),
369                    methods: methods_opt,
370                }])
371            }
372
373            Intent::InlineTrait {
374                trait_symbol_id,
375                trait_symbol_path,
376                target_trait,
377                struct_symbol_id: _,
378                struct_symbol_path: _,
379                target_struct,
380                remove_trait,
381            } => {
382                let resolved_id = resolve_from_3fields(
383                    registry,
384                    trait_symbol_id.as_deref(),
385                    trait_symbol_path.as_deref(),
386                    target_trait.as_deref(),
387                    "InlineTrait",
388                )?;
389
390                Ok(vec![MutationSpec::InlineTrait {
391                    target: MutationTargetSymbol::ById(resolved_id),
392                    struct_name: target_struct.clone().unwrap_or_default(),
393                    remove_trait: *remove_trait,
394                }])
395            }
396
397            Intent::EnumToTrait {
398                symbol_id,
399                symbol_path,
400                target_enum,
401                new_trait_name,
402                remove_enum,
403                strategy,
404                match_handling,
405            } => {
406                let resolved_id = resolve_from_3fields(
407                    registry,
408                    symbol_id.as_deref(),
409                    symbol_path.as_deref(),
410                    target_enum.as_deref(),
411                    "EnumToTrait",
412                )?;
413
414                Ok(vec![MutationSpec::EnumToTrait {
415                    target: MutationTargetSymbol::ById(resolved_id),
416                    trait_name: new_trait_name.clone(),
417                    remove_enum: *remove_enum,
418                    strategy: *strategy,
419                    match_handling: *match_handling,
420                }])
421            }
422
423            // === モジュール操作系 ===
424            // Note: AddMod was consolidated into CreateMod.
425            Intent::RemoveMod {
426                parent_mod,
427                mod_name,
428            } => Ok(vec![MutationSpec::RemoveMod {
429                target: MutationTargetSymbol::ByPath(Box::new(vec_to_symbol_path(
430                    parent_mod, registry,
431                )?)),
432                mod_name: mod_name.clone(),
433            }]),
434
435            Intent::CreateMod {
436                parent_mod,
437                mod_name,
438                content,
439                is_pub,
440            } => Ok(vec![MutationSpec::CreateMod {
441                target: MutationTargetSymbol::ByPath(Box::new(vec_to_symbol_path(
442                    parent_mod, registry,
443                )?)),
444                mod_name: mod_name.clone(),
445                content: content.clone(),
446                is_pub: *is_pub,
447            }]),
448
449            // === フィールド操作系 ===
450            Intent::AddField {
451                symbol_id,
452                symbol_path,
453                target_struct,
454                field_name,
455                field_type,
456                is_pub,
457            } => {
458                let resolved_id = resolve_from_3fields(
459                    registry,
460                    symbol_id.as_deref(),
461                    symbol_path.as_deref(),
462                    target_struct.as_deref(),
463                    "AddField",
464                )?;
465
466                Ok(vec![MutationSpec::AddField {
467                    target: MutationTargetSymbol::ById(resolved_id),
468                    field_name: field_name.clone(),
469                    field_type: field_type.clone(),
470                    visibility: if *is_pub {
471                        ryo_executor::Visibility::Pub
472                    } else {
473                        ryo_executor::Visibility::Private
474                    },
475                }])
476            }
477
478            Intent::RemoveField {
479                symbol_id,
480                symbol_path,
481                target_struct,
482                field_name,
483            } => {
484                let resolved_id = resolve_from_3fields(
485                    registry,
486                    symbol_id.as_deref(),
487                    symbol_path.as_deref(),
488                    target_struct.as_deref(),
489                    "RemoveField",
490                )?;
491
492                Ok(vec![MutationSpec::RemoveField {
493                    target: MutationTargetSymbol::ById(resolved_id),
494                    field_name: field_name.clone(),
495                }])
496            }
497
498            // === Derive操作系 ===
499            Intent::AddDerive {
500                symbol_id,
501                symbol_path,
502                target_type,
503                derives,
504            } => {
505                // Deferred resolution: if target is pending (created by earlier AddItem), set symbol_id to None
506                let resolved_id = match resolve_from_3fields(
507                    registry,
508                    symbol_id.as_deref(),
509                    symbol_path.as_deref(),
510                    target_type.as_deref(),
511                    "AddDerive",
512                ) {
513                    Ok(id) => Some(id),
514                    Err(_)
515                        if target_type
516                            .as_ref()
517                            .is_some_and(|n| pending_symbols.contains(n)) =>
518                    {
519                        None
520                    }
521                    Err(e) => return Err(e),
522                };
523
524                Ok(vec![MutationSpec::AddDerive {
525                    target: match resolved_id {
526                        Some(id) => MutationTargetSymbol::ById(id),
527                        None => MutationTargetSymbol::ByKindAndName(
528                            ryo_executor::ItemKind::Struct,
529                            target_type.clone().unwrap_or_default(),
530                        ),
531                    },
532                    derives: derives.clone(),
533                }])
534            }
535
536            Intent::RemoveDerive {
537                symbol_id,
538                symbol_path,
539                target_type,
540                derives,
541            } => {
542                // Deferred resolution: if target is pending (created by earlier AddItem), set symbol_id to None
543                let resolved_id = match resolve_from_3fields(
544                    registry,
545                    symbol_id.as_deref(),
546                    symbol_path.as_deref(),
547                    target_type.as_deref(),
548                    "RemoveDerive",
549                ) {
550                    Ok(id) => Some(id),
551                    Err(_)
552                        if target_type
553                            .as_ref()
554                            .is_some_and(|n| pending_symbols.contains(n)) =>
555                    {
556                        None
557                    }
558                    Err(e) => return Err(e),
559                };
560
561                Ok(vec![MutationSpec::RemoveDerive {
562                    target: match resolved_id {
563                        Some(id) => MutationTargetSymbol::ById(id),
564                        None => MutationTargetSymbol::ByKindAndName(
565                            ryo_executor::ItemKind::Struct,
566                            target_type.clone().unwrap_or_default(),
567                        ),
568                    },
569                    derives: derives.clone(),
570                }])
571            }
572
573            // === Enum操作系 ===
574            Intent::AddEnum {
575                symbol_path,
576                name,
577                variants,
578                is_pub,
579                derives,
580            } => {
581                let target_path =
582                    SymbolPath::parse(symbol_path).map_err(|e| PlanError::InvalidTarget {
583                        target: symbol_path.clone(),
584                        reason: format!("{}", e),
585                    })?;
586                let vis = if *is_pub { "pub " } else { "" };
587                let derives_attr = if derives.is_empty() {
588                    String::new()
589                } else {
590                    format!("#[derive({})]\n", derives.join(", "))
591                };
592                let variants_str = if variants.is_empty() {
593                    String::new()
594                } else {
595                    variants.join(",\n    ")
596                };
597                let content = format!(
598                    "{}{}enum {} {{\n    {}\n}}",
599                    derives_attr, vis, name, variants_str
600                );
601
602                Ok(vec![MutationSpec::AddItem {
603                    target: MutationTargetSymbol::ByPath(Box::new(target_path)),
604                    content,
605                    position: InsertPosition::Bottom,
606                }])
607            }
608
609            Intent::AddVariant {
610                symbol_id,
611                symbol_path,
612                target_enum,
613                variant_name,
614                variant_type,
615            } => {
616                // Deferred resolution: if target is pending (created by earlier AddItem), set symbol_id to None
617                let resolved_id = match resolve_from_3fields(
618                    registry,
619                    symbol_id.as_deref(),
620                    symbol_path.as_deref(),
621                    target_enum.as_deref(),
622                    "AddVariant",
623                ) {
624                    Ok(id) => Some(id),
625                    Err(_)
626                        if target_enum
627                            .as_ref()
628                            .is_some_and(|n| pending_symbols.contains(n)) =>
629                    {
630                        None
631                    }
632                    Err(e) => return Err(e),
633                };
634                let variant_kind = parse_variant_type(variant_type);
635                Ok(vec![MutationSpec::AddVariant {
636                    target: match resolved_id {
637                        Some(id) => MutationTargetSymbol::ById(id),
638                        None => MutationTargetSymbol::ByKindAndName(
639                            ryo_executor::ItemKind::Enum,
640                            target_enum.clone().unwrap_or_default(),
641                        ),
642                    },
643                    variant_name: variant_name.clone(),
644                    variant_kind,
645                }])
646            }
647
648            Intent::RemoveVariant {
649                symbol_id,
650                symbol_path,
651                target_enum,
652                variant_name,
653            } => {
654                // Deferred resolution: if target is pending (created by earlier AddItem), set symbol_id to None
655                let resolved_id = match resolve_from_3fields(
656                    registry,
657                    symbol_id.as_deref(),
658                    symbol_path.as_deref(),
659                    target_enum.as_deref(),
660                    "RemoveVariant",
661                ) {
662                    Ok(id) => Some(id),
663                    Err(_)
664                        if target_enum
665                            .as_ref()
666                            .is_some_and(|n| pending_symbols.contains(n)) =>
667                    {
668                        None
669                    }
670                    Err(e) => return Err(e),
671                };
672                Ok(vec![MutationSpec::RemoveVariant {
673                    target: match resolved_id {
674                        Some(id) => MutationTargetSymbol::ById(id),
675                        None => MutationTargetSymbol::ByKindAndName(
676                            ryo_executor::ItemKind::Enum,
677                            target_enum.clone().unwrap_or_default(),
678                        ),
679                    },
680                    variant_name: variant_name.clone(),
681                }])
682            }
683
684            Intent::AddMatchArm {
685                symbol_id,
686                symbol_path,
687                target_fn,
688                enum_name,
689                pattern,
690                body,
691            } => {
692                let target = resolve_target_from_3fields(
693                    registry,
694                    symbol_id.as_deref(),
695                    symbol_path.as_deref(),
696                    target_fn.as_deref(),
697                    "AddMatchArm",
698                )?;
699                Ok(vec![MutationSpec::AddMatchArm {
700                    target,
701                    enum_name: enum_name.clone(),
702                    pattern: pattern.clone(),
703                    body: body.clone(),
704                }])
705            }
706
707            Intent::RemoveMatchArm {
708                symbol_id,
709                symbol_path,
710                target_fn,
711                enum_name,
712                pattern,
713            } => {
714                let target = resolve_target_from_3fields(
715                    registry,
716                    symbol_id.as_deref(),
717                    symbol_path.as_deref(),
718                    target_fn.as_deref(),
719                    "RemoveMatchArm",
720                )?;
721                Ok(vec![MutationSpec::RemoveMatchArm {
722                    target,
723                    enum_name: enum_name.clone(),
724                    pattern: pattern.clone(),
725                }])
726            }
727
728            Intent::ReplaceMatchArm {
729                symbol_id,
730                symbol_path,
731                target_fn,
732                enum_name,
733                old_pattern,
734                new_pattern,
735                new_body,
736            } => {
737                let target = resolve_target_from_3fields(
738                    registry,
739                    symbol_id.as_deref(),
740                    symbol_path.as_deref(),
741                    target_fn.as_deref(),
742                    "ReplaceMatchArm",
743                )?;
744                Ok(vec![MutationSpec::ReplaceMatchArm {
745                    target,
746                    enum_name: enum_name.clone(),
747                    old_pattern: old_pattern.clone(),
748                    new_pattern: new_pattern.clone(),
749                    new_body: new_body.clone(),
750                }])
751            }
752
753            Intent::AddStructLiteralField {
754                symbol_id,
755                symbol_path,
756                target_struct,
757                field_name,
758                value,
759            } => {
760                let resolved_id = resolve_from_3fields(
761                    registry,
762                    symbol_id.as_deref(),
763                    symbol_path.as_deref(),
764                    target_struct.as_deref(),
765                    "AddStructLiteralField",
766                )?;
767
768                Ok(vec![MutationSpec::AddStructLiteralField {
769                    target: MutationTargetSymbol::ById(resolved_id),
770                    field_name: field_name.clone(),
771                    value: value.clone(),
772                }])
773            }
774
775            Intent::RemoveStructLiteralField {
776                symbol_id,
777                symbol_path,
778                target_struct,
779                field_name,
780            } => {
781                let resolved_id = resolve_from_3fields(
782                    registry,
783                    symbol_id.as_deref(),
784                    symbol_path.as_deref(),
785                    target_struct.as_deref(),
786                    "RemoveStructLiteralField",
787                )?;
788
789                Ok(vec![MutationSpec::RemoveStructLiteralField {
790                    target: MutationTargetSymbol::ById(resolved_id),
791                    field_name: field_name.clone(),
792                }])
793            }
794
795            // === 構造体/Enum削除系 ===
796            Intent::RemoveStruct {
797                symbol_id,
798                symbol_path,
799                target_struct,
800            } => {
801                let resolved_id = resolve_from_3fields(
802                    registry,
803                    symbol_id.as_deref(),
804                    symbol_path.as_deref(),
805                    target_struct.as_deref(),
806                    "RemoveStruct",
807                )?;
808                Ok(vec![MutationSpec::RemoveItem {
809                    target: MutationTargetSymbol::ById(resolved_id),
810                    item_kind: ryo_executor::ItemKind::Struct,
811                }])
812            }
813
814            Intent::RemoveEnum {
815                symbol_id,
816                symbol_path,
817                target_enum,
818            } => {
819                let resolved_id = resolve_from_3fields(
820                    registry,
821                    symbol_id.as_deref(),
822                    symbol_path.as_deref(),
823                    target_enum.as_deref(),
824                    "RemoveEnum",
825                )?;
826                Ok(vec![MutationSpec::RemoveItem {
827                    target: MutationTargetSymbol::ById(resolved_id),
828                    item_kind: ryo_executor::ItemKind::Enum,
829                }])
830            }
831
832            // === 定数/型エイリアス系 ===
833            Intent::AddConst {
834                symbol_path,
835                name,
836                ty,
837                value,
838                is_pub,
839            } => {
840                let target_path =
841                    SymbolPath::parse(symbol_path).map_err(|e| PlanError::InvalidTarget {
842                        target: symbol_path.clone(),
843                        reason: format!("{}", e),
844                    })?;
845                let vis = if *is_pub { "pub " } else { "" };
846                let content = format!("{}const {}: {} = {};", vis, name, ty, value);
847                Ok(vec![MutationSpec::AddItem {
848                    target: MutationTargetSymbol::ByPath(Box::new(target_path)),
849                    content,
850                    position: InsertPosition::Bottom,
851                }])
852            }
853
854            Intent::AddTypeAlias {
855                symbol_path,
856                name,
857                ty,
858                is_pub,
859            } => {
860                let target_path =
861                    SymbolPath::parse(symbol_path).map_err(|e| PlanError::InvalidTarget {
862                        target: symbol_path.clone(),
863                        reason: format!("{}", e),
864                    })?;
865                let vis = if *is_pub { "pub " } else { "" };
866                let content = format!("{}type {} = {};", vis, name, ty);
867                Ok(vec![MutationSpec::AddItem {
868                    target: MutationTargetSymbol::ByPath(Box::new(target_path)),
869                    content,
870                    position: InsertPosition::Bottom,
871                }])
872            }
873
874            // === Spec系 ===
875            Intent::AddSpec {
876                symbol_id,
877                symbol_path,
878                target_type,
879                module_id,
880                module_path,
881                target_mod,
882                group,
883                alias_name,
884                relations,
885            } => {
886                // Resolve target_type_id from 3-field specification
887                let target_type_id = resolve_from_3fields(
888                    registry,
889                    symbol_id.as_deref(),
890                    symbol_path.as_deref(),
891                    target_type.as_deref(),
892                    "AddSpec target_type",
893                )?;
894
895                // Resolve module_id from 3-field specification
896                let resolved_module_id = resolve_from_3fields(
897                    registry,
898                    module_id.as_deref(),
899                    module_path.as_deref(),
900                    target_mod.as_deref(),
901                    "AddSpec module",
902                )?;
903
904                let _target_path = symbol_path
905                    .as_ref()
906                    .and_then(|p| SymbolPath::parse(p).ok())
907                    .or_else(|| registry.and_then(|r| r.path(target_type_id).cloned()));
908
909                // Convert intent relations to executor relations
910                let executor_relations: Vec<SpecRelation> = relations
911                    .iter()
912                    .map(intent_spec_relation_to_executor)
913                    .collect();
914
915                Ok(vec![MutationSpec::AddSpec {
916                    type_id: target_type_id,
917                    module_id: resolved_module_id,
918                    group: group.clone(),
919                    alias_name: alias_name.clone(),
920                    relations: executor_relations,
921                }])
922            }
923
924            // === メソッド追加・削除 ===
925            Intent::AddMethod {
926                symbol_id,
927                symbol_path,
928                target_type,
929                method_name,
930                params,
931                return_type,
932                body,
933                is_pub,
934                self_param,
935            } => {
936                // Resolve target SymbolPath from 3-field specification
937                let target = if let Some(path_str) = symbol_path {
938                    SymbolPath::parse(path_str).ok()
939                } else if let Some(id_str) = symbol_id {
940                    // If symbol_id provided, try to get path from registry
941                    if let Some(id) = ryo_analysis::SymbolId::parse(id_str) {
942                        registry.and_then(|r| r.path(id).cloned())
943                    } else {
944                        None
945                    }
946                } else {
947                    None
948                };
949
950                Ok(vec![MutationSpec::AddMethod {
951                    target: match target {
952                        Some(path) => MutationTargetSymbol::ByPath(Box::new(path)),
953                        None => {
954                            // Strip generics from target_type since registry stores struct names
955                            // without generic parameters (e.g., "CreateOrderUseCase" not "CreateOrderUseCase<U, P, O>")
956                            let type_name =
957                                strip_generics(&target_type.clone().unwrap_or_default());
958                            MutationTargetSymbol::ByKindAndName(
959                                ryo_executor::ItemKind::Struct,
960                                type_name,
961                            )
962                        }
963                    },
964                    method_name: method_name.clone(),
965                    params: params.clone(),
966                    return_type: return_type.clone(),
967                    body: body.clone(),
968                    is_pub: *is_pub,
969                    self_param: self_param.map(intent_self_param_to_spec),
970                }])
971            }
972
973            Intent::RemoveMethod {
974                symbol_id,
975                symbol_path,
976                target_type,
977                method_name,
978            } => {
979                // RemoveMethod needs the Type's SymbolId (struct/enum), not impl block
980                // Converter builds Type::method path to find the method
981                let resolved_id = resolve_from_3fields(
982                    registry,
983                    symbol_id.as_deref(),
984                    symbol_path.as_deref(),
985                    target_type.as_deref(),
986                    "RemoveMethod",
987                )?;
988                Ok(vec![MutationSpec::RemoveMethod {
989                    target: MutationTargetSymbol::ById(resolved_id),
990                    method_name: method_name.clone(),
991                }])
992            }
993
994            // === 削除系 ===
995            Intent::RemoveConst {
996                symbol_id,
997                symbol_path,
998                target_const,
999            } => {
1000                let resolved_id = resolve_from_3fields(
1001                    registry,
1002                    symbol_id.as_deref(),
1003                    symbol_path.as_deref(),
1004                    target_const.as_deref(),
1005                    "RemoveConst",
1006                )?;
1007                Ok(vec![MutationSpec::RemoveItem {
1008                    target: MutationTargetSymbol::ById(resolved_id),
1009                    item_kind: ryo_executor::ItemKind::Const,
1010                }])
1011            }
1012
1013            Intent::RemoveTypeAlias {
1014                symbol_id,
1015                symbol_path,
1016                target_type_alias,
1017            } => {
1018                let resolved_id = resolve_from_3fields(
1019                    registry,
1020                    symbol_id.as_deref(),
1021                    symbol_path.as_deref(),
1022                    target_type_alias.as_deref(),
1023                    "RemoveTypeAlias",
1024                )?;
1025                Ok(vec![MutationSpec::RemoveItem {
1026                    target: MutationTargetSymbol::ById(resolved_id),
1027                    item_kind: ryo_executor::ItemKind::TypeAlias,
1028                }])
1029            }
1030
1031            Intent::RemoveUse {
1032                symbol_id,
1033                symbol_path,
1034                target_use,
1035            } => {
1036                let resolved_id = resolve_from_3fields(
1037                    registry,
1038                    symbol_id.as_deref(),
1039                    symbol_path.as_deref(),
1040                    target_use.as_deref(),
1041                    "RemoveUse",
1042                )?;
1043                Ok(vec![MutationSpec::RemoveItem {
1044                    target: MutationTargetSymbol::ById(resolved_id),
1045                    item_kind: ryo_executor::ItemKind::Use,
1046                }])
1047            }
1048
1049            Intent::RemoveTrait {
1050                symbol_id,
1051                symbol_path,
1052                target_trait,
1053            } => {
1054                let resolved_id = resolve_from_3fields(
1055                    registry,
1056                    symbol_id.as_deref(),
1057                    symbol_path.as_deref(),
1058                    target_trait.as_deref(),
1059                    "RemoveTrait",
1060                )?;
1061                Ok(vec![MutationSpec::RemoveItem {
1062                    target: MutationTargetSymbol::ById(resolved_id),
1063                    item_kind: ryo_executor::ItemKind::Trait,
1064                }])
1065            }
1066
1067            Intent::RemoveImpl {
1068                symbol_id,
1069                symbol_path,
1070                target_type,
1071                trait_name,
1072            } => {
1073                let resolved_id = resolve_from_3fields(
1074                    registry,
1075                    symbol_id.as_deref(),
1076                    symbol_path.as_deref(),
1077                    target_type.as_deref(),
1078                    "RemoveImpl",
1079                )?;
1080                let _target = match (target_type.as_ref(), trait_name.as_ref()) {
1081                    (Some(s), Some(t)) => Some(format!("{} for {}", t, s)),
1082                    (Some(s), None) => Some(s.clone()),
1083                    _ => None,
1084                };
1085                Ok(vec![MutationSpec::RemoveItem {
1086                    target: MutationTargetSymbol::ById(resolved_id),
1087                    item_kind: ryo_executor::ItemKind::Impl,
1088                }])
1089            }
1090
1091            // === アイテム追加・削除 ===
1092            Intent::AddItem {
1093                symbol_id,
1094                symbol_path,
1095                target_mod,
1096                content,
1097                item_kind: _,
1098            } => {
1099                let target = resolve_target_from_3fields(
1100                    registry,
1101                    symbol_id.as_deref(),
1102                    symbol_path.as_deref(),
1103                    target_mod.as_deref(),
1104                    "AddItem",
1105                )?;
1106                Ok(vec![MutationSpec::AddItem {
1107                    target,
1108                    content: content.clone(),
1109                    position: InsertPosition::Bottom,
1110                }])
1111            }
1112
1113            Intent::RemoveItem {
1114                symbol_id,
1115                symbol_path,
1116                target_item,
1117                item_kind,
1118            } => {
1119                let resolved_id = resolve_from_3fields(
1120                    registry,
1121                    symbol_id.as_deref(),
1122                    symbol_path.as_deref(),
1123                    target_item.as_deref(),
1124                    "RemoveItem",
1125                )?;
1126                Ok(vec![MutationSpec::RemoveItem {
1127                    target: MutationTargetSymbol::ById(resolved_id),
1128                    item_kind: item_kind_to_spec(*item_kind),
1129                }])
1130            }
1131
1132            Intent::AddCode {
1133                symbol_id,
1134                symbol_path,
1135                target_mod: _,
1136                code,
1137            } => {
1138                // 3-field解決: symbol_id優先、次にsymbol_path(必須)
1139                let target = if let Some(ref id_str) = symbol_id {
1140                    let id = SymbolId::parse(id_str).ok_or_else(|| PlanError::InvalidTarget {
1141                        target: id_str.clone(),
1142                        reason: "Invalid SymbolId format".to_string(),
1143                    })?;
1144                    symbol_id_to_symbol_path(id, registry)?
1145                } else if let Some(ref path) = symbol_path {
1146                    // Try SymbolPath first (handles both "crate_name" and "crate_name::module")
1147                    if let Ok(symbol_path) = SymbolPath::parse(path) {
1148                        symbol_path
1149                    } else {
1150                        // FilePath format - requires registry to resolve crate name
1151                        let reg = registry.ok_or_else(|| PlanError::CannotResolve {
1152                            intent: "AddCode".to_string(),
1153                        })?;
1154                        let crate_name = reg
1155                            .iter()
1156                            .next()
1157                            .map(|(_, p)| p.crate_name())
1158                            .ok_or_else(|| PlanError::CannotResolve {
1159                                intent: "AddCode".to_string(),
1160                            })?;
1161                        file_path_to_symbol_path(path, crate_name)?
1162                    }
1163                } else {
1164                    return Err(PlanError::MissingTargetModule {
1165                        intent: "AddCode".to_string(),
1166                    });
1167                };
1168
1169                let mut specs = Vec::new();
1170
1171                // Generate CreateMod specs for missing modules
1172                // With registry: skip existing modules
1173                // Without registry: generate CreateMod for all parent segments (CreateMod is idempotent)
1174                let create_mods = if let Some(reg) = registry {
1175                    generate_create_mod_specs(&target, reg)
1176                } else {
1177                    generate_create_mod_specs_without_registry(&target)
1178                };
1179                specs.extend(create_mods);
1180
1181                specs.push(MutationSpec::AddItem {
1182                    target: MutationTargetSymbol::ByPath(Box::new(target)),
1183                    content: code.clone(),
1184                    position: InsertPosition::Bottom,
1185                });
1186
1187                Ok(specs)
1188            }
1189
1190            // === IDIOM系 ===
1191            Intent::OrganizeImports {
1192                target_mod: _,
1193                deduplicate,
1194                merge_groups,
1195            } => Ok(vec![MutationSpec::OrganizeImports {
1196                module_id: None, // TODO: resolve target_mod to SymbolId
1197                deduplicate: *deduplicate,
1198                merge_groups: *merge_groups,
1199            }]),
1200
1201            Intent::MergeImplBlocks {
1202                target_mod: _,
1203                target_type: _,
1204                inherent_only: _,
1205            } => {
1206                // MergeImplBlocks MutationSpec was removed - not currently supported
1207                Err(PlanError::UnsupportedIntent(
1208                    "MergeImplBlocks is not currently supported".to_string(),
1209                ))
1210            }
1211
1212            Intent::LoopToIterator {
1213                target_mod: _,
1214                target_var,
1215            } => Ok(vec![MutationSpec::LoopToIterator {
1216                module_id: None, // TODO: resolve target_mod to SymbolId
1217                target_var: target_var.clone(),
1218            }]),
1219
1220            Intent::UnwrapToQuestion {
1221                target_mod: _,
1222                target_fn,
1223                include_expect,
1224            } => {
1225                // TODO: Resolve target_fn from String to SymbolId
1226                // Currently, target_fn filtering by name is not supported in MutationSpec
1227                // MutationSpec::UnwrapToQuestion expects SymbolId, but we only have String
1228                if target_fn.is_some() {
1229                    return Err(PlanError::UnsupportedIntent(
1230                        "UnwrapToQuestion with target_fn name filtering is not currently supported. Use SymbolId-based targeting instead.".to_string(),
1231                    ));
1232                }
1233
1234                Ok(vec![MutationSpec::UnwrapToQuestion {
1235                    module_id: None,
1236                    target_fn: None,
1237                    include_expect: *include_expect,
1238                }])
1239            }
1240
1241            Intent::IntroduceVariable {
1242                target_mod: _,
1243                target_fn: _,
1244                expr,
1245                var_name,
1246            } => Ok(vec![MutationSpec::IntroduceVariable {
1247                module_id: None, // TODO: resolve target_mod to SymbolId
1248                fn_id: None,     // TODO: resolve target_fn to SymbolId
1249                expr: expr.clone(),
1250                var_name: var_name.clone(),
1251            }]),
1252
1253            // === Builder Pattern ===
1254            Intent::GenerateBuilder {
1255                symbol_id: _,
1256                symbol_path: _,
1257                target_struct,
1258                target_mod,
1259                fields,
1260                add_builder_method,
1261            } => {
1262                // target_mod is required - no implicit crate root fallback
1263                let target_mod_str =
1264                    target_mod
1265                        .as_deref()
1266                        .ok_or_else(|| PlanError::MissingTargetModule {
1267                            intent: "GenerateBuilder".to_string(),
1268                        })?;
1269
1270                // Parse target_mod (no "test_crate" replacement - use actual crate name)
1271                let target = SymbolPath::parse(target_mod_str).map_err(|e| {
1272                    PlanError::InvalidModulePath {
1273                        message: format!("Failed to parse target_mod '{}': {}", target_mod_str, e),
1274                    }
1275                })?;
1276                let struct_name_str = target_struct
1277                    .clone()
1278                    .unwrap_or_else(|| "Unknown".to_string());
1279                let builder_name = format!("{}Builder", struct_name_str);
1280
1281                let mut specs = Vec::new();
1282
1283                // 1. Generate Builder struct
1284                // Note: Use Bottom to avoid position conflicts
1285                let builder_struct = Self::generate_builder_struct(&builder_name, fields);
1286                specs.push(MutationSpec::AddItem {
1287                    target: MutationTargetSymbol::ByPath(Box::new(target.clone())),
1288                    content: builder_struct,
1289                    position: InsertPosition::Bottom,
1290                });
1291
1292                // 2. Generate Builder impl
1293                // Note: Use Bottom position to avoid circular dependency with Builder struct
1294                let builder_impl =
1295                    Self::generate_builder_impl(&struct_name_str, &builder_name, fields);
1296                specs.push(MutationSpec::AddItem {
1297                    target: MutationTargetSymbol::ByPath(Box::new(target.clone())),
1298                    content: builder_impl,
1299                    position: InsertPosition::Bottom,
1300                });
1301
1302                // 3. (Optional) Add builder() method to original struct
1303                if *add_builder_method {
1304                    // Construct the struct's SymbolPath by appending struct_name to target module
1305                    let struct_path =
1306                        target
1307                            .child(&struct_name_str)
1308                            .map_err(|e| PlanError::InvalidTarget {
1309                                target: format!("{}::{}", target, struct_name_str),
1310                                reason: format!("Failed to create struct path: {}", e),
1311                            })?;
1312                    specs.push(MutationSpec::AddMethod {
1313                        target: MutationTargetSymbol::ByPath(Box::new(struct_path)),
1314                        method_name: "builder".to_string(),
1315                        params: vec![],
1316                        return_type: Some(builder_name.clone()),
1317                        body: format!("{}::new()", builder_name),
1318                        is_pub: true,
1319                        self_param: None,
1320                    });
1321                }
1322
1323                Ok(specs)
1324            }
1325
1326            // === PureStmt/PureExpr 操作系 ===
1327            Intent::ReplaceExpr {
1328                target_mod: _,
1329                target_fn: _,
1330                old_expr,
1331                new_expr,
1332                replace_all,
1333                symbol_path,
1334            } => Ok(vec![MutationSpec::ReplaceExpr {
1335                module_id: None, // TODO: resolve target_mod to SymbolId
1336                fn_id: None,     // TODO: resolve target_fn to SymbolId
1337                old_expr: old_expr.clone(),
1338                new_expr: new_expr.clone(),
1339                replace_all: *replace_all,
1340                symbol_path: symbol_path.clone(),
1341            }]),
1342
1343            Intent::RemoveStatement {
1344                target_mod: _,
1345                target_fn: _,
1346                pattern,
1347                remove_all,
1348                symbol_path,
1349            } => Ok(vec![MutationSpec::RemoveStatement {
1350                module_id: None, // TODO: resolve target_mod to SymbolId
1351                fn_id: None,     // TODO: resolve target_fn to SymbolId
1352                pattern: pattern.clone(),
1353                remove_all: *remove_all,
1354                symbol_path: symbol_path.clone(),
1355            }]),
1356
1357            Intent::InsertStatement {
1358                target_mod: _,
1359                target_fn,
1360                stmt,
1361                position,
1362                reference_pattern,
1363                symbol_path,
1364            } => {
1365                let fn_id = if let Some(reg) = registry {
1366                    resolve_symbol_by_name(target_fn, SymbolKind::Function, reg)?
1367                } else {
1368                    return Err(PlanError::SymbolNotFound {
1369                        name: target_fn.clone(),
1370                        kind: Some(SymbolKind::Function),
1371                    });
1372                };
1373                Ok(vec![MutationSpec::InsertStatement {
1374                    module_id: None,
1375                    fn_id,
1376                    stmt: stmt.clone(),
1377                    position: intent_stmt_position_to_spec(position),
1378                    reference_pattern: reference_pattern.clone(),
1379                    symbol_path: symbol_path.clone(),
1380                }])
1381            }
1382
1383            Intent::ReplaceStatement {
1384                target_mod: _,
1385                target_fn: _,
1386                old_stmt,
1387                new_stmt,
1388                symbol_path,
1389            } => Ok(vec![MutationSpec::ReplaceStatement {
1390                module_id: None, // TODO: resolve target_mod to SymbolId
1391                fn_id: None,     // TODO: resolve target_fn to SymbolId
1392                old_stmt: old_stmt.clone(),
1393                new_stmt: new_stmt.clone(),
1394                symbol_path: symbol_path.clone(),
1395            }]),
1396
1397            // === 追加 Idiom変換系 ===
1398            Intent::AssignOp {
1399                target_mod: _,
1400                target_fn: _,
1401            } => Ok(vec![MutationSpec::AssignOp {
1402                module_id: None, // TODO: resolve target_mod to SymbolId
1403                fn_id: None,     // TODO: resolve target_fn to SymbolId
1404            }]),
1405
1406            Intent::BoolSimplify { target_mod: _ } => Ok(vec![MutationSpec::BoolSimplify {
1407                module_id: None, // TODO: resolve target_mod to SymbolId
1408            }]),
1409
1410            Intent::CloneOnCopy { target_mod: _ } => Ok(vec![MutationSpec::CloneOnCopy {
1411                module_id: None, // TODO: resolve target_mod to SymbolId
1412            }]),
1413
1414            Intent::CollapsibleIf { target_mod: _ } => Ok(vec![MutationSpec::CollapsibleIf {
1415                module_id: None, // TODO: resolve target_mod to SymbolId
1416            }]),
1417
1418            Intent::ComparisonToMethod { target_mod: _ } => {
1419                Ok(vec![MutationSpec::ComparisonToMethod {
1420                    module_id: None, // TODO: resolve target_mod to SymbolId
1421                }])
1422            }
1423
1424            Intent::RedundantClosure { target_mod: _ } => {
1425                Ok(vec![MutationSpec::RedundantClosure {
1426                    module_id: None, // TODO: resolve target_mod to SymbolId
1427                }])
1428            }
1429
1430            Intent::ManualMap { target_mod: _ } => Ok(vec![MutationSpec::ManualMap {
1431                module_id: None, // TODO: resolve target_mod to SymbolId
1432            }]),
1433
1434            Intent::MatchToIfLet { target_mod: _ } => Ok(vec![MutationSpec::MatchToIfLet {
1435                module_id: None, // TODO: resolve target_mod to SymbolId
1436            }]),
1437
1438            Intent::FilterNext {
1439                target_mod: _,
1440                target_fn: _,
1441            } => Ok(vec![MutationSpec::FilterNext {
1442                module_id: None, // TODO: resolve target_mod to SymbolId
1443                fn_id: None,     // TODO: resolve target_fn to SymbolId
1444            }]),
1445
1446            Intent::MapUnwrapOr {
1447                target_mod: _,
1448                target_fn: _,
1449            } => Ok(vec![MutationSpec::MapUnwrapOr {
1450                module_id: None, // TODO: resolve target_mod to SymbolId
1451                fn_id: None,     // TODO: resolve target_fn to SymbolId
1452            }]),
1453
1454            // === 複製系 ===
1455            Intent::DuplicateFunction {
1456                symbol_id,
1457                symbol_path,
1458                target_fn,
1459                to,
1460            } => {
1461                let resolved_id = resolve_from_3fields(
1462                    registry,
1463                    symbol_id.as_deref(),
1464                    symbol_path.as_deref(),
1465                    target_fn.as_deref(),
1466                    "DuplicateFunction",
1467                )?;
1468                Ok(vec![MutationSpec::DuplicateFunction {
1469                    target: MutationTargetSymbol::ById(resolved_id),
1470                    to: to.clone(),
1471                }])
1472            }
1473
1474            Intent::DuplicateStruct {
1475                symbol_id,
1476                symbol_path,
1477                target_struct,
1478                to,
1479                include_impls,
1480            } => {
1481                let resolved_id = resolve_from_3fields(
1482                    registry,
1483                    symbol_id.as_deref(),
1484                    symbol_path.as_deref(),
1485                    target_struct.as_deref(),
1486                    "DuplicateStruct",
1487                )?;
1488                Ok(vec![MutationSpec::DuplicateStruct {
1489                    target: MutationTargetSymbol::ById(resolved_id),
1490                    to: to.clone(),
1491                    include_impls: *include_impls,
1492                }])
1493            }
1494
1495            Intent::DuplicateEnum {
1496                symbol_id,
1497                symbol_path,
1498                target_enum,
1499                to,
1500                include_impls,
1501            } => {
1502                let resolved_id = resolve_from_3fields(
1503                    registry,
1504                    symbol_id.as_deref(),
1505                    symbol_path.as_deref(),
1506                    target_enum.as_deref(),
1507                    "DuplicateEnum",
1508                )?;
1509                Ok(vec![MutationSpec::DuplicateEnum {
1510                    target: MutationTargetSymbol::ById(resolved_id),
1511                    to: to.clone(),
1512                    include_impls: *include_impls,
1513                }])
1514            }
1515
1516            Intent::DuplicateModTree {
1517                symbol_id,
1518                symbol_path,
1519                target_mod,
1520                to,
1521            } => {
1522                let resolved_id = resolve_from_3fields(
1523                    registry,
1524                    symbol_id.as_deref(),
1525                    symbol_path.as_deref(),
1526                    target_mod.as_deref(),
1527                    "DuplicateModTree",
1528                )?;
1529                Ok(vec![MutationSpec::DuplicateModTree {
1530                    target: MutationTargetSymbol::ById(resolved_id),
1531                    to: to.clone(),
1532                }])
1533            }
1534
1535            // === カスタム ===
1536            Intent::Custom { description, .. } => Err(PlanError::UnsupportedIntent(format!(
1537                "Custom intent not directly supported: {}",
1538                description
1539            ))),
1540
1541            // === WASM Plugin ===
1542            #[cfg(feature = "wasm-plugin")]
1543            Intent::Plugin {
1544                name,
1545                file_patterns,
1546            } => Ok(vec![MutationSpec::PluginTransform {
1547                plugin_name: name.clone(),
1548                target: None,
1549                file_patterns: file_patterns.clone(),
1550                config: serde_json::Value::Null,
1551            }]),
1552        }
1553    }
1554
1555    // === Builder Pattern Helpers ===
1556
1557    /// Generate Builder struct code
1558    fn generate_builder_struct(builder_name: &str, fields: &[(String, String)]) -> String {
1559        let mut code = format!("pub struct {} {{\n", builder_name);
1560        for (name, ty) in fields {
1561            code.push_str(&format!("    {}: Option<{}>,\n", name, ty));
1562        }
1563        code.push('}');
1564        code
1565    }
1566
1567    /// Generate Builder impl code
1568    fn generate_builder_impl(
1569        struct_name: &str,
1570        builder_name: &str,
1571        fields: &[(String, String)],
1572    ) -> String {
1573        let mut code = format!("impl {} {{\n", builder_name);
1574
1575        // new() method
1576        code.push_str("    pub fn new() -> Self {\n");
1577        code.push_str("        Self {\n");
1578        for (name, _) in fields {
1579            code.push_str(&format!("            {}: None,\n", name));
1580        }
1581        code.push_str("        }\n");
1582        code.push_str("    }\n\n");
1583
1584        // setter methods
1585        for (name, ty) in fields {
1586            code.push_str(&format!(
1587                "    pub fn {}(mut self, {}: {}) -> Self {{\n",
1588                name, name, ty
1589            ));
1590            code.push_str(&format!("        self.{} = Some({});\n", name, name));
1591            code.push_str("        self\n");
1592            code.push_str("    }\n\n");
1593        }
1594
1595        // build() method
1596        code.push_str(&format!(
1597            "    pub fn build(self) -> Result<{}, &'static str> {{\n",
1598            struct_name
1599        ));
1600        code.push_str(&format!("        Ok({} {{\n", struct_name));
1601        for (name, _) in fields {
1602            code.push_str(&format!(
1603                "            {}: self.{}.ok_or(\"{} is required\")?,\n",
1604                name, name, name
1605            ));
1606        }
1607        code.push_str("        })\n");
1608        code.push_str("    }\n");
1609        code.push('}');
1610        code
1611    }
1612}
1613
1614// === Helper functions ===
1615
1616// === Symbol Resolution Helpers ===
1617
1618/// Resolve SymbolId from optional id or path
1619///
1620/// Priority:
1621/// 1. `symbol_id: Some(id)` → return as-is
1622/// 2. `symbol_path: Some(path)` → resolve via registry.lookup()
1623/// 3. Both None → CannotResolve error
1624///
1625/// For name-based resolution (when only a String name is available),
1626/// use `resolve_symbol_by_name()` separately as it requires kind filtering.
1627/// Get crate name from registry (required for canonical SymbolPath construction)
1628///
1629/// Convert file path to SymbolPath using crate name
1630///
1631/// Converts relative file paths (e.g., "src/foo/bar.rs") to canonical SymbolPath
1632/// (e.g., "my_crate::foo::bar").
1633///
1634/// # Examples
1635/// - "src/foo/bar.rs" → "my_crate::foo::bar"
1636/// - "src/lib.rs" → "my_crate"
1637/// - "foo/bar.rs" → "my_crate::foo::bar" (without src/ prefix)
1638fn file_path_to_symbol_path(file_path: &str, crate_name: &str) -> Result<SymbolPath, PlanError> {
1639    let path_str = file_path.trim_start_matches("src/");
1640    let path_str = path_str.trim_end_matches(".rs");
1641    let path_str = path_str.trim_end_matches("/mod");
1642
1643    // lib.rs or empty path -> crate root
1644    if path_str == "lib" || path_str.is_empty() {
1645        return SymbolPath::parse(crate_name).map_err(|e| PlanError::InvalidTarget {
1646            target: crate_name.to_string(),
1647            reason: format!("Invalid crate name: {}", e),
1648        });
1649    }
1650
1651    // Convert path separators to :: and prepend crate name
1652    let symbol_str = format!("{}::{}", crate_name, path_str.replace('/', "::"));
1653    SymbolPath::parse(&symbol_str).map_err(|e| PlanError::InvalidTarget {
1654        target: symbol_str.clone(),
1655        reason: format!("Invalid file path: {}", e),
1656    })
1657}
1658
1659/// Resolve SymbolId by SymbolPath from registry
1660///
1661/// Returns error if not found.
1662fn resolve_symbol_by_path(
1663    path: &SymbolPath,
1664    registry: &SymbolRegistry,
1665) -> PlanResult<ryo_analysis::SymbolId> {
1666    registry
1667        .lookup(path)
1668        .ok_or_else(|| PlanError::SymbolNotFound {
1669            name: path.to_string(),
1670            kind: None,
1671        })
1672}
1673
1674/// Resolve SymbolId by name and kind from registry
1675///
1676/// Returns error if not found or if multiple matches exist (duplicate).
1677fn resolve_symbol_by_name(
1678    name: &str,
1679    kind: SymbolKind,
1680    registry: &SymbolRegistry,
1681) -> PlanResult<ryo_analysis::SymbolId> {
1682    let matches: Vec<_> = registry
1683        .iter()
1684        .filter(|(id, path)| path.name() == name && registry.kind(*id) == Some(kind))
1685        .collect();
1686
1687    match matches.len() {
1688        0 => Err(PlanError::SymbolNotFound {
1689            name: name.to_string(),
1690            kind: Some(kind),
1691        }),
1692        1 => Ok(matches[0].0),
1693        count => Err(PlanError::DuplicateSymbol {
1694            name: name.to_string(),
1695            kind: Some(kind),
1696            count,
1697        }),
1698    }
1699}
1700
1701/// Resolve SymbolId by name from registry (any kind)
1702///
1703/// Used for Rename operations which can target any symbol kind.
1704/// Returns error if not found or if multiple matches exist (duplicate).
1705///
1706/// If the name contains "::", it is treated as a path and resolved via path lookup.
1707fn resolve_symbol_by_name_any_kind(
1708    name: &str,
1709    registry: &SymbolRegistry,
1710) -> PlanResult<ryo_analysis::SymbolId> {
1711    // If name contains "::", treat it as a path (e.g., "crate::user::UserStatus")
1712    if name.contains("::") {
1713        if let Ok(path) = SymbolPath::parse(name) {
1714            return resolve_symbol_by_path(&path, registry);
1715        }
1716        // If path parsing fails, fall through to name search
1717    }
1718
1719    let matches: Vec<_> = registry
1720        .iter()
1721        .filter(|(_, path)| path.name() == name)
1722        .collect();
1723
1724    match matches.len() {
1725        0 => Err(PlanError::SymbolNotFound {
1726            name: name.to_string(),
1727            kind: None,
1728        }),
1729        1 => Ok(matches[0].0),
1730        count => Err(PlanError::DuplicateSymbol {
1731            name: name.to_string(),
1732            kind: None,
1733            count,
1734        }),
1735    }
1736}
1737
1738/// Resolve SymbolId from 3-field specification (new unified format)
1739///
1740/// Priority:
1741/// 1. `symbol_id: Some(str)` → parse as SymbolId ("7v2" format)
1742/// 2. `symbol_path: Some(str)` → parse as SymbolPath and lookup
1743/// 3. `target_name: Some(str)` → name search (any kind)
1744fn resolve_from_3fields(
1745    registry: Option<&SymbolRegistry>,
1746    symbol_id: Option<&str>,
1747    symbol_path: Option<&str>,
1748    target_name: Option<&str>,
1749    context: &str,
1750) -> PlanResult<ryo_analysis::SymbolId> {
1751    // Priority 1: Direct SymbolId
1752    if let Some(id_str) = symbol_id {
1753        return ryo_analysis::SymbolId::parse(id_str).ok_or_else(|| PlanError::InvalidTarget {
1754            target: id_str.to_string(),
1755            reason: "invalid SymbolId format (expected 'NvM' format like '7v2')".to_string(),
1756        });
1757    }
1758
1759    // Priority 2: Resolve from SymbolPath
1760    if let Some(path_str) = symbol_path {
1761        let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
1762            target: path_str.to_string(),
1763        })?;
1764        let path = SymbolPath::parse(path_str).map_err(|e| PlanError::InvalidTarget {
1765            target: path_str.to_string(),
1766            reason: format!("invalid SymbolPath: {:?}", e),
1767        })?;
1768        return resolve_symbol_by_path(&path, reg);
1769    }
1770
1771    // Priority 3: Name search
1772    if let Some(name) = target_name {
1773        let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
1774            target: name.to_string(),
1775        })?;
1776        return resolve_symbol_by_name_any_kind(name, reg);
1777    }
1778
1779    // All None
1780    Err(PlanError::CannotResolve {
1781        intent: context.to_string(),
1782    })
1783}
1784
1785/// Resolve target from 3-field specification, returning MutationTargetSymbol
1786///
1787/// For AddItem, returns ByPath or ById depending on what's specified.
1788/// Returns error if all fields are None (no implicit fallback to crate root).
1789fn resolve_target_from_3fields(
1790    registry: Option<&SymbolRegistry>,
1791    symbol_id: Option<&str>,
1792    symbol_path: Option<&str>,
1793    module_name: Option<&str>,
1794    context: &str,
1795) -> PlanResult<MutationTargetSymbol> {
1796    // Priority 1: Direct SymbolId
1797    if let Some(id_str) = symbol_id {
1798        let id = ryo_analysis::SymbolId::parse(id_str).ok_or_else(|| PlanError::InvalidTarget {
1799            target: id_str.to_string(),
1800            reason: "invalid SymbolId format (expected 'NvM' format like '7v2')".to_string(),
1801        })?;
1802        return Ok(MutationTargetSymbol::ById(id));
1803    }
1804
1805    // Priority 2: Use SymbolPath directly
1806    if let Some(path_str) = symbol_path {
1807        // Use parse_validated if registry available to check crate name exists
1808        let path = if let Some(reg) = registry {
1809            SymbolPath::parse_validated(path_str, reg).map_err(|e| match e {
1810                ryo_symbol::ParseError::UnknownCrate {
1811                    path,
1812                    crate_name,
1813                    known,
1814                } => PlanError::UnknownCrate {
1815                    path,
1816                    crate_name,
1817                    known_crates: known,
1818                },
1819                other => PlanError::InvalidTarget {
1820                    target: path_str.to_string(),
1821                    reason: format!("invalid SymbolPath: {:?}", other),
1822                },
1823            })?
1824        } else {
1825            SymbolPath::parse(path_str).map_err(|e| PlanError::InvalidTarget {
1826                target: path_str.to_string(),
1827                reason: format!("invalid SymbolPath: {:?}", e),
1828            })?
1829        };
1830
1831        return Ok(MutationTargetSymbol::ByPath(Box::new(path)));
1832    }
1833
1834    // Priority 3: Name search → ById
1835    if let Some(name) = module_name {
1836        let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
1837            target: name.to_string(),
1838        })?;
1839        let id = resolve_symbol_by_name_any_kind(name, reg)?;
1840        return Ok(MutationTargetSymbol::ById(id));
1841    }
1842
1843    // All None → Error (no implicit fallback to crate root)
1844    Err(PlanError::CannotResolve {
1845        intent: format!(
1846            "{}: at least one of symbol_id, symbol_path, or target_mod must be specified",
1847            context
1848        ),
1849    })
1850}
1851
1852/// Resolve impl block SymbolId from 3-field specification
1853///
1854/// For RemoveMethod/AddMethod, we need the impl block's SymbolId, not the type's.
1855/// When target_type_name is specified (e.g., "Status"), we search for `<impl Status>`.
1856fn resolve_impl_from_3fields(
1857    registry: Option<&SymbolRegistry>,
1858    symbol_id: Option<&str>,
1859    symbol_path: Option<&str>,
1860    target_type_name: Option<&str>,
1861    context: &str,
1862) -> PlanResult<ryo_analysis::SymbolId> {
1863    // Priority 1: Direct SymbolId (assumed to be impl block's ID)
1864    if let Some(id_str) = symbol_id {
1865        return ryo_analysis::SymbolId::parse(id_str).ok_or_else(|| PlanError::InvalidTarget {
1866            target: id_str.to_string(),
1867            reason: "invalid SymbolId format (expected 'NvM' format like '7v2')".to_string(),
1868        });
1869    }
1870
1871    // Priority 2: Resolve from SymbolPath (assumed to be impl block's path like "crate::types::<impl Status>")
1872    if let Some(path_str) = symbol_path {
1873        let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
1874            target: path_str.to_string(),
1875        })?;
1876        let path = SymbolPath::parse(path_str).map_err(|e| PlanError::InvalidTarget {
1877            target: path_str.to_string(),
1878            reason: format!("invalid SymbolPath: {:?}", e),
1879        })?;
1880        return resolve_symbol_by_path(&path, reg);
1881    }
1882
1883    // Priority 3: Search for impl block by type name
1884    // e.g., target_type_name="Status" -> search for `<impl Status>`
1885    if let Some(type_name) = target_type_name {
1886        let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
1887            target: type_name.to_string(),
1888        })?;
1889
1890        // Search for impl block with matching self_ty name pattern
1891        // Note: Registry stores generic types with spaces (e.g., "Generic < T , U >")
1892        // due to to_token_stream().to_string() behavior, so we normalize both for comparison
1893        let impl_name = format!("<impl {}>", type_name);
1894        let normalized_expected = normalize_generic_name(&impl_name);
1895        let matches: Vec<_> = reg
1896            .iter()
1897            .filter(|(id, path)| {
1898                // Check if it's an impl block and name matches (normalized)
1899                reg.kind(*id) == Some(SymbolKind::Impl)
1900                    && normalize_generic_name(path.name()) == normalized_expected
1901            })
1902            .collect();
1903
1904        return match matches.len() {
1905            0 => Err(PlanError::SymbolNotFound {
1906                name: impl_name,
1907                kind: Some(SymbolKind::Impl),
1908            }),
1909            1 => Ok(matches[0].0),
1910            count => Err(PlanError::DuplicateSymbol {
1911                name: impl_name,
1912                kind: Some(SymbolKind::Impl),
1913                count,
1914            }),
1915        };
1916    }
1917
1918    // All None
1919    Err(PlanError::CannotResolve {
1920        intent: context.to_string(),
1921    })
1922}
1923
1924/// Normalize generic type names for comparison.
1925///
1926/// The registry stores generic types with spaces due to `to_token_stream().to_string()`
1927/// behavior (e.g., "Generic < T , U >"), but DSL specifies them without spaces
1928/// (e.g., "Generic<T, U>"). This function normalizes both formats for comparison.
1929///
1930/// # Examples
1931/// - "Generic < T , U >" → "Generic<T,U>"
1932/// - "Generic<T, U>" → "Generic<T,U>"
1933/// - "<impl Foo < T >>" → "<impl Foo<T>>"
1934fn normalize_generic_name(name: &str) -> String {
1935    // Remove spaces around angle brackets and commas
1936    name.replace(" < ", "<")
1937        .replace(" > ", ">")
1938        .replace("< ", "<")
1939        .replace(" >", ">")
1940        .replace(", ", ",")
1941        .replace(" ,", ",")
1942}
1943
1944/// Strip generic parameters from a type name.
1945///
1946/// The symbol registry stores struct/enum names without generic parameters
1947/// (e.g., "CreateOrderUseCase"), but DSL may specify them with generics
1948/// (e.g., "CreateOrderUseCase<U, P, O>"). This function strips the generics
1949/// for symbol lookup purposes.
1950///
1951/// # Examples
1952/// - "CreateOrderUseCase<U, P, O>" → "CreateOrderUseCase"
1953/// - "Vec<String>" → "Vec"
1954/// - "HashMap<K, V>" → "HashMap"
1955/// - "SimpleType" → "SimpleType"
1956fn strip_generics(name: &str) -> String {
1957    if let Some(idx) = name.find('<') {
1958        name[..idx].to_string()
1959    } else {
1960        name.to_string()
1961    }
1962}
1963
1964/// Convert Vec<String> (module names) to SymbolPath
1965///
1966/// # Arguments
1967/// * `segments` - Module name segments (e.g., ["infrastructure", "memory"])
1968/// * `registry` - SymbolRegistry to resolve crate name
1969///
1970/// # Rules
1971/// - `segments` must NOT contain "test_crate" literal (use empty array for crate root)
1972/// - Empty array `[]` → crate root (e.g., "my_crate")
1973/// - Non-empty → crate::module::... (e.g., "my_crate::infrastructure::memory")
1974///
1975/// # Errors
1976/// Returns PlanError if:
1977/// - segments contains "test_crate" literal
1978/// - registry is None (required for resolution)
1979fn vec_to_symbol_path(
1980    segments: &[String],
1981    registry: Option<&SymbolRegistry>,
1982) -> PlanResult<SymbolPath> {
1983    // CRITICAL: "test_crate" literal is forbidden (relative path)
1984    if segments.iter().any(|s| s == "test_crate") {
1985        return Err(PlanError::InvalidModulePath {
1986            message: format!(
1987                "Module path must NOT contain 'crate' literal (got: {:?}). \
1988                 Use empty array [] for crate root, or actual module names like ['infrastructure', 'memory']",
1989                segments
1990            ),
1991        });
1992    }
1993
1994    // Registry is required to resolve crate name
1995    let reg = registry.ok_or_else(|| PlanError::RegistryRequired {
1996        message: "Cannot resolve module path without SymbolRegistry".to_string(),
1997    })?;
1998
1999    // Get crate name from any symbol in registry
2000    let crate_name_str = reg
2001        .iter()
2002        .next()
2003        .map(|(_, path)| path.crate_name().to_string())
2004        .ok_or_else(|| PlanError::RegistryRequired {
2005            message: "Registry is empty - cannot determine crate name".to_string(),
2006        })?;
2007    let crate_name = crate_name_str.as_str();
2008
2009    if segments.is_empty() {
2010        // Empty array = crate root
2011        SymbolPath::parse(crate_name).map_err(|e| PlanError::InvalidModulePath {
2012            message: format!("Failed to create crate root path '{}': {}", crate_name, e),
2013        })
2014    } else {
2015        // Non-empty = crate::module::...
2016        let mut full_path = vec![crate_name];
2017        full_path.extend(segments.iter().map(|s| s.as_str()));
2018        SymbolPath::from_segments(full_path.iter().copied()).map_err(|e| {
2019            PlanError::InvalidModulePath {
2020                message: format!("Failed to create module path '{:?}': {}", full_path, e),
2021            }
2022        })
2023    }
2024}
2025
2026fn visibility_to_spec(vis: Visibility) -> ryo_executor::Visibility {
2027    match vis {
2028        Visibility::Private => ryo_executor::Visibility::Private,
2029        Visibility::Pub => ryo_executor::Visibility::Pub,
2030        Visibility::PubCrate => ryo_executor::Visibility::PubCrate,
2031        Visibility::PubSuper => ryo_executor::Visibility::PubSuper,
2032    }
2033}
2034
2035fn intent_stmt_position_to_spec(pos: &IntentStmtPosition) -> StmtInsertPosition {
2036    match pos {
2037        IntentStmtPosition::Start => StmtInsertPosition::Start,
2038        IntentStmtPosition::End => StmtInsertPosition::End,
2039        IntentStmtPosition::BeforePattern => StmtInsertPosition::BeforePattern,
2040        IntentStmtPosition::AfterPattern => StmtInsertPosition::AfterPattern,
2041    }
2042}
2043
2044fn item_kind_to_spec(kind: ItemKind) -> ryo_executor::ItemKind {
2045    match kind {
2046        ItemKind::Struct => ryo_executor::ItemKind::Struct,
2047        ItemKind::Enum => ryo_executor::ItemKind::Enum,
2048        ItemKind::Trait => ryo_executor::ItemKind::Trait,
2049        ItemKind::Impl => ryo_executor::ItemKind::Impl,
2050        ItemKind::Function => ryo_executor::ItemKind::Function,
2051        ItemKind::Const => ryo_executor::ItemKind::Const,
2052        ItemKind::Static => ryo_executor::ItemKind::Static,
2053        ItemKind::TypeAlias => ryo_executor::ItemKind::TypeAlias,
2054        ItemKind::Use => ryo_executor::ItemKind::Use,
2055        ItemKind::Mod => ryo_executor::ItemKind::Mod,
2056        ItemKind::Macro => ryo_executor::ItemKind::Macro,
2057        // Nested items - map to closest equivalent
2058        ItemKind::Method => ryo_executor::ItemKind::Function,
2059        ItemKind::Field | ItemKind::TupleField => ryo_executor::ItemKind::Struct,
2060        ItemKind::Variant => ryo_executor::ItemKind::Enum,
2061        // Variable-level items (for DataFlow analysis)
2062        ItemKind::LocalVar | ItemKind::Parameter => ryo_executor::ItemKind::Function,
2063        // Wildcard/other
2064        ItemKind::Any | ItemKind::Other => ryo_executor::ItemKind::Struct, // fallback
2065    }
2066}
2067
2068/// Extract item name from Rust code content.
2069/// Used for deferred resolution in batch intents.
2070fn extract_item_name_from_content(content: &str) -> Option<String> {
2071    let trimmed = content.trim();
2072
2073    // Skip attributes and find the item declaration
2074    let mut lines = trimmed.lines();
2075    let mut decl_line = "";
2076    for line in lines.by_ref() {
2077        let line = line.trim();
2078        if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
2079            decl_line = line;
2080            break;
2081        }
2082    }
2083
2084    // Parse common patterns: pub? (struct|enum|fn|type|const|static|trait|impl|mod|use) NAME
2085    let tokens: Vec<&str> = decl_line.split_whitespace().collect();
2086    if tokens.is_empty() {
2087        return None;
2088    }
2089
2090    let mut idx = 0;
2091
2092    // Skip visibility
2093    if tokens.get(idx) == Some(&"pub") {
2094        idx += 1;
2095        // Skip pub(crate), pub(super), etc.
2096        if let Some(t) = tokens.get(idx) {
2097            if t.starts_with('(') {
2098                idx += 1;
2099            }
2100        }
2101    }
2102
2103    // Get keyword
2104    let keyword = tokens.get(idx)?;
2105    idx += 1;
2106
2107    match *keyword {
2108        "struct" | "enum" | "fn" | "type" | "const" | "static" | "trait" | "mod" => {
2109            // Next token is the name (may include generics)
2110            let name = tokens.get(idx)?;
2111            // Strip generics <...> and trailing punctuation
2112            let name = name.split('<').next().unwrap_or(name);
2113            let name = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
2114            Some(name.to_string())
2115        }
2116        "impl" => {
2117            // impl blocks don't define a named symbol we can reference by simple name
2118            None
2119        }
2120        _ => None,
2121    }
2122}
2123
2124/// Convert SymbolId to SymbolPath using registry (internal use)
2125///
2126/// SymbolId is an internal identifier from Discover/Registry operations.
2127/// Requires SymbolRegistry to resolve back to SymbolPath.
2128fn symbol_id_to_symbol_path(
2129    id: ryo_analysis::SymbolId,
2130    registry: Option<&SymbolRegistry>,
2131) -> Result<SymbolPath, PlanError> {
2132    if let Some(reg) = registry {
2133        if let Some(path) = reg.resolve(id) {
2134            return Ok(path.clone());
2135        }
2136    }
2137    Err(PlanError::InvalidTarget {
2138        target: format!("SymbolId({:?})", id),
2139        reason: "SymbolId not found in registry. SymbolId is an internal identifier \
2140             that requires SymbolRegistry for resolution. Ensure the registry is \
2141             provided and contains this symbol (from Discover operation)."
2142            .to_string(),
2143    })
2144}
2145
2146/// Generate CreateMod specs for all parent modules without registry
2147///
2148/// Used when SymbolRegistry is not available. Generates CreateMod specs
2149/// for all segments of the path (except "test_crate"). CreateMod mutations
2150/// are idempotent - they skip if the module already exists.
2151///
2152/// # Example
2153/// If target is "test_crate::domain::model", generates:
2154/// 1. CreateMod { parent: "test_crate", mod_name: "domain" }
2155/// 2. CreateMod { parent: "test_crate::domain", mod_name: "model" }
2156fn generate_create_mod_specs_without_registry(target: &SymbolPath) -> Vec<MutationSpec> {
2157    let segments: Vec<&str> = target.segments().collect();
2158
2159    // Skip "test_crate" (index 0), generate CreateMod for each remaining segment
2160    let mut specs = Vec::new();
2161    for depth in 1..segments.len() {
2162        let parent_path = segments[..depth].join("::");
2163        let mod_name = segments[depth].to_string();
2164
2165        if let Ok(parent) = SymbolPath::parse(&parent_path) {
2166            specs.push(MutationSpec::CreateMod {
2167                target: MutationTargetSymbol::ByPath(Box::new(parent)),
2168                mod_name,
2169                content: String::new(),
2170                is_pub: true, // Default to pub for auto-created modules
2171            });
2172        }
2173    }
2174
2175    specs
2176}
2177
2178/// Generate CreateMod specs for missing modules in a path
2179///
2180/// Checks each segment of the SymbolPath against the registry.
2181/// For missing modules, generates CreateMod specs in order from
2182/// closest existing ancestor to target.
2183///
2184/// # Example
2185/// If target is "crate::domain::model::entity" and only "test_crate" exists,
2186/// generates:
2187/// 1. CreateMod { parent: "test_crate", mod_name: "domain" }
2188/// 2. CreateMod { parent: "test_crate::domain", mod_name: "model" }
2189/// 3. CreateMod { parent: "test_crate::domain::model", mod_name: "entity" }
2190fn generate_create_mod_specs(target: &SymbolPath, registry: &SymbolRegistry) -> Vec<MutationSpec> {
2191    let segments: Vec<&str> = target.segments().collect();
2192
2193    // Find the deepest existing ancestor
2194    let mut existing_depth = 0;
2195    for depth in 1..=segments.len() {
2196        let partial_path = segments[..depth].join("::");
2197        if let Ok(path) = SymbolPath::parse(&partial_path) {
2198            if registry.lookup(&path).is_some() {
2199                existing_depth = depth;
2200            } else {
2201                break;
2202            }
2203        }
2204    }
2205
2206    // Generate CreateMod for each missing segment
2207    let mut specs = Vec::new();
2208    for depth in existing_depth..segments.len() {
2209        if depth == 0 {
2210            // Can't create crate root
2211            continue;
2212        }
2213
2214        let parent_path = segments[..depth].join("::");
2215        let mod_name = segments[depth].to_string();
2216
2217        if let Ok(parent) = SymbolPath::parse(&parent_path) {
2218            specs.push(MutationSpec::CreateMod {
2219                target: MutationTargetSymbol::ByPath(Box::new(parent)),
2220                mod_name,
2221                content: String::new(),
2222                is_pub: true, // Default to pub for auto-created modules
2223            });
2224        }
2225    }
2226
2227    specs
2228}
2229
2230fn parse_variant_type(variant_type: &str) -> VariantKind {
2231    if variant_type == "unit" || variant_type.is_empty() {
2232        VariantKind::Unit
2233    } else if let Some(types) = variant_type.strip_prefix("tuple:") {
2234        let types: Vec<String> = types.split(',').map(|s| s.trim().to_string()).collect();
2235        VariantKind::Tuple { types }
2236    } else if let Some(fields) = variant_type.strip_prefix("struct:") {
2237        let fields: Vec<(String, String)> = fields
2238            .split(',')
2239            .filter_map(|f| {
2240                let parts: Vec<&str> = f.trim().split(':').collect();
2241                if parts.len() == 2 {
2242                    Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
2243                } else {
2244                    None
2245                }
2246            })
2247            .collect();
2248        VariantKind::Struct { fields }
2249    } else {
2250        VariantKind::Unit
2251    }
2252}
2253
2254fn intent_self_param_to_spec(intent_param: IntentSelfParam) -> SelfParam {
2255    match intent_param {
2256        IntentSelfParam::Ref => SelfParam::Ref,
2257        IntentSelfParam::Mut => SelfParam::Mut,
2258        IntentSelfParam::Owned => SelfParam::Owned,
2259    }
2260}
2261
2262/// Convert Intent's SpecRelation to executor's SpecRelation
2263fn intent_spec_relation_to_executor(rel: &IntentSpecRelation) -> SpecRelation {
2264    SpecRelation {
2265        kind: intent_spec_relation_kind_to_executor(&rel.kind),
2266        target: rel.target.clone(),
2267        symbol_id: None,
2268        target_path: None,
2269    }
2270}
2271
2272/// Convert Intent's SpecRelationKind to executor's SpecRelationKind
2273fn intent_spec_relation_kind_to_executor(kind: &IntentSpecRelationKind) -> SpecRelationKind {
2274    match kind {
2275        IntentSpecRelationKind::DependsOn => SpecRelationKind::DependsOn,
2276        IntentSpecRelationKind::RelatedTo => SpecRelationKind::RelatedTo,
2277        IntentSpecRelationKind::PartOf => SpecRelationKind::PartOf,
2278    }
2279}
2280
2281#[cfg(test)]
2282mod tests {
2283    use super::*;
2284    use crate::intent::IdentKind;
2285
2286    #[test]
2287    fn test_rename_intent() {
2288        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
2289
2290        // Create registry with foo symbol
2291        let mut registry = SymbolRegistry::new();
2292        let path = SymbolPath::parse("test_crate::foo").unwrap();
2293        let symbol_id = registry.register(path, SymbolKind::Function).unwrap();
2294
2295        let goal = Goal::new(
2296            "rename foo to bar".to_string(),
2297            Intent::RenameIdent {
2298                symbol_id: Some(format!("{:?}", symbol_id)),
2299                symbol_path: None,
2300                target_ident: Some("foo".to_string()),
2301                to: "bar".to_string(),
2302                kind: IdentKind::Any,
2303            },
2304        );
2305
2306        let specs = Planner::plan(&goal, Some(&registry)).unwrap();
2307        assert_eq!(specs.len(), 1);
2308        match &specs[0] {
2309            MutationSpec::Rename { target, to, .. } => {
2310                assert_eq!(*target, MutationTargetSymbol::ById(symbol_id));
2311                assert_eq!(to, "bar");
2312            }
2313            _ => panic!("Expected Rename spec"),
2314        }
2315    }
2316
2317    #[test]
2318    fn test_add_field_intent() {
2319        // Create a dummy SymbolId for testing (symbol_id is now required)
2320        let dummy_id = ryo_analysis::SymbolId::parse("0v1").expect("valid dummy id");
2321
2322        let goal = Goal::new(
2323            "add field".to_string(),
2324            Intent::AddField {
2325                symbol_id: Some(format!("{:?}", dummy_id)),
2326                symbol_path: None,
2327                target_struct: Some("User".to_string()),
2328                field_name: "email".to_string(),
2329                field_type: "String".to_string(),
2330                is_pub: true,
2331            },
2332        );
2333
2334        let specs = Planner::plan(&goal, None).unwrap();
2335        assert_eq!(specs.len(), 1);
2336        match &specs[0] {
2337            MutationSpec::AddField {
2338                field_name,
2339                field_type,
2340                visibility,
2341                ..
2342            } => {
2343                assert_eq!(field_name, "email");
2344                assert_eq!(field_type, "String");
2345                assert_eq!(*visibility, ryo_executor::Visibility::Pub);
2346            }
2347            _ => panic!("Expected AddField spec"),
2348        }
2349    }
2350
2351    // === AddCode Intent Tests ===
2352
2353    #[test]
2354    fn test_add_code_with_parent_symbol_path() {
2355        let goal = Goal::new(
2356            "add code".to_string(),
2357            Intent::AddCode {
2358                symbol_id: None,
2359                symbol_path: Some("test_crate::domain::model".to_string()),
2360                target_mod: None,
2361                code: "pub struct User { pub id: u64 }".to_string(),
2362            },
2363        );
2364
2365        let specs = Planner::plan(&goal, None).unwrap();
2366        // Without registry: generates CreateMod for each missing segment + AddItem
2367        // 2 CreateMods (domain, model) + 1 AddItem = 3
2368        assert_eq!(specs.len(), 3);
2369        // Last spec should be AddItem
2370        match specs.last().unwrap() {
2371            MutationSpec::AddItem {
2372                target,
2373                content,
2374                position,
2375            } => {
2376                if let MutationTargetSymbol::ByPath(path) = target {
2377                    assert_eq!(path.to_string(), "test_crate::domain::model");
2378                } else {
2379                    panic!("Expected ByPath target");
2380                }
2381                assert_eq!(content, "pub struct User { pub id: u64 }");
2382                assert_eq!(*position, InsertPosition::Bottom);
2383            }
2384            _ => panic!("Expected AddItem spec"),
2385        }
2386    }
2387
2388    #[test]
2389    fn test_add_code_with_nested_symbol_path() {
2390        // Use SymbolPath format (contains ::)
2391        let goal = Goal::new(
2392            "add code".to_string(),
2393            Intent::AddCode {
2394                symbol_id: None,
2395                symbol_path: Some("test_crate::domain::model".to_string()),
2396                target_mod: None,
2397                code: "pub struct Order { pub id: u64 }".to_string(),
2398            },
2399        );
2400
2401        let specs = Planner::plan(&goal, None).unwrap();
2402        // Without registry: generates CreateMod for each missing segment + AddItem
2403        // 2 CreateMods (domain, model) + 1 AddItem = 3
2404        assert_eq!(specs.len(), 3);
2405        // Last spec should be AddItem
2406        match specs.last().unwrap() {
2407            MutationSpec::AddItem {
2408                target, content, ..
2409            } => {
2410                if let MutationTargetSymbol::ByPath(path) = target {
2411                    assert_eq!(path.to_string(), "test_crate::domain::model");
2412                } else {
2413                    panic!("Expected ByPath target");
2414                }
2415                assert_eq!(content, "pub struct Order { pub id: u64 }");
2416            }
2417            _ => panic!("Expected AddItem spec"),
2418        }
2419    }
2420
2421    #[test]
2422    fn test_add_code_with_parent_ref_symbol_path() {
2423        let goal = Goal::new(
2424            "add code".to_string(),
2425            Intent::AddCode {
2426                symbol_id: None,
2427                symbol_path: Some("test_crate::usecase".to_string()),
2428                target_mod: None,
2429                code: "pub fn create_user() {}".to_string(),
2430            },
2431        );
2432
2433        let specs = Planner::plan(&goal, None).unwrap();
2434        // Without registry: generates CreateMod for each missing segment + AddItem
2435        // 1 CreateMod (usecase) + 1 AddItem = 2
2436        assert_eq!(specs.len(), 2);
2437        // Last spec should be AddItem
2438        match specs.last().unwrap() {
2439            MutationSpec::AddItem {
2440                target, content, ..
2441            } => {
2442                if let MutationTargetSymbol::ByPath(path) = target {
2443                    assert_eq!(path.to_string(), "test_crate::usecase");
2444                } else {
2445                    panic!("Expected ByPath target");
2446                }
2447                assert_eq!(content, "pub fn create_user() {}");
2448            }
2449            _ => panic!("Expected AddItem spec"),
2450        }
2451    }
2452
2453    #[test]
2454    fn test_add_code_with_single_module_path() {
2455        // Use SymbolPath format (contains ::)
2456        let goal = Goal::new(
2457            "add code".to_string(),
2458            Intent::AddCode {
2459                symbol_id: None,
2460                symbol_path: Some("test_crate::handlers".to_string()),
2461                target_mod: None,
2462                code: "pub fn handle() {}".to_string(),
2463            },
2464        );
2465
2466        let specs = Planner::plan(&goal, None).unwrap();
2467        // Without registry: generates CreateMod for each missing segment + AddItem
2468        // 1 CreateMod (handlers) + 1 AddItem = 2
2469        assert_eq!(specs.len(), 2);
2470        // Last spec should be AddItem
2471        match specs.last().unwrap() {
2472            MutationSpec::AddItem {
2473                target, content, ..
2474            } => {
2475                if let MutationTargetSymbol::ByPath(path) = target {
2476                    assert_eq!(path.to_string(), "test_crate::handlers");
2477                } else {
2478                    panic!("Expected ByPath target");
2479                }
2480                assert_eq!(content, "pub fn handle() {}");
2481            }
2482            _ => panic!("Expected AddItem spec"),
2483        }
2484    }
2485
2486    #[test]
2487    fn test_add_code_to_crate_root() {
2488        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
2489
2490        // Create minimal registry with crate root
2491        let mut registry = SymbolRegistry::new();
2492        let crate_root = SymbolPath::parse("test_crate").unwrap();
2493        registry
2494            .register(crate_root.clone(), SymbolKind::Mod)
2495            .unwrap();
2496
2497        // Explicit symbol_path to crate root
2498        let goal = Goal::new(
2499            "add code".to_string(),
2500            Intent::AddCode {
2501                symbol_id: None,
2502                symbol_path: Some("test_crate".to_string()),
2503                target_mod: None,
2504                code: "pub const VERSION: &str = \"1.0\";".to_string(),
2505            },
2506        );
2507
2508        let specs = Planner::plan(&goal, Some(&registry)).unwrap();
2509        assert_eq!(specs.len(), 1);
2510        match &specs[0] {
2511            MutationSpec::AddItem {
2512                target, content, ..
2513            } => {
2514                if let MutationTargetSymbol::ByPath(path) = target {
2515                    assert_eq!(path.to_string(), "test_crate");
2516                } else {
2517                    panic!("Expected ByPath target");
2518                }
2519                assert_eq!(content, "pub const VERSION: &str = \"1.0\";");
2520            }
2521            _ => panic!("Expected AddItem spec"),
2522        }
2523    }
2524
2525    #[test]
2526    fn test_add_code_missing_symbol_path_returns_error() {
2527        // symbol_path: None should return MissingTargetModule error
2528        let goal = Goal::new(
2529            "add code".to_string(),
2530            Intent::AddCode {
2531                symbol_id: None,
2532                symbol_path: None,
2533                target_mod: None,
2534                code: "pub const VERSION: &str = \"1.0\";".to_string(),
2535            },
2536        );
2537
2538        let result = Planner::plan(&goal, None);
2539        assert!(matches!(
2540            result,
2541            Err(PlanError::MissingTargetModule { intent }) if intent == "AddCode"
2542        ));
2543    }
2544
2545    #[test]
2546    fn test_add_code_generates_create_mod_for_missing_modules() {
2547        use ryo_analysis::{SymbolKind, SymbolRegistry};
2548
2549        // Setup: registry with only "test_crate" registered
2550        let mut registry = SymbolRegistry::new();
2551        let crate_path = SymbolPath::parse("test_crate").unwrap();
2552        registry.register(crate_path, SymbolKind::Mod).unwrap();
2553
2554        let goal = Goal::new(
2555            "add code to nested module".to_string(),
2556            Intent::AddCode {
2557                symbol_id: None,
2558                symbol_path: Some("test_crate::domain::model".to_string()),
2559                target_mod: None,
2560                code: "pub struct Entity;".to_string(),
2561            },
2562        );
2563
2564        let specs = Planner::plan(&goal, Some(&registry)).unwrap();
2565
2566        // Should generate: CreateMod(domain), CreateMod(model), AddItem
2567        assert_eq!(specs.len(), 3);
2568
2569        // First: CreateMod for "domain"
2570        match &specs[0] {
2571            MutationSpec::CreateMod {
2572                target,
2573                mod_name,
2574                is_pub,
2575                ..
2576            } => {
2577                // FIXME: MutationTargetSymbol does not have to_string()
2578                // assert_eq!(target.to_string(), "test_crate");
2579                match target {
2580                    ryo_executor::MutationTargetSymbol::ByPath(path) => {
2581                        assert_eq!(path.to_string().as_str(), "test_crate");
2582                    }
2583                    _ => panic!("Expected ByPath variant"),
2584                }
2585                assert_eq!(mod_name, "domain");
2586                assert!(*is_pub);
2587            }
2588            _ => panic!("Expected CreateMod spec for domain"),
2589        }
2590
2591        // Second: CreateMod for "model"
2592        match &specs[1] {
2593            MutationSpec::CreateMod {
2594                target,
2595                mod_name,
2596                is_pub,
2597                ..
2598            } => {
2599                // FIXME: MutationTargetSymbol does not have to_string()
2600                // assert_eq!(target.to_string(), "test_crate::domain");
2601                match target {
2602                    ryo_executor::MutationTargetSymbol::ByPath(path) => {
2603                        assert_eq!(path.to_string().as_str(), "test_crate::domain");
2604                    }
2605                    _ => panic!("Expected ByPath variant"),
2606                }
2607                assert_eq!(mod_name, "model");
2608                assert!(*is_pub);
2609            }
2610            _ => panic!("Expected CreateMod spec for model"),
2611        }
2612
2613        // Third: AddItem
2614        match &specs[2] {
2615            MutationSpec::AddItem {
2616                target, content, ..
2617            } => {
2618                if let MutationTargetSymbol::ByPath(path) = target {
2619                    assert_eq!(path.to_string(), "test_crate::domain::model");
2620                } else {
2621                    panic!("Expected ByPath target");
2622                }
2623                assert_eq!(content, "pub struct Entity;");
2624            }
2625            _ => panic!("Expected AddItem spec"),
2626        }
2627    }
2628
2629    #[test]
2630    fn test_add_code_no_create_mod_when_module_exists() {
2631        use ryo_analysis::{SymbolKind, SymbolRegistry};
2632
2633        // Setup: registry with "test_crate" and "test_crate::domain" registered
2634        let mut registry = SymbolRegistry::new();
2635        registry
2636            .register(SymbolPath::parse("test_crate").unwrap(), SymbolKind::Mod)
2637            .unwrap();
2638        registry
2639            .register(
2640                SymbolPath::parse("test_crate::domain").unwrap(),
2641                SymbolKind::Mod,
2642            )
2643            .unwrap();
2644
2645        let goal = Goal::new(
2646            "add code to existing module".to_string(),
2647            Intent::AddCode {
2648                symbol_id: None,
2649                symbol_path: Some("test_crate::domain".to_string()),
2650                target_mod: None,
2651                code: "pub struct User;".to_string(),
2652            },
2653        );
2654
2655        let specs = Planner::plan(&goal, Some(&registry)).unwrap();
2656
2657        // Should only have AddItem (no CreateMod needed)
2658        assert_eq!(specs.len(), 1);
2659        match &specs[0] {
2660            MutationSpec::AddItem { target, .. } => {
2661                if let MutationTargetSymbol::ByPath(path) = target {
2662                    assert_eq!(path.to_string(), "test_crate::domain");
2663                } else {
2664                    panic!("Expected ByPath target");
2665                }
2666            }
2667            _ => panic!("Expected AddItem spec"),
2668        }
2669    }
2670
2671    #[test]
2672    fn test_add_code_without_registry_generates_create_mods() {
2673        // Without registry, AddCode should generate CreateMod for all parent segments
2674        let goal = Goal::new(
2675            "add code without registry".to_string(),
2676            Intent::AddCode {
2677                symbol_id: None,
2678                symbol_path: Some("test_crate::infrastructure::memory".to_string()),
2679                target_mod: None,
2680                code: "pub struct InMemoryRepo;".to_string(),
2681            },
2682        );
2683
2684        // Plan WITHOUT registry (None)
2685        let specs = Planner::plan(&goal, None).unwrap();
2686
2687        // Should generate: CreateMod(infrastructure), CreateMod(memory), AddItem
2688        assert_eq!(
2689            specs.len(),
2690            3,
2691            "Expected 3 specs (2 CreateMod + 1 AddItem), got {}",
2692            specs.len()
2693        );
2694
2695        // First: CreateMod for "infrastructure"
2696        match &specs[0] {
2697            MutationSpec::CreateMod {
2698                target,
2699                mod_name,
2700                is_pub,
2701                ..
2702            } => {
2703                // FIXME: MutationTargetSymbol does not have to_string()
2704                // assert_eq!(target.to_string(), "test_crate");
2705                match target {
2706                    ryo_executor::MutationTargetSymbol::ByPath(path) => {
2707                        assert_eq!(path.to_string().as_str(), "test_crate");
2708                    }
2709                    _ => panic!("Expected ByPath variant"),
2710                }
2711                assert_eq!(mod_name, "infrastructure");
2712                assert!(*is_pub);
2713            }
2714            _ => panic!(
2715                "Expected CreateMod spec for infrastructure, got {:?}",
2716                specs[0]
2717            ),
2718        }
2719
2720        // Second: CreateMod for "memory"
2721        match &specs[1] {
2722            MutationSpec::CreateMod {
2723                target,
2724                mod_name,
2725                is_pub,
2726                ..
2727            } => {
2728                // FIXME: MutationTargetSymbol does not have to_string()
2729                // assert_eq!(target.to_string(), "test_crate::infrastructure");
2730                match target {
2731                    ryo_executor::MutationTargetSymbol::ByPath(path) => {
2732                        assert_eq!(path.to_string().as_str(), "test_crate::infrastructure");
2733                    }
2734                    _ => panic!("Expected ByPath variant"),
2735                }
2736                assert_eq!(mod_name, "memory");
2737                assert!(*is_pub);
2738            }
2739            _ => panic!("Expected CreateMod spec for memory, got {:?}", specs[1]),
2740        }
2741
2742        // Third: AddItem
2743        match &specs[2] {
2744            MutationSpec::AddItem {
2745                target, content, ..
2746            } => {
2747                if let MutationTargetSymbol::ByPath(path) = target {
2748                    assert_eq!(path.to_string(), "test_crate::infrastructure::memory");
2749                } else {
2750                    panic!("Expected ByPath target");
2751                }
2752                assert_eq!(content, "pub struct InMemoryRepo;");
2753            }
2754            _ => panic!("Expected AddItem spec, got {:?}", specs[2]),
2755        }
2756    }
2757
2758    #[test]
2759    fn test_add_code_crate_root_no_create_mod() {
2760        // AddCode to crate root should not generate any CreateMod
2761        let goal = Goal::new(
2762            "add code to crate root".to_string(),
2763            Intent::AddCode {
2764                symbol_id: None,
2765                symbol_path: Some("test_crate".to_string()), // Explicit crate root
2766                target_mod: None,
2767                code: "pub const VERSION: &str = \"1.0\";".to_string(),
2768            },
2769        );
2770
2771        let specs = Planner::plan(&goal, None).unwrap();
2772
2773        // Should only have AddItem (no CreateMod for crate root)
2774        assert_eq!(specs.len(), 1);
2775        match &specs[0] {
2776            MutationSpec::AddItem { target, .. } => {
2777                if let MutationTargetSymbol::ByPath(path) = target {
2778                    assert_eq!(path.to_string(), "test_crate");
2779                } else {
2780                    panic!("Expected ByPath target");
2781                }
2782            }
2783            _ => panic!("Expected AddItem spec"),
2784        }
2785    }
2786
2787    // === GenerateBuilder Intent Tests ===
2788
2789    #[test]
2790    fn test_generate_builder_intent() {
2791        let goal = Goal::new(
2792            "generate builder".to_string(),
2793            Intent::GenerateBuilder {
2794                symbol_id: None,
2795                symbol_path: None,
2796                target_struct: Some("Config".to_string()),
2797                target_mod: Some("test_crate::config".to_string()),
2798                fields: vec![
2799                    ("host".to_string(), "String".to_string()),
2800                    ("port".to_string(), "u16".to_string()),
2801                ],
2802                add_builder_method: true,
2803            },
2804        );
2805
2806        let specs = Planner::plan(&goal, None).unwrap();
2807
2808        // Should have 3 specs: AddItem (struct), AddItem (impl), AddMethod (builder)
2809        assert_eq!(specs.len(), 3);
2810
2811        // First spec: Builder struct
2812        match &specs[0] {
2813            MutationSpec::AddItem {
2814                target,
2815                content,
2816                position,
2817            } => {
2818                if let MutationTargetSymbol::ByPath(path) = target {
2819                    assert_eq!(path.to_string(), "test_crate::config");
2820                } else {
2821                    panic!("Expected ByPath target");
2822                }
2823                assert!(content.contains("pub struct ConfigBuilder"));
2824                assert!(content.contains("host: Option<String>"));
2825                assert!(content.contains("port: Option<u16>"));
2826                assert!(matches!(position, InsertPosition::Bottom));
2827            }
2828            _ => panic!("Expected AddItem spec for Builder struct"),
2829        }
2830
2831        // Second spec: Builder impl
2832        match &specs[1] {
2833            MutationSpec::AddItem {
2834                target, content, ..
2835            } => {
2836                if let MutationTargetSymbol::ByPath(path) = target {
2837                    assert_eq!(path.to_string(), "test_crate::config");
2838                } else {
2839                    panic!("Expected ByPath target");
2840                }
2841                assert!(content.contains("impl ConfigBuilder"));
2842                assert!(content.contains("pub fn new()"));
2843                assert!(content.contains("pub fn host("));
2844                assert!(content.contains("pub fn port("));
2845                assert!(content.contains("pub fn build("));
2846            }
2847            _ => panic!("Expected AddItem spec for Builder impl"),
2848        }
2849
2850        // Third spec: builder() method on Config
2851        match &specs[2] {
2852            MutationSpec::AddMethod {
2853                method_name,
2854                return_type,
2855                ..
2856            } => {
2857                assert_eq!(method_name, "builder");
2858                assert_eq!(return_type.as_deref(), Some("ConfigBuilder"));
2859            }
2860            _ => panic!("Expected AddMethod spec"),
2861        }
2862    }
2863
2864    #[test]
2865    fn test_generate_builder_without_builder_method() {
2866        let goal = Goal::new(
2867            "generate builder".to_string(),
2868            Intent::GenerateBuilder {
2869                symbol_id: None,
2870                symbol_path: None,
2871                target_struct: Some("User".to_string()),
2872                target_mod: Some("test_crate".to_string()), // Explicit target module
2873                fields: vec![("name".to_string(), "String".to_string())],
2874                add_builder_method: false,
2875            },
2876        );
2877
2878        let specs = Planner::plan(&goal, None).unwrap();
2879
2880        // Should have 2 specs: AddItem (struct), AddItem (impl)
2881        // No AddMethod because add_builder_method is false
2882        assert_eq!(specs.len(), 2);
2883
2884        // Target should be the specified module
2885        match &specs[0] {
2886            MutationSpec::AddItem { target, .. } => {
2887                if let MutationTargetSymbol::ByPath(path) = target {
2888                    assert_eq!(path.to_string(), "test_crate");
2889                } else {
2890                    panic!("Expected ByPath target");
2891                }
2892            }
2893            _ => panic!("Expected AddItem spec"),
2894        }
2895    }
2896
2897    #[test]
2898    fn test_generate_builder_missing_target_mod_returns_error() {
2899        // target_mod: None should return MissingTargetModule error
2900        let goal = Goal::new(
2901            "generate builder".to_string(),
2902            Intent::GenerateBuilder {
2903                symbol_id: None,
2904                symbol_path: None,
2905                target_struct: Some("User".to_string()),
2906                target_mod: None,
2907                fields: vec![("name".to_string(), "String".to_string())],
2908                add_builder_method: false,
2909            },
2910        );
2911
2912        let result = Planner::plan(&goal, None);
2913        assert!(matches!(
2914            result,
2915            Err(PlanError::MissingTargetModule { intent }) if intent == "GenerateBuilder"
2916        ));
2917    }
2918}