// business-flow: the minimal form of a flow — a chart. Steps are [from, when, // to] tuples (the condition sits between source and target; a two-element // [from, to] step has no label); nodes are collected by name from the edges // (a name shared by several steps is one node); the first step's from is the // start; every node with no outgoing edge falls through to the flow's return // end. A node with several outgoing edges is a decision — the mermaid driver // draws it as a diamond, and every branch edge must carry a when label or the // chart cannot be read (nor later upgraded to machine conditions). // // The chart compiles to the same graph IR as flow-script: mermaid rendering, // reachability checks, and future consumers (an executor) all work unchanged. // A chart is the skeleton — upgrade a decision to IF(cond) and a node to // invoke(...) when the business logic arrives. import { defineFlow, edge, node } from './flow.js'; import type { FlowEdge, FlowNode, FlowSchema, FlowStep } from './flow.js'; /** One chart step: from → to, the optional branch label between them * (mandatory for decision branches). */ export type ChartStep = [from: string, to: string] | [from: string, when: string, to: string]; /** Compile a tuple chart into a FlowSchema. */ export function flowChart( name: string, steps: ChartStep[], options: { description?: string; start?: string } = {}, ): FlowSchema { if (steps.length === 0) { throw new Error(`business-flow ${name}: at least one step is required`); } for (const step of steps) { const from = step[0]; const to = step.length === 3 ? step[2] : step[1]; if (from === '' || to === '') { throw new Error(`business-flow ${name}: node names must not be empty`); } } if (options.start !== undefined && !steps.some((step) => step[0] === options.start)) { throw new Error(`business-flow ${name}: start "${options.start}" is not the source of any step`); } const nodes = new Map(); const byName = (n: string): FlowNode => { let found = nodes.get(n); if (found === undefined) { found = node(n, {}); nodes.set(n, found); } return found; }; const edges: FlowEdge[] = steps.map((step) => { const from = step[0]; const to = step.length === 3 ? step[2] : step[1]; const when = step.length === 3 ? step[1] : undefined; return edge(byName(from), byName(to), { when }); }); // Decision rule: every branch of a multi-outgoing node must carry a label. const outgoing = new Map(); for (const e of edges) { const list = outgoing.get(e.start); if (list === undefined) outgoing.set(e.start, [e]); else list.push(e); } for (const [n, list] of outgoing) { if (list.length > 1 && list.some((e) => e.when === undefined)) { throw new Error(`business-flow ${name}: decision node "${n.name}" — every outgoing branch edge needs a when label`); } } return defineFlow(name, { start: byName(options.start ?? steps[0][0]), description: options.description, edges: (flow) => { // Every path ends somewhere: nodes with no outgoing edge return. const sinks: FlowEdge[] = []; for (const n of nodes.values()) { if (!outgoing.has(n)) sinks.push(edge(n, flow.returnEnd)); } return [...edges, ...sinks]; }, }); }