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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +51 -13
  3. package/dungeons/technical/ad-spend.js +2 -2
  4. package/dungeons/technical/anonymous-users.js +2 -2
  5. package/dungeons/technical/array-of-object-lookup.js +2 -4
  6. package/dungeons/technical/experiments.js +2 -2
  7. package/dungeons/technical/foobar.js +2 -2
  8. package/dungeons/technical/group-analytics.js +2 -2
  9. package/dungeons/technical/mirror-strategies.js +2 -2
  10. package/dungeons/technical/nested-objects.js +2 -2
  11. package/dungeons/technical/retention-cadence.js +2 -3
  12. package/dungeons/technical/sanity.js +2 -2
  13. package/dungeons/technical/scale-test.js +2 -2
  14. package/dungeons/technical/scd.js +2 -2
  15. package/dungeons/technical/simple.js +5 -6
  16. package/dungeons/technical/simplest-schema.json +5 -0
  17. package/dungeons/technical/simplest.js +2 -2
  18. package/dungeons/technical/text-generation.js +3 -3
  19. package/dungeons/vertical/ai-platform.js +858 -0
  20. package/dungeons/vertical/community.js +84 -40
  21. package/dungeons/vertical/crypto.js +830 -0
  22. package/dungeons/vertical/dating.js +744 -0
  23. package/dungeons/vertical/devtools.js +175 -69
  24. package/dungeons/vertical/ecommerce.js +242 -94
  25. package/dungeons/vertical/education.js +330 -313
  26. package/dungeons/vertical/fintech.js +442 -313
  27. package/dungeons/vertical/fitness.js +143 -61
  28. package/dungeons/vertical/food-delivery.js +327 -353
  29. package/dungeons/vertical/gaming.js +912 -382
  30. package/dungeons/vertical/healthcare.js +142 -63
  31. package/dungeons/vertical/insurance-application.js +170 -76
  32. package/dungeons/vertical/logistics.js +115 -20
  33. package/dungeons/vertical/marketplace.js +152 -58
  34. package/dungeons/vertical/media.js +248 -384
  35. package/dungeons/vertical/real-estate.js +781 -0
  36. package/dungeons/vertical/sass.js +255 -266
  37. package/dungeons/vertical/social.js +264 -206
  38. package/dungeons/vertical/travel.js +117 -41
  39. package/index.js +17 -17
  40. package/lib/core/config-validator.js +159 -31
  41. package/lib/core/context.js +10 -24
  42. package/lib/core/storage.js +6 -1
  43. package/lib/generators/events.js +11 -14
  44. package/lib/generators/funnels.js +12 -4
  45. package/lib/generators/mirror.js +3 -2
  46. package/lib/generators/product-names.js +1 -1
  47. package/lib/generators/scd.js +2 -1
  48. package/lib/generators/text.js +1 -1
  49. package/lib/orchestrators/user-loop.js +81 -47
  50. package/lib/templates/macro-presets.js +111 -0
  51. package/lib/templates/soup-presets.js +19 -36
  52. package/lib/utils/utils.js +71 -39
  53. package/package.json +8 -2
  54. package/scripts/smoke-test-all.mjs +162 -0
  55. package/scripts/verify-runner.mjs +72 -24
  56. package/types.d.ts +251 -51
  57. package/dungeons/technical/ad-spend-schema.json +0 -128
  58. package/dungeons/technical/anonymous-users-schema.json +0 -92
  59. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  60. package/dungeons/technical/experiments-schema.json +0 -203
  61. package/dungeons/technical/foobar-schema.json +0 -362
  62. package/dungeons/technical/group-analytics-schema.json +0 -241
  63. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  64. package/dungeons/technical/nested-objects-schema.json +0 -145
  65. package/dungeons/technical/retention-cadence-schema.json +0 -37
  66. package/dungeons/technical/sanity-schema.json +0 -185
  67. package/dungeons/technical/scale-test-schema.json +0 -70
  68. package/dungeons/technical/scd-schema.json +0 -467
  69. package/dungeons/technical/simple-schema.json +0 -362
  70. package/dungeons/technical/text-generation-schema.json +0 -1062
  71. package/dungeons/user/.gitkeep +0 -0
  72. package/dungeons/vertical/community-schema.json +0 -579
  73. package/dungeons/vertical/devtools-schema.json +0 -601
  74. package/dungeons/vertical/ecommerce-schema.json +0 -604
  75. package/dungeons/vertical/education-schema.json +0 -5686
  76. package/dungeons/vertical/fintech-schema.json +0 -630
  77. package/dungeons/vertical/fitness-schema.json +0 -530
  78. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  79. package/dungeons/vertical/gaming-schema.json +0 -438
  80. package/dungeons/vertical/healthcare-schema.json +0 -549
  81. package/dungeons/vertical/insurance-application-schema.json +0 -485
  82. package/dungeons/vertical/logistics-schema.json +0 -574
  83. package/dungeons/vertical/marketplace-schema.json +0 -533
  84. package/dungeons/vertical/media-schema.json +0 -4749
  85. package/dungeons/vertical/rpg-schema.json +0 -2491
  86. package/dungeons/vertical/rpg.js +0 -976
  87. package/dungeons/vertical/sass-schema.json +0 -3128
  88. package/dungeons/vertical/social-schema.json +0 -620
  89. package/dungeons/vertical/travel-schema.json +0 -580
@@ -2,7 +2,7 @@
2
2
  const SEED = "harness-media";
3
3
  const num_days = 100;
4
4
  const num_users = 5_000;
5
- const avg_events_per_user = 120;
5
+ const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
8
8
  // ── env overrides ──
@@ -44,179 +44,225 @@ const chance = u.initChance(SEED);
44
44
 
