@ak--47/dungeon-master 1.3.0 → 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 (74) hide show
  1. package/dungeons/technical/array-of-object-lookup.js +0 -2
  2. package/dungeons/technical/simple.js +3 -4
  3. package/dungeons/technical/simplest-schema.json +5 -0
  4. package/dungeons/technical/text-generation.js +1 -1
  5. package/dungeons/vertical/ai-platform.js +192 -133
  6. package/dungeons/vertical/community.js +64 -34
  7. package/dungeons/vertical/crypto.js +333 -224
  8. package/dungeons/vertical/dating.js +228 -282
  9. package/dungeons/vertical/devtools.js +164 -62
  10. package/dungeons/vertical/ecommerce.js +240 -91
  11. package/dungeons/vertical/education.js +328 -310
  12. package/dungeons/vertical/fintech.js +437 -311
  13. package/dungeons/vertical/fitness.js +141 -58
  14. package/dungeons/vertical/food-delivery.js +298 -318
  15. package/dungeons/vertical/gaming.js +378 -231
  16. package/dungeons/vertical/healthcare.js +140 -60
  17. package/dungeons/vertical/insurance-application.js +168 -73
  18. package/dungeons/vertical/logistics.js +95 -6
  19. package/dungeons/vertical/marketplace.js +137 -51
  20. package/dungeons/vertical/media.js +241 -386
  21. package/dungeons/vertical/real-estate.js +322 -315
  22. package/dungeons/vertical/sass.js +253 -263
  23. package/dungeons/vertical/social.js +262 -206
  24. package/dungeons/vertical/travel.js +59 -16
  25. package/index.js +17 -17
  26. package/lib/core/config-validator.js +93 -21
  27. package/lib/core/context.js +10 -24
  28. package/lib/core/storage.js +6 -1
  29. package/lib/generators/events.js +11 -14
  30. package/lib/generators/funnels.js +12 -4
  31. package/lib/generators/mirror.js +3 -2
  32. package/lib/generators/product-names.js +1 -1
  33. package/lib/generators/scd.js +2 -1
  34. package/lib/generators/text.js +1 -1
  35. package/lib/orchestrators/user-loop.js +47 -47
  36. package/lib/utils/utils.js +71 -39
  37. package/package.json +1 -1
  38. package/scripts/smoke-test-all.mjs +162 -0
  39. package/scripts/verify-runner.mjs +72 -24
  40. package/types.d.ts +38 -15
  41. package/dungeons/technical/ad-spend-schema.json +0 -128
  42. package/dungeons/technical/anonymous-users-schema.json +0 -92
  43. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  44. package/dungeons/technical/experiments-schema.json +0 -203
  45. package/dungeons/technical/foobar-schema.json +0 -362
  46. package/dungeons/technical/group-analytics-schema.json +0 -241
  47. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  48. package/dungeons/technical/nested-objects-schema.json +0 -145
  49. package/dungeons/technical/retention-cadence-schema.json +0 -37
  50. package/dungeons/technical/sanity-schema.json +0 -185
  51. package/dungeons/technical/scale-test-schema.json +0 -70
  52. package/dungeons/technical/scd-schema.json +0 -467
  53. package/dungeons/technical/simple-schema.json +0 -362
  54. package/dungeons/technical/text-generation-schema.json +0 -1062
  55. package/dungeons/vertical/ai-platform-schema.json +0 -617
  56. package/dungeons/vertical/community-schema.json +0 -579
  57. package/dungeons/vertical/crypto-schema.json +0 -546
  58. package/dungeons/vertical/dating-schema.json +0 -401
  59. package/dungeons/vertical/devtools-schema.json +0 -601
  60. package/dungeons/vertical/ecommerce-schema.json +0 -604
  61. package/dungeons/vertical/education-schema.json +0 -5686
  62. package/dungeons/vertical/fintech-schema.json +0 -630
  63. package/dungeons/vertical/fitness-schema.json +0 -530
  64. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  65. package/dungeons/vertical/gaming-schema.json +0 -2703
  66. package/dungeons/vertical/healthcare-schema.json +0 -549
  67. package/dungeons/vertical/insurance-application-schema.json +0 -485
  68. package/dungeons/vertical/logistics-schema.json +0 -574
  69. package/dungeons/vertical/marketplace-schema.json +0 -533
  70. package/dungeons/vertical/media-schema.json +0 -4749
  71. package/dungeons/vertical/real-estate-schema.json +0 -527
  72. package/dungeons/vertical/sass-schema.json +0 -3128
  73. package/dungeons/vertical/social-schema.json +0 -620
  74. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,8 +1,8 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "meetcute";
3
3
  const num_days = 120;
4
- const num_users = 8_000;
5
- const avg_events_per_user_per_day = 0.75;
4
+ const num_users = 30_000;
5
+ const avg_events_per_user_per_day = 1.5;
6
6
  let token = "your-mixpanel-token";
7
7
 
8
8
  // ── env overrides ──
@@ -16,9 +16,6 @@ import * as v from "ak-tools";
16
16
 
17
17
  dayjs.extend(utc);
18
18
  const chance = u.initChance(SEED);
19
- const NOW = dayjs();
20
- const DATASET_START = NOW.subtract(num_days, "days");
21
-
22
19
  /** @typedef {import("../../types").Dungeon} Config */
23
20
 
