using w4c_workflows.Models; namespace w4c_workflows.Services; /// /// Precomputes the execution graph from a parsed YAML definition so validation /// and compilation share one deterministic interpretation: /// - is the entry of the success chain (the top-level /// task with no incoming `next` that is not purely a compensation target). /// - is the linear happy path root → head → … → terminal. /// - marks tasks that are only ever reached as /// an `onError` target (they have no `next` of their own). /// internal sealed class WorkflowGraph { public required IReadOnlyList Tasks { get; init; } public required IReadOnlyDictionary ById { get; init; } public required IReadOnlyDictionary IncomingNext { get; init; } public required IReadOnlySet OnErrorTargets { get; init; } public required IReadOnlySet CompensationOnly { get; init; } public string? HeadId { get; init; } public required IReadOnlyList Chain { get; init; } public static WorkflowGraph Compute(WorkflowDefinition def) { var tasks = def.Tasks ?? new List(); var byId = new Dictionary(StringComparer.Ordinal); foreach (var task in tasks) if (!string.IsNullOrEmpty(task.Id)) byId[task.Id] = task; var incomingNext = new Dictionary(StringComparer.Ordinal); var onErrorTargets = new HashSet(StringComparer.Ordinal); foreach (var task in tasks) { if (!string.IsNullOrEmpty(task.Next) && byId.ContainsKey(task.Next)) incomingNext[task.Next] = incomingNext.GetValueOrDefault(task.Next) + 1; if (!string.IsNullOrEmpty(task.OnError) && byId.ContainsKey(task.OnError)) onErrorTargets.Add(task.OnError); } // A compensation-only task is an onError target that does not continue // the success chain (no `next`). It lives outside the happy path. var compensationOnly = new HashSet(StringComparer.Ordinal); foreach (var task in tasks) { if (task.Id == null) continue; if (onErrorTargets.Contains(task.Id) && string.IsNullOrEmpty(task.Next)) compensationOnly.Add(task.Id); } // Head: top-level (parent == root), no incoming `next`, not compensation-only. string? headId = null; foreach (var task in tasks) { if (task.Id == null) continue; var parent = string.IsNullOrEmpty(task.Parent) ? "root" : task.Parent; if (!string.Equals(parent, "root", StringComparison.Ordinal)) continue; if (incomingNext.GetValueOrDefault(task.Id) > 0) continue; if (compensationOnly.Contains(task.Id)) continue; headId = task.Id; break; } // Walk the success chain (each node has at most one `next` after validation). var chain = new List(); var visited = new HashSet(StringComparer.Ordinal); var cursor = headId; while (cursor != null && visited.Add(cursor) && byId.TryGetValue(cursor, out var task)) { chain.Add(cursor); cursor = string.IsNullOrEmpty(task.Next) ? null : task.Next; } return new WorkflowGraph { Tasks = tasks, ById = byId, IncomingNext = incomingNext, OnErrorTargets = onErrorTargets, CompensationOnly = compensationOnly, HeadId = headId, Chain = chain, }; } }