import type {CompiledWorkflow} from '../../../types/bot/workflow/runtime.ts'; import {validate_workflow} from './validate.ts'; import {assert_json} from './value.ts'; import {resolve_fields} from './template.ts'; import {node_registry, start_registry} from '../registry.ts'; import {node_fields, policy_fields} from '../common_template.ts'; import type {Json} from '../../../types/bot/workflow/workflow.ts'; export function compile_workflow(value: unknown): CompiledWorkflow { assert_json(value); const normalized = structuredClone(value) as Record; if (normalized && !Array.isArray(normalized) && typeof normalized === 'object') { const start = normalized.start; if (start && typeof start.type === 'string' && Object.hasOwn(start_registry, start.type)) { const module = start_registry[start.type as keyof typeof start_registry]; normalized.start = resolve_fields(module.config_template.config, start); } if (normalized.policy !== null && (normalized.policy === undefined || typeof normalized.policy === 'object' && !Array.isArray(normalized.policy))) { normalized.policy = resolve_fields(policy_fields, normalized.policy ?? {}); } if (Array.isArray(normalized.nodes)) normalized.nodes = normalized.nodes.map((node: Record) => { if (!node || typeof node !== 'object' || Array.isArray(node) || typeof node.type !== 'string' || !Object.hasOwn(node_registry, node.type)) return node; const fields = node_registry[node.type as keyof typeof node_registry].config_template.config; const result = resolve_fields(node_fields, node as Record); if (node.config && typeof node.config === 'object' && !Array.isArray(node.config)) result.config = resolve_fields(fields, node.config); return result; }); } validate_workflow(normalized); const definition = normalized; const graph: CompiledWorkflow = { definition, nodes: new Map(definition.nodes.map(node => [node.id, node])), outgoing: new Map(), incoming: new Map(), join_owners: new Map(), }; for (const edge of definition.edges) { graph.outgoing.set(edge.source, [...graph.outgoing.get(edge.source) ?? [], edge]); graph.incoming.set(edge.target, [...graph.incoming.get(edge.target) ?? [], edge]); } graph.join_owners = find_join_owners(graph); return graph; } /** The nearest dominating fork owns each join, including joins downstream of other joins. */ export function find_join_owners(graph: CompiledWorkflow): Map { const ids = [...graph.nodes.keys()]; const dominators = new Map(ids.map(id => [id, new Set(id === graph.definition.entry ? [id] : ids)])); let changed = true; while (changed) { changed = false; for (const id of ids) { if (id === graph.definition.entry) continue; const predecessors = (graph.incoming.get(id) ?? []).map(edge => edge.source); const common = new Set(dominators.get(predecessors[0]) ?? []); for (const predecessor of predecessors.slice(1)) { for (const candidate of common) if (!dominators.get(predecessor)!.has(candidate)) common.delete(candidate); } common.add(id); const previous = dominators.get(id)!; if (common.size !== previous.size || [...common].some(item => !previous.has(item))) { dominators.set(id, common); changed = true; } } } const owners = new Map(); for (const node of graph.nodes.values()) { if (node.type !== 'merge' || node.config.mode !== 'all') continue; const candidates = [...dominators.get(node.id)!].filter(id => id !== node.id && (graph.outgoing.get(id)?.length ?? 0) > 1); candidates.sort((a, b) => dominators.get(b)!.size - dominators.get(a)!.size); if (candidates[0]) owners.set(node.id, candidates[0]); } return owners; }