@ak--47/dungeon-master 1.3.0 → 1.4.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 (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "harness-food";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -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
+ * ───────────────────────────────────────────────────────────────────────────────
85
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
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
+ * ───────────────────────────────────────────────────────────────────────────────
93
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:
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.
117
190
  *
118
191
  * ───────────────────────────────────────────────────────────────────────────────
119
- * ADVANCED ANALYSIS IDEAS
192
+ * 7. FIRST ORDER BONUS (everything)
120
193
  * ───────────────────────────────────────────────────────────────────────────────
121
194
  *
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
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.
209
+ *
210
+ * ───────────────────────────────────────────────────────────────────────────────
211
+ * 8. ORDER-COUNT MAGIC NUMBER (everything)
212
+ * ───────────────────────────────────────────────────────────────────────────────
213
+ *
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
@@ -163,12 +261,16 @@ const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
163
261
 
164
262
  /** @type {Config} */
165
263
  const config = {
264
+ version: 2,
166
265
  token,
167
266
  seed: SEED,
168
- numDays: num_days,
267
+ datasetStart: "2026-01-01T00:00:00Z",
268
+ datasetEnd: "2026-05-01T23:59:59Z",
269
+ // numDays: num_days,
169
270
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
170
271
  numUsers: num_users,
171
- hasAnonIds: false,
272
+ hasAnonIds: true,
273
+ avgDevicePerUser: 2,
172
274
  hasSessionIds: true,
173
275
  format: "json",
174
276
  gzip: true,
@@ -265,10 +367,10 @@ const config = {
265
367
  event: "account created",
266
368
  weight: 1,
267
369
  isFirstEvent: true,
370
+ isAuthEvent: true,
268
371
  properties: {
269
372
  "signup_method": ["email", "google", "apple", "facebook"],
270
373
  "referral_code": [false, false, true],
271
- "trial_retained": [false],
272
374
  }
273
375
  },
274
376
  {
@@ -287,7 +389,6 @@ const config = {
287
389
  ],
288
390
  "sort_by": ["recommended", "distance", "rating", "price"],
289
391
  "filter_applied": [false, false, false, true, true],
290
- "trial_retained": [false],
291
392
  }
292
393
  },
293
394
  {
@@ -308,8 +409,6 @@ const config = {
308
409
  "avg_rating": u.weighNumRange(1, 5, 0.8, 30),
309
410
  "delivery_time_est_mins": u.weighNumRange(15, 90, 1.2, 40),
310
411
  "price_tier": ["$", "$$", "$$$", "$$$$"],
311
- "late_night_order": [false],
312
- "trial_retained": [false],
313
412
  }
314
413
  },
315
414
  {
@@ -320,8 +419,6 @@ const config = {
320
419
  "item_category": ["entree", "appetizer", "drink", "dessert", "side"],
321
420
  "item_price": u.weighNumRange(3, 65, 1.0, 40),
322
421
  "customization_count": u.weighNumRange(0, 5, 1.5, 20),
323
- "late_night_order": [false],
324
- "trial_retained": [false],
325
422
  }
326
423
  },
327
424
  {
@@ -330,7 +427,6 @@ const config = {
330
427
  properties: {
331
428
  "item_id": itemIds,
332
429
  "removal_reason": ["changed_mind", "too_expensive", "substitution"],
333
- "trial_retained": [false],
334
430
  }
335
431
  },
336
432
  {
@@ -340,8 +436,6 @@ const config = {
340
436
  "coupon_code": couponCodes,
341
437
  "discount_type": ["percent", "flat", "free_delivery"],
342
438
  "discount_value": u.weighNumRange(5, 50, 1.2, 20),
343
- "coupon_injected": [false],
344
- "trial_retained": [false],
345
439
  }
346
440
  },
347
441
  {
@@ -351,7 +445,6 @@ const config = {
351
445
  "cart_total": u.weighNumRange(8, 150, 0.8, 40),
352
446
  "items_count": u.weighNumRange(1, 8, 1.2, 20),
353
447
  "delivery_address_saved": [false, false, false, true, true, true, true, true, true, true],
354
- "trial_retained": [false],
355
448
  }
356
449
  },
357
450
  {
@@ -363,9 +456,6 @@ const config = {
363
456
  "order_total": u.weighNumRange(10, 200, 0.8, 40),
364
457
  "tip_amount": u.weighNumRange(0, 30, 1.5, 20),
365
458
  "delivery_fee": u.weighNumRange(0, 12, 1.0, 20),
366
- "surge_pricing": [false],
367
- "rainy_week": [false],
368
- "trial_retained": [false],
369
459
  }
370
460
  },
371
461
  {
@@ -375,7 +465,6 @@ const config = {
375
465
  "order_id": orderIds,
376
466
  "order_status": ["confirmed", "preparing", "picked_up", "en_route", "delivered"],
377
467
  "eta_mins": u.weighNumRange(5, 60, 1.0, 30),
378
- "trial_retained": [false],
379
468
  }
380
469
  },
381
470
  {
@@ -385,10 +474,6 @@ const config = {
385
474
  "order_id": orderIds,
386
475
  "actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
387
476
  "on_time": [false, false, false, true, true, true, true, true, true, true],
388
- "lunch_rush": [false],
389
- "dinner_rush": [false],
390
- "first_order_bonus": [false],
391
- "trial_retained": [false],
392
477
  }
393
478
  },
394
479
  {
@@ -399,8 +484,6 @@ const config = {
399
484
  "food_rating": u.weighNumRange(1, 5, 0.8, 30),
400
485
  "delivery_rating": u.weighNumRange(1, 5, 0.8, 30),
401
486
  "would_reorder": [false, true, true],
402
- "referral_user": [false],
403
- "trial_retained": [false],
404
487
  }
405
488
  },
406
489
  {
@@ -414,7 +497,6 @@ const config = {
414
497
  ]),
415
498
  "results_count": u.weighNumRange(0, 50, 0.8, 30),
416
499
  "search_type": ["restaurant", "cuisine", "dish"],
417
- "trial_retained": [false],
418
500
  }
419
501
  },