24
21
  /*
@@ -51,180 +48,178 @@ const DATASET_START = NOW.subtract(num_days, "days");
51
48
 
52
49
  /*
53
50
  * =====================================================================================
54
- * ANALYTICS HOOKS (8 hooks)
51
+ * ANALYTICS HOOKS (9 hooks)
55
52
  * =====================================================================================
56
53
  *
54
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
55
+ * behavioral cohorts, raw-prop breakdowns, or funnel analysis.
56
+ *
57
57
  * -------------------------------------------------------------------------------------
58
- * 1. PHOTO UPLOAD CONVERSION (everything hook)
58
+ * 1. PHOTO MAGIC NUMBER (everything)
59
59
  * -------------------------------------------------------------------------------------
60
- * PATTERN: Users with 4+ "photo uploaded" events get 5x more "match received"
61
- * events injected (cloned from existing matches). complete_profile=true on
62
- * those match events. Complete profiles are magnets.
60
+ * PATTERN: Sweet 2-5 photos uploaded 2-4 extra cloned match-received events
61
+ * per existing match. Over 6+ photos match_score drops 35% on match-received
62
+ * events (over-curated profile reads as fake; quality matches don't trust it).
63
+ * No flag.
63
64
  *
64
65
  * HOW TO FIND IT IN MIXPANEL:
65
- * Report 1: Match Volume by Profile Completeness
66
- * - Insights > "match received" > Total per user > Breakdown: "complete_profile"
67
- * - Expected: complete_profile=true users have ~5x more matches
66
+ * Report 1: Matches per User by Photo-Count Bucket
67
+ * - Cohort A: users with 2-5 "photo uploaded"
68
+ * - Cohort B: users with 0-1
69
+ * - Event: "match received" → Total per user
70
+ * - Expected: A ~ 2-3x B
68
71
  *
69
- * Report 2: Photo Count Correlation
70
- * - Insights > "photo uploaded" > Uniques > Compare to "match received" > Uniques
71
- * - Expected: strong positive correlation between photo uploads and matches
72
+ * Report 2: Avg match_score on Heavy Photo Uploaders
73
+ * - Cohort C: users with >= 6 "photo uploaded"
74
+ * - Cohort A: users with 2-5
75
+ * - Event: "match received" → AVG of match_score
76
+ * - Expected: C ~ 0.65x A (35% lower match quality)
77
+ *
78
+ * REAL-WORLD ANALOGUE: A few good photos signal authenticity; too many
79
+ * curated shots read as catfish or staged.
72
80
  *
73
81
  * -------------------------------------------------------------------------------------
74
- * 2. WEEKEND SWIPE SURGE (event hook)
82
+ * 2. WEEKEND SWIPE SURGE (everything)
75
83
  * -------------------------------------------------------------------------------------
76
- * PATTERN: On Sunday evenings (day=0, hour 18-23), "swipe right" events are
77
- * duplicated 2.5x with weekend_surge=true. The Sunday Scaries drive swipes.
84
+ * PATTERN: Sunday 18-23 UTC swipes get 1.5x duplication. No flag.
78
85
  *
79
86
  * HOW TO FIND IT IN MIXPANEL:
80
87
  * Report 1: Swipe Volume by Day of Week
81
- * - Insights (bar) > "swipe right" > Total > Breakdown: Day of Week
82
- * - Expected: Sunday bar significantly taller than other days
88
+ * - Insights > "swipe right" Total Breakdown: Day of Week
89
+ * - Expected: Sunday taller than other days
83
90
  *
84
- * Report 2: Weekend Surge Flag
85
- * - Insights (line) > "swipe right" > Total > Breakdown: "weekend_surge"
86
- * - Expected: weekend_surge=true events clustered on Sunday evenings
91
+ * REAL-WORLD ANALOGUE: Sunday Scaries drive swipe activity.
87
92
  *
88
93
  * -------------------------------------------------------------------------------------
89
- * 3. SUPER-LIKE EFFECT (everything hook)
94
+ * 3. SUPER-LIKE EFFECT (everything)
90
95
  * -------------------------------------------------------------------------------------
91
- * PATTERN: "swipe right" events with is_super_like=true generate 3x more
92
- * "match received" events nearby in time. super_like_match=true on those
93
- * matches. Super-likes dramatically improve match rates.
96
+ * PATTERN: Each is_super_like=true swipe clones 3 extra match-received events
97
+ * within 5-120 minutes. No flag.
94
98
  *
95
99
  * HOW TO FIND IT IN MIXPANEL:
96
- * Report 1: Match Rate by Super-Like
97
- * - Insights > "match received" > Total per user > Breakdown: "super_like_match"
98
- * - Expected: super_like_match=true concentrated among super-likers
100
+ * Report 1: Super-Like to Match Funnel
101
+ * - Funnels > "swipe right" (filter: is_super_like=true) "match received"
102
+ * - vs same with is_super_like=false
103
+ * - Expected: super-like funnel ~ 3x higher conversion
99
104
  *
100
- * Report 2: Super-Like Conversion
101
- * - Funnels > "swipe right" (filter: is_super_like=true) > "match received"
102
- * - Compare to: "swipe right" (filter: is_super_like=false) > "match received"
103
- * - Expected: super-like funnel ~3x higher conversion
105
+ * REAL-WORLD ANALOGUE: Super-likes dramatically lift match rates.
104
106
  *
105
107
  * -------------------------------------------------------------------------------------
106
- * 4. PREMIUM MATCH BOOST (everything hook)
108
+ * 4. PREMIUM MATCH BOOST (everything)
107
109
  * -------------------------------------------------------------------------------------
108
- * PATTERN: Premium subscribers get 2x "match received" events. Elite
109
- * subscribers get 4x + injected "profile viewed" events (see-who-liked-you).
110
- * premium_boost=true on injected events. Subscription pays off.
110
+ * PATTERN: Premium subscribers get 2x match events; Elite get 4x + cloned
111
+ * profile-viewed events (see-who-liked-you). Reads subscription from profile.
111
112
  *
112
113
  * HOW TO FIND IT IN MIXPANEL:
113
114
  * Report 1: Matches by Subscription Tier
114
- * - Insights > "match received" > Total per user > Breakdown: user prop "subscription"
115
- * - Expected: Free ~baseline, Premium ~2x, Elite ~4x
115
+ * - Insights > "match received" Total per user Breakdown: subscription
116
+ * - Expected: Free ~ 1x, Premium ~ 2x, Elite ~ 4x
116
117
  *
117
- * Report 2: Profile Views (Elite Exclusive)
118
- * - Insights > "profile viewed" > Total > Breakdown: "premium_boost"
119
- * - Expected: premium_boost=true events only from Elite subscribers
118
+ * REAL-WORLD ANALOGUE: Premium tiers boost visibility.
120
119
  *
121
120
  * -------------------------------------------------------------------------------------
122
- * 5. GHOSTING CHURN (everything hook)
121
+ * 5. GHOSTING CHURN (everything)
123
122
  * -------------------------------------------------------------------------------------
124
- * PATTERN: Users who receive a match but send zero messages within 48 hours
125
- * lose 70% of their events after the match date. ghosted=true stamped on
126
- * remaining "app opened" events. Ghosters churn.
123
+ * PATTERN: Users with match-received but no message-sent within 48 hours lose
124
+ * 80% of post-match events. No flag.
127
125
  *
128
126
  * HOW TO FIND IT IN MIXPANEL:
129
127
  * Report 1: Retention by Messaging Behavior
130
- * - Retention > Starting: "match received" > Return: any event
131
- * - Segment: users who did "message sent" vs users who didn't
132
- * - Expected: non-messengers have ~70% lower retention
128
+ * - Retention starting "match received"
129
+ * - Cohort A: users with >= 1 "message sent" within 48 hours of match
130
+ * - Cohort B: rest
131
+ * - Expected: B drops sharply
133
132
  *
134
- * Report 2: Ghost Flag
135
- * - Insights (line) > "app opened" > Total > Breakdown: "ghosted"
136
- * - Expected: ghosted=true users trail off sharply after match date
133
+ * REAL-WORLD ANALOGUE: Non-responders churn.
137
134
  *
138
135
  * -------------------------------------------------------------------------------------
139
- * 6. BIO + PROMPT POWER USERS (everything hook)
136
+ * 6. BIO + PROMPT POWER USERS (everything)
140
137
  * -------------------------------------------------------------------------------------
141
- * PATTERN: Users with both "bio updated" AND 3+ "prompt answered" events
142
- * get 4x "date scheduled" events. complete_profile_power=true on injected
143
- * dates. Effort on profile = real dates.
138
+ * PATTERN: Users with bio-updated AND 3+ prompt-answered events get 3 extra
139
+ * cloned date-scheduled events per existing. No flag.
144
140
  *
145
141
  * HOW TO FIND IT IN MIXPANEL:
146
- * Report 1: Dates by Profile Effort
147
- * - Insights > "date scheduled" > Total per user > Breakdown: "complete_profile_power"
148
- * - Expected: complete_profile_power=true users schedule ~4x more dates
142
+ * Report 1: Dates per User by Profile Effort
143
+ * - Cohort A: users with bio-updated AND >= 3 "prompt answered"
144
+ * - Cohort B: rest
145
+ * - Event: "date scheduled" → Total per user
146
+ * - Expected: A ~ 4x B
149
147
  *
150
- * Report 2: Profile Effort Funnel
151
- * - Funnels > "bio updated" > "prompt answered" > "date scheduled"
152
- * - Expected: users completing all 3 have dramatically higher date rates
148
+ * REAL-WORLD ANALOGUE: Profile effort signals serious intent.
153
149
  *
154
150
  * -------------------------------------------------------------------------------------
155
- * 7. VALENTINE'S DAY SPIKE (event + everything hook)
151
+ * 7. VALENTINE'S DAY SPIKE (everything)
156
152
  * -------------------------------------------------------------------------------------
157
- * PATTERN: Day 60 = Feb 14. Days 58-63: "profile created" events 3x via
158
- * cloned events (valentines=true). In everything hook, "premium upgrade"
159
- * events 5x for users active during V-Day window. Love is expensive.
153
+ * PATTERN: Days 58-63 (V-Day window): profile-created events cloned 3x and
154
+ * premium-upgrade events cloned 5x. No flag discover via line chart by day.
160
155
  *
161
156
  * HOW TO FIND IT IN MIXPANEL:
162
- * Report 1: Signup Spike
163
- * - Insights (line) > "profile created" > Total > Daily
164
- * - Expected: sharp 3x spike around day 58-63
157
+ * Report 1: Signup Volume Over Time
158
+ * - Insights > "profile created" Total Line by day
159
+ * - Expected: spike ~ 3x days 58-63
165
160
  *
166
- * Report 2: Premium Upgrades Around V-Day
167
- * - Insights (line) > "premium upgrade" > Total > Breakdown: "valentines"
168
- * - Expected: 5x premium upgrades during V-Day window
161
+ * REAL-WORLD ANALOGUE: Love is expensive.
169
162
  *
170
163
  * -------------------------------------------------------------------------------------
171
- * 8. OFF-APP RETENTION (everything hook)
164
+ * 8. OFF-APP RETENTION (everything)
172
165
  * -------------------------------------------------------------------------------------
173
- * PATTERN: Users with "phone number exchanged" OR "date scheduled" in first
174
- * 14 days maintain 60% D30 retention (extra events injected). Users without
175
- * these milestones lose events after day 30 the app becomes irrelevant
176
- * once they find someone IRL.
166
+ * PATTERN: Users with phone-exchanged OR date-scheduled in first 14 days
167
+ * get extra cloned app-open + swipe events past day 30. Non-milestone
168
+ * users lose 80% of post-day-30 events. No flag.
177
169
  *
178
170
  * HOW TO FIND IT IN MIXPANEL:
179
- * Report 1: Retention by Milestone
180
- * - Retention > Starting: "profile created" > Return: any event
181
- * - Segment: users who did "date scheduled" in first 14 days vs not
182
- * - Expected: milestone users ~60% D30 retention vs ~20% for others
171
+ * Report 1: Retention by Milestone Cohort
172
+ * - Cohort A: users with phone-exchanged OR date-scheduled in first 14 days
173
+ * - Cohort B: rest
174
+ * - Expected: A ~ 60% D30 retention vs B ~ 20%
183
175
  *
184
- * Report 2: Long-Term Engagement
185
- * - Insights (line) > "app opened" > Total per user > Weekly > Breakdown: user prop "subscription"
186
- * - Expected: after week 4, non-milestone users nearly disappear
176
+ * REAL-WORLD ANALOGUE: Once they find someone IRL, app becomes irrelevant.
187
177
  *
188
- * =====================================================================================
189
- * EXPECTED METRICS SUMMARY
190
- * =====================================================================================
178
+ * -------------------------------------------------------------------------------------
179
+ * 9. MATCH FLOW TIME-TO-CONVERT (funnel-post)
180
+ * -------------------------------------------------------------------------------------
181
+ * PATTERN: Elite users complete swipe→match→message funnel 1.4x faster
182
+ * (factor 0.71 on inter-event gaps); Free users 1.4x slower (factor 1.4).
191
183
  *
192
- * Hook | Metric | Baseline | Effect | Ratio
193
- * ───────────────────────────-|──────────────────────-|──────────|──────────|──────
194
- * Photo Upload Conversion | matches/user | ~3 | ~15 | 5x
195
- * Weekend Swipe Surge | Sunday swipes | 1x | 2.5x | 2.5x
196
- * Super-Like Effect | matches from super | 1x | 3x | 3x
197
- * Premium Match Boost | matches (Elite) | 1x | 4x | 4x
198
- * Ghosting Churn | post-match retention | 100% | 30% | 0.3x
199
- * Bio + Prompt Power Users | dates/user | 1x | 4x | 4x
200
- * Valentine's Day Spike | signups (days 58-63) | 1x | 3x | 3x
201
- * Off-App Retention | D30 retention | ~20% | ~60% | 3x
184
+ * HOW TO FIND IT IN MIXPANEL:
185
+ * Report 1: Match Flow Median Time-to-Convert by Tier
186
+ * - Funnels > "swipe right" "match received" → "message sent"
187
+ * - Measure: Median time to convert
188
+ * - Breakdown: subscription
189
+ * - Expected: Elite ~ 0.71x; Free ~ 1.4x
190
+ *
191
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
192
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
193
+ * show this — funnel-post adjusts gaps within funnel instances, not
194
+ * across the user's full event history.
195
+ *
196
+ * REAL-WORLD ANALOGUE: Premium notifications + boost surface matches faster.
202
197
  *
203
198
  * =====================================================================================
204
- * ADVANCED ANALYSIS IDEAS
199
+ * EXPECTED METRICS SUMMARY
205
200
  * =====================================================================================
206
201
  *
207
- * - Photo Conversion + Premium Boost: Do complete-profile Elite users compound
208
- * to 20x matches (5x photos * 4x Elite)?
209
- * - Weekend Surge + Super-Like: Sunday evening super-likes should be the
210
- * highest-converting swipe segment.
211
- * - Ghosting + Off-App Retention: Users who ghost AND never schedule a date
212
- * churn fastest double negative retention effect.
213
- * - Valentine's Spike + Monetization: V-Day signups who upgrade to Premium
214
- * within 7 days have the highest LTV window.
215
- * - Bio/Prompt Power + Date Scheduling: Complete profile users schedule dates
216
- * at 4x the rate, but do they also have higher message response rates?
217
- * - Cohort by age_range, gender, subscription tier, and signup week.
218
- * - Funnel analysis: onboarding by gender, match flow by subscription,
219
- * date funnel by profile completeness.
220
- * =====================================================================================
202
+ * Hook | Metric | Baseline | Effect | Ratio
203
+ * ----------------------------|----------------------|----------|----------|------
204
+ * Photo Magic Number | sweet matches/user | 1x | ~ 1.7x | 1.7x
205
+ * Photo Magic Number | over matches/user | 1x | 0.7x | -30%
206
+ * Weekend Swipe Surge | Sunday swipes | 1x | ~ 1.5x | 1.5x
207
+ * Super-Like Effect | matches from super | 1x | 3x | 3x
208
+ * Premium Match Boost | matches (Elite) | 1x | 4x | 4x
209
+ * Ghosting Churn | post-match retention | 100% | 20% | 0.2x
210
+ * Bio + Prompt Power Users | dates/user | 1x | 4x | 4x
211
+ * Valentine's Day Spike | signups days 58-63 | 1x | 3x | 3x
212
+ * Off-App Retention | D30 retention | ~ 20% | ~ 60% | 3x
213
+ * Match Flow T2C | median min by tier | 1x | 0.71x/1.4x| ~ 2x range
221
214
  */
