@ak--47/dungeon-master 1.2.3 → 1.3.1

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 (89) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +51 -13
  3. package/dungeons/technical/ad-spend.js +2 -2
  4. package/dungeons/technical/anonymous-users.js +2 -2
  5. package/dungeons/technical/array-of-object-lookup.js +2 -4
  6. package/dungeons/technical/experiments.js +2 -2
  7. package/dungeons/technical/foobar.js +2 -2
  8. package/dungeons/technical/group-analytics.js +2 -2
  9. package/dungeons/technical/mirror-strategies.js +2 -2
  10. package/dungeons/technical/nested-objects.js +2 -2
  11. package/dungeons/technical/retention-cadence.js +2 -3
  12. package/dungeons/technical/sanity.js +2 -2
  13. package/dungeons/technical/scale-test.js +2 -2
  14. package/dungeons/technical/scd.js +2 -2
  15. package/dungeons/technical/simple.js +5 -6
  16. package/dungeons/technical/simplest-schema.json +5 -0
  17. package/dungeons/technical/simplest.js +2 -2
  18. package/dungeons/technical/text-generation.js +3 -3
  19. package/dungeons/vertical/ai-platform.js +858 -0
  20. package/dungeons/vertical/community.js +84 -40
  21. package/dungeons/vertical/crypto.js +830 -0
  22. package/dungeons/vertical/dating.js +744 -0
  23. package/dungeons/vertical/devtools.js +175 -69
  24. package/dungeons/vertical/ecommerce.js +242 -94
  25. package/dungeons/vertical/education.js +330 -313
  26. package/dungeons/vertical/fintech.js +442 -313
  27. package/dungeons/vertical/fitness.js +143 -61
  28. package/dungeons/vertical/food-delivery.js +327 -353
  29. package/dungeons/vertical/gaming.js +912 -382
  30. package/dungeons/vertical/healthcare.js +142 -63
  31. package/dungeons/vertical/insurance-application.js +170 -76
  32. package/dungeons/vertical/logistics.js +115 -20
  33. package/dungeons/vertical/marketplace.js +152 -58
  34. package/dungeons/vertical/media.js +248 -384
  35. package/dungeons/vertical/real-estate.js +781 -0
  36. package/dungeons/vertical/sass.js +255 -266
  37. package/dungeons/vertical/social.js +264 -206
  38. package/dungeons/vertical/travel.js +117 -41
  39. package/index.js +17 -17
  40. package/lib/core/config-validator.js +159 -31
  41. package/lib/core/context.js +10 -24
  42. package/lib/core/storage.js +6 -1
  43. package/lib/generators/events.js +11 -14
  44. package/lib/generators/funnels.js +12 -4
  45. package/lib/generators/mirror.js +3 -2
  46. package/lib/generators/product-names.js +1 -1
  47. package/lib/generators/scd.js +2 -1
  48. package/lib/generators/text.js +1 -1
  49. package/lib/orchestrators/user-loop.js +81 -47
  50. package/lib/templates/macro-presets.js +111 -0
  51. package/lib/templates/soup-presets.js +19 -36
  52. package/lib/utils/utils.js +71 -39
  53. package/package.json +8 -2
  54. package/scripts/smoke-test-all.mjs +162 -0
  55. package/scripts/verify-runner.mjs +72 -24
  56. package/types.d.ts +251 -51
  57. package/dungeons/technical/ad-spend-schema.json +0 -128
  58. package/dungeons/technical/anonymous-users-schema.json +0 -92
  59. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  60. package/dungeons/technical/experiments-schema.json +0 -203
  61. package/dungeons/technical/foobar-schema.json +0 -362
  62. package/dungeons/technical/group-analytics-schema.json +0 -241
  63. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  64. package/dungeons/technical/nested-objects-schema.json +0 -145
  65. package/dungeons/technical/retention-cadence-schema.json +0 -37
  66. package/dungeons/technical/sanity-schema.json +0 -185
  67. package/dungeons/technical/scale-test-schema.json +0 -70
  68. package/dungeons/technical/scd-schema.json +0 -467
  69. package/dungeons/technical/simple-schema.json +0 -362
  70. package/dungeons/technical/text-generation-schema.json +0 -1062
  71. package/dungeons/user/.gitkeep +0 -0
  72. package/dungeons/vertical/community-schema.json +0 -579
  73. package/dungeons/vertical/devtools-schema.json +0 -601
  74. package/dungeons/vertical/ecommerce-schema.json +0 -604
  75. package/dungeons/vertical/education-schema.json +0 -5686
  76. package/dungeons/vertical/fintech-schema.json +0 -630
  77. package/dungeons/vertical/fitness-schema.json +0 -530
  78. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  79. package/dungeons/vertical/gaming-schema.json +0 -438
  80. package/dungeons/vertical/healthcare-schema.json +0 -549
  81. package/dungeons/vertical/insurance-application-schema.json +0 -485
  82. package/dungeons/vertical/logistics-schema.json +0 -574
  83. package/dungeons/vertical/marketplace-schema.json +0 -533
  84. package/dungeons/vertical/media-schema.json +0 -4749
  85. package/dungeons/vertical/rpg-schema.json +0 -2491
  86. package/dungeons/vertical/rpg.js +0 -976
  87. package/dungeons/vertical/sass-schema.json +0 -3128
  88. package/dungeons/vertical/social-schema.json +0 -620
  89. package/dungeons/vertical/travel-schema.json +0 -580
@@ -2,7 +2,7 @@
2
2
  const SEED = "harness-food";
3
3
  const num_days = 100;
4
4
  const num_users = 5_000;
5
- const avg_events_per_user = 120;
5
+ const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
8
8
  // ── env overrides ──
@@ -16,7 +16,6 @@ import * as v from "ak-tools";
16
16
 
17
17
  dayjs.extend(utc);
18
18
  const chance = u.initChance(SEED);
19
-
20
19
  /** @typedef {import("../../types").Dungeon} Config */
21
20
 
