Skip to main content

ryo_executor/executor/
conflict.rs

1//! Conflict Detection for MutationSpecs
2//!
3//! Provides simple, reliable conflict detection based on MutationTargetSymbol.
4//! Used by Executor and Orchestrator for grouping mutations.
5//!
6//! # Design
7//!
8//! - **SymbolId/Path-based**: Works with MutationTargetSymbol directly
9//! - **Conservative**: Unresolved targets = potential conflict (safe default)
10//! - **Global**: Can be used anywhere in executor layer
11//!
12//! # Functions
13//!
14//! - `target_conflicts`: Check if two MutationTargetSymbols conflict
15//! - `specs_conflict`: Check if two MutationSpecs conflict
16//! - `group_by_conflicts`: Partition specs into conflict-free groups
17
18use super::spec::{MutationSpec, MutationTargetSymbol};
19use std::collections::HashMap;
20
21/// Check if two MutationTargetSymbols potentially conflict.
22///
23/// # Rules
24///
25/// - **ById vs ById**: Compare SymbolId directly
26/// - **ByPath vs ByPath**: Compare SymbolPath directly
27/// - **ByKindAndName vs ByKindAndName**: Compare Kind + Name
28/// - **Mixed variants**: Conservative (return true = potential conflict)
29/// - **ByAffectedId**: Compare parent_id + name
30///
31/// # Conservative Strategy
32///
33/// When resolution status differs (e.g., ById vs ByPath), we conservatively
34/// assume they *might* conflict until both are resolved. This prevents
35/// incorrect parallel execution that could corrupt code.
36///
37/// # Examples
38///
39/// ```
40/// use ryo_executor::executor::target_conflicts;
41/// use ryo_executor::MutationTargetSymbol;
42/// use ryo_symbol::SymbolId;
43///
44/// let id1 = SymbolId::parse("1v1").unwrap();
45/// let id2 = SymbolId::parse("2v1").unwrap();
46///
47/// // Same ID = conflict
48/// assert!(target_conflicts(
49///     &MutationTargetSymbol::ById(id1),
50///     &MutationTargetSymbol::ById(id1)
51/// ));
52///
53/// // Different IDs = no conflict
54/// assert!(!target_conflicts(
55///     &MutationTargetSymbol::ById(id1),
56///     &MutationTargetSymbol::ById(id2)
57/// ));
58/// ```
59pub fn target_conflicts(a: &MutationTargetSymbol, b: &MutationTargetSymbol) -> bool {
60    match (a, b) {
61        // Same SymbolId = definite conflict
62        (MutationTargetSymbol::ById(id_a), MutationTargetSymbol::ById(id_b)) => id_a == id_b,
63
64        // Same SymbolPath = definite conflict
65        (MutationTargetSymbol::ByPath(path_a), MutationTargetSymbol::ByPath(path_b)) => {
66            path_a == path_b
67        }
68
69        // Same Kind+Name = definite conflict
70        (
71            MutationTargetSymbol::ByKindAndName(kind_a, name_a),
72            MutationTargetSymbol::ByKindAndName(kind_b, name_b),
73        ) => kind_a == kind_b && name_a == name_b,
74
75        // Same parent + name = definite conflict
76        (
77            MutationTargetSymbol::ByAffectedId {
78                parent_id: parent_a,
79                name: name_a,
80                ..
81            },
82            MutationTargetSymbol::ByAffectedId {
83                parent_id: parent_b,
84                name: name_b,
85                ..
86            },
87        ) => parent_a == parent_b && name_a == name_b,
88
89        // Mixed variants: Conservative approach
90        // Can't determine without resolution, assume potential conflict
91        _ => true,
92    }
93}
94
95/// Check if two MutationSpecs conflict.
96///
97/// # Rules
98///
99/// - Compare target_symbol fields using `target_conflicts`
100/// - Specs without target_symbol (project-wide) always conflict
101/// - Some variants have additional conflict rules (e.g., MoveItem has source+target)
102///
103/// # Conservative Defaults
104///
105/// - Unknown/deprecated variants without target_symbol = always conflict
106/// - This ensures safety over performance
107///
108/// # Examples
109///
110/// ```
111/// use ryo_executor::executor::specs_conflict;
112/// use ryo_executor::{MutationSpec, MutationTargetSymbol};
113/// use ryo_symbol::SymbolId;
114///
115/// let id1 = SymbolId::parse("1v1").unwrap();
116/// let id2 = SymbolId::parse("2v1").unwrap();
117///
118/// let spec_a = MutationSpec::AddField {
119///     target: MutationTargetSymbol::ById(id1),
120///     field_name: "name".into(),
121///     field_type: "String".into(),
122///     visibility: Default::default(),
123/// };
124///
125/// let spec_b = MutationSpec::AddField {
126///     target: MutationTargetSymbol::ById(id2),
127///     field_name: "email".into(),
128///     field_type: "String".into(),
129///     visibility: Default::default(),
130/// };
131///
132/// // Different targets = no conflict
133/// assert!(!specs_conflict(&spec_a, &spec_b));
134/// ```
135pub fn specs_conflict(a: &MutationSpec, b: &MutationSpec) -> bool {
136    use MutationSpec::*;
137
138    match (a, b) {
139        // === Field operations ===
140        // AddField to same struct with DIFFERENT field names = no conflict (parallel safe)
141        (
142            AddField {
143                target: target_a,
144                field_name: field_a,
145                ..
146            },
147            AddField {
148                target: target_b,
149                field_name: field_b,
150                ..
151            },
152        ) => target_conflicts(target_a, target_b) && field_a == field_b,
153        // AddField vs RemoveField, or RemoveField vs RemoveField always check target+field
154        (
155            AddField {
156                target: target_a,
157                field_name: field_a,
158                ..
159            },
160            RemoveField {
161                target: target_b,
162                field_name: field_b,
163                ..
164            },
165        )
166        | (
167            RemoveField {
168                target: target_a,
169                field_name: field_a,
170                ..
171            },
172            AddField {
173                target: target_b,
174                field_name: field_b,
175                ..
176            },
177        )
178        | (
179            RemoveField {
180                target: target_a,
181                field_name: field_a,
182                ..
183            },
184            RemoveField {
185                target: target_b,
186                field_name: field_b,
187                ..
188            },
189        ) => target_conflicts(target_a, target_b) && field_a == field_b,
190
191        // === Variant operations ===
192        // AddVariant to same enum with DIFFERENT variant names = no conflict
193        (
194            AddVariant {
195                target: target_a,
196                variant_name: variant_a,
197                ..
198            },
199            AddVariant {
200                target: target_b,
201                variant_name: variant_b,
202                ..
203            },
204        ) => target_conflicts(target_a, target_b) && variant_a == variant_b,
205        (
206            AddVariant {
207                target: target_a,
208                variant_name: variant_a,
209                ..
210            },
211            RemoveVariant {
212                target: target_b,
213                variant_name: variant_b,
214                ..
215            },
216        )
217        | (
218            RemoveVariant {
219                target: target_a,
220                variant_name: variant_a,
221                ..
222            },
223            AddVariant {
224                target: target_b,
225                variant_name: variant_b,
226                ..
227            },
228        )
229        | (
230            RemoveVariant {
231                target: target_a,
232                variant_name: variant_a,
233                ..
234            },
235            RemoveVariant {
236                target: target_b,
237                variant_name: variant_b,
238                ..
239            },
240        ) => target_conflicts(target_a, target_b) && variant_a == variant_b,
241
242        // === Method operations ===
243        // AddMethod to same impl with DIFFERENT method names = no conflict
244        (
245            AddMethod {
246                target: target_a,
247                method_name: method_a,
248                ..
249            },
250            AddMethod {
251                target: target_b,
252                method_name: method_b,
253                ..
254            },
255        ) => target_conflicts(target_a, target_b) && method_a == method_b,
256        (
257            AddMethod {
258                target: target_a,
259                method_name: method_a,
260                ..
261            },
262            RemoveMethod {
263                target: target_b,
264                method_name: method_b,
265                ..
266            },
267        )
268        | (
269            RemoveMethod {
270                target: target_a,
271                method_name: method_a,
272                ..
273            },
274            AddMethod {
275                target: target_b,
276                method_name: method_b,
277                ..
278            },
279        )
280        | (
281            RemoveMethod {
282                target: target_a,
283                method_name: method_a,
284                ..
285            },
286            RemoveMethod {
287                target: target_b,
288                method_name: method_b,
289                ..
290            },
291        ) => target_conflicts(target_a, target_b) && method_a == method_b,
292
293        // === Module operations ===
294        (
295            RemoveMod {
296                target: target_a, ..
297            },
298            RemoveMod {
299                target: target_b, ..
300            },
301        )
302        | (
303            RemoveMod {
304                target: target_a, ..
305            },
306            CreateMod {
307                target: target_b, ..
308            },
309        )
310        | (
311            CreateMod {
312                target: target_a, ..
313            },
314            RemoveMod {
315                target: target_b, ..
316            },
317        )
318        | (
319            CreateMod {
320                target: target_a, ..
321            },
322            CreateMod {
323                target: target_b, ..
324            },
325        ) => target_conflicts(target_a, target_b),
326
327        // === Rename operations ===
328        (
329            Rename {
330                target: target_a, ..
331            },
332            Rename {
333                target: target_b, ..
334            },
335        ) => target_conflicts(target_a, target_b),
336
337        // === Derive operations ===
338        (
339            AddDerive {
340                target: target_a, ..
341            },
342            AddDerive {
343                target: target_b, ..
344            },
345        )
346        | (
347            AddDerive {
348                target: target_a, ..
349            },
350            RemoveDerive {
351                target: target_b, ..
352            },
353        )
354        | (
355            RemoveDerive {
356                target: target_a, ..
357            },
358            AddDerive {
359                target: target_b, ..
360            },
361        )
362        | (
363            RemoveDerive {
364                target: target_a, ..
365            },
366            RemoveDerive {
367                target: target_b, ..
368            },
369        ) => target_conflicts(target_a, target_b),
370
371        // === Visibility operations ===
372        (
373            ChangeVisibility {
374                target: target_a, ..
375            },
376            ChangeVisibility {
377                target: target_b, ..
378            },
379        ) => target_conflicts(target_a, target_b),
380
381        // === Remove item operations ===
382        (
383            RemoveItem {
384                target: target_a, ..
385            },
386            RemoveItem {
387                target: target_b, ..
388            },
389        ) => target_conflicts(target_a, target_b),
390
391        // === Trait operations ===
392        (
393            ExtractTrait {
394                target: target_a, ..
395            },
396            ExtractTrait {
397                target: target_b, ..
398            },
399        )
400        | (
401            ExtractTrait {
402                target: target_a, ..
403            },
404            InlineTrait {
405                target: target_b, ..
406            },
407        )
408        | (
409            InlineTrait {
410                target: target_a, ..
411            },
412            ExtractTrait {
413                target: target_b, ..
414            },
415        )
416        | (
417            InlineTrait {
418                target: target_a, ..
419            },
420            InlineTrait {
421                target: target_b, ..
422            },
423        ) => target_conflicts(target_a, target_b),
424
425        // === EnumToTrait ===
426        (
427            EnumToTrait {
428                target: target_a, ..
429            },
430            EnumToTrait {
431                target: target_b, ..
432            },
433        ) => target_conflicts(target_a, target_b),
434
435        // === Duplicate operations ===
436        (
437            DuplicateFunction {
438                target: target_a, ..
439            },
440            DuplicateFunction {
441                target: target_b, ..
442            },
443        )
444        | (
445            DuplicateStruct {
446                target: target_a, ..
447            },
448            DuplicateStruct {
449                target: target_b, ..
450            },
451        )
452        | (
453            DuplicateEnum {
454                target: target_a, ..
455            },
456            DuplicateEnum {
457                target: target_b, ..
458            },
459        )
460        | (
461            DuplicateModTree {
462                target: target_a, ..
463            },
464            DuplicateModTree {
465                target: target_b, ..
466            },
467        ) => target_conflicts(target_a, target_b),
468
469        // === MoveItem (special: has source + target) ===
470        (
471            MoveItem {
472                source: source_a,
473                target: target_a,
474                ..
475            },
476            MoveItem {
477                source: source_b,
478                target: target_b,
479                ..
480            },
481        ) => {
482            // Conflict if either source or target conflicts
483            target_conflicts(source_a, source_b) || target_conflicts(target_a, target_b)
484        }
485
486        // === Struct literal field operations ===
487        (
488            AddStructLiteralField {
489                target: target_a, ..
490            },
491            AddStructLiteralField {
492                target: target_b, ..
493            },
494        )
495        | (
496            AddStructLiteralField {
497                target: target_a, ..
498            },
499            RemoveStructLiteralField {
500                target: target_b, ..
501            },
502        )
503        | (
504            RemoveStructLiteralField {
505                target: target_a, ..
506            },
507            AddStructLiteralField {
508                target: target_b, ..
509            },
510        )
511        | (
512            RemoveStructLiteralField {
513                target: target_a, ..
514            },
515            RemoveStructLiteralField {
516                target: target_b, ..
517            },
518        ) => target_conflicts(target_a, target_b),
519
520        // === Cross-variant conflicts ===
521        // Operations on same target but different variant types can conflict
522        // Example: AddField + RemoveItem on same struct
523        (
524            AddField {
525                target: target_a, ..
526            },
527            RemoveItem {
528                target: target_b, ..
529            },
530        )
531        | (
532            RemoveItem {
533                target: target_a, ..
534            },
535            AddField {
536                target: target_b, ..
537            },
538        )
539        | (
540            AddMethod {
541                target: target_a, ..
542            },
543            RemoveItem {
544                target: target_b, ..
545            },
546        )
547        | (
548            RemoveItem {
549                target: target_a, ..
550            },
551            AddMethod {
552                target: target_b, ..
553            },
554        )
555        | (
556            AddVariant {
557                target: target_a, ..
558            },
559            RemoveItem {
560                target: target_b, ..
561            },
562        )
563        | (
564            RemoveItem {
565                target: target_a, ..
566            },
567            AddVariant {
568                target: target_b, ..
569            },
570        )
571        | (
572            AddDerive {
573                target: target_a, ..
574            },
575            RemoveItem {
576                target: target_b, ..
577            },
578        )
579        | (
580            RemoveItem {
581                target: target_a, ..
582            },
583            AddDerive {
584                target: target_b, ..
585            },
586        )
587        | (
588            ChangeVisibility {
589                target: target_a, ..
590            },
591            RemoveItem {
592                target: target_b, ..
593            },
594        )
595        | (
596            RemoveItem {
597                target: target_a, ..
598            },
599            ChangeVisibility {
600                target: target_b, ..
601            },
602        )
603        | (
604            Rename {
605                target: target_a, ..
606            },
607            RemoveItem {
608                target: target_b, ..
609            },
610        )
611        | (
612            RemoveItem {
613                target: target_a, ..
614            },
615            Rename {
616                target: target_b, ..
617            },
618        ) => target_conflicts(target_a, target_b),
619
620        // === Project-wide operations (no target_symbol) ===
621        // These always conflict with each other (conservative)
622        (AddItem { .. }, AddItem { .. }) => true, // Module-level, path-based
623        (OrganizeImports { .. }, OrganizeImports { .. }) => true,
624
625        // === Idiom operations (module-scoped) ===
626        // These have module_id but no specific target_symbol
627        // Conflict if same module
628        (LoopToIterator { module_id: m_a, .. }, LoopToIterator { module_id: m_b, .. })
629        | (UnwrapToQuestion { module_id: m_a, .. }, UnwrapToQuestion { module_id: m_b, .. })
630        | (AssignOp { module_id: m_a, .. }, AssignOp { module_id: m_b, .. })
631        | (BoolSimplify { module_id: m_a, .. }, BoolSimplify { module_id: m_b, .. })
632        | (CloneOnCopy { module_id: m_a, .. }, CloneOnCopy { module_id: m_b, .. })
633        | (CollapsibleIf { module_id: m_a, .. }, CollapsibleIf { module_id: m_b, .. })
634        | (NoOpArmToTodo { module_id: m_a, .. }, NoOpArmToTodo { module_id: m_b, .. })
635        | (ComparisonToMethod { module_id: m_a, .. }, ComparisonToMethod { module_id: m_b, .. })
636        | (RedundantClosure { module_id: m_a, .. }, RedundantClosure { module_id: m_b, .. })
637        | (IntroduceVariable { module_id: m_a, .. }, IntroduceVariable { module_id: m_b, .. })
638        | (ManualMap { module_id: m_a, .. }, ManualMap { module_id: m_b, .. })
639        | (MatchToIfLet { module_id: m_a, .. }, MatchToIfLet { module_id: m_b, .. })
640        | (FilterNext { module_id: m_a, .. }, FilterNext { module_id: m_b, .. })
641        | (MapUnwrapOr { module_id: m_a, .. }, MapUnwrapOr { module_id: m_b, .. }) => m_a == m_b,
642
643        // === Statement operations (function-scoped) ===
644        // ReplaceExpr, RemoveStatement, ReplaceStatement have Option<SymbolId> fn_id
645        (
646            ReplaceExpr {
647                fn_id: f_a,
648                module_id: m_a,
649                ..
650            },
651            ReplaceExpr {
652                fn_id: f_b,
653                module_id: m_b,
654                ..
655            },
656        )
657        | (
658            RemoveStatement {
659                fn_id: f_a,
660                module_id: m_a,
661                ..
662            },
663            RemoveStatement {
664                fn_id: f_b,
665                module_id: m_b,
666                ..
667            },
668        )
669        | (
670            ReplaceStatement {
671                fn_id: f_a,
672                module_id: m_a,
673                ..
674            },
675            ReplaceStatement {
676                fn_id: f_b,
677                module_id: m_b,
678                ..
679            },
680        ) => {
681            // Conflict if same function or both target same module (None = all functions)
682            (f_a.is_some() && f_a == f_b) || (f_a.is_none() && f_b.is_none() && m_a == m_b)
683        }
684
685        // InsertStatement has non-optional fn_id
686        (InsertStatement { fn_id: f_a, .. }, InsertStatement { fn_id: f_b, .. }) => f_a == f_b,
687
688        // === Match arm operations (function-scoped) ===
689        (AddMatchArm { target: t_a, .. }, AddMatchArm { target: t_b, .. })
690        | (RemoveMatchArm { target: t_a, .. }, RemoveMatchArm { target: t_b, .. })
691        | (ReplaceMatchArm { target: t_a, .. }, ReplaceMatchArm { target: t_b, .. }) => t_a == t_b,
692
693        // === Type operations ===
694        (ReplaceType { .. }, ReplaceType { .. }) => true, // Project-wide
695
696        // === Spec operations ===
697        (AddSpec { .. }, AddSpec { .. }) => true, // Project-wide
698
699        // === Plugin operations ===
700        (PluginTransform { .. }, PluginTransform { .. }) => true, // Project-wide
701
702        // === Additional cross-variant conflicts ===
703        // Same target operations of different types (e.g., AddDerive + ChangeVisibility)
704        (
705            AddField {
706                target: target_a, ..
707            },
708            AddDerive {
709                target: target_b, ..
710            },
711        )
712        | (
713            AddField {
714                target: target_a, ..
715            },
716            RemoveDerive {
717                target: target_b, ..
718            },
719        )
720        | (
721            AddField {
722                target: target_a, ..
723            },
724            ChangeVisibility {
725                target: target_b, ..
726            },
727        )
728        | (
729            AddField {
730                target: target_a, ..
731            },
732            Rename {
733                target: target_b, ..
734            },
735        )
736        | (
737            RemoveField {
738                target: target_a, ..
739            },
740            AddDerive {
741                target: target_b, ..
742            },
743        )
744        | (
745            RemoveField {
746                target: target_a, ..
747            },
748            RemoveDerive {
749                target: target_b, ..
750            },
751        )
752        | (
753            RemoveField {
754                target: target_a, ..
755            },
756            ChangeVisibility {
757                target: target_b, ..
758            },
759        )
760        | (
761            RemoveField {
762                target: target_a, ..
763            },
764            Rename {
765                target: target_b, ..
766            },
767        )
768        | (
769            AddVariant {
770                target: target_a, ..
771            },
772            AddDerive {
773                target: target_b, ..
774            },
775        )
776        | (
777            AddVariant {
778                target: target_a, ..
779            },
780            RemoveDerive {
781                target: target_b, ..
782            },
783        )
784        | (
785            AddVariant {
786                target: target_a, ..
787            },
788            ChangeVisibility {
789                target: target_b, ..
790            },
791        )
792        | (
793            AddVariant {
794                target: target_a, ..
795            },
796            Rename {
797                target: target_b, ..
798            },
799        )
800        | (
801            RemoveVariant {
802                target: target_a, ..
803            },
804            AddDerive {
805                target: target_b, ..
806            },
807        )
808        | (
809            RemoveVariant {
810                target: target_a, ..
811            },
812            RemoveDerive {
813                target: target_b, ..
814            },
815        )
816        | (
817            RemoveVariant {
818                target: target_a, ..
819            },
820            ChangeVisibility {
821                target: target_b, ..
822            },
823        )
824        | (
825            RemoveVariant {
826                target: target_a, ..
827            },
828            Rename {
829                target: target_b, ..
830            },
831        )
832        | (
833            AddMethod {
834                target: target_a, ..
835            },
836            AddDerive {
837                target: target_b, ..
838            },
839        )
840        | (
841            AddMethod {
842                target: target_a, ..
843            },
844            RemoveDerive {
845                target: target_b, ..
846            },
847        )
848        | (
849            AddMethod {
850                target: target_a, ..
851            },
852            ChangeVisibility {
853                target: target_b, ..
854            },
855        )
856        | (
857            AddMethod {
858                target: target_a, ..
859            },
860            Rename {
861                target: target_b, ..
862            },
863        )
864        | (
865            RemoveMethod {
866                target: target_a, ..
867            },
868            AddDerive {
869                target: target_b, ..
870            },
871        )
872        | (
873            RemoveMethod {
874                target: target_a, ..
875            },
876            RemoveDerive {
877                target: target_b, ..
878            },
879        )
880        | (
881            RemoveMethod {
882                target: target_a, ..
883            },
884            ChangeVisibility {
885                target: target_b, ..
886            },
887        )
888        | (
889            RemoveMethod {
890                target: target_a, ..
891            },
892            Rename {
893                target: target_b, ..
894            },
895        )
896        | (
897            AddDerive {
898                target: target_a, ..
899            },
900            ChangeVisibility {
901                target: target_b, ..
902            },
903        )
904        | (
905            AddDerive {
906                target: target_a, ..
907            },
908            Rename {
909                target: target_b, ..
910            },
911        )
912        | (
913            RemoveDerive {
914                target: target_a, ..
915            },
916            ChangeVisibility {
917                target: target_b, ..
918            },
919        )
920        | (
921            RemoveDerive {
922                target: target_a, ..
923            },
924            Rename {
925                target: target_b, ..
926            },
927        )
928        | (
929            ChangeVisibility {
930                target: target_a, ..
931            },
932            AddDerive {
933                target: target_b, ..
934            },
935        )
936        | (
937            ChangeVisibility {
938                target: target_a, ..
939            },
940            RemoveDerive {
941                target: target_b, ..
942            },
943        )
944        | (
945            ChangeVisibility {
946                target: target_a, ..
947            },
948            Rename {
949                target: target_b, ..
950            },
951        )
952        | (
953            Rename {
954                target: target_a, ..
955            },
956            AddField {
957                target: target_b, ..
958            },
959        )
960        | (
961            Rename {
962                target: target_a, ..
963            },
964            RemoveField {
965                target: target_b, ..
966            },
967        )
968        | (
969            Rename {
970                target: target_a, ..
971            },
972            AddVariant {
973                target: target_b, ..
974            },
975        )
976        | (
977            Rename {
978                target: target_a, ..
979            },
980            RemoveVariant {
981                target: target_b, ..
982            },
983        )
984        | (
985            Rename {
986                target: target_a, ..
987            },
988            AddMethod {
989                target: target_b, ..
990            },
991        )
992        | (
993            Rename {
994                target: target_a, ..
995            },
996            RemoveMethod {
997                target: target_b, ..
998            },
999        )
1000        | (
1001            Rename {
1002                target: target_a, ..
1003            },
1004            AddDerive {
1005                target: target_b, ..
1006            },
1007        )
1008        | (
1009            Rename {
1010                target: target_a, ..
1011            },
1012            RemoveDerive {
1013                target: target_b, ..
1014            },
1015        )
1016        | (
1017            Rename {
1018                target: target_a, ..
1019            },
1020            ChangeVisibility {
1021                target: target_b, ..
1022            },
1023        ) => target_conflicts(target_a, target_b),
1024
1025        // === Different variant types = no conflict by default ===
1026        // Exception: Already handled above
1027        _ => false,
1028    }
1029}
1030
1031/// Find all conflicting spec pairs in a list.
1032///
1033/// Returns pairs of indices (i, j) where specs[i] conflicts with specs[j].
1034///
1035/// # Examples
1036///
1037/// ```
1038/// use ryo_executor::executor::find_conflicting_pairs;
1039/// use ryo_executor::{MutationSpec, MutationTargetSymbol};
1040/// use ryo_symbol::SymbolId;
1041///
1042/// let id1 = SymbolId::parse("1v1").unwrap();
1043///
1044/// let specs = vec![
1045///     MutationSpec::AddField {
1046///         target: MutationTargetSymbol::ById(id1),
1047///         field_name: "name".into(),
1048///         field_type: "String".into(),
1049///         visibility: Default::default(),
1050///     },
1051///     MutationSpec::AddField {
1052///         target: MutationTargetSymbol::ById(id1),
1053///         field_name: "name".into(),  // Same field name = conflict
1054///         field_type: "i32".into(),
1055///         visibility: Default::default(),
1056///     },
1057/// ];
1058///
1059/// let conflicts = find_conflicting_pairs(&specs);
1060/// // specs[0] and specs[1] conflict (same target AND same field name)
1061/// assert_eq!(conflicts, vec![(0, 1)]);
1062/// ```
1063pub fn find_conflicting_pairs(specs: &[MutationSpec]) -> Vec<(usize, usize)> {
1064    let mut conflicts = Vec::new();
1065    for i in 0..specs.len() {
1066        for j in (i + 1)..specs.len() {
1067            if specs_conflict(&specs[i], &specs[j]) {
1068                conflicts.push((i, j));
1069            }
1070        }
1071    }
1072    conflicts
1073}
1074
1075/// Partition specs into conflict-free groups using Union-Find.
1076///
1077/// Specs in the same group may conflict with each other.
1078/// Specs in different groups are guaranteed to be independent.
1079///
1080/// # Algorithm
1081///
1082/// Uses Union-Find to efficiently group specs that transitively conflict:
1083/// - If A conflicts with B, and B conflicts with C, then A, B, C are in same group
1084/// - Groups can be executed sequentially, while different groups can run in parallel
1085///
1086/// # Returns
1087///
1088/// Vector of groups, where each group is a vector of spec indices.
1089///
1090/// # Examples
1091///
1092/// ```
1093/// use ryo_executor::executor::group_by_conflicts;
1094/// use ryo_executor::{MutationSpec, MutationTargetSymbol};
1095/// use ryo_symbol::SymbolId;
1096///
1097/// let id1 = SymbolId::parse("1v1").unwrap();
1098/// let id2 = SymbolId::parse("2v1").unwrap();
1099///
1100/// let specs = vec![
1101///     MutationSpec::AddField {
1102///         target: MutationTargetSymbol::ById(id1),
1103///         field_name: "name".into(),
1104///         field_type: "String".into(),
1105///         visibility: Default::default(),
1106///     },
1107///     MutationSpec::AddField {
1108///         target: MutationTargetSymbol::ById(id2),
1109///         field_name: "email".into(),
1110///         field_type: "String".into(),
1111///         visibility: Default::default(),
1112///     },
1113/// ];
1114///
1115/// let groups = group_by_conflicts(&specs);
1116/// // Two independent groups (different targets)
1117/// assert_eq!(groups.len(), 2);
1118/// ```
1119pub fn group_by_conflicts(specs: &[MutationSpec]) -> Vec<Vec<usize>> {
1120    if specs.is_empty() {
1121        return vec![];
1122    }
1123
1124    // Union-Find structure
1125    let mut parent: Vec<usize> = (0..specs.len()).collect();
1126    let mut rank: Vec<usize> = vec![0; specs.len()];
1127
1128    fn find(parent: &mut [usize], i: usize) -> usize {
1129        if parent[i] != i {
1130            parent[i] = find(parent, parent[i]); // Path compression
1131        }
1132        parent[i]
1133    }
1134
1135    fn union(parent: &mut [usize], rank: &mut [usize], i: usize, j: usize) {
1136        let root_i = find(parent, i);
1137        let root_j = find(parent, j);
1138        if root_i != root_j {
1139            // Union by rank
1140            if rank[root_i] < rank[root_j] {
1141                parent[root_i] = root_j;
1142            } else if rank[root_i] > rank[root_j] {
1143                parent[root_j] = root_i;
1144            } else {
1145                parent[root_j] = root_i;
1146                rank[root_i] += 1;
1147            }
1148        }
1149    }
1150
1151    // Union specs that conflict
1152    for i in 0..specs.len() {
1153        for j in (i + 1)..specs.len() {
1154            if specs_conflict(&specs[i], &specs[j]) {
1155                union(&mut parent, &mut rank, i, j);
1156            }
1157        }
1158    }
1159
1160    // Group by root
1161    let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
1162    for i in 0..specs.len() {
1163        let root = find(&mut parent, i);
1164        groups.entry(root).or_default().push(i);
1165    }
1166
1167    groups.into_values().collect()
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172    use super::*;
1173    use ryo_symbol::SymbolId;
1174
1175    fn dummy_id(index: u32) -> SymbolId {
1176        SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
1177    }
1178
1179    #[test]
1180    fn test_target_conflicts_same_id() {
1181        let id1 = dummy_id(1);
1182        let target_a = MutationTargetSymbol::ById(id1);
1183        let target_b = MutationTargetSymbol::ById(id1);
1184
1185        assert!(target_conflicts(&target_a, &target_b));
1186    }
1187
1188    #[test]
1189    fn test_target_conflicts_different_ids() {
1190        let id1 = dummy_id(1);
1191        let id2 = dummy_id(2);
1192        let target_a = MutationTargetSymbol::ById(id1);
1193        let target_b = MutationTargetSymbol::ById(id2);
1194
1195        assert!(!target_conflicts(&target_a, &target_b));
1196    }
1197
1198    #[test]
1199    fn test_specs_conflict_same_target_same_field() {
1200        let id1 = dummy_id(1);
1201        let spec_a = MutationSpec::AddField {
1202            target: MutationTargetSymbol::ById(id1),
1203            field_name: "name".into(),
1204            field_type: "String".into(),
1205            visibility: Default::default(),
1206        };
1207        let spec_b = MutationSpec::AddField {
1208            target: MutationTargetSymbol::ById(id1),
1209            field_name: "name".into(), // Same field name = conflict
1210            field_type: "String".into(),
1211            visibility: Default::default(),
1212        };
1213
1214        assert!(specs_conflict(&spec_a, &spec_b));
1215    }
1216
1217    #[test]
1218    fn test_specs_no_conflict_same_target_different_fields() {
1219        let id1 = dummy_id(1);
1220        let spec_a = MutationSpec::AddField {
1221            target: MutationTargetSymbol::ById(id1),
1222            field_name: "name".into(),
1223            field_type: "String".into(),
1224            visibility: Default::default(),
1225        };
1226        let spec_b = MutationSpec::AddField {
1227            target: MutationTargetSymbol::ById(id1),
1228            field_name: "email".into(), // Different field name = no conflict
1229            field_type: "String".into(),
1230            visibility: Default::default(),
1231        };
1232
1233        assert!(!specs_conflict(&spec_a, &spec_b));
1234    }
1235
1236    #[test]
1237    fn test_specs_conflict_different_targets() {
1238        let id1 = dummy_id(1);
1239        let id2 = dummy_id(2);
1240        let spec_a = MutationSpec::AddField {
1241            target: MutationTargetSymbol::ById(id1),
1242            field_name: "name".into(),
1243            field_type: "String".into(),
1244            visibility: Default::default(),
1245        };
1246        let spec_b = MutationSpec::AddField {
1247            target: MutationTargetSymbol::ById(id2),
1248            field_name: "email".into(),
1249            field_type: "String".into(),
1250            visibility: Default::default(),
1251        };
1252
1253        assert!(!specs_conflict(&spec_a, &spec_b));
1254    }
1255
1256    #[test]
1257    fn test_group_by_conflicts_independent() {
1258        let id1 = dummy_id(1);
1259        let id2 = dummy_id(2);
1260
1261        let specs = vec![
1262            MutationSpec::AddField {
1263                target: MutationTargetSymbol::ById(id1),
1264                field_name: "name".into(),
1265                field_type: "String".into(),
1266                visibility: Default::default(),
1267            },
1268            MutationSpec::AddField {
1269                target: MutationTargetSymbol::ById(id2),
1270                field_name: "email".into(),
1271                field_type: "String".into(),
1272                visibility: Default::default(),
1273            },
1274        ];
1275
1276        let groups = group_by_conflicts(&specs);
1277        assert_eq!(groups.len(), 2); // Two independent groups
1278    }
1279
1280    #[test]
1281    fn test_group_by_conflicts_conflicting() {
1282        let id1 = dummy_id(1);
1283
1284        let specs = vec![
1285            MutationSpec::AddField {
1286                target: MutationTargetSymbol::ById(id1),
1287                field_name: "name".into(),
1288                field_type: "String".into(),
1289                visibility: Default::default(),
1290            },
1291            MutationSpec::AddField {
1292                target: MutationTargetSymbol::ById(id1),
1293                field_name: "name".into(), // Same field name = conflict
1294                field_type: "i32".into(),
1295                visibility: Default::default(),
1296            },
1297        ];
1298
1299        let groups = group_by_conflicts(&specs);
1300        assert_eq!(groups.len(), 1); // One group (both conflict)
1301        assert_eq!(groups[0].len(), 2);
1302    }
1303
1304    #[test]
1305    fn test_cross_variant_conflict() {
1306        let id1 = dummy_id(1);
1307
1308        let spec_add = MutationSpec::AddField {
1309            target: MutationTargetSymbol::ById(id1),
1310            field_name: "name".into(),
1311            field_type: "String".into(),
1312            visibility: Default::default(),
1313        };
1314
1315        let spec_remove = MutationSpec::RemoveItem {
1316            target: MutationTargetSymbol::ById(id1),
1317            item_kind: ryo_source::ItemKind::Struct,
1318        };
1319
1320        // AddField + RemoveItem on same target should conflict
1321        assert!(specs_conflict(&spec_add, &spec_remove));
1322    }
1323}