@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,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,183 +48,184 @@ 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 swipes get heavy cloning (evening 4x, daytime 2x)
85
+ * to overcome soup DOW weight deficit. No flag.
78
86
  *
79
87
  * HOW TO FIND IT IN MIXPANEL:
80
88
  * 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
89
+ * - Insights > "swipe right" Total Breakdown: Day of Week
90
+ * - Expected: Sunday taller than other days
83
91
  *
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
92
+ * REAL-WORLD ANALOGUE: Sunday Scaries drive swipe activity.
87
93
  *
88
94
  * -------------------------------------------------------------------------------------
89
- * 3. SUPER-LIKE EFFECT (everything hook)
95
+ * 3. SUPER-LIKE EFFECT (everything)
90
96
  * -------------------------------------------------------------------------------------
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.
97
+ * PATTERN: Each is_super_like=true swipe clones 3 extra match-received events
98
+ * within 5-120 minutes. No flag.
94
99
  *
95
100
  * 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
101
+ * Report 1: Super-Like to Match Funnel
102
+ * - Funnels > "swipe right" (filter: is_super_like=true) "match received"
103
+ * - vs same with is_super_like=false
104
+ * - Expected: super-like funnel ~ 3x higher conversion
99
105
  *
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
106
+ * REAL-WORLD ANALOGUE: Super-likes dramatically lift match rates.
104
107
  *
105
108
  * -------------------------------------------------------------------------------------
106
- * 4. PREMIUM MATCH BOOST (everything hook)
109
+ * 4. PREMIUM MATCH BOOST (everything)
107
110
  * -------------------------------------------------------------------------------------
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.
111
+ * PATTERN: Premium subscribers get 2x match events; Elite get 4x + cloned
112
+ * profile-viewed events (see-who-liked-you). Reads subscription from profile.
111
113
  *
112
114
  * HOW TO FIND IT IN MIXPANEL:
113
115
  * 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
116
+ * - Insights > "match received" Total per user Breakdown: subscription
117
+ * - Expected: Free ~ 1x, Premium ~ 2x, Elite ~ 4x
116
118
  *
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
119
+ * REAL-WORLD ANALOGUE: Premium tiers boost visibility.
120
120
  *
121
121
  * -------------------------------------------------------------------------------------
122
- * 5. GHOSTING CHURN (everything hook)
122
+ * 5. GHOSTING CHURN (everything)
123
123
  * -------------------------------------------------------------------------------------
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.
124
+ * PATTERN: Users with match-received but no message-sent within 48 hours lose
125
+ * 80% of post-match events. No flag.
127
126
  *
128
127
  * HOW TO FIND IT IN MIXPANEL:
129
128
  * 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
129
+ * - Retention starting "match received"
130
+ * - Cohort A: users with >= 1 "message sent" within 48 hours of match
131
+ * - Cohort B: rest
132
+ * - Expected: B drops sharply
133
133
  *
134
- * Report 2: Ghost Flag
135
- * - Insights (line) > "app opened" > Total > Breakdown: "ghosted"
136
- * - Expected: ghosted=true users trail off sharply after match date
134
+ * REAL-WORLD ANALOGUE: Non-responders churn.
137
135
  *
138
136
  * -------------------------------------------------------------------------------------
139
- * 6. BIO + PROMPT POWER USERS (everything hook)
137
+ * 6. BIO + PROMPT POWER USERS (everything)
140
138
  * -------------------------------------------------------------------------------------
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.
139
+ * PATTERN: Users with bio-updated AND 3+ prompt-answered events get 3 extra
140
+ * cloned date-scheduled events per existing. No flag.
144
141
  *
145
142
  * 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
143
+ * Report 1: Dates per User by Profile Effort
144
+ * - Cohort A: users with bio-updated AND >= 3 "prompt answered"
145
+ * - Cohort B: rest
146
+ * - Event: "date scheduled" → Total per user
147
+ * - Expected: A ~ 4x B
149
148
  *
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
149
+ * REAL-WORLD ANALOGUE: Profile effort signals serious intent.
153
150
  *
