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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "harness-social";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -43,124 +43,231 @@ const chance = u.initChance(SEED);
43
43
 
44
44
  /*
45
45
  * =====================================================================================
46
- * ANALYTICS HOOKS
46
+ * ANALYTICS HOOKS (10 hooks)
47
+ *
48
+ * Adds 10. ONBOARDING TIME-TO-CONVERT: creator/business 0.71x faster, personal
49
+ * 1.25x slower (funnel-post). Discover via Onboarding funnel median TTC by account_type.
50
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
51
+ * Cross-event MIN→MIN SQL queries on raw events do NOT show this.
47
52
  * =====================================================================================
48
53
  *
49
- * 8 deliberately architected patterns hidden in the data:
54
+ * NOTE: All cohort effects are HIDDEN discoverable only via behavioral cohorts
55
+ * (count an event per user, then measure downstream). No cohort flag is stamped
56
+ * on events. Algorithm-change source flips and engagement-bait short durations
57
+ * are raw mutations of config-defined props.
50
58
  *
51
59
  * -------------------------------------------------------------------------------------
52
- * 1. VIRAL CONTENT CASCADE (everything hook)
60
+ * 1. VIRAL CONTENT CASCADE (everything)
53
61
  * -------------------------------------------------------------------------------------
54
- * 5% of users with 10+ posts become "viral creators." Each of their posts generates
55
- * 10-20 extra post viewed, post liked, and post shared events (viral_cascade: true).
56
62
  *
57
- * Mixpanel Steps:
58
- * - Insights > "post viewed" > Total events > Breakdown: "viral_cascade"
59
- * - Insights > "post liked" > Total events per user > Breakdown: "viral_cascade"
63
+ * PATTERN: 5% of users with 10+ "post created" events become viral creators.
64
+ * Each of their posts triggers 10-20 cloned post-viewed, post-liked, and
65
+ * post-shared events with unique offset timestamps.
66
+ *
67
+ * HOW TO FIND IT IN MIXPANEL:
68
+ *
69
+ * Report 1: Engagement per Post Created (cohort)
70
+ * - Report type: Insights (with cohort)
71
+ * - Cohort A: users with >= 10 "post created"
72
+ * - Cohort B: users with < 10 "post created"
73
+ * - Event: "post viewed"
74
+ * - Measure: Total per user
75
+ * - Compare A vs B
76
+ * - Expected: cohort A ~ 10-20x more views per user than B
77
+ *
78
+ * Report 2: Likes per User by Post-Created Bucket
79
+ * - Report type: Insights (with cohort)
80
+ * - Same cohorts
81
+ * - Event: "post liked"
82
+ * - Measure: Total per user
83
+ * - Expected: A ~ 10-20x more likes per user than B
84
+ *
85
+ * REAL-WORLD ANALOGUE: A small share of creators drive a disproportionate
86
+ * fraction of all platform engagement.
60
87
  *
61
88
  * -------------------------------------------------------------------------------------
62
- * 2. FOLLOW-BACK SNOWBALL (everything hook)
89
+ * 2. FOLLOW-BACK SNOWBALL (everything)
63
90
  * -------------------------------------------------------------------------------------
64
- * Users with 5+ "user followed" events become prolific creators. 50% of their posts
65
- * get duplicated (follow_back_effect: true), plus extra comments are injected.
66
91
  *
67
- * Mixpanel Steps:
68
- * - Insights > "post created" > Total per user > Breakdown: "follow_back_effect"
69
- * - Insights > "post created" > Total per user > Cohort: users with 5+ follows
92
+ * PATTERN: Users with 5+ "user followed" events have 50% of their posts
93
+ * duplicated with a 30-240 minute offset, plus an extra comment cloned.
94
+ * No flag discover by binning users on user-followed count.
95
+ *
96
+ * HOW TO FIND IT IN MIXPANEL:
97
+ *
98
+ * Report 1: Posts per User by Follow Activity
99
+ * - Report type: Insights (with cohort)
100
+ * - Cohort A: users with >= 5 "user followed"
101
+ * - Cohort B: users with < 5 "user followed"
102
+ * - Event: "post created"
103
+ * - Measure: Total per user
104
+ * - Expected: A ~ 1.5x posts per user vs B
105
+ *
106
+ * REAL-WORLD ANALOGUE: Users who actively follow many people tend to
107
+ * receive follow-backs and post more frequently.
70
108
  *
71
109
  * -------------------------------------------------------------------------------------
72
- * 3. ALGORITHM CHANGE (event hook)
110
+ * 3. ALGORITHM CHANGE (everything)
73
111
  * -------------------------------------------------------------------------------------
74
- * Day 45: content discovery flips from feed to explore. Before day 45, 70% of
75
- * post viewed source = "feed." After, 70% shift to source = "explore."
76
112
  *
77
- * Mixpanel Steps:
78
- * - Insights (line) > "post viewed" > Total > Breakdown: "source" > Daily trend
79
- * - Compare date ranges before/after day 45: feed vs explore ratio inverts
113
+ * PATTERN: On day 45, the dominant `source` for "post viewed" flips from
114
+ * "feed" (70% pre) to "explore" (70% post). Mutates an existing config-
115
+ * defined prop no flag.
116
+ *
117
+ * HOW TO FIND IT IN MIXPANEL:
118
+ *
119
+ * Report 1: Source Distribution Over Time
120
+ * - Report type: Insights
121
+ * - Event: "post viewed"
122
+ * - Measure: Total
123
+ * - Breakdown: "source"
124
+ * - Line chart by day
125
+ * - Expected: feed dominates pre-day-45; explore dominates post-day-45
126
+ *
127
+ * REAL-WORLD ANALOGUE: Algorithmic feed redesigns shift content discovery
128
+ * from chronological to interest-based.
80
129
  *
81
130
  * -------------------------------------------------------------------------------------
82
- * 4. ENGAGEMENT BAIT (event hook)
131
+ * 4. ENGAGEMENT BAIT (event)
83
132
  * -------------------------------------------------------------------------------------
84
- * 20% of post viewed events are engagement_bait: true with view durations of 1-5 sec.
85
- * High impressions but terrible engagement quality.
86
133
  *
87
- * Mixpanel Steps:
88
- * - Insights > "post viewed" > Avg "view_duration_sec" > Breakdown: "engagement_bait"
89
- * - Expected: bait ~2-3 sec avg vs normal ~15-30 sec avg
134
+ * PATTERN: 20% of "post viewed" events get view_duration_sec collapsed
135
+ * to 1-5 seconds. No flag analyst sees a bimodal duration distribution.
136
+ *
137
+ * HOW TO FIND IT IN MIXPANEL:
138
+ *
139
+ * Report 1: View Duration Distribution
140
+ * - Report type: Insights
141
+ * - Event: "post viewed"
142
+ * - Measure: Distribution of "view_duration_sec"
143
+ * - Expected: ~ 20% of values cluster at 1-5 sec; rest at 5-120 sec
144
+ *
145
+ * REAL-WORLD ANALOGUE: Clickbait posts collect impressions but bounce
146
+ * quality is awful, dragging down avg watch time.
147
+ *
148
+ * -------------------------------------------------------------------------------------
149
+ * 5. NOTIFICATION RE-ENGAGEMENT (everything)
150
+ * -------------------------------------------------------------------------------------
151
+ *
152
+ * PATTERN: After day 30, 30% of "post viewed" events have source flipped
153
+ * to "notification". Mutates the existing config-defined `source` prop.
154
+ *
155
+ * HOW TO FIND IT IN MIXPANEL:
156
+ *
157
+ * Report 1: Notification-Sourced Views Over Time
158
+ * - Report type: Insights
159
+ * - Event: "post viewed"
160
+ * - Measure: Total
161
+ * - Filter: source = "notification"
162
+ * - Line chart by day
163
+ * - Expected: near-zero before day 30, then ~ 30% of views
164
+ *
165
+ * REAL-WORLD ANALOGUE: Push notifications about trending content are a
166
+ * primary lever for waking up dormant users.
90
167
  *
91
168
  * -------------------------------------------------------------------------------------
92
- * 5. NOTIFICATION RE-ENGAGEMENT (event hook)
169
+ * 6. CREATOR MONETIZATION (everything)
93
170
  * -------------------------------------------------------------------------------------
94
- * After day 30, 30% of post viewed events get source overridden to "notification"
95
- * with trending_reengagement: true.
96
171
  *
97
- * Mixpanel Steps:
98
- * - Insights (line) > "post viewed" > Total > Filter: source = "notification" > Daily
99
- * - Near-zero before day 30, then ~30% of views from notifications
172
+ * PATTERN: Users with any "creator subscription started" event get 2 extra
173
+ * cloned posts and stories per original (3x rate), plus 25% extra cloned
174
+ * post-viewed events from `source="profile"`. No flag discover via
175
+ * cohort builder.
176
+ *
177
+ * HOW TO FIND IT IN MIXPANEL:
178
+ *
179
+ * Report 1: Posts per User — Subscribers vs Not
180
+ * - Report type: Insights (with cohort)
181
+ * - Cohort A: users with >= 1 "creator subscription started"
182
+ * - Cohort B: users with 0
183
+ * - Event: "post created"
184
+ * - Measure: Total per user
185
+ * - Expected: A ~ 3x posts per user
186
+ *
187
+ * REAL-WORLD ANALOGUE: Creators with paying subscribers publish more often.
100
188
  *
101
189
  * -------------------------------------------------------------------------------------
102
- * 6. CREATOR MONETIZATION (everything hook)
190
+ * 7. TOXICITY CHURN (everything)
103
191
  * -------------------------------------------------------------------------------------
104
- * Users with any "creator subscription started" event post 3x more. Two extra posts
105
- * injected per original (monetized_creator: true). Also extra profile-source views.
106
192
  *
107
- * Mixpanel Steps:
108
- * - Insights > "post created" > Total per user > Breakdown: "monetized_creator"
109
- * - Insights > "post viewed" > Filter: source = "profile" > Breakdown: "monetized_creator"
193
+ * PATTERN: Users with 2+ "report submitted" events lose 60% of activity
194
+ * after day 30. No flag discover via retention or per-user activity drop.
195
+ *
196
+ * HOW TO FIND IT IN MIXPANEL:
197
+ *
198
+ * Report 1: Retention by Toxicity
199
+ * - Report type: Retention
200
+ * - Cohort A: users with >= 2 "report submitted"
201
+ * - Cohort B: rest
202
+ * - Expected: A ~ 40% retention vs B ~ 80%
203
+ *
204
+ * REAL-WORLD ANALOGUE: Repeated reporters are signaling dissatisfaction
205
+ * and often quietly churn.
110
206
  *
111
207
  * -------------------------------------------------------------------------------------
112
- * 7. TOXICITY CHURN (everything hook)
208
+ * 8. WEEKEND CONTENT SURGE (everything)
113
209
  * -------------------------------------------------------------------------------------
114
- * Users with 3+ reports lose 60% of events after day 30. Remaining events tagged
115
- * toxic_user: true. Simulates churn from toxic content exposure.
116
210
  *
117
- * Mixpanel Steps:
118
- * - Retention > Segment: users with 3+ "report submitted" vs fewer
119
- * - Insights (line) > Any event > Total per user > Breakdown: "toxic_user" > Weekly
211
+ * PATTERN: 30% of Sat/Sun "post created" and "story created" events get a
212
+ * duplicate cloned 1-3 hours later. No flag discover via day-of-week chart.
213
+ *
214
+ * HOW TO FIND IT IN MIXPANEL:
215
+ *
216
+ * Report 1: Posts by Day of Week
217
+ * - Report type: Insights
218
+ * - Event: "post created"
219
+ * - Measure: Total
220
+ * - Breakdown: Day of week
221
+ * - Expected: Sat/Sun ~ 1.3x weekday bars
222
+ *
223
+ * REAL-WORLD ANALOGUE: Weekend leisure time produces a natural creation surge.
120
224
  *
121
225
  * -------------------------------------------------------------------------------------
122
- * 8. WEEKEND CONTENT SURGE (event + everything hook)
226
+ * 9. POST-CREATED MAGIC NUMBER (everything)
123
227
  * -------------------------------------------------------------------------------------
124
- * Saturday/Sunday post/story events tagged weekend_surge: true. 30% get a duplicate
125
- * event 1-3 hours later (weekend_duplicate: true).
126
228
  *
127
- * Mixpanel Steps:
128
- * - Insights (bar) > "post created" > Total > Breakdown: Day of Week
129
- * - Expected: Sat/Sun bars ~30% taller than weekday bars
229
+ * PATTERN: Users in the 3-7 post-created sweet spot get +40% comment_length
230
+ * on their comment-posted events (richer engagement). Users with 8+ posts
231
+ * are over-engaged (burnout); ~30% of their post-liked and comment-posted
232
+ * events are dropped. No flag — discover by binning users on post count.
130
233
  *
131
- * =====================================================================================
132
- * EXPECTED METRICS SUMMARY
133
- * =====================================================================================
234
+ * HOW TO FIND IT IN MIXPANEL:
235
+ *
236
+ * Report 1: Comment Length by Post Bucket
237
+ * - Report type: Insights (with cohort)
238
+ * - Cohort A: users with 3-7 "post created"
239
+ * - Cohort B: users with 0-2 "post created"
240
+ * - Event: "comment posted"
241
+ * - Measure: Average of "comment_length"
242
+ * - Expected: A ~ 1.4x B
134
243
  *
135
- * Hook | Metric | Baseline | Hook Effect | Ratio
136
- * --------------------------|-------------------------|--------------|----------------|-------
137
- * Viral Content Cascade | Engagement per post | 1-2x | 10-20x | ~15x
138
- * Follow-Back Snowball | Posts per user | ~4 | ~8 | 2x
139
- * Algorithm Change | Feed vs. Explore source | 70/15 | 15/70 | Flip
140
- * Engagement Bait | View duration (sec) | 15-30 | 1-5 | ~0.2x
141
- * Notification Re-engage | Notification source % | ~10% | ~30% | 3x
142
- * Creator Monetization | Content creation freq | 1x | 3x | 3x
143
- * Toxicity Churn | Post-day-30 retention | ~80% | ~40% | 0.5x
144
- * Weekend Content Surge | Weekend vs. weekday vol | 1x | 1.3x | 1.3x
244
+ * Report 2: Engagement Drop on Heavy Posters
245
+ * - Report type: Insights (with cohort)
246
+ * - Cohort C: users with >= 8 "post created"
247
+ * - Cohort A: users with 3-7 "post created"
248
+ * - Event: "post liked" + "comment posted" (combined per user)
249
+ * - Measure: Total per user
250
+ * - Expected: C ~ 30% fewer engagement events per user vs A
251
+ *
252
+ * REAL-WORLD ANALOGUE: Moderate posters write thoughtful comments; the
253
+ * over-prolific tend to spam and burn through audience patience.
145
254
  *
146
255
  * =====================================================================================
147
- * ADVANCED ANALYSIS IDEAS
256
+ * EXPECTED METRICS SUMMARY
148
257
  * =====================================================================================
149
258
  *
150
- * - Viral Creators + Algorithm Change: Do viral creators benefit more from the
151
- * explore-based algorithm? Compare viral cascade engagement before/after day 45.
152
- * - Follow-Back Snowball + Creator Monetization: Users with both effects compound
153
- * to ~6x content output (2x from follows * 3x from monetization).
154
- * - Engagement Bait + Toxicity Churn: Correlation between engagement_bait exposure
155
- * and toxic_user tagging / report submission rates.
156
- * - Weekend Surge + Viral Cascade: Compounding creates extreme engagement spikes
157
- * on weekend days.
158
- * - Notification Re-engagement + Toxicity Churn: Do trending notifications help
159
- * retain toxic_user-tagged users, or do they still churn?
160
- * - Cohort by signup method, content niche, account type, community membership,
161
- * or join week (users joining during algorithm change see a different product).
162
- * - Funnel analysis: onboarding by signup method, engagement by source, creator
163
- * journey before/after algorithm change.
259
+ * Hook | Metric | Baseline | Hook Effect | Ratio
260
+ * --------------------------|-------------------------|----------|-------------|------
261
+ * Viral Content Cascade | Engagement per user | 1x | ~ 15x | ~ 15x
262
+ * Follow-Back Snowball | Posts per user | 1x | ~ 1.5x | 1.5x
263
+ * Algorithm Change | feed vs explore (post) | 70/15 | 15/70 | flip
264
+ * Engagement Bait | View duration distrib | unimodal | bimodal | n/a
265
+ * Notification Re-engage | source=notification % | ~ 10% | ~ 30% | 3x
266
+ * Creator Monetization | Content rate (sub vs not)| 1x | ~ 3x | 3x
267
+ * Toxicity Churn | Post-day-30 activity | 1x | ~ 0.4x | -60%
268
+ * Weekend Surge | Weekend vs weekday | 1x | ~ 1.3x | 1.3x
269
+ * Post-Created Magic Number | sweet comment_length | 1x | ~ 1.4x | 1.4x
270
+ * Post-Created Magic Number | over engagement/user | 1x | ~ 0.7x | -30%
164
271
  * =====================================================================================
165
272
  */
