@engenty/wizards-runtime 0.0.0-stage → 0.7.6

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 (523) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +214 -2
  3. package/apps/runtime/dist/agents/architect.js +444 -0
  4. package/apps/runtime/dist/agents/studio-assistant.js +883 -0
  5. package/apps/runtime/dist/agents/subscription.js +207 -0
  6. package/apps/runtime/dist/agents/thought.js +21 -0
  7. package/apps/runtime/dist/agents/website.js +480 -0
  8. package/apps/runtime/dist/app.js +359 -0
  9. package/apps/runtime/dist/apple/wizards-apple +0 -0
  10. package/apps/runtime/dist/assistant/browser.js +26 -0
  11. package/apps/runtime/dist/assistant/docs.js +158 -0
  12. package/apps/runtime/dist/assistant/knows.js +353 -0
  13. package/apps/runtime/dist/assistant/primer.js +26 -0
  14. package/apps/runtime/dist/assistant/runs.js +24 -0
  15. package/apps/runtime/dist/auth/account.js +175 -0
  16. package/apps/runtime/dist/auth/device.js +112 -0
  17. package/apps/runtime/dist/auth/index.js +358 -0
  18. package/apps/runtime/dist/auth/keys.js +55 -0
  19. package/apps/runtime/dist/auth/local-ticket.js +26 -0
  20. package/apps/runtime/dist/authoring/example.js +74 -0
  21. package/apps/runtime/dist/authoring/guide.js +433 -0
  22. package/apps/runtime/dist/authoring/ops.js +117 -0
  23. package/apps/runtime/dist/browser/fast/action-space.js +160 -0
  24. package/apps/runtime/dist/browser/fast/classifier.js +91 -0
  25. package/apps/runtime/dist/browser/fast/decide.js +171 -0
  26. package/apps/runtime/dist/browser/fast/driver.js +141 -0
  27. package/apps/runtime/dist/browser/fast/instructions.js +33 -0
  28. package/apps/runtime/dist/browser/fast/run.js +249 -0
  29. package/apps/runtime/dist/browser/fast/span-picker.js +153 -0
  30. package/apps/runtime/dist/browser/fast/text-helper.js +178 -0
  31. package/apps/runtime/dist/browser/snapshot.js +335 -0
  32. package/apps/runtime/dist/cli/account.js +385 -0
  33. package/apps/runtime/dist/cli/autostart.js +182 -0
  34. package/apps/runtime/dist/cli/autoupdate.js +221 -0
  35. package/apps/runtime/dist/cli/clients.js +44 -0
  36. package/apps/runtime/dist/cli/connect.js +540 -0
  37. package/apps/runtime/dist/cli/environment.js +95 -0
  38. package/apps/runtime/dist/cli/exec.js +45 -0
  39. package/apps/runtime/dist/cli/features.js +50 -0
  40. package/apps/runtime/dist/cli/home.js +88 -0
  41. package/apps/runtime/dist/cli/install-env.js +33 -0
  42. package/apps/runtime/dist/cli/machine.js +107 -0
  43. package/apps/runtime/dist/cli/main.js +231 -0
  44. package/apps/runtime/dist/cli/mcp.js +136 -0
  45. package/apps/runtime/dist/cli/menu.js +202 -0
  46. package/apps/runtime/dist/cli/processes.js +72 -0
  47. package/apps/runtime/dist/cli/report.js +66 -0
  48. package/apps/runtime/dist/cli/sandbox.js +77 -0
  49. package/apps/runtime/dist/cli/setup.js +335 -0
  50. package/apps/runtime/dist/cli/start.js +276 -0
  51. package/apps/runtime/dist/cli/status.js +212 -0
  52. package/apps/runtime/dist/cli/ui.js +59 -0
  53. package/apps/runtime/dist/cli/uninstall-main.js +20 -0
  54. package/apps/runtime/dist/cli/uninstall.js +231 -0
  55. package/apps/runtime/dist/cli/update.js +86 -0
  56. package/apps/runtime/dist/cli/validate.js +75 -0
  57. package/apps/runtime/dist/cli/versions.js +123 -0
  58. package/apps/runtime/dist/connectors/builtin.js +86 -0
  59. package/apps/runtime/dist/connectors/external.js +409 -0
  60. package/apps/runtime/dist/connectors/gmail.js +107 -0
  61. package/apps/runtime/dist/connectors/imap.js +261 -0
  62. package/apps/runtime/dist/connectors/index.js +325 -0
  63. package/apps/runtime/dist/connectors/mail.js +72 -0
  64. package/apps/runtime/dist/connectors/outlook.js +90 -0
  65. package/apps/runtime/dist/connectors/plugin-connections.js +343 -0
  66. package/apps/runtime/dist/connectors/via-account.js +65 -0
  67. package/apps/runtime/dist/credits/credits.js +100 -0
  68. package/apps/runtime/dist/credits/estimate.js +152 -0
  69. package/apps/runtime/dist/db/adopt.js +149 -0
  70. package/apps/runtime/dist/db/client.js +173 -0
  71. package/apps/runtime/dist/db/control-schema.js +133 -0
  72. package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
  73. package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
  74. package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
  75. package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
  76. package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
  77. package/apps/runtime/dist/db/migrations/control/0005_chilly_oracle.sql +1 -0
  78. package/apps/runtime/dist/db/migrations/control/0006_sleepy_mojo.sql +1 -0
  79. package/apps/runtime/dist/db/migrations/control/0007_common_the_hand.sql +15 -0
  80. package/apps/runtime/dist/db/migrations/control/0008_runner.sql +8 -0
  81. package/apps/runtime/dist/db/migrations/control/0009_plugin_claim.sql +9 -0
  82. package/apps/runtime/dist/db/migrations/control/0010_tan_snowbird.sql +6 -0
  83. package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
  84. package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
  85. package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
  86. package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
  87. package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
  88. package/apps/runtime/dist/db/migrations/control/meta/0005_snapshot.json +358 -0
  89. package/apps/runtime/dist/db/migrations/control/meta/0006_snapshot.json +365 -0
  90. package/apps/runtime/dist/db/migrations/control/meta/0007_snapshot.json +460 -0
  91. package/apps/runtime/dist/db/migrations/control/meta/0008_snapshot.json +505 -0
  92. package/apps/runtime/dist/db/migrations/control/meta/0009_snapshot.json +561 -0
  93. package/apps/runtime/dist/db/migrations/control/meta/0010_snapshot.json +600 -0
  94. package/apps/runtime/dist/db/migrations/control/meta/_journal.json +83 -0
  95. package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
  96. package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
  97. package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
  98. package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
  99. package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
  100. package/apps/runtime/dist/db/migrations/tenant/0005_reflective_forgotten_one.sql +2 -0
  101. package/apps/runtime/dist/db/migrations/tenant/0006_hard_war_machine.sql +30 -0
  102. package/apps/runtime/dist/db/migrations/tenant/0007_space_data.sql +43 -0
  103. package/apps/runtime/dist/db/migrations/tenant/0008_run_event_note.sql +1 -0
  104. package/apps/runtime/dist/db/migrations/tenant/0009_knowledge.sql +104 -0
  105. package/apps/runtime/dist/db/migrations/tenant/0010_plugin_connection.sql +15 -0
  106. package/apps/runtime/dist/db/migrations/tenant/0011_runners.sql +2 -0
  107. package/apps/runtime/dist/db/migrations/tenant/0012_assistant_chat.sql +40 -0
  108. package/apps/runtime/dist/db/migrations/tenant/0013_run_origin.sql +1 -0
  109. package/apps/runtime/dist/db/migrations/tenant/0014_run_answer.sql +2 -0
  110. package/apps/runtime/dist/db/migrations/tenant/0015_device_calls.sql +13 -0
  111. package/apps/runtime/dist/db/migrations/tenant/0016_even_trish_tilby.sql +12 -0
  112. package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
  113. package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
  114. package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
  115. package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
  116. package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
  117. package/apps/runtime/dist/db/migrations/tenant/meta/0005_snapshot.json +1659 -0
  118. package/apps/runtime/dist/db/migrations/tenant/meta/0006_snapshot.json +1696 -0
  119. package/apps/runtime/dist/db/migrations/tenant/meta/0007_snapshot.json +2012 -0
  120. package/apps/runtime/dist/db/migrations/tenant/meta/0008_snapshot.json +2019 -0
  121. package/apps/runtime/dist/db/migrations/tenant/meta/0009_snapshot.json +2837 -0
  122. package/apps/runtime/dist/db/migrations/tenant/meta/0010_snapshot.json +2942 -0
  123. package/apps/runtime/dist/db/migrations/tenant/meta/0011_snapshot.json +2956 -0
  124. package/apps/runtime/dist/db/migrations/tenant/meta/0012_snapshot.json +3132 -0
  125. package/apps/runtime/dist/db/migrations/tenant/meta/0013_snapshot.json +3139 -0
  126. package/apps/runtime/dist/db/migrations/tenant/meta/0014_snapshot.json +3154 -0
  127. package/apps/runtime/dist/db/migrations/tenant/meta/0015_snapshot.json +3243 -0
  128. package/apps/runtime/dist/db/migrations/tenant/meta/0016_snapshot.json +3326 -0
  129. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +125 -0
  130. package/apps/runtime/dist/db/schema.js +602 -0
  131. package/apps/runtime/dist/debug.js +197 -0
  132. package/apps/runtime/dist/docs/dev/index.md +41 -0
  133. package/apps/runtime/dist/docs/dev/meta.json +7 -0
  134. package/apps/runtime/dist/docs/dev/plugins/connections.md +74 -0
  135. package/apps/runtime/dist/docs/dev/plugins/events.md +89 -0
  136. package/apps/runtime/dist/docs/dev/plugins/examples.md +101 -0
  137. package/apps/runtime/dist/docs/dev/plugins/index.md +78 -0
  138. package/apps/runtime/dist/docs/dev/plugins/layout.md +90 -0
  139. package/apps/runtime/dist/docs/dev/plugins/meta.json +28 -0
  140. package/apps/runtime/dist/docs/dev/plugins/quick-start.md +94 -0
  141. package/apps/runtime/dist/docs/dev/plugins/reference.md +348 -0
  142. package/apps/runtime/dist/docs/dev/plugins/reloading.md +76 -0
  143. package/apps/runtime/dist/docs/dev/plugins/routes.md +185 -0
  144. package/apps/runtime/dist/docs/dev/plugins/runners.md +230 -0
  145. package/apps/runtime/dist/docs/dev/plugins/server.md +281 -0
  146. package/apps/runtime/dist/docs/dev/plugins/shipping.md +79 -0
  147. package/apps/runtime/dist/docs/dev/plugins/space.md +297 -0
  148. package/apps/runtime/dist/docs/dev/plugins/starters.md +70 -0
  149. package/apps/runtime/dist/docs/dev/plugins/studio.md +363 -0
  150. package/apps/runtime/dist/docs/dev/plugins/styling.md +129 -0
  151. package/apps/runtime/dist/docs/dev/plugins/tables.md +104 -0
  152. package/apps/runtime/dist/docs/dev/plugins/tools.md +139 -0
  153. package/apps/runtime/dist/docs/dev/wizard-format.md +125 -0
  154. package/apps/runtime/dist/docs/meta.json +3 -0
  155. package/apps/runtime/dist/docs/user/ai-apps.md +112 -0
  156. package/apps/runtime/dist/docs/user/assistant.md +77 -0
  157. package/apps/runtime/dist/docs/user/build-a-wizard.md +116 -0
  158. package/apps/runtime/dist/docs/user/command-line.md +86 -0
  159. package/apps/runtime/dist/docs/user/connectors.md +90 -0
  160. package/apps/runtime/dist/docs/user/create-account.md +52 -0
  161. package/apps/runtime/dist/docs/user/data.md +66 -0
  162. package/apps/runtime/dist/docs/user/first-start.md +60 -0
  163. package/apps/runtime/dist/docs/user/index.md +49 -0
  164. package/apps/runtime/dist/docs/user/install.md +123 -0
  165. package/apps/runtime/dist/docs/user/local-only.md +42 -0
  166. package/apps/runtime/dist/docs/user/meta.json +32 -0
  167. package/apps/runtime/dist/docs/user/models.md +156 -0
  168. package/apps/runtime/dist/docs/user/plugins.md +71 -0
  169. package/apps/runtime/dist/docs/user/run.md +98 -0
  170. package/apps/runtime/dist/docs/user/server.md +52 -0
  171. package/apps/runtime/dist/docs/user/space.md +126 -0
  172. package/apps/runtime/dist/docs/user/steps.md +220 -0
  173. package/apps/runtime/dist/docs/user/test-and-share.md +224 -0
  174. package/apps/runtime/dist/docs/user/troubleshooting.md +67 -0
  175. package/apps/runtime/dist/docs/user/wordpress.md +121 -0
  176. package/apps/runtime/dist/documents/parse.js +246 -0
  177. package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
  178. package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
  179. package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
  180. package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
  181. package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
  182. package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
  183. package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
  184. package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
  185. package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
  186. package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
  187. package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
  188. package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
  189. package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
  190. package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
  191. package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
  192. package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +381 -0
  193. package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
  194. package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
  195. package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
  196. package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
  197. package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
  198. package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
  199. package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
  200. package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
  201. package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
  202. package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +492 -0
  203. package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
  204. package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
  205. package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
  206. package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
  207. package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
  208. package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
  209. package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
  210. package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
  211. package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
  212. package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
  213. package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
  214. package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
  215. package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
  216. package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
  217. package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
  218. package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
  219. package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
  220. package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
  221. package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
  222. package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
  223. package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
  224. package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
  225. package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
  226. package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
  227. package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
  228. package/apps/runtime/dist/engenty/shims/web-ingest.js +19 -0
  229. package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
  230. package/apps/runtime/dist/engine/asks.js +82 -0
  231. package/apps/runtime/dist/engine/decide.js +82 -0
  232. package/apps/runtime/dist/engine/device.js +194 -0
  233. package/apps/runtime/dist/engine/events.js +128 -0
  234. package/apps/runtime/dist/engine/input.js +208 -0
  235. package/apps/runtime/dist/engine/owner.js +32 -0
  236. package/apps/runtime/dist/engine/prepare.js +90 -0
  237. package/apps/runtime/dist/engine/push.js +58 -0
  238. package/apps/runtime/dist/engine/requirements.js +355 -0
  239. package/apps/runtime/dist/engine/resources.js +198 -0
  240. package/apps/runtime/dist/engine/runner.js +970 -0
  241. package/apps/runtime/dist/engine/steps.js +1000 -0
  242. package/apps/runtime/dist/engine/surface.js +91 -0
  243. package/apps/runtime/dist/engine/template.js +240 -0
  244. package/apps/runtime/dist/engine/types.js +4 -0
  245. package/apps/runtime/dist/env.js +279 -0
  246. package/apps/runtime/dist/features.js +123 -0
  247. package/apps/runtime/dist/files/blobs.js +22 -0
  248. package/apps/runtime/dist/files/objects.js +86 -0
  249. package/apps/runtime/dist/files/storage.js +120 -0
  250. package/apps/runtime/dist/files/web-images.js +101 -0
  251. package/apps/runtime/dist/film/project.js +257 -0
  252. package/apps/runtime/dist/film/skills.js +91 -0
  253. package/apps/runtime/dist/film/sounds.js +122 -0
  254. package/apps/runtime/dist/film/step.js +289 -0
  255. package/apps/runtime/dist/film/tools.js +99 -0
  256. package/apps/runtime/dist/geocode.js +58 -0
  257. package/apps/runtime/dist/harness/antigravity.js +129 -0
  258. package/apps/runtime/dist/harness/apple.js +270 -0
  259. package/apps/runtime/dist/harness/claude.js +147 -0
  260. package/apps/runtime/dist/harness/codex.js +229 -0
  261. package/apps/runtime/dist/harness/cursor.js +93 -0
  262. package/apps/runtime/dist/harness/env.js +124 -0
  263. package/apps/runtime/dist/harness/index.js +95 -0
  264. package/apps/runtime/dist/harness/model.js +279 -0
  265. package/apps/runtime/dist/harness/prompt.js +121 -0
  266. package/apps/runtime/dist/harness/terminal.js +194 -0
  267. package/apps/runtime/dist/harness/types.js +7 -0
  268. package/apps/runtime/dist/index.js +113 -0
  269. package/apps/runtime/dist/limits.js +81 -0
  270. package/apps/runtime/dist/link-preview.js +60 -0
  271. package/apps/runtime/dist/log.js +106 -0
  272. package/apps/runtime/dist/manage.js +249 -0
  273. package/apps/runtime/dist/mcp/auth.js +161 -0
  274. package/apps/runtime/dist/mcp/bridge.js +59 -0
  275. package/apps/runtime/dist/mcp/flow-app.js +97 -0
  276. package/apps/runtime/dist/mcp/handler.js +54 -0
  277. package/apps/runtime/dist/mcp/scopes.js +14 -0
  278. package/apps/runtime/dist/mcp/seen.js +65 -0
  279. package/apps/runtime/dist/mcp/tools.js +572 -0
  280. package/apps/runtime/dist/media/conversation.js +167 -0
  281. package/apps/runtime/dist/media/ffmpeg.js +121 -0
  282. package/apps/runtime/dist/media/generate.js +120 -0
  283. package/apps/runtime/dist/media/marking.js +121 -0
  284. package/apps/runtime/dist/media/transcribe.js +198 -0
  285. package/apps/runtime/dist/model-errors.js +42 -0
  286. package/apps/runtime/dist/models.js +1164 -0
  287. package/apps/runtime/dist/plugin.js +61 -0
  288. package/apps/runtime/dist/plugins/channels.js +78 -0
  289. package/apps/runtime/dist/plugins/claims.js +56 -0
  290. package/apps/runtime/dist/plugins/discovery.js +155 -0
  291. package/apps/runtime/dist/plugins/events.js +63 -0
  292. package/apps/runtime/dist/plugins/jobs.js +64 -0
  293. package/apps/runtime/dist/plugins/loader.js +508 -0
  294. package/apps/runtime/dist/plugins/migrations.js +69 -0
  295. package/apps/runtime/dist/plugins/pro-keys.js +34 -0
  296. package/apps/runtime/dist/plugins/pro-state.js +83 -0
  297. package/apps/runtime/dist/plugins/pro.js +310 -0
  298. package/apps/runtime/dist/plugins/public.js +44 -0
  299. package/apps/runtime/dist/plugins/registry.js +122 -0
  300. package/apps/runtime/dist/plugins/runs.js +210 -0
  301. package/apps/runtime/dist/render/browser-path.js +54 -0
  302. package/apps/runtime/dist/render/chromium.js +98 -0
  303. package/apps/runtime/dist/render/convert.js +158 -0
  304. package/apps/runtime/dist/render/downloads.js +299 -0
  305. package/apps/runtime/dist/render/guard.js +25 -0
  306. package/apps/runtime/dist/render/shaped.js +137 -0
  307. package/apps/runtime/dist/routes/api.js +440 -0
  308. package/apps/runtime/dist/routes/debug.js +42 -0
  309. package/apps/runtime/dist/routes/delivery.js +90 -0
  310. package/apps/runtime/dist/routes/internal.js +81 -0
  311. package/apps/runtime/dist/routes/lang.js +25 -0
  312. package/apps/runtime/dist/routes/marketplace.js +43 -0
  313. package/apps/runtime/dist/routes/plugin-public.js +113 -0
  314. package/apps/runtime/dist/routes/plugins.js +212 -0
  315. package/apps/runtime/dist/routes/runs.js +1422 -0
  316. package/apps/runtime/dist/routes/studio.js +1629 -0
  317. package/apps/runtime/dist/routes/wizard-stream.js +30 -0
  318. package/apps/runtime/dist/running.js +39 -0
  319. package/apps/runtime/dist/sandbox/agentos-package.js +73 -0
  320. package/apps/runtime/dist/sandbox/agentos.js +162 -0
  321. package/apps/runtime/dist/sandbox/docker.js +45 -0
  322. package/apps/runtime/dist/sandbox/index.js +52 -0
  323. package/apps/runtime/dist/sandbox/install.js +122 -0
  324. package/apps/runtime/dist/sandbox/mastra.js +71 -0
  325. package/apps/runtime/dist/sandbox/types.js +1 -0
  326. package/apps/runtime/dist/secrets/crypto.js +28 -0
  327. package/apps/runtime/dist/secrets/signing.js +52 -0
  328. package/apps/runtime/dist/secrets/vault.js +59 -0
  329. package/apps/runtime/dist/services/access.js +88 -0
  330. package/apps/runtime/dist/services/api.js +248 -0
  331. package/apps/runtime/dist/services/architect.js +95 -0
  332. package/apps/runtime/dist/services/assistant-chats.js +311 -0
  333. package/apps/runtime/dist/services/brand.js +85 -0
  334. package/apps/runtime/dist/services/cloud.js +456 -0
  335. package/apps/runtime/dist/services/draft-events.js +27 -0
  336. package/apps/runtime/dist/services/engenty-shapes.js +71 -0
  337. package/apps/runtime/dist/services/errors.js +29 -0
  338. package/apps/runtime/dist/services/files.js +178 -0
  339. package/apps/runtime/dist/services/knowledge-model.js +559 -0
  340. package/apps/runtime/dist/services/knowledge.js +721 -0
  341. package/apps/runtime/dist/services/links.js +16 -0
  342. package/apps/runtime/dist/services/marketplace.js +492 -0
  343. package/apps/runtime/dist/services/onboarding.js +152 -0
  344. package/apps/runtime/dist/services/package-file.js +82 -0
  345. package/apps/runtime/dist/services/package.js +170 -0
  346. package/apps/runtime/dist/services/profile.js +51 -0
  347. package/apps/runtime/dist/services/project-files.js +439 -0
  348. package/apps/runtime/dist/services/project-index.js +707 -0
  349. package/apps/runtime/dist/services/projects.js +387 -0
  350. package/apps/runtime/dist/services/relevance.js +58 -0
  351. package/apps/runtime/dist/services/runners.js +178 -0
  352. package/apps/runtime/dist/services/runs.js +524 -0
  353. package/apps/runtime/dist/services/shares.js +130 -0
  354. package/apps/runtime/dist/services/site.js +270 -0
  355. package/apps/runtime/dist/services/space-categories.js +861 -0
  356. package/apps/runtime/dist/services/space-data.js +792 -0
  357. package/apps/runtime/dist/services/space-origin.js +207 -0
  358. package/apps/runtime/dist/services/spaces.js +657 -0
  359. package/apps/runtime/dist/services/translations.js +306 -0
  360. package/apps/runtime/dist/services/widgets.js +34 -0
  361. package/apps/runtime/dist/services/wizard-icon.js +157 -0
  362. package/apps/runtime/dist/services/wizards.js +420 -0
  363. package/apps/runtime/dist/settings.js +16 -0
  364. package/apps/runtime/dist/store/errors.js +4 -0
  365. package/apps/runtime/dist/store/index.js +299 -0
  366. package/apps/runtime/dist/store/shared-lists.js +123 -0
  367. package/apps/runtime/dist/tenants/control.js +238 -0
  368. package/apps/runtime/dist/tenants/tenant.js +26 -0
  369. package/apps/runtime/dist/texts.js +207 -0
  370. package/apps/runtime/dist/tools/browser.js +703 -0
  371. package/apps/runtime/dist/tools/connector.js +204 -0
  372. package/apps/runtime/dist/tools/index.js +291 -0
  373. package/apps/runtime/dist/tools/mail.js +104 -0
  374. package/apps/runtime/dist/tools/net-guard.js +170 -0
  375. package/apps/runtime/dist/tools/pages.js +81 -0
  376. package/apps/runtime/dist/tools/plugin.js +180 -0
  377. package/apps/runtime/dist/tools/project.js +154 -0
  378. package/apps/runtime/dist/tools/shared.js +69 -0
  379. package/apps/runtime/dist/tools/store.js +238 -0
  380. package/apps/runtime/dist/update.js +113 -0
  381. package/apps/runtime/dist/widgets/bundle.js +99 -0
  382. package/apps/runtime/dist/widgets/render.js +329 -0
  383. package/apps/runtime/dist/widgets/runtime.js +108 -0
  384. package/apps/runtime/dist/widgets/script.js +30 -0
  385. package/apps/runtime/dist/widgets/step.js +223 -0
  386. package/apps/web/dist/ai-labels/ai-black.svg +20 -0
  387. package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
  388. package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
  389. package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
  390. package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
  391. package/apps/web/dist/ai-labels/ai-white.svg +21 -0
  392. package/apps/web/dist/assets/AboutDialog-DYgXPagX.js +1 -0
  393. package/apps/web/dist/assets/ChangelogDialog-BXqBW8j3.js +1 -0
  394. package/apps/web/dist/assets/ChatView-CK2--tsE.js +10 -0
  395. package/apps/web/dist/assets/EditorPage-jvcwyXbU.js +3 -0
  396. package/apps/web/dist/assets/FlowPreview-CusP7ci9.js +1 -0
  397. package/apps/web/dist/assets/Harness-BrP-ENHg.css +1 -0
  398. package/apps/web/dist/assets/Harness-CV9uYzbv.js +16 -0
  399. package/apps/web/dist/assets/NewWizardChannels-BakW5q6s.js +1 -0
  400. package/apps/web/dist/assets/NewWizardPage-BoA-LJuW.js +7 -0
  401. package/apps/web/dist/assets/OnboardingPage-DaD1XnOe.js +1 -0
  402. package/apps/web/dist/assets/ProjectPage-BFgk1DEv.js +1 -0
  403. package/apps/web/dist/assets/PublicRunner-C9MwEvxy.js +1 -0
  404. package/apps/web/dist/assets/ReadOnly-DC2Kp7l8.js +1 -0
  405. package/apps/web/dist/assets/RunDrawer-CY6NZSrI.js +4 -0
  406. package/apps/web/dist/assets/RunnerView-BT26XgcY.js +1 -0
  407. package/apps/web/dist/assets/SettingsPage-AFt9IDLt.js +1 -0
  408. package/apps/web/dist/assets/SetupPage-DtFYAbwJ.js +1 -0
  409. package/apps/web/dist/assets/SharePage-t2xwNS61.js +1 -0
  410. package/apps/web/dist/assets/changelog-CATQFxCQ.js +1 -0
  411. package/apps/web/dist/assets/engenty-eXZ-EAwd.js +1 -0
  412. package/apps/web/dist/assets/esm-DXeMu8Lb.js +7 -0
  413. package/apps/web/dist/assets/fluffy-engenty-ClRou7yu.js +456 -0
  414. package/apps/web/dist/assets/index-AfUa8xKD.css +1 -0
  415. package/apps/web/dist/assets/index-Drw7FMTQ.js +14 -0
  416. package/apps/web/dist/assets/jsx-runtime-BNakU3Ej.js +1 -0
  417. package/apps/web/dist/assets/lock-R_CfU0Xo.js +1 -0
  418. package/apps/web/dist/assets/oss-credits-cZ9NUaLZ.js +1 -0
  419. package/apps/web/dist/assets/pdfmake-Cx9scTbD.js +228 -0
  420. package/apps/web/dist/assets/phone-CgrIS4_-.js +1 -0
  421. package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
  422. package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
  423. package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
  424. package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  425. package/apps/web/dist/assets/settings-menu-2IMOOlLl.js +1 -0
  426. package/apps/web/dist/assets/ui-dzGvc3xk.js +137 -0
  427. package/apps/web/dist/assets/upload-BSuTilFF.js +1 -0
  428. package/apps/web/dist/assets/vfs_fonts-DllDSYt1.js +1 -0
  429. package/apps/web/dist/embed.js +266 -0
  430. package/apps/web/dist/favicon.svg +1 -0
  431. package/apps/web/dist/icons/apple-touch-icon.png +0 -0
  432. package/apps/web/dist/icons/icon-192.png +0 -0
  433. package/apps/web/dist/icons/icon-512.png +0 -0
  434. package/apps/web/dist/icons/icon-maskable-512.png +0 -0
  435. package/apps/web/dist/index.html +48 -0
  436. package/apps/web/dist/install.sh +577 -0
  437. package/apps/web/dist/manifest.webmanifest +21 -0
  438. package/apps/web/dist/mcp/flow.html +980 -0
  439. package/apps/web/dist/sw.js +15 -0
  440. package/bin/wizards.mjs +288 -0
  441. package/modules/README.md +15 -0
  442. package/node_modules/@engenty/wizards/dist/plugin/index.d.ts +1150 -0
  443. package/node_modules/@engenty/wizards/dist/plugin/index.js +4 -0
  444. package/node_modules/@engenty/wizards/dist/plugin/studio.d.ts +322 -0
  445. package/node_modules/@engenty/wizards/dist/plugin/studio.js +4 -0
  446. package/node_modules/@engenty/wizards/dist/plugin/thread.d.ts +174 -0
  447. package/node_modules/@engenty/wizards/dist/plugin/thread.js +821 -0
  448. package/node_modules/@engenty/wizards/package.json +11 -0
  449. package/node_modules/@engenty-wizards/shared/dist/assistant.d.ts +174 -0
  450. package/node_modules/@engenty-wizards/shared/dist/assistant.js +65 -0
  451. package/node_modules/@engenty-wizards/shared/dist/changes.d.ts +22 -0
  452. package/node_modules/@engenty-wizards/shared/dist/changes.js +61 -0
  453. package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
  454. package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
  455. package/node_modules/@engenty-wizards/shared/dist/decision.d.ts +76 -0
  456. package/node_modules/@engenty-wizards/shared/dist/decision.js +147 -0
  457. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +2922 -0
  458. package/node_modules/@engenty-wizards/shared/dist/definition.js +1920 -0
  459. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
  460. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
  461. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
  462. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
  463. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
  464. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +217 -0
  465. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
  466. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
  467. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
  468. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
  469. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
  470. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
  471. package/node_modules/@engenty-wizards/shared/dist/feature-flags.d.ts +78 -0
  472. package/node_modules/@engenty-wizards/shared/dist/feature-flags.js +70 -0
  473. package/node_modules/@engenty-wizards/shared/dist/features.d.ts +41 -0
  474. package/node_modules/@engenty-wizards/shared/dist/features.js +89 -0
  475. package/node_modules/@engenty-wizards/shared/dist/fit.d.ts +71 -0
  476. package/node_modules/@engenty-wizards/shared/dist/fit.js +382 -0
  477. package/node_modules/@engenty-wizards/shared/dist/json-schema.d.ts +14 -0
  478. package/node_modules/@engenty-wizards/shared/dist/json-schema.js +66 -0
  479. package/node_modules/@engenty-wizards/shared/dist/knowledge.d.ts +199 -0
  480. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +68 -0
  481. package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
  482. package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +107 -0
  483. package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
  484. package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +136 -0
  485. package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +227 -0
  486. package/node_modules/@engenty-wizards/shared/dist/marketplace.js +354 -0
  487. package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
  488. package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
  489. package/node_modules/@engenty-wizards/shared/dist/numbers.d.ts +24 -0
  490. package/node_modules/@engenty-wizards/shared/dist/numbers.js +54 -0
  491. package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +224 -0
  492. package/node_modules/@engenty-wizards/shared/dist/projects.js +319 -0
  493. package/node_modules/@engenty-wizards/shared/dist/providers.d.ts +101 -0
  494. package/node_modules/@engenty-wizards/shared/dist/providers.js +390 -0
  495. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +680 -0
  496. package/node_modules/@engenty-wizards/shared/dist/run.js +620 -0
  497. package/node_modules/@engenty-wizards/shared/dist/runner-texts.d.ts +546 -0
  498. package/node_modules/@engenty-wizards/shared/dist/runner-texts.js +523 -0
  499. package/node_modules/@engenty-wizards/shared/dist/runners.d.ts +93 -0
  500. package/node_modules/@engenty-wizards/shared/dist/runners.js +146 -0
  501. package/node_modules/@engenty-wizards/shared/dist/shape.d.ts +102 -0
  502. package/node_modules/@engenty-wizards/shared/dist/shape.js +318 -0
  503. package/node_modules/@engenty-wizards/shared/dist/space-data.d.ts +89 -0
  504. package/node_modules/@engenty-wizards/shared/dist/space-data.js +47 -0
  505. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +117 -0
  506. package/node_modules/@engenty-wizards/shared/dist/store.js +81 -0
  507. package/node_modules/@engenty-wizards/shared/dist/surface.d.ts +115 -0
  508. package/node_modules/@engenty-wizards/shared/dist/surface.js +442 -0
  509. package/node_modules/@engenty-wizards/shared/dist/texts.d.ts +45 -0
  510. package/node_modules/@engenty-wizards/shared/dist/texts.js +54 -0
  511. package/node_modules/@engenty-wizards/shared/dist/translation.d.ts +23 -0
  512. package/node_modules/@engenty-wizards/shared/dist/translation.js +154 -0
  513. package/node_modules/@engenty-wizards/shared/dist/wizard-package.d.ts +48 -0
  514. package/node_modules/@engenty-wizards/shared/dist/wizard-package.js +49 -0
  515. package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
  516. package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
  517. package/node_modules/@engenty-wizards/shared/package.json +10 -0
  518. package/package.json +77 -4
  519. package/plugin/.claude-plugin/plugin.json +9 -0
  520. package/plugin/.mcp.json +8 -0
  521. package/plugin/commands/wizard.md +8 -0
  522. package/plugin/skills/build-wizard/SKILL.md +27 -0
  523. package/plugin/skills/use-wizard/SKILL.md +18 -0