22
21
  /*
@@ -50,109 +49,208 @@ const chance = u.initChance(SEED);
50
49
 
51
50
  /*
52
51
  * ═══════════════════════════════════════════════════════════════════════════════
53
- * ANALYTICS HOOKS (8 architected patterns)
52
+ * ANALYTICS HOOKS (9 hooks)
53
+ *
54
+ * Adds 9. ORDER LIFECYCLE TIME-TO-CONVERT: QuickBite+ 0.74x faster, Free 1.3x
55
+ * slower (funnel-post). Discover via order funnel median TTC by subscription_tier.
56
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
57
+ * Cross-event MIN→MIN SQL queries on raw events do NOT show this.
54
58
  * ═══════════════════════════════════════════════════════════════════════════════
55
59
  *
56
- * 1. LUNCH RUSH CONVERSION (funnel-pre)
57
- * Funnel conversion boosted during meal hours: lunch 1.4x, dinner 1.2x.
58
- * Mixpanel:
59
- * Funnels → "checkout started" → "order placed" → "order delivered"
60
- * Breakdown: "lunch_rush" expect ~84% vs ~60% baseline
61
- * Same funnel, breakdown: "dinner_rush" → expect ~72% vs ~60%
62
- *
63
- * 2. COUPON INJECTION (funnel-post)
64
- * Free-tier users get coupon_applied spliced into funnels 30% of the time.
65
- * Mixpanel:
66
- * • Insights → "coupon applied" → breakdown "coupon_injected"
67
- * ~30% of Free-tier coupons are injected
68
- * • Insights → "coupon applied" → breakdown "subscription_tier"
69
- * Free users have more coupon events
70
- *
71
- * 3. LATE NIGHT MUNCHIES (event)
72
- * 10PM–2AM: 70% American cuisine, 1.3x item prices, late_night_order=true.
73
- * Mixpanel:
74
- * Insights "restaurant viewed" breakdown "cuisine_type"
75
- * filter: late_night_order=true → ~70% American vs ~12% daytime
76
- * Insights "item added to cart" avg "item_price"
77
- * breakdown: "late_night_order" → 1.3x higher at night
60
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable only via
61
+ * behavioral cohorts or raw-prop breakdowns (HOD, day, segment).
62
+ *
63
+ * ───────────────────────────────────────────────────────────────────────────────
64
+ * 1. LUNCH/DINNER RUSH (everything)
65
+ * ───────────────────────────────────────────────────────────────────────────────
66
+ *
67
+ * PATTERN: 30% of "order delivered" events that fall outside meal-hour
68
+ * windows (11-13 UTC and 17-20 UTC) are dropped, depressing non-meal-time
69
+ * conversion. Mutation: event drop. Discover via order delivered HOD chart.
70
+ *
71
+ * HOW TO FIND IT IN MIXPANEL:
72
+ *
73
+ * Report 1: Order Delivered Volume by Hour of Day
74
+ * - Report type: Insights
75
+ * - Event: "order delivered"
76
+ * - Measure: Total
77
+ * - Breakdown: Hour of day
78
+ * - Expected: 11-13 and 17-20 dominate; off-hours visibly suppressed
79
+ *
80
+ * REAL-WORLD ANALOGUE: Meal-hour orders convert at higher rates.
81
+ *
82
+ * ───────────────────────────────────────────────────────────────────────────────
83
+ * 2. COUPON INJECTION (everything)
84
+ * ───────────────────────────────────────────────────────────────────────────────
85
+ *
86
+ * PATTERN: Free-tier users get extra "coupon applied" events cloned into the
87
+ * stream near checkout (30% chance per checkout). Cloned with unique offset
88
+ * timestamps. No flag.
78
89
  *
90
+ * HOW TO FIND IT IN MIXPANEL:
91
+ *
92
+ * Report 1: Coupons per User by Tier
93
+ * - Report type: Insights
94
+ * - Event: "coupon applied"
95
+ * - Measure: Total per user
96
+ * - Breakdown: "subscription_tier"
97
+ * - Expected: Free ~ 1.3x QuickBite+
98
+ *
99
+ * REAL-WORLD ANALOGUE: Free-tier users are the target of coupon promos.
100
+ *
101
+ * ───────────────────────────────────────────────────────────────────────────────
102
+ * 3. LATE NIGHT MUNCHIES (everything)
103
+ * ───────────────────────────────────────────────────────────────────────────────
104
+ *
105
+ * PATTERN: 10PM-2AM UTC: 70% of "restaurant viewed" / "item added to cart"
106
+ * events get cuisine_type flipped to American, item_price bumped 1.3x.
107
+ * Mutates existing props. No flag — discover via HOD breakdown.
108
+ *
109
+ * HOW TO FIND IT IN MIXPANEL:
110
+ *
111
+ * Report 1: Cuisine Distribution by Hour of Day
112
+ * - Report type: Insights
113
+ * - Event: "restaurant viewed"
114
+ * - Measure: Total
115
+ * - Breakdown: "cuisine_type"
116
+ * - Filter: hour 22-02
117
+ * - Expected: American share spikes
118
+ *
119
+ * Report 2: Avg item_price by Hour of Day
120
+ * - Report type: Insights
121
+ * - Event: "item added to cart"
122
+ * - Measure: Average of "item_price"
123
+ * - Breakdown: Hour of day
124
+ * - Expected: 22-02 hours show ~ 1.3x baseline price
125
+ *
126
+ * REAL-WORLD ANALOGUE: Late-night ordering skews to fast food and impulse buys.
127
+ *
128
+ * ───────────────────────────────────────────────────────────────────────────────
79
129
  * 4. RAINY WEEK SURGE (event + everything)
80
- * Days 20–27: delivery fees 2x, surge_pricing=true, 40% order duplication.
81
- * Mixpanel:
82
- * Insights (line) "order placed" daily visible spike days 20–27
83
- * Insights "order placed" avg "delivery_fee"
84
- * breakdown: "surge_pricing" 2x higher
130
+ * ───────────────────────────────────────────────────────────────────────────────
131
+ *
132
+ * PATTERN: Days 20-27, "order placed" delivery_fee doubled and 40% of those
133
+ * events get a duplicate cloned event with unique offset. No flag — discover
134
+ * via line chart by day on order placed volume + delivery_fee average.
135
+ *
136
+ * HOW TO FIND IT IN MIXPANEL:
137
+ *
138
+ * Report 1: Order Volume Over Time
139
+ * - Report type: Insights
140
+ * - Event: "order placed"
141
+ * - Measure: Total
142
+ * - Line chart by day
143
+ * - Expected: visible spike days 20-27
85
144
  *
145
+ * Report 2: Avg delivery_fee Over Time
146
+ * - Report type: Insights
147
+ * - Event: "order placed"
148
+ * - Measure: Average of "delivery_fee"
149
+ * - Line chart by day
150
+ * - Expected: ~ 2x days 20-27
151
+ *
152
+ * REAL-WORLD ANALOGUE: Weather-driven demand surge with surge pricing.
153
+ *
154
+ * ───────────────────────────────────────────────────────────────────────────────
86
155
  * 5. REFERRAL POWER USERS (everything)
87
- * Referred users: 2x reorders, food_rating 4–5 stars, referral_user=true.
88
- * Mixpanel:
89
- * Insights "reorder initiated" per user
90
- * breakdown: "referral_user" ~2x more reorders
91
- * Insights "order rated" avg "food_rating"
92
- * breakdown: "referral_user" → 4–5 vs ~3.5 baseline
156
+ * ───────────────────────────────────────────────────────────────────────────────
157
+ *
158
+ * PATTERN: Users with referral_code=true on account-created event get
159
+ * food_rating boosted to 4-5 and 50% of their reorder events cloned (unique
160
+ * offset). Mutates existing prop, no flag.
161
+ *
162
+ * HOW TO FIND IT IN MIXPANEL:
93
163
  *
164
+ * Report 1: Reorders per User by Referral Cohort
165
+ * - Report type: Insights (with cohort)
166
+ * - Cohort A: users with account-created.referral_code=true
167
+ * - Cohort B: rest
168
+ * - Event: "reorder initiated"
169
+ * - Measure: Total per user
170
+ * - Expected: A ~ 1.5-2x B
171
+ *
172
+ * REAL-WORLD ANALOGUE: Referred users tend to be more loyal.
173
+ *
174
+ * ───────────────────────────────────────────────────────────────────────────────
94
175
  * 6. TRIAL CONVERSION (everything)
95
- * Trial subs with 3+ orders in 14 days retained; others lose 60% of events.
96
- * Mixpanel:
97
- * Insights any event per user
98
- * breakdown: "trial_retained" sustained vs ~60% drop
99
- * • Retention → "subscription started" (trial=true) → "order placed"
100
- * sharp drop after day 14 for non-retained
101
- *
102
- * 7. SUPPORT TICKET CHURN (user)
103
- * 15% of users flagged is_high_risk=true with churn_risk_score 70–100.
104
- * Mixpanel:
105
- * Insights any event unique users
106
- * breakdown: user "is_high_risk" ~15% high-risk
107
- * • Insights → breakdown: user "churn_risk_score"
108
- * bimodal: 85% at 0–40, 15% at 70–100
109
- *
110
- * 8. FIRST ORDER BONUS (funnel-pre)
111
- * New users get 1.4x conversion boost on checkout→order funnel.
112
- * Mixpanel:
113
- * • Funnels → "checkout started" → "order placed" → "order delivered"
114
- * breakdown: "first_order_bonus" 1.4x higher conversion
115
- * Insights → "order placed" breakdown "first_order_bonus"
116
- * ~50% tagged (hash-based user split)
176
+ * ───────────────────────────────────────────────────────────────────────────────
177
+ *
178
+ * PATTERN: Users with subscription-started.trial=true who place <3 orders in
179
+ * first 14 days have 60% of post-day-14 events dropped. No flag.
180
+ *
181
+ * HOW TO FIND IT IN MIXPANEL:
182
+ *
183
+ * Report 1: Retention by Trial Order Count
184
+ * - Report type: Retention
185
+ * - Cohort A: trial users with >= 3 orders in first 14 days
186
+ * - Cohort B: trial users with < 3
187
+ * - Expected: B sharp activity drop after day 14
188
+ *
189
+ * REAL-WORLD ANALOGUE: Trial users who fail to activate churn fast.
190
+ *
191
+ * ───────────────────────────────────────────────────────────────────────────────
192
+ * 7. FIRST ORDER BONUS (everything)
193
+ * ───────────────────────────────────────────────────────────────────────────────
194
+ *
195
+ * PATTERN: ~50% of users (deterministic by user_id hash) have 30% of their
196
+ * "order delivered" events dropped — simulating that returning users convert
197
+ * worse than new users. No flag — analyst sees segment-level conversion gap
198
+ * via cohort builder by hash bucket.
199
+ *
200
+ * HOW TO FIND IT IN MIXPANEL:
201
+ *
202
+ * Report 1: Order Delivered Conversion by First-Letter-Hash
203
+ * - Report type: Funnels
204
+ * - Steps: "checkout started" -> "order placed" -> "order delivered"
205
+ * - Breakdown: derived hash bucket on distinct_id
206
+ * - Expected: half of users show ~ 30% lower conversion on final step
207
+ *
208
+ * REAL-WORLD ANALOGUE: First-order promos lift new-user conversion.
117
209
  *
118
210
  * ───────────────────────────────────────────────────────────────────────────────
119
- * ADVANCED ANALYSIS IDEAS
211
+ * 8. ORDER-COUNT MAGIC NUMBER (everything)
120
212
  * ───────────────────────────────────────────────────────────────────────────────
121
213
  *
122
- * Cross-hook patterns:
123
- * The Perfect Customer: referral (5) + trial retained (6) + lunch rush (1)
124
- * + low churn risk (7) exceptional LTV and retention
125
- * • Rainy Night Double Whammy: late-night (3) + rainy week (4)
126
- * compounded surge pricing?
127
- * • Coupon-Driven Trial: injected coupons (2) → trial starts (6)?
128
- * Referral + First Order: hooks 5 + 8 → highest conversion rates
129
- * Support and Churn: high-risk (7) + support tickets during rainy week (4)
130
- *
131
- * Cohort analysis:
132
- * Signup week (rainy week cohort behaves differently)
133
- * Referral vs organic lifecycle
134
- * Free vs QuickBite+ across all metrics
135
- * • City-level cuisine and ordering patterns
136
- *
137
- * Funnel analysis:
138
- * Onboarding: account created first restaurant view, by signup method
139
- * Order: checkout delivery, by platform / tier / time of day
140
- * Discovery: search type → conversion to ordering
214
+ * PATTERN: Users in the 4-8 order-placed sweet spot get +40% on order_total.
215
+ * Users with 9+ orders are over-engaged; 35% of their order-placed events
216
+ * drop. No flag discover by binning users on order count.
217
+ *
218
+ * HOW TO FIND IT IN MIXPANEL:
219
+ *
220
+ * Report 1: Avg Order Total by Order-Count Bucket
221
+ * - Report type: Insights (with cohort)
222
+ * - Cohort A: users with 4-8 "order placed"
223
+ * - Cohort B: users with 0-3
224
+ * - Event: "order placed"
225
+ * - Measure: Average of "order_total"
226
+ * - Expected: A ~ 1.4x B
227
+ *
228
+ * Report 2: Orders per User on Heavy Orderers
229
+ * - Report type: Insights (with cohort)
230
+ * - Cohort C: users with >= 9 "order placed"
231
+ * - Cohort A: users with 4-8
232
+ * - Event: "order placed"
233
+ * - Measure: Total per user
234
+ * - Expected: C ~ 35% fewer orders per user vs A
235
+ *
236
+ * REAL-WORLD ANALOGUE: Engaged orderers lift basket size; over-orderers
237
+ * hit fatigue and slow down.
141
238
  *
142
239
  * ───────────────────────────────────────────────────────────────────────────────
143
240
  * EXPECTED METRICS SUMMARY
144
241
  * ───────────────────────────────────────────────────────────────────────────────
145
242
  *
146
243
  * Hook | Metric | Baseline | Hook Effect | Ratio
147
- * ──────────────────────|──────────────────────|──────────|─────────────|──────
148
- * Lunch Rush | Funnel conversion | 60% | 84% | 1.4x
149
- * Coupon Injection | Free user coupons | 0% | 30% | N/A
150
- * Late Night Munchies | American cuisine % | ~15% | 70% | ~4.7x
151
- * Rainy Week Surge | Order volume | 100% | 140% | 1.4x
152
- * Referral Power Users | Reorder frequency | 1x | 2x | 2.0x
153
- * Trial Conversion | Post-trial retention | 100% | 40% | 0.4x
154
- * Support Ticket Churn | High-risk users | 0% | 15% | N/A
155
- * First Order Bonus | New user conversion | 1x | 1.4x | 1.4x
244
+ * ──────────────────────|──────────────────────|----------|-------------|------
245
+ * Lunch/Dinner Rush | off-hour delivered | 1x | 0.7x | -30%
246
+ * Coupon Injection | Free user coupons/u | 1x | ~ 1.3x | 1.3x
247
+ * Late Night Munchies | American share 22-02 | ~ 15% | ~ 60% | 4x
248
+ * Rainy Week Surge | order vol days 20-27 | 1x | ~ 1.4x | 1.4x
249
+ * Referral Power Users | Reorders/user | 1x | ~ 1.5-2x | ~ 1.7x
250
+ * Trial Conversion | post-day-14 activity | 1x | ~ 0.4x | -60%
251
+ * First Order Bonus | returning conversion | 1x | 0.7x | -30%
252
+ * Order-Count Magic Num | sweet order_total | 1x | 1.4x | 1.4x
253
+ * Order-Count Magic Num | over orders/user | 1x | 0.65x | -35%
156
254
  */
