1use 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#[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
143pub struct Planner;
145
146impl Planner {
147 pub fn plan(goal: &Goal, registry: Option<&SymbolRegistry>) -> PlanResult<Vec<MutationSpec>> {
160 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 Ok(Self::deduplicate_create_mods(all_specs))
170 }
171
172 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 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 result.push(spec);
205 }
206 }
208 _ => {
209 result.push(spec);
211 }
212 }
213 }
214
215 result
216 }
217
218 fn intent_to_specs(
224 intent: &Intent,
225 registry: Option<&SymbolRegistry>,
226 pending_symbols: &HashSet<String>,
227 ) -> PlanResult<Vec<MutationSpec>> {
228 match intent {
229 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 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 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 let to_path_str = if to_module == "test_crate" || to_module.starts_with("crate::") {
302 to_module.replacen("test_crate", crate_name, 1)
304 } else if to_module.contains("::") {
305 to_module.to_string()
307 } else {
308 format!("{}::{}", crate_name, to_module)
310 };
311
312 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 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 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 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 Intent::AddDerive {
500 symbol_id,
501 symbol_path,
502 target_type,
503 derives,
504 } => {
505 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if let Ok(symbol_path) = SymbolPath::parse(path) {
1148 symbol_path
1149 } else {
1150 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 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 Intent::OrganizeImports {
1192 target_mod: _,
1193 deduplicate,
1194 merge_groups,
1195 } => Ok(vec![MutationSpec::OrganizeImports {
1196 module_id: None, deduplicate: *deduplicate,
1198 merge_groups: *merge_groups,
1199 }]),
1200
1201 Intent::MergeImplBlocks {
1202 target_mod: _,
1203 target_type: _,
1204 inherent_only: _,
1205 } => {
1206 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, target_var: target_var.clone(),
1218 }]),
1219
1220 Intent::UnwrapToQuestion {
1221 target_mod: _,
1222 target_fn,
1223 include_expect,
1224 } => {
1225 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, fn_id: None, expr: expr.clone(),
1250 var_name: var_name.clone(),
1251 }]),
1252
1253 Intent::GenerateBuilder {
1255 symbol_id: _,
1256 symbol_path: _,
1257 target_struct,
1258 target_mod,
1259 fields,
1260 add_builder_method,
1261 } => {
1262 let target_mod_str =
1264 target_mod
1265 .as_deref()
1266 .ok_or_else(|| PlanError::MissingTargetModule {
1267 intent: "GenerateBuilder".to_string(),
1268 })?;
1269
1270 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 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 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 if *add_builder_method {
1304 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 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, fn_id: None, 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, fn_id: None, 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, fn_id: None, old_stmt: old_stmt.clone(),
1393 new_stmt: new_stmt.clone(),
1394 symbol_path: symbol_path.clone(),
1395 }]),
1396
1397 Intent::AssignOp {
1399 target_mod: _,
1400 target_fn: _,
1401 } => Ok(vec![MutationSpec::AssignOp {
1402 module_id: None, fn_id: None, }]),
1405
1406 Intent::BoolSimplify { target_mod: _ } => Ok(vec![MutationSpec::BoolSimplify {
1407 module_id: None, }]),
1409
1410 Intent::CloneOnCopy { target_mod: _ } => Ok(vec![MutationSpec::CloneOnCopy {
1411 module_id: None, }]),
1413
1414 Intent::CollapsibleIf { target_mod: _ } => Ok(vec![MutationSpec::CollapsibleIf {
1415 module_id: None, }]),
1417
1418 Intent::ComparisonToMethod { target_mod: _ } => {
1419 Ok(vec![MutationSpec::ComparisonToMethod {
1420 module_id: None, }])
1422 }
1423
1424 Intent::RedundantClosure { target_mod: _ } => {
1425 Ok(vec![MutationSpec::RedundantClosure {
1426 module_id: None, }])
1428 }
1429
1430 Intent::ManualMap { target_mod: _ } => Ok(vec![MutationSpec::ManualMap {
1431 module_id: None, }]),
1433
1434 Intent::MatchToIfLet { target_mod: _ } => Ok(vec![MutationSpec::MatchToIfLet {
1435 module_id: None, }]),
1437
1438 Intent::FilterNext {
1439 target_mod: _,
1440 target_fn: _,
1441 } => Ok(vec![MutationSpec::FilterNext {
1442 module_id: None, fn_id: None, }]),
1445
1446 Intent::MapUnwrapOr {
1447 target_mod: _,
1448 target_fn: _,
1449 } => Ok(vec![MutationSpec::MapUnwrapOr {
1450 module_id: None, fn_id: None, }]),
1453
1454 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 Intent::Custom { description, .. } => Err(PlanError::UnsupportedIntent(format!(
1537 "Custom intent not directly supported: {}",
1538 description
1539 ))),
1540
1541 #[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 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 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 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 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 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
1614fn 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 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 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
1659fn 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
1674fn 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
1701fn resolve_symbol_by_name_any_kind(
1708 name: &str,
1709 registry: &SymbolRegistry,
1710) -> PlanResult<ryo_analysis::SymbolId> {
1711 if name.contains("::") {
1713 if let Ok(path) = SymbolPath::parse(name) {
1714 return resolve_symbol_by_path(&path, registry);
1715 }
1716 }
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
1738fn 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 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 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 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 Err(PlanError::CannotResolve {
1781 intent: context.to_string(),
1782 })
1783}
1784
1785fn 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 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 if let Some(path_str) = symbol_path {
1807 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 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 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
1852fn 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 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 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 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 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 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 Err(PlanError::CannotResolve {
1920 intent: context.to_string(),
1921 })
1922}
1923
1924fn normalize_generic_name(name: &str) -> String {
1935 name.replace(" < ", "<")
1937 .replace(" > ", ">")
1938 .replace("< ", "<")
1939 .replace(" >", ">")
1940 .replace(", ", ",")
1941 .replace(" ,", ",")
1942}
1943
1944fn 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
1964fn vec_to_symbol_path(
1980 segments: &[String],
1981 registry: Option<&SymbolRegistry>,
1982) -> PlanResult<SymbolPath> {
1983 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 let reg = registry.ok_or_else(|| PlanError::RegistryRequired {
1996 message: "Cannot resolve module path without SymbolRegistry".to_string(),
1997 })?;
1998
1999 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 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 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 ItemKind::Method => ryo_executor::ItemKind::Function,
2059 ItemKind::Field | ItemKind::TupleField => ryo_executor::ItemKind::Struct,
2060 ItemKind::Variant => ryo_executor::ItemKind::Enum,
2061 ItemKind::LocalVar | ItemKind::Parameter => ryo_executor::ItemKind::Function,
2063 ItemKind::Any | ItemKind::Other => ryo_executor::ItemKind::Struct, }
2066}
2067
2068fn extract_item_name_from_content(content: &str) -> Option<String> {
2071 let trimmed = content.trim();
2072
2073 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 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 if tokens.get(idx) == Some(&"pub") {
2094 idx += 1;
2095 if let Some(t) = tokens.get(idx) {
2097 if t.starts_with('(') {
2098 idx += 1;
2099 }
2100 }
2101 }
2102
2103 let keyword = tokens.get(idx)?;
2105 idx += 1;
2106
2107 match *keyword {
2108 "struct" | "enum" | "fn" | "type" | "const" | "static" | "trait" | "mod" => {
2109 let name = tokens.get(idx)?;
2111 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 None
2119 }
2120 _ => None,
2121 }
2122}
2123
2124fn 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
2146fn generate_create_mod_specs_without_registry(target: &SymbolPath) -> Vec<MutationSpec> {
2157 let segments: Vec<&str> = target.segments().collect();
2158
2159 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, });
2172 }
2173 }
2174
2175 specs
2176}
2177
2178fn generate_create_mod_specs(target: &SymbolPath, registry: &SymbolRegistry) -> Vec<MutationSpec> {
2191 let segments: Vec<&str> = target.segments().collect();
2192
2193 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 let mut specs = Vec::new();
2208 for depth in existing_depth..segments.len() {
2209 if depth == 0 {
2210 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, });
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
2262fn 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
2272fn 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 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(®istry)).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 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 #[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 assert_eq!(specs.len(), 3);
2369 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 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 assert_eq!(specs.len(), 3);
2405 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 assert_eq!(specs.len(), 2);
2437 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 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 assert_eq!(specs.len(), 2);
2470 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 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 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(®istry)).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 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 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(®istry)).unwrap();
2565
2566 assert_eq!(specs.len(), 3);
2568
2569 match &specs[0] {
2571 MutationSpec::CreateMod {
2572 target,
2573 mod_name,
2574 is_pub,
2575 ..
2576 } => {
2577 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 match &specs[1] {
2593 MutationSpec::CreateMod {
2594 target,
2595 mod_name,
2596 is_pub,
2597 ..
2598 } => {
2599 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 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 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(®istry)).unwrap();
2656
2657 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 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 let specs = Planner::plan(&goal, None).unwrap();
2686
2687 assert_eq!(
2689 specs.len(),
2690 3,
2691 "Expected 3 specs (2 CreateMod + 1 AddItem), got {}",
2692 specs.len()
2693 );
2694
2695 match &specs[0] {
2697 MutationSpec::CreateMod {
2698 target,
2699 mod_name,
2700 is_pub,
2701 ..
2702 } => {
2703 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 match &specs[1] {
2722 MutationSpec::CreateMod {
2723 target,
2724 mod_name,
2725 is_pub,
2726 ..
2727 } => {
2728 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 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 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()), 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 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 #[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 assert_eq!(specs.len(), 3);
2810
2811 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 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 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()), 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 assert_eq!(specs.len(), 2);
2883
2884 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 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}