@cowliss/cli 0.10.0 → 0.11.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 (117) hide show
  1. package/{examples → blueprints}/abandoned-checkout/journeys/abandoned-checkout.ts +1 -1
  2. package/{examples → blueprints}/activity-decay/journeys/activity-decay.ts +1 -1
  3. package/blueprints/ecommerce/emails/_shell.tsx +133 -0
  4. package/blueprints/ecommerce/emails/abandoned-checkout-last-call.tsx +46 -0
  5. package/blueprints/ecommerce/emails/abandoned-checkout.tsx +46 -0
  6. package/blueprints/ecommerce/emails/order-thanks.tsx +44 -0
  7. package/blueprints/ecommerce/emails/replenishment.tsx +40 -0
  8. package/blueprints/ecommerce/emails/review-request.tsx +44 -0
  9. package/blueprints/ecommerce/emails/vip-thanks.tsx +51 -0
  10. package/blueprints/ecommerce/emails/welcome.tsx +48 -0
  11. package/blueprints/ecommerce/emails/winback-offer.tsx +41 -0
  12. package/blueprints/ecommerce/emails/winback-reminder.tsx +45 -0
  13. package/blueprints/ecommerce/journeys/_helpers.ts +14 -0
  14. package/blueprints/ecommerce/journeys/abandoned-checkout.ts +44 -0
  15. package/blueprints/ecommerce/journeys/first-order-thanks.ts +40 -0
  16. package/blueprints/ecommerce/journeys/replenishment.ts +39 -0
  17. package/blueprints/ecommerce/journeys/vip.ts +29 -0
  18. package/blueprints/ecommerce/journeys/welcome.ts +42 -0
  19. package/blueprints/ecommerce/journeys/winback.ts +35 -0
  20. package/blueprints/ecommerce/scenarios/abandoned-checkout.last-call.json +30 -0
  21. package/blueprints/ecommerce/scenarios/abandoned-checkout.recovered.json +24 -0
  22. package/blueprints/ecommerce/scenarios/abandoned-checkout.reminded.json +33 -0
  23. package/blueprints/ecommerce/scenarios/first-order-thanks.delivered.json +35 -0
  24. package/blueprints/ecommerce/scenarios/first-order-thanks.never-delivered.json +21 -0
  25. package/blueprints/ecommerce/scenarios/replenishment.due.json +21 -0
  26. package/blueprints/ecommerce/scenarios/replenishment.reordered.json +20 -0
  27. package/blueprints/ecommerce/scenarios/vip.crossed.json +22 -0
  28. package/blueprints/ecommerce/scenarios/welcome.new-shopper.json +21 -0
  29. package/blueprints/ecommerce/scenarios/winback.returned.json +26 -0
  30. package/blueprints/ecommerce/scenarios/winback.still-gone.json +30 -0
  31. package/blueprints/ecommerce/segments/first-time-buyers.ts +24 -0
  32. package/blueprints/ecommerce/segments/lapsed.ts +27 -0
  33. package/blueprints/ecommerce/segments/repeat-buyers.ts +18 -0
  34. package/blueprints/ecommerce/segments/vip-customers.ts +20 -0
  35. package/blueprints/mobile-game/emails/_shell.tsx +138 -0
  36. package/blueprints/mobile-game/emails/day-two-return.tsx +41 -0
  37. package/blueprints/mobile-game/emails/lapsed-offer.tsx +41 -0
  38. package/blueprints/mobile-game/emails/lapsed-reminder.tsx +48 -0
  39. package/blueprints/mobile-game/emails/purchase-receipt.tsx +47 -0
  40. package/blueprints/mobile-game/emails/spender-thanks.tsx +53 -0
  41. package/blueprints/mobile-game/emails/stuck-hint.tsx +48 -0
  42. package/blueprints/mobile-game/emails/tutorial-nudge.tsx +48 -0
  43. package/blueprints/mobile-game/journeys/_helpers.ts +14 -0
  44. package/blueprints/mobile-game/journeys/day-two-return.ts +40 -0
  45. package/blueprints/mobile-game/journeys/first-purchase.ts +34 -0
  46. package/blueprints/mobile-game/journeys/lapsed-player.ts +39 -0
  47. package/blueprints/mobile-game/journeys/onboarding.ts +35 -0
  48. package/blueprints/mobile-game/journeys/spender-care.ts +41 -0
  49. package/blueprints/mobile-game/journeys/stuck.ts +58 -0
  50. package/blueprints/mobile-game/scenarios/day-two-return.came-back.json +16 -0
  51. package/blueprints/mobile-game/scenarios/day-two-return.no-show.json +21 -0
  52. package/blueprints/mobile-game/scenarios/first-purchase.paid.json +21 -0
  53. package/blueprints/mobile-game/scenarios/lapsed-player.returned.json +26 -0
  54. package/blueprints/mobile-game/scenarios/lapsed-player.still-gone.json +30 -0
  55. package/blueprints/mobile-game/scenarios/onboarding.finished.json +16 -0
  56. package/blueprints/mobile-game/scenarios/onboarding.stalled.json +21 -0
  57. package/blueprints/mobile-game/scenarios/spender-care.crossed.json +22 -0
  58. package/blueprints/mobile-game/scenarios/stuck.cleared.json +16 -0
  59. package/blueprints/mobile-game/scenarios/stuck.three-fails.json +30 -0
  60. package/blueprints/mobile-game/segments/active-players.ts +30 -0
  61. package/blueprints/mobile-game/segments/lapsed-players.ts +29 -0
  62. package/blueprints/mobile-game/segments/new-players.ts +31 -0
  63. package/blueprints/mobile-game/segments/paying-players.ts +21 -0
  64. package/blueprints/saas-trial/emails/_shell.tsx +151 -0
  65. package/blueprints/saas-trial/emails/activated.tsx +61 -0
  66. package/blueprints/saas-trial/emails/activation-help.tsx +49 -0
  67. package/blueprints/saas-trial/emails/activation-nudge.tsx +48 -0
  68. package/blueprints/saas-trial/emails/invite-your-team.tsx +58 -0
  69. package/blueprints/saas-trial/emails/trial-converted.tsx +57 -0
  70. package/blueprints/saas-trial/emails/trial-ended.tsx +52 -0
  71. package/blueprints/saas-trial/emails/trial-ending-activated.tsx +53 -0
  72. package/blueprints/saas-trial/emails/trial-ending-unactivated.tsx +54 -0
  73. package/blueprints/saas-trial/emails/trial-welcome.tsx +57 -0
  74. package/blueprints/saas-trial/journeys/_helpers.ts +14 -0
  75. package/blueprints/saas-trial/journeys/activated.ts +42 -0
  76. package/blueprints/saas-trial/journeys/activation-nudge.ts +44 -0
  77. package/blueprints/saas-trial/journeys/invite-your-team.ts +42 -0
  78. package/blueprints/saas-trial/journeys/trial-ended.ts +39 -0
  79. package/blueprints/saas-trial/journeys/trial-ending.ts +58 -0
  80. package/blueprints/saas-trial/journeys/trial-welcome.ts +35 -0
  81. package/blueprints/saas-trial/scenarios/activated.first-project.json +14 -0
  82. package/blueprints/saas-trial/scenarios/activated.second-project.json +12 -0
  83. package/blueprints/saas-trial/scenarios/activation-nudge.activated-late.json +16 -0
  84. package/blueprints/saas-trial/scenarios/activation-nudge.activated.json +8 -0
  85. package/blueprints/saas-trial/scenarios/activation-nudge.stalled.json +23 -0
  86. package/blueprints/saas-trial/scenarios/invite-your-team.alone.json +16 -0
  87. package/blueprints/saas-trial/scenarios/invite-your-team.invited.json +8 -0
  88. package/blueprints/saas-trial/scenarios/trial-ended.converted.json +16 -0
  89. package/blueprints/saas-trial/scenarios/trial-ended.expired.json +13 -0
  90. package/blueprints/saas-trial/scenarios/trial-ending.activated.json +20 -0
  91. package/blueprints/saas-trial/scenarios/trial-ending.converted.json +12 -0
  92. package/blueprints/saas-trial/scenarios/trial-ending.unactivated.json +16 -0
  93. package/blueprints/saas-trial/scenarios/trial-welcome.started.json +13 -0
  94. package/blueprints/saas-trial/segments/activated-users.ts +25 -0
  95. package/blueprints/saas-trial/segments/converted.ts +28 -0
  96. package/blueprints/saas-trial/segments/trialing.ts +26 -0
  97. package/blueprints/saas-trial/segments/unactivated.ts +27 -0
  98. package/{examples → blueprints}/winback/journeys/winback.ts +18 -7
  99. package/blueprints/winback/scenarios/winback.bought-again.json +14 -0
  100. package/{examples/winback/scenarios/winback.json → blueprints/winback/scenarios/winback.lapsed.json} +2 -5
  101. package/dist/guest/{driver-poSdZIj8.js → driver-B_pjmy5g.js} +2 -1
  102. package/dist/guest/driver-emails.js +3 -2
  103. package/dist/guest/driver.d.ts +1 -1
  104. package/dist/guest/driver.js +1 -1
  105. package/dist/guest/{index-EqBCZnpq.d.ts → index-DADBwPaG.d.ts} +63 -12
  106. package/dist/guest/{journeys-F8Yk8s1P.js → journeys-v2-Djgs8U91.js} +87 -71
  107. package/dist/guest/journeys.d.ts +1 -1
  108. package/dist/guest/journeys.js +58 -1
  109. package/dist/guest/segments.d.ts +81 -0
  110. package/dist/guest/segments.js +35 -0
  111. package/dist/index.js +1177 -405
  112. package/package.json +6 -2
  113. /package/{examples → blueprints}/abandoned-checkout/emails/abandoned-checkout.tsx +0 -0
  114. /package/{examples → blueprints}/abandoned-checkout/scenarios/abandoned-checkout.recovered.json +0 -0
  115. /package/{examples → blueprints}/abandoned-checkout/scenarios/abandoned-checkout.timeout.json +0 -0
  116. /package/{examples → blueprints}/activity-decay/scenarios/activity-decay.json +0 -0
  117. /package/{examples → blueprints}/winback/emails/winback.tsx +0 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": { "email": "ada@example.com", "firstName": "Ada" }