157
255
 
158
256
  // Generate consistent IDs for lookup tables and event properties
@@ -165,8 +263,10 @@ const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
165
263
  const config = {
166
264
  token,
167
265
  seed: SEED,
168
- numDays: num_days,
169
- numEvents: num_users * avg_events_per_user,
266
+ datasetStart: "2026-01-01T00:00:00Z",
267
+ datasetEnd: "2026-04-28T23:59:59Z",
268
+ // numDays: num_days,
269
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
170
270
  numUsers: num_users,
171
271
  hasAnonIds: false,
172
272
  hasSessionIds: true,
@@ -181,7 +281,6 @@ const config = {
181
281
  hasCampaigns: false,
182
282
  isAnonymous: false,
183
283
  hasAdSpend: false,
184
- percentUsersBornInDataset: 50,
185
284
  hasAvatar: true,
186
285
  concurrency: 1,
187
286
  writeToDisk: false,
@@ -269,7 +368,6 @@ const config = {
269
368
  properties: {
270
369
  "signup_method": ["email", "google", "apple", "facebook"],
271
370
  "referral_code": [false, false, true],
272
- "trial_retained": [false],
273
371
  }
274
372
  },
275
373
  {
@@ -288,7 +386,6 @@ const config = {
288
386
  ],
289
387
  "sort_by": ["recommended", "distance", "rating", "price"],
290
388
  "filter_applied": [false, false, false, true, true],
291
- "trial_retained": [false],
292
389
  }
293
390
  },
294
391
  {
@@ -309,8 +406,6 @@ const config = {
309
406
  "avg_rating": u.weighNumRange(1, 5, 0.8, 30),
310
407
  "delivery_time_est_mins": u.weighNumRange(15, 90, 1.2, 40),
311
408
  "price_tier": ["$", "$$", "$$$", "$$$$"],
312
- "late_night_order": [false],
313
- "trial_retained": [false],
314
409
  }
315
410
  },
316
411
  {
@@ -321,8 +416,6 @@ const config = {
321
416
  "item_category": ["entree", "appetizer", "drink", "dessert", "side"],
322
417
  "item_price": u.weighNumRange(3, 65, 1.0, 40),
323
418
  "customization_count": u.weighNumRange(0, 5, 1.5, 20),
324
- "late_night_order": [false],
325
- "trial_retained": [false],
326
419
  }
327
420
  },
328
421
  {
@@ -331,7 +424,6 @@ const config = {
331
424
  properties: {
332
425
  "item_id": itemIds,
333
426
  "removal_reason": ["changed_mind", "too_expensive", "substitution"],
334
- "trial_retained": [false],
335
427
  }
336
428
  },
337
429
  {
@@ -341,8 +433,6 @@ const config = {
341
433
  "coupon_code": couponCodes,
342
434
  "discount_type": ["percent", "flat", "free_delivery"],
343
435
  "discount_value": u.weighNumRange(5, 50, 1.2, 20),
344
- "coupon_injected": [false],
345
- "trial_retained": [false],
346
436
  }
347
437
  },
348
438
  {
@@ -352,7 +442,6 @@ const config = {
352
442
  "cart_total": u.weighNumRange(8, 150, 0.8, 40),
353
443
  "items_count": u.weighNumRange(1, 8, 1.2, 20),
354
444
  "delivery_address_saved": [false, false, false, true, true, true, true, true, true, true],
355
- "trial_retained": [false],
356
445
  }
357
446
  },
358
447
  {
@@ -364,9 +453,6 @@ const config = {
364
453
  "order_total": u.weighNumRange(10, 200, 0.8, 40),
365
454
  "tip_amount": u.weighNumRange(0, 30, 1.5, 20),
366
455
  "delivery_fee": u.weighNumRange(0, 12, 1.0, 20),
367
- "surge_pricing": [false],
368
- "rainy_week": [false],
369
- "trial_retained": [false],
370
456
  }
371
457
  },
372
458
  {
@@ -376,7 +462,6 @@ const config = {
376
462
  "order_id": orderIds,
377
463
  "order_status": ["confirmed", "preparing", "picked_up", "en_route", "delivered"],
378
464
  "eta_mins": u.weighNumRange(5, 60, 1.0, 30),
379
- "trial_retained": [false],
380
465
  }
381
466
  },
382
467
  {
@@ -386,10 +471,6 @@ const config = {
386
471
  "order_id": orderIds,
387
472
  "actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
388
473
  "on_time": [false, false, false, true, true, true, true, true, true, true],
389
- "lunch_rush": [false],
390
- "dinner_rush": [false],
391
- "first_order_bonus": [false],
392
- "trial_retained": [false],
393
474
  }
394
475
  },
395
476
  {
@@ -400,8 +481,6 @@ const config = {
400
481
  "food_rating": u.weighNumRange(1, 5, 0.8, 30),
401
482
  "delivery_rating": u.weighNumRange(1, 5, 0.8, 30),
402
483
  "would_reorder": [false, true, true],
403
- "referral_user": [false],
404
- "trial_retained": [false],
405
484
  }
406
485
  },
407
486
  {
@@ -415,7 +494,6 @@ const config = {
415
494
  ]),
416
495
  "results_count": u.weighNumRange(0, 50, 0.8, 30),
417
496
  "search_type": ["restaurant", "cuisine", "dish"],
418
- "trial_retained": [false],
419
497
  }
420
498
  },