@@ -0,0 +1,1920 @@
1
+ // SPDX-License-Identifier: FSL-1.1-MIT OR MIT
2
+ import { z } from "zod";
3
+ import { tableColumnsSchema } from "./engenty/data-tables/index.js";
4
+ import { readNumber } from "./numbers.js";
5
+ import { outputShapeSchema, shapeOfFields } from "./shape.js";
6
+ import { CONNECTION_KINDS } from "./store.js";
7
+ import { surfaceComponentsSchema, validateSurface } from "./surface.js";
8
+ /** The engenty avatars a wizard can wear. Same cast as engenty's ui-core. */
9
+ export const ENGENTY_KINDS = [
10
+ "round",
11
+ "drop",
12
+ "dome",
13
+ "flame",
14
+ "oval",
15
+ "bean",
16
+ "pebble",
17
+ "sprout",
18
+ "tower",
19
+ "wedge",
20
+ ];
21
+ // --- languages -------------------------------------------------------------------------------
22
+ /**
23
+ * The languages engenty speaks: the studio, the runner, the mails, a wizard's own words. A
24
+ * language joins once its strings are written; English is the one every string has, and what a
25
+ * language still lacks is said in English.
26
+ */
27
+ export const LANGS = ["en", "de"];
28
+ /** Each language's name, in that language: what a switch or a menu shows. */
29
+ export const LANG_NAMES = { en: "English", de: "Deutsch" };
30
+ /** The locale amounts and dates are written in for a language. */
31
+ export const LOCALES = { en: "en-GB", de: "de-DE" };
32
+ /** The locale speech is heard and spoken in for a language (recognition, voices). */
33
+ export const SPEECH_LOCALES = { en: "en-US", de: "de-DE" };
34
+ export function isLang(value) {
35
+ return typeof value === "string" && LANGS.includes(value);
36
+ }
37
+ /** The language of a tag such as "de-AT", "fr" or "pt_BR", where engenty speaks it. */
38
+ export function langOf(tag) {
39
+ const code = tag?.trim().toLowerCase().split(/[-_]/)[0];
40
+ return isLang(code) ? code : null;
41
+ }
42
+ /**
43
+ * The main language of each European country. It holds languages engenty does not speak yet:
44
+ * `langOf` passes over them until they join `LANGS`.
45
+ */
46
+ const COUNTRY_LANGUAGE = {
47
+ AT: "de",
48
+ BE: "nl",
49
+ BG: "bg",
50
+ CH: "de",
51
+ CY: "el",
52
+ CZ: "cs",
53
+ DE: "de",
54
+ DK: "da",
55
+ EE: "et",
56
+ ES: "es",
57
+ FI: "fi",
58
+ FR: "fr",
59
+ GB: "en",
60
+ GR: "el",
61
+ HR: "hr",
62
+ HU: "hu",
63
+ IE: "en",
64
+ IS: "is",
65
+ IT: "it",
66
+ LI: "de",
67
+ LT: "lt",
68
+ LU: "fr",
69
+ LV: "lv",
70
+ MC: "fr",
71
+ MT: "en",
72
+ NL: "nl",
73
+ NO: "nb",
74
+ PL: "pl",
75
+ PT: "pt",
76
+ RO: "ro",
77
+ RS: "sr",
78
+ SE: "sv",
79
+ SI: "sl",
80
+ SK: "sk",
81
+ SM: "it",
82
+ UA: "uk",
83
+ VA: "it",
84
+ };
85
+ /**
86
+ * The languages a wizard is offered in when its author picks one: English, German and the main
87
+ * languages of Europe. A wizard may name any other real language by its code as well.
88
+ */
89
+ export const WIZARD_LANGS = [
90
+ ...new Set(["en", "de", ...Object.values(COUNTRY_LANGUAGE)]),
91
+ ];
92
+ /** The country of each European time zone (IANA names, as `Intl` gives them). */
93
+ const ZONE_COUNTRY = {
94
+ "Europe/Amsterdam": "NL",
95
+ "Europe/Athens": "GR",
96
+ "Europe/Belgrade": "RS",
97
+ "Europe/Berlin": "DE",
98
+ "Europe/Bratislava": "SK",
99
+ "Europe/Brussels": "BE",
100
+ "Europe/Bucharest": "RO",
101
+ "Europe/Budapest": "HU",
102
+ "Europe/Busingen": "DE",
103
+ "Europe/Copenhagen": "DK",
104
+ "Europe/Dublin": "IE",
105
+ "Europe/Helsinki": "FI",
106
+ "Europe/Kiev": "UA",
107
+ "Europe/Kyiv": "UA",
108
+ "Europe/Lisbon": "PT",
109
+ "Europe/Ljubljana": "SI",
110
+ "Europe/London": "GB",
111
+ "Europe/Luxembourg": "LU",
112
+ "Europe/Madrid": "ES",
113
+ "Europe/Malta": "MT",
114
+ "Europe/Monaco": "MC",
115
+ "Europe/Oslo": "NO",
116
+ "Europe/Paris": "FR",
117
+ "Europe/Prague": "CZ",
118
+ "Europe/Riga": "LV",
119
+ "Europe/Rome": "IT",
120
+ "Europe/San_Marino": "SM",
121
+ "Europe/Sofia": "BG",
122
+ "Europe/Stockholm": "SE",
123
+ "Europe/Tallinn": "EE",
124
+ "Europe/Vaduz": "LI",
125
+ "Europe/Vatican": "VA",
126
+ "Europe/Vienna": "AT",
127
+ "Europe/Vilnius": "LT",
128
+ "Europe/Warsaw": "PL",
129
+ "Europe/Zagreb": "HR",
130
+ "Europe/Zurich": "CH",
131
+ "Atlantic/Canary": "ES",
132
+ "Atlantic/Madeira": "PT",
133
+ "Atlantic/Reykjavik": "IS",
134
+ "Asia/Nicosia": "CY",
135
+ };
136
+ /**
137
+ * The person's own language beside English: the first of their languages (the browser's, in
138
+ * its order) that engenty speaks, else the main language of the country their time zone lies
139
+ * in; null where neither names one.
140
+ */
141
+ export function localLang(languages, timeZone) {
142
+ for (const tag of languages) {
143
+ const found = langOf(tag);
144
+ if (found && found !== "en") {
145
+ return found;
146
+ }
147
+ }
148
+ const country = timeZone ? ZONE_COUNTRY[timeZone] : undefined;
149
+ const found = country ? langOf(COUNTRY_LANGUAGE[country]) : null;
150
+ return found === "en" ? null : found;
151
+ }
152
+ /**
153
+ * A language a wizard can be translated into: any language, by its two- or three-letter code
154
+ * ("fr", "it", "pl"), not only those of `LANGS`.
155
+ */
156
+ export const LANG_CODE = /^[a-z]{2,3}$/;
157
+ /** The code of a tag such as "fr-CA" or "pt_BR"; null where it names no language. */
158
+ export function langCodeOf(tag) {
159
+ const code = tag?.trim().toLowerCase().split(/[-_]/)[0] ?? "";
160
+ return LANG_CODE.test(code) ? code : null;
161
+ }
162
+ /**
163
+ * A real language, as the platform's own language data knows it: "it" and "nb" are, "xx" is
164
+ * not. The schema only checks the shape of a code; this checks that it names a language.
165
+ */
166
+ export function isRealLanguage(code) {
167
+ if (!LANG_CODE.test(code)) {
168
+ return false;
169
+ }
170
+ try {
171
+ return Boolean(new Intl.DisplayNames(["en"], { type: "language", fallback: "none" }).of(code));
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ }
177
+ /** The language engenty's own strings are read in for any language: itself where they are written, else English. */
178
+ export function textLang(code) {
179
+ return isLang(code) ? code : "en";
180
+ }
181
+ /**
182
+ * The language the person reads, for a wizard translated on request: the first of their
183
+ * languages (the browser's, in its order), else the main language of the country their time
184
+ * zone lies in. Any language, not only one engenty's own strings are written in.
185
+ */
186
+ export function viewerLang(languages, timeZone) {
187
+ for (const tag of languages) {
188
+ const code = langCodeOf(tag);
189
+ if (code) {
190
+ return code;
191
+ }
192
+ }
193
+ const country = timeZone ? ZONE_COUNTRY[timeZone] : undefined;
194
+ return country ? langCodeOf(COUNTRY_LANGUAGE[country]) : null;
195
+ }
196
+ /** A language's name in that language, as a switch shows it: "Italiano", "Polski". */
197
+ export function langName(code) {
198
+ if (isLang(code)) {
199
+ return LANG_NAMES[code];
200
+ }
201
+ try {
202
+ const name = new Intl.DisplayNames([code], { type: "language" }).of(code) ?? code;
203
+ return name.charAt(0).toLocaleUpperCase(code) + name.slice(1);
204
+ }
205
+ catch {
206
+ return code.toUpperCase();
207
+ }
208
+ }
209
+ /** A language's name in English, for a model told what to write in: "Italian". */
210
+ export function langNameEn(code) {
211
+ try {
212
+ return new Intl.DisplayNames(["en"], { type: "language" }).of(code) ?? code;
213
+ }
214
+ catch {
215
+ return code;
216
+ }
217
+ }
218
+ /** The locale amounts, dates and plural forms are written in for any language. */
219
+ export function localeOf(code) {
220
+ return isLang(code) ? LOCALES[code] : code;
221
+ }
222
+ /** The locale speech is recognised and read aloud in for any language. */
223
+ export function speechLocaleOf(code) {
224
+ return isLang(code) ? SPEECH_LOCALES[code] : code;
225
+ }
226
+ export const FIELD_KINDS = [
227
+ "text",
228
+ "textarea",
229
+ "number",
230
+ "select",
231
+ "multiselect",
232
+ "date",
233
+ "email",
234
+ "url",
235
+ "toggle",
236
+ "color",
237
+ "image",
238
+ "file",
239
+ "items",
240
+ "connection",
241
+ "list",
242
+ "location",
243
+ "audio",
244
+ "signature",
245
+ "slot",
246
+ ];
247
+ export const FIELD_WAYS = {
248
+ text: "typed",
249
+ textarea: "typed",
250
+ number: "typed",
251
+ date: "typed",
252
+ email: "typed",
253
+ url: "typed",
254
+ select: "tapped",
255
+ multiselect: "tapped",
256
+ slot: "tapped",
257
+ toggle: "tapped",
258
+ color: "value",
259
+ items: "value",
260
+ location: "value",
261
+ image: "device",
262
+ file: "device",
263
+ audio: "device",
264
+ signature: "device",
265
+ connection: "device",
266
+ list: "device",
267
+ };
268
+ export function isLocationValue(v) {
269
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
270
+ return false;
271
+ }
272
+ const { lat, lng, label } = v;
273
+ return ((typeof lat === "number" && typeof lng === "number") ||
274
+ (typeof label === "string" && label.trim().length > 0));
275
+ }
276
+ export function isAudioValue(v) {
277
+ return Boolean(v &&
278
+ typeof v === "object" &&
279
+ typeof v.asset === "string" &&
280
+ v.asset);
281
+ }
282
+ /** A position the way a person reads it: "Hauptplatz 1, Graz (47.07071, 15.43950, ±12 m)". */
283
+ export function locationText(v) {
284
+ const point = typeof v.lat === "number" && typeof v.lng === "number"
285
+ ? `${v.lat.toFixed(5)}, ${v.lng.toFixed(5)}${v.accuracy ? `, ±${Math.round(v.accuracy)} m` : ""}`
286
+ : "";
287
+ const label = v.label?.trim() ?? "";
288
+ return label && point ? `${label} (${point})` : label || point;
289
+ }
290
+ /** A field takes at most this many files. */
291
+ export const MAX_FILES = 30;
292
+ export const TOOL_IDS = [
293
+ "web_search",
294
+ "web_fetch",
295
+ "browser",
296
+ "sandbox",
297
+ "image",
298
+ "http",
299
+ "pages",
300
+ ];
301
+ /**
302
+ * A tool a plugin of the runtime adds: `<plugin id>.<tool name>`. A wizard that names one runs
303
+ * only where that plugin is installed.
304
+ */
305
+ export const PLUGIN_ID = /^[a-z][a-z0-9-]{0,39}$/;
306
+ export const PLUGIN_TOOL_NAME = /^[a-z][a-z0-9_]{0,39}$/;
307
+ /** The plugin and the tool a step's tool id names, or null for one of the built-in tools. */
308
+ export function pluginToolOf(id) {
309
+ const [plugin, tool, ...rest] = id.split(".");
310
+ return tool !== undefined &&
311
+ rest.length === 0 &&
312
+ PLUGIN_ID.test(plugin) &&
313
+ PLUGIN_TOOL_NAME.test(tool)
314
+ ? { plugin, tool }
315
+ : null;
316
+ }
317
+ /**
318
+ * The kinds of model a step can ask for. A step names a class; which model serves it is bound
319
+ * outside the wizard (the model-gateway, or the settings of a runtime that runs alone).
320
+ */
321
+ export const TEXT_CLASSES = ["classifier", "standard", "high", "highest"];
322
+ export const MODEL_CLASSES = [...TEXT_CLASSES, "image", "video", "audio", "speech"];
323
+ /** How hard the model should think, where the bound model can be told. */
324
+ export const EFFORTS = ["low", "medium", "high"];
325
+ export const ASSET_KINDS = ["image", "video", "voice", "document", "dashboard"];
326
+ export const FORMATS = [
327
+ "png",
328
+ "mp4",
329
+ "mp3",
330
+ "pdf",
331
+ "docx",
332
+ "html",
333
+ "md",
334
+ "txt",
335
+ "csv",
336
+ "xlsx",
337
+ "json",
338
+ "zip",
339
+ ];
340
+ export const ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:5", "3:2", "2:3"];
341
+ /** What a video model renders at: 720p unless the step asks for less. Never more. */
342
+ export const VIDEO_RESOLUTIONS = ["720p", "480p"];
343
+ const id = z
344
+ .string()
345
+ .regex(/^[a-zA-Z][a-zA-Z0-9_]{0,40}$/, "ids are letters, digits and _ and start with a letter");
346
+ export const itemColumnSchema = z.object({
347
+ id,
348
+ label: z.string().min(1),
349
+ kind: z.enum(["text", "number", "money"]),
350
+ });
351
+ export const CONDITION_OPS = [
352
+ "equals",
353
+ "notEquals",
354
+ "in",
355
+ "notEmpty",
356
+ "empty",
357
+ "gt",
358
+ "lt",
359
+ "contains",
360
+ ];
361
+ /**
362
+ * One test on what the run knows: an answer by field id, an output of an agent step as
363
+ * "steps.<step>.<field>", or how many rows a stored list has as "lists.<list>.count".
364
+ */
365
+ export const conditionSchema = z.object({
366
+ field: z.string(),
367
+ op: z.enum(CONDITION_OPS),
368
+ value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]).optional(),
369
+ });
370
+ /** One condition, or several that must all hold. */
371
+ export const whenSchema = z.union([conditionSchema, z.array(conditionSchema).min(1)]);
372
+ /** What a card of a choice shows: the columns of the table the choices come from. */
373
+ export const cardsSchema = z.object({
374
+ label: z.string().min(1),
375
+ value: z.string().optional(),
376
+ image: z.string().optional(),
377
+ subtitle: z.string().optional(),
378
+ meta: z.string().optional(),
379
+ link: z.string().optional(),
380
+ });
381
+ export const fieldSchema = z.object({
382
+ id,
383
+ label: z.string().min(1),
384
+ kind: z.enum(FIELD_KINDS),
385
+ required: z.boolean().optional(),
386
+ placeholder: z.string().optional(),
387
+ help: z.string().optional(),
388
+ /**
389
+ * Choices for select / multiselect. slot: the appointment times offered, as ISO 8601 date-times
390
+ * with their offset ("2026-10-12T09:00:00+02:00").
391
+ */
392
+ options: z.array(z.string()).optional(),
393
+ default: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]).optional(),
394
+ /** Line-item columns for `items`. A row's amount is the product of its number and money columns. */
395
+ columns: z.array(itemColumnSchema).optional(),
396
+ /** VAT for `items`: a fixed rate in percent, or the id of a number/select field holding it. */
397
+ vat: z.object({ rate: z.number().optional(), field: z.string().optional() }).optional(),
398
+ currency: z.string().optional(),
399
+ /** image / file: several files; the value is then a list, in the order the person put them. */
400
+ multiple: z.boolean().optional(),
401
+ /** image / file with `multiple`: how many files at least and at most (up to 30). */
402
+ min: z.number().int().min(1).max(MAX_FILES).optional(),
403
+ max: z.number().int().min(1).max(MAX_FILES).optional(),
404
+ /** file: also offer the camera, for documents the person only has on paper. Images always offer it. */
405
+ camera: z.boolean().optional(),
406
+ /** file: the camera also records short video clips. */
407
+ video: z.boolean().optional(),
408
+ /**
409
+ * file: a recording (video or audio) is listened to before the next step runs — what is said,
410
+ * piece by piece between pauses, with exact times. Read as {{field.speech}}.
411
+ */
412
+ listen: z.boolean().optional(),
413
+ /** text: also fill it by scanning a QR code or barcode with the camera. */
414
+ scan: z.boolean().optional(),
415
+ /** connection: id of the wizard connection the person connects here. */
416
+ connection: z.string().optional(),
417
+ /** list: id of the wizard list shown here for the person to check and correct. */
418
+ list: z.string().optional(),
419
+ /**
420
+ * What the field starts with, from an earlier step: "steps.<id>" or "steps.<id>.<key>". An
421
+ * `items` field takes a table whose columns are its column ids; the person corrects it.
422
+ */
423
+ prefill: z.string().optional(),
424
+ /**
425
+ * The field is shown only while this holds. It reads an answer of this page (decided as the
426
+ * person types) or anything known before the page. A hidden field is not asked and not kept.
427
+ */
428
+ when: whenSchema.optional(),
429
+ /**
430
+ * select / multiselect / slot: the choices come from earlier data — a list output
431
+ * ("steps.<id>.<key>"), a column of a table output ("steps.<id>.<key>.<column>") or a column
432
+ * of a stored list ("lists.<id>.<column>"). `options` stand in while that data is empty.
433
+ */
434
+ optionsFrom: z.string().optional(),
435
+ /**
436
+ * select / multiselect with `optionsFrom` naming a table output ("steps.<id>.<key>"): each row
437
+ * is a choice drawn as a card. The columns that hold what the card shows; `value` (default:
438
+ * `label`) is what the answer holds, `link` a page the card opens.
439
+ */
440
+ cards: cardsSchema.optional(),
441
+ /**
442
+ * The field is shown when a decision, made as the run reaches the page, finds this statement
443
+ * true of the run ("The complaint is about a damaged article").
444
+ */
445
+ ask: z.string().min(1).optional(),
446
+ /** The id of one of the page's `groups`: of a group's fields a decision shows one (or none). */
447
+ group: z.string().optional(),
448
+ });
449
+ export const fieldGroupSchema = z.object({
450
+ id,
451
+ /** What the decision picks: "How should we reach the person?" */
452
+ instructions: z.string().min(1),
453
+ /** True when showing none of the group's fields is a valid answer. */
454
+ optional: z.boolean().optional(),
455
+ });
456
+ export const nextRuleSchema = z
457
+ .object({
458
+ /** The rule holds when this holds … */
459
+ when: whenSchema.optional(),
460
+ /**
461
+ * … or, instead, when a decision finds this statement true of the run ("The person wants a
462
+ * refund"). All `ask` rules of a step are decided together, after no `when` rule matched.
463
+ */
464
+ ask: z.string().min(1).optional(),
465
+ /**
466
+ * A step id, or "end". A rule with neither `when` nor `ask` is the step's "otherwise": where
467
+ * the run goes when no other rule holds, in place of the next step in the list.
468
+ */
469
+ goto: z.string(),
470
+ /**
471
+ * How often this rule may send the run on in one run; afterwards it is skipped. Meant for a
472
+ * branch back to an earlier step (a loop), which without it is taken at most ten times.
473
+ */
474
+ max: z.number().int().min(1).max(50).optional(),
475
+ })
476
+ .refine((rule) => rule.when === undefined || rule.ask === undefined, {
477
+ message: 'A branch has "when" or "ask", not both.',
478
+ });
479
+ const stepBase = {
480
+ id,
481
+ title: z.string().min(1),
482
+ description: z.string().optional(),
483
+ next: z.array(nextRuleSchema).optional(),
484
+ };
485
+ export const outputFieldSchema = z
486
+ .object({
487
+ id,
488
+ kind: z.enum(["text", "number", "list", "table", "yesno", "choice", "score"]),
489
+ /** What the field holds; for `yesno`, `choice` and `score` the question it answers. */
490
+ description: z.string().optional(),
491
+ /** Table only: the exact column keys every row has — what a widget reads. */
492
+ columns: z.array(z.string().min(1)).optional(),
493
+ /** Choice: the options, one of which is the answer. Score: the levels, lowest first. */
494
+ options: z.array(z.string().min(1)).min(2).max(40).optional(),
495
+ })
496
+ .refine((field) => (field.kind !== "choice" && field.kind !== "score") || Boolean(field.options?.length), { message: "A choice or score field lists its options.", path: ["options"] })
497
+ .refine((field) => field.kind !== "score" || (field.options?.length ?? 0) <= 10, {
498
+ message: "A score has at most ten levels.",
499
+ path: ["options"],
500
+ });
501
+ export const pageStepSchema = z.object({
502
+ ...stepBase,
503
+ type: z.literal("page"),
504
+ fields: z.array(fieldSchema).min(1),
505
+ /** Fields of which a decision shows one: see `group` on a field. */
506
+ groups: z.array(fieldGroupSchema).max(6).optional(),
507
+ cta: z.string().optional(),
508
+ });
509
+ /** A plugin's tool as a step names it: `<plugin>.<tool>`. */
510
+ const pluginToolIdSchema = z.templateLiteral([
511
+ z.string().regex(PLUGIN_ID),
512
+ ".",
513
+ z.string().regex(PLUGIN_TOOL_NAME),
514
+ ]);
515
+ export const agentStepSchema = z.object({
516
+ ...stepBase,
517
+ type: z.literal("agent"),
518
+ /** What the agent does. A template: {{field}}, {{steps.id}}, {{brand.name}}. Empty with `call`. */
519
+ instructions: z.string().default(""),
520
+ /**
521
+ * One tool of a plugin, called once with these inputs and no model: its answer is the step's
522
+ * json output, as it is. For data a later page shows unchanged (free appointment times), where
523
+ * a model would only copy it. String inputs are templates; a filled-in whole number or
524
+ * true/false is passed as one, an empty one is left out.
525
+ */
526
+ call: z
527
+ .object({
528
+ tool: pluginToolIdSchema,
529
+ input: z.record(z.string(), z.unknown()).default({}),
530
+ })
531
+ .optional(),
532
+ tools: z.array(z.union([z.enum(TOOL_IDS), pluginToolIdSchema])).default([]),
533
+ /** Ids of project MCP servers this step may use. */
534
+ mcp: z.array(z.string()).optional(),
535
+ /** Ids of wizard connections (the person's accounts) this step may use. */
536
+ connections: z.array(z.string()).optional(),
537
+ output: z
538
+ .object({
539
+ format: z.enum(["text", "markdown", "json"]),
540
+ /** The short form: one level of keys. A step has `fields` or `schema`, not both. */
541
+ fields: z.array(outputFieldSchema).optional(),
542
+ /**
543
+ * json only: nested data (lists of entries with lists of their own), in the kinds of
544
+ * `shape.ts`. Objects nest at most four deep, a hundred keys in all.
545
+ */
546
+ schema: outputShapeSchema.optional(),
547
+ /**
548
+ * json only: a view of the result from the surface catalog, bound to the step's own fields;
549
+ * reviews and the result show it instead of a plain table. Components: written once, the
550
+ * same every run, nothing to pay. `true`: a model composes one each run.
551
+ */
552
+ surface: z.union([z.boolean(), surfaceComponentsSchema]).optional(),
553
+ })
554
+ .default({ format: "markdown" }),
555
+ /** The kind of model the step needs; default `high`. The model itself is bound elsewhere. */
556
+ model: z.enum(TEXT_CLASSES).optional(),
557
+ effort: z.enum(EFFORTS).optional(),
558
+ /** Shown to the person while it works. */
559
+ /**
560
+ * One run of the step per entry, at most twenty: the rows of a table output
561
+ * ("steps.<id>.<key>"), the entries of a list output, or the rows of a stored list
562
+ * ("lists.<id>"). The instructions read the entry as {{item.<column>}}, {{index}}, {{count}}.
563
+ * The output is a table: the entry's columns and the step's output fields, one row per entry.
564
+ */
565
+ each: z.string().optional(),
566
+ working: z.string().optional(),
567
+ });
568
+ export const generateStepSchema = z.object({
569
+ ...stepBase,
570
+ type: z.literal("generate"),
571
+ asset: z.enum(ASSET_KINDS),
572
+ /** For image/video the visual prompt; for document/dashboard the brief. A template. */
573
+ prompt: z.string().min(1),
574
+ options: z
575
+ .object({
576
+ aspectRatio: z.enum(ASPECT_RATIOS).optional(),
577
+ /** Video length in seconds. */
578
+ duration: z.number().optional(),
579
+ /** Video: the height the model renders at; default 720p, the most there is. */
580
+ resolution: z.enum(VIDEO_RESOLUTIONS).optional(),
581
+ style: z.string().optional(),
582
+ /** Document flavour, steers the layout. */
583
+ template: z.enum(["invoice", "offer", "briefing", "letter", "report", "free"]).optional(),
584
+ })
585
+ .optional(),
586
+ /**
587
+ * The image the image/video model starts from: an earlier image field, or an earlier step
588
+ * that made images.
589
+ */
590
+ referenceImage: z.string().optional(),
591
+ /**
592
+ * Image / video: one result per entry instead of one. Names an image field with several
593
+ * uploads, an earlier step that made several images, or a table of an earlier agent step
594
+ * ("steps.<id>.<key>"). The prompt reads the entry as {{item.<column>}}, {{index}}, {{count}};
595
+ * `referenceImage` gives each entry its own image when it holds as many.
596
+ */
597
+ each: z.string().optional(),
598
+ /** For document/dashboard: the kind of model that writes it; default `high`. */
599
+ model: z.enum(TEXT_CLASSES).optional(),
600
+ effort: z.enum(EFFORTS).optional(),
601
+ working: z.string().optional(),
602
+ });
603
+ export const widgetStepSchema = z.object({
604
+ ...stepBase,
605
+ type: z.literal("widget"),
606
+ /** Workspace path of the widget's HTML. Written once; every run only brings new data. */
607
+ entry: z.string().min(1),
608
+ /** What the widget gets as `wizard.data`: key → a field id, steps.id, steps.id.key, brand.name or today. */
609
+ data: z.record(z.string(), z.string()).default({}),
610
+ /** Workspace path of example data (JSON) for previews before any run. */
611
+ sample: z.string().optional(),
612
+ /**
613
+ * How the run shows it: `fixed` (default) at `size`, scaled to the page; `page` as a part of
614
+ * the page, as wide as it and as tall as its content — a result people scroll and use, not a
615
+ * picture. PNG/PDF/MP4 still render at `size`.
616
+ */
617
+ layout: z.enum(["fixed", "page"]).optional(),
618
+ /** The size the widget is designed for; previews scale it, PNG/PDF/MP4 render at it. */
619
+ size: z
620
+ .object({
621
+ width: z.number().int().min(200).max(3840),
622
+ height: z.number().int().min(200).max(3840),
623
+ })
624
+ .optional(),
625
+ /**
626
+ * The result is a film: the timeline is rendered to an MP4 with sound when the step runs,
627
+ * and that video is what the person sees and downloads.
628
+ */
629
+ video: z.boolean().optional(),
630
+ working: z.string().optional(),
631
+ });
632
+ /** Formats a film step makes; the composition is drawn for this frame. */
633
+ export const FILM_FORMATS = ["16:9", "9:16", "1:1"];
634
+ export const FILM_SIZES = {
635
+ "16:9": { width: 1920, height: 1080 },
636
+ "9:16": { width: 1080, height: 1920 },
637
+ "1:1": { width: 1080, height: 1080 },
638
+ };
639
+ /**
640
+ * A film the AI client installed on this machine makes in a folder of its own: it writes the
641
+ * composition as code (HTML, CSS, JS) with the skills it is given, looks at its frames and fixes
642
+ * them; the runtime renders the MP4. It runs on the person's subscription, never on credits.
643
+ */
644
+ export const filmStepSchema = z.object({
645
+ ...stepBase,
646
+ type: z.literal("film"),
647
+ /** What the film is: story, look, what to use. A template. */
648
+ brief: z.string().min(1),
649
+ /**
650
+ * Skill packages the client reads: platform packages ("hyperframes") or a folder of the
651
+ * workspace with a SKILL.md ("skills/<name>").
652
+ */
653
+ skills: z.array(z.string().min(1)).max(12).default(["hyperframes"]),
654
+ /**
655
+ * A style kit in the workspace — a showreel made once with its rules, helpers and stills, as
656
+ * a .zip — unpacked as `showcase/`: the bar every film of this wizard is made to.
657
+ */
658
+ kit: z.string().optional(),
659
+ /**
660
+ * What the film is made from: name → a field id or "steps.<id>". Files (uploads, voice lines,
661
+ * pictures) are copied into `input/<name>/`, text is written to `input/<name>.md`.
662
+ */
663
+ inputs: z.record(z.string(), z.string()).default({}),
664
+ format: z.enum(FILM_FORMATS).default("16:9"),
665
+ /** About how long the film runs, in seconds. */
666
+ seconds: z.number().int().min(3).max(600).optional(),
667
+ /** The kind of model; default `standard` — the kit carries the craft. */
668
+ model: z.enum(TEXT_CLASSES).optional(),
669
+ effort: z.enum(EFFORTS).optional(),
670
+ working: z.string().optional(),
671
+ });
672
+ /** A piece a decided surface may show: its components (the first has the piece's id) and what it shows. */
673
+ export const surfaceCandidateSchema = z.object({
674
+ id,
675
+ /** What this piece shows, for the decision: "A chart of prices per supplier". */
676
+ description: z.string().min(1),
677
+ components: surfaceComponentsSchema,
678
+ /** Pieces of a group exclude each other: the decision shows one (or none, if optional). */
679
+ group: z.string().optional(),
680
+ /** Always shown: a title, the main list. */
681
+ required: z.boolean().optional(),
682
+ });
683
+ export const surfaceStepSchema = z
684
+ .object({
685
+ ...stepBase,
686
+ type: z.literal("surface"),
687
+ /** A view from the surface catalog, written once like a widget; every run brings new data. */
688
+ components: surfaceComponentsSchema.optional(),
689
+ /**
690
+ * Instead of `components`: pieces a decision picks from per run (no chart when there is
691
+ * nothing to chart), drawn in this order under one column.
692
+ */
693
+ candidates: z.array(surfaceCandidateSchema).max(16).optional(),
694
+ groups: z.array(fieldGroupSchema).max(6).optional(),
695
+ /** What the surface reads as data: key → a field id, steps.id, steps.id.key, lists.id, brand.name or today. */
696
+ data: z.record(z.string(), z.string()).default({}),
697
+ working: z.string().optional(),
698
+ })
699
+ .refine((step) => Boolean(step.components) !== Boolean(step.candidates), {
700
+ message: 'A surface step has either "components" or "candidates".',
701
+ });
702
+ /**
703
+ * The components of a decided surface for the pieces kept: one root column holding them in
704
+ * their order. Every piece kept is the same as all of them for the validator.
705
+ */
706
+ export function assembleSurface(candidates, kept) {
707
+ const keep = new Set(kept);
708
+ const chosen = candidates.filter((c) => keep.has(c.id));
709
+ return [
710
+ { id: "root", component: "Column", children: chosen.map((c) => c.components[0]?.id ?? c.id) },
711
+ ...chosen.flatMap((c) => c.components),
712
+ ];
713
+ }
714
+ export const reviewStepSchema = z.object({
715
+ ...stepBase,
716
+ type: z.literal("review"),
717
+ /** Step ids whose output is shown. */
718
+ show: z.array(z.string()).min(1),
719
+ edit: z.boolean().optional(),
720
+ regenerate: z.boolean().optional(),
721
+ });
722
+ export const deliverableSchema = z.object({
723
+ from: z.string(),
724
+ label: z.string().optional(),
725
+ formats: z.array(z.enum(FORMATS)).min(1),
726
+ });
727
+ export const resultStepSchema = z.object({
728
+ ...stepBase,
729
+ type: z.literal("result"),
730
+ message: z.string().optional(),
731
+ deliverables: z.array(deliverableSchema),
732
+ });
733
+ export const stepSchema = z.discriminatedUnion("type", [
734
+ pageStepSchema,
735
+ agentStepSchema,
736
+ generateStepSchema,
737
+ widgetStepSchema,
738
+ surfaceStepSchema,
739
+ filmStepSchema,
740
+ reviewStepSchema,
741
+ resultStepSchema,
742
+ ]);
743
+ /**
744
+ * A decision: a step on the classifier class that uses no tools and whose output is only
745
+ * yes/no and choice fields. It is answered in one call, which a model that only decides
746
+ * (no text) can take.
747
+ */
748
+ export function isDecisionStep(step) {
749
+ const fields = step.output.fields ?? [];
750
+ return (!step.call &&
751
+ !step.output.schema &&
752
+ step.model === "classifier" &&
753
+ step.output.format === "json" &&
754
+ fields.length > 0 &&
755
+ fields.every((f) => f.kind === "yesno" || f.kind === "choice" || f.kind === "score") &&
756
+ !step.each &&
757
+ step.tools.length === 0 &&
758
+ !step.mcp?.length &&
759
+ !step.connections?.length);
760
+ }
761
+ /**
762
+ * What an agent step's json output holds: its schema, or its fields as one. Undefined when it
763
+ * names neither (the model then returns one table).
764
+ */
765
+ export function declaredShape(step) {
766
+ if (step.output.schema) {
767
+ return step.output.schema;
768
+ }
769
+ return step.output.fields?.length ? shapeOfFields(step.output.fields) : undefined;
770
+ }
771
+ /** What an agent step's json output holds: its declared shape, or else one table. */
772
+ export function stepShape(step) {
773
+ return (declaredShape(step) ?? {
774
+ kind: "object",
775
+ fields: { data: { kind: "table", description: "The result as a table" } },
776
+ });
777
+ }
778
+ /** The top-level keys of an agent step's output: what `steps.<id>.<key>` may name. */
779
+ export function outputKeys(step) {
780
+ return Object.keys(declaredShape(step)?.fields ?? {});
781
+ }
782
+ export const listSchema = z.object({
783
+ id,
784
+ title: z.string().min(1),
785
+ description: z.string().optional(),
786
+ /** engenty data-table columns: `{ id, name, type, format }`. */
787
+ columns: tableColumnsSchema,
788
+ /** Column that identifies a row; saving a row with a known key updates it. */
789
+ key: z.string().optional(),
790
+ /**
791
+ * The person goes through the rows one by one in a review: `file` is the column holding the
792
+ * path of a kept file, shown beside the row; `status` a select column whose options are the
793
+ * answers — the first one means "not looked at yet".
794
+ */
795
+ check: z.object({ file: z.string().optional(), status: z.string().optional() }).optional(),
796
+ /**
797
+ * The list is the wizard's, not one person's: every run reads and writes the same rows, and
798
+ * the studio shows it under Space → Data. It is never shown to the person running the wizard,
799
+ * who would see what others gave.
800
+ */
801
+ shared: z.boolean().optional(),
802
+ });
803
+ export const connectionSchema = z.object({
804
+ id,
805
+ /** A built-in kind of account … */
806
+ kind: z.enum(CONNECTION_KINDS).optional(),
807
+ /** … or the id of one of the project's imported connectors. */
808
+ connector: z.string().optional(),
809
+ /** Imported connectors: the actions steps may call. Default: every action that only reads. */
810
+ actions: z.array(z.string()).max(200).optional(),
811
+ /** Imported connectors, per action: "allow" runs it without asking, "ask" asks the person first. */
812
+ policy: z.record(z.string(), z.enum(["allow", "ask"])).optional(),
813
+ title: z.string().optional(),
814
+ description: z.string().optional(),
815
+ });
816
+ /** The version of the definition this app writes and reads; raised only by a breaking change. */
817
+ export const DEFINITION_VERSION = 1;
818
+ export const wizardSchema = z.object({
819
+ version: z.literal(DEFINITION_VERSION).default(DEFINITION_VERSION),
820
+ title: z.string().min(1),
821
+ description: z.string().default(""),
822
+ avatar: z.enum(ENGENTY_KINDS).default("round"),
823
+ /** Shown on the first page above the first question. */
824
+ intro: z.string().optional(),
825
+ /**
826
+ * The language the wizard speaks to people: its texts, the runner's words, its numbers and
827
+ * dates, by its two-letter code ("en", "de", "it"). Any language, not only those engenty's own
828
+ * strings are written in; `validateWizard` checks it names a real one. Without one it is read
829
+ * from the wizard's own words (`wizardLang`).
830
+ */
831
+ lang: z
832
+ .string()
833
+ .regex(/^[a-z]{2}$/)
834
+ .optional(),
835
+ /** Tabular data the wizard keeps between runs, per person — or for all runs (`shared`). */
836
+ lists: z.array(listSchema).max(12).optional(),
837
+ /** Accounts the person connects; kept between runs, per person. */
838
+ connections: z.array(connectionSchema).max(12).optional(),
839
+ steps: z.array(stepSchema).min(1),
840
+ });
841
+ export const INTERACTIVE = new Set(["page", "review", "result"]);
842
+ /** Which formats each producing step can be downloaded in. */
843
+ export function formatsFor(step) {
844
+ if (step.type === "generate") {
845
+ switch (step.asset) {
846
+ case "image":
847
+ return step.each ? ["png", "zip"] : ["png"];
848
+ case "video":
849
+ return step.each ? ["mp4", "zip"] : ["mp4"];
850
+ case "voice":
851
+ return ["mp3"];
852
+ case "document":
853
+ return ["pdf", "docx", "html", "md", "png"];
854
+ case "dashboard":
855
+ return ["html", "pdf", "png"];
856
+ }
857
+ }
858
+ if (step.type === "film") {
859
+ return ["mp4"];
860
+ }
861
+ if (step.type === "widget") {
862
+ return step.video ? ["mp4", "png"] : ["html", "png", "pdf", "mp4", "json"];
863
+ }
864
+ if (step.type === "surface") {
865
+ return ["json"];
866
+ }
867
+ if (step.type === "agent") {
868
+ if (step.output.format === "json") {
869
+ // Nested data reads as a document too.
870
+ return step.output.schema
871
+ ? ["json", "csv", "xlsx", "md", "pdf", "zip"]
872
+ : ["json", "csv", "xlsx", "md", "zip"];
873
+ }
874
+ return ["md", "txt", "docx", "pdf", "html", "zip"];
875
+ }
876
+ return [];
877
+ }
878
+ /** Formats a stored list can be downloaded in; zip = the kept files its rows name, with the list. */
879
+ export const LIST_FORMATS = ["xlsx", "csv", "json", "md", "zip"];
880
+ const LIST_REF = /^lists\.([a-zA-Z][a-zA-Z0-9_]*)$/;
881
+ /** "lists.providers" → "providers"; anything else → null. */
882
+ export function listRef(ref) {
883
+ return ref.match(LIST_REF)?.[1] ?? null;
884
+ }
885
+ export const DEFAULT_WIDGET_SIZE = { width: 1280, height: 720 };
886
+ export function widgetSize(step) {
887
+ return step.size ?? DEFAULT_WIDGET_SIZE;
888
+ }
889
+ /** A widget data reference without braces: "{{steps.x}}" and "steps.x" mean the same. */
890
+ export function dataRef(raw) {
891
+ return raw
892
+ .trim()
893
+ .replace(/^\{\{\s*/, "")
894
+ .replace(/\s*\}\}$/, "");
895
+ }
896
+ const TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
897
+ export function templateRefs(template) {
898
+ return [...template.matchAll(TEMPLATE_RE)].map((m) => m[1]);
899
+ }
900
+ /**
901
+ * Structural checks the schema cannot express: ids, references between steps,
902
+ * templates naming things that exist by the time the step runs.
903
+ */
904
+ export function validateWizard(def, files) {
905
+ const issues = [];
906
+ const stepIds = new Set();
907
+ const fieldIds = new Set();
908
+ const seenSteps = new Set();
909
+ const seenFields = new Set();
910
+ const listIds = new Set();
911
+ const sharedLists = new Set();
912
+ const connectionIds = new Set();
913
+ const notShown = (id) => `List "${id}" is shared: every run writes it, so the person running the wizard never sees it.`;
914
+ if (def.lang && !isRealLanguage(def.lang)) {
915
+ issues.push({
916
+ message: `"${def.lang}" isn't a language code. Use a two-letter code such as "en", "de" or "it".`,
917
+ });
918
+ }
919
+ for (const list of def.lists ?? []) {
920
+ if (listIds.has(list.id)) {
921
+ issues.push({ message: `Duplicate list id "${list.id}".` });
922
+ }
923
+ listIds.add(list.id);
924
+ if (list.shared) {
925
+ sharedLists.add(list.id);
926
+ if (list.check) {
927
+ issues.push({ message: `${notShown(list.id)} It cannot be checked row by row.` });
928
+ }
929
+ }
930
+ if (list.key && !list.columns.some((c) => c.id === list.key)) {
931
+ issues.push({ message: `List "${list.id}": key "${list.key}" is not one of its columns.` });
932
+ }
933
+ for (const [role, column] of Object.entries(list.check ?? {})) {
934
+ const found = list.columns.find((c) => c.id === column);
935
+ if (!found) {
936
+ issues.push({
937
+ message: `List "${list.id}": check.${role} "${column}" is not one of its columns.`,
938
+ });
939
+ }
940
+ else if (role === "status" && found.type !== "select") {
941
+ issues.push({ message: `List "${list.id}": check.status "${column}" must be a select.` });
942
+ }
943
+ }
944
+ }
945
+ for (const connection of def.connections ?? []) {
946
+ if (connectionIds.has(connection.id)) {
947
+ issues.push({ message: `Duplicate connection id "${connection.id}".` });
948
+ }
949
+ connectionIds.add(connection.id);
950
+ if (Boolean(connection.kind) === Boolean(connection.connector)) {
951
+ issues.push({
952
+ message: `Connection "${connection.id}" needs either "kind" or "connector", not both.`,
953
+ });
954
+ }
955
+ }
956
+ for (const step of def.steps) {
957
+ if (stepIds.has(step.id)) {
958
+ issues.push({ stepId: step.id, message: `Duplicate step id "${step.id}".` });
959
+ }
960
+ stepIds.add(step.id);
961
+ if (step.type === "page") {
962
+ for (const field of step.fields) {
963
+ if (fieldIds.has(field.id)) {
964
+ issues.push({ stepId: step.id, message: `Duplicate field id "${field.id}".` });
965
+ }
966
+ fieldIds.add(field.id);
967
+ if ((field.kind === "select" || field.kind === "multiselect" || field.kind === "slot") &&
968
+ !field.options?.length &&
969
+ !field.optionsFrom) {
970
+ issues.push({ stepId: step.id, message: `Field "${field.id}" needs options.` });
971
+ }
972
+ if (field.kind === "slot") {
973
+ const wrong = (field.options ?? []).find((o) => !slotTime(o));
974
+ if (wrong !== undefined) {
975
+ issues.push({
976
+ stepId: step.id,
977
+ message: `Field "${field.id}": "${wrong}" is not a date and time with its offset, like "2026-10-12T09:00:00+02:00".`,
978
+ });
979
+ }
980
+ }
981
+ if (field.kind === "items" && !field.columns?.length) {
982
+ issues.push({ stepId: step.id, message: `Field "${field.id}" needs columns.` });
983
+ }
984
+ if (field.kind === "connection" && !connectionIds.has(field.connection ?? "")) {
985
+ issues.push({
986
+ stepId: step.id,
987
+ message: `Field "${field.id}" needs "connection": the id of one of the wizard's connections.`,
988
+ });
989
+ }
990
+ if (field.kind === "list" && !listIds.has(field.list ?? "")) {
991
+ issues.push({
992
+ stepId: step.id,
993
+ message: `Field "${field.id}" needs "list": the id of one of the wizard's lists.`,
994
+ });
995
+ }
996
+ else if (field.kind === "list" && sharedLists.has(field.list ?? "")) {
997
+ issues.push({
998
+ stepId: step.id,
999
+ message: `Field "${field.id}": ${notShown(field.list ?? "")}`,
1000
+ });
1001
+ }
1002
+ if (field.min !== undefined || field.max !== undefined) {
1003
+ if ((field.kind !== "image" && field.kind !== "file") || !field.multiple) {
1004
+ issues.push({
1005
+ stepId: step.id,
1006
+ message: `Field "${field.id}": "min" and "max" count the files of an image or file field with "multiple".`,
1007
+ });
1008
+ }
1009
+ else if (field.min !== undefined && field.max !== undefined && field.min > field.max) {
1010
+ issues.push({
1011
+ stepId: step.id,
1012
+ message: `Field "${field.id}": min is larger than max.`,
1013
+ });
1014
+ }
1015
+ }
1016
+ if (field.scan && field.kind !== "text") {
1017
+ issues.push({
1018
+ stepId: step.id,
1019
+ message: `Field "${field.id}": "scan" fills a text field; kind is "${field.kind}".`,
1020
+ });
1021
+ }
1022
+ if (field.listen && field.kind !== "file") {
1023
+ issues.push({
1024
+ stepId: step.id,
1025
+ message: `Field "${field.id}": "listen" belongs to a file field; kind is "${field.kind}".`,
1026
+ });
1027
+ }
1028
+ if (field.video && field.kind !== "file") {
1029
+ issues.push({
1030
+ stepId: step.id,
1031
+ message: `Field "${field.id}": "video" belongs to a file field; kind is "${field.kind}".`,
1032
+ });
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ const last = def.steps.at(-1);
1038
+ if (last?.type !== "result") {
1039
+ issues.push({ message: "The last step must be a result step." });
1040
+ }
1041
+ const checkTemplate = (step, template) => {
1042
+ for (const ref of templateRefs(template)) {
1043
+ const [head, second] = ref.split(".");
1044
+ if (head === "steps") {
1045
+ if (!second || !seenSteps.has(second)) {
1046
+ issues.push({
1047
+ stepId: step.id,
1048
+ message: `"{{${ref}}}" refers to a step that has not run before "${step.id}".`,
1049
+ });
1050
+ }
1051
+ }
1052
+ else if (head === "brand" || head === "facts" || head === "today" || head === "notes") {
1053
+ // provided by the runner
1054
+ }
1055
+ else if (head === "item" || head === "index" || head === "count") {
1056
+ if ((step.type !== "generate" && step.type !== "agent") || !step.each) {
1057
+ issues.push({
1058
+ stepId: step.id,
1059
+ message: `"{{${ref}}}" only exists in a generate or agent step with "each".`,
1060
+ });
1061
+ }
1062
+ }
1063
+ else if (head === "lists") {
1064
+ if (!second || !listIds.has(second)) {
1065
+ issues.push({ stepId: step.id, message: `"{{${ref}}}" refers to an unknown list.` });
1066
+ }
1067
+ }
1068
+ else if (!seenFields.has(head)) {
1069
+ issues.push({
1070
+ stepId: step.id,
1071
+ message: `"{{${ref}}}" refers to a field that is not asked before "${step.id}".`,
1072
+ });
1073
+ }
1074
+ }
1075
+ };
1076
+ /**
1077
+ * What a condition may read before `step`: an answer asked earlier, an output field of an
1078
+ * earlier agent step ("steps.<step>.<field>", the step itself for a branch), or how many rows
1079
+ * a stored list has ("lists.<list>.count").
1080
+ */
1081
+ const knownBefore = (step, ref, self = false) => {
1082
+ if (seenFields.has(ref)) {
1083
+ return true;
1084
+ }
1085
+ const [head, from, key, ...more] = ref.split(".");
1086
+ if (head === "lists") {
1087
+ return key === "count" && more.length === 0 && listIds.has(from ?? "");
1088
+ }
1089
+ // `steps.<step>.<field>.p`: how probable the decision's answer is.
1090
+ const probability = more.length === 1 && more[0] === "p";
1091
+ if (head !== "steps" || !key || (more.length > 0 && !probability)) {
1092
+ return false;
1093
+ }
1094
+ if (!(seenSteps.has(from) || (self && from === step.id))) {
1095
+ return false;
1096
+ }
1097
+ const source = def.steps.find((s) => s.id === from);
1098
+ return (source?.type === "agent" &&
1099
+ (!probability || isDecisionStep(source)) &&
1100
+ (outputKeys(source).includes(key) || (Boolean(source.each) && key === "rows" && !probability)));
1101
+ };
1102
+ const conditionIssues = (stepId, c) => {
1103
+ if ((c.op === "gt" || c.op === "lt") && typeof c.value !== "number") {
1104
+ return [{ stepId, message: `"${c.op}" on "${c.field}" compares with a number value.` }];
1105
+ }
1106
+ if (c.op === "in" && !Array.isArray(c.value)) {
1107
+ return [{ stepId, message: `"in" on "${c.field}" needs a list of values.` }];
1108
+ }
1109
+ if (c.op !== "empty" && c.op !== "notEmpty" && c.value === undefined) {
1110
+ return [{ stepId, message: `"${c.op}" on "${c.field}" needs a value.` }];
1111
+ }
1112
+ return [];
1113
+ };
1114
+ /** Why an `optionsFrom` reference finds nothing, or null when it is fine. */
1115
+ const optionsFromProblem = (raw, cards) => {
1116
+ const [head, from, key, column, ...more] = dataRef(raw).split(".");
1117
+ if (head === "lists" && from && key && !column) {
1118
+ const list = def.lists?.find((l) => l.id === from);
1119
+ if (!list) {
1120
+ return `"optionsFrom" names an unknown list "${from}".`;
1121
+ }
1122
+ if (sharedLists.has(from)) {
1123
+ return notShown(from);
1124
+ }
1125
+ return list.columns.some((c) => c.id === key)
1126
+ ? null
1127
+ : `"optionsFrom": list "${from}" has no column "${key}".`;
1128
+ }
1129
+ if (head === "steps" && from && key && more.length === 0) {
1130
+ const source = seenSteps.has(from) ? def.steps.find((s) => s.id === from) : undefined;
1131
+ // An agent step with `each` hands on one table, `rows`: the entry's columns and its fields.
1132
+ if (source?.type === "agent" && source.each && key === "rows") {
1133
+ return column ? null : `"optionsFrom": "${from}.rows" is a table; name one of its columns.`;
1134
+ }
1135
+ const out = source?.type === "agent" ? declaredShape(source)?.fields[key] : undefined;
1136
+ if (!out) {
1137
+ return `"optionsFrom" names "${from}.${key}", which is not an output of an earlier agent step.`;
1138
+ }
1139
+ // The keys of a table's rows, or of a list's entries; undefined: any (free columns).
1140
+ const columns = out.kind === "table"
1141
+ ? out.columns?.length
1142
+ ? out.columns
1143
+ : undefined
1144
+ : out.kind === "list" && out.of.kind === "object"
1145
+ ? Object.keys(out.of.fields)
1146
+ : null;
1147
+ if (columns === null) {
1148
+ if (column || cards) {
1149
+ return column
1150
+ ? `"optionsFrom": "${from}.${key}" is not a table with the column "${column}".`
1151
+ : `"cards": "${from}.${key}" is not a table.`;
1152
+ }
1153
+ return out.kind === "list"
1154
+ ? null
1155
+ : `"optionsFrom": "${from}.${key}" is not a list (a table takes a column, or "cards").`;
1156
+ }
1157
+ if (column) {
1158
+ return !columns || columns.includes(column)
1159
+ ? null
1160
+ : `"optionsFrom": "${from}.${key}" is not a table with the column "${column}".`;
1161
+ }
1162
+ if (cards) {
1163
+ const missing = Object.values(cards).find((c) => columns && !columns.includes(c));
1164
+ return missing ? `"cards": "${from}.${key}" has no column "${missing}".` : null;
1165
+ }
1166
+ return `"optionsFrom": "${from}.${key}" is not a list (a table takes a column, or "cards").`;
1167
+ }
1168
+ return `"optionsFrom" is "steps.<id>.<key>", "steps.<id>.<key>.<column>" or "lists.<id>.<column>".`;
1169
+ };
1170
+ for (const step of def.steps) {
1171
+ switch (step.type) {
1172
+ case "page": {
1173
+ const groups = new Set();
1174
+ for (const g of step.groups ?? []) {
1175
+ if (groups.has(g.id)) {
1176
+ issues.push({ stepId: step.id, message: `Duplicate field group "${g.id}".` });
1177
+ }
1178
+ groups.add(g.id);
1179
+ if (!step.fields.some((f) => f.group === g.id)) {
1180
+ issues.push({ stepId: step.id, message: `Field group "${g.id}" has no fields.` });
1181
+ }
1182
+ }
1183
+ const decided = step.fields.some((f) => f.ask || f.group);
1184
+ if (decided && step.fields.length > MAX_DECIDED_FIELDS) {
1185
+ issues.push({
1186
+ stepId: step.id,
1187
+ message: `A page whose fields are decided has at most ${MAX_DECIDED_FIELDS} fields.`,
1188
+ });
1189
+ }
1190
+ for (const field of step.fields) {
1191
+ if (field.ask && field.group) {
1192
+ issues.push({
1193
+ stepId: step.id,
1194
+ message: `Field "${field.id}" has "ask" or "group", not both.`,
1195
+ });
1196
+ }
1197
+ if (field.group && !groups.has(field.group)) {
1198
+ issues.push({
1199
+ stepId: step.id,
1200
+ message: `Field "${field.id}" names an unknown group "${field.group}".`,
1201
+ });
1202
+ }
1203
+ }
1204
+ for (const field of step.fields) {
1205
+ for (const c of conditionsOf(field.when)) {
1206
+ const own = step.fields.some((f) => f.id === c.field && f.id !== field.id);
1207
+ if (!own && !knownBefore(step, c.field)) {
1208
+ issues.push({
1209
+ stepId: step.id,
1210
+ message: `Field "${field.id}": "when" reads "${c.field}", which is neither on this page nor known before it.`,
1211
+ });
1212
+ }
1213
+ issues.push(...conditionIssues(step.id, c));
1214
+ }
1215
+ if (field.optionsFrom) {
1216
+ const problem = field.kind !== "select" && field.kind !== "multiselect" && field.kind !== "slot"
1217
+ ? `"optionsFrom" fills the choices of a select, multiselect or slot; kind is "${field.kind}".`
1218
+ : field.cards && field.kind === "slot"
1219
+ ? '"cards" draws the choices of a select or multiselect, not of a slot.'
1220
+ : optionsFromProblem(field.optionsFrom, field.cards);
1221
+ if (problem) {
1222
+ issues.push({ stepId: step.id, message: `Field "${field.id}": ${problem}` });
1223
+ }
1224
+ }
1225
+ else if (field.cards) {
1226
+ issues.push({
1227
+ stepId: step.id,
1228
+ message: `Field "${field.id}": "cards" needs "optionsFrom" naming a table output.`,
1229
+ });
1230
+ }
1231
+ if (field.prefill) {
1232
+ const ref = dataRef(field.prefill);
1233
+ if (ref.split(".")[0] !== "steps") {
1234
+ issues.push({
1235
+ stepId: step.id,
1236
+ message: `Field "${field.id}": prefill names an earlier step ("steps.<id>.<key>").`,
1237
+ });
1238
+ }
1239
+ else {
1240
+ checkTemplate(step, `{{${ref}}}`);
1241
+ }
1242
+ }
1243
+ seenFields.add(field.id);
1244
+ }
1245
+ break;
1246
+ }
1247
+ case "agent":
1248
+ checkTemplate(step, step.instructions);
1249
+ if (step.call) {
1250
+ checkTemplate(step, JSON.stringify(step.call.input));
1251
+ if (step.each) {
1252
+ issues.push({ stepId: step.id, message: 'A step with "call" runs once: no "each".' });
1253
+ }
1254
+ if (step.output.format !== "json") {
1255
+ issues.push({
1256
+ stepId: step.id,
1257
+ message: 'A step with "call" keeps the tool\'s answer: output format "json".',
1258
+ });
1259
+ }
1260
+ }
1261
+ else if (!step.instructions.trim()) {
1262
+ issues.push({ stepId: step.id, message: "An AI step needs instructions." });
1263
+ }
1264
+ for (const c of step.connections ?? []) {
1265
+ if (!connectionIds.has(c)) {
1266
+ issues.push({ stepId: step.id, message: `Step uses unknown connection "${c}".` });
1267
+ }
1268
+ }
1269
+ if (step.output.surface && step.output.format !== "json") {
1270
+ issues.push({
1271
+ stepId: step.id,
1272
+ message: 'A step composes a view ("surface") of json output only.',
1273
+ });
1274
+ }
1275
+ if (step.output.schema) {
1276
+ if (step.output.fields?.length) {
1277
+ issues.push({
1278
+ stepId: step.id,
1279
+ message: 'Output has "fields" or "schema", not both.',
1280
+ });
1281
+ }
1282
+ if (step.output.format !== "json" || step.call) {
1283
+ issues.push({
1284
+ stepId: step.id,
1285
+ message: step.call
1286
+ ? 'A step with "call" keeps the tool\'s answer as it is: no "schema".'
1287
+ : 'A "schema" describes json output: output format "json".',
1288
+ });
1289
+ }
1290
+ }
1291
+ if (Array.isArray(step.output.surface)) {
1292
+ // A step with `each` hands over its entries as `rows`.
1293
+ for (const issue of validateSurface(step.output.surface, step.each ? ["rows"] : declaredShape(step))) {
1294
+ issues.push({ stepId: step.id, message: `View: ${issue.message}` });
1295
+ }
1296
+ }
1297
+ if (step.each) {
1298
+ const ref = dataRef(step.each);
1299
+ const list = ref.match(/^lists\.([a-zA-Z][a-zA-Z0-9_]*)$/)?.[1];
1300
+ if (list) {
1301
+ if (!listIds.has(list)) {
1302
+ issues.push({ stepId: step.id, message: `each names an unknown list "${list}".` });
1303
+ }
1304
+ }
1305
+ else if (ref.startsWith("steps.") && ref.split(".").length === 3) {
1306
+ checkTemplate(step, `{{${ref}}}`);
1307
+ }
1308
+ else {
1309
+ issues.push({
1310
+ stepId: step.id,
1311
+ message: `each "${step.each}" is "steps.<id>.<key>" (a table or list output) or "lists.<id>".`,
1312
+ });
1313
+ }
1314
+ }
1315
+ break;
1316
+ case "generate":
1317
+ checkTemplate(step, step.prompt);
1318
+ checkTemplate(step, step.options?.style ?? "");
1319
+ if (step.referenceImage &&
1320
+ !seenFields.has(step.referenceImage) &&
1321
+ !seenSteps.has(step.referenceImage)) {
1322
+ issues.push({
1323
+ stepId: step.id,
1324
+ message: `referenceImage "${step.referenceImage}" is not an earlier field or step.`,
1325
+ });
1326
+ }
1327
+ if (step.each) {
1328
+ const ref = dataRef(step.each);
1329
+ if (step.asset !== "image" && step.asset !== "video") {
1330
+ issues.push({ stepId: step.id, message: `"each" is for image and video steps.` });
1331
+ }
1332
+ else if (ref.startsWith("steps.")) {
1333
+ checkTemplate(step, `{{${ref}}}`);
1334
+ }
1335
+ else if (!seenFields.has(ref) && !seenSteps.has(ref)) {
1336
+ issues.push({
1337
+ stepId: step.id,
1338
+ message: `each "${step.each}" is not an earlier field, step or "steps.<id>.<key>".`,
1339
+ });
1340
+ }
1341
+ }
1342
+ break;
1343
+ case "widget":
1344
+ for (const [key, ref] of Object.entries(step.data)) {
1345
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1346
+ issues.push({
1347
+ stepId: step.id,
1348
+ message: `Widget data key "${key}" is not an identifier.`,
1349
+ });
1350
+ }
1351
+ checkTemplate(step, `{{${dataRef(ref)}}}`);
1352
+ }
1353
+ if (files && !files.includes(step.entry)) {
1354
+ issues.push({
1355
+ stepId: step.id,
1356
+ message: `Widget entry "${step.entry}" is not in the workspace.`,
1357
+ });
1358
+ }
1359
+ if (files && step.sample && !files.includes(step.sample)) {
1360
+ issues.push({
1361
+ stepId: step.id,
1362
+ message: `Widget sample "${step.sample}" is not in the workspace.`,
1363
+ });
1364
+ }
1365
+ break;
1366
+ case "surface": {
1367
+ for (const [key, ref] of Object.entries(step.data)) {
1368
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1369
+ issues.push({
1370
+ stepId: step.id,
1371
+ message: `Surface data key "${key}" is not an identifier.`,
1372
+ });
1373
+ }
1374
+ const list = listRef(dataRef(ref));
1375
+ if (list) {
1376
+ if (!listIds.has(list)) {
1377
+ issues.push({
1378
+ stepId: step.id,
1379
+ message: `Surface data reads an unknown list "${list}".`,
1380
+ });
1381
+ }
1382
+ }
1383
+ else {
1384
+ checkTemplate(step, `{{${dataRef(ref)}}}`);
1385
+ }
1386
+ }
1387
+ const groups = new Set((step.groups ?? []).map((g) => g.id));
1388
+ for (const c of step.candidates ?? []) {
1389
+ if (c.components[0]?.id !== c.id) {
1390
+ issues.push({
1391
+ stepId: step.id,
1392
+ message: `Surface piece "${c.id}": its first component carries the piece's id.`,
1393
+ });
1394
+ }
1395
+ if (c.group && !groups.has(c.group)) {
1396
+ issues.push({
1397
+ stepId: step.id,
1398
+ message: `Surface piece "${c.id}" names an unknown group "${c.group}".`,
1399
+ });
1400
+ }
1401
+ }
1402
+ const components = step.components ??
1403
+ assembleSurface(step.candidates ?? [], (step.candidates ?? []).map((c) => c.id));
1404
+ for (const issue of validateSurface(components, Object.keys(step.data))) {
1405
+ issues.push({ stepId: step.id, message: `Surface: ${issue.message}` });
1406
+ }
1407
+ break;
1408
+ }
1409
+ case "film":
1410
+ checkTemplate(step, step.brief);
1411
+ for (const [name, ref] of Object.entries(step.inputs)) {
1412
+ if (!/^[a-z][a-z0-9-]{0,40}$/.test(name)) {
1413
+ issues.push({
1414
+ stepId: step.id,
1415
+ message: `Film input "${name}" is not a folder name (lowercase letters, digits, -).`,
1416
+ });
1417
+ }
1418
+ const r = dataRef(ref);
1419
+ if (r.startsWith("steps.")) {
1420
+ checkTemplate(step, `{{${r}}}`);
1421
+ }
1422
+ else if (!seenFields.has(r)) {
1423
+ issues.push({
1424
+ stepId: step.id,
1425
+ message: `Film input "${name}": "${ref}" is not an earlier field or "steps.<id>".`,
1426
+ });
1427
+ }
1428
+ }
1429
+ if (step.kit && !step.kit.endsWith(".zip")) {
1430
+ issues.push({ stepId: step.id, message: `Film kit "${step.kit}" is a .zip.` });
1431
+ }
1432
+ else if (files && step.kit && !files.includes(step.kit)) {
1433
+ issues.push({
1434
+ stepId: step.id,
1435
+ message: `Film kit "${step.kit}" is not in the workspace.`,
1436
+ });
1437
+ }
1438
+ for (const skill of step.skills) {
1439
+ if (skill.startsWith("skills/") && files && !files.includes(`${skill}/SKILL.md`)) {
1440
+ issues.push({
1441
+ stepId: step.id,
1442
+ message: `Skill "${skill}" has no SKILL.md in the workspace.`,
1443
+ });
1444
+ }
1445
+ }
1446
+ break;
1447
+ case "review":
1448
+ for (const ref of step.show) {
1449
+ const list = listRef(ref);
1450
+ if (list ? !listIds.has(list) : !seenSteps.has(ref)) {
1451
+ issues.push({
1452
+ stepId: step.id,
1453
+ message: `Review shows unknown ${list ? "list" : "step"} "${ref}".`,
1454
+ });
1455
+ }
1456
+ else if (list && sharedLists.has(list)) {
1457
+ issues.push({ stepId: step.id, message: notShown(list) });
1458
+ }
1459
+ }
1460
+ break;
1461
+ case "result":
1462
+ for (const d of step.deliverables) {
1463
+ const list = listRef(d.from);
1464
+ if (list ? !listIds.has(list) : !seenSteps.has(d.from)) {
1465
+ issues.push({
1466
+ stepId: step.id,
1467
+ message: `Deliverable from unknown ${list ? "list" : "step"} "${d.from}".`,
1468
+ });
1469
+ }
1470
+ else if (list && sharedLists.has(list)) {
1471
+ issues.push({ stepId: step.id, message: notShown(list) });
1472
+ }
1473
+ }
1474
+ break;
1475
+ }
1476
+ if ((step.next ?? []).filter(isOtherwise).length > 1) {
1477
+ issues.push({
1478
+ stepId: step.id,
1479
+ message: 'A step has at most one "otherwise" branch (one without "when" and "ask").',
1480
+ });
1481
+ }
1482
+ for (const rule of step.next ?? []) {
1483
+ if (rule.goto !== "end" && !stepIds.has(rule.goto)) {
1484
+ issues.push({ stepId: step.id, message: `Branch goes to unknown step "${rule.goto}".` });
1485
+ }
1486
+ // A branch reads what the person answered (on any page), an output field of this or an
1487
+ // earlier agent step, or the row count of a stored list.
1488
+ for (const c of conditionsOf(rule.when)) {
1489
+ if (!fieldIds.has(c.field) && !knownBefore(step, c.field, true)) {
1490
+ issues.push({ stepId: step.id, message: `Branch reads unknown field "${c.field}".` });
1491
+ }
1492
+ issues.push(...conditionIssues(step.id, c));
1493
+ }
1494
+ }
1495
+ seenSteps.add(step.id);
1496
+ }
1497
+ return issues;
1498
+ }
1499
+ /** Parse + validate in one go. */
1500
+ export function parseWizard(input, files) {
1501
+ const parsed = wizardSchema.safeParse(input);
1502
+ if (!parsed.success) {
1503
+ return { ok: false, issues: shapeIssues(parsed.error, input) };
1504
+ }
1505
+ const issues = validateWizard(parsed.data, files);
1506
+ return { ok: true, wizard: parsed.data, issues };
1507
+ }
1508
+ function stepLabel(raw, index) {
1509
+ const steps = raw?.steps;
1510
+ const id = steps?.[index]?.id;
1511
+ return typeof id === "string" ? `steps[${index}] "${id}"` : `steps[${index}]`;
1512
+ }
1513
+ /** Where the shape is wrong, a step named by its id: `steps[2] "draft".title: …`. */
1514
+ export function shapeIssues(error, raw) {
1515
+ return error.issues.map((i) => {
1516
+ const [head, index, ...rest] = i.path;
1517
+ const where = head === "steps" && typeof index === "number"
1518
+ ? [stepLabel(raw, index), ...rest].join(".")
1519
+ : i.path.join(".");
1520
+ return { message: `${where || "wizard"}: ${i.message}` };
1521
+ });
1522
+ }
1523
+ /**
1524
+ * What branch rules can read in a run: the person's answers by field id, and the structured
1525
+ * output of the steps run so far as `steps.<step>.<field>`.
1526
+ */
1527
+ export function branchValues(values, outputs,
1528
+ /** Rows per stored list, read as `lists.<list>.count`. */
1529
+ listCounts = {}) {
1530
+ const out = { ...values };
1531
+ for (const [list, count] of Object.entries(listCounts)) {
1532
+ out[`lists.${list}.count`] = count;
1533
+ }
1534
+ for (const [stepId, output] of Object.entries(outputs)) {
1535
+ if (output.json && typeof output.json === "object" && !Array.isArray(output.json)) {
1536
+ for (const [key, value] of Object.entries(output.json)) {
1537
+ out[`steps.${stepId}.${key}`] = value;
1538
+ }
1539
+ }
1540
+ for (const [key, p] of Object.entries(output.decided ?? {})) {
1541
+ out[`steps.${stepId}.${key}.p`] = p;
1542
+ }
1543
+ }
1544
+ return out;
1545
+ }
1546
+ /** A branch back to an earlier step without `max` is taken at most this often in one run. */
1547
+ export const DEFAULT_LOOP_MAX = 10;
1548
+ /** How a run counts the times it took a branch: "<step>→<goto>". */
1549
+ export function loopKey(stepId, goto) {
1550
+ return `${stepId}→${goto}`;
1551
+ }
1552
+ /** Whether a rule may still send the run on: one with `max`, or one back, stops after so many. */
1553
+ export function ruleOpen(def, stepId, rule, loops = {}) {
1554
+ const from = def.steps.findIndex((s) => s.id === stepId);
1555
+ const to = rule.goto === "end" ? def.steps.length : def.steps.findIndex((s) => s.id === rule.goto);
1556
+ const max = rule.max ?? (to >= 0 && to <= from ? DEFAULT_LOOP_MAX : Number.POSITIVE_INFINITY);
1557
+ return (loops[loopKey(stepId, rule.goto)] ?? 0) < max;
1558
+ }
1559
+ export function followRules(def, stepId, values, loops = {}) {
1560
+ const index = def.steps.findIndex((s) => s.id === stepId);
1561
+ const step = def.steps[index];
1562
+ if (!step) {
1563
+ return { kind: "go", cursor: null, rule: null };
1564
+ }
1565
+ const rules = step.next ?? [];
1566
+ for (const [i, rule] of rules.entries()) {
1567
+ if (ruleOpen(def, stepId, rule, loops) && ruleMatches(rule, values)) {
1568
+ return { kind: "go", cursor: rule.goto === "end" ? null : rule.goto, rule: i };
1569
+ }
1570
+ }
1571
+ const asks = rules.flatMap((rule, i) => rule.ask && ruleOpen(def, stepId, rule, loops) ? [i] : []);
1572
+ if (asks.length) {
1573
+ return { kind: "decide", rules: asks };
1574
+ }
1575
+ return { kind: "go", ...otherwiseOf(def, stepId, loops) };
1576
+ }
1577
+ /** A rule with neither a condition nor a statement: where the run goes when no other rule holds. */
1578
+ export function isOtherwise(rule) {
1579
+ return rule.when === undefined && rule.ask === undefined;
1580
+ }
1581
+ /**
1582
+ * Where a step goes when none of its rules holds: its "otherwise" rule (`rule` is its index in
1583
+ * `next`), else the next step in the list. `cursor` null = the end.
1584
+ */
1585
+ export function otherwiseOf(def, stepId, loops = {}) {
1586
+ const index = def.steps.findIndex((s) => s.id === stepId);
1587
+ const rules = def.steps[index]?.next ?? [];
1588
+ const i = rules.findIndex((r) => isOtherwise(r) && ruleOpen(def, stepId, r, loops));
1589
+ if (i >= 0) {
1590
+ return { cursor: rules[i].goto === "end" ? null : rules[i].goto, rule: i };
1591
+ }
1592
+ return { cursor: def.steps[index + 1]?.id ?? null, rule: null };
1593
+ }
1594
+ /** The step after `step` by its `when` rules alone. `null` = the end. */
1595
+ export function nextStepId(def, stepId, values) {
1596
+ const outcome = followRules(def, stepId, values);
1597
+ if (outcome.kind === "go") {
1598
+ return outcome.cursor;
1599
+ }
1600
+ const index = def.steps.findIndex((s) => s.id === stepId);
1601
+ return def.steps[index + 1]?.id ?? null;
1602
+ }
1603
+ function isEmptyValue(v) {
1604
+ return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
1605
+ }
1606
+ /** The conditions of a rule or a field, as a list: one, several, or none. */
1607
+ export function conditionsOf(when) {
1608
+ return when === undefined ? [] : Array.isArray(when) ? when : [when];
1609
+ }
1610
+ export function conditionMatches(c, values) {
1611
+ const v = values[c.field];
1612
+ switch (c.op) {
1613
+ case "empty":
1614
+ return isEmptyValue(v);
1615
+ case "notEmpty":
1616
+ return !isEmptyValue(v);
1617
+ case "equals":
1618
+ return String(v) === String(c.value);
1619
+ case "notEquals":
1620
+ return String(v) !== String(c.value);
1621
+ case "in":
1622
+ return Array.isArray(c.value) && c.value.map(String).includes(String(v));
1623
+ case "gt":
1624
+ case "lt": {
1625
+ const a = readNumber(v);
1626
+ const b = readNumber(c.value);
1627
+ return a !== null && b !== null && (c.op === "gt" ? a > b : a < b);
1628
+ }
1629
+ case "contains":
1630
+ if (Array.isArray(v)) {
1631
+ return v.map(String).includes(String(c.value));
1632
+ }
1633
+ return (typeof v === "string" &&
1634
+ c.value !== undefined &&
1635
+ v.toLowerCase().includes(String(c.value).toLowerCase()));
1636
+ }
1637
+ }
1638
+ /** Whether every condition holds; no conditions hold trivially. */
1639
+ export function whenHolds(when, values) {
1640
+ return conditionsOf(when).every((c) => conditionMatches(c, values));
1641
+ }
1642
+ /** Whether a `when` rule holds. An `ask` rule never holds here: a decision takes it (see decide). */
1643
+ export function ruleMatches(rule, values) {
1644
+ return rule.when !== undefined && whenHolds(rule.when, values);
1645
+ }
1646
+ /** A page with decided fields has at most this many; it shows at most MAX_SHOWN_FIELDS of them. */
1647
+ export const MAX_DECIDED_FIELDS = 12;
1648
+ export const MAX_SHOWN_FIELDS = 5;
1649
+ /** Whether a field is shown only when a decision keeps it. */
1650
+ export function isDecidedField(field) {
1651
+ return Boolean(field.ask || field.group);
1652
+ }
1653
+ /**
1654
+ * The fields of a page that are shown: those whose `when` holds on what was known before the
1655
+ * page (`known`) and what the page holds so far (`page`). A hidden field's value does not count
1656
+ * for the fields after it.
1657
+ */
1658
+ export function shownFields(fields, known, page) {
1659
+ const values = { ...known, ...page };
1660
+ const out = [];
1661
+ for (const field of fields) {
1662
+ if (whenHolds(field.when, values)) {
1663
+ out.push(field);
1664
+ }
1665
+ else {
1666
+ delete values[field.id];
1667
+ }
1668
+ }
1669
+ return out;
1670
+ }
1671
+ /** The references the `when` of a page's fields read outside the page: what the page must know. */
1672
+ export function pageNeeds(fields) {
1673
+ const own = new Set(fields.map((f) => f.id));
1674
+ return [
1675
+ ...new Set(fields.flatMap((f) => conditionsOf(f.when).map((c) => c.field)).filter((r) => !own.has(r))),
1676
+ ];
1677
+ }
1678
+ /** At most this many choices come from data. */
1679
+ export const MAX_DATA_OPTIONS = 100;
1680
+ /**
1681
+ * The choices an `optionsFrom` reference finds in a run: the entries of a list output, one
1682
+ * column of a table output, or one column of a stored list — as text, without duplicates and
1683
+ * empties. Empty when the data is not there (yet).
1684
+ */
1685
+ export function optionsFromData(ref, outputs, lists = {}, cards) {
1686
+ if (cards) {
1687
+ return cardsFromData(ref, outputs, cards).map((c) => c.value);
1688
+ }
1689
+ const [head, id, key, column, ...more] = dataRef(ref).split(".");
1690
+ let raw = [];
1691
+ if (head === "lists" && id && key && !column) {
1692
+ raw = (lists[id]?.rows ?? []).map((r) => r.cells[key]);
1693
+ }
1694
+ else if (head === "steps" && id && key && more.length === 0) {
1695
+ const json = outputs[id]?.json;
1696
+ const value = json && typeof json === "object" ? json[key] : undefined;
1697
+ if (Array.isArray(value)) {
1698
+ raw = column
1699
+ ? value.map((row) => row && typeof row === "object" ? row[column] : undefined)
1700
+ : value;
1701
+ }
1702
+ }
1703
+ const seen = new Set();
1704
+ for (const v of raw) {
1705
+ if (v === null || v === undefined || typeof v === "object") {
1706
+ continue;
1707
+ }
1708
+ const text = String(v).trim().slice(0, 200);
1709
+ if (text) {
1710
+ seen.add(text);
1711
+ }
1712
+ if (seen.size >= MAX_DATA_OPTIONS) {
1713
+ break;
1714
+ }
1715
+ }
1716
+ return [...seen];
1717
+ }
1718
+ /** The rows of a table output as cards: what `optionsFrom` with `cards` finds in the run. */
1719
+ export function cardsFromData(ref, outputs, cards) {
1720
+ const [head, id, key, ...more] = dataRef(ref).split(".");
1721
+ if (head !== "steps" || !id || !key || more.length) {
1722
+ return [];
1723
+ }
1724
+ const json = outputs[id]?.json;
1725
+ const rows = json && typeof json === "object" ? json[key] : undefined;
1726
+ if (!Array.isArray(rows)) {
1727
+ return [];
1728
+ }
1729
+ const cell = (row, column, max = 200) => {
1730
+ const v = column ? row[column] : undefined;
1731
+ if (v === null || v === undefined || typeof v === "object") {
1732
+ return "";
1733
+ }
1734
+ return String(v).trim().slice(0, max);
1735
+ };
1736
+ const out = [];
1737
+ const seen = new Set();
1738
+ for (const raw of rows) {
1739
+ if (!raw || typeof raw !== "object") {
1740
+ continue;
1741
+ }
1742
+ const row = raw;
1743
+ const label = cell(row, cards.label);
1744
+ const value = cell(row, cards.value) || label;
1745
+ if (!value || seen.has(value)) {
1746
+ continue;
1747
+ }
1748
+ seen.add(value);
1749
+ const card = { value, label: label || value };
1750
+ const image = cell(row, cards.image, 2000);
1751
+ const link = cell(row, cards.link, 2000);
1752
+ const subtitle = cell(row, cards.subtitle, 300);
1753
+ const meta = cell(row, cards.meta, 80);
1754
+ if (/^(https?:|data:|\/)/.test(image) || /^[a-zA-Z0-9_-]{6,64}$/.test(image)) {
1755
+ card.image = image;
1756
+ }
1757
+ if (/^https?:\/\//.test(link)) {
1758
+ card.link = link;
1759
+ }
1760
+ if (subtitle) {
1761
+ card.subtitle = subtitle;
1762
+ }
1763
+ if (meta) {
1764
+ card.meta = meta;
1765
+ }
1766
+ out.push(card);
1767
+ if (out.length >= MAX_DATA_OPTIONS) {
1768
+ break;
1769
+ }
1770
+ }
1771
+ return out;
1772
+ }
1773
+ // ---------------------------------------------------------------------------
1774
+ // Appointment times
1775
+ const SLOT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
1776
+ /**
1777
+ * A slot option as day and time in its own offset (the business's time zone, not the reader's).
1778
+ * Null when it is not an ISO 8601 date-time with an offset.
1779
+ */
1780
+ export function slotTime(iso) {
1781
+ const m = SLOT_RE.exec(iso.trim());
1782
+ if (!m) {
1783
+ return null;
1784
+ }
1785
+ const [, y, mo, d, h, mi] = m.map(Number);
1786
+ const wall = new Date(Date.UTC(y, mo - 1, d, h, mi));
1787
+ if (wall.getUTCMonth() !== mo - 1 ||
1788
+ wall.getUTCDate() !== d ||
1789
+ h > 23 ||
1790
+ mi > 59 ||
1791
+ !Number.isFinite(Date.parse(iso))) {
1792
+ return null;
1793
+ }
1794
+ return { day: `${m[1]}-${m[2]}-${m[3]}`, time: `${m[4]}:${m[5]}`, wall };
1795
+ }
1796
+ /** The weekday and date of a slot's day: "Montag, 12. Oktober" / "Monday, 12 October". */
1797
+ export function slotDayText(iso, lang = "en") {
1798
+ const slot = slotTime(iso);
1799
+ if (!slot) {
1800
+ return iso;
1801
+ }
1802
+ const locale = localeOf(lang);
1803
+ const weekday = slot.wall.toLocaleDateString(locale, { weekday: "long", timeZone: "UTC" });
1804
+ const date = slot.wall.toLocaleDateString(locale, {
1805
+ day: "numeric",
1806
+ month: "long",
1807
+ timeZone: "UTC",
1808
+ });
1809
+ return `${weekday}, ${date}`;
1810
+ }
1811
+ /** A chosen slot as a person reads it back: "Di 13.10.2026, 10:30" / "Tue 13/10/2026, 10:30". */
1812
+ export function slotText(iso, lang = "en") {
1813
+ const slot = slotTime(iso);
1814
+ if (!slot) {
1815
+ return iso;
1816
+ }
1817
+ const locale = localeOf(lang);
1818
+ const weekday = slot.wall
1819
+ .toLocaleDateString(locale, { weekday: "short", timeZone: "UTC" })
1820
+ .replace(/\.$/, "");
1821
+ const date = slot.wall.toLocaleDateString(locale, {
1822
+ day: "2-digit",
1823
+ month: "2-digit",
1824
+ year: "numeric",
1825
+ timeZone: "UTC",
1826
+ });
1827
+ return `${weekday} ${date}, ${slot.time}`;
1828
+ }
1829
+ /**
1830
+ * The slot among `options` that is the same moment as `value`, written as offered — so an answer
1831
+ * in another offset ("…T07:00:00Z" for "…T09:00:00+02:00") picks the offered time.
1832
+ */
1833
+ export function offeredSlot(value, options) {
1834
+ if (options.includes(value)) {
1835
+ return value;
1836
+ }
1837
+ const at = slotTime(value) ? Date.parse(value) : Number.NaN;
1838
+ return Number.isFinite(at) ? options.find((o) => slotTime(o) && Date.parse(o) === at) : undefined;
1839
+ }
1840
+ export function itemsTotals(field, value, values) {
1841
+ const rows = Array.isArray(value) ? value : [];
1842
+ const numeric = (field.columns ?? []).filter((c) => c.kind !== "text");
1843
+ const withAmount = rows.map((row) => {
1844
+ const amount = numeric.length ? numeric.reduce((acc, c) => acc * amountOf(row[c.id]), 1) : 0;
1845
+ return { ...row, amount: round2(amount) };
1846
+ });
1847
+ const net = round2(withAmount.reduce((a, r) => a + r.amount, 0));
1848
+ let vatRate = field.vat?.rate ?? 0;
1849
+ if (field.vat?.field) {
1850
+ const raw = values[field.vat.field];
1851
+ const parsed = readNumber(raw, { units: true });
1852
+ if (parsed !== null) {
1853
+ vatRate = parsed;
1854
+ }
1855
+ }
1856
+ const vat = round2((net * vatRate) / 100);
1857
+ return {
1858
+ rows: withAmount,
1859
+ net,
1860
+ vatRate,
1861
+ vat,
1862
+ gross: round2(net + vat),
1863
+ currency: field.currency ?? "EUR",
1864
+ };
1865
+ }
1866
+ /** A typed amount as a number: a phone's number pad in German gives "89,90". */
1867
+ function amountOf(v) {
1868
+ return readNumber(v) ?? 0;
1869
+ }
1870
+ function round2(n) {
1871
+ return Math.round(n * 100) / 100;
1872
+ }
1873
+ export function allFields(def) {
1874
+ return def.steps.flatMap((s) => (s.type === "page" ? s.fields : []));
1875
+ }
1876
+ const GERMAN = /\b(und|der|die|das|ist|nicht|für|mit|ein|eine|wird|zu|dein|deine|ihr|wir|was|wie|oder)\b|[äöüß]/gi;
1877
+ const ENGLISH = /\b(and|the|is|not|for|with|a|an|your|you|we|what|how|or|to|of)\b/gi;
1878
+ /**
1879
+ * The language a wizard speaks to people: the one it names, else read from its own words
1880
+ * (titles, labels, intro) — English where they are not clearly German.
1881
+ */
1882
+ export function wizardLang(def) {
1883
+ if (def.lang) {
1884
+ return def.lang;
1885
+ }
1886
+ const text = [
1887
+ def.title,
1888
+ def.description,
1889
+ def.intro,
1890
+ ...def.steps.flatMap((s) => [
1891
+ s.title,
1892
+ s.description,
1893
+ ...(s.type === "page" ? s.fields.flatMap((f) => [f.label, f.help, f.placeholder]) : []),
1894
+ ]),
1895
+ ]
1896
+ .filter(Boolean)
1897
+ .join(" ");
1898
+ const de = text.match(GERMAN)?.length ?? 0;
1899
+ const en = text.match(ENGLISH)?.length ?? 0;
1900
+ return de > en ? "de" : "en";
1901
+ }
1902
+ // --- can this app read a definition? ---------------------------------------------------------
1903
+ // The schema drops what it does not know. A definition written for a newer app would be read
1904
+ // without complaint and run wrongly, so it counts only when nothing of it is lost.
1905
+ function keepsAll(given, read) {
1906
+ if (Array.isArray(given)) {
1907
+ return Array.isArray(read) && given.every((v, i) => keepsAll(v, read[i]));
1908
+ }
1909
+ if (given && typeof given === "object") {
1910
+ return (Boolean(read) &&
1911
+ typeof read === "object" &&
1912
+ Object.entries(given).every(([k, v]) => v === undefined || (k in read && keepsAll(v, read[k]))));
1913
+ }
1914
+ return true;
1915
+ }
1916
+ /** True when this app's schema reads the definition without losing anything of it. */
1917
+ export function readable(definition) {
1918
+ const parsed = wizardSchema.safeParse(definition);
1919
+ return parsed.success && keepsAll(definition, parsed.data);
1920
+ }