@@ -867,60 +867,129 @@ func findSplitMergePoints(
867867 return result
868868}
869869
870- // findMergeForSplit finds the ExclusiveMerge where branches from a split converge.
870+ // findMergeForSplit finds the nearest node where branches from a split converge.
871+ // Studio Pro models often converge directly on the next activity instead of an
872+ // explicit ExclusiveMerge, so the join can be any executable microflow object.
871873func findMergeForSplit (
872874 ctx * ExecContext ,
873875 splitID model.ID ,
874876 flowsByOrigin map [model.ID ][]* microflows.SequenceFlow ,
875877 activityMap map [model.ID ]microflows.MicroflowObject ,
876878) model.ID {
877- flows := flowsByOrigin [splitID ]
879+ flows := findNormalFlows ( flowsByOrigin [splitID ])
878880 if len (flows ) < 2 {
879881 return ""
880882 }
881883
882- // Follow each branch and collect all reachable nodes
883- branch0Nodes := collectReachableNodes (ctx , flows [0 ].DestinationID , flowsByOrigin , activityMap , make (map [model.ID ]bool ))
884- branch1Nodes := collectReachableNodes (ctx , flows [1 ].DestinationID , flowsByOrigin , activityMap , make (map [model.ID ]bool ))
885-
886- // Find the first common node that is an ExclusiveMerge
887- // This is a simplification - we look for the first merge point reachable from both branches
888- for nodeID := range branch0Nodes {
889- if branch1Nodes [nodeID ] {
890- if _ , ok := activityMap [nodeID ].(* microflows.ExclusiveMerge ); ok {
891- return nodeID
892- }
893- }
884+ branchDistances := make ([]map [model.ID ]int , 0 , len (flows ))
885+ for _ , flow := range flows {
886+ branchDistances = append (branchDistances , collectReachableDistances (flow .DestinationID , flowsByOrigin ))
894887 }
895888
896- return ""
889+ return selectNearestCommonJoin ( activityMap , branchDistances )
897890}
898891
899- // collectReachableNodes collects all nodes reachable from a starting node.
900- func collectReachableNodes (
901- ctx * ExecContext ,
892+ // collectReachableDistances collects the shortest normal-flow distance from a
893+ // branch start to every reachable node. Error handler flows are excluded because
894+ // they do not participate in split/merge structural pairing.
895+ func collectReachableDistances (
902896 startID model.ID ,
903897 flowsByOrigin map [model.ID ][]* microflows.SequenceFlow ,
898+ ) map [model.ID ]int {
899+ distances := map [model.ID ]int {}
900+ type queueItem struct {
901+ id model.ID
902+ distance int
903+ }
904+ queue := []queueItem {{id : startID }}
905+
906+ for len (queue ) > 0 {
907+ item := queue [0 ]
908+ queue = queue [1 :]
909+
910+ if previous , ok := distances [item .id ]; ok && previous <= item .distance {
911+ continue
912+ }
913+ distances [item .id ] = item .distance
914+
915+ for _ , flow := range findNormalFlows (flowsByOrigin [item .id ]) {
916+ queue = append (queue , queueItem {
917+ id : flow .DestinationID ,
918+ distance : item .distance + 1 ,
919+ })
920+ }
921+ }
922+
923+ return distances
924+ }
925+
926+ func selectNearestCommonJoin (
904927 activityMap map [model.ID ]microflows.MicroflowObject ,
905- visited map [model.ID ]bool ,
906- ) map [model.ID ]bool {
907- result := make (map [model.ID ]bool )
928+ branchDistances []map [model.ID ]int ,
929+ ) model.ID {
930+ if len (branchDistances ) < 2 {
931+ return ""
932+ }
908933
909- var traverse func (id model.ID )
910- traverse = func (id model.ID ) {
911- if visited [id ] {
912- return
934+ type candidate struct {
935+ id model.ID
936+ maxDistance int
937+ sumDistance int
938+ }
939+ candidates := []candidate {}
940+
941+ for nodeID , firstDistance := range branchDistances [0 ] {
942+ if ! isSplitJoinCandidate (activityMap [nodeID ]) {
943+ continue
913944 }
914- visited [id ] = true
915- result [id ] = true
916945
917- for _ , flow := range flowsByOrigin [id ] {
918- traverse (flow .DestinationID )
946+ maxDistance := firstDistance
947+ sumDistance := firstDistance
948+ common := true
949+ for _ , distances := range branchDistances [1 :] {
950+ distance , ok := distances [nodeID ]
951+ if ! ok {
952+ common = false
953+ break
954+ }
955+ if distance > maxDistance {
956+ maxDistance = distance
957+ }
958+ sumDistance += distance
959+ }
960+ if common {
961+ candidates = append (candidates , candidate {
962+ id : nodeID ,
963+ maxDistance : maxDistance ,
964+ sumDistance : sumDistance ,
965+ })
919966 }
920967 }
921968
922- traverse (startID )
923- return result
969+ if len (candidates ) == 0 {
970+ return ""
971+ }
972+
973+ sort .Slice (candidates , func (i , j int ) bool {
974+ if candidates [i ].maxDistance != candidates [j ].maxDistance {
975+ return candidates [i ].maxDistance < candidates [j ].maxDistance
976+ }
977+ if candidates [i ].sumDistance != candidates [j ].sumDistance {
978+ return candidates [i ].sumDistance < candidates [j ].sumDistance
979+ }
980+ return string (candidates [i ].id ) < string (candidates [j ].id )
981+ })
982+
983+ return candidates [0 ].id
984+ }
985+
986+ func isSplitJoinCandidate (obj microflows.MicroflowObject ) bool {
987+ switch obj .(type ) {
988+ case nil , * microflows.StartEvent , * microflows.EndEvent :
989+ return false
990+ default :
991+ return true
992+ }
924993}
925994
926995// --- Executor method wrappers for callers in unmigrated code ---
0 commit comments