45
45
  /**
46
46
  * ═══════════════════════════════════════════════════════════════════════════════
47
- * ANALYTICS HOOKS (8 architected patterns)
47
+ * ANALYTICS HOOKS (10 hooks)
48
+ *
49
+ * Adds 10. CORE VIEWING LOOP TIME-TO-CONVERT: premium 0.71x faster, free 1.25x
50
+ * slower (funnel-post). Discover via funnel median TTC by subscription_plan.
51
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
52
+ * Cross-event MIN→MIN SQL queries on raw events do NOT show this.
48
53
  * ═══════════════════════════════════════════════════════════════════════════════
49
54
  *
55
+ * NOTE: All cohort effects are HIDDEN — discoverable only via behavioral cohorts
56
+ * (count an event per user, then measure downstream). No cohort flag is stamped
57
+ * on events. Time-window patterns mutate config-defined props or drop events.
58
+ *
50
59
  * ───────────────────────────────────────────────────────────────────────────────
51
- * 1. GENRE FUNNEL CONVERSION (funnel-pre)
60
+ * 1. GENRE FUNNEL CONVERSION (everything)
52
61
  * ───────────────────────────────────────────────────────────────────────────────
53
62
  *
54
- * PATTERN: Comedy and Animation content has 1.3x higher funnel conversion rates,
55
- * while Documentary content has 0.7x conversion (users browse but abandon more).
63
+ * PATTERN: 25% of "playback completed" events on documentary genre are dropped,
64
+ * depressing the documentary funnel completion rate. No flag discover via
65
+ * funnel breakdown by genre.
66
+ *
67
+ * HOW TO FIND IT IN MIXPANEL:
56
68
  *
57
- * HOW TO FIND IT:
58
- * - Funnels: "content browsed" -> "content selected" -> "playback started" -> "playback completed"
69
+ * Report 1: Discovery Funnel by Genre
70
+ * - Report type: Funnels
71
+ * - Steps: "content browsed" -> "content selected" -> "playback started" -> "playback completed"
59
72
  * - Breakdown: "genre"
60
- * - Expected: Comedy/Animation convert at ~65% vs Documentary at ~35%
61
- * - Also try: Insights on "content selected", breakdown by "genre_boost"
73
+ * - Expected: documentary ~ 0.7x conversion vs other genres
74
+ *
75
+ * REAL-WORLD ANALOGUE: Heavier content gets started but abandoned more often
76
+ * than light comedy or animation.
62
77
  *
63
78
  * ───────────────────────────────────────────────────────────────────────────────
64
79
  * 2. BINGE-WATCHING PATTERN (everything)
65
80
  * ───────────────────────────────────────────────────────────────────────────────
66
81
  *
67
- * PATTERN: Users who complete 3+ episodes consecutively become "binge-watchers":
68
- * - Extra playback started + playback completed events are spliced in
69
- * - Completion percentages are 90-100% (they finish every episode)
70
- * - Pause events are reduced by 60% (they don't stop watching)
71
- * - Events tagged with binge_session = true
82
+ * PATTERN: Users with 3+ consecutive playback-completed events get extra
83
+ * cloned playback-started + playback-completed pairs (with unique offset
84
+ * timestamps). 60% of their pause events are dropped. No flag discover by
85
+ * binning users on completion-streak length and comparing per-user completions.
86
+ *
87
+ * HOW TO FIND IT IN MIXPANEL:
72
88
  *
73
- * HOW TO FIND IT:
74
- * - Insights: "playback completed", total events per user, breakdown by "binge_session"
75
- * - Expected: binge_session=true users have 40-60% more completions per user
76
- * - Also try: Average of "completion_percent" by "binge_session" (90-100% vs ~70%)
89
+ * Report 1: Completions per User by Streak Cohort
90
+ * - Report type: Insights (with cohort)
91
+ * - Cohort A: users with >= 3 consecutive "playback completed"
92
+ * - Cohort B: rest
93
+ * - Event: "playback completed"
94
+ * - Measure: Total per user
95
+ * - Expected: A ~ 1.5x more completions per user
96
+ *
97
+ * REAL-WORLD ANALOGUE: Autoplay and cliffhangers push hooked viewers through
98
+ * entire seasons in a sitting.
77
99
  *
78
100
  * ───────────────────────────────────────────────────────────────────────────────
79
- * 3. WEEKEND vs WEEKDAY PATTERNS (event)
101
+ * 3. WEEKEND VS WEEKDAY PATTERNS (everything)
80
102
  * ───────────────────────────────────────────────────────────────────────────────
81
103
  *
82
- * PATTERN: Weekend viewing sessions are 1.5x longer than weekday sessions.
83
- * Weekday viewing concentrates in evening prime-time (6PM-11PM).
104
+ * PATTERN: Saturday/Sunday "playback completed" events get watch_duration_min
105
+ * boosted 1.5x. Mutates the existing watch_duration_min prop. No flag —
106
+ * discover via day-of-week breakdown.
107
+ *
108
+ * HOW TO FIND IT IN MIXPANEL:
109
+ *
110
+ * Report 1: Avg Watch Duration by Day of Week
111
+ * - Report type: Insights
112
+ * - Event: "playback completed"
113
+ * - Measure: Average of "watch_duration_min"
114
+ * - Breakdown: Day of week
115
+ * - Expected: Sat/Sun ~ 1.5x weekday avg
84
116
  *
85
- * HOW TO FIND IT:
86
- * - Insights: "playback completed", average of "watch_duration_min", breakdown by "weekend_viewing"
87
- * - Expected: weekend_viewing=true ~67 min avg vs false ~45 min avg (1.5x)
88
- * - Also try: Total "playback completed" events by "prime_time" (60-70% of weekday views)
117
+ * REAL-WORLD ANALOGUE: Weekend viewing stretches into multi-hour marathons.
89
118
  *
90
119
  * ───────────────────────────────────────────────────────────────────────────────
91
120
  * 4. AD FATIGUE CHURN (everything)
92
121
  * ───────────────────────────────────────────────────────────────────────────────
93
122
  *
94
- * PATTERN: Free-tier users who see 10+ ad impressions experience 50% churn
95
- * after day 45 of their lifecycle.
123
+ * PATTERN: Free-tier users with 10+ ad impressions lose 50% of events after
124
+ * day 45 of their lifecycle. No flag — discover via cohort retention.
125
+ *
126
+ * HOW TO FIND IT IN MIXPANEL:
127
+ *
128
+ * Report 1: Retention by Ad Exposure Cohort
129
+ * - Report type: Retention
130
+ * - Cohort A: free-tier users with >= 10 "ad impression"
131
+ * - Cohort B: free-tier users with < 10 ads
132
+ * - Expected: A ~ 50% retention drop after day 45
96
133
  *
97
- * HOW TO FIND IT:
98
- * - Retention: any event (filter subscription_plan=free), segment by ad impression count >= 10 vs < 10
99
- * - Expected: 10+ ad users show ~50% retention drop after day 45
100
- * - Also try: Insights line chart, any event filtered by "ad_fatigue" = true, weekly trend
134
+ * REAL-WORLD ANALOGUE: Ad-supported tiers carry a tolerance ceiling.
101
135
  *
102
136
  * ───────────────────────────────────────────────────────────────────────────────
103
137
  * 5. NEW RELEASE SPIKE (event)
104
138
  * ───────────────────────────────────────────────────────────────────────────────
105
139
  *
106
- * PATTERN: On day 50, a blockbuster movie releases, creating a content spike:
107
- * - 20% of content selected and playback started events redirect to the blockbuster
108
- * - Content rated events for the blockbuster skew to 4-5 star ratings
109
- * - All affected events tagged with blockbuster_release = true
140
+ * PATTERN: Days 50-65, 20% of "content selected" / "playback started" events
141
+ * have content_id swapped to the blockbuster id and content_type to "movie".
142
+ * 20% of "content rated" in the window get a 4-5 star rating on the blockbuster.
143
+ * Mutates existing props no flag.
110
144
  *
111
- * HOW TO FIND IT:
112
- * - Insights line chart: "content selected", filter "blockbuster_release" = true, daily trend
113
- * - Expected: Zero before day 50, then ~20% of content selections
114
- * - Also try: "content rated" average of "rating" by "blockbuster_release" (4-5 vs ~3.5)
145
+ * HOW TO FIND IT IN MIXPANEL:
146
+ *
147
+ * Report 1: Selections by content_id Over Time
148
+ * - Report type: Insights
149
+ * - Event: "content selected"
150
+ * - Measure: Total
151
+ * - Filter: content_id = "<blockbuster id>"
152
+ * - Line chart by day
153
+ * - Expected: zero before day 50, ~ 20% of selections days 50-65
154
+ *
155
+ * REAL-WORLD ANALOGUE: Tentpole releases dominate traffic.
115
156
  *
116
157
  * ───────────────────────────────────────────────────────────────────────────────
117
158
  * 6. KIDS PROFILE SAFETY (event)
118
159
  * ───────────────────────────────────────────────────────────────────────────────
119
160
  *
120
- * PATTERN: 15% of the time, events are tagged as kids profile activity:
121
- * - Content selection restricted to animation and documentary genres
122
- * - Ad impressions are blocked (ad_blocked = true)
123
- * - Events tagged with kids_profile = true
124
- *
125
- * HOW TO FIND IT:
126
- * - Insights: "content selected", breakdown by "genre", filter "kids_profile" = true
127
- * - Expected: 100% animation and documentary genres only
128
- * - Also try: "ad impression" breakdown by "kids_profile" — kids should have ad_blocked=true
161
+ * PATTERN: 15% of "content selected" / "playback started" events get genre
162
+ * restricted to "animation" or "documentary". Mutates the existing genre prop.
129
163
  *
130
- * ───────────────────────────────────────────────────────────────────────────────
131
- * 7. RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
132
- * ───────────────────────────────────────────────────────────────────────────────
164
+ * HOW TO FIND IT IN MIXPANEL:
133
165
  *
134
- * PATTERN: After day 60, the engagement loop funnel (recommendation clicked ->
135
- * playback started -> content rated) gets a 1.5x conversion rate boost,
136
- * simulating a recommendation engine improvement.
166
+ * Report 1: Genre Distribution Over Time
167
+ * - Report type: Insights
168
+ * - Event: "content selected"
169
+ * - Measure: Total
170
+ * - Breakdown: "genre"
171
+ * - Expected: animation + documentary share is elevated above pure random
137
172
  *
138
- * HOW TO FIND IT:
139
- * - Funnels: "recommendation clicked" -> "playback started" -> "playback completed" -> "content rated"
140
- * - Breakdown: "improved_recs"
141
- * - Expected: improved_recs=true shows ~1.5x higher conversion rate
142
- * - Also try: Insights line chart of "recommendation clicked" filtered by "improved_recs" = true
173
+ * REAL-WORLD ANALOGUE: Kids profiles enforce age-appropriate content.
143
174
  *
144
175
  * ───────────────────────────────────────────────────────────────────────────────
145
- * 8. SUBTITLE USERS WATCH MORE (everything)
176
+ * 7. RECOMMENDATION ENGINE IMPROVEMENT (everything)
146
177
  * ───────────────────────────────────────────────────────────────────────────────
147
178
  *
148
- * PATTERN: Users who enable subtitles have measurably higher engagement:
149
- * - 25% higher completion_percent on playback completed events (capped at 100)
150
- * - 15% longer watch_duration_min
151
- * - 20% more playback completed events (extra content consumption)
152
- * - Events tagged with subtitle_user = true
179
+ * PATTERN: Pre-day-60 "content rated" events get 30% dropped, depressing the
180
+ * recommendation funnel conversion in the first 60 days. No flag — discover
181
+ * via funnel/insights line chart by day.
153
182
  *
154
- * HOW TO FIND IT:
155
- * - Insights: "playback completed", average of "completion_percent", breakdown by "subtitle_user"
156
- * - Expected: subtitle_user=true ~85% completion vs false ~68% (1.25x)
157
- * - Also try: Average of "watch_duration_min" by "subtitle_user" (~15% longer)
158
- * - Also try: Total events per user by "subtitle_user" (~20% more completions)
183
+ * HOW TO FIND IT IN MIXPANEL:
159
184
  *
160
- * ═══════════════════════════════════════════════════════════════════════════════
161
- * EXPECTED METRICS SUMMARY
162
- * ═══════════════════════════════════════════════════════════════════════════════
185
+ * Report 1: Content Rated Volume Over Time
186
+ * - Report type: Insights
187
+ * - Event: "content rated"
188
+ * - Measure: Total
189
+ * - Line chart by day
190
+ * - Expected: visible step-up at day 60
163
191
  *
164
- * Hook | Metric | Baseline | Hook Effect | Ratio
165
- * ─────────────────────────|─────────────────────────|───────────|─────────────|──────
166
- * Genre Funnel Conversion | Funnel conversion rate | 50% | 65% / 35% | 1.3x / 0.7x
167
- * Binge-Watching | Content consumed/user | 12 | 18-20 | ~1.5x
168
- * Weekend vs Weekday | Watch duration (min) | 45 | 67 (weekend)| 1.5x
169
- * Ad Fatigue Churn | Post-day-45 activity | 100% | 50% | 0.5x
170
- * New Release Spike | Content selections/day | baseline | +20% spike | 1.2x
171
- * Kids Profile Safety | Ad impressions | normal | 0 (dropped) | 0x
172
- * Rec Engine Improvement | Engagement funnel conv | 30% | 45% | 1.5x
173
- * Subtitle Users | Completion percent | 68% | 85% | 1.25x
192
+ * Report 2: Recommendation Funnel Conversion Over Time
193
+ * - Report type: Funnels
194
+ * - Steps: "recommendation clicked" -> "playback started" -> "content rated"
195
+ * - Compare date ranges (days 0-59 vs 60-100)
196
+ * - Expected: post-improvement window ~ 1.5x conversion
174
197
  *
175
- * ═══════════════════════════════════════════════════════════════════════════════
176
- * ADVANCED ANALYSIS IDEAS
177
- * ═══════════════════════════════════════════════════════════════════════════════
198
+ * REAL-WORLD ANALOGUE: Rec model upgrades produce a step-change in CTR.
178
199
  *
179
- * CROSS-HOOK PATTERNS:
200
+ * ───────────────────────────────────────────────────────────────────────────────
201
+ * 8. SUBTITLE USERS WATCH MORE (everything)
202
+ * ───────────────────────────────────────────────────────────────────────────────
203
+ *
204
+ * PATTERN: Users who toggle subtitles enabled get 1.25x completion_percent
205
+ * (cap 100), 1.15x watch_duration_min, plus 20% extra cloned playback-completed
206
+ * events. No flag — discover via cohort builder on subtitle-toggled-enabled.
180
207
  *
181
- * 1. Binge + Subtitle: Do subtitle-enabled binge-watchers have the highest
182
- * total watch hours? (Hooks #2 + #8 combined)
208
+ * HOW TO FIND IT IN MIXPANEL:
183
209
  *
184
- * 2. Ad Fatigue + Blockbuster: Does the blockbuster release (Hook #5) rescue
185
- * free-tier users from ad fatigue churn (Hook #4)?
210
+ * Report 1: Completion by Subtitle Cohort
211
+ * - Report type: Insights (with cohort)
212
+ * - Cohort A: users with >= 1 "subtitle toggled" with action="enabled"
213
+ * - Cohort B: rest
214
+ * - Event: "playback completed"
215
+ * - Measure: Average of "completion_percent"
216
+ * - Expected: A ~ 85% vs B ~ 68%
186
217
  *
187
- * 3. Kids + Weekend: Is kids profile viewing concentrated on weekends (Hook #6
188
- * + #3)? Does weekend kids viewing show different genre preferences?
218
+ * REAL-WORLD ANALOGUE: Subtitle adoption correlates with attentive viewers.
189
219
  *
190
- * 4. Rec Engine + Genre: Does the recommendation engine improvement (Hook #7)
191
- * disproportionately help certain genres (Hook #1)?
220
+ * ───────────────────────────────────────────────────────────────────────────────
221
+ * 9. RECOMMENDATION-CLICKED MAGIC NUMBER (everything)
222
+ * ───────────────────────────────────────────────────────────────────────────────
192
223
  *
193
- * 5. Subtitle + Binge + Weekend: The "super viewer" - subtitle-enabled,
194
- * binge-watching on weekends. What is their lifetime watch hours?
224
+ * PATTERN: Users in the 4-6 recommendation-clicked sweet spot get 1.25x
225
+ * watch_duration_min on playback-completed events. Users with 7+ rec clicks
226
+ * are over-engaged (decision fatigue); 30% of their playback-completed events
227
+ * drop. No flag — discover by binning users on rec-click count.
195
228
  *
196
- * COHORT ANALYSIS:
229
+ * HOW TO FIND IT IN MIXPANEL:
197
230
  *
198
- * - Cohort by signup_source: Do referral users binge more than organic?
199
- * - Cohort by device_type: Do smart TV users watch longer than mobile?
200
- * - Cohort by subscription_plan: Do premium users binge more, or does
201
- * ad-free viewing change consumption patterns?
202
- * - Cohort by preferred_genre: Does genre preference predict churn?
231
+ * Report 1: Avg Watch Duration by Rec-Click Bucket
232
+ * - Report type: Insights (with cohort)
233
+ * - Cohort A: users with 4-6 "recommendation clicked"
234
+ * - Cohort B: users with 0-3 "recommendation clicked"
235
+ * - Event: "playback completed"
236
+ * - Measure: Average of "watch_duration_min"
237
+ * - Expected: A ~ 1.25x B
203
238
  *
204
- * FUNNEL ANALYSIS:
239
+ * Report 2: Completions per User on Heavy Rec Clickers
240
+ * - Report type: Insights (with cohort)
241
+ * - Cohort C: users with >= 7 "recommendation clicked"
242
+ * - Cohort A: users with 4-6
243
+ * - Event: "playback completed"
244
+ * - Measure: Total per user
245
+ * - Expected: C ~ 30% fewer completions per user vs A
205
246
  *
206
- * - Onboarding Funnel: account created -> content browsed -> playback started.
207
- * How does signup_source affect first-session conversion?
208
- * - Content Discovery Funnel: Does the browse_section (home vs trending vs
209
- * genre) affect downstream completion rates?
210
- * - Engagement Loop: How does recommendation algorithm type (collaborative
211
- * filtering vs editorial) affect the full loop conversion?
247
+ * REAL-WORLD ANALOGUE: A few good recs surface a watchworthy title; too many
248
+ * clicks signals indecision and drives abandonment.
212
249
  *
213
- * MONETIZATION ANALYSIS:
250
+ * ═══════════════════════════════════════════════════════════════════════════════
251
+ * EXPECTED METRICS SUMMARY
252
+ * ═══════════════════════════════════════════════════════════════════════════════
214
253
  *
215
- * - Free-to-Standard conversion: Which events predict upgrade?
216
- * - Ad tolerance threshold: At what ad count do free users start churning?
217
- * - Premium value: Do premium users actually consume more content, or just
218
- * consume at higher quality (4K)?
219
- * - Download behavior: Does offline download usage correlate with retention?
254
+ * Hook | Metric | Baseline | Hook Effect | Ratio
255
+ * ─────────────────────────|─────────────────────────|----------|-------------|------
256
+ * Genre Funnel Conversion | documentary funnel conv | 1x | 0.7x | -30%
257
+ * Binge-Watching | completions per streak | 1x | ~ 1.5x | 1.5x
258
+ * Weekend vs Weekday | weekend watch_duration | 1x | ~ 1.5x | 1.5x
259
+ * Ad Fatigue Churn | retention free+10+ads | 1x | ~ 0.5x | -50%
260
+ * New Release Spike | blockbuster id share | 0% | ~ 20% | n/a
261
+ * Kids Profile Safety | animation/doc share | baseline | + 15% | n/a
262
+ * Rec Engine Improvement | content-rated post day60| 1x | ~ 1.5x | 1.5x
263
+ * Subtitle Users | completion % | 68% | 85% | 1.25x
264
+ * Rec-Click Magic Number | sweet watch duration | 1x | 1.25x | 1.25x
265
+ * Rec-Click Magic Number | over completions/user | 1x | 0.7x | -30%
220
266
  */