420
502
  {
@@ -424,7 +506,6 @@ const config = {
424
506
  "promo_id": () => `promo_${v.uid(5)}`,
425
507
  "promo_type": ["banner", "push", "in_feed"],
426
508
  "promo_value": ["10%", "15%", "20%", "25%", "30%", "40%", "50%"],
427
- "trial_retained": [false],
428
509
  }
429
510
  },
430
511
  {
@@ -434,7 +515,6 @@ const config = {
434
515
  "plan": ["quickbite_plus_monthly", "quickbite_plus_monthly", "quickbite_plus_annual"],
435
516
  "price": [9.99, 9.99, 79.99],
436
517
  "trial": [true, false],
437
- "trial_retained": [false],
438
518
  }
439
519
  },
440
520
  {
@@ -443,7 +523,6 @@ const config = {
443
523
  properties: {
444
524
  "reason": ["too_expensive", "not_ordering_enough", "found_alternative", "bad_experience"],
445
525
  "months_subscribed": u.weighNumRange(1, 24, 1.5, 15),
446
- "trial_retained": [false],
447
526
  }
448
527
  },
449
528
  {
@@ -452,7 +531,6 @@ const config = {
452
531
  properties: {
453
532
  "issue_type": ["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request"],
454
533
  "order_id": orderIds,
455
- "trial_retained": [false],
456
534
  }
457
535
  },
458
536
  {
@@ -461,8 +539,6 @@ const config = {
461
539
  properties: {
462
540
  "order_id": orderIds,
463
541
  "original_order_age_days": u.weighNumRange(1, 60, 1.5, 30),
464
- "referral_user": [false],
465
- "trial_retained": [false],
466
542
  }
467
543
  },
468
544
  ],
@@ -487,8 +563,6 @@ const config = {
487
563
  "avg_order_value": u.weighNumRange(15, 80, 0.8, 40),
488
564
  "orders_per_month": u.weighNumRange(1, 20, 1.5, 10),
489
565
  "favorite_restaurant_count": u.weighNumRange(1, 10),
490
- "is_high_risk": [false],
491
- "churn_risk_score": [0],
492
566
  "Platform": ["iOS", "Android", "Web"],
493
567
  "subscription_tier": ["Free", "Free", "Free", "Free", "QuickBite+"],
494
568
  "city": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
@@ -518,135 +592,36 @@ const config = {
518
592
 
519
593
  lookupTables: [],
520
594
 
521
- /**
522
- * ARCHITECTED ANALYTICS HOOKS
523
- *
524
- * This hook function creates 8 deliberate patterns in the data:
525
- *
526
- * 1. LUNCH RUSH CONVERSION: Orders during lunch/dinner hours convert at higher rates
527
- * 2. COUPON INJECTION: Free-tier users get coupons spliced into funnels
528
- * 3. LATE NIGHT MUNCHIES: Late-night orders skew to fast food with inflated prices
529
- * 4. RAINY WEEK SURGE: Days 20-27 have surge pricing and doubled order volume
530
- * 5. REFERRAL POWER USERS: Referred users reorder more and rate higher
531
- * 6. TRIAL CONVERSION: Trial subscribers with 3+ early orders are retained
532
- * 7. SUPPORT TICKET CHURN: 15% of users flagged as high-risk churners
533
- * 8. FIRST ORDER BONUS: New users in the Order Completion funnel convert at 90%
534
- */
535
595
  hook: function (record, type, meta) {
536
- const NOW = dayjs();
537
- const DATASET_START = NOW.subtract(num_days, 'days');
538
- const RAINY_WEEK_START = DATASET_START.add(20, 'days');
539
- const RAINY_WEEK_END = DATASET_START.add(27, 'days');
540
-
541
- // ═══════════════════════════════════════════════════════════════════
542
- // HOOK 1: LUNCH RUSH CONVERSION (funnel-pre)
543
- // During lunch (11AM-1PM) and dinner (5PM-8PM), funnel conversion
544
- // rates are boosted to reflect real-world meal-time ordering surges.
545
- // ═══════════════════════════════════════════════════════════════════
546
- if (type === "funnel-pre") {
547
- if (meta && meta.firstEventTime) {
548
- const hour = dayjs.unix(meta.firstEventTime).hour();
549
-
550
- record.props = record.props || {};
551
-
552
- // Lunch rush: 11AM - 1PM
553
- if (hour >= 11 && hour <= 13) {
554
- record.props.lunch_rush = true;
555
- record.props.dinner_rush = false;
556
- }
557
- // Dinner rush: 5PM - 8PM
558
- else if (hour >= 17 && hour <= 20) {
559
- record.props.lunch_rush = false;
560
- record.props.dinner_rush = true;
561
- } else {
562
- record.props.lunch_rush = false;
563
- record.props.dinner_rush = false;
564
- }
565
- }
566
-
567
- // ═══════════════════════════════════════════════════════════════
568
- // HOOK 8: FIRST ORDER BONUS (funnel-pre)
569
- // New users (born in dataset) get a massive conversion boost on
570
- // the Order Completion funnel, simulating first-order promotions
571
- // and new-user incentives that drive initial purchases.
572
- // ═══════════════════════════════════════════════════════════════
573
- if (record.sequence &&
574
- record.sequence[0] === "checkout started" &&
575
- record.sequence[1] === "order placed") {
576
- record.props = record.props || {};
577
- // Use hash to deterministically assign ~50% of users as "new"
578
- const userId = meta && meta.user && (meta.user.distinct_id || String(meta.user));
579
- const isNewUser = userId && userId.charCodeAt(0) % 2 === 0;
580
- if (isNewUser) {
581
- record.props.first_order_bonus = true;
582
- } else {
583
- record.props.first_order_bonus = false;
584
- }
585
- }
586
- }
587
-
588
- // (Hook #2 coupon injection moved to everything hook where
589
- // meta.profile.subscription_tier is authoritative)
590
-
591
- // ═══════════════════════════════════════════════════════════════════
592
- // HOOK 3: LATE NIGHT MUNCHIES (event)
593
- // Between 10PM and 2AM, restaurant views and cart additions skew
594
- // heavily toward fast food (American cuisine) with inflated prices,
595
- // modeling the real-world late-night ordering pattern.
596
- // ═══════════════════════════════════════════════════════════════════
597
- if (type === "event") {
598
- const EVENT_TIME = dayjs(record.time);
599
- const hour = EVENT_TIME.hour();
600
- const isLateNight = hour >= 22 || hour <= 2;
601
-
602
- if (record.event === "restaurant viewed" || record.event === "item added to cart") {
603
- if (isLateNight) {
604
- // 70% of late-night browsing/ordering is fast food (American)
605
- if (chance.bool({ likelihood: 70 })) {
606
- if (record.cuisine_type !== undefined) {
607
- record.cuisine_type = "American";
608
- }
596
+ // HOOK 9 (T2C): ORDER LIFECYCLE TIME-TO-CONVERT (funnel-post)
597
+ // QuickBite+ subscribers complete order funnels 1.35x faster
598
+ // (factor 0.74); Free users 1.3x slower (factor 1.3).
599
+ if (type === "funnel-post") {
600
+ const segment = meta?.profile?.subscription_tier;
601
+ if (Array.isArray(record) && record.length > 1) {
602
+ const factor = (
603
+ segment === "QuickBite+" ? 0.74 :
604
+ segment === "Free" ? 1.3 :
605
+ 1.0
606
+ );
607
+ if (factor !== 1.0) {
608
+ for (let i = 1; i < record.length; i++) {
609
+ const prev = dayjs(record[i - 1].time);
610
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
611
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
609
612
  }
610
-
611
- // Boost item price by 1.3x (late-night surcharge effect)
612
- if (record.item_price !== undefined) {
613
- record.item_price = Math.round(record.item_price * 1.3 * 100) / 100;
614
- }
615
-
616
- record.late_night_order = true;
617
- } else {
618
- record.late_night_order = false;
619
- }
620
- }
621
-
622
- // ═══════════════════════════════════════════════════════════════
623
- // HOOK 4: RAINY WEEK SURGE (event)
624
- // During days 20-27, delivery fees double on order_placed events
625
- // and there is a 40% chance of duplicating the event, simulating
626
- // a weather-driven demand surge with surge pricing.
627
- // ═══════════════════════════════════════════════════════════════
628
- if (record.event === "order placed") {
629
- if (EVENT_TIME.isAfter(RAINY_WEEK_START) && EVENT_TIME.isBefore(RAINY_WEEK_END)) {
630
- record.delivery_fee = (record.delivery_fee || 5) * 2;
631
- record.surge_pricing = true;
632
- record.rainy_week = true;
633
- } else {
634
- record.surge_pricing = false;
635
613
  }
636
614
  }
637
615
  }
638
616
 
639
- // ═══════════════════════════════════════════════════════════════════
640
- // HOOK 5: REFERRAL POWER USERS (everything)
641
- // Users who signed up with a referral code get 2x more reorder
642
- // events and boosted food ratings. Referred users are more loyal
643
- // and satisfied, mirroring real referral program outcomes.
644
- // ═══════════════════════════════════════════════════════════════════
645
617
  if (type === "everything") {
618
+ const datasetStart = dayjs.unix(meta.datasetStart);
619
+ const RAINY_WEEK_START = datasetStart.add(20, 'days');
620
+ const RAINY_WEEK_END = datasetStart.add(27, 'days');
646
621
  const userEvents = record;
647
622
  const profile = meta.profile;
648
623
 
649
- // ── Stamp superProps from profile so they stay consistent per user ──
624
+ // Stamp superProps from profile so they stay consistent per user
650
625
  if (profile) {
651
626
  userEvents.forEach((event) => {
652
627
  if (profile.Platform !== undefined) event.Platform = profile.Platform;
@@ -655,10 +630,28 @@ const config = {
655
630
  });
656
631
  }
657
632
 
658
- // ── Hook #2: COUPON INJECTION (moved from funnel-post) ──
659
- // Free-tier users get coupon_applied events injected 30% of the
660
- // time near checkout events. Uses meta.profile.subscription_tier
661
- // which is authoritative here.
633
+ // HOOK 3: LATE NIGHT MUNCHIES 10PM-2AM UTC, 70% of restaurant
634
+ // views/cart additions get cuisine flipped to American, 1.3x
635
+ // item_price. Mutates existing props. No flag — discover via
636
+ // HOD breakdown on cuisine_type / item_price.
637
+ userEvents.forEach(e => {
638
+ if (e.event === "restaurant viewed" || e.event === "item added to cart") {
639
+ const hour = new Date(e.time).getUTCHours();
640
+ const isLateNight = hour >= 22 || hour <= 2;
641
+ if (isLateNight) {
642
+ if (e.cuisine_type !== undefined && chance.bool({ likelihood: 70 })) {
643
+ e.cuisine_type = "American";
644
+ }
645
+ if (e.item_price !== undefined) {
646
+ e.item_price = Math.round(e.item_price * 1.3 * 100) / 100;
647
+ }
648
+ }
649
+ }
650
+ });
651
+
652
+ // HOOK 2: COUPON INJECTION — Free-tier users get coupon applied
653
+ // events spliced into stream near checkout. Cloned from existing
654
+ // coupon-applied event with unique offset time. No flag.
662
655
  if (profile && profile.subscription_tier === "Free") {
663
656
  for (let i = userEvents.length - 1; i >= 1; i--) {
664
657
  const evt = userEvents[i];
@@ -681,143 +674,129 @@ const config = {
681
674
  coupon_code: chance.pickone(couponCodes),
682
675
  discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
683
676
  discount_value: chance.integer({ min: 10, max: 30 }),
684
- coupon_injected: true,
685
677
  };
686
678
  userEvents.splice(i, 0, couponEvent);
687
679
  }
688
680
  }
689
681
  }
690
682
 
691
- // ── Bug 2 fix: Conversion filtering for lunch rush + first order ──
692
- // Hook 1: Non-lunch/dinner-hour events drop ~30% of final funnel step
693
- // Hook 8: Returning users (no first_order_bonus) → drop ~30% of final funnel step
683
+ // HOOK 1: LUNCH/DINNER RUSH drop 30% of order delivered events
684
+ // that fall outside the meal hours (11-13 UTC and 17-20 UTC).
685
+ // Discoverable via order delivered breakdown by HOD.
694
686
  for (let i = userEvents.length - 1; i >= 0; i--) {
695
687
  const event = userEvents[i];
696
688
  if (event.event === "order delivered") {
697
- // Hook 1: if not during lunch or dinner rush, drop 30%
698
- if (!event.lunch_rush && !event.dinner_rush && chance.bool({ likelihood: 30 })) {
689
+ const hour = new Date(event.time).getUTCHours();
690
+ const inRush = (hour >= 11 && hour <= 13) || (hour >= 17 && hour <= 20);
691
+ if (!inRush && chance.bool({ likelihood: 30 })) {
699
692
  userEvents.splice(i, 1);
700
- continue;
701
693
  }
702
- // Hook 8: returning users (no first_order_bonus) drop 30%
703
- if (event.first_order_bonus === false && chance.bool({ likelihood: 30 })) {
694
+ }
695
+ }
696
+
697
+ // HOOK 8: FIRST ORDER BONUS — hash-based ~50% of users (returning)
698
+ // drop 30% of order delivered events. New users keep all.
699
+ // Discover via cohort builder on hash bucket vs conversion.
700
+ const hashUser = userEvents[0] && userEvents[0].user_id;
701
+ const isNewUser = typeof hashUser === "string" && hashUser.charCodeAt(0) % 2 === 0;
702
+ if (!isNewUser) {
703
+ for (let i = userEvents.length - 1; i >= 0; i--) {
704
+ if (userEvents[i].event === "order delivered" && chance.bool({ likelihood: 30 })) {
704
705
  userEvents.splice(i, 1);
705
- continue;
706
706
  }
707
707
  }
708
708
  }
709
709
 
710
- // First pass: identify user patterns
710
+ // First pass: identify behavioral patterns (no flags written)
711
711
  let isReferralUser = false;
712
712
  let hasTrialSubscription = false;
713
713
  let earlyOrderCount = 0;
714
- let firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
714
+ let orderPlacedCount = 0;
715
+ const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
715
716
 
716
717
  userEvents.forEach((event) => {
717
718
  const eventTime = dayjs(event.time);
718
719
  const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
719
-
720
- // Hook #5: Track referral users
721
- if (event.event === "account created" && event.referral_code === true) {
722
- isReferralUser = true;
723
- }
724
-
725
- // Hook #6: Track trial subscribers and early orders
726
- if (event.event === "subscription started" && event.trial === true) {
727
- hasTrialSubscription = true;
728
- }
729
- if (event.event === "order placed" && daysSinceStart <= 14) {
730
- earlyOrderCount++;
720
+ if (event.event === "account created" && event.referral_code === true) isReferralUser = true;
721
+ if (event.event === "subscription started" && event.trial === true) hasTrialSubscription = true;
722
+ if (event.event === "order placed") {
723
+ orderPlacedCount++;
724
+ if (daysSinceStart <= 14) earlyOrderCount++;
731
725
  }
732
726
  });
733
727
 
734
- // Second pass: apply referral power user effects
728
+ // HOOK 5: REFERRAL POWER USERS boost food rating to 4-5,
729
+ // clone reorder events. No flag.
735
730
  userEvents.forEach((event, idx) => {
736
- if (event.event === "order rated") {
737
- if (isReferralUser) {
738
- event.food_rating = chance.integer({ min: 4, max: 5 });
739
- event.referral_user = true;
740
- } else {
741
- event.referral_user = false;
742
- }
731
+ if (isReferralUser && event.event === "order rated") {
732
+ event.food_rating = chance.integer({ min: 4, max: 5 });
743
733
  }
744
734
  if (isReferralUser && event.event === "reorder initiated" && chance.bool({ likelihood: 50 })) {
745
735
  const eventTime = dayjs(event.time);
746
- const extraReorder = {
736
+ userEvents.splice(idx + 1, 0, {
747
737
  ...event,
748
738
  time: eventTime.add(chance.integer({ min: 1, max: 7 }), 'days').toISOString(),
749
739
  user_id: event.user_id,
750
740
  order_id: chance.pickone(orderIds),
751
741
  original_order_age_days: chance.integer({ min: 3, max: 30 }),
752
- referral_user: true,
753
- };
754
- userEvents.splice(idx + 1, 0, extraReorder);
742
+ });
755
743
  }
756
744
  });
757
745
 
758
- // ═══════════════════════════════════════════════════════════════
759
- // HOOK 6: TRIAL CONVERSION (everything)
760
- // Trial subscribers who place 3+ orders in their first 14 days
761
- // are retained. Those with fewer orders churn: 60% of their
762
- // events after day 14 are removed, simulating subscription
763
- // abandonment after a failed trial experience.
764
- // ═══════════════════════════════════════════════════════════════
765
- if (hasTrialSubscription) {
746
+ // HOOK 6: TRIAL CONVERSION — trial subs with <3 early orders
747
+ // drop 60% of post-day-14 events. No flag.
748
+ if (hasTrialSubscription && earlyOrderCount < 3 && chance.bool({ likelihood: 60 })) {
766
749
  const trialCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
767
-
768
- if (earlyOrderCount >= 3) {
769
- // Retained: mark events as trial_retained
770
- userEvents.forEach((event) => {
771
- event.trial_retained = true;
772
- });
773
- } else {
774
- // Churned: remove 60% of events after day 14
775
- userEvents.forEach((event) => {
776
- event.trial_retained = false;
777
- });
750
+ if (trialCutoff) {
778
751
  for (let i = userEvents.length - 1; i >= 0; i--) {
779
- const evt = userEvents[i];
780
- if (trialCutoff && dayjs(evt.time).isAfter(trialCutoff)) {
781
- if (chance.bool({ likelihood: 60 })) {
782
- userEvents.splice(i, 1);
783
- }
752
+ if (dayjs(userEvents[i].time).isAfter(trialCutoff)) {
753
+ userEvents.splice(i, 1);
784
754
  }
785
755
  }
786
756
  }
787
757
  }
788
758
 
789
- // ═══════════════════════════════════════════════════════════════
790
- // HOOK 4: RAINY WEEK SURGE - duplicate order events (everything)
791
- // Events tagged surge_pricing=true in the event hook get a 40%
792
- // chance of duplication here, creating visible demand spikes.
793
- // ═══════════════════════════════════════════════════════════════
759
+ // HOOK 4: RAINY WEEK SURGE — days 20-27, double delivery_fee on
760
+ // order placed events. Mutates existing prop. No flag.
761
+ userEvents.forEach(e => {
762
+ if (e.event === "order placed") {
763
+ const t = dayjs(e.time);
764
+ if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END)) {
765
+ e.delivery_fee = (e.delivery_fee || 5) * 2;
766
+ }
767
+ }
768
+ });
769
+
770
+ // HOOK 4 (cont): RAINY WEEK SURGE — duplicate 40% of order placed
771
+ // events that fall in the rainy window. Cloned with unique offset.
794
772
  const rainyDuplicates = [];
795
773
  userEvents.forEach((event) => {
796
- if (event.surge_pricing === true && event.event === "order placed" && chance.bool({ likelihood: 40 })) {
797
- const dup = JSON.parse(JSON.stringify(event));
798
- dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString();
799
- dup.rainy_week = true;
800
- rainyDuplicates.push(dup);
774
+ if (event.event === "order placed") {
775
+ const t = dayjs(event.time);
776
+ if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END) && chance.bool({ likelihood: 40 })) {
777
+ const dup = JSON.parse(JSON.stringify(event));
778
+ dup.time = t.add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString();
779
+ rainyDuplicates.push(dup);
780
+ }
801
781
  }
802
782
  });
803
- if (rainyDuplicates.length > 0) {
804
- userEvents.push(...rainyDuplicates);
805
- }
806
- }
807
-
808
- // ═══════════════════════════════════════════════════════════════════
809
- // HOOK 7: SUPPORT TICKET CHURN (user)
810
- // 15% of users are flagged as high-risk with elevated churn scores.
811
- // The remaining users get low churn scores. This creates a clear
812
- // segmentation opportunity for proactive retention campaigns.
813
- // ═══════════════════════════════════════════════════════════════════
814
- if (type === "user") {
815
- if (chance.bool({ likelihood: 15 })) {
816
- record.is_high_risk = true;
817
- record.churn_risk_score = chance.integer({ min: 70, max: 100 });
818
- } else {
819
- record.is_high_risk = false;
820
- record.churn_risk_score = chance.integer({ min: 0, max: 40 });
783
+ if (rainyDuplicates.length > 0) userEvents.push(...rainyDuplicates);
784
+
785
+ // HOOK 9: ORDER-COUNT MAGIC NUMBER (no flags)
786
+ // Sweet 4-8 orders → +40% on order_total. Over 9+ → drop 35% of
787
+ // order placed events (oversaturated; analyst sees inverted-U).
788
+ if (orderPlacedCount >= 4 && orderPlacedCount <= 8) {
789
+ userEvents.forEach(e => {
790
+ if (e.event === "order placed" && typeof e.order_total === "number") {
791
+ e.order_total = Math.round(e.order_total * 1.4);
792
+ }
793
+ });
794
+ } else if (orderPlacedCount >= 9) {
795
+ userEvents.forEach(e => {
796
+ if (e.event === "order placed" && typeof e.order_total === "number") {
797
+ e.order_total = Math.round(e.order_total * 0.65);
798
+ }
799
+ });
821
800
  }
822
801
  }
823
802