| 1 | namespace AgentForge.Plugin.Ci.Ir; |
| 2 | |
| 3 | public sealed record ForgeCiPlanStep( |
| 4 | string NodeId, |
| 5 | string TaskId, |
| 6 | string DisplayName, |
| 7 | IReadOnlyDictionary<string, string> Inputs); |
| 8 | |
| 9 | public static class ForgeCiPlanBuilder |
| 10 | { |
| 11 | public static IReadOnlyList<ForgeCiPlanStep> Build(ForgeCiDocument document) |
| 12 | { |
| 13 | var tasks = document.Nodes.Where(n => string.Equals(n.Kind, "task", StringComparison.Ordinal)).ToList(); |
| 14 | if (tasks.Count == 0) |
| 15 | throw new ForgeCiValidationException("Pipeline has no executable task nodes."); |
| 16 | |
| 17 | var taskIds = tasks.Select(t => t.Id).ToHashSet(StringComparer.Ordinal); |
| 18 | var incoming = taskIds.ToDictionary(id => id, _ => 0, StringComparer.Ordinal); |
| 19 | var adjacency = taskIds.ToDictionary(id => id, _ => new List<string>(), StringComparer.Ordinal); |
| 20 | |
| 21 | foreach (var edge in document.Edges) |
| 22 | { |
| 23 | if (!string.Equals(edge.Kind, "dependsOn", StringComparison.Ordinal)) |
| 24 | continue; |
| 25 | |
| 26 | if (!taskIds.Contains(edge.From) || !taskIds.Contains(edge.To)) |
| 27 | continue; |
| 28 | |
| 29 | adjacency[edge.From].Add(edge.To); |
| 30 | incoming[edge.To]++; |
| 31 | } |
| 32 | |
| 33 | var queue = new Queue<string>(incoming.Where(kv => kv.Value == 0).Select(kv => kv.Key)); |
| 34 | var orderedIds = new List<string>(tasks.Count); |
| 35 | |
| 36 | while (queue.Count > 0) |
| 37 | { |
| 38 | var id = queue.Dequeue(); |
| 39 | orderedIds.Add(id); |
| 40 | |
| 41 | foreach (var next in adjacency[id]) |
| 42 | { |
| 43 | incoming[next]--; |
| 44 | if (incoming[next] == 0) |
| 45 | queue.Enqueue(next); |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | if (orderedIds.Count != tasks.Count) |
| 50 | { |
| 51 | foreach (var task in tasks) |
| 52 | { |
| 53 | if (!orderedIds.Contains(task.Id)) |
| 54 | orderedIds.Add(task.Id); |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | return orderedIds |
| 59 | .Select(id => tasks.First(t => t.Id == id)) |
| 60 | .Select(node => new ForgeCiPlanStep( |
| 61 | node.Id, |
| 62 | node.Task ?? throw new ForgeCiValidationException($"Task node '{node.Id}' has no task id."), |
| 63 | string.IsNullOrWhiteSpace(node.Name) ? node.Task! : node.Name!, |
| 64 | node.Inputs ?? new Dictionary<string, string>(StringComparer.Ordinal))) |
| 65 | .ToList(); |
| 66 | } |
| 67 | } |
| 68 | |