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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dungeons/technical/array-of-object-lookup.js +0 -2
  2. package/dungeons/technical/simple.js +3 -4
  3. package/dungeons/technical/simplest-schema.json +5 -0
  4. package/dungeons/technical/text-generation.js +1 -1
  5. package/dungeons/vertical/ai-platform.js +192 -133
  6. package/dungeons/vertical/community.js +64 -34
  7. package/dungeons/vertical/crypto.js +333 -224
  8. package/dungeons/vertical/dating.js +228 -282
  9. package/dungeons/vertical/devtools.js +164 -62
  10. package/dungeons/vertical/ecommerce.js +240 -91
  11. package/dungeons/vertical/education.js +328 -310
  12. package/dungeons/vertical/fintech.js +437 -311
  13. package/dungeons/vertical/fitness.js +141 -58
  14. package/dungeons/vertical/food-delivery.js +298 -318
  15. package/dungeons/vertical/gaming.js +378 -231
  16. package/dungeons/vertical/healthcare.js +140 -60
  17. package/dungeons/vertical/insurance-application.js +168 -73
  18. package/dungeons/vertical/logistics.js +95 -6
  19. package/dungeons/vertical/marketplace.js +137 -51
  20. package/dungeons/vertical/media.js +241 -386
  21. package/dungeons/vertical/real-estate.js +322 -315
  22. package/dungeons/vertical/sass.js +253 -263
  23. package/dungeons/vertical/social.js +262 -206
  24. package/dungeons/vertical/travel.js +59 -16
  25. package/index.js +17 -17
  26. package/lib/core/config-validator.js +93 -21
  27. package/lib/core/context.js +10 -24
  28. package/lib/core/storage.js +6 -1
  29. package/lib/generators/events.js +11 -14
  30. package/lib/generators/funnels.js +12 -4
  31. package/lib/generators/mirror.js +3 -2
  32. package/lib/generators/product-names.js +1 -1
  33. package/lib/generators/scd.js +2 -1
  34. package/lib/generators/text.js +1 -1
  35. package/lib/orchestrators/user-loop.js +47 -47
  36. package/lib/utils/utils.js +71 -39
  37. package/package.json +1 -1
  38. package/scripts/smoke-test-all.mjs +162 -0
  39. package/scripts/verify-runner.mjs +72 -24
  40. package/types.d.ts +38 -15
  41. package/dungeons/technical/ad-spend-schema.json +0 -128
  42. package/dungeons/technical/anonymous-users-schema.json +0 -92
  43. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  44. package/dungeons/technical/experiments-schema.json +0 -203
  45. package/dungeons/technical/foobar-schema.json +0 -362
  46. package/dungeons/technical/group-analytics-schema.json +0 -241
  47. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  48. package/dungeons/technical/nested-objects-schema.json +0 -145
  49. package/dungeons/technical/retention-cadence-schema.json +0 -37
  50. package/dungeons/technical/sanity-schema.json +0 -185
  51. package/dungeons/technical/scale-test-schema.json +0 -70
  52. package/dungeons/technical/scd-schema.json +0 -467
  53. package/dungeons/technical/simple-schema.json +0 -362
  54. package/dungeons/technical/text-generation-schema.json +0 -1062
  55. package/dungeons/vertical/ai-platform-schema.json +0 -617
  56. package/dungeons/vertical/community-schema.json +0 -579
  57. package/dungeons/vertical/crypto-schema.json +0 -546
  58. package/dungeons/vertical/dating-schema.json +0 -401
  59. package/dungeons/vertical/devtools-schema.json +0 -601
  60. package/dungeons/vertical/ecommerce-schema.json +0 -604
  61. package/dungeons/vertical/education-schema.json +0 -5686
  62. package/dungeons/vertical/fintech-schema.json +0 -630
  63. package/dungeons/vertical/fitness-schema.json +0 -530
  64. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  65. package/dungeons/vertical/gaming-schema.json +0 -2703
  66. package/dungeons/vertical/healthcare-schema.json +0 -549
  67. package/dungeons/vertical/insurance-application-schema.json +0 -485
  68. package/dungeons/vertical/logistics-schema.json +0 -574
  69. package/dungeons/vertical/marketplace-schema.json +0 -533
  70. package/dungeons/vertical/media-schema.json +0 -4749
  71. package/dungeons/vertical/real-estate-schema.json +0 -527
  72. package/dungeons/vertical/sass-schema.json +0 -3128
  73. package/dungeons/vertical/social-schema.json +0 -620
  74. package/dungeons/vertical/travel-schema.json +0 -580
