// Flow-script: a sequential source layer for flows. Statements read top to // bottom like Java — IF/TRY are control steps, invoke/write/THROW/RETURN are // actions — and the script compiles down to the graph IR (FlowSchema). The // graph's ceremony (node names, edges, exception ends, sub-flow registries) // is machine-generated: method throws are routed to auto-synthesized ends, // decisions become guard/ifNode diamonds, and TRY/sub bodies become // sub-flows carrying only the slots they actually use. // // The IR stays the single executable model (mermaid, must-analysis, throws // coverage, service contracts all consume it); this layer is a lowering, not // a parallel model. import type { DtoMessage } from './dto.js'; import type { DomainEventSchema } from './domain-event.js'; import type { EnumValue } from './dsl.js'; import type { ExceptionSchema } from './exception.js'; import { defineFlow, defineSlots, edge, guard, ifNode, isCall, isConditionGroup, isEnd, isFlowNode, isFlowSlot, isGuard, isIfNode, methodOf, node, tryNode, } from './flow.js'; import type { FlowEdge, FlowEnd, FlowMethodRef, FlowNodeMethodRef, FlowNodeOrEnd, FlowSchema, FlowSlot, FlowSlots, FlowStep, GuardCondition, } from './flow.js'; // --------------------------------------------------------------------------- // Source language: statements /** A method call in statement position: invoke(m) mutates the input slot in * place, invoke(m, args) passes one explicit arg slot, invoke(m, args, result) * additionally assigns the method's results to a slot. args may be a slot * list (multi-input methods — e.g. a predicate over two slots). In condition * position (IF(...)), the same call is a utils predicate (result is rejected). */ export interface InvokeStep { kind: 'invoke'; method: FlowMethodRef; args?: FlowSlot | FlowSlot[]; result?: FlowSlot; } /** A construction: the slot is assigned without a method call. */ export interface WriteStep { kind: 'write'; slot: FlowSlot; } /** An unconditional throw (terminal — nothing runs after it in its block). */ export interface ThrowStep { kind: 'throw'; exception: ExceptionSchema; message?: string; } /** An early return to the flow's return end (terminal in its block). */ export interface ReturnStep { kind: 'return'; } /** A domain event publication: writes the event to the outbox inside the * surrounding transaction. The payload slot defaults to the flow input. */ export interface PublishStep { kind: 'publish'; event: DomainEventSchema; payload?: FlowSlot; } /** A conditional: the then-branch runs when the condition holds, the * else-branch (or the next statement) otherwise. */ export interface IfStep { kind: 'if'; cond: GuardCondition; then: ScriptStep[]; else?: ScriptStep[]; } export type CatchRoute = [exception: ExceptionSchema, steps: ScriptStep[]]; /** A protected region: body and catch handlers (and optional finally) compile * to sub-flows; a catch's steps may be empty (swallow). */ export interface TryStep { kind: 'try'; name?: string; body: ScriptStep[]; catches: CatchRoute[]; finally?: ScriptStep[]; } /** A named sub-flow region (a private method inlined as a sub-flow). */ export interface SubStep { kind: 'sub'; name: string; steps: ScriptStep[]; description?: string; } export type ScriptStep = InvokeStep | WriteStep | ThrowStep | ReturnStep | PublishStep | IfStep | TryStep | SubStep; /** Call a method as a statement or (in IF position) as a utils predicate. */ export function invoke(method: FlowMethodRef, args?: FlowSlot | FlowSlot[], result?: FlowSlot): InvokeStep { return { kind: 'invoke', method, args, result }; } /** Assign a slot without a method call (a construction). */ export function write(slot: FlowSlot): WriteStep { return { kind: 'write', slot }; } /** Throw an exception — inside an IF branch the condition and the exit merge * into one guard check; bare in a block it is an unconditional exit. */ export function THROW(exception: ExceptionSchema, message?: string): ThrowStep { return { kind: 'throw', exception, message }; } /** Return early to the flow's return end. */ export function RETURN(): ReturnStep { return { kind: 'return' }; } /** Publish a domain event — the outbox write joins the surrounding * transaction. The payload slot must carry exactly the event's fields * (compile-time checked against the slot's declared message). */ export function publish(event: DomainEventSchema, payload?: FlowSlot): PublishStep { if (payload !== undefined) { const type = payload.type as { fields?: Record } | undefined; if (type?.fields !== undefined) { const expected = Object.keys(event.fields); const got = Object.keys(type.fields); const missing = expected.filter((k) => !got.includes(k)); const extra = got.filter((k) => !expected.includes(k)); if (missing.length > 0 || extra.length > 0) { throw new Error( `flow-script publish('${event.name}', ${payload.name}): payload fields mismatch — ` + `${missing.length > 0 ? `missing ${missing.join(', ')}` : ''}` + `${missing.length > 0 && extra.length > 0 ? '; ' : ''}` + `${extra.length > 0 ? `unexpected ${extra.join(', ')}` : ''}`, ); } } } return { kind: 'publish', event, payload }; } export interface IfBuilder { THEN(...steps: ScriptStep[]): IfBuilt; } export interface IfBuilt extends IfStep { ELSE(...steps: ScriptStep[]): IfStep; } /** Conditional step: IF(cond).THEN(...) with optional .ELSE(...). The * condition is a comparison (lt/gt/eq/...) or a predicate call — an * invoke(...) in this position is a utils predicate. */ export function IF(cond: GuardCondition | InvokeStep): IfBuilder { const c = toCondition(cond); return { THEN(...steps: ScriptStep[]): IfBuilt { if (steps.length === 0) { throw new Error('flow-script IF: THEN requires at least one step'); } return { kind: 'if', cond: c, then: steps, ELSE(...elseSteps: ScriptStep[]): IfStep { if (elseSteps.length === 0) { throw new Error('flow-script IF: ELSE requires at least one step'); } return { kind: 'if', cond: c, then: steps, else: elseSteps }; }, }; }, }; } export interface TryBuilt extends TryStep { CATCH(...routes: CatchRoute[]): TryBuilt; FINALLY(steps: ScriptStep[]): TryBuilt; } /** Protected region: TRY([...]).CATCH([Exception, [...]], ...).FINALLY([...]). */ export function TRY(body: ScriptStep[], name?: string): TryBuilt { return { kind: 'try', name, body, catches: [], CATCH(...routes: CatchRoute[]): TryBuilt { return { ...this, catches: [...this.catches, ...routes] }; }, FINALLY(steps: ScriptStep[]): TryBuilt { return { ...this, finally: steps }; }, }; } /** A named sub-flow region. */ export function sub(name: string, steps: ScriptStep[], description?: string): SubStep { return { kind: 'sub', name, steps, description }; } /** The script's slot registry: ctx.slots.args and named slots from options.slots. */ export interface FlowCtx { /** Append statements — they run in order. */ next(...steps: ScriptStep[]): void; slots: FlowSlots; } /** Compile a sequential script into a FlowSchema. options.slots declares the * named slots (message bindings); every declared slot must be used somewhere * in the compiled flow. */ export function flowScript( name: string, options: { args: DtoMessage; slots?: Record; description?: string; }, build: (ctx: FlowCtx) => void, ): FlowSchema { if (options.slots !== undefined && 'args' in options.slots) { throw new Error(`flow-script ${name}: "args" is the built-in input slot — declare the input message via the args option`); } const slots = defineSlots({ args: options.args, ...(options.slots ?? {}) }); const steps: ScriptStep[] = []; build({ next: (...s: ScriptStep[]): void => void steps.push(...s), slots }); const root: FlowCompile = { name, argsMessage: options.args, slotMessages: options.slots ?? {}, usedNames: new Set(), ends: new Map(), seen: new Set(), usedSlots: new Set(), edges: [], tryTotal: countTrys(steps), tryCount: 0, entrySlots: [], }; const flow = compileFlowBody(name, options.description, steps, root, root.entrySlots); for (const key of Object.keys(options.slots ?? {})) { if (!root.usedNames.has(key)) { throw new Error(`flow-script ${name}: slot "${key}" is declared but never used`); } } return flow; } // --------------------------------------------------------------------------- // Lowering to the graph IR /** Per-flow compile state; sub-flows share the message bindings and the used * name set, but own their ends, nodes, edges, and used slots. */ interface FlowCompile { name: string; argsMessage: DtoMessage; slotMessages: Record; usedNames: Set; ends: Map; seen: Set; usedSlots: Set; edges: FlowEdge[]; tryTotal: number; tryCount: number; /** Slots produced before this flow's entry by the enclosing flow. */ entrySlots: FlowSlot[]; } /** The hole the flow's own return end fills: the last statement of every flow * links here, and the edges callback swaps it for the real return end. */ const DANGLE: FlowEnd = { type: 'return', name: 'return' }; function addUsed(ctx: FlowCompile, slot: FlowSlot | undefined): void { if (slot === undefined) return; ctx.usedSlots.add(slot); ctx.usedNames.add(slot.name); } function addConditionUsed(ctx: FlowCompile, c: GuardCondition): void { if (isConditionGroup(c)) { for (const s of c.conds) addConditionUsed(ctx, s); return; } if (!isCall(c)) { addUsed(ctx, isFlowSlot(c.field) ? c.field : c.field.slot); return; } for (const t of c.args ?? []) addUsed(ctx, t); addUsed(ctx, c.result); } /** An invoke in condition position becomes a utils predicate call. */ function isInvokeStep(c: GuardCondition | InvokeStep): c is InvokeStep { return (c as InvokeStep).kind === 'invoke'; } /** Lower an IF condition: a top-level invoke is a predicate call, and * composites convert invoke steps nested inside them recursively. */ function toCondition(c: GuardCondition | InvokeStep): GuardCondition { if (isInvokeStep(c)) { if (c.result !== undefined) { throw new Error('flow-script IF: a predicate call cannot assign a result slot'); } return { method: c.method, args: c.args === undefined ? [] : Array.isArray(c.args) ? c.args : [c.args] }; } if (isConditionGroup(c)) { return { kind: c.kind, conds: c.conds.map((sub) => toCondition(sub)) }; } return c; } /** Display form mirrors the mermaid driver: owner.name / schema.name. */ function displayMethodName(m: FlowMethodRef): string { if ('owner' in m) return `${m.owner}.${m.name}`; return `${m.schema.name}.${m.name}`; } /** The flow's exception end for `ex` — one per exception name per flow. */ function exceptionEnd(ctx: FlowCompile, ex: ExceptionSchema): FlowEnd { let end = ctx.ends.get(ex.name); if (end === undefined) { end = { type: 'exception', name: `throw ${ex.name}`, exception: ex, description: 'method throws' }; ctx.ends.set(ex.name, end); } return end; } function methodThrows(m: FlowMethodRef): ExceptionSchema[] { return 'throws' in m && m.throws !== undefined ? m.throws : []; } /** Readable condition text used as node/branch labels (and as the throw * label when THROW carries no message). Composites render parenthesized * sub-conditions: !(a), (a && b), (a || b). */ function renderCondition(c: GuardCondition): string { if (isConditionGroup(c)) { const inner = c.conds.map(renderCondition).join(c.kind === 'and' ? ' && ' : c.kind === 'or' ? ' || ' : ''); return c.kind === 'not' ? `!(${inner})` : `(${inner})`; } if (!isCall(c)) { if (isFlowSlot(c.field)) { const nullOp = c.op === 'isNull' || c.op === 'isNotNull'; if (nullOp) return c.op === 'isNull' ? `${c.field.name} is null` : `${c.field.name} is not null`; // scalar slot comparison: total > 100 const ref = c.field.name; switch (c.op) { case 'lt': return `${ref} < ${renderValue(c.value)}`; case 'le': return `${ref} <= ${renderValue(c.value)}`; case 'gt': return `${ref} > ${renderValue(c.value)}`; case 'ge': return `${ref} >= ${renderValue(c.value)}`; case 'eq': return `${ref} = ${renderValue(c.value)}`; case 'ne': return `${ref} ≠ ${renderValue(c.value)}`; default: throw new Error(`unsupported comparison op '${c.op}'`); } } const field = c.field.field as { name: string }; const ref = `${c.field.slot.name}.${field.name}`; switch (c.op) { case 'lt': return `${ref} < ${c.value}`; case 'le': return `${ref} <= ${c.value}`; case 'gt': return `${ref} > ${c.value}`; case 'ge': return `${ref} >= ${c.value}`; case 'eq': return `${ref} = ${renderValue(c.value)}`; case 'ne': return `${ref} ≠ ${renderValue(c.value)}`; case 'isNull': return `${ref} is null`; case 'isNotNull': return `${ref} is not null`; } } const args = (c.args ?? []).map((a) => a.name).join(', '); return `${methodOf(c).name}(${args})`; } function renderValue(v: string | number | EnumValue | undefined): string { if (typeof v === 'object') return v.symbol; return JSON.stringify(v); } /** Compile statements back to front so every step knows its continuation * (the entry of what follows it). Edges are pushed into the flow's own edge * list — a continuation shared by several branches is not re-emitted. */ function compileStatements( steps: ScriptStep[], ctx: FlowCompile, cont: FlowNodeOrEnd, inherited: FlowSlot[] = [], ): FlowNodeOrEnd { for (let i = 0; i < steps.length - 1; i++) { const s = steps[i]; if (s.kind === 'throw' || s.kind === 'return') { throw new Error( `flow-script ${ctx.name}: ${s.kind === 'throw' ? 'THROW' : 'RETURN'} ends its block — steps after it are unreachable`, ); } } let entry = cont; for (let i = steps.length - 1; i >= 0; i--) { entry = compileStep(steps[i], ctx, entry, unionSlots(inherited, producedBefore(steps, i))); } return entry; } /** Slots produced by top-level invoke/write steps before `index` (script * order). Branch-internal productions are excluded — a slot produced only * inside one branch is not guaranteed on every path past it. */ function producedBefore(steps: ScriptStep[], index: number): FlowSlot[] { const out = new Set(); for (let i = 0; i < index; i++) { const s = steps[i]; if (s.kind === 'invoke' && s.result !== undefined) out.add(s.result); else if (s.kind === 'write') out.add(s.slot); } return [...out]; } function unionSlots(a: FlowSlot[], b: FlowSlot[]): FlowSlot[] { return [...new Set([...a, ...b])]; } function compileStep(step: ScriptStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd { switch (step.kind) { case 'invoke': return compileInvoke(step, ctx, cont); case 'write': return compileWrite(step, ctx, cont); case 'throw': return compileThrow(step, ctx); case 'return': return compileReturn(ctx); case 'publish': return compilePublish(step, ctx, cont); case 'if': return compileIf(step, ctx, cont, inherit); case 'try': return compileTry(step, ctx, cont, inherit); case 'sub': return compileSub(step, ctx, cont, inherit); } } function compileInvoke(step: InvokeStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd { if (Array.isArray(step.args)) step.args.forEach((s) => addUsed(ctx, s)); else addUsed(ctx, step.args); addUsed(ctx, step.result); const call: FlowNodeMethodRef = { method: step.method, args: step.args === undefined ? undefined : Array.isArray(step.args) ? step.args : [step.args], result: step.result, }; const n = node(displayMethodName(step.method), { methods: [call] }); ctx.seen.add(n); for (const ex of methodThrows(step.method)) { const end = exceptionEnd(ctx, ex); ctx.seen.add(end); ctx.edges.push(edge(n, end, { throws: ex })); } ctx.edges.push(edge(n, cont)); return n; } function compileWrite(step: WriteStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd { addUsed(ctx, step.slot); const n = node(`写 ${step.slot.name}`, { writes: [step.slot] }); ctx.seen.add(n); ctx.edges.push(edge(n, cont)); return n; } function compileThrow(step: ThrowStep, ctx: FlowCompile): FlowNodeOrEnd { // Unconditional exit: a check-less guard always takes its route; no // outgoing edge — nothing in the block runs after it. const label = step.message ?? `throw ${step.exception.name}`; const g = guard(label, { checks: [{ when: label, exception: step.exception }] }); ctx.seen.add(g); return g; } function compileReturn(ctx: FlowCompile): FlowNodeOrEnd { const g = guard('返回', { checks: [{ when: '返回', return: true }] }); ctx.seen.add(g); return g; } function compilePublish(step: PublishStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd { addUsed(ctx, step.payload); const n = node(`发布 ${step.event.name}`, { publish: { event: step.event, payload: step.payload }, reads: step.payload ? [step.payload] : undefined, }); ctx.seen.add(n); ctx.edges.push(edge(n, cont)); return n; } function compileIf(step: IfStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd { addConditionUsed(ctx, step.cond); const label = renderCondition(step.cond); const single = step.then.length === 1 ? step.then[0] : undefined; // Single-exit branches merge the condition and the exit into one guard // check (the IR's guard shape); the else path is the guard's fall-through. if (single !== undefined && (single.kind === 'throw' || single.kind === 'return')) { const elseEntry = compileStatements(step.else ?? [], ctx, cont, inherit); const g = single.kind === 'throw' ? guard(label, { checks: [{ when: single.message ?? label, exception: single.exception, check: step.cond }], }) : guard(label, { checks: [{ when: '返回', return: true, check: step.cond }] }); ctx.seen.add(g); ctx.edges.push(edge(g, elseEntry)); return g; } const thenEntry = compileStatements(step.then, ctx, cont, inherit); let elseEntry = compileStatements(step.else ?? [], ctx, cont, inherit); // A trailing IF (its else is the flow exit) cannot target the return end // directly — ifNode targets are steps only — so the else runs through a // return guard whose implicit exit reaches the return end. if (elseEntry === DANGLE) { const r = guard('返回', { checks: [{ when: '返回', return: true }] }); ctx.seen.add(r); elseEntry = r; } const d = ifNode(label, { cases: [{ when: label, check: step.cond, to: thenEntry as FlowStep }], else: elseEntry, }); ctx.seen.add(d); return d; } /** TRY steps in this flow's own statement tree (branch chains share the * flow's compile context; try bodies and sub-flows number their own). */ function countTrys(steps: ScriptStep[]): number { let n = 0; for (const s of steps) { if (s.kind === 'try') n += 1; else if (s.kind === 'if') n += countTrys(s.then) + countTrys(s.else ?? []); } return n; } function compileTry(step: TryStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd { // The back-to-front walk meets the last try first; ordinal restores the // script order for stable flow names. const ordinal = ctx.tryTotal - ++ctx.tryCount + 1; const suffix = ordinal === 1 ? '' : `${ordinal}`; const body = compileFlowBody(`${ctx.name}.tryBody${suffix}`, undefined, step.body, ctx, inherit); // Catch handlers and finally may read what the body produced before the // failure point (Java semantics: try { row = dao.get() } catch { use(row) }). // The body's productions seed their entry availability alongside the // enclosing flow's inherited slots. const bodyProduced = flowProducedSlots(body); const catchInherit = [...inherit, ...bodyProduced]; const catches = step.catches.map(([ex, steps]) => ({ exception: ex, handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx, catchInherit), })); const t = tryNode(step.name ?? 'try', { body, catches, finally: step.finally ? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx, catchInherit) : undefined, }); ctx.seen.add(t); // Handler exception ends rethrow out of the region: route them as typed // throws edges to the enclosing flow's ends (Java semantics — a catch // handler may throw out of the try). for (const c of catches) { for (const end of c.handler.nodes) { if (!isEnd(end) || end.type !== 'exception') continue; const ex = end.exception; if (ex === undefined) { throw new Error(`flow-script ${ctx.name}: exception end "${end.name}" has no exception type`); } const target = exceptionEnd(ctx, ex); ctx.seen.add(target); ctx.edges.push(edge(t, target, { throws: ex })); } } ctx.edges.push(edge(t, cont)); return t; } function compileSub(step: SubStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd { const subFlow = compileFlowBody(`${ctx.name}.${step.name}`, step.description, step.steps, ctx, inherit); const n = node(step.name, { flow: subFlow }); ctx.seen.add(n); ctx.edges.push(edge(n, cont)); return n; } /** Slots a flow's own nodes produce (call results and writes) — the try * body's productions become visible to its catch handlers and finally. */ function flowProducedSlots(f: FlowSchema): FlowSlot[] { const out = new Set(); for (const n of f.nodes) { if (isEnd(n)) continue; if (isGuard(n) || isFlowNode(n)) { for (const m of n.methods ?? []) { if (isCall(m) && m.result !== undefined) out.add(m.result); } if (isFlowNode(n)) for (const w of n.writes ?? []) out.add(w); } } return [...out]; } /** Compile one flow (top-level or sub-flow): its own slots registry (args * plus the named slots actually used inside), its own exception ends, and * its own return end (linked through DANGLE). */ function compileFlowBody( name: string, description: string | undefined, steps: ScriptStep[], parent: FlowCompile, entrySlots: FlowSlot[], ): FlowSchema { const ctx: FlowCompile = { name, argsMessage: parent.argsMessage, slotMessages: parent.slotMessages, usedNames: parent.usedNames, ends: new Map(), seen: new Set(), usedSlots: new Set(), edges: [], tryTotal: countTrys(steps), tryCount: 0, entrySlots, }; const entry = compileStatements(steps, ctx, DANGLE, entrySlots); if (steps.length === 0) { // An empty flow (e.g. a swallow catch) needs a real start node. const pass = node('pass', {}); ctx.seen.add(pass); ctx.edges.push(edge(pass, DANGLE)); return buildFlow(name, description, ctx, pass); } return buildFlow(name, description, ctx, entry as FlowStep); } function buildFlow( name: string, description: string | undefined, ctx: FlowCompile, start: FlowStep, ): FlowSchema { const nodes = [...ctx.seen]; const registry = defineSlots(buildSlots(ctx)); // Entry inheritance only for slots the flow actually consumes; unused // productions of the enclosing flow are not this flow's concern. Name-based // matching: inherited instances may come from another flow's registry (the // try body's re-bound productions), so identity comparison would drop them. const usedNames = new Set([...ctx.usedSlots].map((s) => s.name)); const entrySlots = rewriteSlots(nodes, ctx.edges, registry, ctx.entrySlots.filter((s) => usedNames.has(s.name))); return defineFlow(name, { start, description, args: ctx.argsMessage, slots: registry, entrySlots, edges: (flow) => ctx.edges.map((e) => (e.end === DANGLE ? { ...e, end: flow.returnEnd } : e)), }); } /** The flow's slot registry: args plus the named slots used inside the flow. */ function buildSlots(ctx: FlowCompile): Record { const out: Record = { args: ctx.argsMessage }; for (const s of ctx.usedSlots) { if (s.name === 'args') continue; const msg = ctx.slotMessages[s.name]; if (msg === undefined) { throw new Error(`flow-script ${ctx.name}: slot "${s.name}" is used but has no declared message`); } out[s.name] = msg; } return out; } /** Every flow compiles with its own registry objects; slot references inside * its nodes still point at the script-level registry, so they are re-bound * by name to this flow's registry. Returns the entry slots re-bound the same * way. */ function rewriteSlots(nodes: FlowNodeOrEnd[], edges: FlowEdge[], slots: FlowSlots, entrySlots: FlowSlot[]): FlowSlot[] { const map = (s: FlowSlot): FlowSlot => { const t = slots[s.name]; if (t === undefined) { throw new Error(`flow-script: slot "${s.name}" is missing from the compiled registry`); } return t; }; const ref = (m: FlowNodeMethodRef): FlowNodeMethodRef => { if (!isCall(m)) return m; return { method: m.method, args: m.args?.map(map), result: m.result ? map(m.result) : undefined }; }; const cond = (c: GuardCondition): GuardCondition => { if (isConditionGroup(c)) { return { kind: c.kind, conds: c.conds.map(cond) }; } if (!isCall(c)) { if (isFlowSlot(c.field)) { return { kind: 'comparison', op: c.op, field: map(c.field), value: c.value }; } return { kind: 'comparison', op: c.op, field: { slot: map(c.field.slot), field: c.field.field }, value: c.value }; } return { method: c.method, args: c.args?.map(map), result: c.result ? map(c.result) : undefined }; }; for (const n of nodes) { if (isFlowNode(n)) { n.methods = n.methods?.map(ref); if (n.publish) n.publish = { event: n.publish.event, payload: n.publish.payload ? map(n.publish.payload) : undefined }; n.reads = n.reads?.map(map); n.writes = n.writes?.map(map); } if (isGuard(n)) { for (const c of n.checks) { c.reads = c.reads?.map(map); if (c.check) c.check = cond(c.check); } } if (isIfNode(n)) { for (const c of n.cases) c.check = cond(c.check); } } for (const e of edges) { if (e.check) e.check = cond(e.check); } return entrySlots.map(map); }