154
151
  * -------------------------------------------------------------------------------------
155
- * 7. VALENTINE'S DAY SPIKE (event + everything hook)
152
+ * 7. VALENTINE'S DAY SPIKE (everything)
156
153
  * -------------------------------------------------------------------------------------
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.
154
+ * PATTERN: Days 58-63 (V-Day window): profile-created events cloned 3x and
155
+ * premium-upgrade events cloned 5x. No flag discover via line chart by day.
160
156
  *
161
157
  * 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
158
+ * Report 1: Signup Volume Over Time
159
+ * - Insights > "profile created" Total Line by day
160
+ * - Expected: spike ~ 3x days 58-63
165
161
  *
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
162
+ * REAL-WORLD ANALOGUE: Love is expensive.
169
163
  *
170
164
  * -------------------------------------------------------------------------------------
171
- * 8. OFF-APP RETENTION (everything hook)
165
+ * 8. OFF-APP RETENTION (everything)
172
166
  * -------------------------------------------------------------------------------------
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.
167
+ * PATTERN: Users with phone-exchanged OR date-scheduled in first 14 days
168
+ * get extra cloned app-open + swipe events past day 30. Non-milestone
169
+ * users lose 80% of post-day-30 events. No flag.
177
170
  *
178
171
  * 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
172
+ * Report 1: Retention by Milestone Cohort
173
+ * - Cohort A: users with phone-exchanged OR date-scheduled in first 14 days
174
+ * - Cohort B: rest
175
+ * - Expected: A ~ 60% D30 retention vs B ~ 20%
183
176
  *
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
177
+ * REAL-WORLD ANALOGUE: Once they find someone IRL, app becomes irrelevant.
187
178
  *
188
- * =====================================================================================
189
- * EXPECTED METRICS SUMMARY
190
- * =====================================================================================
179
+ * -------------------------------------------------------------------------------------
180
+ * 9. MATCH FLOW TIME-TO-CONVERT (funnel-post)
181
+ * -------------------------------------------------------------------------------------
182
+ * PATTERN: Elite users complete swipe→match→message funnel 1.4x faster
183
+ * (factor 0.71 on inter-event gaps); Free users 1.4x slower (factor 1.4).
191
184
  *
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
185
+ * HOW TO FIND IT IN MIXPANEL:
186
+ * Report 1: Match Flow Median Time-to-Convert by Tier
187
+ * - Funnels > "swipe right" "match received" → "message sent"
188
+ * - Measure: Median time to convert
189
+ * - Breakdown: subscription
190
+ * - Expected: Elite ~ 0.71x; Free ~ 1.4x
191
+ *
192
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
193
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
194
+ * show this — funnel-post adjusts gaps within funnel instances, not
195
+ * across the user's full event history.
196
+ *
197
+ * REAL-WORLD ANALOGUE: Premium notifications + boost surface matches faster.
202
198
  *
203
199
  * =====================================================================================
204
- * ADVANCED ANALYSIS IDEAS
200
+ * EXPECTED METRICS SUMMARY
205
201
  * =====================================================================================
206
202
  *
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
- * =====================================================================================
203
+ * Hook | Metric | Baseline | Effect | Ratio
204
+ * ----------------------------|----------------------|----------|----------|------
205
+ * Photo Magic Number | sweet matches/user | 1x | ~ 1.7x | 1.7x
206
+ * Photo Magic Number | over matches/user | 1x | 0.7x | -30%
207
+ * Weekend Swipe Surge | Sunday swipes | 1x | ~ 1.5x | 1.5x
208
+ * Super-Like Effect | matches from super | 1x | 3x | 3x
209
+ * Premium Match Boost | matches (Elite) | 1x | 4x | 4x
210
+ * Ghosting Churn | post-match retention | 100% | 20% | 0.2x
211
+ * Bio + Prompt Power Users | dates/user | 1x | 4x | 4x
212
+ * Valentine's Day Spike | signups days 58-63 | 1x | 3x | 3x
213
+ * Off-App Retention | D30 retention | ~ 20% | ~ 60% | 3x
214
+ * Match Flow T2C | median min by tier | 1x | 0.71x/1.4x| ~ 2x range
221
215
  */
