using w4c_workflows.Models.Nodes; namespace w4c_workflows.Services.Nodes; /// /// Loop support for the node graph. An edge that closes a cycle is a /// loop-back edge: it must target a node whose blueprint declares /// (currently core.splitInBatches), /// and at run time it re-triggers that node for the next iteration instead of /// counting toward its readiness. /// /// The classification is derived from the edge set (a DFS back edge), so it is /// recomputed identically by the compiler and when a persisted graph is rebuilt. /// public static class NodeGraphLinks { /// /// Returns the edges with set, or an /// error when a cycle passes through a node that is not loop-capable. /// public static (List Edges, string? Error) MarkLoopBackEdges( IReadOnlyList nodes, IReadOnlyList edges) { var byId = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal); var adjacency = new Dictionary>(StringComparer.Ordinal); for (var i = 0; i < edges.Count; i++) { if (!adjacency.TryGetValue(edges[i].FromNodeId, out var outgoing)) adjacency[edges[i].FromNodeId] = outgoing = new List(); outgoing.Add(i); } var color = new Dictionary(StringComparer.Ordinal); // 0 = white, 1 = on stack, 2 = done var backEdges = new HashSet(); void Visit(string nodeId) { color[nodeId] = 1; if (adjacency.TryGetValue(nodeId, out var outgoing)) { foreach (var index in outgoing) { var target = edges[index].ToNodeId; var targetColor = color.GetValueOrDefault(target, 0); if (targetColor == 1) backEdges.Add(index); else if (targetColor == 0) Visit(target); } } color[nodeId] = 2; } foreach (var node in nodes) { if (color.GetValueOrDefault(node.Id, 0) == 0) Visit(node.Id); } var marked = new List(edges.Count); string? error = null; for (var i = 0; i < edges.Count; i++) { var edge = edges[i]; var isLoopBack = backEdges.Contains(i); if (isLoopBack && !byId[edge.ToNodeId].Blueprint.LoopBack) { error ??= $"a cycle through '{edge.FromNodeId}' → '{edge.ToNodeId}' is not allowed: " + $"'{byId[edge.ToNodeId].Blueprint.Type}' is not a loop node"; } marked.Add(edge with { IsLoopBack = isLoopBack }); } return (marked, error); } }