@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-fintech";
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
 
@@ -15,7 +15,6 @@ import * as u from "../../lib/utils/utils.js";
15
15
 
16
16
  dayjs.extend(utc);
17
17
  const chance = u.initChance(SEED);
18
-
19
18
  /** @typedef {import("../../types").Dungeon} Config */
20
19
 
21
20
  /**
@@ -48,58 +47,223 @@ const chance = u.initChance(SEED);
48
47
 
49
48
  /**
50
49
  * ===================================================================
51
- * ANALYTICS HOOKS (8 architected patterns)
50
+ * ANALYTICS HOOKS (10 hooks)
52
51
  * ===================================================================
53
52
  *
54
- * 1. PERSONAL VS BUSINESS ACCOUNTS (user hook)
55
- * 20% business (employee_count, revenue, industry), 80% personal
56
- * (age_range, life_stage). Breakdown: account_segment.
57
- * → Insights: any event by unique users, breakdown account_segment
58
- * Insights: "transaction completed" avg amount, breakdown account_segment
59
- *
60
- * 2. PAYDAY PATTERNS (event hook)
61
- * Direct deposits 3x on 1st/15th (payday: true). Transfers 2x on
62
- * days 1-3 and 15-17 (post_payday_spending: true).
63
- * → Insights: "transaction completed" avg amount, filter direct_deposit, breakdown payday
64
- * Insights: "transfer sent" avg amount, breakdown post_payday_spending
65
- *
66
- * 3. FRAUD DETECTION (everything hook)
67
- * 3% of users get a fraud burst at timeline midpoint: 3-5 rapid
68
- * high-value txns card locked → dispute → support. Tagged
69
- * fraud_sequence: true.
70
- * Insights: "transaction completed" total, filter fraud_sequence = true
71
- * Funnels: card locked → dispute filed → support contacted, filter fraud_sequence = true
72
- *
73
- * 4. LOW BALANCE CHURN (everything hook)
74
- * Users with 3+ balance checks < $15K lose 50% of events after
75
- * day 30. Surviving events tagged low_balance_churn: true.
76
- * Retention: any any, segment low_balance_churn = true vs others
77
- * Insights: any event total over time, filter low_balance_churn = true
78
- *
79
- * 5. BUDGET USERS SAVE MORE (everything hook)
80
- * Budget creators get 2x savings contributions, 1.5x investment
81
- * amounts, extra savings goals. Tagged budget_discipline: true.
82
- * Insights: "savings goal set" avg monthly_contribution, breakdown budget_discipline
83
- * → Insights: "investment made" avg amount, breakdown budget_discipline
84
- *
85
- * 6. AUTO-PAY LOYALTY (event hook)
86
- * Manual payers (auto_pay=false) miss 30% of bills (event renamed
87
- * to "bill payment missed"). Auto-pay users never miss.
88
- * Insights: "bill paid" + "bill payment missed" totals side by side
89
- * Insights: "bill paid" total, breakdown manual_payment
90
- *
91
- * 7. PREMIUM TIER VALUE (event hook)
92
- * Premium: 3x rewards, 2x investment sell returns. Plus: 1.5x
93
- * rewards. Tagged premium_reward / premium_returns.
94
- * Insights: "reward redeemed" avg value, breakdown account_tier
95
- * (expected: premium ~$30, plus ~$15, basic ~$10)
96
- * Insights: "investment made" avg amount, filter action=sell, breakdown premium_returns
97
- *
98
- * 8. MONTH-END ANXIETY (event hook)
99
- * Days >= 28: sessions 40% longer, balances 30% lower. Tagged
100
- * month_end_anxiety / month_end_check.
101
- * → Insights: "app session" avg session_duration_sec, breakdown month_end_anxiety
102
- * Insights: "balance checked" avg account_balance, breakdown month_end_check
53
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
54
+ * via behavioral cohorts, raw-prop breakdowns (date, account_tier),
55
+ * or funnel time-to-convert.
56
+ *
57
+ * 1. PERSONAL VS BUSINESS ACCOUNTS (user)
58
+ *
59
+ * PATTERN: 20% of accounts are business (employee_count, revenue,
60
+ * industry attached) and 80% are personal (age_range, life_stage).
61
+ * Account segment shapes downstream transaction sizes.
62
+ *
63
+ * HOW TO FIND IT IN MIXPANEL:
64
+ *
65
+ * Report 1: Account Segment Mix
66
+ * - Report type: Insights
67
+ * - Event: any event
68
+ * - Measure: Unique users
69
+ * - Breakdown: "account_segment"
70
+ * - Expected: ~80% personal, ~20% business
71
+ *
72
+ * Report 2: Transaction Size by Segment
73
+ * - Report type: Insights
74
+ * - Event: "transaction completed"
75
+ * - Measure: Average of "amount"
76
+ * - Breakdown: "account_segment"
77
+ * - Expected: business ~ $200+, personal ~ $50 (~4x larger)
78
+ *
79
+ * REAL-WORLD ANALOGUE: Neobanks serve both consumers and small
80
+ * businesses with the same core product, but business activity is
81
+ * meaningfully higher value per transaction.
82
+ *
83
+ * ---------------------------------------------------------------
84
+ * 2. PAYDAY PATTERNS (everything)
85
+ *
86
+ * PATTERN: Direct deposit transactions are 3x larger on the 1st and
87
+ * 15th of the month. Transfers are ~1.6x larger on the 1st-3rd and
88
+ * 15th-17th (60% likelihood × 2x boost). No flag — discover via day-of-month
89
+ * breakdown on raw amount.
90
+ *
91
+ * HOW TO FIND IT IN MIXPANEL:
92
+ *
93
+ * Report 1: Direct Deposit Size by Day of Month
94
+ * - Report type: Insights
95
+ * - Event: "transaction completed"
96
+ * - Measure: Average of "amount"
97
+ * - Filter: "transaction_type" = "direct_deposit"
98
+ * - Breakdown: day of month
99
+ * - Expected: 1st and 15th avg ~ 3x other days
100
+ *
101
+ * Report 2: Post-Payday Transfer Spending by Day of Month
102
+ * - Report type: Insights
103
+ * - Event: "transfer sent"
104
+ * - Measure: Average of "amount"
105
+ * - Breakdown: day of month
106
+ * - Expected: 1-3 and 15-17 avg ~ 1.6x other days
107
+ *
108
+ * REAL-WORLD ANALOGUE: Bi-monthly payroll cycles drive predictable
109
+ * spikes in deposit and outbound spending volume.
110
+ *
111
+ * ---------------------------------------------------------------
112
+ * 3. FRAUD DETECTION (everything)
113
+ *
114
+ * PATTERN: ~1-2% of users experience a fraud burst at the timeline
115
+ * midpoint: 3-5 rapid high-value transactions, then card locked
116
+ * (reason="suspicious_activity"), dispute filed (reason="unauthorized"),
117
+ * and support contacted (issue_type="card"). No flag — derive cohort
118
+ * by joining users who had all three event types within ~1 hour.
119
+ *
120
+ * HOW TO FIND IT IN MIXPANEL:
121
+ *
122
+ * Report 1: Fraud Cohort
123
+ * - Report type: Cohort builder
124
+ * - Filter: did "card locked" with reason="suspicious_activity"
125
+ * AND "dispute filed" with reason="unauthorized"
126
+ * within 1 hour of each other
127
+ * - Expected: ~1-2% of total users
128
+ *
129
+ * Report 2: Fraud Resolution Funnel
130
+ * - Report type: Funnels
131
+ * - Steps: "card locked" -> "dispute filed" -> "support contacted"
132
+ * - Filter: card locked.reason = "suspicious_activity"
133
+ * - Expected: high completion across all three resolution steps
134
+ *
135
+ * REAL-WORLD ANALOGUE: A small but consistent slice of accounts
136
+ * triggers fraud pipelines every cycle, generating the bulk of
137
+ * dispute and support load.
138
+ *
139
+ * ---------------------------------------------------------------
140
+ * 4. LOW BALANCE CHURN (everything)
141
+ *
142
+ * PATTERN: Users with 3+ "balance checked" events where account_balance
143
+ * < $15K lose 50% of their events after day 30. No flag — derive cohort
144
+ * by counting low-balance checks per user.
145
+ *
146
+ * HOW TO FIND IT IN MIXPANEL:
147
+ *
148
+ * Report 1: Activity by Low Balance Cohort
149
+ * - Cohort A: users with >= 3 "balance checked" where account_balance < 15000
150
+ * - Cohort B: users with < 3
151
+ * - Event: any event
152
+ * - Measure: Total per user, line chart by day
153
+ * - Expected: A growth post-d30 ~ 0.5x B's growth (suppressed)
154
+ *
155
+ * Report 2: Activity Decline Timeline
156
+ * - Report type: Insights (with cohort A above)
157
+ * - Event: any event
158
+ * - Measure: Total
159
+ * - Line chart by day
160
+ * - Expected: pre-d30/post-d30 ratio ~ 1.0x for A vs ~ 2.0x for B
161
+ *
162
+ * REAL-WORLD ANALOGUE: Customers running thin balances lose trust
163
+ * in the platform and migrate their primary banking elsewhere.
164
+ *
165
+ * ---------------------------------------------------------------
166
+ * 5. BUDGET USERS SAVE MORE (everything)
167
+ *
168
+ * PATTERN: Users with any "budget created" event get 2x savings
169
+ * contributions, 1.5x investment amounts, and extra cloned savings
170
+ * goal events. No flag — derive cohort behaviorally.
171
+ *
172
+ * HOW TO FIND IT IN MIXPANEL:
173
+ *
174
+ * Report 1: Savings Contribution by Budget Cohort
175
+ * - Cohort A: users with >= 1 "budget created" event
176
+ * - Cohort B: users with 0
177
+ * - Event: "savings goal set"
178
+ * - Measure: Average of "monthly_contribution"
179
+ * - Expected: A ~ 2x B
180
+ *
181
+ * Report 2: Investment Size by Budget Cohort
182
+ * - Cohort A vs B (as above)
183
+ * - Event: "investment made"
184
+ * - Measure: Average of "amount"
185
+ * - Expected: A ~ 1.5x B
186
+ *
187
+ * REAL-WORLD ANALOGUE: Active budget tooling correlates strongly
188
+ * with healthier savings rates and broader product adoption.
189
+ *
190
+ * ---------------------------------------------------------------
191
+ * 6. AUTO-PAY LOYALTY (event)
192
+ *
193
+ * PATTERN: Manual payers (auto_pay=false) miss 30% of their bill
194
+ * payments — those events are renamed to "bill payment missed".
195
+ * Auto-pay users never miss.
196
+ *
197
+ * HOW TO FIND IT IN MIXPANEL:
198
+ *
199
+ * Report 1: Bill Outcomes by Manual vs Auto-Pay
200
+ * - Report type: Insights
201
+ * - Events: "bill paid" and "bill payment missed"
202
+ * - Measure: Total
203
+ * - Expected: missed events appear only for manual payers, ~30% rate
204
+ *
205
+ * Report 2: Bill Completion Rate by Auto-Pay
206
+ * - Report type: Insights
207
+ * - Event: "bill paid"
208
+ * - Measure: Total
209
+ * - Breakdown: "auto_pay"
210
+ * - Expected: auto_pay=false ~ 70% completion vs auto_pay=true ~ 100%
211
+ *
212
+ * REAL-WORLD ANALOGUE: Auto-pay locks users into a frictionless
213
+ * payment cadence that virtually eliminates missed bills.
214
+ *
215
+ * ---------------------------------------------------------------
216
+ * 7. PREMIUM TIER VALUE (event)
217
+ *
218
+ * PATTERN: Premium-tier users get 3x reward values and 2x sell
219
+ * returns on investments. Plus tier gets 1.5x rewards. No flag —
220
+ * mutates raw "value"/"amount" properties; discover via account_tier breakdown.
221
+ *
222
+ * HOW TO FIND IT IN MIXPANEL:
223
+ *
224
+ * Report 1: Reward Value by Tier
225
+ * - Report type: Insights
226
+ * - Event: "reward redeemed"
227
+ * - Measure: Average of "value"
228
+ * - Breakdown: "account_tier"
229
+ * - Expected: Premium ~ $30, Plus ~ $15, Basic ~ $10
230
+ *
231
+ * Report 2: Investment Sell Amount by Tier
232
+ * - Report type: Insights
233
+ * - Event: "investment made"
234
+ * - Measure: Average of "amount"
235
+ * - Filter: "action" = "sell"
236
+ * - Breakdown: "account_tier"
237
+ * - Expected: Premium ~ 2x baseline; Basic/Plus baseline
238
+ *
239
+ * REAL-WORLD ANALOGUE: Premium subscription tiers justify their
240
+ * price by delivering visibly better cashback and investment perks.
241
+ *
242
+ * ---------------------------------------------------------------
243
+ * 8. MONTH-END ANXIETY (everything)
244
+ *
245
+ * PATTERN: On days >= 28 of the calendar month, app sessions run
246
+ * 40% longer and reported balances are 30% lower. No flag — discover
247
+ * via day-of-month breakdown.
248
+ *
249
+ * HOW TO FIND IT IN MIXPANEL:
250
+ *
251
+ * Report 1: Session Duration by Day of Month
252
+ * - Report type: Insights
253
+ * - Event: "app session"
254
+ * - Measure: Average of "session_duration_sec"
255
+ * - Breakdown: day of month
256
+ * - Expected: days >= 28 ~ 1.4x other days
257
+ *
258
+ * Report 2: Balance by Day of Month
259
+ * - Report type: Insights
260
+ * - Event: "balance checked"
261
+ * - Measure: Average of "account_balance"
262
+ * - Breakdown: day of month
263
+ * - Expected: days >= 28 ~ 0.7x other days
264
+ *
265
+ * REAL-WORLD ANALOGUE: Users obsessively check balances at month
266
+ * end as bills hit and runway tightens.
103
267
  *
104
268
  * ===================================================================
105
269
  * ADVANCED ANALYSIS IDEAS
@@ -118,30 +282,82 @@ const chance = u.initChance(SEED);
118
282
  * - By income_bracket: feature adoption by income
119
283
  * - By credit_score_range: loan approvals, tier adoption
120
284
  *
285
+ * ---------------------------------------------------------------
286
+ * 9. ONBOARDING TIME-TO-CONVERT (funnel-post)
287
+ *
288
+ * PATTERN: Premium tier users complete the Onboarding funnel 1.5x
289
+ * faster (factor 0.67); Basic users 1.33x slower (factor 1.33).
290
+ *
291
+ * HOW TO FIND IT IN MIXPANEL:
292
+ *
293
+ * Report 1: Onboarding Median Time-to-Convert by Tier
294
+ * - Funnels > "account opened" -> "app session" -> "balance checked"
295
+ * - Measure: Median time to convert
296
+ * - Breakdown: account_tier
297
+ * - Expected: premium ~ 0.67x baseline; basic ~ 1.33x
298
+ *
299
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
300
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
301
+ * show this — funnel-post adjusts gaps within funnel instances, not
302
+ * across the user's full event history.
303
+ *
304
+ * ---------------------------------------------------------------
305
+ * 10. TRANSACTION-COUNT MAGIC NUMBER (everything)
306
+ *
307
+ * PATTERN: Sweet 6-10 transactions/user → +40% on investment-made
308
+ * amount (engaged transactor compounds wealth). Over 11+ → drop 20%
309
+ * of premium-upgraded events. No flag.
310
+ *
311
+ * HOW TO FIND IT IN MIXPANEL:
312
+ *
313
+ * Report 1: Avg Investment Amount by Transaction Bucket
314
+ * - Cohort A: users with 6-10 "transaction completed"
315
+ * - Cohort B: users with 0-5
316
+ * - Event: "investment made"
317
+ * - Measure: Average of "amount"
318
+ * - Expected: A ~ 1.4x B
319
+ *
320
+ * Report 2: Premium Upgrades on Heavy Transactors
321
+ * - Cohort C: users with >= 11 "transaction completed"
322
+ * - Cohort A: users with 6-10
323
+ * - Event: "premium upgraded"
324
+ * - Measure: Total per user
325
+ * - Expected: C ~ 20% fewer upgrades per user
326
+ *
327
+ * REAL-WORLD ANALOGUE: Engaged transactors invest more; over-active
328
+ * already extract value without upgrading.
329
+ *
121
330
  * ===================================================================
122
331
  * EXPECTED METRICS SUMMARY
123
332
  * ===================================================================
124
333
  *
125
- * Hook | Metric | Baseline | Effect | Ratio
126
- * ----------------------|-----------------------|----------|--------|------
127
- * Personal vs Business | Avg transaction amt | $50 | $200+ | ~4x
128
- * Payday Patterns | Deposit amount | $50 | $100 | 2x
129
- * Fraud Detection | Users affected | 0% | 3% | --
130
- * Low Balance Churn | D30+ event count | 100% | 50% | 0.5x
131
- * Budget Discipline | Monthly contribution | $200 | $400 | 2x
132
- * Auto-Pay Loyalty | Bill completion rate | 100% | 70% | 0.7x
133
- * Premium Tier Value | Reward value | $10 | $30 | 3x
134
- * Month-End Anxiety | Session duration | 60s | 84s | 1.4x
334
+ * Hook | Metric | Baseline | Effect | Ratio
335
+ * ----------------------|-----------------------|----------|-----------|------
336
+ * Personal vs Business | Avg transaction amt | $50 | $200+ | ~ 4x
337
+ * Payday Patterns | Deposit amt 1st/15th | 1x | 3x | 3x
338
+ * Fraud Detection | Users affected | 0% | 3% | --
339
+ * Low Balance Churn | D30+ events | 1x | 0.5x | -50%
340
+ * Budget Discipline | Savings contribution | 1x | 2x | 2x
341
+ * Auto-Pay Loyalty | Bill completion rate | 100% | 70% | -30%
342
+ * Premium Tier Value | Reward value (Premium)| 1x | 3x | 3x
343
+ * Month-End Anxiety | Session duration d28+ | 1x | 1.4x | 1.4x
344
+ * Onboarding T2C | median min by tier | 1x | 0.67/1.33x| 2x range
345
+ * Txn-Count Magic Num | sweet investment amt | 1x | 1.4x | 1.4x
346
+ * Txn-Count Magic Num | over premium upgrades | 1x | 0.8x | -20%
135
347
  */