221
267
 
222
268
  // Generate consistent content IDs for lookup tables and events
@@ -227,8 +273,10 @@ const blockbusterId = `blockbuster_${v.uid(8)}`;
227
273
  const config = {
228
274
  token,
229
275
  seed: SEED,
230
- numDays: num_days,
231
- numEvents: num_users * avg_events_per_user,
276
+ datasetStart: "2026-01-01T00:00:00Z",
277
+ datasetEnd: "2026-04-28T23:59:59Z",
278
+ // numDays: num_days,
279
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
232
280
  numUsers: num_users,
233
281
  hasAnonIds: false,
234
282
  hasSessionIds: true,
@@ -243,7 +291,6 @@ const config = {
243
291
  hasCampaigns: false,
244
292
  isAnonymous: false,
245
293
  hasAdSpend: false,
246
- percentUsersBornInDataset: 50,
247
294
  hasAvatar: true,
248
295
  concurrency: 1,
249
296
  writeToDisk: false,
@@ -324,10 +371,6 @@ const config = {
324
371
  properties: {
325
372
  "signup_source": ["organic", "referral", "trial_offer", "ad"],
326
373
  "plan_selected": ["free", "standard", "premium"],
327
- "weekend_viewing": [false],
328
- "prime_time": [false],
329
- "kids_profile": [false],
330
- "ad_fatigue": [false],
331
374
  }
332
375
  },
333
376
  {
@@ -336,10 +379,6 @@ const config = {
336
379
  properties: {
337
380
  "browse_section": ["home", "trending", "new_releases", "genre", "continue_watching"],
338
381
  "genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
339
- "weekend_viewing": [false],
340
- "prime_time": [false],
341
- "kids_profile": [false],
342
- "ad_fatigue": [false],
343
382
  }
344
383
  },
345
384
  {
@@ -349,11 +388,6 @@ const config = {
349
388
  "content_type": ["movie", "series", "documentary", "special"],
350
389
  "genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
351
390
  "content_id": contentIds,
352
- "blockbuster_release": [false],
353
- "kids_profile": [false],
354
- "weekend_viewing": [false],
355
- "prime_time": [false],
356
- "ad_fatigue": [false],
357
391
  }
358
392
  },
359
393
  {
@@ -365,12 +399,6 @@ const config = {
365
399
  "playback_quality": ["480p", "720p", "1080p", "4k"],
366
400
  "subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
367
401
  "playback_speed": ["0.5x", "1x", "1x", "1x", "1.25x", "1.5x", "2x"],
368
- "blockbuster_release": [false],
369
- "kids_profile": [false],
370
- "binge_session": [false],
371
- "weekend_viewing": [false],
372
- "prime_time": [false],
373
- "ad_fatigue": [false],
374
402
  }
375
403
  },
376
404
  {
@@ -381,12 +409,6 @@ const config = {
381
409
  "content_type": ["movie", "series", "documentary", "special"],
382
410
  "watch_duration_min": u.weighNumRange(5, 180, 0.5, 45),
383
411
  "completion_percent": u.weighNumRange(10, 100, 1.5, 85),
384
- "binge_session": [false],
385
- "subtitle_user": [false],
386
- "weekend_viewing": [false],
387
- "prime_time": [false],
388
- "kids_profile": [false],
389
- "ad_fatigue": [false],
390
412
  }
391
413
  },
392
414
  {
@@ -395,10 +417,6 @@ const config = {
395
417
  properties: {
396
418
  "content_id": contentIds,
397
419
  "pause_reason": ["manual", "ad_break", "buffering", "notification"],
398
- "weekend_viewing": [false],
399
- "prime_time": [false],
400
- "kids_profile": [false],
401
- "ad_fatigue": [false],
402
420
  }
403
421
  },
404
422
  {
@@ -408,11 +426,6 @@ const config = {
408
426
  "content_id": contentIds,
409
427
  "rating": u.weighNumRange(1, 5, 2, 4),
410
428
  "review_text_length": u.weighNumRange(0, 500, 0.2, 0),
411
- "blockbuster_release": [false],
412
- "weekend_viewing": [false],
413
- "prime_time": [false],
414
- "kids_profile": [false],
415
- "ad_fatigue": [false],
416
429
  }
417
430
  },
418
431
  {
@@ -422,10 +435,6 @@ const config = {
422
435
  "content_id": contentIds,
423
436
  "content_type": ["movie", "series", "documentary", "special"],
424
437
  "genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
425
- "weekend_viewing": [false],
426
- "prime_time": [false],
427
- "kids_profile": [false],
428
- "ad_fatigue": [false],
429
438
  }
430
439
  },
431
440
  {
@@ -434,10 +443,6 @@ const config = {
434
443
  properties: {
435
444
  "content_id": contentIds,
436
445
  "reason": ["watched", "not_interested", "expired"],
437
- "weekend_viewing": [false],
438
- "prime_time": [false],
439
- "kids_profile": [false],
440
- "ad_fatigue": [false],
441
446
  }
442
447
  },
443
448
  {
@@ -447,10 +452,6 @@ const config = {
447
452
  "search_term": () => chance.word(),
448
453
  "results_count": u.weighNumRange(0, 50, 0.5, 15),
449
454
  "search_type": ["title", "actor", "director", "genre"],
450
- "weekend_viewing": [false],
451
- "prime_time": [false],
452
- "kids_profile": [false],
453
- "ad_fatigue": [false],
454
455
  }
455
456
  },
456
457
  {
@@ -459,10 +460,6 @@ const config = {
459
460
  properties: {
460
461
  "algorithm": ["collaborative_filtering", "content_based", "trending", "editorial"],
461
462
  "position": u.weighNumRange(1, 20),
462
- "weekend_viewing": [false],
463
- "prime_time": [false],
464
- "kids_profile": [false],
465
- "ad_fatigue": [false],
466
463
  }
467
464
  },
468
465
  {
@@ -470,10 +467,6 @@ const config = {
470
467
  weight: 4,
471
468
  properties: {
472
469
  "profile_type": ["main", "kids", "partner", "guest"],
473
- "weekend_viewing": [false],
474
- "prime_time": [false],
475
- "kids_profile": [false],
476
- "ad_fatigue": [false],
477
470
  }
478
471
  },
479
472
  {
@@ -483,11 +476,6 @@ const config = {
483
476
  "ad_type": ["pre_roll", "mid_roll", "banner", "interstitial"],
484
477
  "ad_duration_sec": u.weighNumRange(5, 30),
485
478
  "skipped": [false, false, false, true, true],
486
- "ad_blocked": [false],
487
- "kids_profile": [false],
488
- "weekend_viewing": [false],
489
- "prime_time": [false],
490
- "ad_fatigue": [false],
491
479
  }
492
480
  },
493
481
  {
@@ -497,10 +485,6 @@ const config = {
497
485
  "old_plan": ["free", "standard", "premium"],
498
486
  "new_plan": ["free", "standard", "premium"],
499
487
  "change_reason": ["upgrade", "downgrade", "cancel", "resubscribe"],
500
- "weekend_viewing": [false],
501
- "prime_time": [false],
502
- "kids_profile": [false],
503
- "ad_fatigue": [false],
504
488
  }
505
489
  },
506
490
  {
@@ -510,10 +494,6 @@ const config = {
510
494
  "content_id": contentIds,
511
495
  "content_type": ["movie", "series", "documentary", "special"],
512
496
  "download_quality": ["720p", "1080p", "4k"],
513
- "weekend_viewing": [false],
514
- "prime_time": [false],
515
- "kids_profile": [false],
516
- "ad_fatigue": [false],
517
497
  }
518
498
  },
519
499
  {
@@ -522,10 +502,6 @@ const config = {
522
502
  properties: {
523
503
  "share_method": ["link", "social", "dm", "email"],
524
504
  "content_type": ["movie", "series", "documentary", "special"],
525
- "weekend_viewing": [false],
526
- "prime_time": [false],
527
- "kids_profile": [false],
528
- "ad_fatigue": [false],
529
505
  }
530
506
  },
531
507
  {
@@ -534,10 +510,6 @@ const config = {
534
510
  properties: {
535
511
  "subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
536
512
  "action": ["enabled", "disabled", "changed"],
537
- "weekend_viewing": [false],
538
- "prime_time": [false],
539
- "kids_profile": [false],
540
- "ad_fatigue": [false],
541
513
  }
542
514
  },
543
515
  ],
@@ -568,156 +540,68 @@ const config = {
568
540
 
569
541
  lookupTables: [],
570
542
 
571
- /**
572
- * ARCHITECTED ANALYTICS HOOKS
573
- *
574
- * This hook function creates 8 deliberate patterns in the data:
575
- *
576
- * 1. GENRE FUNNEL CONVERSION: Comedy/Animation props in funnel-pre; documentary completion drop in everything
577
- * 2. BINGE-WATCHING: Users with 3+ consecutive completions get extra episodes (everything)
578
- * 3. WEEKEND vs WEEKDAY: Weekend sessions 1.5x longer; weekday prime-time tagging (event)
579
- * 4. AD FATIGUE CHURN: Free-tier users with 10+ ads churn after day 45 (everything)
580
- * 5. NEW RELEASE SPIKE: Blockbuster release on day 50 drives content selection (event)
581
- * 6. KIDS PROFILE SAFETY: Kids mode restricts genres and drops ads (event)
582
- * 7. RECOMMENDATION ENGINE IMPROVEMENT: Props in funnel-pre; pre-day-60 content rated drop in everything
583
- * 8. SUBTITLE USERS WATCH MORE: Subtitle-enabled users have higher completion rates (everything)
584
- */
585
543
  hook: function (record, type, meta) {
586
- const NOW = dayjs();
587
- const DATASET_START = NOW.subtract(num_days, 'days');
588
- // FIXED_START is the pre-shift time range start; needed because
589
- // meta.firstEventTime in funnel-pre is in the FIXED (pre-shift) timeframe
590
- const FIXED_NOW_UNIX = dayjs('2024-02-02').unix();
591
- const FIXED_START = dayjs.unix(FIXED_NOW_UNIX).subtract(num_days, 'days');
592
-
593
- // ─────────────────────────────────────────────────────────────
594
- // Hook #1: GENRE FUNNEL CONVERSION (funnel-pre)
595
- // Comedy/Animation funnels convert 1.3x better; Documentary 0.7x
596
- // ─────────────────────────────────────────────────────────────
597
- if (type === "funnel-pre") {
598
- record.props = record.props || {};
599
- const genre = record.props.genre;
600
-
601
- if (genre === "comedy" || genre === "animation") {
602
- record.props.genre_boost = true;
603
- record.props.genre_penalty = false;
604
- } else if (genre === "documentary") {
605
- record.props.genre_boost = false;
606
- record.props.genre_penalty = true;
607
- } else if (!genre && chance.bool({ likelihood: 25 })) {
608
- // Randomly apply genre effects when genre isn't in funnel props
609
- if (chance.bool({ likelihood: 60 })) {
610
- record.props.genre_boost = true;
611
- record.props.genre_penalty = false;
612
- } else {
613
- record.props.genre_boost = false;
614
- record.props.genre_penalty = true;
544
+ // Hook #10 (T2C): CORE VIEWING LOOP TIME-TO-CONVERT (funnel-post)
545
+ // Premium subscribers complete browse→play funnel 1.4x faster
546
+ // (factor 0.71); free users 1.25x slower (factor 1.25).
547
+ if (type === "funnel-post") {
548
+ const segment = meta?.profile?.subscription_plan;
549
+ if (Array.isArray(record) && record.length > 1) {
550
+ const factor = (
551
+ segment === "premium" ? 0.71 :
552
+ segment === "free" ? 1.25 :
553
+ 1.0
554
+ );
555
+ if (factor !== 1.0) {
556
+ for (let i = 1; i < record.length; i++) {
557
+ const prev = dayjs(record[i - 1].time);
558
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
559
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
560
+ }
615
561
  }
616
- } else {
617
- record.props.genre_boost = false;
618
- record.props.genre_penalty = false;
619
562
  }
620
-
621
- // ─────────────────────────────────────────────────────────────
622
- // Hook #7: RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
623
- // After day 60, engagement funnel gets 1.5x boost
624
- // ─────────────────────────────────────────────────────────────
625
- const seq = record.sequence || [];
626
- const isEngagementFunnel = seq.includes("recommendation clicked");
627
- const funnelTime = meta && meta.firstEventTime ? dayjs.unix(meta.firstEventTime) : null;
628
- const isAfterImprovement = funnelTime && funnelTime.isAfter(FIXED_START.add(60, 'days'));
629
-
630
- if (isEngagementFunnel && isAfterImprovement) {
631
- record.props.improved_recs = true;
632
- } else {
633
- record.props.improved_recs = false;
634
- }
635
-
636
- return record;
637
563
  }
638
564
 
639
- // ─────────────────────────────────────────────────────────────
640
- // Hook #3: WEEKEND vs WEEKDAY PATTERNS (event)
641
- // Weekend: 1.5x watch duration. Weekday 6PM-11PM: prime_time tag
642
- // ─────────────────────────────────────────────────────────────
643
565
  if (type === "event") {
566
+ const datasetStart = dayjs.unix(meta.datasetStart);
567
+ const BLOCKBUSTER_START = datasetStart.add(50, 'days');
568
+ const BLOCKBUSTER_END = datasetStart.add(65, 'days');
644
569
  const EVENT_TIME = dayjs(record.time);
645
- const dayOfWeek = EVENT_TIME.day(); // 0 = Sunday, 6 = Saturday
646
- const hour = EVENT_TIME.hour();
647
- const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
648
-
649
- if (isWeekend) {
650
- record.weekend_viewing = true;
651
- record.prime_time = false;
652
- if (record.event === "playback completed" && record.watch_duration_min) {
653
- record.watch_duration_min = Math.round(record.watch_duration_min * 1.5);
654
- }
655
- } else {
656
- record.weekend_viewing = false;
657
- // Weekday prime-time: 6PM to 11PM
658
- if (hour >= 18 && hour <= 23) {
659
- record.prime_time = true;
660
- } else {
661
- record.prime_time = false;
662
- }
663
- }
664
570
 
665
- // ─────────────────────────────────────────────────────────────
666
- // Hook #5: NEW RELEASE SPIKE (event)
667
- // After day 50, blockbuster release drives content selection
668
- // ─────────────────────────────────────────────────────────────
669
- const BLOCKBUSTER_RELEASE = DATASET_START.add(50, 'days');
571
+ // Hook #5: NEW RELEASE SPIKE — days 50-65, 20% of selections/starts
572
+ // switch to the blockbuster id. Mutates existing content_id/content_type props.
670
573
  if (record.event === "content selected" || record.event === "playback started") {
671
- if (EVENT_TIME.isAfter(BLOCKBUSTER_RELEASE) && chance.bool({ likelihood: 20 })) {
574
+ if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
672
575
  record.content_type = "movie";
673
576
  record.content_id = blockbusterId;
674
- record.blockbuster_release = true;
675
- } else {
676
- record.blockbuster_release = false;
677
577
  }
678
578
  }
679
-
680
579
  if (record.event === "content rated") {
681
- if (EVENT_TIME.isAfter(BLOCKBUSTER_RELEASE) && chance.bool({ likelihood: 20 })) {
580
+ if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
682
581
  record.rating = chance.integer({ min: 4, max: 5 });
683
582
  record.content_id = blockbusterId;
684
- record.blockbuster_release = true;
685
- } else {
686
- record.blockbuster_release = false;
687
583
  }
688
584
  }
689
585
 
690
- // ─────────────────────────────────────────────────────────────
691
- // Hook #6: KIDS PROFILE SAFETY (event)
692
- // 15% of the time, apply kids mode: restrict genres, drop ads
693
- // ─────────────────────────────────────────────────────────────
586
+ // Hook #6: KIDS PROFILE SAFETY — 15% of selections/starts get genre
587
+ // restricted to animation or documentary. Mutates existing genre prop.
694
588
  if (chance.bool({ likelihood: 15 })) {
695
- record.kids_profile = true;
696
589
  if (record.event === "content selected" || record.event === "playback started") {
697
590
  record.genre = chance.pickone(["animation", "documentary"]);
698
- } else if (record.event === "ad impression") {
699
- record.ad_blocked = true;
700
- }
701
- } else {
702
- record.kids_profile = false;
703
- if (record.event === "ad impression") {
704
- record.ad_blocked = false;
705
591
  }
706
592
  }
707
593
 
708
594
  return record;
709
595
  }
710
596
 
711
- // ─────────────────────────────────────────────────────────────
712
- // Hook #2, #4, #8: EVERYTHING PASS - Complex behavioral patterns
713
- // ─────────────────────────────────────────────────────────────
714
597
  if (type === "everything") {
598
+ const datasetStart = dayjs.unix(meta.datasetStart);
715
599
  const userEvents = record;
716
600
  if (!userEvents || userEvents.length === 0) return record;
717
601
 
718
602
  const profile = meta.profile;
719
603
 
720
- // ─── Stamp superProps from profile (consistent per user) ───
604
+ // Stamp superProps from profile (consistent per user)
721
605
  const stampPlan = profile && profile.subscription_plan ? profile.subscription_plan : undefined;
722
606
  const stampDevice = profile && profile.device_type ? profile.device_type : undefined;
723
607
  if (stampPlan || stampDevice) {
@@ -727,101 +611,75 @@ const config = {
727
611
  });
728
612
  }
729
613
 
730
- // ─── Bug 2 fix: Genre funnel conversion filtering ───
731
- // Drop ~25% of "playback completed" events for documentary genre
732
- // to simulate lower documentary funnel completion (was conversionRate * 0.7)
614
+ // Hook #3: WEEKEND VS WEEKDAY 1.5x watch_duration_min on weekends.
615
+ // No flag analyst breaks down by day of week.
616
+ for (const e of userEvents) {
617
+ const dow = new Date(e.time).getUTCDay();
618
+ if ((dow === 0 || dow === 6) && e.event === "playback completed" && typeof e.watch_duration_min === "number") {
619
+ e.watch_duration_min = Math.round(e.watch_duration_min * 1.5);
620
+ }
621
+ }
622
+
623
+ // Hook #1: GENRE FUNNEL CONVERSION — drop 25% of documentary playback
624
+ // completed events to depress documentary funnel conversion. Raw genre check.
733
625
  for (let i = userEvents.length - 1; i >= 0; i--) {
734
626
  const evt = userEvents[i];
735
- if (evt.event === "playback completed" && evt.genre_penalty === true) {
736
- if (chance.bool({ likelihood: 25 })) {
737
- userEvents.splice(i, 1);
738
- }
627
+ if (evt.event === "playback completed" && evt.genre === "documentary" && chance.bool({ likelihood: 25 })) {
628
+ userEvents.splice(i, 1);
739
629
  }
740
630
  }
741
631
 
742
- // ─── Bug 2 fix: Recommendation engine improvement filtering ───
743
- // Drop ~30% of "content rated" events that occur BEFORE day 60
744
- // to simulate lower pre-improvement rec engine conversion (was conversionRate * 1.5)
745
- const IMPROVEMENT_DATE = DATASET_START.add(60, 'days');
632
+ // Hook #7: RECOMMENDATION ENGINE IMPROVEMENT drop 30% of pre-day-60
633
+ // content rated events. Raw time check.
634
+ const IMPROVEMENT_DATE = datasetStart.add(60, 'days');
746
635
  for (let i = userEvents.length - 1; i >= 0; i--) {
747
636
  const evt = userEvents[i];
748
- if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE)) {
749
- if (chance.bool({ likelihood: 30 })) {
750
- userEvents.splice(i, 1);
751
- }
637
+ if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE) && chance.bool({ likelihood: 30 })) {
638
+ userEvents.splice(i, 1);
752
639
  }
753
640
  }
754
641
 
755
642
  const firstEventTime = dayjs(userEvents[0].time);
756
643
 
757
- // ─── First pass: identify user patterns ───
644
+ // Identify behavioral patterns (no flags written)
758
645
  let consecutiveCompletions = 0;
759
646
  let maxConsecutiveCompletions = 0;
760
- let isBingeWatcher = false;
761
647
  let adImpressionCount = 0;
762
648
  let isFreeTier = false;
763
649
  let hasSubtitlesEnabled = false;
650
+ let recClickCount = 0;
764
651
 
765
652
  userEvents.forEach((event, idx) => {
766
- // Track subscription tier from superProps
767
653
  if (idx === 0 && event.subscription_plan) {
768
654
  isFreeTier = event.subscription_plan === "free";
769
655
  }
770
-
771
- // Hook #2: Track consecutive playback completions
772
656
  if (event.event === "playback completed") {
773
657
  consecutiveCompletions++;
774
658
  if (consecutiveCompletions > maxConsecutiveCompletions) {
775
659
  maxConsecutiveCompletions = consecutiveCompletions;
776
660
  }
777
661
  } else if (event.event !== "playback started") {
778
- // Reset streak on non-playback events (started doesn't break streak)
779
662
  consecutiveCompletions = 0;
780
663
  }
781
-
782
- // Hook #4: Count ad impressions for free-tier users
783
- if (event.event === "ad impression") {
784
- adImpressionCount++;
785
- }
786
-
787
- // Hook #8: Check for subtitle enabled
788
- if (event.event === "subtitle toggled" && event.action === "enabled") {
789
- hasSubtitlesEnabled = true;
790
- }
664
+ if (event.event === "ad impression") adImpressionCount++;
665
+ if (event.event === "subtitle toggled" && event.action === "enabled") hasSubtitlesEnabled = true;
666
+ if (event.event === "recommendation clicked") recClickCount++;
791
667
  });
792
668
 
793
- isBingeWatcher = maxConsecutiveCompletions >= 3;
794
-
795
- // ─── Second pass: set schema defaults then modify ───
796
-
797
- // Set defaults for conditional properties on all events
798
- userEvents.forEach((event) => {
799
- if (event.event === "playback completed") {
800
- if (event.binge_session === undefined) event.binge_session = false;
801
- if (event.subtitle_user === undefined) event.subtitle_user = false;
802
- }
803
- if (event.event === "playback started") {
804
- if (event.binge_session === undefined) event.binge_session = false;
805
- }
806
- if (event.ad_fatigue === undefined) event.ad_fatigue = false;
807
- });
669
+ const isBingeWatcher = maxConsecutiveCompletions >= 3;
808
670
 
809
- // Hook #2: BINGE-WATCHING PATTERN
810
- // Binge-watchers get extra episodes, high completion, fewer pauses
671
+ // Hook #2: BINGE-WATCHING — drop pauses, inject extra start+complete pairs.
672
+ // Cloned events use unique offset timestamps. No flag.
811
673
  if (isBingeWatcher) {
812
674
  for (let i = userEvents.length - 1; i >= 0; i--) {
813
675
  const event = userEvents[i];
814
676
  const eventTime = dayjs(event.time);
815
677
 
816
- // Remove some pause events (binge-watchers don't stop)
817
- if (event.event === "playback paused") {
818
- if (chance.bool({ likelihood: 60 })) {
819
- userEvents.splice(i, 1);
820
- continue;
821
- }
678
+ if (event.event === "playback paused" && chance.bool({ likelihood: 60 })) {
679
+ userEvents.splice(i, 1);
680
+ continue;
822
681
  }
823
682
 
824
- // Add extra viewing events after completions
825
683
  if (event.event === "playback completed" && chance.bool({ likelihood: 40 })) {
826
684
  const nextContentId = chance.pickone(contentIds);
827
685
  const startTemplate = userEvents.find(e => e.event === "playback started");
@@ -833,7 +691,6 @@ const config = {
833
691
  content_id: nextContentId,
834
692
  content_type: "series",
835
693
  playback_quality: event.playback_quality || "1080p",
836
- binge_session: true,
837
694
  };
838
695
  const extraComplete = {
839
696
  ...event,
@@ -843,51 +700,42 @@ const config = {
843
700
  content_type: "series",
844
701
  watch_duration_min: chance.integer({ min: 20, max: 55 }),
845
702
  completion_percent: chance.integer({ min: 90, max: 100 }),
846
- binge_session: true,
847
703
  };
848
704
  userEvents.splice(i + 1, 0, extraStart, extraComplete);
849
705
  }
850
706
  }
851
707
  }
852
708
 
853
- // Hook #4: AD FATIGUE CHURN
854
- // Free-tier users with 10+ ads lose 50% of events after day 45
709
+ // Hook #4: AD FATIGUE CHURN — free-tier users w/ 10+ ads lose 50% of
710
+ // events after day 45. No flag discover via cohort retention.
855
711
  if (isFreeTier && adImpressionCount >= 10) {
856
712
  const churnCutoff = firstEventTime.add(45, 'days');
857
713
  for (let i = userEvents.length - 1; i >= 0; i--) {
858
714
  const evt = userEvents[i];
859
- if (dayjs(evt.time).isAfter(churnCutoff)) {
860
- if (chance.bool({ likelihood: 50 })) {
861
- userEvents.splice(i, 1);
862
- } else {
863
- evt.ad_fatigue = true;
864
- }
715
+ if (dayjs(evt.time).isAfter(churnCutoff) && chance.bool({ likelihood: 50 })) {
716
+ userEvents.splice(i, 1);
865
717
  }
866
718
  }
867
719
  }
868
720
 
869
- // Hook #8: SUBTITLE USERS WATCH MORE
870
- // Users who enabled subtitles have 25% higher completion and 15% longer watch time
721
+ // Hook #8: SUBTITLE USERS WATCH MORE — 1.25x completion_percent (cap 100),
722
+ // 1.15x watch_duration_min, plus 20% extra cloned playback completions.
723
+ // No flag — discover via cohort builder on subtitle-toggled-enabled.
871
724
  if (hasSubtitlesEnabled) {
872
725
  for (let i = 0; i < userEvents.length; i++) {
873
726
  const event = userEvents[i];
874
-
875
727
  if (event.event === "playback completed") {
876
- // Boost completion percent (cap at 100)
877
728
  if (event.completion_percent) {
878
729
  event.completion_percent = Math.min(100, Math.round(event.completion_percent * 1.25));
879
730
  }
880
- // Boost watch duration
881
731
  if (event.watch_duration_min) {
882
732
  event.watch_duration_min = Math.round(event.watch_duration_min * 1.15);
883
733
  }
884
- event.subtitle_user = true;
885
734
  }
886
735
  }
887
736
 
888
- // Splice extra playback completed events (subtitle users watch more overall)
889
737
  const completionEvents = userEvents.filter(e => e.event === "playback completed");
890
- const extraCount = Math.floor(completionEvents.length * 0.2); // 20% more completions
738
+ const extraCount = Math.floor(completionEvents.length * 0.2);
891
739
  for (let j = 0; j < extraCount; j++) {
892
740
  const templateEvent = chance.pickone(completionEvents);
893
741
  const templateTime = dayjs(templateEvent.time);
@@ -899,12 +747,28 @@ const config = {
899
747
  content_type: chance.pickone(["movie", "series", "documentary"]),
900
748
  watch_duration_min: chance.integer({ min: 25, max: 120 }),
901
749
  completion_percent: chance.integer({ min: 80, max: 100 }),
902
- subtitle_user: true,
903
750
  };
904
751
  userEvents.push(extraCompletion);
905
752
  }
906
753
  }
907
754
 
755
+ // Hook #9: RECOMMENDATION-CLICKED MAGIC NUMBER (no flags)
756
+ // Sweet 4-6 rec clicks → +25% watch_duration_min on playback completed.
757
+ // Over 7+ → drop 30% of playback completed events (rec fatigue).
758
+ if (recClickCount >= 4 && recClickCount <= 6) {
759
+ userEvents.forEach(e => {
760
+ if (e.event === 'playback completed' && typeof e.watch_duration_min === 'number') {
761
+ e.watch_duration_min = Math.round(e.watch_duration_min * 1.25);
762
+ }
763
+ });
764
+ } else if (recClickCount >= 7) {
765
+ for (let i = userEvents.length - 1; i >= 0; i--) {
766
+ if (userEvents[i].event === 'playback completed' && chance.bool({ likelihood: 30 })) {
767
+ userEvents.splice(i, 1);
768
+ }
769
+ }
770
+ }
771
+
908
772
  return record;
909
773
  }
910
774