@@ -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:
84
109
  *
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)
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
116
+ *
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:
96
127
  *
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
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
133
+ *
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.
144
+ *
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
110
154
  *
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)
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
161
+ * PATTERN: 15% of "content selected" / "playback started" events get genre
162
+ * restricted to "animation" or "documentary". Mutates the existing genre prop.
124
163
  *
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
164
+ * HOW TO FIND IT IN MIXPANEL:
129
165
  *
130
- * ───────────────────────────────────────────────────────────────────────────────
131
- * 7. RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
132
- * ───────────────────────────────────────────────────────────────────────────────
133
- *
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
+ * ───────────────────────────────────────────────────────────────────────────────
180
203
  *
181
- * 1. Binge + Subtitle: Do subtitle-enabled binge-watchers have the highest
182
- * total watch hours? (Hooks #2 + #8 combined)
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.
183
207
  *
184
- * 2. Ad Fatigue + Blockbuster: Does the blockbuster release (Hook #5) rescue
185
- * free-tier users from ad fatigue churn (Hook #4)?
208
+ * HOW TO FIND IT IN MIXPANEL:
186
209
  *
187
- * 3. Kids + Weekend: Is kids profile viewing concentrated on weekends (Hook #6
188
- * + #3)? Does weekend kids viewing show different genre preferences?
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%
189
217
  *
190
- * 4. Rec Engine + Genre: Does the recommendation engine improvement (Hook #7)
191
- * disproportionately help certain genres (Hook #1)?
218
+ * REAL-WORLD ANALOGUE: Subtitle adoption correlates with attentive viewers.
219
+ *
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,7 +273,9 @@ const blockbusterId = `blockbuster_${v.uid(8)}`;
227
273
  const config = {
228
274
  token,
229
275
  seed: SEED,
230
- numDays: num_days,
276
+ datasetStart: "2026-01-01T00:00:00Z",
277
+ datasetEnd: "2026-04-28T23:59:59Z",
278
+ // numDays: num_days,
231
279
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
232
280
  numUsers: num_users,
233
281
  hasAnonIds: false,
@@ -323,10 +371,6 @@ const config = {
323
371
  properties: {
324
372
  "signup_source": ["organic", "referral", "trial_offer", "ad"],
325
373
  "plan_selected": ["free", "standard", "premium"],
326
- "weekend_viewing": [false],
327
- "prime_time": [false],
328
- "kids_profile": [false],
329
- "ad_fatigue": [false],
330
374
  }
331
375
  },
332
376
  {
@@ -335,10 +379,6 @@ const config = {
335
379
  properties: {
336
380
  "browse_section": ["home", "trending", "new_releases", "genre", "continue_watching"],
337
381
  "genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
338
- "weekend_viewing": [false],
339
- "prime_time": [false],
340
- "kids_profile": [false],
341
- "ad_fatigue": [false],
342
382
  }
343
383
  },
344
384
  {
@@ -348,11 +388,6 @@ const config = {
348
388
  "content_type": ["movie", "series", "documentary", "special"],
349
389
  "genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
350
390
  "content_id": contentIds,
351
- "blockbuster_release": [false],
352
- "kids_profile": [false],
353
- "weekend_viewing": [false],
354
- "prime_time": [false],
355
- "ad_fatigue": [false],
356
391
  }
357
392
  },
358
393
  {
@@ -364,12 +399,6 @@ const config = {
364
399
  "playback_quality": ["480p", "720p", "1080p", "4k"],
365
400
  "subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
366
401
  "playback_speed": ["0.5x", "1x", "1x", "1x", "1.25x", "1.5x", "2x"],
367
- "blockbuster_release": [false],
368
- "kids_profile": [false],
369
- "binge_session": [false],
370
- "weekend_viewing": [false],
371
- "prime_time": [false],
372
- "ad_fatigue": [false],
373
402
  }
374
403
  },
375
404
  {
@@ -380,12 +409,6 @@ const config = {
380
409
  "content_type": ["movie", "series", "documentary", "special"],
381
410
  "watch_duration_min": u.weighNumRange(5, 180, 0.5, 45),
382
411
  "completion_percent": u.weighNumRange(10, 100, 1.5, 85),
383
- "binge_session": [false],
384
- "subtitle_user": [false],
385
- "weekend_viewing": [false],
386
- "prime_time": [false],
387
- "kids_profile": [false],
388
- "ad_fatigue": [false],
389
412
  }
390
413
  },
391
414
  {
@@ -394,10 +417,6 @@ const config = {
394
417
  properties: {
395
418
  "content_id": contentIds,
396
419
  "pause_reason": ["manual", "ad_break", "buffering", "notification"],
397
- "weekend_viewing": [false],
398
- "prime_time": [false],
399
- "kids_profile": [false],
400
- "ad_fatigue": [false],
401
420
  }
402
421
  },
403
422
  {
@@ -407,11 +426,6 @@ const config = {
407
426
  "content_id": contentIds,
408
427
  "rating": u.weighNumRange(1, 5, 2, 4),
409
428
  "review_text_length": u.weighNumRange(0, 500, 0.2, 0),
410
- "blockbuster_release": [false],
411
- "weekend_viewing": [false],
412
- "prime_time": [false],
413
- "kids_profile": [false],
414
- "ad_fatigue": [false],
415
429
  }
416
430
  },
417
431
  {
@@ -421,10 +435,6 @@ const config = {
421
435
  "content_id": contentIds,
422
436
  "content_type": ["movie", "series", "documentary", "special"],
423
437
  "genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
424
- "weekend_viewing": [false],
425
- "prime_time": [false],
426
- "kids_profile": [false],
427
- "ad_fatigue": [false],
428
438
  }
429
439
  },
430
440
  {
@@ -433,10 +443,6 @@ const config = {
433
443
  properties: {
434
444
  "content_id": contentIds,
435
445
  "reason": ["watched", "not_interested", "expired"],
436
- "weekend_viewing": [false],
437
- "prime_time": [false],
438
- "kids_profile": [false],
439
- "ad_fatigue": [false],
440
446
  }
441
447
  },
442
448
  {
@@ -446,10 +452,6 @@ const config = {
446
452
  "search_term": () => chance.word(),
447
453
  "results_count": u.weighNumRange(0, 50, 0.5, 15),
448
454
  "search_type": ["title", "actor", "director", "genre"],
449
- "weekend_viewing": [false],
450
- "prime_time": [false],
451
- "kids_profile": [false],
452
- "ad_fatigue": [false],
453
455
  }
454
456
  },
455
457
  {
@@ -458,10 +460,6 @@ const config = {
458
460
  properties: {
459
461
  "algorithm": ["collaborative_filtering", "content_based", "trending", "editorial"],
460
462
  "position": u.weighNumRange(1, 20),
461
- "weekend_viewing": [false],
462
- "prime_time": [false],
463
- "kids_profile": [false],
464
- "ad_fatigue": [false],
465
463
  }
466
464
  },
467
465
  {
@@ -469,10 +467,6 @@ const config = {
469
467
  weight: 4,
470
468
  properties: {
471
469
  "profile_type": ["main", "kids", "partner", "guest"],
472
- "weekend_viewing": [false],
473
- "prime_time": [false],
474
- "kids_profile": [false],
475
- "ad_fatigue": [false],
476
470
  }
477
471
  },
478
472
  {
@@ -482,11 +476,6 @@ const config = {
482
476
  "ad_type": ["pre_roll", "mid_roll", "banner", "interstitial"],
483
477
  "ad_duration_sec": u.weighNumRange(5, 30),
484
478
  "skipped": [false, false, false, true, true],
485
- "ad_blocked": [false],
486
- "kids_profile": [false],
487
- "weekend_viewing": [false],
488
- "prime_time": [false],
489
- "ad_fatigue": [false],
490
479
  }
491
480
  },
492
481
  {
@@ -496,10 +485,6 @@ const config = {
496
485
  "old_plan": ["free", "standard", "premium"],
497
486
  "new_plan": ["free", "standard", "premium"],
498
487
  "change_reason": ["upgrade", "downgrade", "cancel", "resubscribe"],
499
- "weekend_viewing": [false],
500
- "prime_time": [false],
501
- "kids_profile": [false],
502
- "ad_fatigue": [false],
503
488
  }
504
489
  },
505
490
  {
@@ -509,10 +494,6 @@ const config = {
509
494
  "content_id": contentIds,
510
495
  "content_type": ["movie", "series", "documentary", "special"],
511
496
  "download_quality": ["720p", "1080p", "4k"],
512
- "weekend_viewing": [false],
513
- "prime_time": [false],
514
- "kids_profile": [false],
515
- "ad_fatigue": [false],
516
497
  }
517
498
  },
518
499
  {
@@ -521,10 +502,6 @@ const config = {
521
502
  properties: {
522
503
  "share_method": ["link", "social", "dm", "email"],
523
504
  "content_type": ["movie", "series", "documentary", "special"],
524
- "weekend_viewing": [false],
525
- "prime_time": [false],
526
- "kids_profile": [false],
527
- "ad_fatigue": [false],
528
505
  }
529
506
  },
530
507
  {
@@ -533,10 +510,6 @@ const config = {
533
510
  properties: {
534
511
  "subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
535
512
  "action": ["enabled", "disabled", "changed"],
536
- "weekend_viewing": [false],
537
- "prime_time": [false],
538
- "kids_profile": [false],
539
- "ad_fatigue": [false],
540
513
  }
541
514
  },
542
515
  ],
@@ -567,141 +540,68 @@ const config = {
567
540
 
568
541
  lookupTables: [],
569
542
 
570
- /**
571
- * ARCHITECTED ANALYTICS HOOKS
572
- *
573
- * This hook function creates 8 deliberate patterns in the data:
574
- *
575
- * 1. GENRE FUNNEL CONVERSION: Comedy/Animation props in funnel-pre; documentary completion drop in everything
576
- * 2. BINGE-WATCHING: Users with 3+ consecutive completions get extra episodes (everything)
577
- * 3. WEEKEND vs WEEKDAY: Weekend sessions 1.5x longer; weekday prime-time tagging (event)
578
- * 4. AD FATIGUE CHURN: Free-tier users with 10+ ads churn after day 45 (everything)
579
- * 5. NEW RELEASE SPIKE: Blockbuster release on day 50 drives content selection (event)
580
- * 6. KIDS PROFILE SAFETY: Kids mode restricts genres and drops ads (event)
581
- * 7. RECOMMENDATION ENGINE IMPROVEMENT: Props in funnel-pre; pre-day-60 content rated drop in everything
582
- * 8. SUBTITLE USERS WATCH MORE: Subtitle-enabled users have higher completion rates (everything)
583
- */
584
543
  hook: function (record, type, meta) {
585
- const NOW = dayjs();
586
- const DATASET_START = NOW.subtract(num_days, 'days');
587
- // FIXED_START is the pre-shift time range start; needed because
588
- // meta.firstEventTime in funnel-pre is in the FIXED (pre-shift) timeframe
589
- const FIXED_NOW_UNIX = dayjs('2024-02-02').unix();
590
- const FIXED_START = dayjs.unix(FIXED_NOW_UNIX).subtract(num_days, 'days');
591
-
592
- // ─────────────────────────────────────────────────────────────
593
- // Hook #1: GENRE FUNNEL CONVERSION (funnel-pre)
594
- // Comedy/Animation funnels convert 1.3x better; Documentary 0.7x
595
- // ─────────────────────────────────────────────────────────────
596
- if (type === "funnel-pre") {
597
- record.props = record.props || {};
598
- const genre = record.props.genre;
599
-
600
- if (genre === "comedy" || genre === "animation") {
601
- record.props.genre_boost = true;
602
- record.props.genre_penalty = false;
603
- } else if (genre === "documentary") {
604
- record.props.genre_boost = false;
605
- record.props.genre_penalty = true;
606
- } else if (!genre && chance.bool({ likelihood: 25 })) {
607
- // Randomly apply genre effects when genre isn't in funnel props
608
- if (chance.bool({ likelihood: 60 })) {
609
- record.props.genre_boost = true;
610
- record.props.genre_penalty = false;
611
- } else {
612
- record.props.genre_boost = false;
613
- 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
+ }
614
561
  }
615
- } else {
616
- record.props.genre_boost = false;
617
- record.props.genre_penalty = false;
618
562
  }
619
-
620
- // ─────────────────────────────────────────────────────────────
621
- // Hook #7: RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
622
- // After day 60, engagement funnel gets 1.5x boost
623
- // ─────────────────────────────────────────────────────────────
624
- const seq = record.sequence || [];
625
- const isEngagementFunnel = seq.includes("recommendation clicked");
626
- const funnelTime = meta && meta.firstEventTime ? dayjs.unix(meta.firstEventTime) : null;
627
- const isAfterImprovement = funnelTime && funnelTime.isAfter(FIXED_START.add(60, 'days'));
628
-
629
- if (isEngagementFunnel && isAfterImprovement) {
630
- record.props.improved_recs = true;
631
- } else {
632
- record.props.improved_recs = false;
633
- }
634
-
635
- return record;
636
563
  }
637
564
 
638
- // ─────────────────────────────────────────────────────────────
639
- // Hook #3: WEEKEND vs WEEKDAY PATTERNS
640
- // Moved to everything hook (after sessionization) so DOW/HOD
641
- // tags match final timestamps. See everything hook below.
642
- // ─────────────────────────────────────────────────────────────
643
-
644
565
  if (type === "event") {
645
- // ─────────────────────────────────────────────────────────────
646
- // Hook #5: NEW RELEASE SPIKE (event)
647
- // Days 50-65: blockbuster release drives content selection.
648
- // Bounded window concentrates the spike instead of spreading
649
- // it across the entire second half of the dataset.
650
- // ─────────────────────────────────────────────────────────────
651
- const BLOCKBUSTER_START = DATASET_START.add(50, 'days');
652
- const BLOCKBUSTER_END = DATASET_START.add(65, 'days');
566
+ const datasetStart = dayjs.unix(meta.datasetStart);
567
+ const BLOCKBUSTER_START = datasetStart.add(50, 'days');
568
+ const BLOCKBUSTER_END = datasetStart.add(65, 'days');
653
569
  const EVENT_TIME = dayjs(record.time);
570
+
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.
654
573
  if (record.event === "content selected" || record.event === "playback started") {
655
574
  if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
656
575
  record.content_type = "movie";
657
576
  record.content_id = blockbusterId;
658
- record.blockbuster_release = true;
659
- } else {
660
- record.blockbuster_release = false;
661
577
  }
662
578
  }
663
-
664
579
  if (record.event === "content rated") {
665
580
  if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
666
581
  record.rating = chance.integer({ min: 4, max: 5 });
667
582
  record.content_id = blockbusterId;
668
- record.blockbuster_release = true;
669
- } else {
670
- record.blockbuster_release = false;
671
583
  }
672
584
  }
673
585
 
674
- // ─────────────────────────────────────────────────────────────
675
- // Hook #6: KIDS PROFILE SAFETY (event)
676
- // 15% of the time, apply kids mode: restrict genres, drop ads
677
- // ─────────────────────────────────────────────────────────────
586
+ // Hook #6: KIDS PROFILE SAFETY — 15% of selections/starts get genre
587
+ // restricted to animation or documentary. Mutates existing genre prop.
678
588
  if (chance.bool({ likelihood: 15 })) {
679
- record.kids_profile = true;
680
589
  if (record.event === "content selected" || record.event === "playback started") {
681
590
  record.genre = chance.pickone(["animation", "documentary"]);
682
- } else if (record.event === "ad impression") {
683
- record.ad_blocked = true;
684
- }
685
- } else {
686
- record.kids_profile = false;
687
- if (record.event === "ad impression") {
688
- record.ad_blocked = false;
689
591
  }
690
592
  }
691
593
 
692
594
  return record;
693
595
  }
694
596
 
695
- // ─────────────────────────────────────────────────────────────
696
- // Hook #2, #4, #8: EVERYTHING PASS - Complex behavioral patterns
697
- // ─────────────────────────────────────────────────────────────
698
597
  if (type === "everything") {
598
+ const datasetStart = dayjs.unix(meta.datasetStart);
699
599
  const userEvents = record;
700
600
  if (!userEvents || userEvents.length === 0) return record;
701
601
 
702
602
  const profile = meta.profile;
703
603
 
704
- // ─── Stamp superProps from profile (consistent per user) ───
604
+ // Stamp superProps from profile (consistent per user)
705
605
  const stampPlan = profile && profile.subscription_plan ? profile.subscription_plan : undefined;
706
606
  const stampDevice = profile && profile.device_type ? profile.device_type : undefined;
707
607
  if (stampPlan || stampDevice) {
@@ -711,126 +611,75 @@ const config = {
711
611
  });
712
612
  }
713
613
 
714
- // ─── Hook #3: WEEKEND vs WEEKDAY PATTERNS ───
715
- // Weekend: 1.5x watch duration. Weekday 6PM-11PM: prime_time tag.
716
- // Runs after sessionization so DOW/HOD tags match final timestamps.
614
+ // Hook #3: WEEKEND VS WEEKDAY 1.5x watch_duration_min on weekends.
615
+ // No flag analyst breaks down by day of week.
717
616
  for (const e of userEvents) {
718
- const eventDate = new Date(e.time);
719
- const dayOfWeek = eventDate.getUTCDay(); // 0 = Sunday, 6 = Saturday
720
- const hour = eventDate.getUTCHours();
721
- const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
722
-
723
- if (isWeekend) {
724
- e.weekend_viewing = true;
725
- e.prime_time = false;
726
- if (e.event === "playback completed" && e.watch_duration_min) {
727
- e.watch_duration_min = Math.round(e.watch_duration_min * 1.5);
728
- }
729
- } else {
730
- e.weekend_viewing = false;
731
- if (hour >= 18 && hour <= 23) {
732
- e.prime_time = true;
733
- } else {
734
- e.prime_time = false;
735
- }
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);
736
620
  }
737
621
  }
738
622
 
739
- // ─── Bug 2 fix: Genre funnel conversion filtering ───
740
- // Drop ~25% of "playback completed" events for documentary genre
741
- // to simulate lower documentary funnel completion (was conversionRate * 0.7)
623
+ // Hook #1: GENRE FUNNEL CONVERSION drop 25% of documentary playback
624
+ // completed events to depress documentary funnel conversion. Raw genre check.
742
625
  for (let i = userEvents.length - 1; i >= 0; i--) {
743
626
  const evt = userEvents[i];
744
- if (evt.event === "playback completed" && evt.genre_penalty === true) {
745
- if (chance.bool({ likelihood: 25 })) {
746
- userEvents.splice(i, 1);
747
- }
627
+ if (evt.event === "playback completed" && evt.genre === "documentary" && chance.bool({ likelihood: 25 })) {
628
+ userEvents.splice(i, 1);
748
629
  }
749
630
  }
750
631
 
751
- // ─── Bug 2 fix: Recommendation engine improvement filtering ───
752
- // Drop ~30% of "content rated" events that occur BEFORE day 60
753
- // to simulate lower pre-improvement rec engine conversion (was conversionRate * 1.5)
754
- 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');
755
635
  for (let i = userEvents.length - 1; i >= 0; i--) {
756
636
  const evt = userEvents[i];
757
- if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE)) {
758
- if (chance.bool({ likelihood: 30 })) {
759
- userEvents.splice(i, 1);
760
- }
637
+ if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE) && chance.bool({ likelihood: 30 })) {
638
+ userEvents.splice(i, 1);
761
639
  }
762
640
  }
763
641
 
764
642
  const firstEventTime = dayjs(userEvents[0].time);
765
643
 
766
- // ─── First pass: identify user patterns ───
644
+ // Identify behavioral patterns (no flags written)
767
645
  let consecutiveCompletions = 0;
768
646
  let maxConsecutiveCompletions = 0;
769
- let isBingeWatcher = false;
770
647
  let adImpressionCount = 0;
771
648
  let isFreeTier = false;
772
649
  let hasSubtitlesEnabled = false;
650
+ let recClickCount = 0;
773
651
 
774
652
  userEvents.forEach((event, idx) => {
775
- // Track subscription tier from superProps
776
653
  if (idx === 0 && event.subscription_plan) {
777
654
  isFreeTier = event.subscription_plan === "free";
778
655
  }
779
-
780
- // Hook #2: Track consecutive playback completions
781
656
  if (event.event === "playback completed") {
782
657
  consecutiveCompletions++;
783
658
  if (consecutiveCompletions > maxConsecutiveCompletions) {
784
659
  maxConsecutiveCompletions = consecutiveCompletions;
785
660
  }
786
661
  } else if (event.event !== "playback started") {
787
- // Reset streak on non-playback events (started doesn't break streak)
788
662
  consecutiveCompletions = 0;
789
663
  }
790
-
791
- // Hook #4: Count ad impressions for free-tier users
792
- if (event.event === "ad impression") {
793
- adImpressionCount++;
794
- }
795
-
796
- // Hook #8: Check for subtitle enabled
797
- if (event.event === "subtitle toggled" && event.action === "enabled") {
798
- hasSubtitlesEnabled = true;
799
- }
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++;
800
667
  });
801
668
 
802
- isBingeWatcher = maxConsecutiveCompletions >= 3;
803
-
804
- // ─── Second pass: set schema defaults then modify ───
669
+ const isBingeWatcher = maxConsecutiveCompletions >= 3;
805
670
 
806
- // Set defaults for conditional properties on all events
807
- userEvents.forEach((event) => {
808
- if (event.event === "playback completed") {
809
- if (event.binge_session === undefined) event.binge_session = false;
810
- if (event.subtitle_user === undefined) event.subtitle_user = false;
811
- }
812
- if (event.event === "playback started") {
813
- if (event.binge_session === undefined) event.binge_session = false;
814
- }
815
- if (event.ad_fatigue === undefined) event.ad_fatigue = false;
816
- });
817
-
818
- // Hook #2: BINGE-WATCHING PATTERN
819
- // 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.
820
673
  if (isBingeWatcher) {
821
674
  for (let i = userEvents.length - 1; i >= 0; i--) {
822
675
  const event = userEvents[i];
823
676
  const eventTime = dayjs(event.time);
824
677
 
825
- // Remove some pause events (binge-watchers don't stop)
826
- if (event.event === "playback paused") {
827
- if (chance.bool({ likelihood: 60 })) {
828
- userEvents.splice(i, 1);
829
- continue;
830
- }
678
+ if (event.event === "playback paused" && chance.bool({ likelihood: 60 })) {
679
+ userEvents.splice(i, 1);
680
+ continue;
831
681
  }
832
682
 
833
- // Add extra viewing events after completions
834
683
  if (event.event === "playback completed" && chance.bool({ likelihood: 40 })) {
835
684
  const nextContentId = chance.pickone(contentIds);
836
685
  const startTemplate = userEvents.find(e => e.event === "playback started");
@@ -842,7 +691,6 @@ const config = {
842
691
  content_id: nextContentId,
843
692
  content_type: "series",
844
693
  playback_quality: event.playback_quality || "1080p",
845
- binge_session: true,
846
694
  };
847
695
  const extraComplete = {
848
696
  ...event,
@@ -852,51 +700,42 @@ const config = {
852
700
  content_type: "series",
853
701
  watch_duration_min: chance.integer({ min: 20, max: 55 }),
854
702
  completion_percent: chance.integer({ min: 90, max: 100 }),
855
- binge_session: true,
856
703
  };
857
704
  userEvents.splice(i + 1, 0, extraStart, extraComplete);
858
705
  }
859
706
  }
860
707
  }
861
708
 
862
- // Hook #4: AD FATIGUE CHURN
863
- // 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.
864
711
  if (isFreeTier && adImpressionCount >= 10) {
865
712
  const churnCutoff = firstEventTime.add(45, 'days');
866
713
  for (let i = userEvents.length - 1; i >= 0; i--) {
867
714
  const evt = userEvents[i];
868
- if (dayjs(evt.time).isAfter(churnCutoff)) {
869
- if (chance.bool({ likelihood: 50 })) {
870
- userEvents.splice(i, 1);
871
- } else {
872
- evt.ad_fatigue = true;
873
- }
715
+ if (dayjs(evt.time).isAfter(churnCutoff) && chance.bool({ likelihood: 50 })) {
716
+ userEvents.splice(i, 1);
874
717
  }
875
718
  }
876
719
  }
877
720
 
878
- // Hook #8: SUBTITLE USERS WATCH MORE
879
- // 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.
880
724
  if (hasSubtitlesEnabled) {
881
725
  for (let i = 0; i < userEvents.length; i++) {
882
726
  const event = userEvents[i];
883
-
884
727
  if (event.event === "playback completed") {
885
- // Boost completion percent (cap at 100)
886
728
  if (event.completion_percent) {
887
729
  event.completion_percent = Math.min(100, Math.round(event.completion_percent * 1.25));
888
730
  }
889
- // Boost watch duration
890
731
  if (event.watch_duration_min) {
891
732
  event.watch_duration_min = Math.round(event.watch_duration_min * 1.15);
892
733
  }
893
- event.subtitle_user = true;
894
734
  }
895
735
  }
896
736
 
897
- // Splice extra playback completed events (subtitle users watch more overall)
898
737
  const completionEvents = userEvents.filter(e => e.event === "playback completed");
899
- const extraCount = Math.floor(completionEvents.length * 0.2); // 20% more completions
738
+ const extraCount = Math.floor(completionEvents.length * 0.2);
900
739
  for (let j = 0; j < extraCount; j++) {
901
740
  const templateEvent = chance.pickone(completionEvents);
902
741
  const templateTime = dayjs(templateEvent.time);
@@ -908,12 +747,28 @@ const config = {
908
747
  content_type: chance.pickone(["movie", "series", "documentary"]),
909
748
  watch_duration_min: chance.integer({ min: 25, max: 120 }),
910
749
  completion_percent: chance.integer({ min: 80, max: 100 }),
911
- subtitle_user: true,
912
750
  };
913
751
  userEvents.push(extraCompletion);
914
752
  }
915
753
  }
916
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
+
917
772
  return record;
918
773
  }
919
774