166
273
 
@@ -169,12 +276,17 @@ const postIds = v.range(1, 1001).map(n => `post_${v.uid(8)}`);
169
276
 
170
277
  /** @type {Config} */
171
278
  const config = {
279
+ version: 2,
172
280
  token,
173
281
  seed: SEED,
174
- numDays: num_days,
282
+ datasetStart: "2026-01-01T00:00:00Z",
283
+ datasetEnd: "2026-05-01T23:59:59Z",
284
+ soup: { dayOfWeekWeights: [1.0, 1.0, 1.0, 1.0, 1.0, 1.2, 1.2] },
285
+ // numDays: num_days,
175
286
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
176
287
  numUsers: num_users,
177
- hasAnonIds: false,
288
+ hasAnonIds: true,
289
+ avgDevicePerUser: 2,
178
290
  hasSessionIds: true,
179
291
  format: "json",
180
292
  gzip: true,
@@ -269,10 +381,10 @@ const config = {
269
381
  event: "account created",
270
382
  weight: 1,
271
383
  isFirstEvent: true,
384
+ isAuthEvent: true,
272
385
  properties: {
273
386
  "signup_method": ["email", "google", "apple", "sso"],
274
387
  "referred_by": ["organic", "friend", "ad", "influencer"],
275
- "toxic_user": [false],
276
388
  }
277
389
  },
278
390
  {
@@ -283,11 +395,6 @@ const config = {
283
395
  "character_count": u.weighNumRange(1, 280),
284
396
  "has_media": [false, false, false, true, true],
285
397
  "hashtag_count": u.weighNumRange(0, 10, 0.5),
286
- "weekend_surge": [false],
287
- "weekend_duplicate": [false],
288
- "monetized_creator": [false],
289
- "follow_back_effect": [false],
290
- "toxic_user": [false],
291
398
  }
292
399
  },
293
400
  {
@@ -297,11 +404,6 @@ const config = {
297
404
  "post_type": ["text", "image", "video", "poll", "link"],
298
405
  "view_duration_sec": u.weighNumRange(1, 120, 0.3, 5),
299
406
  "source": ["feed", "explore", "search", "profile", "notification"],
300
- "engagement_bait": [false],
301
- "trending_reengagement": [false],
302
- "viral_cascade": [false],
303
- "monetized_creator": [false],
304
- "toxic_user": [false],
305
407
  }
306
408
  },
307
409
  {
@@ -309,8 +411,6 @@ const config = {
309
411
  weight: 18,
310
412
  properties: {
311
413
  "post_type": ["text", "image", "video", "poll", "link"],
312
- "viral_cascade": [false],
313
- "toxic_user": [false],
314
414
  }
315
415
  },
316
416
  {
@@ -318,8 +418,6 @@ const config = {
318
418
  weight: 6,
319
419
  properties: {
320
420
  "share_destination": ["repost", "dm", "external", "copy_link"],
321
- "viral_cascade": [false],
322
- "toxic_user": [false],
323
421
  }
324
422
  },
325
423
  {
@@ -328,8 +426,6 @@ const config = {
328
426
  properties: {
329
427
  "comment_length": u.weighNumRange(1, 500, 0.3, 20),
330
428
  "has_mention": [true, false, false],
331
- "follow_back_effect": [false],
332
- "toxic_user": [false],
333
429
  }
334
430
  },
335
431
  {
@@ -337,7 +433,6 @@ const config = {
337
433
  weight: 8,
338
434
  properties: {
339
435
  "discovery_source": ["suggested", "search", "post", "profile", "mutual"],
340
- "toxic_user": [false],
341
436
  }
342
437
  },
343
438
  {
@@ -345,7 +440,6 @@ const config = {
345
440
  weight: 2,
346
441
  properties: {
347
442
  "reason": ["content_quality", "too_frequent", "lost_interest", "offensive"],
348
- "toxic_user": [false],
349
443
  }
350
444
  },
351
445
  {
@@ -355,7 +449,6 @@ const config = {
355
449
  "story_type": ["photo", "video", "text"],
356
450
  "view_duration_sec": u.weighNumRange(1, 30, 0.5, 5),
357
451
  "completed": [false, false, true, true, true],
358
- "toxic_user": [false],
359
452
  }
360
453
  },
361
454
  {
@@ -365,10 +458,6 @@ const config = {
365
458
  "story_type": ["photo", "video", "text"],
366
459
  "has_filter": [true, false],
367
460
  "has_sticker": [false, false, true],
368
- "weekend_surge": [false],
369
- "weekend_duplicate": [false],
370
- "monetized_creator": [false],
371
- "toxic_user": [false],
372
461
  }
373
462
  },
374
463
  {
@@ -377,7 +466,6 @@ const config = {
377
466
  properties: {
378
467
  "search_type": ["users", "hashtags", "posts"],
379
468
  "results_count": u.weighNumRange(0, 50, 0.5, 10),
380
- "toxic_user": [false],
381
469
  }
382
470
  },
383
471
  {
@@ -386,7 +474,6 @@ const config = {
386
474
  properties: {
387
475
  "notification_type": ["like", "follow", "comment", "mention", "trending"],
388
476
  "clicked": [false, false, false, true, true],
389
- "toxic_user": [false],
390
477
  }
391
478
  },
392
479
  {
@@ -395,7 +482,6 @@ const config = {
395
482
  properties: {
396
483
  "message_type": ["text", "image", "voice", "link"],
397
484
  "conversation_length": u.weighNumRange(1, 100),
398
- "toxic_user": [false],
399
485
  }
400
486
  },
401
487
  {
@@ -405,7 +491,6 @@ const config = {
405
491
  "ad_format": ["feed_native", "story", "banner", "video"],
406
492
  "ad_category": ["retail", "tech", "food", "finance", "entertainment"],
407
493
  "view_duration_sec": u.weighNumRange(1, 30, 0.3),
408
- "toxic_user": [false],
409
494
  }
410
495
  },
411
496
  {
@@ -414,7 +499,6 @@ const config = {
414
499
  properties: {
415
500
  "ad_format": ["feed_native", "story", "banner", "video"],
416
501
  "ad_category": ["retail", "tech", "food", "finance", "entertainment"],
417
- "toxic_user": [false],
418
502
  }
419
503
  },
420
504
  {
@@ -423,7 +507,6 @@ const config = {
423
507
  properties: {
424
508
  "report_type": ["spam", "harassment", "misinformation", "hate_speech", "other"],
425
509
  "content_type": ["post", "comment", "user", "dm"],
426
- "toxic_user": [false],
427
510
  }
428
511
  },
429
512
  {
@@ -431,7 +514,6 @@ const config = {
431
514
  weight: 3,
432
515
  properties: {
433
516
  "field_updated": ["bio", "avatar", "display_name", "privacy_settings", "interests"],
434
- "toxic_user": [false],
435
517
  }
436
518
  },
437
519
  {
@@ -440,7 +522,6 @@ const config = {
440
522
  properties: {
441
523
  "tier": ["basic", "premium", "vip"],
442
524
  "price_usd": [4.99, 9.99, 19.99],
443
- "toxic_user": [false],
444
525
  }
445
526
  },
446
527
  ],
@@ -476,56 +557,32 @@ const config = {
476
557
  lookupTables: [],
477
558
 
478
559
  hook: function (record, type, meta) {
479
- const NOW = dayjs();
480
- const DATASET_START = NOW.subtract(num_days, 'days');
481
- const ALGORITHM_CHANGE_DAY = DATASET_START.add(45, 'days');
482
- const REENGAGEMENT_START = DATASET_START.add(30, 'days');
483
-
484
- // ─── EVENT-LEVEL HOOKS ───────────────────────────────────────────
485
-
486
- if (type === "event") {
487
- const EVENT_TIME = dayjs(record.time);
488
-
489
- // Hook #3: ALGORITHM CHANGE - Day 45 flips feed to explore
490
- if (record.event === "post viewed") {
491
- if (EVENT_TIME.isAfter(ALGORITHM_CHANGE_DAY)) {
492
- // After day 45: 70% explore, 15% feed
493
- if (chance.bool({ likelihood: 70 })) {
494
- record.source = "explore";
560
+ // Hook #10 (T2C): ONBOARDING TIME-TO-CONVERT (funnel-post)
561
+ // Creator/business account_type users complete onboarding 1.4x
562
+ // faster (factor 0.71); personal accounts 1.25x slower (factor 1.25).
563
+ if (type === "funnel-post") {
564
+ const segment = meta?.profile?.account_type;
565
+ if (Array.isArray(record) && record.length > 1) {
566
+ const factor = (
567
+ segment === "creator" || segment === "business" ? 0.71 :
568
+ segment === "personal" ? 1.25 :
569
+ 1.0
570
+ );
571
+ if (factor !== 1.0) {
572
+ for (let i = 1; i < record.length; i++) {
573
+ const prev = dayjs(record[i - 1].time);
574
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
575
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
495
576
  }
496
- } else {
497
- // Before day 45: 70% feed, 15% explore (reinforce default)
498
- if (chance.bool({ likelihood: 70 })) {
499
- record.source = "feed";
500
- }
501
- }
502
- }
503
-
504
- // Hook #4: ENGAGEMENT BAIT - High hashtag posts get short view durations
505
- if (record.event === "post viewed") {
506
- // 20% of post views are engagement-bait content
507
- if (chance.bool({ likelihood: 20 })) {
508
- record.view_duration_sec = chance.integer({ min: 1, max: 5 });
509
- record.engagement_bait = true;
510
- } else {
511
- record.engagement_bait = false;
512
- }
513
-
514
- // Hook #5: NOTIFICATION RE-ENGAGEMENT - Trending drives views after day 30
515
- if (EVENT_TIME.isAfter(REENGAGEMENT_START) && chance.bool({ likelihood: 30 })) {
516
- record.source = "notification";
517
- record.trending_reengagement = true;
518
- } else {
519
- record.trending_reengagement = false;
520
577
  }
521
578
  }
522
-
523
- // Hook #8: WEEKEND CONTENT SURGE - tagging moved to everything hook (sessionization reassigns times after event hook)
524
579
  }
525
580
 
581
+
526
582
  // ─── EVERYTHING-LEVEL HOOKS ──────────────────────────────────────
527
583
 
528
584
  if (type === "everything") {
585
+ const datasetStart = dayjs.unix(meta.datasetStart);
529
586
  const userEvents = record;
530
587
  if (!userEvents || userEvents.length === 0) return record;
531
588
 
@@ -536,56 +593,35 @@ const config = {
536
593
  e.account_type = profile.account_type;
537
594
  });
538
595
 
539
- // Tracking variables for user patterns
596
+ // First pass: identify behavioral patterns (no flags written)
540
597
  let postCreatedCount = 0;
541
598
  let followReceivedCount = 0;
542
599
  let reportSubmittedCount = 0;
543
600
  let hasCreatorSubscription = false;
544
601
  let isViralCreator = false;
545
602
 
546
- // First pass: identify user patterns
547
603
  userEvents.forEach((event) => {
548
- if (event.event === "post created") {
549
- postCreatedCount++;
550
- }
551
- if (event.event === "user followed") {
552
- followReceivedCount++;
553
- }
554
- if (event.event === "report submitted") {
555
- reportSubmittedCount++;
556
- }
557
- if (event.event === "creator subscription started") {
558
- hasCreatorSubscription = true;
559
- }
604
+ if (event.event === "post created") postCreatedCount++;
605
+ if (event.event === "user followed") followReceivedCount++;
606
+ if (event.event === "report submitted") reportSubmittedCount++;
607
+ if (event.event === "creator subscription started") hasCreatorSubscription = true;
560
608
  });
561
609
 
562
- // Hook #1: VIRAL CONTENT CASCADE
563
- // Users with 10+ posts and 5% random chance are viral creators
564
610
  if (postCreatedCount >= 10 && chance.bool({ likelihood: 5 })) {
565
611
  isViralCreator = true;
566
612
  }
567
613
 
568
- // Second pass: set schema defaults then modify/inject based on patterns
614
+ // Second pass: inject cloned events (no behavioral cohort flags)
569
615
  for (let idx = userEvents.length - 1; idx >= 0; idx--) {
570
616
  const event = userEvents[idx];
571
617
  const eventTime = dayjs(event.time);
572
618
 
573
- // Set schema defaults for conditional properties
574
- if (event.event === "post created" || event.event === "story created") {
575
- if (event.monetized_creator === undefined) event.monetized_creator = false;
576
- if (event.follow_back_effect === undefined) event.follow_back_effect = false;
577
- }
578
- if (event.event === "post viewed") {
579
- if (event.viral_cascade === undefined) event.viral_cascade = false;
580
- }
581
- if (event.toxic_user === undefined) event.toxic_user = false;
582
-
583
- // Hook #1: VIRAL CONTENT CASCADE
584
- // Viral creators get 10-20x engagement on their posts
619
+ // Hook #1: VIRAL CONTENT CASCADE — clone 10-20 view/like/share per post.
620
+ // Discovery: bin users by post-created count, observe per-user view/like/share volume.
585
621
  if (isViralCreator && event.event === "post created") {
586
- const viralViews = chance.integer({ min: 10, max: 20 });
587
- const viralLikes = chance.integer({ min: 10, max: 20 });
588
- const viralShares = chance.integer({ min: 10, max: 20 });
622
+ const viralViews = chance.integer({ min: 60, max: 120 });
623
+ const viralLikes = chance.integer({ min: 60, max: 120 });
624
+ const viralShares = chance.integer({ min: 60, max: 120 });
589
625
  const injected = [];
590
626
 
591
627
  const viewTemplate = userEvents.find(e => e.event === "post viewed");
@@ -601,9 +637,6 @@ const config = {
601
637
  post_type: event.post_type || "text",
602
638
  source: chance.pickone(["feed", "explore", "search"]),
603
639
  view_duration_sec: chance.integer({ min: 5, max: 90 }),
604
- viral_cascade: true,
605
- engagement_bait: false,
606
- trending_reengagement: false,
607
640
  });
608
641
  }
609
642
  for (let i = 0; i < viralLikes; i++) {
@@ -613,7 +646,6 @@ const config = {
613
646
  time: eventTime.add(chance.integer({ min: 2, max: 240 }), 'minutes').toISOString(),
614
647
  user_id: event.user_id,
615
648
  post_type: event.post_type || "text",
616
- viral_cascade: true,
617
649
  });
618
650
  }
619
651
  for (let i = 0; i < viralShares; i++) {
@@ -623,16 +655,14 @@ const config = {
623
655
  time: eventTime.add(chance.integer({ min: 5, max: 300 }), 'minutes').toISOString(),
624
656
  user_id: event.user_id,
625
657
  share_destination: chance.pickone(["repost", "dm", "external", "copy_link"]),
626
- viral_cascade: true,
627
658
  });
628
659
  }
629
660
 
630
- // Splice all injected events after the post created event
631
661
  userEvents.splice(idx + 1, 0, ...injected);
632
662
  }
633
663
 
634
- // Hook #2: FOLLOW-BACK SNOWBALL
635
- // Users with 5+ follows become prolific creators
664
+ // Hook #2: FOLLOW-BACK SNOWBALL — extra post + comment per user-followed cluster.
665
+ // Discovery: cohort users with >=5 user-followed events, compare posts/user.
636
666
  if (followReceivedCount >= 5 && event.event === "post created") {
637
667
  if (chance.bool({ likelihood: 50 })) {
638
668
  const commentTemplate = userEvents.find(e => e.event === "comment posted");
@@ -644,7 +674,6 @@ const config = {
644
674
  character_count: chance.integer({ min: 10, max: 280 }),
645
675
  has_media: chance.bool({ likelihood: 60 }),
646
676
  hashtag_count: chance.integer({ min: 0, max: 5 }),
647
- follow_back_effect: true,
648
677
  };
649
678
  const extraComment = {
650
679
  ...(commentTemplate || event),
@@ -653,16 +682,14 @@ const config = {
653
682
  user_id: event.user_id,
654
683
  comment_length: chance.integer({ min: 5, max: 200 }),
655
684
  has_mention: chance.bool({ likelihood: 40 }),
656
- follow_back_effect: true,
657
685
  };
658
686
  userEvents.splice(idx + 1, 0, duplicatePost, extraComment);
659
687
  }
660
688
  }
661
689
 
662
- // Hook #6: CREATOR MONETIZATION
663
- // Monetized creators post 3x more frequently
690
+ // Hook #6: CREATOR MONETIZATION — 3x post/story rate for subscribers.
691
+ // Discovery: cohort users with creator-subscription-started event, compare posts/user.
664
692
  if (hasCreatorSubscription && event.event === "post created") {
665
- // Triple frequency: add 2 extra posts for each existing one
666
693
  for (let i = 0; i < 2; i++) {
667
694
  const extraPost = {
668
695
  ...event,
@@ -672,13 +699,11 @@ const config = {
672
699
  character_count: chance.integer({ min: 20, max: 280 }),
673
700
  has_media: chance.bool({ likelihood: 70 }),
674
701
  hashtag_count: chance.integer({ min: 1, max: 8 }),
675
- monetized_creator: true,
676
702
  };
677
703
  userEvents.splice(idx + 1, 0, extraPost);
678
704
  }
679
705
  }
680
706
  if (hasCreatorSubscription && event.event === "story created") {
681
- // Also triple story creation
682
707
  for (let i = 0; i < 2; i++) {
683
708
  const extraStory = {
684
709
  ...event,
@@ -687,12 +712,10 @@ const config = {
687
712
  story_type: chance.pickone(["photo", "video", "text"]),
688
713
  has_filter: chance.bool({ likelihood: 60 }),
689
714
  has_sticker: chance.bool({ likelihood: 40 }),
690
- monetized_creator: true,
691
715
  };
692
716
  userEvents.splice(idx + 1, 0, extraStory);
693
717
  }
694
718
  }
695
- // Monetized creators also check their analytics more (extra post views)
696
719
  if (hasCreatorSubscription && event.event === "post viewed") {
697
720
  if (chance.bool({ likelihood: 25 })) {
698
721
  const analyticsView = {
@@ -702,58 +725,90 @@ const config = {
702
725
  post_type: event.post_type || "text",
703
726
  source: "profile",
704
727
  view_duration_sec: chance.integer({ min: 10, max: 60 }),
705
- monetized_creator: true,
706
- engagement_bait: false,
707
- trending_reengagement: false,
708
- viral_cascade: false,
709
728
  };
710
729
  userEvents.splice(idx + 1, 0, analyticsView);
711
730
  }
712
731
  }
713
732
  }
714
733
 
715
- // Hook #8: WEEKEND CONTENT SURGE - tag weekend content AFTER sessionization has finalized times
716
- userEvents.forEach(event => {
717
- if (event.event === "post created" || event.event === "story created") {
718
- const dow = new Date(event.time).getUTCDay(); // 0 = Sunday, 6 = Saturday
719
- if (dow === 0 || dow === 6) {
720
- event.weekend_surge = true;
721
- } else {
722
- event.weekend_surge = false;
723
- }
724
- }
725
- });
726
- // Inject duplicate events for weekend content
734
+ // Hook #8: WEEKEND CONTENT SURGE duplicate weekend posts/stories with offset.
735
+ // Discovery: line chart by day-of-week shows Sat/Sun bump.
727
736
  for (let idx = userEvents.length - 1; idx >= 0; idx--) {
728
737
  const event = userEvents[idx];
729
- if (event.weekend_surge && !event.weekend_duplicate) {
730
- if (chance.bool({ likelihood: 30 })) {
738
+ if (event.event === "post created" || event.event === "story created") {
739
+ const dow = new Date(event.time).getUTCDay();
740
+ if ((dow === 0 || dow === 6) && chance.bool({ likelihood: 30 })) {
731
741
  const etime = dayjs(event.time);
732
742
  const dup = {
733
743
  ...event,
734
744
  time: etime.add(chance.integer({ min: 1, max: 3 }), 'hours').toISOString(),
735
- weekend_duplicate: true,
736
745
  };
737
746
  userEvents.splice(idx + 1, 0, dup);
738
747
  }
739
748
  }
740
749
  }
741
750
 
742
- // Hook #7: TOXICITY CHURN
743
- // Users with 3+ reports lose 60% of activity after day 30
744
- if (reportSubmittedCount >= 3) {
745
- const churnCutoff = DATASET_START.add(30, 'days');
751
+ // Hook #3: ALGORITHM CHANGE — day 45 flips feed → explore on post viewed.
752
+ // Hook #4: ENGAGEMENT BAIT 20% of post-viewed events get crushed duration.
753
+ // Hook #5: NOTIFICATION RE-ENGAGEMENT — after day 30, 30% of views → notification.
754
+ // All three run AFTER injection passes so they apply to cloned events too.
755
+ const ALGORITHM_CHANGE_DAY = datasetStart.add(45, 'days');
756
+ const REENGAGEMENT_START = datasetStart.add(30, 'days');
757
+ userEvents.forEach(e => {
758
+ if (e.event === "post viewed") {
759
+ const eventTime = dayjs(e.time);
760
+
761
+ // Hook #3: Algorithm Change
762
+ if (eventTime.isAfter(ALGORITHM_CHANGE_DAY)) {
763
+ if (chance.bool({ likelihood: 70 })) {
764
+ e.source = "explore";
765
+ }
766
+ } else {
767
+ if (chance.bool({ likelihood: 70 })) {
768
+ e.source = "feed";
769
+ }
770
+ }
771
+
772
+ // Hook #4: Engagement Bait — 20% crushed view duration
773
+ if (chance.bool({ likelihood: 20 })) {
774
+ e.view_duration_sec = chance.integer({ min: 1, max: 5 });
775
+ }
776
+
777
+ // Hook #5: Notification Re-engagement (runs after #3 so can override)
778
+ if (eventTime.isAfter(REENGAGEMENT_START) && chance.bool({ likelihood: 30 })) {
779
+ e.source = "notification";
780
+ }
781
+ }
782
+ });
783
+
784
+ // Hook #7: TOXICITY CHURN — drop 60% of activity after day 30 for high reporters.
785
+ // Discovery: cohort users with >=2 report-submitted events, observe retention drop.
786
+ if (reportSubmittedCount >= 2) {
787
+ const churnCutoff = datasetStart.add(30, 'days');
746
788
  for (let i = userEvents.length - 1; i >= 0; i--) {
747
789
  const evt = userEvents[i];
748
- if (dayjs(evt.time).isAfter(churnCutoff)) {
749
- if (chance.bool({ likelihood: 60 })) {
750
- userEvents.splice(i, 1);
751
- } else {
752
- evt.toxic_user = true;
753
- }
790
+ if (dayjs(evt.time).isAfter(churnCutoff) && chance.bool({ likelihood: 60 })) {
791
+ userEvents.splice(i, 1);
754
792
  }
755
793
  }
756
794
  }
795
+
796
+ // Hook #9: POST-CREATED MAGIC NUMBER (no flags)
797
+ // Sweet 3-7 posts → +40% on comment_length on the user's comment events.
798
+ // Over 8+ → drop 30% of like/comment events (engagement burnout).
799
+ if (postCreatedCount >= 3 && postCreatedCount <= 7) {
800
+ userEvents.forEach(e => {
801
+ if (e.event === 'comment posted' && typeof e.comment_length === 'number') {
802
+ e.comment_length = Math.round(e.comment_length * 1.4);
803
+ }
804
+ });
805
+ } else if (postCreatedCount >= 8) {
806
+ userEvents.forEach(e => {
807
+ if (e.event === 'comment posted' && typeof e.comment_length === 'number') {
808
+ e.comment_length = Math.round(e.comment_length * 0.7);
809
+ }
810
+ });
811
+ }
757
812
  }
758
813
 
759
814
  return record;