222
216
 
223
217
  /** @type {Config} */
224
218
  const config = {
219
+ version: 2,
225
220
  token,
226
221
  seed: SEED,
227
- numDays: num_days,
222
+ datasetStart: "2026-01-01T00:00:00Z",
223
+ datasetEnd: "2026-05-01T23:59:59Z",
224
+ // numDays: num_days,
228
225
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
229
226
  numUsers: num_users,
230
- hasAnonIds: false,
227
+ hasAnonIds: true,
228
+ avgDevicePerUser: 2,
231
229
  hasSessionIds: true,
232
230
  format: "json",
233
231
  gzip: true,
@@ -251,16 +249,16 @@ const config = {
251
249
  event: "profile created",
252
250
  weight: 1,
253
251
  isFirstEvent: true,
252
+ isAuthEvent: true,
254
253
  properties: {
255
254
  age_range: ["18-24", "25-29", "30-34", "35-39", "40+"],
256
255
  gender: ["Male", "Male", "Female", "Female", "Non-binary"],
257
256
  looking_for: ["Men", "Women", "Everyone"],
258
- valentines: [false],
259
257
  },
260
258
  },
261
259
  {
262
260
  event: "photo uploaded",
263
- weight: 3,
261
+ weight: 12,
264
262
  properties: {
265
263
  photo_number: u.weighNumRange(1, 6, 0.5, 2),
266
264
  has_face: [true, true, true, true, false],
@@ -286,7 +284,6 @@ const config = {
286
284
  weight: 10,
287
285
  properties: {
288
286
  is_super_like: [false, false, false, false, false, false, false, false, false, true],
289
- weekend_surge: [false],
290
287
  swipe_source: ["feed", "feed", "feed", "discover", "boost", "nearby"],
291
288
  },
292
289
  },
@@ -302,9 +299,6 @@ const config = {
302
299
  weight: 4,
303
300
  properties: {
304
301
  match_score: u.weighNumRange(50, 100, 0.5, 75),
305
- complete_profile: [false],
306
- super_like_match: [false],
307
- premium_boost: [false],
308
302
  },
309
303
  },
310
304
  {
@@ -335,7 +329,6 @@ const config = {
335
329
  weight: 1,
336
330
  properties: {
337
331
  venue_type: ["coffee", "dinner", "drinks", "activity", "virtual"],
338
- complete_profile_power: [false],
339
332
  },
340
333
  },
341
334
  {
@@ -343,7 +336,6 @@ const config = {
343
336
  weight: 5,
344
337
  properties: {
345
338
  viewer_source: ["feed", "discover", "liked_you", "nearby"],
346
- premium_boost: [false],
347
339
  },
348
340
  },
349
341
  {
@@ -352,7 +344,6 @@ const config = {
352
344
  properties: {
353
345
  plan: ["Premium", "Premium", "Premium", "Elite"],
354
346
  price_usd: [14.99, 14.99, 14.99, 29.99],
355
- valentines: [false],
356
347
  },
357
348
  },
358
349
  {
@@ -385,7 +376,6 @@ const config = {
385
376
  weight: 8,
386
377
  properties: {
387
378
  session_duration_mins: u.weighNumRange(1, 120, 0.3, 8),
388
- ghosted: [false],
389
379
  },
390
380
  },
391
381
  ],
@@ -430,7 +420,7 @@ const config = {
430
420
  // ── SuperProps ──────────────────────────────────────────
431
421
  superProps: {
432
422
  subscription: ["Free", "Free", "Free", "Premium", "Elite"],
433
- platform: ["ios", "ios", "android"],
423
+ Platform: ["ios", "ios", "android"],
434
424
  },
435
425
 
436
426
  // ── UserProps ──────────────────────────────────────────
@@ -443,7 +433,7 @@ const config = {
443
433
  total_matches: u.weighNumRange(0, 200, 0.3, 15),
444
434
  total_messages_sent: u.weighNumRange(0, 500, 0.3, 30),
445
435
  profile_completeness: ["incomplete", "incomplete", "basic", "basic", "complete"],
446
- platform: ["ios", "ios", "android"],
436
+ Platform: ["ios", "ios", "android"],
447
437
  },
448
438
 
449
439
  // ── SCD Props ──────────────────────────────────────────
@@ -461,25 +451,25 @@ const config = {
461
451
  mirrorProps: {},
462
452
  lookupTables: [],
463
453
 
464
- // ── Hook Function ──────────────────────────────────────
465
454
  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
455
 
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;
456
+ // HOOK 9 (T2C): MATCH FLOW TIME-TO-CONVERT (funnel-post)
457
+ // Elite users complete swipe→match→message funnel 1.4x faster
458
+ // (factor 0.71); Free users 1.4x slower (factor 1.4).
459
+ if (type === "funnel-post") {
460
+ const segment = meta?.profile?.subscription;
461
+ if (Array.isArray(record) && record.length > 1) {
462
+ const factor = (
463
+ segment === "Elite" ? 0.71 :
464
+ segment === "Free" ? 1.4 :
465
+ 1.0
466
+ );
467
+ if (factor !== 1.0) {
468
+ for (let i = 1; i < record.length; i++) {
469
+ const prev = dayjs(record[i - 1].time);
470
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
471
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
472
+ }
483
473
  }
484
474
  }
485
475
  }
@@ -487,25 +477,25 @@ const config = {
487
477
  // ─── EVERYTHING-LEVEL HOOKS ──────────────────────────────────────
488
478
 
489
479
  if (type === "everything") {
480
+ const datasetStart = dayjs.unix(meta.datasetStart);
481
+ const VDAY_WINDOW_START = datasetStart.add(58, "days");
482
+ const VDAY_WINDOW_END = datasetStart.add(63, "days");
490
483
  const events = record;
491
484
  if (!events || events.length === 0) return record;
492
485
 
493
486
  const profile = meta.profile || {};
494
487
 
495
- // Stamp superProps from profile for consistency
496
488
  events.forEach(e => {
497
489
  if (profile.subscription) e.subscription = profile.subscription;
498
490
  if (profile.platform) e.platform = profile.platform;
499
491
  });
500
492
 
501
- // ── First pass: scan user patterns ──
502
493
  let photoUploadCount = 0;
503
494
  let promptAnsweredCount = 0;
504
495
  let hasBioUpdated = false;
505
- let matchEvents = [];
506
- let messageSentEvents = [];
507
- let hasSuperLike = false;
508
- let superLikeEvents = [];
496
+ const matchEvents = [];
497
+ const messageSentEvents = [];
498
+ const superLikeEvents = [];
509
499
  let hasPhoneExchangedEarly = false;
510
500
  let hasDateScheduledEarly = false;
511
501
  let firstEventTime = null;
@@ -519,13 +509,9 @@ const config = {
519
509
  if (event.event === "bio updated") hasBioUpdated = true;
520
510
  if (event.event === "match received") matchEvents.push(event);
521
511
  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
- }
512
+ if (event.event === "swipe right" && event.is_super_like === true) superLikeEvents.push(event);
526
513
  });
527
514
 
528
- // Check early milestones (first 14 days)
529
515
  if (firstEventTime) {
530
516
  const earlyWindow = dayjs(firstEventTime).add(14, "days");
531
517
  events.forEach(event => {
@@ -537,65 +523,59 @@ const config = {
537
523
  });
538
524
  }
539
525
 
540
- // ── HOOK 1: PHOTO UPLOAD CONVERSION ─────────────
541
- // Users with 4+ photo uploads get 5x match events
542
- if (photoUploadCount >= 4 && matchEvents.length > 0) {
526
+ // HOOK 1 + HOOK 9: PHOTO MAGIC NUMBER (no flags)
527
+ // Sweet 2-5 photos clone 2-4 extra match events per existing.
528
+ // Over 6+ photos drop match_score by 35% on match received events
529
+ // (over-curated profile reads as fake/staged).
530
+ if (photoUploadCount >= 2 && photoUploadCount <= 5 && matchEvents.length > 0) {
543
531
  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
- }
532
+ matchEvents.forEach(m => {
533
+ const extras = chance.integer({ min: 2, max: 4 });
534
+ for (let i = 0; i < extras; i++) {
535
+ events.push({
536
+ ...matchTemplate,
537
+ time: dayjs(m.time).add(chance.integer({ min: 1, max: 180 }), "minutes").toISOString(),
538
+ user_id: m.user_id,
539
+ match_score: chance.integer({ min: 60, max: 98 }),
540
+ });
541
+ }
542
+ });
543
+ } else if (photoUploadCount >= 6) {
544
+ events.forEach(e => {
545
+ if (e.event === "match received" && typeof e.match_score === "number") {
546
+ e.match_score = Math.max(20, Math.round(e.match_score * 0.65));
547
+ }
548
+ });
558
549
  }
559
550
 
560
- // ── HOOK 2: WEEKEND SWIPE SURGE (everything part)
561
- // Tag Sunday evening swipes AFTER sessionization has finalized times
562
- events.forEach(event => {
563
- if (event.event === "swipe right") {
564
- const dow = new Date(event.time).getUTCDay();
565
- const hr = new Date(event.time).getUTCHours();
566
- 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
551
+ // HOOK 2: WEEKEND SWIPE SURGE Sunday swipes get heavy cloning
552
+ // to overcome the soup DOW weight deficit. Evening swipes (18-23)
553
+ // get 4 clones; daytime Sunday swipes get 2 clones.
554
+ // No flag discover via day-of-week chart.
572
555
  for (let idx = events.length - 1; idx >= 0; idx--) {
573
556
  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 })) {
583
- events.push({
584
- ...event,
585
- time: etime.add(chance.integer({ min: 5, max: 60 }), "minutes").toISOString(),
586
- user_id: event.user_id,
587
- });
557
+ if (event.event === "swipe right") {
558
+ const dow = new Date(event.time).getUTCDay();
559
+ if (dow === 0) {
560
+ const hr = new Date(event.time).getUTCHours();
561
+ const clones = (hr >= 18 && hr <= 23) ? 4 : 2;
562
+ const etime = dayjs(event.time);
563
+ for (let c = 0; c < clones; c++) {
564
+ events.push({
565
+ ...event,
566
+ time: etime.add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
567
+ user_id: event.user_id,
568
+ });
569
+ }
588
570
  }
589
571
  }
590
572
  }
591
573
 
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
-
574
+ // HOOK 3: SUPER-LIKE EFFECT — clone 3 extra match events per
575
+ // super-like, near in time. No flag discover via funnel
576
+ // "swipe right where is_super_like=true" "match received".
577
+ if (superLikeEvents.length > 0) {
578
+ const matchTemplate = matchEvents[0] || events[0];
599
579
  superLikeEvents.forEach(sle => {
600
580
  for (let i = 0; i < 3; i++) {
601
581
  events.push({
@@ -604,88 +584,112 @@ const config = {
604
584
  time: dayjs(sle.time).add(chance.integer({ min: 5, max: 120 }), "minutes").toISOString(),
605
585
  user_id: sle.user_id,
606
586
  match_score: chance.integer({ min: 70, max: 99 }),
607
- complete_profile: false,
608
- super_like_match: true,
609
- premium_boost: false,
610
587
  });
611
588
  }
612
589
  });
613
590
  }
614
591
 
615
- // ── HOOK 4: PREMIUM MATCH BOOST ─────────────────
616
- // Premium: 2x matches. Elite: 4x matches + profile viewed (see-who-liked-you).
592
+ // HOOK 5: GHOSTING CHURN users with match but no message within
593
+ // 48hrs lose 80% of post-match events. No flag.
594
+ // (runs BEFORE premium boost so injected premium matches survive)
595
+ if (matchEvents.length > 0) {
596
+ let hasTimely = false;
597
+ for (const m of matchEvents) {
598
+ const matchTime = dayjs(m.time);
599
+ const deadline = matchTime.add(48, "hours");
600
+ for (const msg of messageSentEvents) {
601
+ const msgTime = dayjs(msg.time);
602
+ if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
603
+ hasTimely = true;
604
+ break;
605
+ }
606
+ }
607
+ if (hasTimely) break;
608
+ }
609
+ if (!hasTimely) {
610
+ const earliestMatch = matchEvents.reduce((min, m) =>
611
+ dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
612
+ );
613
+ const churnAfter = dayjs(earliestMatch.time);
614
+ for (let i = events.length - 1; i >= 0; i--) {
615
+ if (dayjs(events[i].time).isAfter(churnAfter) && chance.bool({ likelihood: 80 })) {
616
+ events.splice(i, 1);
617
+ }
618
+ }
619
+ }
620
+ }
621
+
622
+ // HOOK 4: PREMIUM MATCH BOOST — Premium 2x, Elite 4x match events.
623
+ // Elite users also get profile-viewed events injected (see-who-liked-you).
624
+ // Reads subscription from profile. Runs AFTER ghosting churn so
625
+ // injected matches are not culled.
617
626
  const sub = profile.subscription;
618
627
  if ((sub === "Premium" || sub === "Elite") && matchEvents.length > 0) {
619
- const multiplier = sub === "Elite" ? 3 : 1; // +3 = 4x total for Elite, +1 = 2x for Premium
628
+ // Count surviving match events post-churn
629
+ const survivingMatches = events.filter(e => e.event === "match received");
630
+ const baseCount = survivingMatches.length || 1;
631
+ const targetMultiplier = sub === "Elite" ? 4 : 2;
632
+ const toAdd = Math.max(0, baseCount * targetMultiplier - baseCount);
620
633
  const matchTemplate = matchEvents[0];
621
-
622
- for (let i = 0; i < matchEvents.length * multiplier; i++) {
623
- const sourceMatch = matchEvents[i % matchEvents.length];
634
+ for (let i = 0; i < toAdd; i++) {
635
+ const sourceMatch = survivingMatches[i % survivingMatches.length] || matchTemplate;
624
636
  events.push({
625
637
  ...matchTemplate,
626
- event: "match received",
627
638
  time: dayjs(sourceMatch.time).add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
628
639
  user_id: sourceMatch.user_id,
629
640
  match_score: chance.integer({ min: 65, max: 99 }),
630
- complete_profile: false,
631
- super_like_match: false,
632
- premium_boost: true,
633
641
  });
634
642
  }
635
-
636
- // Elite exclusive: inject "profile viewed" events (see-who-liked-you)
637
643
  if (sub === "Elite") {
638
644
  const viewTemplate = events.find(e => e.event === "profile viewed") || matchTemplate;
639
- matchEvents.forEach(m => {
645
+ survivingMatches.forEach(m => {
640
646
  events.push({
641
647
  ...viewTemplate,
642
648
  event: "profile viewed",
643
649
  time: dayjs(m.time).subtract(chance.integer({ min: 10, max: 120 }), "minutes").toISOString(),
644
650
  user_id: m.user_id,
645
651
  viewer_source: "liked_you",
646
- premium_boost: true,
647
652
  });
648
653
  });
649
654
  }
650
655
  }
651
656
 
652
- // ── HOOK 6: BIO + PROMPT POWER USERS ────────────
653
- // Users with bio updated AND 3+ prompts get 4x date scheduled
657
+ // HOOK 6: BIO + PROMPT POWER USERS — bio + 3+ prompts → 3 extra
658
+ // cloned date events per existing. No flag.
654
659
  if (hasBioUpdated && promptAnsweredCount >= 3) {
655
660
  const dateEvents = events.filter(e => e.event === "date scheduled");
656
661
  if (dateEvents.length > 0) {
657
662
  const dateTemplate = dateEvents[0];
658
- const extraDates = dateEvents.length * 3; // +3 = 4x total
659
663
  const venueTypes = ["coffee", "dinner", "drinks", "activity", "virtual"];
660
- for (let i = 0; i < extraDates; i++) {
664
+ for (let i = 0; i < dateEvents.length * 3; i++) {
661
665
  const sourceDate = dateEvents[i % dateEvents.length];
662
666
  events.push({
663
667
  ...dateTemplate,
664
- event: "date scheduled",
665
668
  time: dayjs(sourceDate.time).add(chance.integer({ min: 1, max: 72 }), "hours").toISOString(),
666
669
  user_id: sourceDate.user_id,
667
670
  venue_type: chance.pickone(venueTypes),
668
- complete_profile_power: true,
669
671
  });
670
672
  }
671
673
  }
672
674
  }
673
675
 
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);
676
+ // HOOK 7: VALENTINE'S DAY SPIKE clone profile-created events during
677
+ // days 58-63 (3x volume), plus clone premium-upgrade events 5x. No flag.
678
+ const vdaySignups = events.filter(e =>
679
+ e.event === "profile created" &&
680
+ dayjs(e.time).isAfter(VDAY_WINDOW_START) &&
681
+ dayjs(e.time).isBefore(VDAY_WINDOW_END)
682
+ );
677
683
  vdaySignups.forEach(signup => {
678
- for (let i = 0; i < 2; i++) { // +2 clones = 3x total
684
+ for (let i = 0; i < 2; i++) {
679
685
  events.push({
680
686
  ...signup,
681
687
  time: dayjs(signup.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
682
688
  user_id: signup.user_id,
683
- valentines: true,
684
689
  });
685
690
  }
686
691
  });
687
692
 
688
- // 5x premium upgrade events for users active during V-Day window
689
693
  const vdayUpgrades = events.filter(e =>
690
694
  e.event === "premium upgrade" &&
691
695
  dayjs(e.time).isAfter(VDAY_WINDOW_START) &&
@@ -694,74 +698,29 @@ const config = {
694
698
  if (vdayUpgrades.length > 0) {
695
699
  const upgradeTemplate = vdayUpgrades[0];
696
700
  vdayUpgrades.forEach(upgrade => {
697
- for (let i = 0; i < 4; i++) { // +4 clones = 5x total
701
+ for (let i = 0; i < 4; i++) {
698
702
  events.push({
699
703
  ...upgradeTemplate,
700
- event: "premium upgrade",
701
704
  time: dayjs(upgrade.time).add(chance.integer({ min: 1, max: 24 }), "hours").toISOString(),
702
705
  user_id: upgrade.user_id,
703
706
  plan: upgrade.plan,
704
707
  price_usd: upgrade.price_usd,
705
- valentines: true,
706
708
  });
707
709
  }
708
710
  });
709
711
  }
710
712
 
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
713
+ // HOOK 8: OFF-APP RETENTION — users with phone-exchanged or
714
+ // date-scheduled in first 14 days get extra cloned app-open + swipe
715
+ // events past day 30. Non-milestone users lose 80% of post-day-30
716
+ // events. No flag.
754
717
  if (firstEventTime) {
755
718
  const day30 = dayjs(firstEventTime).add(30, "days");
756
719
  const hasEarlyMilestone = hasPhoneExchangedEarly || hasDateScheduledEarly;
757
-
758
720
  if (hasEarlyMilestone) {
759
- // Good retention: inject extra events after day 30 to sustain engagement
760
721
  const appOpenedTemplate = events.find(e => e.event === "app opened") || events[0];
761
722
  const swipeTemplate = events.find(e => e.event === "swipe right") || events[0];
762
723
  const postDay30Events = events.filter(e => dayjs(e.time).isAfter(day30));
763
-
764
- // If they don't have enough post-day30 events, inject some
765
724
  if (postDay30Events.length < events.length * 0.3) {
766
725
  const retentionCount = Math.floor(events.length * 0.3);
767
726
  for (let i = 0; i < retentionCount; i++) {
@@ -775,14 +734,9 @@ const config = {
775
734
  }
776
735
  }
777
736
  } else {
778
- // No milestone: churn after day 30
779
737
  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
- }
738
+ if (dayjs(events[i].time).isAfter(day30) && chance.bool({ likelihood: 80 })) {
739
+ events.splice(i, 1);
786
740
  }
787
741
  }
788
742
  }