@xdelivered/emberflow 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { HttpError, isHttpError } from './httpError';
3
+
4
+ describe('HttpError', () => {
5
+ it('carries status and body', () => {
6
+ const e = new HttpError(404, { error: 'gone' });
7
+ expect(e.status).toBe(404);
8
+ expect(e.body).toEqual({ error: 'gone' });
9
+ expect(isHttpError(e)).toBe(true);
10
+ expect(isHttpError(new Error('x'))).toBe(false);
11
+ });
12
+ });
@@ -0,0 +1,16 @@
1
+ /** A typed error a node can throw to control the HTTP status/body of its operation. */
2
+ export class HttpError extends Error {
3
+ readonly status: number;
4
+ readonly body?: unknown;
5
+
6
+ constructor(status: number, body?: unknown) {
7
+ super(`HTTP ${status}`);
8
+ this.name = 'HttpError';
9
+ this.status = status;
10
+ this.body = body;
11
+ }
12
+ }
13
+
14
+ export function isHttpError(e: unknown): e is HttpError {
15
+ return e instanceof HttpError;
16
+ }
@@ -0,0 +1,11 @@
1
+ export * from './types';
2
+ export * from './registry';
3
+ export * from './validation';
4
+ export * from './trace';
5
+ export * from './executor';
6
+ export * from './output';
7
+ export * from './publish';
8
+ export * from './scenarios';
9
+ export * from './httpError';
10
+ export * from './schemaCheck';
11
+ export * from './authVerify';
@@ -0,0 +1,21 @@
1
+ import type { WorkflowDefinition, WorkflowRun } from './types';
2
+
3
+ /**
4
+ * The output a finished run surfaces: the data collected by its Result nodes —
5
+ * exactly what the Dock's Output tab shows. A run with a single Result that
6
+ * produced output yields that output directly; multiple Results are keyed by
7
+ * node label; a run whose Result nodes were all skipped (or has none) yields
8
+ * undefined. Used to derive a Subflow node's return value on both hosts
9
+ * (browser store and server run registry), so subflow output follows the same
10
+ * convention users already see for a run.
11
+ */
12
+ export function runOutput(run: WorkflowRun, flow: WorkflowDefinition): unknown {
13
+ const produced = flow.nodes.filter(
14
+ (n) => n.type === 'Result' && run.nodeStates[n.id]?.output !== undefined,
15
+ );
16
+ if (produced.length === 0) return undefined;
17
+ if (produced.length === 1) return run.nodeStates[produced[0].id]!.output;
18
+ const out: Record<string, unknown> = {};
19
+ for (const n of produced) out[n.label] = run.nodeStates[n.id]!.output;
20
+ return out;
21
+ }
@@ -0,0 +1,176 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { NodeRegistry } from './registry';
3
+ import { hashImplementation, isArtifact, publishFlow, verifyArtifact } from './publish';
4
+ import type { NodeImplementation, WorkflowDefinition, WorkflowNode } from './types';
5
+
6
+ // Two genuinely different Task implementation *sources* — closure capture does
7
+ // not change impl.toString(), so distinct function bodies are what shifts the
8
+ // hash. Keyed by a marker so tests can pick a build.
9
+ const taskImpls: Record<string, NodeImplementation> = {
10
+ a: async () => ({ result: 'a' }),
11
+ different: async () => {
12
+ const doubled = 2 + 2;
13
+ return { result: 'different', doubled };
14
+ },
15
+ };
16
+
17
+ /** A registry whose `Task` implementation body is chosen by `marker`. */
18
+ function makeRegistry(marker: keyof typeof taskImpls): NodeRegistry {
19
+ const registry = new NodeRegistry();
20
+ registry.register(
21
+ { type: 'Input', label: 'Input', category: 'input' },
22
+ async (ctx) => ({ ...ctx.runInput }),
23
+ );
24
+ registry.register({ type: 'Task', label: 'Task' }, taskImpls[marker]);
25
+ return registry;
26
+ }
27
+
28
+ const node = (id: string, type: string, extra: Partial<WorkflowNode> = {}): WorkflowNode => ({
29
+ id, type, label: id, position: { x: 0, y: 0 }, config: {}, ...extra,
30
+ });
31
+
32
+ function makeFlow(nodes: WorkflowNode[], edges: WorkflowDefinition['edges'] = []): WorkflowDefinition {
33
+ return {
34
+ id: 'f', name: 'f', version: 1, nodes, edges,
35
+ createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
36
+ };
37
+ }
38
+
39
+ const loginish = (): WorkflowDefinition =>
40
+ makeFlow(
41
+ [
42
+ node('input', 'Input', {
43
+ config: {
44
+ fields: [
45
+ { name: 'username', type: 'string', required: true },
46
+ { name: 'password', type: 'string', required: true },
47
+ ],
48
+ defaults: { username: 'ada' },
49
+ },
50
+ }),
51
+ node('task', 'Task'),
52
+ ],
53
+ [{ id: 'e1', source: 'input', target: 'task' }],
54
+ );
55
+
56
+ describe('publishFlow', () => {
57
+ it('strips metadata.pinnedOutput from every node (dropping empty metadata)', async () => {
58
+ const flow = makeFlow([
59
+ node('input', 'Input'),
60
+ node('task', 'Task', { metadata: { pinnedOutput: { marker: 'pinned' }, note: 'keep' } }),
61
+ ]);
62
+ const artifact = await publishFlow(flow, makeRegistry('a'), () => '2026-07-02T00:00:00Z');
63
+
64
+ const task = artifact.flow.nodes.find((n) => n.id === 'task')!;
65
+ expect(task.metadata).toEqual({ note: 'keep' });
66
+ // Node whose metadata becomes empty loses the key entirely.
67
+ const inputNode = artifact.flow.nodes.find((n) => n.id === 'input')!;
68
+ expect(inputNode.metadata).toBeUndefined();
69
+ // Source flow is untouched (deep copy).
70
+ expect(flow.nodes.find((n) => n.id === 'task')!.metadata).toHaveProperty('pinnedOutput');
71
+ });
72
+
73
+ it('drops metadata when pinnedOutput was its only key', async () => {
74
+ const flow = makeFlow([
75
+ node('input', 'Input'),
76
+ node('task', 'Task', { metadata: { pinnedOutput: 42 } }),
77
+ ]);
78
+ const artifact = await publishFlow(flow, makeRegistry('a'));
79
+ expect(artifact.flow.nodes.find((n) => n.id === 'task')!.metadata).toBeUndefined();
80
+ });
81
+
82
+ it('lifts the input schema from the Input node config.fields', async () => {
83
+ const artifact = await publishFlow(loginish(), makeRegistry('a'));
84
+ expect(artifact.inputSchema).toEqual([
85
+ { name: 'username', type: 'string', required: true },
86
+ { name: 'password', type: 'string', required: true },
87
+ ]);
88
+ });
89
+
90
+ it('uses [] when there is no Input node', async () => {
91
+ const flow = makeFlow([node('task', 'Task')]);
92
+ const artifact = await publishFlow(flow, makeRegistry('a'));
93
+ expect(artifact.inputSchema).toEqual([]);
94
+ });
95
+
96
+ it('ignores malformed field entries when lifting the schema', async () => {
97
+ const flow = makeFlow([
98
+ node('input', 'Input', { config: { fields: [{ name: 'ok', type: 'string' }, { type: 'string' }, 'nope'] } }),
99
+ ]);
100
+ const artifact = await publishFlow(flow, makeRegistry('a'));
101
+ expect(artifact.inputSchema).toEqual([{ name: 'ok', type: 'string' }]);
102
+ });
103
+
104
+ it('produces the emberflow/v1 envelope with a publishedAt from now()', async () => {
105
+ const artifact = await publishFlow(loginish(), makeRegistry('a'), () => '2026-07-02T12:00:00Z');
106
+ expect(artifact.$artifact).toBe('emberflow/v1');
107
+ expect(artifact.publishedAt).toBe('2026-07-02T12:00:00Z');
108
+ expect(Object.keys(artifact.nodeHashes).sort()).toEqual(['Input', 'Task']);
109
+ });
110
+
111
+ it('hashes are stable for the same registry and change for a different implementation', async () => {
112
+ const a1 = await publishFlow(loginish(), makeRegistry('a'));
113
+ const a2 = await publishFlow(loginish(), makeRegistry('a'));
114
+ const b = await publishFlow(loginish(), makeRegistry('different'));
115
+ expect(a1.nodeHashes.Task).toBe(a2.nodeHashes.Task);
116
+ expect(a1.nodeHashes.Task).not.toBe(b.nodeHashes.Task);
117
+ // The Input node is identical across registries.
118
+ expect(a1.nodeHashes.Input).toBe(b.nodeHashes.Input);
119
+ });
120
+
121
+ it('strips scenarios on publish without mutating the source flow', async () => {
122
+ const flow = loginish();
123
+ flow.scenarios = [{ id: 's1', name: 'Happy path', input: { username: 'ada', password: 'x' } }];
124
+ const artifact = await publishFlow(flow, makeRegistry('a'));
125
+ expect(artifact.flow.scenarios).toBeUndefined();
126
+ expect(flow.scenarios).toEqual([
127
+ { id: 's1', name: 'Happy path', input: { username: 'ada', password: 'x' } },
128
+ ]);
129
+ });
130
+
131
+ it('throws on an invalid flow', async () => {
132
+ const flow = makeFlow([node('ghost', 'Nonexistent')]);
133
+ await expect(publishFlow(flow, makeRegistry('a'))).rejects.toThrow(/Cannot publish/);
134
+ });
135
+ });
136
+
137
+ describe('verifyArtifact', () => {
138
+ it('returns [] when the registry matches', async () => {
139
+ const artifact = await publishFlow(loginish(), makeRegistry('a'));
140
+ expect(await verifyArtifact(artifact, makeRegistry('a'))).toEqual([]);
141
+ });
142
+
143
+ it('names the drifted type on an implementation mismatch', async () => {
144
+ const artifact = await publishFlow(loginish(), makeRegistry('a'));
145
+ expect(await verifyArtifact(artifact, makeRegistry('different'))).toEqual(['Task']);
146
+ });
147
+
148
+ it('names unknown types missing from the registry', async () => {
149
+ const artifact = await publishFlow(loginish(), makeRegistry('a'));
150
+ const bare = new NodeRegistry();
151
+ // Same Input body as makeRegistry, but Task is absent entirely.
152
+ bare.register({ type: 'Input', label: 'Input' }, async (ctx) => ({ ...ctx.runInput }));
153
+ expect(await verifyArtifact(artifact, bare)).toEqual(['Task']);
154
+ });
155
+ });
156
+
157
+ describe('isArtifact', () => {
158
+ it('accepts a published artifact', async () => {
159
+ const artifact = await publishFlow(loginish(), makeRegistry('a'));
160
+ expect(isArtifact(artifact)).toBe(true);
161
+ });
162
+
163
+ it('rejects plain flows and junk', () => {
164
+ expect(isArtifact(loginish())).toBe(false);
165
+ expect(isArtifact(null)).toBe(false);
166
+ expect(isArtifact({ $artifact: 'other' })).toBe(false);
167
+ expect(isArtifact(42)).toBe(false);
168
+ });
169
+ });
170
+
171
+ describe('hashImplementation', () => {
172
+ it('is a 64-char lowercase hex string', async () => {
173
+ const hash = await hashImplementation(async () => ({}));
174
+ expect(hash).toMatch(/^[0-9a-f]{64}$/);
175
+ });
176
+ });
@@ -0,0 +1,127 @@
1
+ import type { FieldDefinition, NodeImplementation, WorkflowDefinition } from './types';
2
+ import type { NodeRegistry } from './registry';
3
+ import { validateFlow } from './validation';
4
+
5
+ /**
6
+ * A sealed, executable flow artifact. Carries the flow, the input schema lifted
7
+ * from its Input node, and a SHA-256 hash of every node type's implementation
8
+ * so the runner can refuse to execute a flow whose code has drifted.
9
+ */
10
+ export interface PublishedArtifact {
11
+ $artifact: 'emberflow/v1';
12
+ publishedAt: string;
13
+ inputSchema: FieldDefinition[];
14
+ /** node TYPE -> sha256 hex of implementation.toString() */
15
+ nodeHashes: Record<string, string>;
16
+ flow: WorkflowDefinition;
17
+ }
18
+
19
+ const ARTIFACT_TAG = 'emberflow/v1';
20
+
21
+ /** SHA-256 hex of an implementation's source text (WebCrypto — browser + Node). */
22
+ export async function hashImplementation(impl: NodeImplementation): Promise<string> {
23
+ const bytes = new TextEncoder().encode(impl.toString());
24
+ const digest = await crypto.subtle.digest('SHA-256', bytes);
25
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
26
+ }
27
+
28
+ /** Distinct node types used by a flow, in first-seen order. */
29
+ function distinctTypes(flow: WorkflowDefinition): string[] {
30
+ const seen: string[] = [];
31
+ for (const node of flow.nodes) {
32
+ if (!seen.includes(node.type)) seen.push(node.type);
33
+ }
34
+ return seen;
35
+ }
36
+
37
+ /** Lift the declared input schema from the flow's first Input node (or []). */
38
+ function liftInputSchema(flow: WorkflowDefinition): FieldDefinition[] {
39
+ const input = flow.nodes.find((n) => n.type === 'Input');
40
+ const fields = input?.config?.fields;
41
+ if (!Array.isArray(fields)) return [];
42
+ return fields.filter(
43
+ (f): f is FieldDefinition =>
44
+ f !== null && typeof f === 'object' && typeof (f as { name?: unknown }).name === 'string',
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Deep-copy a flow and strip dev-only fixtures: metadata.pinnedOutput from
50
+ * every node, and top-level scenarios. Both are authoring aids that have no
51
+ * business shipping in a published artifact.
52
+ */
53
+ function sealFlow(flow: WorkflowDefinition): WorkflowDefinition {
54
+ const copy = structuredClone(flow);
55
+ for (const node of copy.nodes) {
56
+ if (!node.metadata) continue;
57
+ delete node.metadata.pinnedOutput;
58
+ if (Object.keys(node.metadata).length === 0) delete node.metadata;
59
+ }
60
+ delete copy.scenarios;
61
+ return copy;
62
+ }
63
+
64
+ /**
65
+ * Seal a flow into a PublishedArtifact: validate (error-severity issues throw),
66
+ * strip pins, lift the input schema, and hash each node type's implementation.
67
+ */
68
+ export async function publishFlow(
69
+ flow: WorkflowDefinition,
70
+ registry: NodeRegistry,
71
+ now: () => string = () => new Date().toISOString(),
72
+ ): Promise<PublishedArtifact> {
73
+ const errors = validateFlow(flow, registry).filter((i) => i.severity === 'error');
74
+ if (errors.length > 0) {
75
+ throw new Error(`Cannot publish: ${errors.map((i) => i.message).join('; ')}`);
76
+ }
77
+
78
+ const sealed = sealFlow(flow);
79
+ const nodeHashes: Record<string, string> = {};
80
+ for (const type of distinctTypes(sealed)) {
81
+ nodeHashes[type] = await hashImplementation(registry.get(type).implementation);
82
+ }
83
+
84
+ return {
85
+ $artifact: ARTIFACT_TAG,
86
+ publishedAt: now(),
87
+ inputSchema: liftInputSchema(sealed),
88
+ nodeHashes,
89
+ flow: sealed,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Verify every node hash in an artifact against a live registry. Returns the
95
+ * list of drifted (hash mismatch) or unknown (unregistered) node types; an
96
+ * empty list means the artifact is safe to execute on this registry.
97
+ */
98
+ export async function verifyArtifact(
99
+ artifact: PublishedArtifact,
100
+ registry: NodeRegistry,
101
+ ): Promise<string[]> {
102
+ const bad: string[] = [];
103
+ for (const [type, expected] of Object.entries(artifact.nodeHashes)) {
104
+ if (!registry.has(type)) {
105
+ bad.push(type);
106
+ continue;
107
+ }
108
+ const actual = await hashImplementation(registry.get(type).implementation);
109
+ if (actual !== expected) bad.push(type);
110
+ }
111
+ return bad;
112
+ }
113
+
114
+ /** Structural type guard for a PublishedArtifact envelope. */
115
+ export function isArtifact(value: unknown): value is PublishedArtifact {
116
+ if (value === null || typeof value !== 'object') return false;
117
+ const v = value as Record<string, unknown>;
118
+ return (
119
+ v.$artifact === ARTIFACT_TAG &&
120
+ typeof v.publishedAt === 'string' &&
121
+ Array.isArray(v.inputSchema) &&
122
+ v.nodeHashes !== null &&
123
+ typeof v.nodeHashes === 'object' &&
124
+ v.flow !== null &&
125
+ typeof v.flow === 'object'
126
+ );
127
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { NodeRegistry } from './registry';
3
+ import type { NodeDefinition } from './types';
4
+
5
+ const def = (type: string): NodeDefinition => ({ type, label: type });
6
+ const impl = async () => ({});
7
+
8
+ describe('NodeRegistry', () => {
9
+ it('registers and retrieves a node', () => {
10
+ const r = new NodeRegistry();
11
+ r.register(def('a'), impl);
12
+ expect(r.get('a').definition.type).toBe('a');
13
+ expect(r.has('a')).toBe(true);
14
+ });
15
+
16
+ it('throws on duplicate registration', () => {
17
+ const r = new NodeRegistry();
18
+ r.register(def('a'), impl);
19
+ expect(() => r.register(def('a'), impl)).toThrow(/already registered/);
20
+ });
21
+
22
+ it('throws on unknown type', () => {
23
+ expect(() => new NodeRegistry().get('nope')).toThrow(/Unknown node type/);
24
+ });
25
+
26
+ it('lists definitions', () => {
27
+ const r = new NodeRegistry();
28
+ r.register(def('a'), impl);
29
+ r.register(def('b'), impl);
30
+ expect(r.list().map((d) => d.type)).toEqual(['a', 'b']);
31
+ });
32
+ });
33
+
34
+ describe('NodeRegistry.registerDefinition', () => {
35
+ it('adds a metadata-only node whose implementation throws server-required', async () => {
36
+ const r = new NodeRegistry();
37
+ r.registerDefinition({ type: 'Remote', label: 'Remote' }, 'async () => ({})');
38
+ expect(r.has('Remote')).toBe(true);
39
+ expect(r.list().map((d) => d.type)).toContain('Remote');
40
+ expect(r.getSource('Remote')).toBe('async () => ({})');
41
+ await expect(r.get('Remote').implementation({} as never)).rejects.toThrow(/server/i);
42
+ });
43
+
44
+ it('does not overwrite an already-registered node', () => {
45
+ const r = new NodeRegistry();
46
+ r.register({ type: 'A', label: 'Real' }, async () => ({ real: true }));
47
+ r.registerDefinition({ type: 'A', label: 'Stub' });
48
+ expect(r.get('A').definition.label).toBe('Real');
49
+ });
50
+
51
+ it('getSource returns implementation source for real nodes', () => {
52
+ const r = new NodeRegistry();
53
+ r.register({ type: 'B', label: 'B' }, async () => ({ v: 1 }));
54
+ expect(r.getSource('B')).toContain('v: 1');
55
+ });
56
+
57
+ it('withSameNodes returns a new instance that shares the same nodes', () => {
58
+ const r = new NodeRegistry();
59
+ r.register({ type: 'C', label: 'C' }, async () => ({}));
60
+ const clone = r.withSameNodes();
61
+ expect(clone).not.toBe(r);
62
+ expect(clone.has('C')).toBe(true);
63
+ });
64
+ });
@@ -0,0 +1,80 @@
1
+ import type { NodeDefinition, NodeImplementation } from './types';
2
+
3
+ export interface RegisteredNode {
4
+ definition: NodeDefinition;
5
+ implementation: NodeImplementation;
6
+ }
7
+
8
+ export class NodeRegistry {
9
+ nodes = new Map<string, RegisteredNode>();
10
+ sources = new Map<string, string>();
11
+
12
+ register(definition: NodeDefinition, implementation: NodeImplementation): void {
13
+ if (this.nodes.has(definition.type)) {
14
+ throw new Error(`Node type already registered: ${definition.type}`);
15
+ }
16
+ this.nodes.set(definition.type, { definition, implementation });
17
+ }
18
+
19
+ get(type: string): RegisteredNode {
20
+ const node = this.nodes.get(type);
21
+ if (!node) throw new Error(`Unknown node type: ${type}`);
22
+ return node;
23
+ }
24
+
25
+ has(type: string): boolean {
26
+ return this.nodes.has(type);
27
+ }
28
+
29
+ /**
30
+ * Replace this registry's contents with another's, in place. Keeps this
31
+ * object's identity, so references held elsewhere (RunRegistry's execution
32
+ * registry, request-handler closures) immediately see the new node set. Used
33
+ * by the project-config hot-reload to pick up agent-authored nodes without a
34
+ * process restart (which would kill in-flight agent runs).
35
+ */
36
+ adopt(other: NodeRegistry): void {
37
+ this.nodes = other.nodes;
38
+ this.sources = other.sources;
39
+ }
40
+
41
+ list(): NodeDefinition[] {
42
+ return [...this.nodes.values()].map((n) => n.definition);
43
+ }
44
+
45
+ /**
46
+ * Register a definition-only node (metadata fetched from the runner). Its
47
+ * implementation is a stub that fails loudly if browser-executed — such
48
+ * nodes run on the server. Never overwrites a real registration.
49
+ */
50
+ registerDefinition(definition: NodeDefinition, source?: string): void {
51
+ if (this.nodes.has(definition.type)) return;
52
+ this.nodes.set(definition.type, {
53
+ definition,
54
+ implementation: async () => {
55
+ throw new Error(
56
+ `${definition.type} runs on the server — start the runner or switch execution to Server mode`,
57
+ );
58
+ },
59
+ });
60
+ if (source !== undefined) this.sources.set(definition.type, source);
61
+ }
62
+
63
+ /** Source text for display: stored source for definition-only nodes, else the impl's toString(). */
64
+ getSource(type: string): string | undefined {
65
+ if (this.sources.has(type)) return this.sources.get(type);
66
+ const node = this.nodes.get(type);
67
+ return node ? node.implementation.toString() : undefined;
68
+ }
69
+
70
+ /**
71
+ * A fresh registry instance sharing this one's node + source maps — a new
72
+ * top-level reference (so store subscribers re-render) without copying node data.
73
+ */
74
+ withSameNodes(): NodeRegistry {
75
+ const next = new NodeRegistry();
76
+ next.nodes = this.nodes;
77
+ next.sources = this.sources;
78
+ return next;
79
+ }
80
+ }
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { findScenario, scenarioNames } from './scenarios';
3
+ import type { WorkflowDefinition, WorkflowNode } from './types';
4
+
5
+ const node = (id: string, type = 'plain'): WorkflowNode => ({
6
+ id, type, label: id, position: { x: 0, y: 0 }, config: {},
7
+ });
8
+
9
+ const flow = (scenarios?: WorkflowDefinition['scenarios']): WorkflowDefinition => ({
10
+ id: 'f', name: 'f', version: 1, nodes: [node('a')], edges: [],
11
+ createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
12
+ ...(scenarios ? { scenarios } : {}),
13
+ });
14
+
15
+ describe('findScenario', () => {
16
+ const scenarios = [
17
+ { id: 's1', name: 'Happy path', input: { userId: 'u1' } },
18
+ { id: 's2', name: 'Empty note', input: { userId: 'u2', note: '' } },
19
+ ];
20
+
21
+ it('finds a scenario by exact name', () => {
22
+ const f = flow(scenarios);
23
+ expect(findScenario(f, 'Happy path')).toEqual(scenarios[0]);
24
+ });
25
+
26
+ it('finds a scenario by id', () => {
27
+ const f = flow(scenarios);
28
+ expect(findScenario(f, 's2')).toEqual(scenarios[1]);
29
+ });
30
+
31
+ it('returns undefined when no scenario matches', () => {
32
+ const f = flow(scenarios);
33
+ expect(findScenario(f, 'nope')).toBeUndefined();
34
+ });
35
+
36
+ it('returns undefined when the flow has no scenarios array', () => {
37
+ const f = flow();
38
+ expect(findScenario(f, 'Happy path')).toBeUndefined();
39
+ });
40
+ });
41
+
42
+ describe('scenarioNames', () => {
43
+ it('lists names in order', () => {
44
+ const f = flow([
45
+ { id: 's1', name: 'Happy path', input: {} },
46
+ { id: 's2', name: 'Empty note', input: {} },
47
+ ]);
48
+ expect(scenarioNames(f)).toEqual(['Happy path', 'Empty note']);
49
+ });
50
+
51
+ it('returns an empty array when the flow has no scenarios', () => {
52
+ const f = flow();
53
+ expect(scenarioNames(f)).toEqual([]);
54
+ });
55
+ });
@@ -0,0 +1,29 @@
1
+ import type { ScenarioDefinition, WorkflowDefinition } from './types';
2
+
3
+ /** Find a scenario by name (exact) or id. Returns undefined when absent. */
4
+ export function findScenario(
5
+ flow: WorkflowDefinition,
6
+ nameOrId: string,
7
+ ): ScenarioDefinition | undefined {
8
+ return flow.scenarios?.find((s) => s.name === nameOrId || s.id === nameOrId);
9
+ }
10
+
11
+ /** Names of a flow's scenarios, for error messages. */
12
+ export function scenarioNames(flow: WorkflowDefinition): string[] {
13
+ return (flow.scenarios ?? []).map((s) => s.name);
14
+ }
15
+
16
+ /** Merge an op's flow-level `mocks` with a scenario's `mocks` (scenario wins
17
+ * per nodeId) into the map an executor mock run consults. ONE merge shared
18
+ * by both places that start a mock run: the live serving path
19
+ * (server/index.ts POST /runs, when servingMode === 'mock') and the CLI test
20
+ * runner's `--mock` flag (server/testRunner.ts) — so mock semantics can't
21
+ * drift between "serve mock" and "test mock". */
22
+ export function mergeMocks(
23
+ flow: WorkflowDefinition,
24
+ scenario?: ScenarioDefinition,
25
+ ): Record<string, unknown> {
26
+ const opMocks = (flow.mocks ?? {}) as Record<string, unknown>;
27
+ const scenarioMocks = (scenario?.mocks ?? {}) as Record<string, unknown>;
28
+ return { ...opMocks, ...scenarioMocks };
29
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { validateAgainstSchema } from './schemaCheck';
3
+
4
+ describe('validateAgainstSchema', () => {
5
+ it('passes when no schema', () => {
6
+ expect(validateAgainstSchema(undefined, { a: 1 })).toBeNull();
7
+ });
8
+ it('flags a missing required field', () => {
9
+ const s = { type: 'object', required: ['name'] };
10
+ expect(validateAgainstSchema(s, {})).toMatch(/name/);
11
+ expect(validateAgainstSchema(s, { name: 'x' })).toBeNull();
12
+ });
13
+ it('flags a wrong property type', () => {
14
+ const s = { type: 'object', properties: { age: { type: 'number' } } };
15
+ expect(validateAgainstSchema(s, { age: 'not a number' })).toMatch(/age/);
16
+ expect(validateAgainstSchema(s, { age: 5 })).toBeNull();
17
+ });
18
+ it('requires an object at the top when type is object', () => {
19
+ expect(validateAgainstSchema({ type: 'object' }, 'nope')).toMatch(/object/);
20
+ });
21
+ });
@@ -0,0 +1,35 @@
1
+ type PropType = 'string' | 'number' | 'boolean' | 'object' | 'array';
2
+ interface ObjectSchema {
3
+ type?: 'object';
4
+ required?: string[];
5
+ properties?: Record<string, { type?: PropType }>;
6
+ }
7
+
8
+ const typeOfValue = (v: unknown): PropType | 'null' | 'undefined' => {
9
+ if (v === null) return 'null';
10
+ if (v === undefined) return 'undefined';
11
+ if (Array.isArray(v)) return 'array';
12
+ return typeof v as PropType;
13
+ };
14
+
15
+ /** Minimal subset validator: object type, required keys, per-property primitive
16
+ * type. Returns null when valid, else the first error message. Unknown schema
17
+ * shapes impose no constraint (valid) — extend as real schemas demand. */
18
+ export function validateAgainstSchema(schema: unknown, value: unknown): string | null {
19
+ if (!schema || typeof schema !== 'object') return null;
20
+ const s = schema as ObjectSchema;
21
+ if (s.type === 'object') {
22
+ if (typeOfValue(value) !== 'object') return 'expected an object';
23
+ const obj = value as Record<string, unknown>;
24
+ for (const key of s.required ?? []) {
25
+ if (!(key in obj) || obj[key] === undefined) return `missing required field: ${key}`;
26
+ }
27
+ for (const [key, spec] of Object.entries(s.properties ?? {})) {
28
+ if (obj[key] === undefined) continue;
29
+ if (spec.type && typeOfValue(obj[key]) !== spec.type) {
30
+ return `field ${key} must be a ${spec.type}`;
31
+ }
32
+ }
33
+ }
34
+ return null;
35
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { InMemoryTraceSink } from './trace';
3
+ import type { NodeExecutionSample } from './types';
4
+
5
+ const sample = (id: string, nodeId: string): NodeExecutionSample => ({
6
+ id, workflowId: 'w', runId: 'r', nodeId, nodeType: 't', nodeLabel: 'T',
7
+ input: { id }, status: 'succeeded', startedAt: '2026-01-01T00:00:00Z',
8
+ });
9
+
10
+ describe('InMemoryTraceSink', () => {
11
+ it('records and filters by node, newest first', () => {
12
+ const sink = new InMemoryTraceSink();
13
+ sink.record(sample('s1', 'a'));
14
+ sink.record(sample('s2', 'b'));
15
+ sink.record(sample('s3', 'a'));
16
+ expect(sink.samplesFor('a').map((s) => s.id)).toEqual(['s3', 's1']);
17
+ expect(sink.all().map((s) => s.id)).toEqual(['s3', 's2', 's1']);
18
+ });
19
+ });