@@ -29,6 +29,11 @@ var ErrNoStartNode = errors.New("no start node found")
2929// ErrNodePointsToStart is returned when a node points to the start node.
3030var ErrNodePointsToStart = errors .New ("node points to start node" )
3131
32+ // ErrDuplicateEdge is returned when an edge set contains two identical edges.
33+ // Two edges with the same (From, To) are rejected regardless of Route; use
34+ // MultiRoute to express alternatives to the same target.
35+ var ErrDuplicateEdge = errors .New ("duplicate edge" )
36+
3237// ErrMultipleDefaultRoutes is returned when a node has more than one default route.
3338var ErrMultipleDefaultRoutes = errors .New ("node has more than one default route" )
3439
@@ -97,7 +102,10 @@ func validateStartNodeNoIncoming(edges []Edge) error {
97102}
98103
99104// validateWorkflow executes a set of workflow validation checks.
100- func validateWorkflow (workflow * Workflow ) error {
105+ func validateWorkflow (workflow * graph ) error {
106+ if err := validateUniqueEdges (workflow ); err != nil {
107+ return err
108+ }
101109 if err := validateDefaultRoute (workflow ); err != nil {
102110 return err
103111 }
@@ -107,9 +115,25 @@ func validateWorkflow(workflow *Workflow) error {
107115 return nil
108116}
109117
118+ // validateUniqueEdges checks that there are no duplicate edges in the workflow.
119+ // Two edges with the same (From, To) are rejected regardless of Route; use
120+ // MultiRoute to express alternatives to the same target.
121+ func validateUniqueEdges (workflow * graph ) error {
122+ for node , edges := range workflow .successors {
123+ uniqueEdges := make (map [Node ]struct {})
124+ for _ , edge := range edges {
125+ if _ , ok := uniqueEdges [edge .To ]; ok {
126+ return fmt .Errorf ("%w: from %q to %q" , ErrDuplicateEdge , node .Name (), edge .To .Name ())
127+ }
128+ uniqueEdges [edge .To ] = struct {}{}
129+ }
130+ }
131+ return nil
132+ }
133+
110134// validateDefaultRoute checks that there are no multiple default routes for one node.
111- func validateDefaultRoute (workflow * Workflow ) error {
112- for node , edges := range workflow .graph . successors {
135+ func validateDefaultRoute (workflow * graph ) error {
136+ for node , edges := range workflow .successors {
113137 hasDefault := false
114138 for _ , edge := range edges {
115139 if edge .Route == Default && ! hasDefault {
@@ -127,7 +151,7 @@ func validateDefaultRoute(workflow *Workflow) error {
127151// for cycles where all edges in the cycle have nil routes.
128152// Default routes (where Route == Default) are treated as conditional edges
129153// and are ignored during unconditional cycle detection.
130- func validateCycles (workflow * Workflow ) error {
154+ func validateCycles (workflow * graph ) error {
131155 visited := make (map [Node ]struct {})
132156
133157 var traverse func (n Node , inStack map [Node ]struct {}) error
@@ -143,7 +167,7 @@ func validateCycles(workflow *Workflow) error {
143167 inStack [n ] = struct {}{}
144168 visited [n ] = struct {}{}
145169
146- for _ , edge := range workflow .graph . successors [n ] {
170+ for _ , edge := range workflow .successors [n ] {
147171 if edge .Route == nil {
148172 if err := traverse (edge .To , inStack ); err != nil {
149173 return err
@@ -155,7 +179,7 @@ func validateCycles(workflow *Workflow) error {
155179 return nil
156180 }
157181
158- for node := range workflow .graph . successors {
182+ for node := range workflow .successors {
159183 if _ , ok := visited [node ]; ! ok {
160184 inStack := make (map [Node ]struct {})
161185 if err := traverse (node , inStack ); err != nil {
0 commit comments