421
499
  {
@@ -425,7 +503,6 @@ const config = {
425
503
  "promo_id": () => `promo_${v.uid(5)}`,
426
504
  "promo_type": ["banner", "push", "in_feed"],
427
505
  "promo_value": ["10%", "15%", "20%", "25%", "30%", "40%", "50%"],
428
- "trial_retained": [false],
429
506
  }
430
507
  },
431
508
  {
@@ -435,7 +512,6 @@ const config = {
435
512
  "plan": ["quickbite_plus_monthly", "quickbite_plus_monthly", "quickbite_plus_annual"],
436
513
  "price": [9.99, 9.99, 79.99],
437
514
  "trial": [true, false],
438
- "trial_retained": [false],
439
515
  }
440
516
  },
441
517
  {
@@ -444,7 +520,6 @@ const config = {
444
520
  properties: {
445
521
  "reason": ["too_expensive", "not_ordering_enough", "found_alternative", "bad_experience"],
446
522
  "months_subscribed": u.weighNumRange(1, 24, 1.5, 15),
447
- "trial_retained": [false],
448
523
  }
449
524
  },
450
525
  {
@@ -453,7 +528,6 @@ const config = {
453
528
  properties: {
454
529
  "issue_type": ["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request"],
455
530
  "order_id": orderIds,
456
- "trial_retained": [false],
457
531
  }
458
532
  },
459
533
  {
@@ -462,8 +536,6 @@ const config = {
462
536
  properties: {
463
537
  "order_id": orderIds,
464
538
  "original_order_age_days": u.weighNumRange(1, 60, 1.5, 30),
465
- "referral_user": [false],
466
- "trial_retained": [false],
467
539
  }
468
540
  },
469
541
  ],
@@ -488,8 +560,6 @@ const config = {
488
560
  "avg_order_value": u.weighNumRange(15, 80, 0.8, 40),
489
561
  "orders_per_month": u.weighNumRange(1, 20, 1.5, 10),
490
562
  "favorite_restaurant_count": u.weighNumRange(1, 10),
491
- "is_high_risk": [false],
492
- "churn_risk_score": [0],
493
563
  "Platform": ["iOS", "Android", "Web"],
494
564
  "subscription_tier": ["Free", "Free", "Free", "Free", "QuickBite+"],
495
565
  "city": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
@@ -519,173 +589,51 @@ const config = {
519
589
 
520
590
  lookupTables: [],
521
591
 
522
- /**
523
- * ARCHITECTED ANALYTICS HOOKS
524
- *
525
- * This hook function creates 8 deliberate patterns in the data:
526
- *
527
- * 1. LUNCH RUSH CONVERSION: Orders during lunch/dinner hours convert at higher rates
528
- * 2. COUPON INJECTION: Free-tier users get coupons spliced into funnels
529
- * 3. LATE NIGHT MUNCHIES: Late-night orders skew to fast food with inflated prices
530
- * 4. RAINY WEEK SURGE: Days 20-27 have surge pricing and doubled order volume
531
- * 5. REFERRAL POWER USERS: Referred users reorder more and rate higher
532
- * 6. TRIAL CONVERSION: Trial subscribers with 3+ early orders are retained
533
- * 7. SUPPORT TICKET CHURN: 15% of users flagged as high-risk churners
534
- * 8. FIRST ORDER BONUS: New users in the Order Completion funnel convert at 90%
535
- */
536
592
  hook: function (record, type, meta) {
537
- const NOW = dayjs();
538
- const DATASET_START = NOW.subtract(num_days, 'days');
539
- const RAINY_WEEK_START = DATASET_START.add(20, 'days');
540
- const RAINY_WEEK_END = DATASET_START.add(27, 'days');
541
-
542
- // ═══════════════════════════════════════════════════════════════════
543
- // HOOK 1: LUNCH RUSH CONVERSION (funnel-pre)
544
- // During lunch (11AM-1PM) and dinner (5PM-8PM), funnel conversion
545
- // rates are boosted to reflect real-world meal-time ordering surges.
546
- // ═══════════════════════════════════════════════════════════════════
547
- if (type === "funnel-pre") {
548
- if (meta && meta.firstEventTime) {
549
- const hour = dayjs.unix(meta.firstEventTime).hour();
550
-
551
- record.props = record.props || {};
552
-
553
- // Lunch rush: 11AM - 1PM
554
- if (hour >= 11 && hour <= 13) {
555
- record.props.lunch_rush = true;
556
- record.props.dinner_rush = false;
557
- }
558
- // Dinner rush: 5PM - 8PM
559
- else if (hour >= 17 && hour <= 20) {
560
- record.props.lunch_rush = false;
561
- record.props.dinner_rush = true;
562
- } else {
563
- record.props.lunch_rush = false;
564
- record.props.dinner_rush = false;
565
- }
566
- }
567
-
568
- // ═══════════════════════════════════════════════════════════════
569
- // HOOK 8: FIRST ORDER BONUS (funnel-pre)
570
- // New users (born in dataset) get a massive conversion boost on
571
- // the Order Completion funnel, simulating first-order promotions
572
- // and new-user incentives that drive initial purchases.
573
- // ═══════════════════════════════════════════════════════════════
574
- if (record.sequence &&
575
- record.sequence[0] === "checkout started" &&
576
- record.sequence[1] === "order placed") {
577
- record.props = record.props || {};
578
- // Use hash to deterministically assign ~50% of users as "new"
579
- const userId = meta && meta.user && (meta.user.distinct_id || String(meta.user));
580
- const isNewUser = userId && userId.charCodeAt(0) % 2 === 0;
581
- if (isNewUser) {
582
- record.props.first_order_bonus = true;
583
- } else {
584
- record.props.first_order_bonus = false;
585
- }
586
- }
587
- }
588
-
589
- // ═══════════════════════════════════════════════════════════════════
590
- // HOOK 2: COUPON INJECTION (funnel-post)
591
- // Free-tier users get coupon_applied events spliced into their
592
- // funnel sequences 30% of the time, simulating promotional nudges
593
- // that push non-subscribers toward conversion.
594
- // ═══════════════════════════════════════════════════════════════════
593
+ // HOOK 9 (T2C): ORDER LIFECYCLE TIME-TO-CONVERT (funnel-post)
594
+ // QuickBite+ subscribers complete order funnels 1.35x faster
595
+ // (factor 0.74); Free users 1.3x slower (factor 1.3).
595
596
  if (type === "funnel-post") {
596
- if (Array.isArray(record) && record.length >= 2) {
597
- // Check if user is Free tier from first event's super props
598
- const firstEvent = record[0];
599
- const isFreeUser = firstEvent && firstEvent.subscription_tier === "Free";
600
-
601
- if (isFreeUser && chance.bool({ likelihood: 30 })) {
602
- // Pick a random insertion point between funnel steps
603
- const insertIdx = chance.integer({ min: 1, max: record.length - 1 });
604
- const prevEvent = record[insertIdx - 1];
605
- const nextEvent = record[insertIdx];
606
- const midTime = dayjs(prevEvent.time).add(
607
- dayjs(nextEvent.time).diff(dayjs(prevEvent.time)) / 2,
608
- 'milliseconds'
609
- ).toISOString();
610
-
611
- const couponTemplate = record.find(e => e.event === "coupon applied");
612
- const couponEvent = {
613
- ...(couponTemplate || firstEvent),
614
- event: "coupon applied",
615
- time: midTime,
616
- user_id: firstEvent.user_id,
617
- subscription_tier: firstEvent.subscription_tier,
618
- Platform: firstEvent.Platform,
619
- city: firstEvent.city,
620
- coupon_code: chance.pickone(couponCodes),
621
- discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
622
- discount_value: chance.integer({ min: 10, max: 30 }),
623
- coupon_injected: true,
624
- };
625
- record.splice(insertIdx, 0, couponEvent);
597
+ const segment = meta?.profile?.subscription_tier;
598
+ if (Array.isArray(record) && record.length > 1) {
599
+ const factor = (
600
+ segment === "QuickBite+" ? 0.74 :
601
+ segment === "Free" ? 1.3 :
602
+ 1.0
603
+ );
604
+ if (factor !== 1.0) {
605
+ for (let i = 1; i < record.length; i++) {
606
+ const prev = dayjs(record[i - 1].time);
607
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
608
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
609
+ }
626
610
  }
627
611
  }
628
612
  }
629
613
 
630
- // ═══════════════════════════════════════════════════════════════════
631
- // HOOK 3: LATE NIGHT MUNCHIES (event)
632
- // Between 10PM and 2AM, restaurant views and cart additions skew
633
- // heavily toward fast food (American cuisine) with inflated prices,
634
- // modeling the real-world late-night ordering pattern.
635
- // ═══════════════════════════════════════════════════════════════════
636
614
  if (type === "event") {
615
+ const datasetStart = dayjs.unix(meta.datasetStart);
616
+ const RAINY_WEEK_START = datasetStart.add(20, 'days');
617
+ const RAINY_WEEK_END = datasetStart.add(27, 'days');
637
618
  const EVENT_TIME = dayjs(record.time);
638
- const hour = EVENT_TIME.hour();
639
- const isLateNight = hour >= 22 || hour <= 2;
640
-
641
- if (record.event === "restaurant viewed" || record.event === "item added to cart") {
642
- if (isLateNight) {
643
- // 70% of late-night browsing/ordering is fast food (American)
644
- if (chance.bool({ likelihood: 70 })) {
645
- if (record.cuisine_type !== undefined) {
646
- record.cuisine_type = "American";
647
- }
648
- }
649
-
650
- // Boost item price by 1.3x (late-night surcharge effect)
651
- if (record.item_price !== undefined) {
652
- record.item_price = Math.round(record.item_price * 1.3 * 100) / 100;
653
- }
654
-
655
- record.late_night_order = true;
656
- } else {
657
- record.late_night_order = false;
658
- }
659
- }
660
619
 
661
- // ═══════════════════════════════════════════════════════════════
662
- // HOOK 4: RAINY WEEK SURGE (event)
663
- // During days 20-27, delivery fees double on order_placed events
664
- // and there is a 40% chance of duplicating the event, simulating
665
- // a weather-driven demand surge with surge pricing.
666
- // ═══════════════════════════════════════════════════════════════
620
+ // HOOK 4: RAINY WEEK SURGE — days 20-27, double delivery_fee on
621
+ // order placed events. Mutates existing prop. No flag.
667
622
  if (record.event === "order placed") {
668
623
  if (EVENT_TIME.isAfter(RAINY_WEEK_START) && EVENT_TIME.isBefore(RAINY_WEEK_END)) {
669
624
  record.delivery_fee = (record.delivery_fee || 5) * 2;
670
- record.surge_pricing = true;
671
- record.rainy_week = true;
672
- } else {
673
- record.surge_pricing = false;
674
625
  }
675
626
  }
676
627
  }
677
628
 
678
- // ═══════════════════════════════════════════════════════════════════
679
- // HOOK 5: REFERRAL POWER USERS (everything)
680
- // Users who signed up with a referral code get 2x more reorder
681
- // events and boosted food ratings. Referred users are more loyal
682
- // and satisfied, mirroring real referral program outcomes.
683
- // ═══════════════════════════════════════════════════════════════════
684
629
  if (type === "everything") {
630
+ const datasetStart = dayjs.unix(meta.datasetStart);
631
+ const RAINY_WEEK_START = datasetStart.add(20, 'days');
632
+ const RAINY_WEEK_END = datasetStart.add(27, 'days');
685
633
  const userEvents = record;
686
634
  const profile = meta.profile;
687
635
 
688
- // ── Stamp superProps from profile so they stay consistent per user ──
636
+ // Stamp superProps from profile so they stay consistent per user
689
637
  if (profile) {
690
638
  userEvents.forEach((event) => {
691
639
  if (profile.Platform !== undefined) event.Platform = profile.Platform;
@@ -694,136 +642,162 @@ const config = {
694
642
  });
695
643
  }
696
644
 
697
- // ── Bug 2 fix: Conversion filtering for lunch rush + first order ──
698
- // Hook 1: Non-lunch/dinner-hour events drop ~30% of final funnel step
699
- // Hook 8: Returning users (no first_order_bonus) drop ~30% of final funnel step
645
+ // HOOK 3: LATE NIGHT MUNCHIES 10PM-2AM UTC, 70% of restaurant
646
+ // views/cart additions get cuisine flipped to American, 1.3x
647
+ // item_price. Mutates existing props. No flag discover via
648
+ // HOD breakdown on cuisine_type / item_price.
649
+ userEvents.forEach(e => {
650
+ if (e.event === "restaurant viewed" || e.event === "item added to cart") {
651
+ const hour = new Date(e.time).getUTCHours();
652
+ const isLateNight = hour >= 22 || hour <= 2;
653
+ if (isLateNight) {
654
+ if (e.cuisine_type !== undefined && chance.bool({ likelihood: 70 })) {
655
+ e.cuisine_type = "American";
656
+ }
657
+ if (e.item_price !== undefined) {
658
+ e.item_price = Math.round(e.item_price * 1.3 * 100) / 100;
659
+ }
660
+ }
661
+ }
662
+ });
663
+
664
+ // HOOK 2: COUPON INJECTION — Free-tier users get coupon applied
665
+ // events spliced into stream near checkout. Cloned from existing
666
+ // coupon-applied event with unique offset time. No flag.
667
+ if (profile && profile.subscription_tier === "Free") {
668
+ for (let i = userEvents.length - 1; i >= 1; i--) {
669
+ const evt = userEvents[i];
670
+ if (evt.event === "checkout started" && chance.bool({ likelihood: 30 })) {
671
+ const prevEvent = userEvents[i - 1];
672
+ const midTime = dayjs(prevEvent.time).add(
673
+ dayjs(evt.time).diff(dayjs(prevEvent.time)) / 2,
674
+ 'milliseconds'
675
+ ).toISOString();
676
+
677
+ const couponTemplate = userEvents.find(e => e.event === "coupon applied");
678
+ const couponEvent = {
679
+ ...(couponTemplate || evt),
680
+ event: "coupon applied",
681
+ time: midTime,
682
+ user_id: evt.user_id,
683
+ subscription_tier: profile.subscription_tier,
684
+ Platform: profile.Platform,
685
+ city: profile.city,
686
+ coupon_code: chance.pickone(couponCodes),
687
+ discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
688
+ discount_value: chance.integer({ min: 10, max: 30 }),
689
+ };
690
+ userEvents.splice(i, 0, couponEvent);
691
+ }
692
+ }
693
+ }
694
+
695
+ // HOOK 1: LUNCH/DINNER RUSH — drop 30% of order delivered events
696
+ // that fall outside the meal hours (11-13 UTC and 17-20 UTC).
697
+ // Discoverable via order delivered breakdown by HOD.
700
698
  for (let i = userEvents.length - 1; i >= 0; i--) {
701
699
  const event = userEvents[i];
702
700
  if (event.event === "order delivered") {
703
- // Hook 1: if not during lunch or dinner rush, drop 30%
704
- if (!event.lunch_rush && !event.dinner_rush && chance.bool({ likelihood: 30 })) {
701
+ const hour = new Date(event.time).getUTCHours();
702
+ const inRush = (hour >= 11 && hour <= 13) || (hour >= 17 && hour <= 20);
703
+ if (!inRush && chance.bool({ likelihood: 30 })) {
705
704
  userEvents.splice(i, 1);
706
- continue;
707
705
  }
708
- // Hook 8: returning users (no first_order_bonus) drop 30%
709
- if (event.first_order_bonus === false && chance.bool({ likelihood: 30 })) {
706
+ }
707
+ }
708
+
709
+ // HOOK 8: FIRST ORDER BONUS — hash-based ~50% of users (returning)
710
+ // drop 30% of order delivered events. New users keep all.
711
+ // Discover via cohort builder on hash bucket vs conversion.
712
+ const hashUser = userEvents[0] && userEvents[0].user_id;
713
+ const isNewUser = typeof hashUser === "string" && hashUser.charCodeAt(0) % 2 === 0;
714
+ if (!isNewUser) {
715
+ for (let i = userEvents.length - 1; i >= 0; i--) {
716
+ if (userEvents[i].event === "order delivered" && chance.bool({ likelihood: 30 })) {
710
717
  userEvents.splice(i, 1);
711
- continue;
712
718
  }
713
719
  }
714
720
  }
715
721
 
716
- // First pass: identify user patterns
722
+ // First pass: identify behavioral patterns (no flags written)
717
723
  let isReferralUser = false;
718
724
  let hasTrialSubscription = false;
719
725
  let earlyOrderCount = 0;
720
- let firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
726
+ let orderPlacedCount = 0;
727
+ const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
721
728
 
722
729
  userEvents.forEach((event) => {
723
730
  const eventTime = dayjs(event.time);
724
731
  const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
725
-
726
- // Hook #5: Track referral users
727
- if (event.event === "account created" && event.referral_code === true) {
728
- isReferralUser = true;
729
- }
730
-
731
- // Hook #6: Track trial subscribers and early orders
732
- if (event.event === "subscription started" && event.trial === true) {
733
- hasTrialSubscription = true;
734
- }
735
- if (event.event === "order placed" && daysSinceStart <= 14) {
736
- earlyOrderCount++;
732
+ if (event.event === "account created" && event.referral_code === true) isReferralUser = true;
733
+ if (event.event === "subscription started" && event.trial === true) hasTrialSubscription = true;
734
+ if (event.event === "order placed") {
735
+ orderPlacedCount++;
736
+ if (daysSinceStart <= 14) earlyOrderCount++;
737
737
  }
738
738
  });
739
739
 
740
- // Second pass: apply referral power user effects
740
+ // HOOK 5: REFERRAL POWER USERS boost food rating to 4-5,
741
+ // clone reorder events. No flag.
741
742
  userEvents.forEach((event, idx) => {
742
- if (event.event === "order rated") {
743
- if (isReferralUser) {
744
- event.food_rating = chance.integer({ min: 4, max: 5 });
745
- event.referral_user = true;
746
- } else {
747
- event.referral_user = false;
748
- }
743
+ if (isReferralUser && event.event === "order rated") {
744
+ event.food_rating = chance.integer({ min: 4, max: 5 });
749
745
  }
750
746
  if (isReferralUser && event.event === "reorder initiated" && chance.bool({ likelihood: 50 })) {
751
747
  const eventTime = dayjs(event.time);
752
- const extraReorder = {
748
+ userEvents.splice(idx + 1, 0, {
753
749
  ...event,
754
750
  time: eventTime.add(chance.integer({ min: 1, max: 7 }), 'days').toISOString(),
755
751
  user_id: event.user_id,
756
752
  order_id: chance.pickone(orderIds),
757
753
  original_order_age_days: chance.integer({ min: 3, max: 30 }),
758
- referral_user: true,
759
- };
760
- userEvents.splice(idx + 1, 0, extraReorder);
754
+ });
761
755
  }
762
756
  });
763
757
 
764
- // ═══════════════════════════════════════════════════════════════
765
- // HOOK 6: TRIAL CONVERSION (everything)
766
- // Trial subscribers who place 3+ orders in their first 14 days
767
- // are retained. Those with fewer orders churn: 60% of their
768
- // events after day 14 are removed, simulating subscription
769
- // abandonment after a failed trial experience.
770
- // ═══════════════════════════════════════════════════════════════
771
- if (hasTrialSubscription) {
758
+ // HOOK 6: TRIAL CONVERSION — trial subs with <3 early orders
759
+ // drop 60% of post-day-14 events. No flag.
760
+ if (hasTrialSubscription && earlyOrderCount < 3 && chance.bool({ likelihood: 60 })) {
772
761
  const trialCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
773
-
774
- if (earlyOrderCount >= 3) {
775
- // Retained: mark events as trial_retained
776
- userEvents.forEach((event) => {
777
- event.trial_retained = true;
778
- });
779
- } else {
780
- // Churned: remove 60% of events after day 14
781
- userEvents.forEach((event) => {
782
- event.trial_retained = false;
783
- });
762
+ if (trialCutoff) {
784
763
  for (let i = userEvents.length - 1; i >= 0; i--) {
785
- const evt = userEvents[i];
786
- if (trialCutoff && dayjs(evt.time).isAfter(trialCutoff)) {
787
- if (chance.bool({ likelihood: 60 })) {
788
- userEvents.splice(i, 1);
789
- }
764
+ if (dayjs(userEvents[i].time).isAfter(trialCutoff)) {
765
+ userEvents.splice(i, 1);
790
766
  }
791
767
  }
792
768
  }
793
769
  }
794
770
 
795
- // ═══════════════════════════════════════════════════════════════
796
- // HOOK 4: RAINY WEEK SURGE - duplicate order events (everything)
797
- // Events tagged surge_pricing=true in the event hook get a 40%
798
- // chance of duplication here, creating visible demand spikes.
799
- // ═══════════════════════════════════════════════════════════════
771
+ // HOOK 4 (cont): RAINY WEEK SURGE — duplicate 40% of order placed
772
+ // events that fall in the rainy window. Cloned with unique offset.
800
773
  const rainyDuplicates = [];
801
774
  userEvents.forEach((event) => {
802
- if (event.surge_pricing === true && event.event === "order placed" && chance.bool({ likelihood: 40 })) {
803
- const dup = JSON.parse(JSON.stringify(event));
804
- dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString();
805
- dup.rainy_week = true;
806
- rainyDuplicates.push(dup);
775
+ if (event.event === "order placed") {
776
+ const t = dayjs(event.time);
777
+ if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END) && chance.bool({ likelihood: 40 })) {
778
+ const dup = JSON.parse(JSON.stringify(event));
779
+ dup.time = t.add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString();
780
+ rainyDuplicates.push(dup);
781
+ }
807
782
  }
808
783
  });
809
- if (rainyDuplicates.length > 0) {
810
- userEvents.push(...rainyDuplicates);
811
- }
812
- }
813
-
814
- // ═══════════════════════════════════════════════════════════════════
815
- // HOOK 7: SUPPORT TICKET CHURN (user)
816
- // 15% of users are flagged as high-risk with elevated churn scores.
817
- // The remaining users get low churn scores. This creates a clear
818
- // segmentation opportunity for proactive retention campaigns.
819
- // ═══════════════════════════════════════════════════════════════════
820
- if (type === "user") {
821
- if (chance.bool({ likelihood: 15 })) {
822
- record.is_high_risk = true;
823
- record.churn_risk_score = chance.integer({ min: 70, max: 100 });
824
- } else {
825
- record.is_high_risk = false;
826
- record.churn_risk_score = chance.integer({ min: 0, max: 40 });
784
+ if (rainyDuplicates.length > 0) userEvents.push(...rainyDuplicates);
785
+
786
+ // HOOK 9: ORDER-COUNT MAGIC NUMBER (no flags)
787
+ // Sweet 4-8 orders → +40% on order_total. Over 9+ → drop 35% of
788
+ // order placed events (oversaturated; analyst sees inverted-U).
789
+ if (orderPlacedCount >= 4 && orderPlacedCount <= 8) {
790
+ userEvents.forEach(e => {
791
+ if (e.event === "order placed" && typeof e.order_total === "number") {
792
+ e.order_total = Math.round(e.order_total * 1.4);
793
+ }
794
+ });
795
+ } else if (orderPlacedCount >= 9) {
796
+ userEvents.forEach(e => {
797
+ if (e.event === "order placed" && typeof e.order_total === "number") {
798
+ e.order_total = Math.round(e.order_total * 0.65);
799
+ }
800
+ });
827
801
  }
828
802
  }
829
803