From b951724c5f7d72670cfbf1b9ccf331514b4233ca Mon Sep 17 00:00:00 2001 From: shad Date: Sun, 14 Jun 2026 14:42:55 +0400 Subject: [PATCH] feat(wizard): produce fully startable EA2 flows Reverse-engineered the EA2 runtime startability contract by diffing pr_to_po_def (startable) against wizard-created drafts (500). Minimum shape required: Parent flow (kind=definition, status=published) + Step flow (kind=definition, status=published, dispatch_kind=human) + View doc (kind=definition, status=active, layout={fields,layout:form}) + Version doc (kind=definition, status=active) + defines edge: parent -> step (role=child) + defines edge: step -> step (role=dispatch self-edge) + defines edge: step -> view (role=presentation) + governs edge collection 'governs': version -> parent (role=governs) wizardApi.ops gains: createView, createVersion, childEdge, dispatchEdge, presentationEdge, governsEdge. createStepEdge renamed to childEdge to reflect the actual EA2 role. Steps now create with status=published (was draft) so they're visible to the runtime. Wizard.handleStructureSave emits all of the above in two apply-batches (creates then wires). Verified live: posted a probe flow with this contract, /api/runtime/transactions returned 200 + a running transaction with step_run_id. --- src/lib/chatApi.ts | 2 +- src/lib/wizardApi.ts | 72 +++++++++++++++++++++++++++++++++++++++++-- src/scenes/Wizard.tsx | 31 ++++++++++++------- 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/src/lib/chatApi.ts b/src/lib/chatApi.ts index 0caae22..5d62ce8 100644 --- a/src/lib/chatApi.ts +++ b/src/lib/chatApi.ts @@ -215,7 +215,7 @@ export const chatApi = { if (!created.ok) throw new Error(`sendMessage create ${created.status}`); const msg = await created.json(); - const linkOps: BatchOperation[] = [wizardApi.ops.createStepEdge(threadKey, msg._key)]; + const linkOps: BatchOperation[] = [wizardApi.ops.childEdge(threadKey, msg._key)]; await fetch(`${api.config.baseUrl}/api/ea2/apply-batch`, { method: "POST", headers: authHeaders(), diff --git a/src/lib/wizardApi.ts b/src/lib/wizardApi.ts index 6960392..54928cb 100644 --- a/src/lib/wizardApi.ts +++ b/src/lib/wizardApi.ts @@ -130,7 +130,7 @@ export const wizardApi = { kind: "definition", name, display_name: node.display_name, - status: "draft", + status: "published", description: node.description || node.display_name, source_context: "EA2_WIZARD_STEP", dispatch_kind: node.dispatch_kind, @@ -138,13 +138,79 @@ export const wizardApi = { }, }; }, - createStepEdge(parentFlowKey: string, childFlowKey: string): CreateEdgeOp { + createView(node: { display_name: string }): CreateOp { + const name = `${node.display_name.toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 40) || "view"}_view`; + return { + op: "create", + coll: "view", + data: { + kind: "definition", + name, + label: `${node.display_name} ยท form`, + status: "active", + description: null, + source_context: null, + channel: "web", + layout: { + layout: "form", + fields: [ + { name: "notes", label: "Notes", type: "textarea", required: false, description: "Notes" }, + ], + }, + actions: [{ label: "Submit", action: "submit" }], + }, + }; + }, + createVersion(parentDisplayName: string): CreateOp { + const name = `${parentDisplayName.toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 40) || "process"}_v1`; + return { + op: "create", + coll: "version", + data: { + kind: "definition", + name, + label: "v1", + status: "active", + change_description: `Initial publish of ${parentDisplayName}`, + legal_entity: "canvas", + source_context: "EA2_WIZARD_VERSION", + }, + }; + }, + childEdge(parentFlowKey: string, childFlowKey: string): CreateEdgeOp { return { op: "create_edge", edge_coll: "defines", from: `flow/${parentFlowKey}`, to: `flow/${childFlowKey}`, - role: "next", + role: "child", + }; + }, + dispatchEdge(stepFlowKey: string): CreateEdgeOp { + return { + op: "create_edge", + edge_coll: "defines", + from: `flow/${stepFlowKey}`, + to: `flow/${stepFlowKey}`, + role: "dispatch", + }; + }, + presentationEdge(stepFlowKey: string, viewKey: string): CreateEdgeOp { + return { + op: "create_edge", + edge_coll: "defines", + from: `flow/${stepFlowKey}`, + to: `view/${viewKey}`, + role: "presentation", + }; + }, + governsEdge(versionKey: string, parentFlowKey: string): CreateEdgeOp { + return { + op: "create_edge", + edge_coll: "governs", + from: `version/${versionKey}`, + to: `flow/${parentFlowKey}`, + role: "governs", }; }, publishFlow(flowKey: string): UpdateOp { diff --git a/src/scenes/Wizard.tsx b/src/scenes/Wizard.tsx index 707ed34..c018149 100644 --- a/src/scenes/Wizard.tsx +++ b/src/scenes/Wizard.tsx @@ -103,27 +103,36 @@ export default function Wizard() { if (!draft.flowKey || !actor) return; setWorking(true); try { - const createOps: BatchOperation[] = draft.nodes.map(n => + const stepCreateOps: BatchOperation[] = draft.nodes.map(n => wizardApi.ops.createStep({ display_name: n.display_name, dispatch_kind: n.dispatch_kind, agent_capability: n.agent_capability, }) ); - const createRes = await wizardApi.applyBatch(draft.flowKey, createOps, actor); - const createdKeys: string[] = (createRes?.result?.ops || []).map((o: any) => o.key); - const nodesWithKeys = draft.nodes.map((n, i) => ({ ...n, _key: createdKeys[i] || n._key })); + const viewCreateOps: BatchOperation[] = draft.nodes.map(n => + wizardApi.ops.createView({ display_name: n.display_name }) + ); + const versionCreateOp: BatchOperation = wizardApi.ops.createVersion(draft.name || "Process"); + const allCreateOps = [...stepCreateOps, ...viewCreateOps, versionCreateOp]; - const linkOps: BatchOperation[] = [ - wizardApi.ops.createStepEdge(draft.flowKey!, nodesWithKeys[0]._key), - ...nodesWithKeys.slice(0, -1).map((n, i) => - wizardApi.ops.createStepEdge(n._key, nodesWithKeys[i + 1]._key) - ), + const createRes = await wizardApi.applyBatch(draft.flowKey, allCreateOps, actor); + const createdOps: any[] = createRes?.result?.ops || []; + const stepKeys = createdOps.slice(0, draft.nodes.length).map(o => o.key); + const viewKeys = createdOps.slice(draft.nodes.length, draft.nodes.length * 2).map(o => o.key); + const versionKey = createdOps[createdOps.length - 1]?.key; + const nodesWithKeys = draft.nodes.map((n, i) => ({ ...n, _key: stepKeys[i] || n._key, viewKey: viewKeys[i] })); + + const wireOps: BatchOperation[] = [ + ...nodesWithKeys.map((n, _i) => wizardApi.ops.childEdge(draft.flowKey!, n._key)), + ...nodesWithKeys.map(n => wizardApi.ops.dispatchEdge(n._key)), + ...nodesWithKeys.map(n => wizardApi.ops.presentationEdge(n._key, n.viewKey)), ]; - if (linkOps.length) await wizardApi.applyBatch(draft.flowKey, linkOps, actor); + if (versionKey) wireOps.push(wizardApi.ops.governsEdge(versionKey, draft.flowKey!)); + if (wireOps.length) await wizardApi.applyBatch(draft.flowKey, wireOps, actor); updateDraft({ step: "Generate", nodes: nodesWithKeys }); - pushToast("ok", `Saved ${createdKeys.length} steps to EA2`); + pushToast("ok", `Saved ${stepKeys.length} steps with views and version to EA2`); } catch (err: any) { pushToast("err", `Save failed: ${err.message}`); } finally {