@shware/workflow 3.3.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +230 -40
  2. package/dist/apns.cjs +113 -26
  3. package/dist/apns.cjs.map +1 -1
  4. package/dist/apns.d.cts +52 -6
  5. package/dist/apns.d.cts.map +1 -1
  6. package/dist/apns.d.mts +52 -6
  7. package/dist/apns.d.mts.map +1 -1
  8. package/dist/apns.mjs +111 -27
  9. package/dist/apns.mjs.map +1 -1
  10. package/dist/aws/engine-step.cjs +5 -5
  11. package/dist/aws/engine-step.cjs.map +1 -1
  12. package/dist/aws/engine-step.d.cts +8 -2
  13. package/dist/aws/engine-step.d.cts.map +1 -1
  14. package/dist/aws/engine-step.d.mts +8 -2
  15. package/dist/aws/engine-step.d.mts.map +1 -1
  16. package/dist/aws/engine-step.mjs +5 -5
  17. package/dist/aws/engine-step.mjs.map +1 -1
  18. package/dist/aws/index.cjs +50 -6
  19. package/dist/aws/index.cjs.map +1 -1
  20. package/dist/aws/index.d.cts +4 -3
  21. package/dist/aws/index.d.cts.map +1 -1
  22. package/dist/aws/index.d.mts +4 -3
  23. package/dist/aws/index.d.mts.map +1 -1
  24. package/dist/aws/index.mjs +49 -7
  25. package/dist/aws/index.mjs.map +1 -1
  26. package/dist/aws/instances.cjs +29 -6
  27. package/dist/aws/instances.cjs.map +1 -1
  28. package/dist/aws/instances.d.cts +11 -1
  29. package/dist/aws/instances.d.cts.map +1 -1
  30. package/dist/aws/instances.d.mts +11 -1
  31. package/dist/aws/instances.d.mts.map +1 -1
  32. package/dist/aws/instances.mjs +27 -4
  33. package/dist/aws/instances.mjs.map +1 -1
  34. package/dist/client.cjs +4 -1
  35. package/dist/client.cjs.map +1 -1
  36. package/dist/client.d.cts +2 -2
  37. package/dist/client.d.cts.map +1 -1
  38. package/dist/client.d.mts +2 -2
  39. package/dist/client.d.mts.map +1 -1
  40. package/dist/client.mjs +4 -1
  41. package/dist/client.mjs.map +1 -1
  42. package/dist/cloudflare/bindings.cjs +28 -25
  43. package/dist/cloudflare/bindings.cjs.map +1 -1
  44. package/dist/cloudflare/bindings.d.cts +15 -129
  45. package/dist/cloudflare/bindings.d.cts.map +1 -1
  46. package/dist/cloudflare/bindings.d.mts +15 -129
  47. package/dist/cloudflare/bindings.d.mts.map +1 -1
  48. package/dist/cloudflare/bindings.mjs +28 -23
  49. package/dist/cloudflare/bindings.mjs.map +1 -1
  50. package/dist/cloudflare/d1-env.cjs +6 -6
  51. package/dist/cloudflare/d1-env.cjs.map +1 -1
  52. package/dist/cloudflare/d1-env.d.cts +5 -5
  53. package/dist/cloudflare/d1-env.d.mts +5 -5
  54. package/dist/cloudflare/d1-env.mjs +6 -6
  55. package/dist/cloudflare/d1-env.mjs.map +1 -1
  56. package/dist/cloudflare/index.cjs +29 -18
  57. package/dist/cloudflare/index.d.cts +8 -4
  58. package/dist/cloudflare/index.d.mts +8 -4
  59. package/dist/cloudflare/index.mjs +9 -5
  60. package/dist/cloudflare/runner.cjs +29 -14
  61. package/dist/cloudflare/runner.cjs.map +1 -1
  62. package/dist/cloudflare/runner.d.cts +9 -2
  63. package/dist/cloudflare/runner.d.cts.map +1 -1
  64. package/dist/cloudflare/runner.d.mts +9 -2
  65. package/dist/cloudflare/runner.d.mts.map +1 -1
  66. package/dist/cloudflare/runner.mjs +28 -13
  67. package/dist/cloudflare/runner.mjs.map +1 -1
  68. package/dist/cloudflare/schema.sql +27 -6
  69. package/dist/cloudflare/worker.cjs +19 -9
  70. package/dist/cloudflare/worker.cjs.map +1 -1
  71. package/dist/cloudflare/worker.d.cts +2 -2
  72. package/dist/cloudflare/worker.d.cts.map +1 -1
  73. package/dist/cloudflare/worker.d.mts +2 -2
  74. package/dist/cloudflare/worker.d.mts.map +1 -1
  75. package/dist/cloudflare/worker.mjs +19 -9
  76. package/dist/cloudflare/worker.mjs.map +1 -1
  77. package/dist/config.cjs +21 -5
  78. package/dist/config.cjs.map +1 -1
  79. package/dist/config.mjs +21 -5
  80. package/dist/config.mjs.map +1 -1
  81. package/dist/dsl/action.cjs +3 -6
  82. package/dist/dsl/action.cjs.map +1 -1
  83. package/dist/dsl/action.d.cts +12 -11
  84. package/dist/dsl/action.d.cts.map +1 -1
  85. package/dist/dsl/action.d.mts +12 -11
  86. package/dist/dsl/action.d.mts.map +1 -1
  87. package/dist/dsl/action.mjs +3 -6
  88. package/dist/dsl/action.mjs.map +1 -1
  89. package/dist/dsl/bundle.cjs +35 -31
  90. package/dist/dsl/bundle.cjs.map +1 -1
  91. package/dist/dsl/bundle.d.cts.map +1 -1
  92. package/dist/dsl/bundle.d.mts.map +1 -1
  93. package/dist/dsl/bundle.mjs +35 -31
  94. package/dist/dsl/bundle.mjs.map +1 -1
  95. package/dist/dsl/condition.cjs +13 -1
  96. package/dist/dsl/condition.cjs.map +1 -1
  97. package/dist/dsl/condition.d.cts +9 -1
  98. package/dist/dsl/condition.d.cts.map +1 -1
  99. package/dist/dsl/condition.d.mts +9 -1
  100. package/dist/dsl/condition.d.mts.map +1 -1
  101. package/dist/dsl/condition.mjs +13 -2
  102. package/dist/dsl/condition.mjs.map +1 -1
  103. package/dist/dsl/flow.cjs +33 -11
  104. package/dist/dsl/flow.cjs.map +1 -1
  105. package/dist/dsl/flow.d.cts +9 -1
  106. package/dist/dsl/flow.d.cts.map +1 -1
  107. package/dist/dsl/flow.d.mts +9 -1
  108. package/dist/dsl/flow.d.mts.map +1 -1
  109. package/dist/dsl/flow.mjs +34 -13
  110. package/dist/dsl/flow.mjs.map +1 -1
  111. package/dist/dsl/workflow.cjs +2 -3
  112. package/dist/dsl/workflow.cjs.map +1 -1
  113. package/dist/dsl/workflow.d.cts.map +1 -1
  114. package/dist/dsl/workflow.d.mts.map +1 -1
  115. package/dist/dsl/workflow.mjs +3 -4
  116. package/dist/dsl/workflow.mjs.map +1 -1
  117. package/dist/engine/actions.cjs +1 -8
  118. package/dist/engine/actions.cjs.map +1 -1
  119. package/dist/engine/actions.d.cts +1 -12
  120. package/dist/engine/actions.d.cts.map +1 -1
  121. package/dist/engine/actions.d.mts +1 -12
  122. package/dist/engine/actions.d.mts.map +1 -1
  123. package/dist/engine/actions.mjs +1 -8
  124. package/dist/engine/actions.mjs.map +1 -1
  125. package/dist/engine/condition.cjs +25 -5
  126. package/dist/engine/condition.cjs.map +1 -1
  127. package/dist/engine/condition.d.cts +12 -3
  128. package/dist/engine/condition.d.cts.map +1 -1
  129. package/dist/engine/condition.d.mts +12 -3
  130. package/dist/engine/condition.d.mts.map +1 -1
  131. package/dist/engine/condition.mjs +25 -6
  132. package/dist/engine/condition.mjs.map +1 -1
  133. package/dist/engine/index.cjs +3 -0
  134. package/dist/engine/index.d.cts +5 -5
  135. package/dist/engine/index.d.mts +5 -5
  136. package/dist/engine/index.mjs +4 -4
  137. package/dist/engine/interpreter.cjs +27 -20
  138. package/dist/engine/interpreter.cjs.map +1 -1
  139. package/dist/engine/interpreter.d.cts.map +1 -1
  140. package/dist/engine/interpreter.d.mts.map +1 -1
  141. package/dist/engine/interpreter.mjs +27 -20
  142. package/dist/engine/interpreter.mjs.map +1 -1
  143. package/dist/engine/ports.d.cts +15 -6
  144. package/dist/engine/ports.d.cts.map +1 -1
  145. package/dist/engine/ports.d.mts +15 -6
  146. package/dist/engine/ports.d.mts.map +1 -1
  147. package/dist/engine/retry.cjs +13 -0
  148. package/dist/engine/retry.cjs.map +1 -1
  149. package/dist/engine/retry.d.cts +12 -0
  150. package/dist/engine/retry.d.cts.map +1 -1
  151. package/dist/engine/retry.d.mts +12 -0
  152. package/dist/engine/retry.d.mts.map +1 -1
  153. package/dist/engine/retry.mjs +13 -1
  154. package/dist/engine/retry.mjs.map +1 -1
  155. package/dist/engine/time-scale.cjs +90 -15
  156. package/dist/engine/time-scale.cjs.map +1 -1
  157. package/dist/engine/time-scale.d.cts +9 -6
  158. package/dist/engine/time-scale.d.cts.map +1 -1
  159. package/dist/engine/time-scale.d.mts +9 -6
  160. package/dist/engine/time-scale.d.mts.map +1 -1
  161. package/dist/engine/time-scale.mjs +90 -16
  162. package/dist/engine/time-scale.mjs.map +1 -1
  163. package/dist/fcm.cjs +111 -38
  164. package/dist/fcm.cjs.map +1 -1
  165. package/dist/fcm.d.cts +45 -5
  166. package/dist/fcm.d.cts.map +1 -1
  167. package/dist/fcm.d.mts +45 -5
  168. package/dist/fcm.d.mts.map +1 -1
  169. package/dist/fcm.mjs +110 -39
  170. package/dist/fcm.mjs.map +1 -1
  171. package/dist/hash.cjs +6 -6
  172. package/dist/hash.cjs.map +1 -1
  173. package/dist/hash.mjs +6 -6
  174. package/dist/hash.mjs.map +1 -1
  175. package/dist/host/auth.cjs +49 -0
  176. package/dist/host/auth.cjs.map +1 -0
  177. package/dist/host/auth.d.cts +35 -0
  178. package/dist/host/auth.d.cts.map +1 -0
  179. package/dist/host/auth.d.mts +35 -0
  180. package/dist/host/auth.d.mts.map +1 -0
  181. package/dist/host/auth.mjs +46 -0
  182. package/dist/host/auth.mjs.map +1 -0
  183. package/dist/host/channels.cjs +118 -40
  184. package/dist/host/channels.cjs.map +1 -1
  185. package/dist/host/channels.d.cts +27 -1
  186. package/dist/host/channels.d.cts.map +1 -1
  187. package/dist/host/channels.d.mts +27 -1
  188. package/dist/host/channels.d.mts.map +1 -1
  189. package/dist/host/channels.mjs +121 -44
  190. package/dist/host/channels.mjs.map +1 -1
  191. package/dist/host/execute.cjs +14 -2
  192. package/dist/host/execute.cjs.map +1 -1
  193. package/dist/host/execute.d.cts +4 -4
  194. package/dist/host/execute.d.cts.map +1 -1
  195. package/dist/host/execute.d.mts +4 -4
  196. package/dist/host/execute.d.mts.map +1 -1
  197. package/dist/host/execute.mjs +13 -1
  198. package/dist/host/execute.mjs.map +1 -1
  199. package/dist/host/fresh-read.cjs +30 -0
  200. package/dist/host/fresh-read.cjs.map +1 -0
  201. package/dist/host/fresh-read.d.cts +18 -0
  202. package/dist/host/fresh-read.d.cts.map +1 -0
  203. package/dist/host/fresh-read.d.mts +18 -0
  204. package/dist/host/fresh-read.d.mts.map +1 -0
  205. package/dist/host/fresh-read.mjs +29 -0
  206. package/dist/host/fresh-read.mjs.map +1 -0
  207. package/dist/host/http.cjs +340 -0
  208. package/dist/host/http.cjs.map +1 -0
  209. package/dist/host/http.d.cts +58 -0
  210. package/dist/host/http.d.cts.map +1 -0
  211. package/dist/host/http.d.mts +58 -0
  212. package/dist/host/http.d.mts.map +1 -0
  213. package/dist/host/http.mjs +336 -0
  214. package/dist/host/http.mjs.map +1 -0
  215. package/dist/host/{router.cjs → ingest.cjs} +147 -175
  216. package/dist/host/ingest.cjs.map +1 -0
  217. package/dist/host/{router.d.cts → ingest.d.cts} +48 -30
  218. package/dist/host/ingest.d.cts.map +1 -0
  219. package/dist/host/{router.d.mts → ingest.d.mts} +48 -30
  220. package/dist/host/ingest.d.mts.map +1 -0
  221. package/dist/host/{router.mjs → ingest.mjs} +144 -173
  222. package/dist/host/ingest.mjs.map +1 -0
  223. package/dist/host/journey-host.cjs +66 -93
  224. package/dist/host/journey-host.cjs.map +1 -1
  225. package/dist/host/journey-host.d.cts +31 -26
  226. package/dist/host/journey-host.d.cts.map +1 -1
  227. package/dist/host/journey-host.d.mts +31 -26
  228. package/dist/host/journey-host.d.mts.map +1 -1
  229. package/dist/host/journey-host.mjs +66 -93
  230. package/dist/host/journey-host.mjs.map +1 -1
  231. package/dist/host/platform.cjs +21 -0
  232. package/dist/host/platform.cjs.map +1 -0
  233. package/dist/host/platform.d.cts +126 -0
  234. package/dist/host/platform.d.cts.map +1 -0
  235. package/dist/host/platform.d.mts +126 -0
  236. package/dist/host/platform.d.mts.map +1 -0
  237. package/dist/host/platform.mjs +19 -0
  238. package/dist/host/platform.mjs.map +1 -0
  239. package/dist/host/senders.cjs +18 -9
  240. package/dist/host/senders.cjs.map +1 -1
  241. package/dist/host/senders.d.cts +9 -4
  242. package/dist/host/senders.d.cts.map +1 -1
  243. package/dist/host/senders.d.mts +9 -4
  244. package/dist/host/senders.d.mts.map +1 -1
  245. package/dist/host/senders.mjs +18 -9
  246. package/dist/host/senders.mjs.map +1 -1
  247. package/dist/index.cjs +1 -1
  248. package/dist/index.mjs +1 -1
  249. package/dist/ir.cjs +5 -7
  250. package/dist/ir.cjs.map +1 -1
  251. package/dist/ir.d.cts +3 -12
  252. package/dist/ir.d.cts.map +1 -1
  253. package/dist/ir.d.mts +3 -12
  254. package/dist/ir.d.mts.map +1 -1
  255. package/dist/ir.mjs +5 -7
  256. package/dist/ir.mjs.map +1 -1
  257. package/dist/plan.cjs +27 -33
  258. package/dist/plan.cjs.map +1 -1
  259. package/dist/plan.d.cts +1 -1
  260. package/dist/plan.d.cts.map +1 -1
  261. package/dist/plan.d.mts +1 -1
  262. package/dist/plan.d.mts.map +1 -1
  263. package/dist/plan.mjs +27 -33
  264. package/dist/plan.mjs.map +1 -1
  265. package/dist/reporting/tracking.cjs +14 -4
  266. package/dist/reporting/tracking.cjs.map +1 -1
  267. package/dist/reporting/tracking.d.cts +10 -10
  268. package/dist/reporting/tracking.d.cts.map +1 -1
  269. package/dist/reporting/tracking.d.mts +10 -10
  270. package/dist/reporting/tracking.d.mts.map +1 -1
  271. package/dist/reporting/tracking.mjs +14 -4
  272. package/dist/reporting/tracking.mjs.map +1 -1
  273. package/dist/store/d1.cjs +88 -29
  274. package/dist/store/d1.cjs.map +1 -1
  275. package/dist/store/d1.d.cts +29 -11
  276. package/dist/store/d1.d.cts.map +1 -1
  277. package/dist/store/d1.d.mts +29 -11
  278. package/dist/store/d1.d.mts.map +1 -1
  279. package/dist/store/d1.mjs +88 -29
  280. package/dist/store/d1.mjs.map +1 -1
  281. package/dist/store/erasure.cjs +22 -0
  282. package/dist/store/erasure.cjs.map +1 -0
  283. package/dist/store/erasure.d.cts +18 -0
  284. package/dist/store/erasure.d.cts.map +1 -0
  285. package/dist/store/erasure.d.mts +18 -0
  286. package/dist/store/erasure.d.mts.map +1 -0
  287. package/dist/store/erasure.mjs +21 -0
  288. package/dist/store/erasure.mjs.map +1 -0
  289. package/dist/store/index.cjs +2 -0
  290. package/dist/store/index.cjs.map +1 -1
  291. package/dist/store/index.d.cts +67 -13
  292. package/dist/store/index.d.cts.map +1 -1
  293. package/dist/store/index.d.mts +67 -13
  294. package/dist/store/index.d.mts.map +1 -1
  295. package/dist/store/index.mjs +2 -1
  296. package/dist/store/index.mjs.map +1 -1
  297. package/dist/store/postgres-reporting.cjs +125 -62
  298. package/dist/store/postgres-reporting.cjs.map +1 -1
  299. package/dist/store/postgres-reporting.d.cts +30 -2
  300. package/dist/store/postgres-reporting.d.cts.map +1 -1
  301. package/dist/store/postgres-reporting.d.mts +30 -2
  302. package/dist/store/postgres-reporting.d.mts.map +1 -1
  303. package/dist/store/postgres-reporting.mjs +125 -62
  304. package/dist/store/postgres-reporting.mjs.map +1 -1
  305. package/dist/store/postgres.cjs +179 -60
  306. package/dist/store/postgres.cjs.map +1 -1
  307. package/dist/store/postgres.d.cts +44 -7
  308. package/dist/store/postgres.d.cts.map +1 -1
  309. package/dist/store/postgres.d.mts +44 -7
  310. package/dist/store/postgres.d.mts.map +1 -1
  311. package/dist/store/postgres.mjs +179 -60
  312. package/dist/store/postgres.mjs.map +1 -1
  313. package/dist/store/tables.cjs +7 -5
  314. package/dist/store/tables.cjs.map +1 -1
  315. package/dist/store/tables.d.cts.map +1 -1
  316. package/dist/store/tables.d.mts.map +1 -1
  317. package/dist/store/tables.mjs +7 -5
  318. package/dist/store/tables.mjs.map +1 -1
  319. package/dist/store/user-lock.cjs +43 -0
  320. package/dist/store/user-lock.cjs.map +1 -0
  321. package/dist/store/user-lock.d.cts +32 -0
  322. package/dist/store/user-lock.d.cts.map +1 -0
  323. package/dist/store/user-lock.d.mts +32 -0
  324. package/dist/store/user-lock.d.mts.map +1 -0
  325. package/dist/store/user-lock.mjs +40 -0
  326. package/dist/store/user-lock.mjs.map +1 -0
  327. package/dist/transports/cloudflare-email.cjs +1 -1
  328. package/dist/transports/cloudflare-email.mjs +1 -1
  329. package/dist/transports/ses.cjs +47 -6
  330. package/dist/transports/ses.cjs.map +1 -1
  331. package/dist/transports/ses.d.cts +9 -1
  332. package/dist/transports/ses.d.cts.map +1 -1
  333. package/dist/transports/ses.d.mts +9 -1
  334. package/dist/transports/ses.d.mts.map +1 -1
  335. package/dist/transports/ses.mjs +47 -7
  336. package/dist/transports/ses.mjs.map +1 -1
  337. package/dist/webpush.cjs +133 -47
  338. package/dist/webpush.cjs.map +1 -1
  339. package/dist/webpush.d.cts +49 -8
  340. package/dist/webpush.d.cts.map +1 -1
  341. package/dist/webpush.d.mts +49 -8
  342. package/dist/webpush.d.mts.map +1 -1
  343. package/dist/webpush.mjs +130 -47
  344. package/dist/webpush.mjs.map +1 -1
  345. package/package.json +1 -1
  346. package/src/cloudflare/schema.sql +27 -6
  347. package/dist/host/router.cjs.map +0 -1
  348. package/dist/host/router.d.cts.map +0 -1
  349. package/dist/host/router.d.mts.map +0 -1
  350. package/dist/host/router.mjs.map +0 -1