136
348
 
137
349
  /** @type {Config} */
138
350
  const config = {
351
+ version: 2,
139
352
  token,
140
353
  seed: SEED,
141
- numDays: num_days,
354
+ datasetStart: "2026-01-01T00:00:00Z",
355
+ datasetEnd: "2026-05-01T23:59:59Z",
356
+ // numDays: num_days,
142
357
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
143
358
  numUsers: num_users,
144
- hasAnonIds: false,
359
+ hasAnonIds: true,
360
+ avgDevicePerUser: 2,
145
361
  hasSessionIds: true,
146
362
  format: "json",
147
363
  gzip: true,
@@ -239,6 +455,7 @@ const config = {
239
455
  event: "account opened",
240
456
  weight: 1,
241
457
  isFirstEvent: true,
458
+ isAuthEvent: true,
242
459
  properties: {
243
460
  "account_type": ["personal", "business", "personal"],
244
461
  "signup_channel": ["app", "web", "referral", "branch"],
@@ -250,7 +467,6 @@ const config = {
250
467
  properties: {
251
468
  "session_duration_sec": u.weighNumRange(10, 600, 0.3, 60),
252
469
  "pages_viewed": u.weighNumRange(1, 15, 0.5, 3),
253
- "month_end_anxiety": [false],
254
470
  }
255
471
  },
256
472
  {
@@ -259,7 +475,6 @@ const config = {
259
475
  properties: {
260
476
  "account_balance": u.weighNumRange(0, 50000, 0.8, 2500),
261
477
  "account_type": ["checking", "savings", "investment"],
262
- "month_end_check": [false],
263
478
  }
264
479
  },
265
480
  {
@@ -270,8 +485,6 @@ const config = {
270
485
  "amount": u.weighNumRange(1, 5000, 0.3, 50),
271
486
  "merchant_category": ["grocery", "restaurant", "gas", "retail", "online", "subscription", "utilities"],
272
487
  "payment_method": ["debit", "credit", "contactless", "online"],
273
- "payday": [false],
274
- "fraud_sequence": [false],
275
488
  }
276
489
  },
277
490
  {
@@ -281,7 +494,6 @@ const config = {
281
494
  "transfer_type": ["internal", "external", "p2p", "wire"],
282
495
  "amount": u.weighNumRange(10, 10000, 0.3, 200),
283
496
  "recipient_type": ["friend", "family", "business", "self"],
284
- "post_payday_spending": [false],
285
497
  }
286
498
  },
287
499
  {
@@ -291,8 +503,15 @@ const config = {
291
503
  "bill_type": ["rent", "utilities", "phone", "insurance", "subscription", "loan_payment"],
292
504
  "amount": u.weighNumRange(20, 3000, 0.5, 150),
293
505
  "auto_pay": [false, false, false, true, true],
294
- "missed_payment": [false],
295
- "manual_payment": [false],
506
+ }
507
+ },
508
+ {
509
+ event: "bill payment missed",
510
+ weight: 1,
511
+ isStrictEvent: true, // hook-only: created by Hook 6 from manual bill-paid events
512
+ properties: {
513
+ "bill_type": ["rent", "utilities", "phone", "insurance", "subscription", "loan_payment"],
514
+ "amount": u.weighNumRange(20, 3000, 0.5, 150),
296
515
  }
297
516
  },
298
517
  {
@@ -318,7 +537,6 @@ const config = {
318
537
  "goal_type": ["emergency", "vacation", "car", "home", "education", "retirement"],
319
538
  "target_amount": u.weighNumRange(500, 50000, 0.3, 5000),
320
539
  "monthly_contribution": u.weighNumRange(25, 2000, 0.5, 200),
321
- "budget_discipline": [false],
322
540
  }
323
541
  },
324
542
  {
@@ -328,25 +546,21 @@ const config = {
328
546
  "investment_type": ["stocks", "etf", "crypto", "bonds", "mutual_fund"],
329
547
  "amount": u.weighNumRange(10, 10000, 0.3, 250),
330
548
  "action": ["buy", "sell", "buy"],
331
- "premium_returns": [false],
332
- "budget_discipline": [false],
333
549
  }
334
550
  },
335
551
  {
336
552
  event: "card locked",
337
- weight: 1,
553
+ weight: 2,
338
554
  properties: {
339
555
  "reason": ["lost", "stolen", "suspicious_activity", "travel"],
340
- "fraud_sequence": [false],
341
556
  }
342
557
  },
343
558
  {
344
559
  event: "dispute filed",
345
- weight: 1,
560
+ weight: 2,
346
561
  properties: {
347
562
  "dispute_amount": u.weighNumRange(10, 2000, 0.5, 100),
348
563
  "reason": ["unauthorized", "duplicate", "not_received", "damaged", "wrong_amount"],
349
- "fraud_sequence": [false],
350
564
  }
351
565
  },
352
566
  {
@@ -382,7 +596,6 @@ const config = {
382
596
  "channel": ["chat", "phone", "email", "in_app"],
383
597
  "issue_type": ["transaction", "account", "card", "transfer", "technical"],
384
598
  "resolved": [false, true, true, true, true],
385
- "fraud_sequence": [false],
386
599
  }
387
600
  },
388
601
  {
@@ -399,7 +612,6 @@ const config = {
399
612
  properties: {
400
613
  "reward_type": ["cashback", "points", "discount", "partner_offer"],
401
614
  "value": u.weighNumRange(1, 100, 0.5, 10),
402
- "premium_reward": [false],
403
615
  }
404
616
  }
405
617
  ],
@@ -407,13 +619,11 @@ const config = {
407
619
  superProps: {
408
620
  account_tier: ["basic", "basic", "basic", "plus", "plus", "premium"],
409
621
  Platform: ["ios", "android", "web"],
410
- low_balance_churn: [false],
411
622
  },
412
623
 
413
624
  userProps: {
414
625
  account_tier: ["basic", "basic", "basic", "plus", "plus", "premium"],
415
626
  Platform: ["ios", "android", "web"],
416
- low_balance_churn: [false],
417
627
  "credit_score_range": ["300-579", "580-669", "670-739", "740-799", "800-850"],
418
628
  "income_bracket": ["under_30k", "30k_50k", "50k_75k", "75k_100k", "100k_150k", "over_150k"],
419
629
  "account_age_months": u.weighNumRange(1, 60, 0.5, 12),
@@ -448,24 +658,16 @@ const config = {
448
658
  * This hook function creates 8 deliberate patterns in the data:
449
659
  *
450
660
  * 1. PERSONAL VS BUSINESS: Business accounts get employee_count, revenue; personal get age_range, life_stage
451
- * 2. PAYDAY PATTERNS: Transactions spike on 1st/15th with bigger deposits and post-payday spending
661
+ * 2. PAYDAY PATTERNS: Transactions spike on 1st/15th with bigger deposits and post-payday spending (everything hook — runs after sessionization)
452
662
  * 3. FRAUD DETECTION: 3% of users experience a fraud burst (rapid high-value txns -> card lock -> dispute -> support)
453
663
  * 4. LOW BALANCE CHURN: Users with chronic low balances (<$15K) lose 50% of activity after day 30
454
664
  * 5. BUDGET DISCIPLINE: Budget creators save 2x more and invest 1.5x more
455
665
  * 6. AUTO-PAY LOYALTY: Auto-pay users never miss bills; manual payers miss 30%
456
666
  * 7. PREMIUM TIER VALUE: Premium users get 3x rewards; Plus users get 1.5x; Premium investors get 2x returns
457
- * 8. MONTH-END ANXIETY: Last 3 days of month see 40% longer sessions and 30% lower balances
667
+ * 8. MONTH-END ANXIETY: Last 3 days of month see 40% longer sessions and 30% lower balances (everything hook — runs after sessionization)
458
668
  */
459
669
  hook: function (record, type, meta) {
460
- const NOW = dayjs();
461
- const DATASET_START = NOW.subtract(num_days, "days");
462
-
463
- // ===============================================================
464
- // Hook #1: PERSONAL VS BUSINESS ACCOUNTS (user)
465
- // 20% of users are randomly tagged as business accounts with
466
- // employee_count, annual_revenue, and industry. The other 80% get
467
- // personal attributes: age_range and life_stage.
468
- // ===============================================================
670
+ // HOOK 1: PERSONAL VS BUSINESS ACCOUNTS (user) — role-based attrs.
469
671
  if (type === "user") {
470
672
  const isBusiness = chance.bool({ likelihood: 20 });
471
673
  if (isBusiness) {
@@ -480,286 +682,215 @@ const config = {
480
682
  }
481
683
  }
482
684
 
483
- // ===============================================================
484
- // Hook #2: PAYDAY PATTERNS (event)
485
- // On the 1st and 15th, direct deposit transactions are 2x bigger
486
- // and tagged with payday: true. On days 1-3 and 15-17, transfers
487
- // have a 40% chance of 1.5x boost (post-payday spending).
488
- // ===============================================================
685
+ // HOOK 6: AUTO-PAY LOYALTY (event) — manual bill-paid events have
686
+ // 30% chance of becoming "bill payment missed". Mutates event name.
489
687
  if (type === "event") {
490
- const EVENT_TIME = dayjs(record.time);
491
- const dayOfMonth = EVENT_TIME.date();
492
-
493
- // Payday: 1st and 15th
494
- if (record.event === "transaction completed" && record.transaction_type === "direct_deposit") {
495
- if (dayOfMonth === 1 || dayOfMonth === 15) {
496
- record.amount = Math.floor((record.amount || 50) * 3);
497
- record.payday = true;
498
- } else {
499
- record.payday = false;
500
- }
501
- }
502
-
503
- // Post-payday spending: days 1-3 and 15-17
504
- if (record.event === "transfer sent") {
505
- const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
506
- if (isPaydayWindow && chance.bool({ likelihood: 60 })) {
507
- record.amount = Math.floor((record.amount || 200) * 2.0);
508
- record.post_payday_spending = true;
509
- } else {
510
- record.post_payday_spending = false;
511
- }
688
+ if (record.event === "bill paid" && record.auto_pay !== true && chance.bool({ likelihood: 30 })) {
689
+ record.event = "bill payment missed";
512
690
  }
691
+ }
513
692
 
514
- // ===============================================================
515
- // Hook #6: AUTO-PAY LOYALTY (event)
516
- // Users with auto_pay=false on bill paid events have a 30% chance
517
- // of the event being dropped entirely (missed payment). Surviving
518
- // manual payments get tagged. Auto-pay users always succeed.
519
- // ===============================================================
520
- if (record.event === "bill paid") {
521
- if (record.auto_pay === false || record.auto_pay === undefined) {
522
- if (chance.bool({ likelihood: 30 })) {
523
- record.event = "bill payment missed";
524
- record.missed_payment = true;
525
- record.manual_payment = false;
526
- } else {
527
- record.missed_payment = false;
528
- record.manual_payment = true;
693
+ // HOOK 9 (T2C): ONBOARDING TIME-TO-CONVERT (funnel-post)
694
+ // Premium tier completes Onboarding funnel 1.5x faster (factor 0.67);
695
+ // Basic users 1.33x slower (factor 1.33).
696
+ if (type === "funnel-post") {
697
+ const segment = meta?.profile?.account_tier;
698
+ if (Array.isArray(record) && record.length > 1) {
699
+ const factor = (
700
+ segment === "premium" ? 0.67 :
701
+ segment === "basic" ? 1.33 :
702
+ 1.0
703
+ );
704
+ if (factor !== 1.0) {
705
+ for (let i = 1; i < record.length; i++) {
706
+ const prev = dayjs(record[i - 1].time);
707
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
708
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
529
709
  }
530
- } else {
531
- record.missed_payment = false;
532
- record.manual_payment = false;
533
- }
534
- }
535
-
536
- // ===============================================================
537
- // Hook #8: MONTH-END ANXIETY (event)
538
- // Last 3 days of month (day >= 28) see 40% longer app sessions
539
- // and 30% lower balances when checked.
540
- // ===============================================================
541
- if (record.event === "app session") {
542
- if (dayOfMonth >= 28) {
543
- record.session_duration_sec = Math.floor((record.session_duration_sec || 60) * 1.4);
544
- record.month_end_anxiety = true;
545
- } else {
546
- record.month_end_anxiety = false;
547
- }
548
- }
549
-
550
- if (record.event === "balance checked") {
551
- if (dayOfMonth >= 28) {
552
- record.account_balance = Math.floor((record.account_balance || 2500) * 0.7);
553
- record.month_end_check = true;
554
- } else {
555
- record.month_end_check = false;
556
710
  }
557
711
  }
558
712
  }
559
713
 
560
- // ===============================================================
561
- // Hook #3, #4, #5: EVERYTHING - Complex behavioral patterns
562
- // These hooks operate on the full event stream per user for
563
- // fraud injection, low-balance churn, and budget discipline.
564
- // ===============================================================
565
714
  if (type === "everything") {
715
+ const datasetStart = dayjs.unix(meta.datasetStart);
566
716
  const userEvents = record;
567
-
568
- // Stamp superProps from profile for consistency
569
717
  const profile = meta.profile;
718
+
570
719
  userEvents.forEach(e => {
571
720
  e.account_tier = profile.account_tier;
572
721
  e.Platform = profile.Platform;
573
- e.low_balance_churn = profile.low_balance_churn;
574
722
  });
575
723
 
576
- // -----------------------------------------------------------
577
- // Hook #7: PREMIUM TIER VALUE (moved from event hook)
578
- // Uses profile.account_tier which is authoritative here.
579
- // Premium: 3x rewards, 2x investment sell returns.
580
- // Plus: 1.5x rewards.
581
- // -----------------------------------------------------------
582
- const tier = profile.account_tier;
583
- userEvents.forEach(e => {
584
- if (e.event === "reward redeemed") {
585
- if (tier === "premium") {
586
- e.value = Math.floor((e.value || 10) * 3);
587
- e.premium_reward = true;
588
- } else if (tier === "plus") {
589
- e.value = Math.floor((e.value || 10) * 1.5);
590
- e.premium_reward = false;
591
- } else {
592
- e.premium_reward = false;
724
+ // HOOK 1B: PERSONAL VS BUSINESS — business segment txns 4x larger
725
+ // (per Report 2 in JSDoc: business ~ $200, personal ~ $50).
726
+ if (profile.account_segment === "business") {
727
+ userEvents.forEach(e => {
728
+ if (e.event === "transaction completed" && typeof e.amount === "number") {
729
+ e.amount = Math.floor(e.amount * 4);
593
730
  }
594
- }
731
+ });
732
+ }
595
733
 
596
- if (e.event === "investment made" && e.action === "sell") {
597
- if (tier === "premium") {
598
- e.amount = Math.floor((e.amount || 250) * 2);
599
- e.premium_returns = true;
600
- } else {
601
- e.premium_returns = false;
734
+ // HOOK 2: PAYDAY PATTERNS 1st & 15th: direct_deposit amount 3x.
735
+ // Days 1-3 and 15-17: 60% of transfers get amount 2x. No flag.
736
+ for (const e of userEvents) {
737
+ const dayOfMonth = new Date(e.time).getUTCDate();
738
+ if (e.event === "transaction completed" && e.transaction_type === "direct_deposit") {
739
+ if (dayOfMonth === 1 || dayOfMonth === 15) {
740
+ e.amount = Math.floor((e.amount || 50) * 3);
602
741
  }
603
742
  }
604
- });
605
-
606
- const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
607
-
608
- // -----------------------------------------------------------
609
- // Hook #3: FRAUD DETECTION PATTERN
610
- // 3% of users experience a fraud event sequence: a burst of
611
- // 3-5 rapid high-value transactions within 1 hour, followed by
612
- // card locked, dispute filed, and support contacted events.
613
- // All injected events are tagged with fraud_sequence: true.
614
- // -----------------------------------------------------------
615
- if (chance.bool({ likelihood: 3 })) {
616
- // Find the midpoint of the user's timeline
617
- if (userEvents.length >= 2) {
618
- const midIdx = Math.floor(userEvents.length / 2);
619
- const midEvent = userEvents[midIdx];
620
- const midTime = dayjs(midEvent.time);
621
- const distinctId = midEvent.user_id;
622
-
623
- // Inject 3-5 rapid high-value transactions
624
- const burstCount = chance.integer({ min: 3, max: 5 });
625
- const fraudEvents = [];
626
-
627
- // Find template events for cloning
628
- const txnTemplate = userEvents.find(e => e.event === "transaction completed");
629
- const cardTemplate = userEvents.find(e => e.event === "card locked");
630
- const disputeTemplate = userEvents.find(e => e.event === "dispute filed");
631
- const supportTemplate = userEvents.find(e => e.event === "support contacted");
632
-
633
- for (let i = 0; i < burstCount; i++) {
634
- if (txnTemplate) {
635
- fraudEvents.push({
636
- ...txnTemplate,
637
- time: midTime.add(i * 10, "minutes").toISOString(),
638
- user_id: distinctId,
639
- transaction_type: "purchase",
640
- amount: chance.integer({ min: 500, max: 3000 }),
641
- merchant_category: chance.pickone(["online", "retail"]),
642
- payment_method: "credit",
643
- fraud_sequence: true,
644
- });
645
- }
743
+ if (e.event === "transfer sent") {
744
+ const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
745
+ if (isPaydayWindow && chance.bool({ likelihood: 60 })) {
746
+ e.amount = Math.floor((e.amount || 200) * 2.0);
646
747
  }
748
+ }
749
+ }
647
750
 
648
- // Card locked after the burst
649
- if (cardTemplate) {
650
- fraudEvents.push({
651
- ...cardTemplate,
652
- time: midTime.add(burstCount * 10 + 5, "minutes").toISOString(),
653
- user_id: distinctId,
654
- reason: "suspicious_activity",
655
- fraud_sequence: true,
656
- });
751
+ // HOOK 8: MONTH-END ANXIETY — days >= 28: app_session duration
752
+ // 1.4x; balance_checked account_balance 0.7x. Mutates raw props.
753
+ for (const e of userEvents) {
754
+ const dayOfMonth = new Date(e.time).getUTCDate();
755
+ if (dayOfMonth >= 28) {
756
+ if (e.event === "app session") {
757
+ e.session_duration_sec = Math.floor((e.session_duration_sec || 60) * 1.4);
657
758
  }
658
-
659
- // Dispute filed shortly after
660
- if (disputeTemplate) {
661
- fraudEvents.push({
662
- ...disputeTemplate,
663
- time: midTime.add(burstCount * 10 + 30, "minutes").toISOString(),
664
- user_id: distinctId,
665
- dispute_amount: chance.integer({ min: 500, max: 3000 }),
666
- reason: "unauthorized",
667
- fraud_sequence: true,
668
- });
759
+ if (e.event === "balance checked") {
760
+ e.account_balance = Math.floor((e.account_balance || 2500) * 0.7);
669
761
  }
762
+ }
763
+ }
670
764
 
671
- // Support contacted
672
- if (supportTemplate) {
765
+ // HOOK 7: PREMIUM TIER VALUE — Premium 3x reward value + 2x
766
+ // investment-sell amount; Plus 1.5x reward value. Reads tier
767
+ // from profile. No flag.
768
+ const tier = profile.account_tier;
769
+ userEvents.forEach(e => {
770
+ if (e.event === "reward redeemed") {
771
+ if (tier === "premium") e.value = Math.floor((e.value || 10) * 3);
772
+ else if (tier === "plus") e.value = Math.floor((e.value || 10) * 1.5);
773
+ }
774
+ if (e.event === "investment made" && e.action === "sell" && tier === "premium") {
775
+ e.amount = Math.floor((e.amount || 250) * 2);
776
+ }
777
+ });
778
+
779
+ // HOOK 3: FRAUD DETECTION — 3% of users get fraud burst
780
+ // (3-5 rapid high-value transactions + card locked + dispute +
781
+ // support contacted) at timeline midpoint. No flag — discover
782
+ // via cohort builder on users with card-locked + dispute-filed.
783
+ if (chance.bool({ likelihood: 15 }) && userEvents.length >= 2) {
784
+ const midIdx = Math.floor(userEvents.length / 2);
785
+ const midEvent = userEvents[midIdx];
786
+ const midTime = dayjs(midEvent.time);
787
+ const distinctId = midEvent.user_id;
788
+ const burstCount = chance.integer({ min: 3, max: 5 });
789
+ const fraudEvents = [];
790
+ const txnTemplate = userEvents.find(e => e.event === "transaction completed");
791
+ const cardTemplate = userEvents.find(e => e.event === "card locked");
792
+ const disputeTemplate = userEvents.find(e => e.event === "dispute filed");
793
+ const supportTemplate = userEvents.find(e => e.event === "support contacted");
794
+
795
+ for (let i = 0; i < burstCount; i++) {
796
+ if (txnTemplate) {
673
797
  fraudEvents.push({
674
- ...supportTemplate,
675
- time: midTime.add(burstCount * 10 + 45, "minutes").toISOString(),
798
+ ...txnTemplate,
799
+ time: midTime.add(i * 10, "minutes").toISOString(),
676
800
  user_id: distinctId,
677
- channel: "phone",
678
- issue_type: "card",
679
- resolved: true,
680
- fraud_sequence: true,
801
+ transaction_type: "purchase",
802
+ amount: chance.integer({ min: 500, max: 3000 }),
803
+ merchant_category: chance.pickone(["online", "retail"]),
804
+ payment_method: "credit",
681
805
  });
682
806
  }
683
-
684
- // Splice all fraud events into the user's timeline
685
- userEvents.splice(midIdx + 1, 0, ...fraudEvents);
686
807
  }
808
+ if (cardTemplate) fraudEvents.push({
809
+ ...cardTemplate,
810
+ time: midTime.add(burstCount * 10 + 5, "minutes").toISOString(),
811
+ user_id: distinctId,
812
+ reason: "suspicious_activity",
813
+ });
814
+ if (disputeTemplate) fraudEvents.push({
815
+ ...disputeTemplate,
816
+ time: midTime.add(burstCount * 10 + 30, "minutes").toISOString(),
817
+ user_id: distinctId,
818
+ dispute_amount: chance.integer({ min: 500, max: 3000 }),
819
+ reason: "unauthorized",
820
+ });
821
+ if (supportTemplate) fraudEvents.push({
822
+ ...supportTemplate,
823
+ time: midTime.add(burstCount * 10 + 45, "minutes").toISOString(),
824
+ user_id: distinctId,
825
+ channel: "phone",
826
+ issue_type: "card",
827
+ resolved: true,
828
+ });
829
+ userEvents.splice(midIdx + 1, 0, ...fraudEvents);
687
830
  }
688
831
 
689
- // -----------------------------------------------------------
690
- // Hook #4: LOW BALANCE CHURN
691
- // Users who have 3+ balance checks showing < $15,000 are
692
- // "struggling". After day 30, 50% of their events are removed
693
- // and remaining events are tagged low_balance_churn: true.
694
- // -----------------------------------------------------------
695
- let lowBalanceChecks = 0;
696
- userEvents.forEach((event) => {
697
- if (event.event === "balance checked" && (event.account_balance || 0) < 15000) {
698
- lowBalanceChecks++;
699
- }
700
- });
701
-
832
+ // HOOK 4: LOW BALANCE CHURN — users with 3+ balance checks
833
+ // under $15K lose 50% of post-day-30 events. No flag.
834
+ const lowBalanceChecks = userEvents.filter(e =>
835
+ e.event === "balance checked" && (e.account_balance || 0) < 15000
836
+ ).length;
702
837
  if (lowBalanceChecks >= 3) {
703
- const day30 = DATASET_START.add(30, "days");
838
+ const day30 = datasetStart.add(30, "days");
704
839
  for (let i = userEvents.length - 1; i >= 0; i--) {
705
- const evt = userEvents[i];
706
- if (dayjs(evt.time).isAfter(day30)) {
707
- if (chance.bool({ likelihood: 50 })) {
708
- userEvents.splice(i, 1);
709
- } else {
710
- evt.low_balance_churn = true;
711
- }
840
+ if (dayjs(userEvents[i].time).isAfter(day30) && chance.bool({ likelihood: 50 })) {
841
+ userEvents.splice(i, 1);
712
842
  }
713
843
  }
714
844
  }
715
845
 
716
- // -----------------------------------------------------------
717
- // Hook #5: BUDGET USERS SAVE MORE
718
- // Users who created any budget have 2x savings contributions,
719
- // 1.5x investment amounts, and extra savings goal events
720
- // spliced into their timeline. Tagged budget_discipline: true.
721
- // -----------------------------------------------------------
722
- let hasBudget = false;
723
- userEvents.forEach((event) => {
724
- if (event.event === "budget created") {
725
- hasBudget = true;
726
- }
727
- });
728
-
846
+ // HOOK 5: BUDGET DISCIPLINE — users with any budget-created event
847
+ // get savings 2x, investment amounts 1.5x, and extra cloned
848
+ // savings-goal events. No flag.
849
+ const hasBudget = userEvents.some(e => e.event === "budget created");
729
850
  if (hasBudget) {
730
851
  userEvents.forEach((event, idx) => {
731
852
  const eventTime = dayjs(event.time);
732
-
733
- // Double savings contributions
734
853
  if (event.event === "savings goal set") {
735
854
  event.monthly_contribution = Math.floor((event.monthly_contribution || 200) * 2);
736
- event.budget_discipline = true;
737
855
  }
738
-
739
- // 1.5x investment amounts
740
856
  if (event.event === "investment made") {
741
857
  event.amount = Math.floor((event.amount || 250) * 1.5);
742
- event.budget_discipline = true;
743
858
  }
744
-
745
- // Splice extra savings goal events (budget-conscious users set more goals)
746
859
  if (event.event === "budget created" && chance.bool({ likelihood: 50 })) {
747
860
  const savingsTemplate = userEvents.find(e => e.event === "savings goal set");
748
861
  if (savingsTemplate) {
749
- const extraGoal = {
862
+ userEvents.splice(idx + 1, 0, {
750
863
  ...savingsTemplate,
751
864
  time: eventTime.add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
752
865
  user_id: event.user_id,
753
866
  goal_type: chance.pickone(["emergency", "vacation", "car", "home"]),
754
867
  target_amount: chance.integer({ min: 1000, max: 20000 }),
755
868
  monthly_contribution: chance.integer({ min: 100, max: 800 }),
756
- budget_discipline: true,
757
- };
758
- userEvents.splice(idx + 1, 0, extraGoal);
869
+ });
759
870
  }
760
871
  }
761
872
  });
762
873
  }
874
+
875
+ // HOOK 10: TRANSACTION-COUNT MAGIC NUMBER (no flags)
876
+ // Sweet 6-10 transactions/user → +40% on investment_made amount
877
+ // (engaged transactor compounds wealth). Over 11+ → drop 20% of
878
+ // premium-upgraded events (already engaged; less upgrade pressure).
879
+ const txnCount = userEvents.filter(e => e.event === "transaction completed").length;
880
+ if (txnCount >= 6 && txnCount <= 10) {
881
+ userEvents.forEach(e => {
882
+ if (e.event === "investment made" && typeof e.amount === "number") {
883
+ e.amount = Math.round(e.amount * 1.4);
884
+ }
885
+ });
886
+ } else if (txnCount >= 11) {
887
+ // Drop 20% of premium-upgraded events for heavy transactors
888
+ for (let i = userEvents.length - 1; i >= 0; i--) {
889
+ if (userEvents[i].event === "premium upgraded" && chance.bool({ likelihood: 20 })) {
890
+ userEvents.splice(i, 1);
891
+ }
892
+ }
893
+ }
763
894
  }
764
895
 
765
896
  return record;