w4c-workflows-api/Services/Nodes/LegacyWorkflowLowerer.cs
2026-09-12 01:02:46 +03:00

113 lines
3.9 KiB
C#

using w4c_workflows.Models;
using w4c_workflows.Models.Nodes;
namespace w4c_workflows.Services.Nodes;
/// <summary>
/// Lowers a legacy script definition (<c>language</c> + <c>entry</c> tasks driven
/// by the linear <c>next</c>/<c>onError</c> chain) into an equivalent node-mode
/// definition where every step is a synthetic <see cref="CodeNodeType"/> node.
/// The result is an ordinary <see cref="WorkflowDefinition"/> and can be compiled
/// by <see cref="NodeGraphCompiler"/>.
///
/// Script execution is unchanged: the <c>core.code</c> executor delegates to the
/// existing <see cref="Execution.IScriptExecutor"/> runtime, so the subprocess
/// path is preserved while the edge kernel owns scheduling.
/// </summary>
public static class LegacyWorkflowLowerer
{
public const string CodeNodeType = "core.code";
/// <summary>Id of the synthetic node representing the header's entry script.</summary>
public const string EntryStepId = "__entry";
/// <summary>
/// Builds the node-mode definition. The header entry becomes a synthetic node
/// whose <c>next</c> is the workflow's chain head; each task keeps its id,
/// <c>next</c> and <c>onError</c>, which the node compiler lowers into ports.
/// </summary>
public static WorkflowDefinition ToNodeDefinition(WorkflowDefinition def)
{
var tasks = def.Tasks ?? new List<TaskDefinition>();
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<TaskDefinition>(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<string, object?> CodeParameters(string? language, EntryDefinition? entry)
{
var parameters = new Dictionary<string, object?>
{
[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<TaskDefinition> 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;
}
}
/// <summary>Parameter names shared with the <c>core.code</c> blueprint.</summary>
private static class ParameterNames
{
public const string Language = "language";
public const string EntryFile = "entryFile";
public const string EntryFunction = "entryFunction";
}
}