using w4c_workflows.Models; using w4c_workflows.Models.Nodes; namespace w4c_workflows.Services.Nodes; /// /// Lowers a legacy script definition (language + entry tasks driven /// by the linear next/onError chain) into an equivalent node-mode /// definition where every step is a synthetic node. /// The result is an ordinary and can be compiled /// by . /// /// Script execution is unchanged: the core.code executor delegates to the /// existing runtime, so the subprocess /// path is preserved while the edge kernel owns scheduling. /// /// This is the S1 migration bridge. It is wired into /// behind Workflows:LowerLegacyScripts (default off, function mode only): /// durable/handler scripts keep the linear engine because the node kernel does /// not checkpoint/resume, and already-persisted script workflows keep running /// unchanged until an operator opts in. /// /// NOTE: the node kernel passes per-node data as FlowItem envelopes /// ({ "json": … }), while the linear engine passes a task's raw JSON output /// to its successor. Lowering therefore changes the exact input JSON a script /// receives. The switch must not be enabled for a workflow whose scripts depend on /// the raw shape until that contract is bridged. /// public static class LegacyWorkflowLowerer { public const string CodeNodeType = "core.code"; /// Id of the synthetic node representing the header's entry script. public const string EntryStepId = "__entry"; /// /// Builds the node-mode definition. The header entry becomes a synthetic node /// whose next is the workflow's chain head; each task keeps its id, /// next and onError, which the node compiler lowers into ports. /// public static WorkflowDefinition ToNodeDefinition(WorkflowDefinition def) { var tasks = def.Tasks ?? new List(); var entryId = UniqueId(EntryStepId, tasks); var headId = WorkflowGraph.Compute(def).HeadId; var lowered = new WorkflowDefinition { Name = def.Name, Version = def.Version, Mode = def.Mode, Target = def.Target, Server = def.Server, Trigger = def.Trigger, Env = def.Env, Entry = null, Language = null, Tasks = new List(tasks.Count + 1), }; if (def.Entry != null) { lowered.Tasks.Add(new TaskDefinition { Id = entryId, Parent = "root", Next = headId, Node = new NodeTaskDefinition { Type = CodeNodeType }, Parameters = CodeParameters(def.Language, def.Entry), }); } foreach (var task in tasks) { lowered.Tasks.Add(new TaskDefinition { Id = task.Id, Parent = "root", Next = task.Next, OnError = task.OnError, Node = new NodeTaskDefinition { Type = CodeNodeType }, Parameters = CodeParameters(task.Language ?? def.Language, task.Entry), }); } return lowered; } private static Dictionary CodeParameters(string? language, EntryDefinition? entry) { var parameters = new Dictionary { [ParameterNames.Language] = language, [ParameterNames.EntryFile] = entry?.File, }; if (!string.IsNullOrWhiteSpace(entry?.Function)) parameters[ParameterNames.EntryFunction] = entry.Function; return parameters; } private static string UniqueId(string preferred, IReadOnlyList tasks) { var taken = tasks.Select(t => t.Id).Where(id => id != null).ToHashSet(StringComparer.Ordinal); if (!taken.Contains(preferred)) return preferred; for (var i = 1; ; i++) { var candidate = $"{preferred}{i}"; if (!taken.Contains(candidate)) return candidate; } } /// Parameter names shared with the core.code blueprint. private static class ParameterNames { public const string Language = "language"; public const string EntryFile = "entryFile"; public const string EntryFunction = "entryFunction"; } }