@@ -62,6 +62,12 @@ export interface TemplateUse {
62
62
  }
63
63
  /** @internal The template definitions a workflow sends, sub-flows and fragments included. */
64
64
  export declare function templateUses(builder: FlowBuilder): readonly TemplateUse[];
65
+ /**
66
+ * @internal The action definitions a workflow runs, sub-flows and fragments included. The IR
67
+ * names an action only; compileBundle checks that each ref is the very object listed in
68
+ * `actions`, which is what keeps two definitions from sharing one name. Never part of the IR.
69
+ */
70
+ export declare function actionUses(builder: FlowBuilder): readonly ActionRef[];
65
71
  /** Sub-flow: a named fragment, or an inline callback (runs once at construction — builder in, builder out). */
66
72
  export type SubFlow = Flow | ((w: FlowBuilder) => FlowBuilder);
67
73
  /** A branch arm: the [condition, sub-flow] tuple — an [if, then] ordering convention. */
@@ -206,7 +212,7 @@ export interface FlowBuilder {
206
212
  sendEvent<P extends object>(event: EventRef<P>, ...payload: EmitArgs<P>): this;
207
213
  /**
208
214
  * Run a custom action (see dsl/action.ts for the model). The chain records
209
- * identity only — name, args, codeHash — never the code: the implementation
215
+ * identity only — name and args — never the code: the implementation
210
216
  * ships inside the deployed Worker bundle and is resolved by name from the
211
217
  * runtime registry. Args are typed by the ActionRef and may reference u.xxx,
212
218
  * resolved at run time exactly like message props. Executed inside a durable
@@ -220,6 +226,8 @@ export declare class FlowBuilderImpl implements FlowBuilder {
220
226
  readonly nodes: NodeIR[];
221
227
  /** The template definitions sent, with where: compile-time only, never in the IR. */
222
228
  readonly uses: TemplateUse[];
229
+ /** The action definitions run: compile-time identity only, never in the IR. */
230
+ readonly actions: ActionRef[];
223
231
  /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */
224
232
  private pushNode;
225
233
  private message;
@@ -1 +1 @@
1
- {"version":3,"file":"flow.d.cts","names":[],"sources":["../../src/dsl/flow.ts"],"mappings":"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"}
1
+ {"version":3,"file":"flow.d.cts","names":[],"sources":["../../src/dsl/flow.ts"],"mappings":"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"}
@@ -62,6 +62,12 @@ export interface TemplateUse {
62
62
  }
63
63
  /** @internal The template definitions a workflow sends, sub-flows and fragments included. */
64
64
  export declare function templateUses(builder: FlowBuilder): readonly TemplateUse[];
65
+ /**
66
+ * @internal The action definitions a workflow runs, sub-flows and fragments included. The IR
67
+ * names an action only; compileBundle checks that each ref is the very object listed in
68
+ * `actions`, which is what keeps two definitions from sharing one name. Never part of the IR.
69
+ */
70
+ export declare function actionUses(builder: FlowBuilder): readonly ActionRef[];
65
71
  /** Sub-flow: a named fragment, or an inline callback (runs once at construction — builder in, builder out). */
66
72
  export type SubFlow = Flow | ((w: FlowBuilder) => FlowBuilder);
67
73
  /** A branch arm: the [condition, sub-flow] tuple — an [if, then] ordering convention. */
@@ -206,7 +212,7 @@ export interface FlowBuilder {
206
212
  sendEvent<P extends object>(event: EventRef<P>, ...payload: EmitArgs<P>): this;
207
213
  /**
208
214
  * Run a custom action (see dsl/action.ts for the model). The chain records
209
- * identity only — name, args, codeHash — never the code: the implementation
215
+ * identity only — name and args — never the code: the implementation
210
216
  * ships inside the deployed Worker bundle and is resolved by name from the
211
217
  * runtime registry. Args are typed by the ActionRef and may reference u.xxx,
212
218
  * resolved at run time exactly like message props. Executed inside a durable
@@ -220,6 +226,8 @@ export declare class FlowBuilderImpl implements FlowBuilder {
220
226
  readonly nodes: NodeIR[];
221
227
  /** The template definitions sent, with where: compile-time only, never in the IR. */
222
228
  readonly uses: TemplateUse[];
229
+ /** The action definitions run: compile-time identity only, never in the IR. */
230
+ readonly actions: ActionRef[];
223
231
  /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */
224
232
  private pushNode;
225
233
  private message;
@@ -1 +1 @@
1
- {"version":3,"file":"flow.d.mts","names":[],"sources":["../../src/dsl/flow.ts"],"mappings":"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"}
1
+ {"version":3,"file":"flow.d.mts","names":[],"sources":["../../src/dsl/flow.ts"],"mappings":"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"}
package/dist/dsl/flow.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { captureLoc } from "../provenance.mjs";
2
2
  import { durationIR, timeOfDayMinutes } from "./base.mjs";
3
- import { condIR } from "./condition.mjs";
3
+ import { condIR, positiveDurationIR } from "./condition.mjs";
4
4
  import { CHANNEL_OF, isTemplate } from "../templates.mjs";
5
5
  //#region src/dsl/flow.ts
6
6
  /** prefer() is typed strictly, but a JS caller (or an `any`) reaches it unchecked — fail at the call, not at send time. */
@@ -22,6 +22,14 @@ function templateUses(builder) {
22
22
  return builder instanceof FlowBuilderImpl ? builder.uses : [];
23
23
  }
24
24
  /**
25
+ * @internal The action definitions a workflow runs, sub-flows and fragments included. The IR
26
+ * names an action only; compileBundle checks that each ref is the very object listed in
27
+ * `actions`, which is what keeps two definitions from sharing one name. Never part of the IR.
28
+ */
29
+ function actionUses(builder) {
30
+ return builder instanceof FlowBuilderImpl ? builder.actions : [];
31
+ }
32
+ /**
25
33
  * A cohort arm name has two jobs, and both constrain its spelling:
26
34
  * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),
27
35
  * and an id is a durable step name — a dot would fake a nesting level and can
@@ -33,15 +41,17 @@ function templateUses(builder) {
33
41
  * experiment would prepend itself and re-bucket everyone already in it.
34
42
  */
35
43
  const ARM_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
36
- /** A sub-flow's nodes; the template definitions it sends are added to `uses`. */
37
- function resolveSubFlow(sub, uses) {
44
+ /** A sub-flow's nodes; the template definitions it sends and the actions it runs are added to `uses` / `actions`. */
45
+ function resolveSubFlow(sub, uses, actions) {
38
46
  if (typeof sub === "function") {
39
47
  const builder = new FlowBuilderImpl();
40
48
  if (typeof sub(builder)?.then === "function") throw new TypeError("sub-flow callbacks must be synchronous: they run once, while the workflow is defined");
41
49
  uses.push(...builder.uses);
50
+ actions.push(...builder.actions);
42
51
  return builder.nodes;
43
52
  }
44
53
  uses.push(...sub.uses);
54
+ actions.push(...sub.actions);
45
55
  return structuredClone(sub.nodes);
46
56
  }
47
57
  const DEFINED_CHANNELS = new Set(Object.values(CHANNEL_OF));
@@ -66,6 +76,8 @@ var FlowBuilderImpl = class {
66
76
  nodes = [];
67
77
  /** The template definitions sent, with where: compile-time only, never in the IR. */
68
78
  uses = [];
79
+ /** The action definitions run: compile-time identity only, never in the IR. */
80
+ actions = [];
69
81
  /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */
70
82
  pushNode(node, loc) {
71
83
  this.nodes.push({
@@ -156,6 +168,14 @@ var FlowBuilderImpl = class {
156
168
  }
157
169
  timeWindow(opts) {
158
170
  const loc = captureLoc(this.timeWindow);
171
+ if (opts.days !== void 0) {
172
+ if (opts.days.length === 0) throw new Error("timeWindow(): 'days' must name at least one day");
173
+ const seen = /* @__PURE__ */ new Set();
174
+ for (const day of opts.days) {
175
+ if (seen.has(day)) throw new Error(`timeWindow(): 'days' lists '${day}' twice`);
176
+ seen.add(day);
177
+ }
178
+ }
159
179
  const start = timeOfDayMinutes(opts.between[0]);
160
180
  if (timeOfDayMinutes(opts.between[1]) <= start) throw new Error(`timeWindow(): 'between' must open before it closes within one day, got [${opts.between[0]}, ${opts.between[1]}] (overnight windows are not supported yet)`);
161
181
  return this.pushNode({
@@ -181,12 +201,12 @@ var FlowBuilderImpl = class {
181
201
  id: "",
182
202
  type: "wait_until",
183
203
  condition: condIR(condition),
184
- timeout: durationIR(opts.timeout),
185
- onTimeout: onTimeout === "continue" || onTimeout === "exit" ? onTimeout : resolveSubFlow(onTimeout, this.uses)
204
+ timeout: positiveDurationIR(opts.timeout, "waitUntil(): timeout"),
205
+ onTimeout: onTimeout === "continue" || onTimeout === "exit" ? onTimeout : resolveSubFlow(onTimeout, this.uses, this.actions)
186
206
  }, loc);
187
207
  }
188
208
  use(fragment) {
189
- this.nodes.push(...resolveSubFlow(fragment, this.uses));
209
+ this.nodes.push(...resolveSubFlow(fragment, this.uses, this.actions));
190
210
  return this;
191
211
  }
192
212
  branch(...args) {
@@ -201,12 +221,12 @@ var FlowBuilderImpl = class {
201
221
  const [condition, sub] = arm;
202
222
  cases.push({
203
223
  condition: condIR(condition),
204
- flow: resolveSubFlow(sub, this.uses)
224
+ flow: resolveSubFlow(sub, this.uses, this.actions)
205
225
  });
206
226
  } else {
207
227
  if (otherwise !== void 0) throw new Error("branch(): only one default arm (bare sub-flow) is allowed");
208
228
  if (i !== arms.length - 1) throw new Error("branch(): default arm (bare sub-flow) must be the last argument");
209
- otherwise = resolveSubFlow(arm, this.uses);
229
+ otherwise = resolveSubFlow(arm, this.uses, this.actions);
210
230
  }
211
231
  });
212
232
  return this.pushNode({
@@ -243,7 +263,7 @@ var FlowBuilderImpl = class {
243
263
  arms: entries.map(([name, a]) => ({
244
264
  name,
245
265
  weight: a.weight,
246
- flow: a.flow ? resolveSubFlow(a.flow, this.uses) : []
266
+ flow: a.flow ? resolveSubFlow(a.flow, this.uses, this.actions) : []
247
267
  }))
248
268
  }, loc);
249
269
  }
@@ -269,12 +289,12 @@ var FlowBuilderImpl = class {
269
289
  }, captureLoc(this.sendEvent));
270
290
  }
271
291
  run(a, ...args) {
292
+ this.actions.push(a);
272
293
  return this.pushNode({
273
294
  id: "",
274
295
  type: "action",
275
296
  action: a.name,
276
- args: args[0] ?? {},
277
- codeHash: a.codeHash
297
+ args: args[0] ?? {}
278
298
  }, captureLoc(this.run));
279
299
  }
280
300
  };
@@ -288,10 +308,11 @@ function flow(build) {
288
308
  return {
289
309
  __flow: true,
290
310
  nodes: builder.nodes,
291
- uses: builder.uses
311
+ uses: builder.uses,
312
+ actions: builder.actions
292
313
  };
293
314
  }
294
315
  //#endregion
295
- export { FlowBuilderImpl, flow, templateUses };
316
+ export { FlowBuilderImpl, actionUses, flow, templateUses };
296
317
 
297
318
  //# sourceMappingURL=flow.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"flow.mjs","names":[],"sources":["../../src/dsl/flow.ts"],"sourcesContent":["import type { ChannelIR, ConditionIR, NodeIR, PropValueIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport {\n type AnyTemplate,\n CHANNEL_OF,\n type DiscordTemplate,\n type EmailTemplate,\n type PropsSchema,\n type PushTemplate,\n type SlackTemplate,\n isTemplate,\n} from '../templates';\nimport type { ActionRef } from './action';\nimport { type Duration, type TimeOfDay, type Weekday, durationIR, timeOfDayMinutes } from './base';\nimport { type Condition, condIR } from './condition';\nimport type { EventRef, MessageArgs, PropInput, PropsInput } from './refs';\nimport type { PropsOf, TemplateRef } from './template';\n\n/**\n * The flow chain — FlowBuilder is the DSL's step surface (messages, delays,\n * flow control, data), and its implementation is the compiler front half: each\n * chained call executes no logic, it just collects one IR node (ids are\n * assigned later, in workflow.toIR()). Sub-flow callbacks run exactly once at\n * construction — builder in, builder out — so the output is always a static\n * tree.\n *\n * Every public method also captures its callsite (provenance): the loc lands\n * in the node's hash-excluded meta and is the anchor for future in-place\n * editing from the studio.\n */\n\ntype Scalar = string | number | boolean;\n\n/** An emitted field: a scalar (or a profile ref to one); a nested field cannot be sent. */\ntype EmitValue<T> = [NonNullable<T>] extends [Scalar] ? PropInput<T> : never;\n\n/** The payload `sendEvent` accepts for an event: its fields, scalar ones only. */\nexport type EmitPayload<P> = { [K in keyof P]: EmitValue<P[K]> };\n\n/** Payload may be omitted when the event declares no required field. */\nexport type EmitArgs<P> =\n Record<string, never> extends P ? [payload?: EmitPayload<P>] : [payload: EmitPayload<P>];\n\n/** Per-node push options. */\nexport interface PushOptions {\n /** Which devices get a copy: 'all' (the default) or only the most recently registered. */\n devices?: 'all' | 'latest';\n}\n\n/** `prefer()` options. */\nexport interface PreferOptions {\n /** The generated branch's name in IR (UI title / observability handle), as `branch(label, …)`. */\n label?: string;\n}\n\n/** The candidates `prefer()` takes: one push and one email template, in priority order. */\ntype PreferPair = readonly [PushTemplate, EmailTemplate] | readonly [EmailTemplate, PushTemplate];\n\ntype NormalizeProps<P> = { -readonly [K in keyof P]: P[K] };\ntype Equal<A, B> =\n (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;\n\n/** Both candidates render the one props object, so they must declare the same props type — not merely overlap. */\ntype SameProps<T extends PreferPair> =\n Equal<NormalizeProps<PropsOf<T[0]>>, NormalizeProps<PropsOf<T[1]>>> extends true\n ? unknown\n : { readonly __prefer_error__: 'Both templates must declare the same props type' };\n\n/** Distributes, so a field only one member of a union variable has still counts as extra. */\ntype KeysOfUnion<A> = A extends unknown ? keyof A : never;\n/** Rejects known extra fields, which `{}` (an empty props schema) would otherwise accept. */\ntype NoExtra<A, P> = Record<Exclude<KeysOfUnion<A>, keyof P>, never>;\n\n/** prefer() is typed strictly, but a JS caller (or an `any`) reaches it unchecked — fail at the call, not at send time. */\nfunction checkPreferArgs(candidates: unknown, props: unknown, options: unknown): void {\n if (!Array.isArray(candidates) || candidates.length !== 2) {\n throw new TypeError('prefer(): candidates must be exactly two templates — one push, one email');\n }\n const channels = (candidates as unknown[]).map((candidate) =>\n isTemplate(candidate) ? CHANNEL_OF[candidate.kind] : undefined\n );\n if (!channels.every((channel) => channel === 'push' || channel === 'email')) {\n throw new TypeError('prefer(): each candidate must be a pushTemplate or an emailTemplate');\n }\n if (channels[0] === channels[1]) {\n throw new TypeError('prefer(): candidates must be one push and one email template');\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n throw new TypeError(\n 'prefer(): props must be an object — pass {} when the templates declare none'\n );\n }\n if (options === undefined) return;\n if (typeof options !== 'object' || options === null || Array.isArray(options)) {\n throw new TypeError('prefer(): options must be an object');\n }\n for (const [name, value] of Object.entries(options)) {\n if (name !== 'label') {\n throw new TypeError(\n `prefer(): unknown option '${name}' — props go in the second argument, options take only 'label'`\n );\n }\n if (typeof value !== 'string') throw new TypeError(\"prefer(): 'label' must be a string\");\n }\n}\n\n/** A reusable flow fragment (what `flow(...)` returns), referenced directly by branch arms. */\nexport interface Flow {\n readonly __flow: true;\n}\n\ninterface FlowInternal extends Flow {\n readonly nodes: NodeIR[];\n readonly uses: readonly TemplateUse[];\n}\n\n/**\n * @internal A template definition a flow sends, kept beside the IR (which names it by id only):\n * compileConfig checks that the registered definition under that id is this very object, so the\n * schema that typed the call is the one that renders it. Never part of the IR or its hash.\n */\nexport interface TemplateUse {\n readonly template: AnyTemplate;\n readonly loc: SourceLocIR | undefined;\n}\n\n/** @internal The template definitions a workflow sends, sub-flows and fragments included. */\nexport function templateUses(builder: FlowBuilder): readonly TemplateUse[] {\n return builder instanceof FlowBuilderImpl ? builder.uses : [];\n}\n\n/** Sub-flow: a named fragment, or an inline callback (runs once at construction — builder in, builder out). */\nexport type SubFlow = Flow | ((w: FlowBuilder) => FlowBuilder);\n\n/** A branch arm: the [condition, sub-flow] tuple — an [if, then] ordering convention. */\nexport type BranchCase = readonly [condition: Condition, flow: SubFlow];\n\n/**\n * A branch argument: either a tuple arm, or a bare sub-flow meaning the default\n * arm. The bare form may appear once, and must come last (checked at runtime).\n */\nexport type BranchArm = BranchCase | SubFlow;\n\n/**\n * A cohort arm name has two jobs, and both constrain its spelling:\n * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),\n * and an id is a durable step name — a dot would fake a nesting level and can\n * collide with a real one, an empty name gives '{id}..{j}';\n * - it is an object key, and JavaScript lists integer-like keys first in\n * ascending numeric order however the author wrote them. The arms array is\n * documented (and interpreted) as authoring order, and the bucketing walk\n * accumulates weights along it, so an arm named '0' added to a live\n * experiment would prepend itself and re-bucket everyone already in it.\n */\nconst ARM_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/** A sub-flow's nodes; the template definitions it sends are added to `uses`. */\nfunction resolveSubFlow(sub: SubFlow, uses: TemplateUse[]): NodeIR[] {\n if (typeof sub === 'function') {\n const builder = new FlowBuilderImpl();\n const returned: unknown = sub(builder);\n // An async callback would add its nodes after construction is over — after ids were assigned\n if (typeof (returned as { then?: unknown } | undefined)?.then === 'function') {\n throw new TypeError(\n 'sub-flow callbacks must be synchronous: they run once, while the workflow is defined'\n );\n }\n uses.push(...builder.uses);\n return builder.nodes;\n }\n uses.push(...(sub as FlowInternal).uses);\n /*\n * A named fragment is a value, and the convention is to reuse it — two branch\n * arms may hold the same one. Handing out its own nodes aliases them into\n * both places, and assignIds writes ids *in place*: the second prefix\n * overwrites the first, so the two placements end up as one object under one\n * id — one durable step name, one message idempotency key, one bucket seed.\n * A copy per placement is what makes a fragment behave like a value.\n */\n return structuredClone((sub as FlowInternal).nodes);\n}\n\nexport interface FlowBuilder {\n /* ---- Messages: every channel is one message node; separate methods buy channel-level typing ---- */\n email<T extends EmailTemplate>(template: T, ...props: MessageArgs<PropsOf<T>>): this;\n sms<P extends object>(template: TemplateRef<'sms', P>, ...props: MessageArgs<P>): this;\n /**\n * Push to the user's devices — every device the host's directory lists, or\n * only the most recently registered with `{ devices: 'latest' }`. A user with\n * no device is skipped (`prefer([pushTpl, emailTpl], props)` falls back to email).\n */\n push<T extends PushTemplate>(\n template: T,\n ...args: [...props: MessageArgs<PropsOf<T>>, options?: PushOptions]\n ): this;\n inApp<P extends object>(template: TemplateRef<'in_app', P>, ...props: MessageArgs<P>): this;\n slack<T extends SlackTemplate<PropsSchema, never>>(\n template: T,\n ...props: MessageArgs<PropsOf<T>>\n ): this;\n discord<T extends DiscordTemplate<PropsSchema, never>>(\n template: T,\n ...props: MessageArgs<PropsOf<T>>\n ): this;\n survey<P extends object>(template: TemplateRef<'survey', P>, ...props: MessageArgs<P>): this;\n\n /**\n * Send one message on the first channel the user can be reached on: one push\n * and one email template, in priority order, rendered with the same props.\n * Sugar for the branch it compiles to — same IR, ids and hash as writing\n *\n * .branch(label, [reachable(first.channel), (b) => b.<first>(first, props)], (b) => b.<second>(second, props))\n *\n * so the choice is made once, when the step is reached, and a first choice\n * that fails to send retries as itself — it never falls through to the\n * second. Props are always passed (`{}` for templates that declare none),\n * and both templates must declare the same props type; for differing props\n * or other channels, write the branch.\n *\n * .prefer([push.tip, email.tip], { name: u.name }, { label: 'tip by channel' })\n */\n prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<PropsOf<T[0]>>>>(\n candidates: T & SameProps<NoInfer<T>>,\n props: A & NoInfer<NoExtra<A, PropsOf<T[0]>>>,\n options?: PreferOptions\n ): this;\n\n /* --------------------------------- Delays --------------------------------- */\n /** Time Delay; passing { min, max } makes it a Randomized Delay. */\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n\n /** Time Window: hold until inside the window (e.g. weekdays 09:00–17:00). tz: 'user' uses the user's timezone. */\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this;\n\n /**\n * Wait Until: continue on the main line once the condition holds, or take\n * onTimeout when it expires (default 'continue'). On the engine side this is\n * \"await event racing a timer\" — a native primitive of durable runtimes.\n */\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this;\n\n /**\n * Splice a fragment in place: a named `flow(...)` or a callback, expanded\n * once while the workflow is defined — a composition step before compiling,\n * not a node (the IR has no trace of it, and ids are assigned over the whole\n * workflow as if the steps were written inline). Each use gets its own copy,\n * so one fragment can be used in several places and workflows.\n *\n * const officeHours = flow((w) => w.timeWindow({ days: ['mon', 'tue', 'wed', 'thu', 'fri'], between: ['09:00', '17:00'] }));\n * workflow({ id: 'onboarding', ... }).delay('1 day').use(officeHours).email(email.tip, { name: u.name })\n */\n use(fragment: SubFlow): this;\n\n /* ------------------------------ Flow Control ------------------------------ */\n /**\n * Conditional branch: ordered first-match (same semantics as Step Functions\n * Choice / Knock branch). This one primitive covers both the UI's True/False\n * Branch (two arms) and Multi-Split Branch (N arms). Arms are\n * [condition, sub-flow] tuples; a bare sub-flow in tail position is the\n * default arm. With no match and no default, execution simply continues.\n * Rejoin is structural: after the matching arm finishes, execution resumes\n * at the node following the branch — to opt out, call .exit() inside the arm.\n *\n * .branch([activeSubscriber, upgradeFlow], firstTimeFlow) // True/False\n * .branch([vip, vipFlow], [trial, trialFlow], defaultFlow) // Multi-Split\n * .branch([not(usedStaging), eduFlow]) // single arm: no match continues\n *\n * The optional first argument `label` becomes the node's name in IR (UI title\n * / observability handle). It is not a jump target — there is no goto here;\n * looping is a future re-entry-policy topic (see sendEvent). Arm labels in\n * the UI are derived from the condition.\n */\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n\n /**\n * Gate: exit unless the condition holds, otherwise continue. A first-class\n * node rather than sugar over branch — same semantics as Zapier's \"only\n * continue if\" / Iterable's Filter tile, and rendered on its own in the UI.\n * `reason` is the audit-log line — metadata, so rewording it is free\n * (excluded from contentHash, see hash.ts).\n */\n filter(condition: Condition, opts?: { reason?: string }): this;\n\n /**\n * Random Cohort Branch (A/B): weights must sum to 100 (checked at runtime).\n * `key` names the experiment: bucketing hashes `userId:key` instead of the\n * structural node id, so assignments survive node insertion/moves and the\n * experiment keeps one identity across workflow versions — set it for any\n * A/B whose results you intend to read.\n *\n * Arm names become node id segments and are read back in authoring order, so\n * they are restricted to letters, digits, '_' and '-', never starting with a\n * digit (checked at runtime).\n */\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this;\n\n /**\n * Exit: end the whole workflow immediately (the UI's Exit node); `reason`\n * goes into the audit log and is metadata, so rewording it neither repins\n * in-flight journeys nor shows up in plan. Inside a branch arm this means\n * \"terminate, do not rejoin\" — it stops here instead of falling through,\n * while an arm without exit rejoins naturally.\n */\n exit(reason?: string): this;\n\n /* ---------------------------------- Data ---------------------------------- */\n /**\n * Emit a typed event (customer.io's Send Event). The event comes from an\n * e.xxx reference, so payload types flow in the same way trigger.event works.\n * This is how workflows compose: an event can trigger another workflow, and\n * the resulting call graph is statically analyzable. Payload values may\n * reference u.xxx. Only scalar fields can be sent (IR carries literals and\n * profile refs, not nested JSON): an event whose payload requires a nested\n * field cannot be emitted from a journey, and the types say so.\n *\n * Note on loops: a trailing self-triggering sendEvent will become the\n * official periodic-flow shape *once a re-entry policy exists* — today the\n * entry policy is once-per-user, so a self-trigger cannot re-enter and does\n * not loop.\n * TODO: the rest of the Data category — outbound webhook / profile update /\n * journey attributes.\n */\n sendEvent<P extends object>(event: EventRef<P>, ...payload: EmitArgs<P>): this;\n\n /**\n * Run a custom action (see dsl/action.ts for the model). The chain records\n * identity only — name, args, codeHash — never the code: the implementation\n * ships inside the deployed Worker bundle and is resolved by name from the\n * runtime registry. Args are typed by the ActionRef and may reference u.xxx,\n * resolved at run time exactly like message props. Executed inside a durable\n * step: retried on throw, checkpointed on success.\n */\n run<A extends object>(action: ActionRef<A>, ...args: MessageArgs<A>): this;\n}\n\nconst DEFINED_CHANNELS: ReadonlySet<ChannelIR> = new Set(Object.values(CHANNEL_OF));\n\n/** A message's template: a definition of the channel (named by its id), or a reference by key. */\ntype MessageTemplate = AnyTemplate | TemplateRef;\n\nfunction templateKey(channel: ChannelIR, tpl: MessageTemplate): string {\n if (!DEFINED_CHANNELS.has(channel)) return (tpl as TemplateRef).key;\n // Typed, but a JS caller (or an `any`) reaches it unchecked: fail where the workflow is written\n if (!isTemplate(tpl) || CHANNEL_OF[tpl.kind] !== channel) {\n throw new TypeError(`${channel}(): expects a ${channel} template definition`);\n }\n return tpl.id;\n}\n\nfunction messageNode(\n channel: ChannelIR,\n tpl: MessageTemplate,\n props: object | undefined,\n devices?: 'latest'\n): NodeIR {\n return {\n id: '',\n type: 'message',\n channel,\n template: templateKey(channel, tpl),\n props: (props ?? {}) as Record<string, PropValueIR>,\n // Absent for 'all', so adding the option left every existing push's hash as it was\n ...(devices !== undefined ? { devices } : {}),\n };\n}\n\n/* oxlint-disable typescript/unbound-method --\n * `captureLoc(this.xxx)` passes the method by identity as the stack-trace\n * boundary for Error.captureStackTrace; it is never invoked, so `this`\n * scoping cannot go wrong. */\n/** @internal extended by WorkflowBuilderImpl; not part of the public API */\nexport class FlowBuilderImpl implements FlowBuilder {\n /** Nodes as collected; ids are assigned in one pass inside toIR, so '' is a placeholder here. */\n readonly nodes: NodeIR[] = [];\n /** The template definitions sent, with where: compile-time only, never in the IR. */\n readonly uses: TemplateUse[] = [];\n\n /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */\n private pushNode(node: NodeIR, loc: SourceLocIR | undefined): this {\n this.nodes.push({ ...node, ...(loc !== undefined ? { meta: { loc } } : {}) });\n return this;\n }\n\n private message(\n channel: ChannelIR,\n tpl: MessageTemplate,\n props: object | undefined,\n loc: SourceLocIR | undefined,\n devices?: 'latest'\n ): this {\n this.pushNode(messageNode(channel, tpl, props, devices), loc);\n if (isTemplate(tpl)) this.uses.push({ template: tpl, loc });\n return this;\n }\n\n email<T extends EmailTemplate>(t: T, ...args: MessageArgs<PropsOf<T>>): this {\n return this.message('email', t, args[0], captureLoc(this.email));\n }\n sms<P extends object>(t: TemplateRef<'sms', P>, ...args: MessageArgs<P>): this {\n return this.message('sms', t, args[0], captureLoc(this.sms));\n }\n push<T extends PushTemplate>(\n t: T,\n ...args: [...props: MessageArgs<PropsOf<T>>, options?: PushOptions]\n ): this {\n const [props, options] = args as unknown as [object | undefined, PushOptions | undefined];\n const devices = options?.devices === 'latest' ? 'latest' : undefined;\n return this.message('push', t, props, captureLoc(this.push), devices);\n }\n inApp<P extends object>(t: TemplateRef<'in_app', P>, ...args: MessageArgs<P>): this {\n return this.message('in_app', t, args[0], captureLoc(this.inApp));\n }\n slack<T extends SlackTemplate<PropsSchema, never>>(t: T, ...args: MessageArgs<PropsOf<T>>): this {\n return this.message('slack', t, args[0], captureLoc(this.slack));\n }\n discord<T extends DiscordTemplate<PropsSchema, never>>(\n t: T,\n ...args: MessageArgs<PropsOf<T>>\n ): this {\n return this.message('discord', t, args[0], captureLoc(this.discord));\n }\n survey<P extends object>(t: TemplateRef<'survey', P>, ...args: MessageArgs<P>): this {\n return this.message('survey', t, args[0], captureLoc(this.survey));\n }\n\n prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<PropsOf<T[0]>>>>(\n candidates: T & SameProps<NoInfer<T>>,\n props: A & NoInfer<NoExtra<A, PropsOf<T[0]>>>,\n options?: PreferOptions\n ): this {\n checkPreferArgs(candidates, props, options);\n // Every generated node points at the author's prefer() call, not at this method's body\n const loc = captureLoc(this.prefer);\n const meta = loc !== undefined ? { meta: { loc } } : {};\n const [first, second] = candidates as readonly (EmailTemplate | PushTemplate)[];\n const channel = (tpl: EmailTemplate | PushTemplate) => CHANNEL_OF[tpl.kind] as 'email' | 'push';\n // Both are sent from here as far as the registry check goes, the fallback included\n this.uses.push({ template: first, loc }, { template: second, loc });\n // A copy per message, as two separately written calls would hold\n const arm = (tpl: EmailTemplate | PushTemplate): NodeIR[] => [\n { ...messageNode(channel(tpl), tpl, { ...props }), ...meta },\n ];\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(options?.label !== undefined ? { label: options.label } : {}),\n cases: [\n { condition: { type: 'reachable', channel: channel(first), ...meta }, flow: arm(first) },\n ],\n otherwise: arm(second),\n },\n loc\n );\n }\n\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n delay(d: Duration | { min: Duration; max: Duration }): this {\n const loc = captureLoc(this.delay);\n if (typeof d === 'string') {\n return this.pushNode({ id: '', type: 'delay', duration: durationIR(d) }, loc);\n }\n const min = durationIR(d.min);\n const max = durationIR(d.max);\n /*\n * The interpreter draws `min + random(max - min)` with the span clamped at\n * zero, so a backwards range does not fail — it quietly collapses into a\n * fixed `min` sleep. Reject it here, where the author can still see it.\n */\n if (max.ms < min.ms) {\n throw new Error(\n `delay(): 'max' must not be shorter than 'min', got { min: '${d.min}', max: '${d.max}' }`\n );\n }\n return this.pushNode({ id: '', type: 'random_delay', min, max }, loc);\n }\n\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this {\n const loc = captureLoc(this.timeWindow);\n const start = timeOfDayMinutes(opts.between[0]);\n const end = timeOfDayMinutes(opts.between[1]);\n if (end <= start) {\n throw new Error(\n `timeWindow(): 'between' must open before it closes within one day, got [${opts.between[0]}, ${opts.between[1]}] (overnight windows are not supported yet)`\n );\n }\n return this.pushNode(\n {\n id: '',\n type: 'time_window',\n days: [...(opts.days ?? (['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const))],\n between: [opts.between[0], opts.between[1]],\n tz: opts.tz ?? 'user',\n },\n loc\n );\n }\n\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this {\n const loc = captureLoc(this.waitUntil);\n const onTimeout = opts.onTimeout ?? 'continue';\n return this.pushNode(\n {\n id: '',\n type: 'wait_until',\n condition: condIR(condition),\n timeout: durationIR(opts.timeout),\n onTimeout:\n onTimeout === 'continue' || onTimeout === 'exit'\n ? onTimeout\n : resolveSubFlow(onTimeout, this.uses),\n },\n loc\n );\n }\n\n use(fragment: SubFlow): this {\n // The fragment's nodes carry their own provenance (where the fragment was written)\n this.nodes.push(...resolveSubFlow(fragment, this.uses));\n return this;\n }\n\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n branch(...args: readonly (string | BranchArm)[]): this {\n const loc = captureLoc(this.branch);\n const label = typeof args[0] === 'string' ? args[0] : undefined;\n const arms = (label === undefined ? args : args.slice(1)) as readonly BranchArm[];\n\n const cases: { condition: ConditionIR; flow: NodeIR[] }[] = [];\n let otherwise: NodeIR[] | undefined;\n arms.forEach((arm, i) => {\n if (Array.isArray(arm)) {\n if (otherwise !== undefined) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n const [condition, sub] = arm as BranchCase;\n cases.push({ condition: condIR(condition), flow: resolveSubFlow(sub, this.uses) });\n } else {\n if (otherwise !== undefined) {\n throw new Error('branch(): only one default arm (bare sub-flow) is allowed');\n }\n if (i !== arms.length - 1) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n otherwise = resolveSubFlow(arm as SubFlow, this.uses);\n }\n });\n\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(label !== undefined ? { label } : {}),\n cases,\n ...(otherwise !== undefined ? { otherwise } : {}),\n },\n loc\n );\n }\n\n filter(condition: Condition, opts?: { reason?: string }): this {\n return this.pushNode(\n {\n id: '',\n type: 'filter',\n condition: condIR(condition),\n ...(opts?.reason !== undefined ? { reason: opts.reason } : {}),\n },\n captureLoc(this.filter)\n );\n }\n\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this {\n const loc = captureLoc(this.cohort);\n const entries = Object.entries(arms);\n /*\n * Weights live on a 0.01% grid: the runtime buckets into 10,000 slots\n * (basis points), so anything finer would be silently quantized — reject\n * it here instead. Converting to integer basis points also makes the\n * sum check exact; no epsilon needed.\n */\n const basisPoints = entries.map(([name, a]) => {\n if (!ARM_NAME.test(name)) {\n throw new Error(\n `cohort(): arm name '${name}' must start with a letter or '_' and contain only letters, digits, '_' or '-' — arm names become node id segments, and integer-like names would silently reorder the arms`\n );\n }\n const bp = a.weight * 100;\n if (Math.abs(bp - Math.round(bp)) > 1e-6) {\n throw new Error(\n `cohort(): arm '${name}' weight ${a.weight} is finer than the 0.01% bucketing grid — use at most two decimal places`\n );\n }\n /*\n * The sum check alone accepts -50 / 150: the runtime walks the arms\n * accumulating basis points and takes the first whose cumulative total\n * clears the bucket, so a negative weight hands 100% of users to its\n * neighbour instead of splitting anything.\n */\n if (bp < 0) {\n throw new Error(`cohort(): arm '${name}' weight ${a.weight} must not be negative`);\n }\n return Math.round(bp);\n });\n const total = basisPoints.reduce((sum, bp) => sum + bp, 0);\n if (total !== 10_000) {\n throw new Error(`cohort(): weights must sum to 100, got ${total / 100}`);\n }\n return this.pushNode(\n {\n id: '',\n type: 'cohort',\n ...(opts?.key !== undefined ? { key: opts.key } : {}),\n arms: entries.map(([name, a]) => ({\n name,\n weight: a.weight,\n flow: a.flow ? resolveSubFlow(a.flow, this.uses) : [],\n })),\n },\n loc\n );\n }\n\n exit(reason?: string): this {\n return this.pushNode(\n { id: '', type: 'exit', ...(reason !== undefined ? { reason } : {}) },\n captureLoc(this.exit)\n );\n }\n\n sendEvent<P extends object>(ev: EventRef<P>, ...payload: EmitArgs<P>): this {\n const values = (payload[0] ?? {}) as Record<string, unknown>;\n for (const [field, value] of Object.entries(values)) {\n const ref =\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'user_property';\n if (!ref && !['string', 'number', 'boolean'].includes(typeof value)) {\n throw new TypeError(\n `sendEvent('${ev.name}'): '${field}' is not a scalar or a u.xxx reference — an emitted payload carries scalar fields only`\n );\n }\n }\n return this.pushNode(\n {\n id: '',\n type: 'send_event',\n event: ev.name,\n payload: values as Record<string, PropValueIR>,\n },\n captureLoc(this.sendEvent)\n );\n }\n\n run<A extends object>(a: ActionRef<A>, ...args: MessageArgs<A>): this {\n return this.pushNode(\n {\n id: '',\n type: 'action',\n action: a.name,\n args: (args[0] ?? {}) as Record<string, PropValueIR>,\n codeHash: a.codeHash,\n },\n captureLoc(this.run)\n );\n }\n}\n/* oxlint-enable typescript/unbound-method */\n\n/**\n * A reusable flow fragment: a free function. Once personalization goes through\n * u.xxx and events through e.xxx, the flow layer carries no schema generics.\n */\nexport function flow(build: (w: FlowBuilder) => FlowBuilder): Flow {\n const builder = new FlowBuilderImpl();\n build(builder);\n const impl: FlowInternal = { __flow: true, nodes: builder.nodes, uses: builder.uses };\n return impl;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"flow.mjs","names":[],"sources":["../../src/dsl/flow.ts"],"sourcesContent":["import type { ChannelIR, ConditionIR, NodeIR, PropValueIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport {\n type AnyTemplate,\n CHANNEL_OF,\n type DiscordTemplate,\n type EmailTemplate,\n type PropsSchema,\n type PushTemplate,\n type SlackTemplate,\n isTemplate,\n} from '../templates';\nimport type { ActionRef } from './action';\nimport { type Duration, type TimeOfDay, type Weekday, durationIR, timeOfDayMinutes } from './base';\nimport { type Condition, condIR, positiveDurationIR } from './condition';\nimport type { EventRef, MessageArgs, PropInput, PropsInput } from './refs';\nimport type { PropsOf, TemplateRef } from './template';\n\n/**\n * The flow chain — FlowBuilder is the DSL's step surface (messages, delays,\n * flow control, data), and its implementation is the compiler front half: each\n * chained call executes no logic, it just collects one IR node (ids are\n * assigned later, in workflow.toIR()). Sub-flow callbacks run exactly once at\n * construction — builder in, builder out — so the output is always a static\n * tree.\n *\n * Every public method also captures its callsite (provenance): the loc lands\n * in the node's hash-excluded meta and is the anchor for future in-place\n * editing from the studio.\n */\n\ntype Scalar = string | number | boolean;\n\n/** An emitted field: a scalar (or a profile ref to one); a nested field cannot be sent. */\ntype EmitValue<T> = [NonNullable<T>] extends [Scalar] ? PropInput<T> : never;\n\n/** The payload `sendEvent` accepts for an event: its fields, scalar ones only. */\nexport type EmitPayload<P> = { [K in keyof P]: EmitValue<P[K]> };\n\n/** Payload may be omitted when the event declares no required field. */\nexport type EmitArgs<P> =\n Record<string, never> extends P ? [payload?: EmitPayload<P>] : [payload: EmitPayload<P>];\n\n/** Per-node push options. */\nexport interface PushOptions {\n /** Which devices get a copy: 'all' (the default) or only the most recently registered. */\n devices?: 'all' | 'latest';\n}\n\n/** `prefer()` options. */\nexport interface PreferOptions {\n /** The generated branch's name in IR (UI title / observability handle), as `branch(label, …)`. */\n label?: string;\n}\n\n/** The candidates `prefer()` takes: one push and one email template, in priority order. */\ntype PreferPair = readonly [PushTemplate, EmailTemplate] | readonly [EmailTemplate, PushTemplate];\n\ntype NormalizeProps<P> = { -readonly [K in keyof P]: P[K] };\ntype Equal<A, B> =\n (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;\n\n/** Both candidates render the one props object, so they must declare the same props type — not merely overlap. */\ntype SameProps<T extends PreferPair> =\n Equal<NormalizeProps<PropsOf<T[0]>>, NormalizeProps<PropsOf<T[1]>>> extends true\n ? unknown\n : { readonly __prefer_error__: 'Both templates must declare the same props type' };\n\n/** Distributes, so a field only one member of a union variable has still counts as extra. */\ntype KeysOfUnion<A> = A extends unknown ? keyof A : never;\n/** Rejects known extra fields, which `{}` (an empty props schema) would otherwise accept. */\ntype NoExtra<A, P> = Record<Exclude<KeysOfUnion<A>, keyof P>, never>;\n\n/** prefer() is typed strictly, but a JS caller (or an `any`) reaches it unchecked — fail at the call, not at send time. */\nfunction checkPreferArgs(candidates: unknown, props: unknown, options: unknown): void {\n if (!Array.isArray(candidates) || candidates.length !== 2) {\n throw new TypeError('prefer(): candidates must be exactly two templates — one push, one email');\n }\n const channels = (candidates as unknown[]).map((candidate) =>\n isTemplate(candidate) ? CHANNEL_OF[candidate.kind] : undefined\n );\n if (!channels.every((channel) => channel === 'push' || channel === 'email')) {\n throw new TypeError('prefer(): each candidate must be a pushTemplate or an emailTemplate');\n }\n if (channels[0] === channels[1]) {\n throw new TypeError('prefer(): candidates must be one push and one email template');\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n throw new TypeError(\n 'prefer(): props must be an object — pass {} when the templates declare none'\n );\n }\n if (options === undefined) return;\n if (typeof options !== 'object' || options === null || Array.isArray(options)) {\n throw new TypeError('prefer(): options must be an object');\n }\n for (const [name, value] of Object.entries(options)) {\n if (name !== 'label') {\n throw new TypeError(\n `prefer(): unknown option '${name}' — props go in the second argument, options take only 'label'`\n );\n }\n if (typeof value !== 'string') throw new TypeError(\"prefer(): 'label' must be a string\");\n }\n}\n\n/** A reusable flow fragment (what `flow(...)` returns), referenced directly by branch arms. */\nexport interface Flow {\n readonly __flow: true;\n}\n\ninterface FlowInternal extends Flow {\n readonly nodes: NodeIR[];\n readonly uses: readonly TemplateUse[];\n readonly actions: readonly ActionRef[];\n}\n\n/**\n * @internal A template definition a flow sends, kept beside the IR (which names it by id only):\n * compileConfig checks that the registered definition under that id is this very object, so the\n * schema that typed the call is the one that renders it. Never part of the IR or its hash.\n */\nexport interface TemplateUse {\n readonly template: AnyTemplate;\n readonly loc: SourceLocIR | undefined;\n}\n\n/** @internal The template definitions a workflow sends, sub-flows and fragments included. */\nexport function templateUses(builder: FlowBuilder): readonly TemplateUse[] {\n return builder instanceof FlowBuilderImpl ? builder.uses : [];\n}\n\n/**\n * @internal The action definitions a workflow runs, sub-flows and fragments included. The IR\n * names an action only; compileBundle checks that each ref is the very object listed in\n * `actions`, which is what keeps two definitions from sharing one name. Never part of the IR.\n */\nexport function actionUses(builder: FlowBuilder): readonly ActionRef[] {\n return builder instanceof FlowBuilderImpl ? builder.actions : [];\n}\n\n/** Sub-flow: a named fragment, or an inline callback (runs once at construction — builder in, builder out). */\nexport type SubFlow = Flow | ((w: FlowBuilder) => FlowBuilder);\n\n/** A branch arm: the [condition, sub-flow] tuple — an [if, then] ordering convention. */\nexport type BranchCase = readonly [condition: Condition, flow: SubFlow];\n\n/**\n * A branch argument: either a tuple arm, or a bare sub-flow meaning the default\n * arm. The bare form may appear once, and must come last (checked at runtime).\n */\nexport type BranchArm = BranchCase | SubFlow;\n\n/**\n * A cohort arm name has two jobs, and both constrain its spelling:\n * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),\n * and an id is a durable step name — a dot would fake a nesting level and can\n * collide with a real one, an empty name gives '{id}..{j}';\n * - it is an object key, and JavaScript lists integer-like keys first in\n * ascending numeric order however the author wrote them. The arms array is\n * documented (and interpreted) as authoring order, and the bucketing walk\n * accumulates weights along it, so an arm named '0' added to a live\n * experiment would prepend itself and re-bucket everyone already in it.\n */\nconst ARM_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/** A sub-flow's nodes; the template definitions it sends and the actions it runs are added to `uses` / `actions`. */\nfunction resolveSubFlow(sub: SubFlow, uses: TemplateUse[], actions: ActionRef[]): NodeIR[] {\n if (typeof sub === 'function') {\n const builder = new FlowBuilderImpl();\n const returned: unknown = sub(builder);\n // An async callback would add its nodes after construction is over — after ids were assigned\n if (typeof (returned as { then?: unknown } | undefined)?.then === 'function') {\n throw new TypeError(\n 'sub-flow callbacks must be synchronous: they run once, while the workflow is defined'\n );\n }\n uses.push(...builder.uses);\n actions.push(...builder.actions);\n return builder.nodes;\n }\n uses.push(...(sub as FlowInternal).uses);\n actions.push(...(sub as FlowInternal).actions);\n /*\n * A named fragment is a value, and the convention is to reuse it — two branch\n * arms may hold the same one. Handing out its own nodes aliases them into\n * both places, and assignIds writes ids *in place*: the second prefix\n * overwrites the first, so the two placements end up as one object under one\n * id — one durable step name, one message idempotency key, one bucket seed.\n * A copy per placement is what makes a fragment behave like a value.\n */\n return structuredClone((sub as FlowInternal).nodes);\n}\n\nexport interface FlowBuilder {\n /* ---- Messages: every channel is one message node; separate methods buy channel-level typing ---- */\n email<T extends EmailTemplate>(template: T, ...props: MessageArgs<PropsOf<T>>): this;\n sms<P extends object>(template: TemplateRef<'sms', P>, ...props: MessageArgs<P>): this;\n /**\n * Push to the user's devices — every device the host's directory lists, or\n * only the most recently registered with `{ devices: 'latest' }`. A user with\n * no device is skipped (`prefer([pushTpl, emailTpl], props)` falls back to email).\n */\n push<T extends PushTemplate>(\n template: T,\n ...args: [...props: MessageArgs<PropsOf<T>>, options?: PushOptions]\n ): this;\n inApp<P extends object>(template: TemplateRef<'in_app', P>, ...props: MessageArgs<P>): this;\n slack<T extends SlackTemplate<PropsSchema, never>>(\n template: T,\n ...props: MessageArgs<PropsOf<T>>\n ): this;\n discord<T extends DiscordTemplate<PropsSchema, never>>(\n template: T,\n ...props: MessageArgs<PropsOf<T>>\n ): this;\n survey<P extends object>(template: TemplateRef<'survey', P>, ...props: MessageArgs<P>): this;\n\n /**\n * Send one message on the first channel the user can be reached on: one push\n * and one email template, in priority order, rendered with the same props.\n * Sugar for the branch it compiles to — same IR, ids and hash as writing\n *\n * .branch(label, [reachable(first.channel), (b) => b.<first>(first, props)], (b) => b.<second>(second, props))\n *\n * so the choice is made once, when the step is reached, and a first choice\n * that fails to send retries as itself — it never falls through to the\n * second. Props are always passed (`{}` for templates that declare none),\n * and both templates must declare the same props type; for differing props\n * or other channels, write the branch.\n *\n * .prefer([push.tip, email.tip], { name: u.name }, { label: 'tip by channel' })\n */\n prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<PropsOf<T[0]>>>>(\n candidates: T & SameProps<NoInfer<T>>,\n props: A & NoInfer<NoExtra<A, PropsOf<T[0]>>>,\n options?: PreferOptions\n ): this;\n\n /* --------------------------------- Delays --------------------------------- */\n /** Time Delay; passing { min, max } makes it a Randomized Delay. */\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n\n /** Time Window: hold until inside the window (e.g. weekdays 09:00–17:00). tz: 'user' uses the user's timezone. */\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this;\n\n /**\n * Wait Until: continue on the main line once the condition holds, or take\n * onTimeout when it expires (default 'continue'). On the engine side this is\n * \"await event racing a timer\" — a native primitive of durable runtimes.\n */\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this;\n\n /**\n * Splice a fragment in place: a named `flow(...)` or a callback, expanded\n * once while the workflow is defined — a composition step before compiling,\n * not a node (the IR has no trace of it, and ids are assigned over the whole\n * workflow as if the steps were written inline). Each use gets its own copy,\n * so one fragment can be used in several places and workflows.\n *\n * const officeHours = flow((w) => w.timeWindow({ days: ['mon', 'tue', 'wed', 'thu', 'fri'], between: ['09:00', '17:00'] }));\n * workflow({ id: 'onboarding', ... }).delay('1 day').use(officeHours).email(email.tip, { name: u.name })\n */\n use(fragment: SubFlow): this;\n\n /* ------------------------------ Flow Control ------------------------------ */\n /**\n * Conditional branch: ordered first-match (same semantics as Step Functions\n * Choice / Knock branch). This one primitive covers both the UI's True/False\n * Branch (two arms) and Multi-Split Branch (N arms). Arms are\n * [condition, sub-flow] tuples; a bare sub-flow in tail position is the\n * default arm. With no match and no default, execution simply continues.\n * Rejoin is structural: after the matching arm finishes, execution resumes\n * at the node following the branch — to opt out, call .exit() inside the arm.\n *\n * .branch([activeSubscriber, upgradeFlow], firstTimeFlow) // True/False\n * .branch([vip, vipFlow], [trial, trialFlow], defaultFlow) // Multi-Split\n * .branch([not(usedStaging), eduFlow]) // single arm: no match continues\n *\n * The optional first argument `label` becomes the node's name in IR (UI title\n * / observability handle). It is not a jump target — there is no goto here;\n * looping is a future re-entry-policy topic (see sendEvent). Arm labels in\n * the UI are derived from the condition.\n */\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n\n /**\n * Gate: exit unless the condition holds, otherwise continue. A first-class\n * node rather than sugar over branch — same semantics as Zapier's \"only\n * continue if\" / Iterable's Filter tile, and rendered on its own in the UI.\n * `reason` is the audit-log line — metadata, so rewording it is free\n * (excluded from contentHash, see hash.ts).\n */\n filter(condition: Condition, opts?: { reason?: string }): this;\n\n /**\n * Random Cohort Branch (A/B): weights must sum to 100 (checked at runtime).\n * `key` names the experiment: bucketing hashes `userId:key` instead of the\n * structural node id, so assignments survive node insertion/moves and the\n * experiment keeps one identity across workflow versions — set it for any\n * A/B whose results you intend to read.\n *\n * Arm names become node id segments and are read back in authoring order, so\n * they are restricted to letters, digits, '_' and '-', never starting with a\n * digit (checked at runtime).\n */\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this;\n\n /**\n * Exit: end the whole workflow immediately (the UI's Exit node); `reason`\n * goes into the audit log and is metadata, so rewording it neither repins\n * in-flight journeys nor shows up in plan. Inside a branch arm this means\n * \"terminate, do not rejoin\" — it stops here instead of falling through,\n * while an arm without exit rejoins naturally.\n */\n exit(reason?: string): this;\n\n /* ---------------------------------- Data ---------------------------------- */\n /**\n * Emit a typed event (customer.io's Send Event). The event comes from an\n * e.xxx reference, so payload types flow in the same way trigger.event works.\n * This is how workflows compose: an event can trigger another workflow, and\n * the resulting call graph is statically analyzable. Payload values may\n * reference u.xxx. Only scalar fields can be sent (IR carries literals and\n * profile refs, not nested JSON): an event whose payload requires a nested\n * field cannot be emitted from a journey, and the types say so.\n *\n * Note on loops: a trailing self-triggering sendEvent will become the\n * official periodic-flow shape *once a re-entry policy exists* — today the\n * entry policy is once-per-user, so a self-trigger cannot re-enter and does\n * not loop.\n * TODO: the rest of the Data category — outbound webhook / profile update /\n * journey attributes.\n */\n sendEvent<P extends object>(event: EventRef<P>, ...payload: EmitArgs<P>): this;\n\n /**\n * Run a custom action (see dsl/action.ts for the model). The chain records\n * identity only — name and args — never the code: the implementation\n * ships inside the deployed Worker bundle and is resolved by name from the\n * runtime registry. Args are typed by the ActionRef and may reference u.xxx,\n * resolved at run time exactly like message props. Executed inside a durable\n * step: retried on throw, checkpointed on success.\n */\n run<A extends object>(action: ActionRef<A>, ...args: MessageArgs<A>): this;\n}\n\nconst DEFINED_CHANNELS: ReadonlySet<ChannelIR> = new Set(Object.values(CHANNEL_OF));\n\n/** A message's template: a definition of the channel (named by its id), or a reference by key. */\ntype MessageTemplate = AnyTemplate | TemplateRef;\n\nfunction templateKey(channel: ChannelIR, tpl: MessageTemplate): string {\n if (!DEFINED_CHANNELS.has(channel)) return (tpl as TemplateRef).key;\n // Typed, but a JS caller (or an `any`) reaches it unchecked: fail where the workflow is written\n if (!isTemplate(tpl) || CHANNEL_OF[tpl.kind] !== channel) {\n throw new TypeError(`${channel}(): expects a ${channel} template definition`);\n }\n return tpl.id;\n}\n\nfunction messageNode(\n channel: ChannelIR,\n tpl: MessageTemplate,\n props: object | undefined,\n devices?: 'latest'\n): NodeIR {\n return {\n id: '',\n type: 'message',\n channel,\n template: templateKey(channel, tpl),\n props: (props ?? {}) as Record<string, PropValueIR>,\n // Absent for 'all', so adding the option left every existing push's hash as it was\n ...(devices !== undefined ? { devices } : {}),\n };\n}\n\n/* oxlint-disable typescript/unbound-method --\n * `captureLoc(this.xxx)` passes the method by identity as the stack-trace\n * boundary for Error.captureStackTrace; it is never invoked, so `this`\n * scoping cannot go wrong. */\n/** @internal extended by WorkflowBuilderImpl; not part of the public API */\nexport class FlowBuilderImpl implements FlowBuilder {\n /** Nodes as collected; ids are assigned in one pass inside toIR, so '' is a placeholder here. */\n readonly nodes: NodeIR[] = [];\n /** The template definitions sent, with where: compile-time only, never in the IR. */\n readonly uses: TemplateUse[] = [];\n /** The action definitions run: compile-time identity only, never in the IR. */\n readonly actions: ActionRef[] = [];\n\n /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */\n private pushNode(node: NodeIR, loc: SourceLocIR | undefined): this {\n this.nodes.push({ ...node, ...(loc !== undefined ? { meta: { loc } } : {}) });\n return this;\n }\n\n private message(\n channel: ChannelIR,\n tpl: MessageTemplate,\n props: object | undefined,\n loc: SourceLocIR | undefined,\n devices?: 'latest'\n ): this {\n this.pushNode(messageNode(channel, tpl, props, devices), loc);\n if (isTemplate(tpl)) this.uses.push({ template: tpl, loc });\n return this;\n }\n\n email<T extends EmailTemplate>(t: T, ...args: MessageArgs<PropsOf<T>>): this {\n return this.message('email', t, args[0], captureLoc(this.email));\n }\n sms<P extends object>(t: TemplateRef<'sms', P>, ...args: MessageArgs<P>): this {\n return this.message('sms', t, args[0], captureLoc(this.sms));\n }\n push<T extends PushTemplate>(\n t: T,\n ...args: [...props: MessageArgs<PropsOf<T>>, options?: PushOptions]\n ): this {\n const [props, options] = args as unknown as [object | undefined, PushOptions | undefined];\n const devices = options?.devices === 'latest' ? 'latest' : undefined;\n return this.message('push', t, props, captureLoc(this.push), devices);\n }\n inApp<P extends object>(t: TemplateRef<'in_app', P>, ...args: MessageArgs<P>): this {\n return this.message('in_app', t, args[0], captureLoc(this.inApp));\n }\n slack<T extends SlackTemplate<PropsSchema, never>>(t: T, ...args: MessageArgs<PropsOf<T>>): this {\n return this.message('slack', t, args[0], captureLoc(this.slack));\n }\n discord<T extends DiscordTemplate<PropsSchema, never>>(\n t: T,\n ...args: MessageArgs<PropsOf<T>>\n ): this {\n return this.message('discord', t, args[0], captureLoc(this.discord));\n }\n survey<P extends object>(t: TemplateRef<'survey', P>, ...args: MessageArgs<P>): this {\n return this.message('survey', t, args[0], captureLoc(this.survey));\n }\n\n prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<PropsOf<T[0]>>>>(\n candidates: T & SameProps<NoInfer<T>>,\n props: A & NoInfer<NoExtra<A, PropsOf<T[0]>>>,\n options?: PreferOptions\n ): this {\n checkPreferArgs(candidates, props, options);\n // Every generated node points at the author's prefer() call, not at this method's body\n const loc = captureLoc(this.prefer);\n const meta = loc !== undefined ? { meta: { loc } } : {};\n const [first, second] = candidates as readonly (EmailTemplate | PushTemplate)[];\n const channel = (tpl: EmailTemplate | PushTemplate) => CHANNEL_OF[tpl.kind] as 'email' | 'push';\n // Both are sent from here as far as the registry check goes, the fallback included\n this.uses.push({ template: first, loc }, { template: second, loc });\n // A copy per message, as two separately written calls would hold\n const arm = (tpl: EmailTemplate | PushTemplate): NodeIR[] => [\n { ...messageNode(channel(tpl), tpl, { ...props }), ...meta },\n ];\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(options?.label !== undefined ? { label: options.label } : {}),\n cases: [\n { condition: { type: 'reachable', channel: channel(first), ...meta }, flow: arm(first) },\n ],\n otherwise: arm(second),\n },\n loc\n );\n }\n\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n delay(d: Duration | { min: Duration; max: Duration }): this {\n const loc = captureLoc(this.delay);\n if (typeof d === 'string') {\n return this.pushNode({ id: '', type: 'delay', duration: durationIR(d) }, loc);\n }\n const min = durationIR(d.min);\n const max = durationIR(d.max);\n /*\n * The interpreter draws `min + random(max - min)` with the span clamped at\n * zero, so a backwards range does not fail — it quietly collapses into a\n * fixed `min` sleep. Reject it here, where the author can still see it.\n */\n if (max.ms < min.ms) {\n throw new Error(\n `delay(): 'max' must not be shorter than 'min', got { min: '${d.min}', max: '${d.max}' }`\n );\n }\n return this.pushNode({ id: '', type: 'random_delay', min, max }, loc);\n }\n\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this {\n const loc = captureLoc(this.timeWindow);\n if (opts.days !== undefined) {\n // An empty list is a window that never opens: the node would hold the user forever\n if (opts.days.length === 0) {\n throw new Error(\"timeWindow(): 'days' must name at least one day\");\n }\n const seen = new Set<Weekday>();\n for (const day of opts.days) {\n if (seen.has(day)) throw new Error(`timeWindow(): 'days' lists '${day}' twice`);\n seen.add(day);\n }\n }\n const start = timeOfDayMinutes(opts.between[0]);\n const end = timeOfDayMinutes(opts.between[1]);\n if (end <= start) {\n throw new Error(\n `timeWindow(): 'between' must open before it closes within one day, got [${opts.between[0]}, ${opts.between[1]}] (overnight windows are not supported yet)`\n );\n }\n return this.pushNode(\n {\n id: '',\n type: 'time_window',\n days: [...(opts.days ?? (['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const))],\n between: [opts.between[0], opts.between[1]],\n tz: opts.tz ?? 'user',\n },\n loc\n );\n }\n\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this {\n const loc = captureLoc(this.waitUntil);\n const onTimeout = opts.onTimeout ?? 'continue';\n return this.pushNode(\n {\n id: '',\n type: 'wait_until',\n condition: condIR(condition),\n timeout: positiveDurationIR(opts.timeout, 'waitUntil(): timeout'),\n onTimeout:\n onTimeout === 'continue' || onTimeout === 'exit'\n ? onTimeout\n : resolveSubFlow(onTimeout, this.uses, this.actions),\n },\n loc\n );\n }\n\n use(fragment: SubFlow): this {\n // The fragment's nodes carry their own provenance (where the fragment was written)\n this.nodes.push(...resolveSubFlow(fragment, this.uses, this.actions));\n return this;\n }\n\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n branch(...args: readonly (string | BranchArm)[]): this {\n const loc = captureLoc(this.branch);\n const label = typeof args[0] === 'string' ? args[0] : undefined;\n const arms = (label === undefined ? args : args.slice(1)) as readonly BranchArm[];\n\n const cases: { condition: ConditionIR; flow: NodeIR[] }[] = [];\n let otherwise: NodeIR[] | undefined;\n arms.forEach((arm, i) => {\n if (Array.isArray(arm)) {\n if (otherwise !== undefined) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n const [condition, sub] = arm as BranchCase;\n cases.push({\n condition: condIR(condition),\n flow: resolveSubFlow(sub, this.uses, this.actions),\n });\n } else {\n if (otherwise !== undefined) {\n throw new Error('branch(): only one default arm (bare sub-flow) is allowed');\n }\n if (i !== arms.length - 1) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n otherwise = resolveSubFlow(arm as SubFlow, this.uses, this.actions);\n }\n });\n\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(label !== undefined ? { label } : {}),\n cases,\n ...(otherwise !== undefined ? { otherwise } : {}),\n },\n loc\n );\n }\n\n filter(condition: Condition, opts?: { reason?: string }): this {\n return this.pushNode(\n {\n id: '',\n type: 'filter',\n condition: condIR(condition),\n ...(opts?.reason !== undefined ? { reason: opts.reason } : {}),\n },\n captureLoc(this.filter)\n );\n }\n\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this {\n const loc = captureLoc(this.cohort);\n const entries = Object.entries(arms);\n /*\n * Weights live on a 0.01% grid: the runtime buckets into 10,000 slots\n * (basis points), so anything finer would be silently quantized — reject\n * it here instead. Converting to integer basis points also makes the\n * sum check exact; no epsilon needed.\n */\n const basisPoints = entries.map(([name, a]) => {\n if (!ARM_NAME.test(name)) {\n throw new Error(\n `cohort(): arm name '${name}' must start with a letter or '_' and contain only letters, digits, '_' or '-' — arm names become node id segments, and integer-like names would silently reorder the arms`\n );\n }\n const bp = a.weight * 100;\n if (Math.abs(bp - Math.round(bp)) > 1e-6) {\n throw new Error(\n `cohort(): arm '${name}' weight ${a.weight} is finer than the 0.01% bucketing grid — use at most two decimal places`\n );\n }\n /*\n * The sum check alone accepts -50 / 150: the runtime walks the arms\n * accumulating basis points and takes the first whose cumulative total\n * clears the bucket, so a negative weight hands 100% of users to its\n * neighbour instead of splitting anything.\n */\n if (bp < 0) {\n throw new Error(`cohort(): arm '${name}' weight ${a.weight} must not be negative`);\n }\n return Math.round(bp);\n });\n const total = basisPoints.reduce((sum, bp) => sum + bp, 0);\n if (total !== 10_000) {\n throw new Error(`cohort(): weights must sum to 100, got ${total / 100}`);\n }\n return this.pushNode(\n {\n id: '',\n type: 'cohort',\n ...(opts?.key !== undefined ? { key: opts.key } : {}),\n arms: entries.map(([name, a]) => ({\n name,\n weight: a.weight,\n flow: a.flow ? resolveSubFlow(a.flow, this.uses, this.actions) : [],\n })),\n },\n loc\n );\n }\n\n exit(reason?: string): this {\n return this.pushNode(\n { id: '', type: 'exit', ...(reason !== undefined ? { reason } : {}) },\n captureLoc(this.exit)\n );\n }\n\n sendEvent<P extends object>(ev: EventRef<P>, ...payload: EmitArgs<P>): this {\n const values = (payload[0] ?? {}) as Record<string, unknown>;\n for (const [field, value] of Object.entries(values)) {\n const ref =\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'user_property';\n if (!ref && !['string', 'number', 'boolean'].includes(typeof value)) {\n throw new TypeError(\n `sendEvent('${ev.name}'): '${field}' is not a scalar or a u.xxx reference — an emitted payload carries scalar fields only`\n );\n }\n }\n return this.pushNode(\n {\n id: '',\n type: 'send_event',\n event: ev.name,\n payload: values as Record<string, PropValueIR>,\n },\n captureLoc(this.sendEvent)\n );\n }\n\n run<A extends object>(a: ActionRef<A>, ...args: MessageArgs<A>): this {\n this.actions.push(a);\n return this.pushNode(\n {\n id: '',\n type: 'action',\n action: a.name,\n args: (args[0] ?? {}) as Record<string, PropValueIR>,\n },\n captureLoc(this.run)\n );\n }\n}\n/* oxlint-enable typescript/unbound-method */\n\n/**\n * A reusable flow fragment: a free function. Once personalization goes through\n * u.xxx and events through e.xxx, the flow layer carries no schema generics.\n */\nexport function flow(build: (w: FlowBuilder) => FlowBuilder): Flow {\n const builder = new FlowBuilderImpl();\n build(builder);\n const impl: FlowInternal = {\n __flow: true,\n nodes: builder.nodes,\n uses: builder.uses,\n actions: builder.actions,\n };\n return impl;\n}\n"],"mappings":"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"}
@@ -1,9 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_hash = require("../hash.cjs");
3
2
  const require_provenance = require("../provenance.cjs");
4
- const require_dsl_base = require("./base.cjs");
5
3
  const require_dsl_condition = require("./condition.cjs");
6
4
  const require_dsl_flow = require("./flow.cjs");
5
+ const require_hash = require("../hash.cjs");
7
6
  const require_ir = require("../ir.cjs");
8
7
  //#region src/dsl/workflow.ts
9
8
  /**
@@ -68,7 +67,7 @@ var WorkflowBuilderImpl = class extends require_dsl_flow.FlowBuilderImpl {
68
67
  };
69
68
  return {
70
69
  condition: require_dsl_condition.condIR(goal.condition),
71
- ...goal.within !== void 0 ? { within: require_dsl_base.durationIR(goal.within) } : {},
70
+ ...goal.within !== void 0 ? { within: require_dsl_condition.positiveDurationIR(goal.within, "goal.within") } : {},
72
71
  exitOnMatch: goal.exitOnMatch ?? true
73
72
  };
74
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.cjs","names":["FlowBuilderImpl","condIR","durationIR","WorkflowIRSchema","semanticHash","captureLoc"],"sources":["../../src/dsl/workflow.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type GoalIR,\n IR_VERSION,\n type MetaIR,\n type NodeIR,\n type SourceLocIR,\n type WorkflowIR,\n WorkflowIR as WorkflowIRSchema,\n} from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport { type Condition, condIR } from './condition';\nimport { type FlowBuilder, FlowBuilderImpl } from './flow';\nimport type { TriggerInternal, TriggerRef } from './trigger';\n\n/**\n * The workflow — a named flow chain plus its configuration (options =\n * configuration, the chain = steps), and the compiler back half: `toIR()`\n * deep-clones the collected nodes, assigns structural-path ids, computes the\n * contentHash (metadata stripped, see hash.ts) and self-checks the result\n * against the authoritative IR schema.\n */\n\n/** Full conversion-goal configuration (customer.io's Goal & Exit semantics). */\nexport interface GoalOptions {\n condition: Condition;\n /** Attribution window: how long after entry a match still counts as a conversion. Unbounded by default. */\n within?: Duration;\n /** Exit on conversion. Defaults to true — this is what absorbs all those True→Exit arms in a UI canvas. */\n exitOnMatch?: boolean;\n}\n\nexport interface WorkflowDefinition {\n /**\n * Wire identity — engine instances, the entry ledger and stats keys are all\n * built on it (`WorkflowIR.id`), so it is not something to rename for\n * readability; `name` is.\n */\n id: string;\n\n /** Trigger: a trigger.xxx() asset. TODO: multiple triggers, entry frequency / re-entry policy. */\n trigger: TriggerRef;\n\n /**\n * Conversion goal: feeds reporting (conversion rate / attribution) and by\n * default exits on match. Checked before each node runs. Pass a condition\n * for the shorthand, or GoalOptions for the full form.\n */\n goal?: Condition | GoalOptions;\n\n /** Plain exit condition: leaving without counting as a conversion (unsubscribed, no longer eligible). Coexists with goal. */\n exitWhen?: Condition;\n\n /*\n * Everything below is human-facing metadata: it lands in IR's `meta` field\n * and is **excluded from contentHash**. Editing it neither invalidates the\n * version in-flight users are pinned to nor shows up as a change in plan.\n */\n\n /** Human label: what the studio shows instead of the id. */\n name?: string;\n\n /** One line on what this flow does (UI lists, plan output). */\n description?: string;\n}\n\nexport interface WorkflowBuilder extends FlowBuilder {\n toIR(): WorkflowIR;\n}\n\n/**\n * Assign node ids from the structural path (rules in ir.ts's header). Written\n * in place into the tree toIR deep-cloned, never into the builder's own nodes.\n */\nfunction assignIds(nodes: NodeIR[], prefix: string): void {\n nodes.forEach((node, i) => {\n const id = prefix === '' ? String(i) : `${prefix}.${i}`;\n node.id = id;\n switch (node.type) {\n case 'branch':\n node.cases.forEach((c, ci) => assignIds(c.flow, `${id}.c${ci}`));\n if (node.otherwise) assignIds(node.otherwise, `${id}.o`);\n break;\n case 'cohort':\n node.arms.forEach((arm) => assignIds(arm.flow, `${id}.${arm.name}`));\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assignIds(node.onTimeout, `${id}.t`);\n break;\n default:\n break;\n }\n });\n}\n\n/**\n * Ids are derived, never authored, so a duplicate is always a compiler bug —\n * and a silent one: assignIds writes in place, so the second write simply wins\n * and two nodes ship as one. What that costs is concrete, because an id is the\n * durable step name (`step.do('{id}:send')`), the message idempotency key\n * (`{instanceId}:{id}`) and the cohort bucket seed: the twins share a\n * checkpoint, and the second send is suppressed as a duplicate of the first.\n * Cheap to check once at the end of the compile, so it is checked.\n */\nfunction assertUniqueIds(nodes: readonly NodeIR[], seen = new Set<string>()): void {\n for (const node of nodes) {\n if (seen.has(node.id)) {\n throw new Error(\n `toIR(): node id '${node.id}' is used twice — ids are durable step names and message idempotency keys, so they must be unique`\n );\n }\n seen.add(node.id);\n switch (node.type) {\n case 'branch':\n for (const branchCase of node.cases) assertUniqueIds(branchCase.flow, seen);\n if (node.otherwise) assertUniqueIds(node.otherwise, seen);\n break;\n case 'cohort':\n for (const arm of node.arms) assertUniqueIds(arm.flow, seen);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assertUniqueIds(node.onTimeout, seen);\n break;\n default:\n break;\n }\n }\n}\n\nclass WorkflowBuilderImpl extends FlowBuilderImpl implements WorkflowBuilder {\n constructor(\n private readonly options: WorkflowDefinition,\n /** Callsite of the workflow() call (provenance; lands in meta.loc). */\n private readonly loc: SourceLocIR | undefined\n ) {\n super();\n }\n\n private goalIR(): GoalIR | undefined {\n const goal = this.options.goal;\n if (goal === undefined) return undefined;\n if ('__condition' in goal) {\n return { condition: condIR(goal), exitOnMatch: true };\n }\n return {\n condition: condIR(goal.condition),\n ...(goal.within !== undefined ? { within: durationIR(goal.within) } : {}),\n exitOnMatch: goal.exitOnMatch ?? true,\n };\n }\n\n private metaIR(): MetaIR | undefined {\n const { name, description } = this.options;\n if (name === undefined && description === undefined && this.loc === undefined) {\n return undefined;\n }\n return {\n ...(name !== undefined ? { name } : {}),\n ...(description !== undefined ? { description } : {}),\n ...(this.loc !== undefined ? { loc: this.loc } : {}),\n };\n }\n\n toIR(): WorkflowIR {\n const flowNodes = structuredClone(this.nodes);\n assignIds(flowNodes, '');\n assertUniqueIds(flowNodes);\n const goal = this.goalIR();\n const meta = this.metaIR();\n const body = {\n irVersion: IR_VERSION,\n id: this.options.id,\n ...(meta !== undefined ? { meta } : {}),\n trigger: (this.options.trigger as TriggerInternal).ir,\n ...(goal !== undefined ? { goal } : {}),\n ...(this.options.exitWhen !== undefined ? { exitWhen: condIR(this.options.exitWhen) } : {}),\n flow: flowNodes,\n };\n // semanticHash strips meta / label / reason, so metadata edits leave contentHash alone\n // Self-check: the compiler's output must pass IR's authoritative schema\n return WorkflowIRSchema.parse({ ...body, contentHash: semanticHash(body) });\n }\n}\n\n/**\n * Workflow definition: an id, configuration (trigger / goal / exitWhen),\n * optional labels, and chained steps.\n *\n * export const onboarding = workflow({\n * id: 'onboarding',\n * name: 'Onboarding',\n * trigger: trigger.event(e.sign_up),\n * }).email(email.welcome);\n */\nexport function workflow(definition: WorkflowDefinition): WorkflowBuilder {\n return new WorkflowBuilderImpl(definition, captureLoc(workflow));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"workflow.cjs","names":["FlowBuilderImpl","condIR","positiveDurationIR","WorkflowIRSchema","semanticHash","captureLoc"],"sources":["../../src/dsl/workflow.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type GoalIR,\n IR_VERSION,\n type MetaIR,\n type NodeIR,\n type SourceLocIR,\n type WorkflowIR,\n WorkflowIR as WorkflowIRSchema,\n} from '../ir';\nimport { captureLoc } from '../provenance';\nimport type { Duration } from './base';\nimport { type Condition, condIR, positiveDurationIR } from './condition';\nimport { type FlowBuilder, FlowBuilderImpl } from './flow';\nimport type { TriggerInternal, TriggerRef } from './trigger';\n\n/**\n * The workflow — a named flow chain plus its configuration (options =\n * configuration, the chain = steps), and the compiler back half: `toIR()`\n * deep-clones the collected nodes, assigns structural-path ids, computes the\n * contentHash (metadata stripped, see hash.ts) and self-checks the result\n * against the authoritative IR schema.\n */\n\n/** Full conversion-goal configuration (customer.io's Goal & Exit semantics). */\nexport interface GoalOptions {\n condition: Condition;\n /** Attribution window: how long after entry a match still counts as a conversion. Unbounded by default. */\n within?: Duration;\n /** Exit on conversion. Defaults to true — this is what absorbs all those True→Exit arms in a UI canvas. */\n exitOnMatch?: boolean;\n}\n\nexport interface WorkflowDefinition {\n /**\n * Wire identity — engine instances, the entry ledger and stats keys are all\n * built on it (`WorkflowIR.id`), so it is not something to rename for\n * readability; `name` is.\n */\n id: string;\n\n /** Trigger: a trigger.xxx() asset. TODO: multiple triggers, entry frequency / re-entry policy. */\n trigger: TriggerRef;\n\n /**\n * Conversion goal: feeds reporting (conversion rate / attribution) and by\n * default exits on match. Checked before each node runs. Pass a condition\n * for the shorthand, or GoalOptions for the full form.\n */\n goal?: Condition | GoalOptions;\n\n /** Plain exit condition: leaving without counting as a conversion (unsubscribed, no longer eligible). Coexists with goal. */\n exitWhen?: Condition;\n\n /*\n * Everything below is human-facing metadata: it lands in IR's `meta` field\n * and is **excluded from contentHash**. Editing it neither invalidates the\n * version in-flight users are pinned to nor shows up as a change in plan.\n */\n\n /** Human label: what the studio shows instead of the id. */\n name?: string;\n\n /** One line on what this flow does (UI lists, plan output). */\n description?: string;\n}\n\nexport interface WorkflowBuilder extends FlowBuilder {\n toIR(): WorkflowIR;\n}\n\n/**\n * Assign node ids from the structural path (rules in ir.ts's header). Written\n * in place into the tree toIR deep-cloned, never into the builder's own nodes.\n */\nfunction assignIds(nodes: NodeIR[], prefix: string): void {\n nodes.forEach((node, i) => {\n const id = prefix === '' ? String(i) : `${prefix}.${i}`;\n node.id = id;\n switch (node.type) {\n case 'branch':\n node.cases.forEach((c, ci) => assignIds(c.flow, `${id}.c${ci}`));\n if (node.otherwise) assignIds(node.otherwise, `${id}.o`);\n break;\n case 'cohort':\n node.arms.forEach((arm) => assignIds(arm.flow, `${id}.${arm.name}`));\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assignIds(node.onTimeout, `${id}.t`);\n break;\n default:\n break;\n }\n });\n}\n\n/**\n * Ids are derived, never authored, so a duplicate is always a compiler bug —\n * and a silent one: assignIds writes in place, so the second write simply wins\n * and two nodes ship as one. What that costs is concrete, because an id is the\n * durable step name (`step.do('{id}:send')`), the message idempotency key\n * (`{instanceId}:{id}`) and the cohort bucket seed: the twins share a\n * checkpoint, and the second send is suppressed as a duplicate of the first.\n * Cheap to check once at the end of the compile, so it is checked.\n */\nfunction assertUniqueIds(nodes: readonly NodeIR[], seen = new Set<string>()): void {\n for (const node of nodes) {\n if (seen.has(node.id)) {\n throw new Error(\n `toIR(): node id '${node.id}' is used twice — ids are durable step names and message idempotency keys, so they must be unique`\n );\n }\n seen.add(node.id);\n switch (node.type) {\n case 'branch':\n for (const branchCase of node.cases) assertUniqueIds(branchCase.flow, seen);\n if (node.otherwise) assertUniqueIds(node.otherwise, seen);\n break;\n case 'cohort':\n for (const arm of node.arms) assertUniqueIds(arm.flow, seen);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assertUniqueIds(node.onTimeout, seen);\n break;\n default:\n break;\n }\n }\n}\n\nclass WorkflowBuilderImpl extends FlowBuilderImpl implements WorkflowBuilder {\n constructor(\n private readonly options: WorkflowDefinition,\n /** Callsite of the workflow() call (provenance; lands in meta.loc). */\n private readonly loc: SourceLocIR | undefined\n ) {\n super();\n }\n\n private goalIR(): GoalIR | undefined {\n const goal = this.options.goal;\n if (goal === undefined) return undefined;\n if ('__condition' in goal) {\n return { condition: condIR(goal), exitOnMatch: true };\n }\n return {\n condition: condIR(goal.condition),\n ...(goal.within !== undefined\n ? { within: positiveDurationIR(goal.within, 'goal.within') }\n : {}),\n exitOnMatch: goal.exitOnMatch ?? true,\n };\n }\n\n private metaIR(): MetaIR | undefined {\n const { name, description } = this.options;\n if (name === undefined && description === undefined && this.loc === undefined) {\n return undefined;\n }\n return {\n ...(name !== undefined ? { name } : {}),\n ...(description !== undefined ? { description } : {}),\n ...(this.loc !== undefined ? { loc: this.loc } : {}),\n };\n }\n\n toIR(): WorkflowIR {\n const flowNodes = structuredClone(this.nodes);\n assignIds(flowNodes, '');\n assertUniqueIds(flowNodes);\n const goal = this.goalIR();\n const meta = this.metaIR();\n const body = {\n irVersion: IR_VERSION,\n id: this.options.id,\n ...(meta !== undefined ? { meta } : {}),\n trigger: (this.options.trigger as TriggerInternal).ir,\n ...(goal !== undefined ? { goal } : {}),\n ...(this.options.exitWhen !== undefined ? { exitWhen: condIR(this.options.exitWhen) } : {}),\n flow: flowNodes,\n };\n // semanticHash strips meta / label / reason, so metadata edits leave contentHash alone\n // Self-check: the compiler's output must pass IR's authoritative schema\n return WorkflowIRSchema.parse({ ...body, contentHash: semanticHash(body) });\n }\n}\n\n/**\n * Workflow definition: an id, configuration (trigger / goal / exitWhen),\n * optional labels, and chained steps.\n *\n * export const onboarding = workflow({\n * id: 'onboarding',\n * name: 'Onboarding',\n * trigger: trigger.event(e.sign_up),\n * }).email(email.welcome);\n */\nexport function workflow(definition: WorkflowDefinition): WorkflowBuilder {\n return new WorkflowBuilderImpl(definition, captureLoc(workflow));\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.cts","names":[],"sources":["../../src/dsl/workflow.ts"],"mappings":";;;;;;;;;;;;;;iBAyBiB;EACf,WAAW;;EAEX,SAAS;;EAET;;iBAGe;;;;;;EAMf;;EAGA,SAAS;;;;;;EAOT,OAAO,YAAY;;EAGnB,WAAW;;EASX;;EAGA;;iBAGe,wBAAwB;EACvC,QAAQ;;;;;;;;;;;;wBA+HM,SAAS,YAAY,qBAAqB"}
1
+ {"version":3,"file":"workflow.d.cts","names":[],"sources":["../../src/dsl/workflow.ts"],"mappings":";;;;;;;;;;;;;;iBAyBiB;EACf,WAAW;;EAEX,SAAS;;EAET;;iBAGe;;;;;;EAMf;;EAGA,SAAS;;;;;;EAOT,OAAO,YAAY;;EAGnB,WAAW;;EASX;;EAGA;;iBAGe,wBAAwB;EACvC,QAAQ;;;;;;;;;;;;wBAiIM,SAAS,YAAY,qBAAqB"}
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.mts","names":[],"sources":["../../src/dsl/workflow.ts"],"mappings":";;;;;;;;;;;;;;iBAyBiB;EACf,WAAW;;EAEX,SAAS;;EAET;;iBAGe;;;;;;EAMf;;EAGA,SAAS;;;;;;EAOT,OAAO,YAAY;;EAGnB,WAAW;;EASX;;EAGA;;iBAGe,wBAAwB;EACvC,QAAQ;;;;;;;;;;;;wBA+HM,SAAS,YAAY,qBAAqB"}
1
+ {"version":3,"file":"workflow.d.mts","names":[],"sources":["../../src/dsl/workflow.ts"],"mappings":";;;;;;;;;;;;;;iBAyBiB;EACf,WAAW;;EAEX,SAAS;;EAET;;iBAGe;;;;;;EAMf;;EAGA,SAAS;;;;;;EAOT,OAAO,YAAY;;EAGnB,WAAW;;EASX;;EAGA;;iBAGe,wBAAwB;EACvC,QAAQ;;;;;;;;;;;;wBAiIM,SAAS,YAAY,qBAAqB"}
@@ -1,8 +1,7 @@
1
- import { semanticHash } from "../hash.mjs";
2
1
  import { captureLoc } from "../provenance.mjs";
3
- import { durationIR } from "./base.mjs";
4
- import { condIR } from "./condition.mjs";
2
+ import { condIR, positiveDurationIR } from "./condition.mjs";
5
3
  import { FlowBuilderImpl } from "./flow.mjs";
4
+ import { semanticHash } from "../hash.mjs";
6
5
  import { WorkflowIR } from "../ir.mjs";
7
6
  //#region src/dsl/workflow.ts
8
7
  /**
@@ -67,7 +66,7 @@ var WorkflowBuilderImpl = class extends FlowBuilderImpl {
67
66
  };
68
67
  return {
69
68
  condition: condIR(goal.condition),
70
- ...goal.within !== void 0 ? { within: durationIR(goal.within) } : {},
69
+ ...goal.within !== void 0 ? { within: positiveDurationIR(goal.within, "goal.within") } : {},
71
70
  exitOnMatch: goal.exitOnMatch ?? true
72
71
  };
73
72
  }
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.mjs","names":["WorkflowIRSchema"],"sources":["../../src/dsl/workflow.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type GoalIR,\n IR_VERSION,\n type MetaIR,\n type NodeIR,\n type SourceLocIR,\n type WorkflowIR,\n WorkflowIR as WorkflowIRSchema,\n} from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport { type Condition, condIR } from './condition';\nimport { type FlowBuilder, FlowBuilderImpl } from './flow';\nimport type { TriggerInternal, TriggerRef } from './trigger';\n\n/**\n * The workflow — a named flow chain plus its configuration (options =\n * configuration, the chain = steps), and the compiler back half: `toIR()`\n * deep-clones the collected nodes, assigns structural-path ids, computes the\n * contentHash (metadata stripped, see hash.ts) and self-checks the result\n * against the authoritative IR schema.\n */\n\n/** Full conversion-goal configuration (customer.io's Goal & Exit semantics). */\nexport interface GoalOptions {\n condition: Condition;\n /** Attribution window: how long after entry a match still counts as a conversion. Unbounded by default. */\n within?: Duration;\n /** Exit on conversion. Defaults to true — this is what absorbs all those True→Exit arms in a UI canvas. */\n exitOnMatch?: boolean;\n}\n\nexport interface WorkflowDefinition {\n /**\n * Wire identity — engine instances, the entry ledger and stats keys are all\n * built on it (`WorkflowIR.id`), so it is not something to rename for\n * readability; `name` is.\n */\n id: string;\n\n /** Trigger: a trigger.xxx() asset. TODO: multiple triggers, entry frequency / re-entry policy. */\n trigger: TriggerRef;\n\n /**\n * Conversion goal: feeds reporting (conversion rate / attribution) and by\n * default exits on match. Checked before each node runs. Pass a condition\n * for the shorthand, or GoalOptions for the full form.\n */\n goal?: Condition | GoalOptions;\n\n /** Plain exit condition: leaving without counting as a conversion (unsubscribed, no longer eligible). Coexists with goal. */\n exitWhen?: Condition;\n\n /*\n * Everything below is human-facing metadata: it lands in IR's `meta` field\n * and is **excluded from contentHash**. Editing it neither invalidates the\n * version in-flight users are pinned to nor shows up as a change in plan.\n */\n\n /** Human label: what the studio shows instead of the id. */\n name?: string;\n\n /** One line on what this flow does (UI lists, plan output). */\n description?: string;\n}\n\nexport interface WorkflowBuilder extends FlowBuilder {\n toIR(): WorkflowIR;\n}\n\n/**\n * Assign node ids from the structural path (rules in ir.ts's header). Written\n * in place into the tree toIR deep-cloned, never into the builder's own nodes.\n */\nfunction assignIds(nodes: NodeIR[], prefix: string): void {\n nodes.forEach((node, i) => {\n const id = prefix === '' ? String(i) : `${prefix}.${i}`;\n node.id = id;\n switch (node.type) {\n case 'branch':\n node.cases.forEach((c, ci) => assignIds(c.flow, `${id}.c${ci}`));\n if (node.otherwise) assignIds(node.otherwise, `${id}.o`);\n break;\n case 'cohort':\n node.arms.forEach((arm) => assignIds(arm.flow, `${id}.${arm.name}`));\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assignIds(node.onTimeout, `${id}.t`);\n break;\n default:\n break;\n }\n });\n}\n\n/**\n * Ids are derived, never authored, so a duplicate is always a compiler bug —\n * and a silent one: assignIds writes in place, so the second write simply wins\n * and two nodes ship as one. What that costs is concrete, because an id is the\n * durable step name (`step.do('{id}:send')`), the message idempotency key\n * (`{instanceId}:{id}`) and the cohort bucket seed: the twins share a\n * checkpoint, and the second send is suppressed as a duplicate of the first.\n * Cheap to check once at the end of the compile, so it is checked.\n */\nfunction assertUniqueIds(nodes: readonly NodeIR[], seen = new Set<string>()): void {\n for (const node of nodes) {\n if (seen.has(node.id)) {\n throw new Error(\n `toIR(): node id '${node.id}' is used twice — ids are durable step names and message idempotency keys, so they must be unique`\n );\n }\n seen.add(node.id);\n switch (node.type) {\n case 'branch':\n for (const branchCase of node.cases) assertUniqueIds(branchCase.flow, seen);\n if (node.otherwise) assertUniqueIds(node.otherwise, seen);\n break;\n case 'cohort':\n for (const arm of node.arms) assertUniqueIds(arm.flow, seen);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assertUniqueIds(node.onTimeout, seen);\n break;\n default:\n break;\n }\n }\n}\n\nclass WorkflowBuilderImpl extends FlowBuilderImpl implements WorkflowBuilder {\n constructor(\n private readonly options: WorkflowDefinition,\n /** Callsite of the workflow() call (provenance; lands in meta.loc). */\n private readonly loc: SourceLocIR | undefined\n ) {\n super();\n }\n\n private goalIR(): GoalIR | undefined {\n const goal = this.options.goal;\n if (goal === undefined) return undefined;\n if ('__condition' in goal) {\n return { condition: condIR(goal), exitOnMatch: true };\n }\n return {\n condition: condIR(goal.condition),\n ...(goal.within !== undefined ? { within: durationIR(goal.within) } : {}),\n exitOnMatch: goal.exitOnMatch ?? true,\n };\n }\n\n private metaIR(): MetaIR | undefined {\n const { name, description } = this.options;\n if (name === undefined && description === undefined && this.loc === undefined) {\n return undefined;\n }\n return {\n ...(name !== undefined ? { name } : {}),\n ...(description !== undefined ? { description } : {}),\n ...(this.loc !== undefined ? { loc: this.loc } : {}),\n };\n }\n\n toIR(): WorkflowIR {\n const flowNodes = structuredClone(this.nodes);\n assignIds(flowNodes, '');\n assertUniqueIds(flowNodes);\n const goal = this.goalIR();\n const meta = this.metaIR();\n const body = {\n irVersion: IR_VERSION,\n id: this.options.id,\n ...(meta !== undefined ? { meta } : {}),\n trigger: (this.options.trigger as TriggerInternal).ir,\n ...(goal !== undefined ? { goal } : {}),\n ...(this.options.exitWhen !== undefined ? { exitWhen: condIR(this.options.exitWhen) } : {}),\n flow: flowNodes,\n };\n // semanticHash strips meta / label / reason, so metadata edits leave contentHash alone\n // Self-check: the compiler's output must pass IR's authoritative schema\n return WorkflowIRSchema.parse({ ...body, contentHash: semanticHash(body) });\n }\n}\n\n/**\n * Workflow definition: an id, configuration (trigger / goal / exitWhen),\n * optional labels, and chained steps.\n *\n * export const onboarding = workflow({\n * id: 'onboarding',\n * name: 'Onboarding',\n * trigger: trigger.event(e.sign_up),\n * }).email(email.welcome);\n */\nexport function workflow(definition: WorkflowDefinition): WorkflowBuilder {\n return new WorkflowBuilderImpl(definition, captureLoc(workflow));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"workflow.mjs","names":["WorkflowIRSchema"],"sources":["../../src/dsl/workflow.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type GoalIR,\n IR_VERSION,\n type MetaIR,\n type NodeIR,\n type SourceLocIR,\n type WorkflowIR,\n WorkflowIR as WorkflowIRSchema,\n} from '../ir';\nimport { captureLoc } from '../provenance';\nimport type { Duration } from './base';\nimport { type Condition, condIR, positiveDurationIR } from './condition';\nimport { type FlowBuilder, FlowBuilderImpl } from './flow';\nimport type { TriggerInternal, TriggerRef } from './trigger';\n\n/**\n * The workflow — a named flow chain plus its configuration (options =\n * configuration, the chain = steps), and the compiler back half: `toIR()`\n * deep-clones the collected nodes, assigns structural-path ids, computes the\n * contentHash (metadata stripped, see hash.ts) and self-checks the result\n * against the authoritative IR schema.\n */\n\n/** Full conversion-goal configuration (customer.io's Goal & Exit semantics). */\nexport interface GoalOptions {\n condition: Condition;\n /** Attribution window: how long after entry a match still counts as a conversion. Unbounded by default. */\n within?: Duration;\n /** Exit on conversion. Defaults to true — this is what absorbs all those True→Exit arms in a UI canvas. */\n exitOnMatch?: boolean;\n}\n\nexport interface WorkflowDefinition {\n /**\n * Wire identity — engine instances, the entry ledger and stats keys are all\n * built on it (`WorkflowIR.id`), so it is not something to rename for\n * readability; `name` is.\n */\n id: string;\n\n /** Trigger: a trigger.xxx() asset. TODO: multiple triggers, entry frequency / re-entry policy. */\n trigger: TriggerRef;\n\n /**\n * Conversion goal: feeds reporting (conversion rate / attribution) and by\n * default exits on match. Checked before each node runs. Pass a condition\n * for the shorthand, or GoalOptions for the full form.\n */\n goal?: Condition | GoalOptions;\n\n /** Plain exit condition: leaving without counting as a conversion (unsubscribed, no longer eligible). Coexists with goal. */\n exitWhen?: Condition;\n\n /*\n * Everything below is human-facing metadata: it lands in IR's `meta` field\n * and is **excluded from contentHash**. Editing it neither invalidates the\n * version in-flight users are pinned to nor shows up as a change in plan.\n */\n\n /** Human label: what the studio shows instead of the id. */\n name?: string;\n\n /** One line on what this flow does (UI lists, plan output). */\n description?: string;\n}\n\nexport interface WorkflowBuilder extends FlowBuilder {\n toIR(): WorkflowIR;\n}\n\n/**\n * Assign node ids from the structural path (rules in ir.ts's header). Written\n * in place into the tree toIR deep-cloned, never into the builder's own nodes.\n */\nfunction assignIds(nodes: NodeIR[], prefix: string): void {\n nodes.forEach((node, i) => {\n const id = prefix === '' ? String(i) : `${prefix}.${i}`;\n node.id = id;\n switch (node.type) {\n case 'branch':\n node.cases.forEach((c, ci) => assignIds(c.flow, `${id}.c${ci}`));\n if (node.otherwise) assignIds(node.otherwise, `${id}.o`);\n break;\n case 'cohort':\n node.arms.forEach((arm) => assignIds(arm.flow, `${id}.${arm.name}`));\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assignIds(node.onTimeout, `${id}.t`);\n break;\n default:\n break;\n }\n });\n}\n\n/**\n * Ids are derived, never authored, so a duplicate is always a compiler bug —\n * and a silent one: assignIds writes in place, so the second write simply wins\n * and two nodes ship as one. What that costs is concrete, because an id is the\n * durable step name (`step.do('{id}:send')`), the message idempotency key\n * (`{instanceId}:{id}`) and the cohort bucket seed: the twins share a\n * checkpoint, and the second send is suppressed as a duplicate of the first.\n * Cheap to check once at the end of the compile, so it is checked.\n */\nfunction assertUniqueIds(nodes: readonly NodeIR[], seen = new Set<string>()): void {\n for (const node of nodes) {\n if (seen.has(node.id)) {\n throw new Error(\n `toIR(): node id '${node.id}' is used twice — ids are durable step names and message idempotency keys, so they must be unique`\n );\n }\n seen.add(node.id);\n switch (node.type) {\n case 'branch':\n for (const branchCase of node.cases) assertUniqueIds(branchCase.flow, seen);\n if (node.otherwise) assertUniqueIds(node.otherwise, seen);\n break;\n case 'cohort':\n for (const arm of node.arms) assertUniqueIds(arm.flow, seen);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assertUniqueIds(node.onTimeout, seen);\n break;\n default:\n break;\n }\n }\n}\n\nclass WorkflowBuilderImpl extends FlowBuilderImpl implements WorkflowBuilder {\n constructor(\n private readonly options: WorkflowDefinition,\n /** Callsite of the workflow() call (provenance; lands in meta.loc). */\n private readonly loc: SourceLocIR | undefined\n ) {\n super();\n }\n\n private goalIR(): GoalIR | undefined {\n const goal = this.options.goal;\n if (goal === undefined) return undefined;\n if ('__condition' in goal) {\n return { condition: condIR(goal), exitOnMatch: true };\n }\n return {\n condition: condIR(goal.condition),\n ...(goal.within !== undefined\n ? { within: positiveDurationIR(goal.within, 'goal.within') }\n : {}),\n exitOnMatch: goal.exitOnMatch ?? true,\n };\n }\n\n private metaIR(): MetaIR | undefined {\n const { name, description } = this.options;\n if (name === undefined && description === undefined && this.loc === undefined) {\n return undefined;\n }\n return {\n ...(name !== undefined ? { name } : {}),\n ...(description !== undefined ? { description } : {}),\n ...(this.loc !== undefined ? { loc: this.loc } : {}),\n };\n }\n\n toIR(): WorkflowIR {\n const flowNodes = structuredClone(this.nodes);\n assignIds(flowNodes, '');\n assertUniqueIds(flowNodes);\n const goal = this.goalIR();\n const meta = this.metaIR();\n const body = {\n irVersion: IR_VERSION,\n id: this.options.id,\n ...(meta !== undefined ? { meta } : {}),\n trigger: (this.options.trigger as TriggerInternal).ir,\n ...(goal !== undefined ? { goal } : {}),\n ...(this.options.exitWhen !== undefined ? { exitWhen: condIR(this.options.exitWhen) } : {}),\n flow: flowNodes,\n };\n // semanticHash strips meta / label / reason, so metadata edits leave contentHash alone\n // Self-check: the compiler's output must pass IR's authoritative schema\n return WorkflowIRSchema.parse({ ...body, contentHash: semanticHash(body) });\n }\n}\n\n/**\n * Workflow definition: an id, configuration (trigger / goal / exitWhen),\n * optional labels, and chained steps.\n *\n * export const onboarding = workflow({\n * id: 'onboarding',\n * name: 'Onboarding',\n * trigger: trigger.event(e.sign_up),\n * }).email(email.welcome);\n */\nexport function workflow(definition: WorkflowDefinition): WorkflowBuilder {\n return new WorkflowBuilderImpl(definition, captureLoc(workflow));\n}\n"],"mappings":"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"}
@@ -1,20 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/engine/actions.ts
3
3
  var RegistryActionInvoker = class {
4
- policy;
5
4
  registry;
6
- constructor(actions, policy = "warn") {
7
- this.policy = policy;
5
+ constructor(actions) {
8
6
  this.registry = new Map(actions.map((a) => [a.name, a]));
9
7
  }
10
8
  async invoke(invocation) {
11
9
  const registered = this.registry.get(invocation.action);
12
10
  if (registered === void 0) throw new Error(`action '${invocation.action}' is not registered in this runtime — was it removed from the deployed bundle while workflows still reference it?`);
13
- if (this.policy !== "rolling" && invocation.codeHash !== void 0 && invocation.codeHash !== registered.codeHash) {
14
- const skew = `action '${invocation.action}': deployed code (${registered.codeHash}) differs from the version pinned in this journey's IR (${invocation.codeHash})`;
15
- if (this.policy === "strict") throw new Error(skew);
16
- console.warn(`${skew} — running the deployed version`);
17
- }
18
11
  await registered.handler(invocation.args, {
19
12
  userId: invocation.userId,
20
13
  idempotencyKey: invocation.idempotencyKey
@@ -1 +1 @@
1
- {"version":3,"file":"actions.cjs","names":[],"sources":["../../src/engine/actions.ts"],"sourcesContent":["import type { ScalarIR } from '../ir';\nimport type { ActionInvocation, ActionInvoker } from './ports';\n\n/**\n * The default ActionInvoker: a name → handler registry (the runtime half of\n * the dual-plane model — IR carries identity, the deployed bundle carries\n * code). Adapter-agnostic: the Cloudflare runner uses it and an AWS adapter\n * would too.\n */\n\n/**\n * What the registry stores — structurally satisfied by the DSL's ActionRef,\n * so the same `action(...)` objects the workflows reference are registered\n * directly, exactly like createMessageSender overrides plug in senders.\n */\nexport interface RegisteredAction {\n readonly name: string;\n readonly codeHash: string;\n /** Method syntax on purpose: accepts ActionRef handlers with narrower arg types. */\n handler(\n args: Record<string, ScalarIR | undefined>,\n ctx: { userId: string; idempotencyKey: string }\n ): Promise<void> | void;\n}\n\n/**\n * Version-consistency tiers for the codeHash pinned in IR vs the code actually\n * registered. Deploys are rolling — Worker code and IR never flip in the same\n * instant, and different build pipelines can print the same function\n * differently — so 'warn' is the default: run the current code, log the skew.\n * 'strict' is for actions where running the wrong version is worse than\n * failing the step; 'rolling' silences the log entirely.\n */\nexport type ActionVersionPolicy = 'rolling' | 'warn' | 'strict';\n\nexport class RegistryActionInvoker implements ActionInvoker {\n private readonly registry: ReadonlyMap<string, RegisteredAction>;\n\n constructor(\n actions: readonly RegisteredAction[],\n private readonly policy: ActionVersionPolicy = 'warn'\n ) {\n this.registry = new Map(actions.map((a) => [a.name, a]));\n }\n\n async invoke(invocation: ActionInvocation): Promise<void> {\n const registered = this.registry.get(invocation.action);\n if (registered === undefined) {\n throw new Error(\n `action '${invocation.action}' is not registered in this runtime — was it removed from the deployed bundle while workflows still reference it?`\n );\n }\n if (\n this.policy !== 'rolling' &&\n invocation.codeHash !== undefined &&\n invocation.codeHash !== registered.codeHash\n ) {\n const skew = `action '${invocation.action}': deployed code (${registered.codeHash}) differs from the version pinned in this journey's IR (${invocation.codeHash})`;\n if (this.policy === 'strict') throw new Error(skew);\n console.warn(`${skew} — running the deployed version`);\n }\n // The key is forwarded, not consumed here: this invoker has no state to\n // de-duplicate against, and the step body it runs inside is replayed — the\n // handler is the only place that can collapse the duplicate side effect.\n await registered.handler(invocation.args, {\n userId: invocation.userId,\n idempotencyKey: invocation.idempotencyKey,\n });\n }\n}\n"],"mappings":";;AAmCA,IAAa,wBAAb,MAA4D;CAKvC;CAJnB;CAEA,YACE,SACA,SAA+C,QAC/C;EADiB,KAAA,SAAA;EAEjB,KAAK,WAAW,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CACzD;CAEA,MAAM,OAAO,YAA6C;EACxD,MAAM,aAAa,KAAK,SAAS,IAAI,WAAW,MAAM;EACtD,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,MACR,WAAW,WAAW,OAAO,kHAC/B;EAEF,IACE,KAAK,WAAW,aAChB,WAAW,aAAa,KAAA,KACxB,WAAW,aAAa,WAAW,UACnC;GACA,MAAM,OAAO,WAAW,WAAW,OAAO,oBAAoB,WAAW,SAAS,0DAA0D,WAAW,SAAS;GAChK,IAAI,KAAK,WAAW,UAAU,MAAM,IAAI,MAAM,IAAI;GAClD,QAAQ,KAAK,GAAG,KAAK,gCAAgC;EACvD;EAIA,MAAM,WAAW,QAAQ,WAAW,MAAM;GACxC,QAAQ,WAAW;GACnB,gBAAgB,WAAW;EAC7B,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"actions.cjs","names":[],"sources":["../../src/engine/actions.ts"],"sourcesContent":["import type { ScalarIR } from '../ir';\nimport type { ActionInvocation, ActionInvoker } from './ports';\n\n/**\n * The default ActionInvoker: a name → handler registry (the runtime half of\n * the dual-plane model — IR carries identity, the deployed bundle carries\n * code). Adapter-agnostic: the Cloudflare runner uses it and an AWS adapter\n * would too.\n */\n\n/**\n * What the registry stores — structurally satisfied by the DSL's ActionRef,\n * so the same `action(...)` objects the workflows reference are registered\n * directly, exactly like createMessageSender overrides plug in senders.\n */\nexport interface RegisteredAction {\n readonly name: string;\n /** Method syntax on purpose: accepts ActionRef handlers with narrower arg types. */\n handler(\n args: Record<string, ScalarIR | undefined>,\n ctx: { userId: string; idempotencyKey: string }\n ): Promise<void> | void;\n}\n\nexport class RegistryActionInvoker implements ActionInvoker {\n private readonly registry: ReadonlyMap<string, RegisteredAction>;\n\n constructor(actions: readonly RegisteredAction[]) {\n this.registry = new Map(actions.map((a) => [a.name, a]));\n }\n\n async invoke(invocation: ActionInvocation): Promise<void> {\n const registered = this.registry.get(invocation.action);\n if (registered === undefined) {\n throw new Error(\n `action '${invocation.action}' is not registered in this runtime — was it removed from the deployed bundle while workflows still reference it?`\n );\n }\n // The key is forwarded, not consumed here: this invoker has no state to\n // de-duplicate against, and the step body it runs inside is replayed — the\n // handler is the only place that can collapse the duplicate side effect.\n await registered.handler(invocation.args, {\n userId: invocation.userId,\n idempotencyKey: invocation.idempotencyKey,\n });\n }\n}\n"],"mappings":";;AAwBA,IAAa,wBAAb,MAA4D;CAC1D;CAEA,YAAY,SAAsC;EAChD,KAAK,WAAW,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CACzD;CAEA,MAAM,OAAO,YAA6C;EACxD,MAAM,aAAa,KAAK,SAAS,IAAI,WAAW,MAAM;EACtD,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,MACR,WAAW,WAAW,OAAO,kHAC/B;EAKF,MAAM,WAAW,QAAQ,WAAW,MAAM;GACxC,QAAQ,WAAW;GACnB,gBAAgB,WAAW;EAC7B,CAAC;CACH;AACF"}