5
+ },
6
+ "events": [{ "at": "5d", "event": "subscription.created" }],
7
+ "expect": [
8
+ {
9
+ "activity": "sendEmail",
10
+ "input": {
11
+ "template": "trial-converted",
12
+ "props": { "firstName": "Ada" }
13
+ }
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": { "email": "ada@example.com", "firstName": "Ada" }
5
+ },
6
+ "events": [],
7
+ "expect": [
8
+ {
9
+ "activity": "sendEmail",
10
+ "input": { "template": "trial-ended", "props": { "firstName": "Ada" } }
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": {
5
+ "email": "ada@example.com",
6
+ "firstName": "Ada",
7
+ "activated": true
8
+ }
9
+ },
10
+ "events": [],
11
+ "expect": [
12
+ {
13
+ "activity": "sendEmail",
14
+ "input": {
15
+ "template": "trial-ending-activated",
16
+ "props": { "firstName": "Ada" }
17
+ }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": {
5
+ "email": "ada@example.com",
6
+ "firstName": "Ada",
7
+ "activated": true
8
+ }
9
+ },
10
+ "events": [{ "at": "5d", "event": "subscription.created" }],
11
+ "expect": []
12
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": { "email": "ada@example.com", "firstName": "Ada" }
5
+ },
6
+ "events": [],
7
+ "expect": [
8
+ {
9
+ "activity": "sendEmail",
10
+ "input": {
11
+ "template": "trial-ending-unactivated",
12
+ "props": { "firstName": "Ada" }
13
+ }
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": { "email": "ada@example.com", "firstName": "Ada" }
5
+ },
6
+ "events": [],
7
+ "expect": [
8
+ {
9
+ "activity": "sendEmail",
10
+ "input": { "template": "trial-welcome", "props": { "firstName": "Ada" } }
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,25 @@
1
+ import { defineSegment } from "@cowliss/cli/segments";
2
+
3
+ /**
4
+ * Reached the activation milestone: created at least one project, ever.
5
+ * No trial predicate, so someone who activates and then subscribes stays
6
+ * in — this is the audience you compare everything else against, and it
7
+ * would tell you nothing if people dropped out of it on payday.
8
+ *
9
+ * Named `activated-users` and not `activated`: a segment name and a
10
+ * journey key share one namespace per organization, and the journey that
11
+ * congratulates someone for getting here is called `activated`.
12
+ *
13
+ * Change `project.created` to your own milestone here and in
14
+ * `unactivated.ts` together. A milestone that means one thing in one file
15
+ * and another in the other is how a person ends up nudged to do what they
16
+ * have already done.
17
+ */
18
+ export default defineSegment({
19
+ description: "Created at least one project.",
20
+ tags: ["saas", "activation"],
21
+ match: "all",
22
+ predicates: [
23
+ { kind: "event", name: "project.created", op: "performed", atLeast: 1 },
24
+ ],
25
+ });
@@ -0,0 +1,28 @@
1
+ import { defineSegment } from "@cowliss/cli/segments";
2
+
3
+ /**
4
+ * Paying: sent `subscription.created` at least once.
5
+ *
6
+ * No journey in this blueprint enrolls from it, and that is on purpose.
7
+ * It is here because it is the number the business watches, because it is
8
+ * what `trialing` is defined against, and because it is the audience you
9
+ * want on hand the day you write the programme that comes after the trial
10
+ * — onboarding, expansion, renewal — which is a different set of files
11
+ * from this one.
12
+ *
13
+ * A segment does not need a journey to earn its place. The dashboard
14
+ * counts it, and every other audience here is drawn relative to it.
15
+ */
16
+ export default defineSegment({
17
+ description: "Subscribed to a paid plan.",
18
+ tags: ["saas", "revenue"],
19
+ match: "all",
20
+ predicates: [
21
+ {
22
+ kind: "event",
23
+ name: "subscription.created",
24
+ op: "performed",
25
+ atLeast: 1,
26
+ },
27
+ ],
28
+ });
@@ -0,0 +1,26 @@
1
+ import { defineSegment } from "@cowliss/cli/segments";
2
+
3
+ /**
4
+ * Started a trial and has not subscribed. Both predicates matter: the
5
+ * first is the trial, the second is what takes someone out of it the
6
+ * moment they pay, so the trial journeys stop on their own rather than
7
+ * checking for a subscription at every step.
8
+ *
9
+ * A file rather than a trigger's inline definition (ADR 0023) because the
10
+ * dashboard reads it too: this is the audience whose size is the answer to
11
+ * "how many trials are open right now".
12
+ *
13
+ * `trial.started` is the one event you have to send yourself; the other
14
+ * three are things your product already does. If yours is called
15
+ * `Trial Started` or `signup_completed`, change the name here and in
16
+ * `unactivated.ts` rather than renaming the event in your own app.
17
+ */
18
+ export default defineSegment({
19
+ description: "In a trial: started one and has not subscribed.",
20
+ tags: ["saas", "trial"],
21
+ match: "all",
22
+ predicates: [
23
+ { kind: "event", name: "trial.started", op: "performed", atLeast: 1 },
24
+ { kind: "event", name: "subscription.created", op: "notPerformed" },
25
+ ],
26
+ });
@@ -0,0 +1,27 @@
1
+ import { defineSegment } from "@cowliss/cli/segments";
2
+
3
+ /**
4
+ * Started a trial and has not reached the activation milestone yet. This
5
+ * is the audience the whole programme is built around: what people in it
6
+ * do next decides your conversion rate.
7
+ *
8
+ * `project.created` is the milestone, and it is the one line in this
9
+ * blueprint most people will change. It stands in for the moment someone
10
+ * has actually used your product rather than looked at it, so pick the
11
+ * event that means that for yours: a workspace created, a first API call,
12
+ * a repository connected, a document shared. Whatever you pick, name it
13
+ * here and in `activated-users.ts`, and the four journeys follow.
14
+ *
15
+ * Nobody leaves this audience by waiting. They leave it by activating, and
16
+ * they never come back, so a journey enrolling from it runs once per
17
+ * person.
18
+ */
19
+ export default defineSegment({
20
+ description: "In a trial and has not created a project yet.",
21
+ tags: ["saas", "activation"],
22
+ match: "all",
23
+ predicates: [
24
+ { kind: "event", name: "trial.started", op: "performed", atLeast: 1 },
25
+ { kind: "event", name: "project.created", op: "notPerformed" },
26
+ ],
27
+ });
@@ -1,10 +1,14 @@
1
1
  import { defineJourney } from "@cowliss/cli/journeys";
2
2
 
3
3
  /**
4
- * Winback: `purchase_completed` starts (or restarts) an execution and marks
5
- * the profile as having bought recently; every further purchase inside 30
6
- * days resets the silence clock; after 30 days of silence the flag is unset
7
- * and one winback email goes out.
4
+ * Winback: `purchase_completed` starts an execution and marks the profile as
5
+ * having bought recently; every further purchase inside 30 days restarts the
6
+ * execution, which resets the silence clock with a fresh, empty journal;
7
+ * after 30 days of silence the flag is unset and one winback email goes out.
8
+ *
9
+ * `purchase_completed` is a name your own app sends through `track`, not one
10
+ * Cowliss defines: the only events Cowliss writes itself are the reserved
11
+ * `system.` ones. Nothing here runs until your app tracks a purchase.
8
12
  *
9
13
  * `marketing` and no `enrollment`, so each customer is won back once, ever:
10
14
  * the purchase that answers the email does not start a second execution.
@@ -12,7 +16,7 @@ import { defineJourney } from "@cowliss/cli/journeys";
12
16
  * customer back every time they lapse, say how long to leave between
13
17
  * attempts: `enrollment: { cooldown: "180d" }`.
14
18
  *
15
- * Example: add it with `cow add example winback`; rename the purchase event,
19
+ * Blueprint: add it with `cow add blueprint winback`; rename the purchase event,
16
20
  * the flag trait, and the email to your domain's vocabulary.
17
21
  */
18
22
  export default defineJourney({
@@ -23,8 +27,15 @@ export default defineJourney({
23
27
  tags: ["demo", "retention"],
24
28
  run: async (_event, api) => {
25
29
  await api.traits.set("purchased_recently", true);
26
- while (await api.waitForEvent("purchase_completed", { timeout: "30d" })) {
27
- // Another purchase inside 30 days: loop to reset the silence timer.
30
+ const purchase = await api.waitForEvent("purchase_completed", {
31
+ timeout: "30d",
32
+ });
33
+ if (purchase) {
34
+ // Another purchase inside 30 days: restart to reset the silence timer
35
+ // with a fresh, empty journal. Looping here instead would journal one
36
+ // entry per purchase, so the best customer is the one whose execution
37
+ // eventually fails on the 1,000-call cap.
38
+ await api.restart({ event: purchase });
28
39
  }
29
40
  await api.traits.unset("purchased_recently");
30
41
  const me = await api.profile.get();
@@ -0,0 +1,14 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": { "email": "ada@example.com", "firstName": "Ada" }
5
+ },
6
+ "events": [{ "at": "10d", "event": "purchase_completed" }],
7
+ "expect": [
8
+ {
9
+ "activity": "setTrait",
10
+ "input": { "key": "purchased_recently", "value": true }
11
+ },
12
+ { "activity": "restart" }
13
+ ]
14
+ }
@@ -3,10 +3,7 @@
3
3
  "id": "usr_1",
4
4
  "traits": { "email": "ada@example.com", "firstName": "Ada" }
5
5
  },
6
- "events": [
7
- { "at": "10d", "event": "purchase_completed" },
8
- { "at": "25d", "event": "purchase_completed" }
9
- ],
6
+ "events": [],
10
7
  "expect": [
11
8
  {
12
9
  "activity": "setTrait",
@@ -15,7 +12,7 @@
15
12
  { "activity": "unsetTrait", "input": { "key": "purchased_recently" } },
16
13
  {
17
14
  "activity": "sendEmail",
18
- "input": { "template": "winback" }
15
+ "input": { "template": "winback", "props": { "firstName": "Ada" } }
19
16
  }
20
17
  ]
21
18
  }
@@ -1,4 +1,5 @@
1
- import { a as manifestOutputSchema, i as journeyStepOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
1
+ import { n as journeyStepOutputSchema, r as manifestOutputSchema, t as guestInputSchema } from "./journeys-v2-Djgs8U91.js";
2
+ import { CapabilityError } from "./journeys.js";
2
3
  import { z } from "zod";
3
4
 
4
5
  //#region src/guest/api.ts
@@ -1,5 +1,6 @@
1
- import { o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
2
- import { n as readManifest, t as asTemplate } from "./driver-poSdZIj8.js";
1
+ import { i as templateRenderOutputSchema, t as guestInputSchema } from "./journeys-v2-Djgs8U91.js";
2
+ import { CapabilityError } from "./journeys.js";
3
+ import { n as readManifest, t as asTemplate } from "./driver-B_pjmy5g.js";
3
4
  import { createElement } from "react";
4
5
  import { renderToStaticMarkup } from "react-dom/server.browser";
5
6
 
@@ -1,4 +1,4 @@
1
- import { a as JourneyStepOutput, o as ManifestOutput, s as TemplateRenderOutput } from "./index-EqBCZnpq.js";
1
+ import { c as TemplateRenderOutput, o as JourneyStepOutput, s as ManifestOutput } from "./index-DADBwPaG.js";
2
2
  import { n as Template } from "./emails-5GindgMQ.js";
3
3
  //#region src/guest/driver.d.ts
4
4
  /**
@@ -1,3 +1,3 @@
1
- import { n as readManifest, r as runGuest, t as asTemplate } from "./driver-poSdZIj8.js";
1
+ import { n as readManifest, r as runGuest, t as asTemplate } from "./driver-B_pjmy5g.js";
2
2
 
3
3
  export { asTemplate, readManifest, runGuest };
@@ -284,6 +284,8 @@ declare const manifestOutputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
284
284
  withinDays: z.ZodOptional<z.ZodNumber>;
285
285
  }, z.core.$strip>], "kind">>;
286
286
  }, z.core.$strict>;
287
+ }, z.core.$strict>, z.ZodObject<{
288
+ segment: z.ZodString;
287
289
  }, z.core.$strict>]>;
288
290
  purpose: z.ZodString;
289
291
  enrollment: z.ZodOptional<z.ZodObject<{
@@ -328,18 +330,19 @@ type Duration = z.infer<typeof durationSchema>;
328
330
  * row carries its `app_id`, and the runtime compares that with the event's.
329
331
  * An `appId` here would be a second way to say it, and the way to leak.
330
332
  *
331
- * A segment trigger carries the predicate list, not a name: the push
332
- * materializes one segment row per journey that inlines a definition, owned
333
- * by the journey and named after its key, so the segment exists because the
334
- * journey exists and there is no order to get wrong (ADR 0016). The
335
- * definition is data — validated here, carried on the version, never
336
- * compiled and never executed.
333
+ * A segment trigger says which audience in two ways (ADR 0016, ADR 0023).
334
+ * Inline, carrying the predicate list itself: the push materializes one
335
+ * segment row per journey that does, owned by the journey and named after
336
+ * its key. Or by name, naming a `segments/<key>.ts` file of the same push:
337
+ * the name resolves inside the tree at build time and the push writes that
338
+ * file's row, so there is still no order to get wrong and no row outside the
339
+ * repository a trigger can point at. Either way the definition is data —
340
+ * validated here, carried on the version, never compiled and never executed.
337
341
  *
338
- * Both members are strict, so a journey holding the retired `source` or
339
- * `appId` key, or the retired `{ segment: "name" }` form, fails to build
340
- * instead of silently triggering on every app or on nothing. There is
341
- * deliberately no pipe filter and no app filter: the journey's own app is
342
- * the only narrowing there is.
342
+ * Every member is strict, so a journey holding the retired `source` or
343
+ * `appId` key fails to build instead of silently triggering on every app or
344
+ * on nothing. There is deliberately no pipe filter and no app filter: the
345
+ * journey's own app is the only narrowing there is.
343
346
  */
344
347
  declare const triggerSchema: z.ZodUnion<readonly [z.ZodObject<{
345
348
  event: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
@@ -375,6 +378,8 @@ declare const triggerSchema: z.ZodUnion<readonly [z.ZodObject<{
375
378
  withinDays: z.ZodOptional<z.ZodNumber>;
376
379
  }, z.core.$strip>], "kind">>;
377
380
  }, z.core.$strict>;
381
+ }, z.core.$strict>, z.ZodObject<{
382
+ segment: z.ZodString;
378
383
  }, z.core.$strict>]>;
379
384
  type Trigger = z.infer<typeof triggerSchema>;
380
385
  /**
@@ -383,5 +388,51 @@ type Trigger = z.infer<typeof triggerSchema>;
383
388
  * everything downstream reads the parsed `Trigger`.
384
389
  */
385
390
  type TriggerInput = z.input<typeof triggerSchema>;
391
+ /**
392
+ * One `segments/<key>.ts` file (ADR 0023): an audience several journeys of
393
+ * one repository enroll from, written once instead of pasted into each
394
+ * trigger. It carries no bundle and no digest, because it compiles to
395
+ * nothing: a definition is a predicate list the evaluator folds, so there is
396
+ * no code for an execution to pin and therefore no version of it.
397
+ *
398
+ * Plain rather than strict, for the same reason the other two entries are: a
399
+ * stored manifest has to keep parsing after a field goes away (ADR 0011).
400
+ */
401
+ declare const manifestSegmentSchema: z.ZodObject<{
402
+ key: z.ZodString;
403
+ tags: z.ZodPipe<z.ZodDefault<z.ZodArray<z.ZodString>>, z.ZodTransform<string[], string[]>>;
404
+ description: z.ZodOptional<z.ZodString>;
405
+ definition: z.ZodObject<{
406
+ match: z.ZodDefault<z.ZodEnum<{
407
+ all: "all";
408
+ any: "any";
409
+ }>>;
410
+ predicates: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
411
+ kind: z.ZodLiteral<"trait">;
412
+ name: z.ZodString;
413
+ op: z.ZodEnum<{
414
+ contains: "contains";
415
+ eq: "eq";
416
+ exists: "exists";
417
+ gt: "gt";
418
+ gte: "gte";
419
+ lt: "lt";
420
+ lte: "lte";
421
+ neq: "neq";
422
+ notExists: "notExists";
423
+ }>;
424
+ value: z.ZodOptional<z.ZodUnknown>;
425
+ }, z.core.$strip>, z.ZodObject<{
426
+ kind: z.ZodLiteral<"event">;
427
+ name: z.ZodString;
428
+ op: z.ZodEnum<{
429
+ notPerformed: "notPerformed";
430
+ performed: "performed";
431
+ }>;
432
+ atLeast: z.ZodDefault<z.ZodNumber>;
433
+ withinDays: z.ZodOptional<z.ZodNumber>;
434
+ }, z.core.$strip>], "kind">>;
435
+ }, z.core.$strict>;
436
+ }, z.core.$strip>;
386
437
  //#endregion
387
- export { JourneyStepOutput as a, GuestEvent as i, Trigger as n, ManifestOutput as o, TriggerInput as r, TemplateRenderOutput as s, Duration as t };
438
+ export { GuestEvent as a, TemplateRenderOutput as c, manifestSegmentSchema as i, Trigger as n, JourneyStepOutput as o, TriggerInput as r, ManifestOutput as s, Duration as t };
@@ -81,6 +81,12 @@ const EXECUTION_LIMITS = {
81
81
  const PUSH_LIMITS = {
82
82
  journeys: 100,
83
83
  templates: 200,
84
+ /**
85
+ * Segment files (ADR 0023). A segment is a predicate list, not a bundle, so
86
+ * the ceiling is about how many audiences one program describes rather than
87
+ * about bytes; a journey's own inline definition costs nothing here.
88
+ */
89
+ segments: 100,
84
90
  bundleBytes: 2097152,
85
91
  sourceBytes: 5242880
86
92
  };
@@ -284,20 +290,25 @@ const patternSchema = z.union([onePatternSchema, z.array(onePatternSchema).min(1
284
290
  * row carries its `app_id`, and the runtime compares that with the event's.
285
291
  * An `appId` here would be a second way to say it, and the way to leak.
286
292
  *
287
- * A segment trigger carries the predicate list, not a name: the push
288
- * materializes one segment row per journey that inlines a definition, owned
289
- * by the journey and named after its key, so the segment exists because the
290
- * journey exists and there is no order to get wrong (ADR 0016). The
291
- * definition is data — validated here, carried on the version, never
292
- * compiled and never executed.
293
+ * A segment trigger says which audience in two ways (ADR 0016, ADR 0023).
294
+ * Inline, carrying the predicate list itself: the push materializes one
295
+ * segment row per journey that does, owned by the journey and named after
296
+ * its key. Or by name, naming a `segments/<key>.ts` file of the same push:
297
+ * the name resolves inside the tree at build time and the push writes that
298
+ * file's row, so there is still no order to get wrong and no row outside the
299
+ * repository a trigger can point at. Either way the definition is data —
300
+ * validated here, carried on the version, never compiled and never executed.
293
301
  *
294
- * Both members are strict, so a journey holding the retired `source` or
295
- * `appId` key, or the retired `{ segment: "name" }` form, fails to build
296
- * instead of silently triggering on every app or on nothing. There is
297
- * deliberately no pipe filter and no app filter: the journey's own app is
298
- * the only narrowing there is.
299
- */
300
- const triggerSchema = z.union([z.strictObject({ event: patternSchema }), z.strictObject({ segment: segmentDefinitionSchema })]);
302
+ * Every member is strict, so a journey holding the retired `source` or
303
+ * `appId` key fails to build instead of silently triggering on every app or
304
+ * on nothing. There is deliberately no pipe filter and no app filter: the
305
+ * journey's own app is the only narrowing there is.
306
+ */
307
+ const triggerSchema = z.union([
308
+ z.strictObject({ event: patternSchema }),
309
+ z.strictObject({ segment: segmentDefinitionSchema }),
310
+ z.strictObject({ segment: journeyKeySchema })
311
+ ]);
301
312
  /**
302
313
  * The address half of a from-header: a local part, an `@`, and a dotted
303
314
  * domain. Deliberately narrower than RFC 5322 (no quoted local parts, no
@@ -452,6 +463,40 @@ const manifestTemplateSchema = z.object({
452
463
  propsSchema: z.record(z.string(), z.unknown()),
453
464
  bundle: digestSchema
454
465
  });
466
+ /**
467
+ * One `segments/<key>.ts` file (ADR 0023): an audience several journeys of
468
+ * one repository enroll from, written once instead of pasted into each
469
+ * trigger. It carries no bundle and no digest, because it compiles to
470
+ * nothing: a definition is a predicate list the evaluator folds, so there is
471
+ * no code for an execution to pin and therefore no version of it.
472
+ *
473
+ * Plain rather than strict, for the same reason the other two entries are: a
474
+ * stored manifest has to keep parsing after a field goes away (ADR 0011).
475
+ */
476
+ const manifestSegmentSchema = z.object({
477
+ key: journeyKeySchema,
478
+ /** The author's labels, the way a journey and a template carry them. */
479
+ tags: tagsSchema,
480
+ /** The author's own sentence about who is in it; absent when unwritten. */
481
+ description: z.string().trim().max(500, `description must be at most ${500} characters`).optional(),
482
+ definition: segmentDefinitionSchema
483
+ });
484
+ /**
485
+ * One event pattern the tree reacts to, read off the tree itself: a trigger,
486
+ * a `waitForEvent`, or an event predicate in a segment. Nobody declares it —
487
+ * a second source of truth would disagree with the code the first time an
488
+ * author edited a trigger — so what a build could not read literally is
489
+ * simply absent, which makes the list incomplete and never wrong.
490
+ *
491
+ * The pattern is stored as written: `order.*` is one entry, not an
492
+ * expansion, because it is what the author asked for and what any reader
493
+ * has to match against. `usedBy` names the journeys and segments that named
494
+ * it, sorted, so a reader can say who is waiting.
495
+ */
496
+ const manifestEventSchema = z.object({
497
+ pattern: onePatternSchema,
498
+ usedBy: z.array(z.string())
499
+ });
455
500
  function uniqueKeys(items, ctx, path) {
456
501
  const seen = /* @__PURE__ */ new Set();
457
502
  for (const [index, item] of items.entries()) {
@@ -487,6 +532,19 @@ const manifestSchema = z.object({
487
532
  journeys: z.array(manifestJourneySchema).max(PUSH_LIMITS.journeys),
488
533
  templates: z.array(manifestTemplateSchema).max(PUSH_LIMITS.templates),
489
534
  /**
535
+ * The `segments/` files this project pushes (ADR 0023). Defaulted rather
536
+ * than required: every manifest stored before segments were files carries
537
+ * none, and a project with no shared audience still writes the field.
538
+ */
539
+ segments: z.array(manifestSegmentSchema).max(PUSH_LIMITS.segments).default([]),
540
+ /**
541
+ * The event patterns this tree reacts to, collected from the journeys
542
+ * and segments above rather than declared. Defaulted for the same reason
543
+ * `segments` is: every manifest stored before the collection existed
544
+ * carries none.
545
+ */
546
+ events: z.array(manifestEventSchema).default([]),
547
+ /**
490
548
  * The consent purposes this project declares, copied from `cow.json`.
491
549
  * Optional rather than defaulted: a push made before purposes existed
492
550
  * carries none, and its stored manifest still parses.
@@ -497,7 +555,22 @@ const manifestSchema = z.object({
497
555
  }).superRefine((manifest, ctx) => {
498
556
  uniqueKeys(manifest.journeys, ctx, "journeys");
499
557
  uniqueKeys(manifest.templates, ctx, "templates");
558
+ uniqueKeys(manifest.segments, ctx, "segments");
500
559
  uniqueKeys(manifest.purposes ?? [], ctx, "purposes");
560
+ const segmentKeys = new Set(manifest.segments.map((segment) => segment.key));
561
+ for (const [index, journey] of manifest.journeys.entries()) {
562
+ const named = "segment" in journey.trigger && typeof journey.trigger.segment === "string" ? journey.trigger.segment : null;
563
+ if (named !== null && !segmentKeys.has(named)) ctx.addIssue({
564
+ code: "custom",
565
+ message: `journey "${journey.key}" enrolls from the segment "${named}", which this push does not carry. Push the segments/${named}.ts file with it, or write the predicate list in the trigger.`,
566
+ path: [
567
+ "journeys",
568
+ index,
569
+ "trigger",
570
+ "segment"
571
+ ]
572
+ });
573
+ }
501
574
  for (const [index, journey] of manifest.journeys.entries()) if (journey.enrollment && journey.purpose === "transactional") ctx.addIssue({
502
575
  code: "custom",
503
576
  message: `journey "${journey.key}": a transactional journey enrolls on every trigger, so it takes no enrollment cooldown`,
@@ -876,61 +949,4 @@ const journeyScenarioSchema = z.object({
876
949
  });
877
950
 
878
951
  //#endregion
879
- //#region src/guest/journeys.ts
880
- /**
881
- * A capability that failed host-side (an unknown webhook, invalid
882
- * props, a profile that is not there). The failure is journaled, so a
883
- * journey that catches it takes the same branch on every replay.
884
- */
885
- var CapabilityError = class extends Error {
886
- code;
887
- constructor(code, message) {
888
- super(message);
889
- this.name = "CapabilityError";
890
- this.code = code;
891
- }
892
- };
893
- /**
894
- * The config half of a journey, validated with the same schema the release
895
- * manifest is validated with, so a bad trigger, a malformed purpose key, or
896
- * a `from` that is not an address fails at build time rather than at the
897
- * first execution. Whether the org declares that purpose, and whether it may
898
- * send from that domain, are questions only the push can answer.
899
- */
900
- const journeyConfigSchema = manifestJourneySchema.pick({
901
- trigger: true,
902
- purpose: true,
903
- enrollment: true,
904
- description: true,
905
- from: true,
906
- tags: true
907
- });
908
- /**
909
- * Author a journey. Throws at definition time on an invalid config.
910
- *
911
- * There is no environment gate here: an organization has one data space, and
912
- * a journey runs wherever it is on and nowhere else (ADR 0011, ADR 0013).
913
- *
914
- * There is no app gate either. A journey belongs to the one app `cow.json`
915
- * names, so it only ever sees that app's events, profiles and segments
916
- * (ADR 0022); a trigger naming an app is refused rather than honoured.
917
- */
918
- function defineJourney(input) {
919
- const config = journeyConfigSchema.parse({
920
- trigger: input.trigger,
921
- purpose: input.purpose,
922
- enrollment: input.enrollment,
923
- description: input.description,
924
- from: input.from,
925
- tags: input.tags
926
- });
927
- if (config.from === void 0) throw new Error("defineJourney needs a \"from\": the address this journey sends as, on a domain your organization verified. Add and verify it on the Domains page (Settings, Domains). For example from: \"Billing <billing@your-domain.com>\".");
928
- return {
929
- ...config,
930
- from: config.from,
931
- run: input.run
932
- };
933
- }
934
-
935
- //#endregion
936
- export { manifestOutputSchema as a, journeyStepOutputSchema as i, defineJourney as n, templateRenderOutputSchema as o, guestInputSchema as r, CapabilityError as t };
952
+ export { manifestJourneySchema as a, templateRenderOutputSchema as i, journeyStepOutputSchema as n, manifestSegmentSchema as o, manifestOutputSchema as r, guestInputSchema as t };
@@ -1,4 +1,4 @@
1
- import { i as GuestEvent, n as Trigger, r as TriggerInput, t as Duration } from "./index-EqBCZnpq.js";
1
+ import { a as GuestEvent, n as Trigger, r as TriggerInput, t as Duration } from "./index-DADBwPaG.js";
2
2
  //#region src/guest/journeys.d.ts
3
3
  /** The event a journey runs for, or the one a `waitForEvent` resolved with. */
4
4
  type Event = GuestEvent;