222
215
 
223
216
  /** @type {Config} */
224
217
  const config = {
225
218
  token,
226
219
  seed: SEED,
227
- numDays: num_days,
220
+ datasetStart: "2026-01-01T00:00:00Z",
221
+ datasetEnd: "2026-04-28T23:59:59Z",
222
+ // numDays: num_days,
228
223
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
229
224
  numUsers: num_users,
230
225
  hasAnonIds: false,
@@ -255,12 +250,11 @@ const config = {
255
250
  age_range: ["18-24", "25-29", "30-34", "35-39", "40+"],
256
251
  gender: ["Male", "Male", "Female", "Female", "Non-binary"],
257
252
  looking_for: ["Men", "Women", "Everyone"],
258
- valentines: [false],
259
253
  },
260
254
  },
261
255
  {
262
256
  event: "photo uploaded",
263
- weight: 3,
257
+ weight: 12,
264
258
  properties: {
265
259
  photo_number: u.weighNumRange(1, 6, 0.5, 2),
266
260
  has_face: [true, true, true, true, false],
@@ -286,7 +280,6 @@ const config = {
286
280
  weight: 10,
287
281
  properties: {
288
282
  is_super_like: [false, false, false, false, false, false, false, false, false, true],
289
- weekend_surge: [false],
290
283
  swipe_source: ["feed", "feed", "feed", "discover", "boost", "nearby"],
291
284
  },
292
285
  },
@@ -302,9 +295,6 @@ const config = {
302
295
  weight: 4,
303
296
  properties: {
304
297
  match_score: u.weighNumRange(50, 100, 0.5, 75),
305
- complete_profile: [false],
306
- super_like_match: [false],
307
- premium_boost: [false],
308
298
  },
309
299
  },
310
300
  {
@@ -335,7 +325,6 @@ const config = {
335
325
  weight: 1,
336
326
  properties: {
337
327
  venue_type: ["coffee", "dinner", "drinks", "activity", "virtual"],
338
- complete_profile_power: [false],
339
328
  },
340
329
  },
341
330
  {
@@ -343,7 +332,6 @@ const config = {
343
332
  weight: 5,
344
333
  properties: {
345
334
  viewer_source: ["feed", "discover", "liked_you", "nearby"],
346
- premium_boost: [false],
347
335
  },
348
336
  },
349
337
  {
@@ -352,7 +340,6 @@ const config = {
352
340
  properties: {
353
341
  plan: ["Premium", "Premium", "Premium", "Elite"],
354
342
  price_usd: [14.99, 14.99, 14.99, 29.99],
355
- valentines: [false],
356
343
  },
357
344
  },
358
345
  {
@@ -385,7 +372,6 @@ const config = {
385
372
  weight: 8,
386
373
  properties: {
387
374
  session_duration_mins: u.weighNumRange(1, 120, 0.3, 8),
388
- ghosted: [false],
389
375
  },
390
376
  },
391
377
  ],
@@ -430,7 +416,7 @@ const config = {
430
416
  // ── SuperProps ──────────────────────────────────────────
431
417
  superProps: {
432
418
  subscription: ["Free", "Free", "Free", "Premium", "Elite"],
433
- platform: ["ios", "ios", "android"],
419
+ Platform: ["ios", "ios", "android"],
434
420
  },
435
421
 
436
422
  // ── UserProps ──────────────────────────────────────────
@@ -443,7 +429,7 @@ const config = {
443
429
  total_matches: u.weighNumRange(0, 200, 0.3, 15),
444
430
  total_messages_sent: u.weighNumRange(0, 500, 0.3, 30),
445
431
  profile_completeness: ["incomplete", "incomplete", "basic", "basic", "complete"],
446
- platform: ["ios", "ios", "android"],
432
+ Platform: ["ios", "ios", "android"],
447
433
  },
448
434
 
449
435
  // ── SCD Props ──────────────────────────────────────────
@@ -461,25 +447,25 @@ const config = {
461
447
  mirrorProps: {},
462
448
  lookupTables: [],
463
449
 
464
- // ── Hook Function ──────────────────────────────────────
465
450
  hook: function (record, type, meta) {
466
- const VALENTINES_DAY = DATASET_START.add(60, "days");
467
- const VDAY_WINDOW_START = DATASET_START.add(58, "days");
468
- const VDAY_WINDOW_END = DATASET_START.add(63, "days");
469
-
470
- // ─── EVENT-LEVEL HOOKS ───────────────────────────────────────────
471
-
472
- if (type === "event") {
473
- const EVENT_TIME = dayjs(record.time);
474
451
 
475
- // ── HOOK 2: WEEKEND SWIPE SURGE ─────────────────
476
- // Tagging moved to everything hook (sessionization reassigns times after event hook)
477
-
478
- // ── HOOK 7: VALENTINE'S DAY SPIKE (event part) ──
479
- // Days 58-63: tag profile created events for 3x duplication
480
- if (record.event === "profile created") {
481
- if (EVENT_TIME.isAfter(VDAY_WINDOW_START) && EVENT_TIME.isBefore(VDAY_WINDOW_END)) {
482
- record.valentines = true;
452
+ // HOOK 9 (T2C): MATCH FLOW TIME-TO-CONVERT (funnel-post)
453
+ // Elite users complete swipe→match→message funnel 1.4x faster
454
+ // (factor 0.71); Free users 1.4x slower (factor 1.4).
455
+ if (type === "funnel-post") {
456
+ const segment = meta?.profile?.subscription;
457
+ if (Array.isArray(record) && record.length > 1) {
458
+ const factor = (
459
+ segment === "Elite" ? 0.71 :
460
+ segment === "Free" ? 1.4 :
461
+ 1.0
462
+ );
463
+ if (factor !== 1.0) {
464
+ for (let i = 1; i < record.length; i++) {
465
+ const prev = dayjs(record[i - 1].time);
466
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
467
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
468
+ }
483
469
  }
484
470
  }
485
471
  }
@@ -487,25 +473,25 @@ const config = {
487
473
  // ─── EVERYTHING-LEVEL HOOKS ──────────────────────────────────────
488
474
 
489
475
  if (type === "everything") {
476
+ const datasetStart = dayjs.unix(meta.datasetStart);
477
+ const VDAY_WINDOW_START = datasetStart.add(58, "days");
478
+ const VDAY_WINDOW_END = datasetStart.add(63, "days");
490
479
  const events = record;
491
480
  if (!events || events.length === 0) return record;
492
481
 
493
482
  const profile = meta.profile || {};
494
483
 
495
- // Stamp superProps from profile for consistency
496
484
  events.forEach(e => {
497
485
  if (profile.subscription) e.subscription = profile.subscription;
498
486
  if (profile.platform) e.platform = profile.platform;
499
487
  });
500
488
 
501
- // ── First pass: scan user patterns ──
502
489
  let photoUploadCount = 0;
503
490
  let promptAnsweredCount = 0;
504
491
  let hasBioUpdated = false;
505
- let matchEvents = [];
506
- let messageSentEvents = [];
507
- let hasSuperLike = false;
508
- let superLikeEvents = [];
492
+ const matchEvents = [];
493
+ const messageSentEvents = [];
494
+ const superLikeEvents = [];
509
495
  let hasPhoneExchangedEarly = false;
510
496
  let hasDateScheduledEarly = false;
511
497
  let firstEventTime = null;
@@ -519,13 +505,9 @@ const config = {
519
505
  if (event.event === "bio updated") hasBioUpdated = true;
520
506
  if (event.event === "match received") matchEvents.push(event);
521
507
  if (event.event === "message sent") messageSentEvents.push(event);
522
- if (event.event === "swipe right" && event.is_super_like === true) {
523
- hasSuperLike = true;
524
- superLikeEvents.push(event);
525
- }
508
+ if (event.event === "swipe right" && event.is_super_like === true) superLikeEvents.push(event);
526
509
  });
527
510
 
528
- // Check early milestones (first 14 days)
529
511
  if (firstEventTime) {
530
512
  const earlyWindow = dayjs(firstEventTime).add(14, "days");
531
513
  events.forEach(event => {
@@ -537,65 +519,61 @@ const config = {
537
519
  });
538
520
  }
539
521
 
540
- // ── HOOK 1: PHOTO UPLOAD CONVERSION ─────────────
541
- // Users with 4+ photo uploads get 5x match events
542
- if (photoUploadCount >= 4 && matchEvents.length > 0) {
522
+ // HOOK 1 + HOOK 9: PHOTO MAGIC NUMBER (no flags)
523
+ // Sweet 2-5 photos clone 2-4 extra match events per existing.
524
+ // Over 6+ photos drop match_score by 35% on match received events
525
+ // (over-curated profile reads as fake/staged).
526
+ if (photoUploadCount >= 2 && photoUploadCount <= 5 && matchEvents.length > 0) {
543
527
  const matchTemplate = matchEvents[0];
544
- const extraMatches = matchEvents.length * 4; // 4 additional = 5x total
545
- for (let i = 0; i < extraMatches; i++) {
546
- const sourceMatch = matchEvents[i % matchEvents.length];
547
- events.push({
548
- ...matchTemplate,
549
- event: "match received",
550
- time: dayjs(sourceMatch.time).add(chance.integer({ min: 1, max: 180 }), "minutes").toISOString(),
551
- user_id: sourceMatch.user_id,
552
- match_score: chance.integer({ min: 60, max: 98 }),
553
- complete_profile: true,
554
- super_like_match: false,
555
- premium_boost: false,
556
- });
557
- }
528
+ matchEvents.forEach(m => {
529
+ const extras = chance.integer({ min: 2, max: 4 });
530
+ for (let i = 0; i < extras; i++) {
531
+ events.push({
532
+ ...matchTemplate,
533
+ time: dayjs(m.time).add(chance.integer({ min: 1, max: 180 }), "minutes").toISOString(),
534
+ user_id: m.user_id,
535
+ match_score: chance.integer({ min: 60, max: 98 }),
536
+ });
537
+ }
538
+ });
539
+ } else if (photoUploadCount >= 6) {
540
+ events.forEach(e => {
541
+ if (e.event === "match received" && typeof e.match_score === "number") {
542
+ e.match_score = Math.max(20, Math.round(e.match_score * 0.65));
543
+ }
544
+ });
558
545
  }
559
546
 
560
- // ── HOOK 2: WEEKEND SWIPE SURGE (everything part)
561
- // Tag Sunday evening swipes AFTER sessionization has finalized times
562
- events.forEach(event => {
547
+ // HOOK 2: WEEKEND SWIPE SURGE Sunday 18-23 UTC swipes get cloned
548
+ // 1.5x. No flag discover via day-of-week chart.
549
+ for (let idx = events.length - 1; idx >= 0; idx--) {
550
+ const event = events[idx];
563
551
  if (event.event === "swipe right") {
564
552
  const dow = new Date(event.time).getUTCDay();
565
553
  const hr = new Date(event.time).getUTCHours();
566
554
  if (dow === 0 && hr >= 18 && hr <= 23) {
567
- event.weekend_surge = true;
568
- }
569
- }
570
- });
571
- // Duplicate weekend_surge=true swipes to create 2.5x volume
572
- for (let idx = events.length - 1; idx >= 0; idx--) {
573
- const event = events[idx];
574
- if (event.event === "swipe right" && event.weekend_surge === true) {
575
- // Add 1.5 extra copies on average (1 always + 50% chance of second)
576
- const etime = dayjs(event.time);
577
- events.push({
578
- ...event,
579
- time: etime.add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
580
- user_id: event.user_id,
581
- });
582
- if (chance.bool({ likelihood: 50 })) {
555
+ const etime = dayjs(event.time);
583
556
  events.push({
584
557
  ...event,
585
- time: etime.add(chance.integer({ min: 5, max: 60 }), "minutes").toISOString(),
558
+ time: etime.add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
586
559
  user_id: event.user_id,
587
560
  });
561
+ if (chance.bool({ likelihood: 50 })) {
562
+ events.push({
563
+ ...event,
564
+ time: etime.add(chance.integer({ min: 5, max: 60 }), "minutes").toISOString(),
565
+ user_id: event.user_id,
566
+ });
567
+ }
588
568
  }
589
569
  }
590
570
  }
591
571
 
592
- // ── HOOK 3: SUPER-LIKE EFFECT ───────────────────
593
- // Super-likes generate 3x more matches nearby in time
594
- if (hasSuperLike && superLikeEvents.length > 0) {
595
- const matchTemplate = matchEvents.length > 0
596
- ? matchEvents[0]
597
- : events.find(e => e.event === "swipe right") || events[0];
598
-
572
+ // HOOK 3: SUPER-LIKE EFFECT — clone 3 extra match events per
573
+ // super-like, near in time. No flag discover via funnel
574
+ // "swipe right where is_super_like=true" "match received".
575
+ if (superLikeEvents.length > 0) {
576
+ const matchTemplate = matchEvents[0] || events[0];
599
577
  superLikeEvents.forEach(sle => {
600
578
  for (let i = 0; i < 3; i++) {
601
579
  events.push({
@@ -604,36 +582,27 @@ const config = {
604
582
  time: dayjs(sle.time).add(chance.integer({ min: 5, max: 120 }), "minutes").toISOString(),
605
583
  user_id: sle.user_id,
606
584
  match_score: chance.integer({ min: 70, max: 99 }),
607
- complete_profile: false,
608
- super_like_match: true,
609
- premium_boost: false,
610
585
  });
611
586
  }
612
587
  });
613
588
  }
614
589
 
615
- // ── HOOK 4: PREMIUM MATCH BOOST ─────────────────
616
- // Premium: 2x matches. Elite: 4x matches + profile viewed (see-who-liked-you).
590
+ // HOOK 4: PREMIUM MATCH BOOST — Premium 2x, Elite 4x match events.
591
+ // Elite users also get profile-viewed events injected (see-who-liked-you).
592
+ // Reads subscription from profile.
617
593
  const sub = profile.subscription;
618
594
  if ((sub === "Premium" || sub === "Elite") && matchEvents.length > 0) {
619
- const multiplier = sub === "Elite" ? 3 : 1; // +3 = 4x total for Elite, +1 = 2x for Premium
595
+ const multiplier = sub === "Elite" ? 3 : 1;
620
596
  const matchTemplate = matchEvents[0];
621
-
622
597
  for (let i = 0; i < matchEvents.length * multiplier; i++) {
623
598
  const sourceMatch = matchEvents[i % matchEvents.length];
624
599
  events.push({
625
600
  ...matchTemplate,
626
- event: "match received",
627
601
  time: dayjs(sourceMatch.time).add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
628
602
  user_id: sourceMatch.user_id,
629
603
  match_score: chance.integer({ min: 65, max: 99 }),
630
- complete_profile: false,
631
- super_like_match: false,
632
- premium_boost: true,
633
604
  });
634
605
  }
635
-
636
- // Elite exclusive: inject "profile viewed" events (see-who-liked-you)
637
606
  if (sub === "Elite") {
638
607
  const viewTemplate = events.find(e => e.event === "profile viewed") || matchTemplate;
639
608
  matchEvents.forEach(m => {
@@ -643,49 +612,47 @@ const config = {
643
612
  time: dayjs(m.time).subtract(chance.integer({ min: 10, max: 120 }), "minutes").toISOString(),
644
613
  user_id: m.user_id,
645
614
  viewer_source: "liked_you",
646
- premium_boost: true,
647
615
  });
648
616
  });
649
617
  }
650
618
  }
651
619
 
652
- // ── HOOK 6: BIO + PROMPT POWER USERS ────────────
653
- // Users with bio updated AND 3+ prompts get 4x date scheduled
620
+ // HOOK 6: BIO + PROMPT POWER USERS — bio + 3+ prompts → 3 extra
621
+ // cloned date events per existing. No flag.
654
622
  if (hasBioUpdated && promptAnsweredCount >= 3) {
655
623
  const dateEvents = events.filter(e => e.event === "date scheduled");
656
624
  if (dateEvents.length > 0) {
657
625
  const dateTemplate = dateEvents[0];
658
- const extraDates = dateEvents.length * 3; // +3 = 4x total
659
626
  const venueTypes = ["coffee", "dinner", "drinks", "activity", "virtual"];
660
- for (let i = 0; i < extraDates; i++) {
627
+ for (let i = 0; i < dateEvents.length * 3; i++) {
661
628
  const sourceDate = dateEvents[i % dateEvents.length];
662
629
  events.push({
663
630
  ...dateTemplate,
664
- event: "date scheduled",
665
631
  time: dayjs(sourceDate.time).add(chance.integer({ min: 1, max: 72 }), "hours").toISOString(),
666
632
  user_id: sourceDate.user_id,
667
633
  venue_type: chance.pickone(venueTypes),
668
- complete_profile_power: true,
669
634
  });
670
635
  }
671
636
  }
672
637
  }
673
638
 
674
- // ── HOOK 7: VALENTINE'S DAY SPIKE (everything part)
675
- // 3x profile created events during V-Day window (via cloning tagged events)
676
- const vdaySignups = events.filter(e => e.event === "profile created" && e.valentines === true);
639
+ // HOOK 7: VALENTINE'S DAY SPIKE clone profile-created events during
640
+ // days 58-63 (3x volume), plus clone premium-upgrade events 5x. No flag.
641
+ const vdaySignups = events.filter(e =>
642
+ e.event === "profile created" &&
643
+ dayjs(e.time).isAfter(VDAY_WINDOW_START) &&
644
+ dayjs(e.time).isBefore(VDAY_WINDOW_END)
645
+ );
677
646
  vdaySignups.forEach(signup => {
678
- for (let i = 0; i < 2; i++) { // +2 clones = 3x total
647
+ for (let i = 0; i < 2; i++) {
679
648
  events.push({
680
649
  ...signup,
681
650
  time: dayjs(signup.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
682
651
  user_id: signup.user_id,
683
- valentines: true,
684
652
  });
685
653
  }
686
654
  });
687
655
 
688
- // 5x premium upgrade events for users active during V-Day window
689
656
  const vdayUpgrades = events.filter(e =>
690
657
  e.event === "premium upgrade" &&
691
658
  dayjs(e.time).isAfter(VDAY_WINDOW_START) &&
@@ -694,74 +661,29 @@ const config = {
694
661
  if (vdayUpgrades.length > 0) {
695
662
  const upgradeTemplate = vdayUpgrades[0];
696
663
  vdayUpgrades.forEach(upgrade => {
697
- for (let i = 0; i < 4; i++) { // +4 clones = 5x total
664
+ for (let i = 0; i < 4; i++) {
698
665
  events.push({
699
666
  ...upgradeTemplate,
700
- event: "premium upgrade",
701
667
  time: dayjs(upgrade.time).add(chance.integer({ min: 1, max: 24 }), "hours").toISOString(),
702
668
  user_id: upgrade.user_id,
703
669
  plan: upgrade.plan,
704
670
  price_usd: upgrade.price_usd,
705
- valentines: true,
706
671
  });
707
672
  }
708
673
  });
709
674
  }
710
675
 
711
- // ── HOOK 5: GHOSTING CHURN ──────────────────────
712
- // Users with match but no message within 48 hours lose 70% of later events
713
- let isGhoster = false;
714
- if (matchEvents.length > 0) {
715
- // Check if any match has a message within 48 hours
716
- let hasTimely = false;
717
- for (const m of matchEvents) {
718
- const matchTime = dayjs(m.time);
719
- const deadline = matchTime.add(48, "hours");
720
- for (const msg of messageSentEvents) {
721
- const msgTime = dayjs(msg.time);
722
- if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
723
- hasTimely = true;
724
- break;
725
- }
726
- }
727
- if (hasTimely) break;
728
- }
729
-
730
- if (!hasTimely) {
731
- isGhoster = true;
732
- // Find earliest match time as churn reference point
733
- const earliestMatch = matchEvents.reduce((min, m) =>
734
- dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
735
- );
736
- const churnAfter = dayjs(earliestMatch.time);
737
-
738
- for (let i = events.length - 1; i >= 0; i--) {
739
- const evt = events[i];
740
- if (dayjs(evt.time).isAfter(churnAfter)) {
741
- if (chance.bool({ likelihood: 70 })) {
742
- events.splice(i, 1);
743
- } else if (evt.event === "app opened") {
744
- evt.ghosted = true;
745
- }
746
- }
747
- }
748
- }
749
- }
750
-
751
- // ── HOOK 8: OFF-APP RETENTION ───────────────────
752
- // Users with phone exchange or date in first 14 days: 60% D30 retention
753
- // Others: lose events after day 30
676
+ // HOOK 8: OFF-APP RETENTION — users with phone-exchanged or
677
+ // date-scheduled in first 14 days get extra cloned app-open + swipe
678
+ // events past day 30. Non-milestone users lose 80% of post-day-30
679
+ // events. No flag.
754
680
  if (firstEventTime) {
755
681
  const day30 = dayjs(firstEventTime).add(30, "days");
756
682
  const hasEarlyMilestone = hasPhoneExchangedEarly || hasDateScheduledEarly;
757
-
758
683
  if (hasEarlyMilestone) {
759
- // Good retention: inject extra events after day 30 to sustain engagement
760
684
  const appOpenedTemplate = events.find(e => e.event === "app opened") || events[0];
761
685
  const swipeTemplate = events.find(e => e.event === "swipe right") || events[0];
762
686
  const postDay30Events = events.filter(e => dayjs(e.time).isAfter(day30));
763
-
764
- // If they don't have enough post-day30 events, inject some
765
687
  if (postDay30Events.length < events.length * 0.3) {
766
688
  const retentionCount = Math.floor(events.length * 0.3);
767
689
  for (let i = 0; i < retentionCount; i++) {
@@ -775,14 +697,38 @@ const config = {
775
697
  }
776
698
  }
777
699
  } else {
778
- // No milestone: churn after day 30
779
700
  for (let i = events.length - 1; i >= 0; i--) {
780
- const evt = events[i];
781
- if (dayjs(evt.time).isAfter(day30)) {
782
- // Keep ~20% of events (remove ~80%) to simulate natural drop-off
783
- if (chance.bool({ likelihood: 80 })) {
784
- events.splice(i, 1);
785
- }
701
+ if (dayjs(events[i].time).isAfter(day30) && chance.bool({ likelihood: 80 })) {
702
+ events.splice(i, 1);
703
+ }
704
+ }
705
+ }
706
+ }
707
+
708
+ // HOOK 5: GHOSTING CHURN — users with match but no message within
709
+ // 48hrs lose 80% of post-match events. No flag.
710
+ if (matchEvents.length > 0) {
711
+ let hasTimely = false;
712
+ for (const m of matchEvents) {
713
+ const matchTime = dayjs(m.time);
714
+ const deadline = matchTime.add(48, "hours");
715
+ for (const msg of messageSentEvents) {
716
+ const msgTime = dayjs(msg.time);
717
+ if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
718
+ hasTimely = true;
719
+ break;
720
+ }
721
+ }
722
+ if (hasTimely) break;
723
+ }
724
+ if (!hasTimely) {
725
+ const earliestMatch = matchEvents.reduce((min, m) =>
726
+ dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
727
+ );
728
+ const churnAfter = dayjs(earliestMatch.time);
729
+ for (let i = events.length - 1; i >= 0; i--) {
730
+ if (dayjs(events[i].time).isAfter(churnAfter) && chance.bool({ likelihood: 80 })) {
731
+ events.splice(i, 1);
786
732
  }
787
733
  }
788
734
  }