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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +51 -13
  3. package/dungeons/technical/ad-spend.js +2 -2
  4. package/dungeons/technical/anonymous-users.js +2 -2
  5. package/dungeons/technical/array-of-object-lookup.js +2 -4
  6. package/dungeons/technical/experiments.js +2 -2
  7. package/dungeons/technical/foobar.js +2 -2
  8. package/dungeons/technical/group-analytics.js +2 -2
  9. package/dungeons/technical/mirror-strategies.js +2 -2
  10. package/dungeons/technical/nested-objects.js +2 -2
  11. package/dungeons/technical/retention-cadence.js +2 -3
  12. package/dungeons/technical/sanity.js +2 -2
  13. package/dungeons/technical/scale-test.js +2 -2
  14. package/dungeons/technical/scd.js +2 -2
  15. package/dungeons/technical/simple.js +5 -6
  16. package/dungeons/technical/simplest-schema.json +5 -0
  17. package/dungeons/technical/simplest.js +2 -2
  18. package/dungeons/technical/text-generation.js +3 -3
  19. package/dungeons/vertical/ai-platform.js +858 -0
  20. package/dungeons/vertical/community.js +84 -40
  21. package/dungeons/vertical/crypto.js +830 -0
  22. package/dungeons/vertical/dating.js +744 -0
  23. package/dungeons/vertical/devtools.js +175 -69
  24. package/dungeons/vertical/ecommerce.js +242 -94
  25. package/dungeons/vertical/education.js +330 -313
  26. package/dungeons/vertical/fintech.js +442 -313
  27. package/dungeons/vertical/fitness.js +143 -61
  28. package/dungeons/vertical/food-delivery.js +327 -353
  29. package/dungeons/vertical/gaming.js +912 -382
  30. package/dungeons/vertical/healthcare.js +142 -63
  31. package/dungeons/vertical/insurance-application.js +170 -76
  32. package/dungeons/vertical/logistics.js +115 -20
  33. package/dungeons/vertical/marketplace.js +152 -58
  34. package/dungeons/vertical/media.js +248 -384
  35. package/dungeons/vertical/real-estate.js +781 -0
  36. package/dungeons/vertical/sass.js +255 -266
  37. package/dungeons/vertical/social.js +264 -206
  38. package/dungeons/vertical/travel.js +117 -41
  39. package/index.js +17 -17
  40. package/lib/core/config-validator.js +159 -31
  41. package/lib/core/context.js +10 -24
  42. package/lib/core/storage.js +6 -1
  43. package/lib/generators/events.js +11 -14
  44. package/lib/generators/funnels.js +12 -4
  45. package/lib/generators/mirror.js +3 -2
  46. package/lib/generators/product-names.js +1 -1
  47. package/lib/generators/scd.js +2 -1
  48. package/lib/generators/text.js +1 -1
  49. package/lib/orchestrators/user-loop.js +81 -47
  50. package/lib/templates/macro-presets.js +111 -0
  51. package/lib/templates/soup-presets.js +19 -36
  52. package/lib/utils/utils.js +71 -39
  53. package/package.json +8 -2
  54. package/scripts/smoke-test-all.mjs +162 -0
  55. package/scripts/verify-runner.mjs +72 -24
  56. package/types.d.ts +251 -51
  57. package/dungeons/technical/ad-spend-schema.json +0 -128
  58. package/dungeons/technical/anonymous-users-schema.json +0 -92
  59. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  60. package/dungeons/technical/experiments-schema.json +0 -203
  61. package/dungeons/technical/foobar-schema.json +0 -362
  62. package/dungeons/technical/group-analytics-schema.json +0 -241
  63. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  64. package/dungeons/technical/nested-objects-schema.json +0 -145
  65. package/dungeons/technical/retention-cadence-schema.json +0 -37
  66. package/dungeons/technical/sanity-schema.json +0 -185
  67. package/dungeons/technical/scale-test-schema.json +0 -70
  68. package/dungeons/technical/scd-schema.json +0 -467
  69. package/dungeons/technical/simple-schema.json +0 -362
  70. package/dungeons/technical/text-generation-schema.json +0 -1062
  71. package/dungeons/user/.gitkeep +0 -0
  72. package/dungeons/vertical/community-schema.json +0 -579
  73. package/dungeons/vertical/devtools-schema.json +0 -601
  74. package/dungeons/vertical/ecommerce-schema.json +0 -604
  75. package/dungeons/vertical/education-schema.json +0 -5686
  76. package/dungeons/vertical/fintech-schema.json +0 -630
  77. package/dungeons/vertical/fitness-schema.json +0 -530
  78. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  79. package/dungeons/vertical/gaming-schema.json +0 -438
  80. package/dungeons/vertical/healthcare-schema.json +0 -549
  81. package/dungeons/vertical/insurance-application-schema.json +0 -485
  82. package/dungeons/vertical/logistics-schema.json +0 -574
  83. package/dungeons/vertical/marketplace-schema.json +0 -533
  84. package/dungeons/vertical/media-schema.json +0 -4749
  85. package/dungeons/vertical/rpg-schema.json +0 -2491
  86. package/dungeons/vertical/rpg.js +0 -976
  87. package/dungeons/vertical/sass-schema.json +0 -3128
  88. package/dungeons/vertical/social-schema.json +0 -620
  89. package/dungeons/vertical/travel-schema.json +0 -580
@@ -2,7 +2,7 @@
2
2
  const SEED = "harness-education";
3
3
  const num_days = 100;
4
4
  const num_users = 5_000;
5
- const avg_events_per_user = 120;
5
+ const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
8
8
  // ── env overrides ──
@@ -52,132 +52,256 @@ const chance = u.initChance(SEED);
52
52
 
53
53
  /**
54
54
  * ═══════════════════════════════════════════════════════════════════════════════
55
- * ANALYTICS HOOKS (8 architected patterns)
55
+ * ANALYTICS HOOKS (9 hooks)
56
56
  * ═══════════════════════════════════════════════════════════════════════════════
57
57
  *
58
- * 1. STUDENT VS INSTRUCTOR PROFILES
59
- * Instructor profiles get teaching attributes (courses_created,
60
- * teaching_experience_years, instructor_rating). Students get learning
61
- * attributes (learning_goal, study_hours_per_week).
62
- *
63
- * Mixpanel reports:
64
- * • Insights → Any event → Unique users → Breakdown: "account_type"
65
- * Expected: ~89% students, ~11% instructors
66
- * • Insights → "instructor feedback given" → Total per user → Breakdown: "account_type"
67
- * Expected: Instructors dominate feedback; students show learning_goal instead
68
- *
69
- * 2. DEADLINE CRAMMING
70
- * Assignments submitted on Sun/Mon are rushed: 60% late (vs ~20% baseline),
71
- * quiz scores drop by 25 points. Events carry is_deadline_rush: true.
72
- *
73
- * Mixpanel reports:
74
- * • Insights → "assignment submitted" → Total → Breakdown: "is_deadline_rush"
75
- * Expected: is_deadline_rush=true shows ~60% late rate vs ~20% baseline
76
- * • Insights → "quiz completed" → Avg "score_percent" → Breakdown: Day of Week
77
- * Expected: Sun/Mon scores ~25 points lower (~40 vs ~65)
78
- *
79
- * 3. NOTES-TAKERS SUCCEED
80
- * Students with 5+ notes_taken=true lectures get +20 quiz score boost
81
- * (capped at 100) and 40% chance of bonus certificate. Marked diligent_student: true.
82
- *
83
- * Mixpanel reports:
84
- * • Insights → "quiz completed" → Avg "score_percent" → Breakdown: "diligent_student"
85
- * Expected: diligent_student=true ≈ 85 avg vs ~65 baseline (+20 pts)
86
- * • Insights → "certificate earned" → Total per user → Breakdown: "diligent_student"
87
- * Expected: diligent_student=true earn ~40% more certificates
88
- *
89
- * 4. STUDY GROUP RETENTION
90
- * Early study group joiners (within 10 days) retain and get bonus discussions.
91
- * Non-joiners with low quiz scores (<60) churn hard at day 14 (all later events removed).
92
- *
93
- * Mixpanel reports:
94
- * • Retention → A: "account registered" → B: Any event → Segment by early study group join
95
- * Expected: Early joiners ~90% D14 retention; non-joiners with low scores ~30%
96
- * • Insights → "discussion posted" → Total per user → Breakdown: "study_group_member"
97
- * Expected: study_group_member=true users post more
98
- *
99
- * 5. HINT DEPENDENCY
100
- * Hint users get 60% chance of easy problems; non-hint users get 40% chance of
101
- * hard problems with independent_solver: true.
102
- *
103
- * Mixpanel reports:
104
- * • Insights → "practice problem solved" → Total → Breakdown: "difficulty" → Filter: hint_used=true
105
- * Expected: ~60% easy (vs ~33% baseline)
106
- * • Insights → "practice problem solved" → Total → Breakdown: "difficulty" → Filter: hint_used=false
107
- * Expected: ~40% hard (vs ~33% baseline)
108
- *
109
- * 6. SEMESTER-END SPIKE
110
- * Days 75-85: quiz_started, quiz_completed, assignment_submitted events duplicated
111
- * at 80% rate. Events carry semester_end_rush: true.
112
- *
113
- * Mixpanel reports:
114
- * • Insights (line) → "quiz started" + "quiz completed" + "assignment submitted" → Daily
115
- * Expected: ~2x volume spike during days 75-85
116
- * • Insights → "quiz completed" → Total → Breakdown: "semester_end_rush"
117
- * Expected: semester_end_rush=true clusters in days 75-85
118
- *
119
- * 7. FREE VS PAID COURSES
120
- * Free users get 0.5x funnel conversion rate; paid subscribers get 1.5x.
121
- * Free users also lose 55% of certificates. Creates ~2.2x completion gap.
122
- *
123
- * Mixpanel reports:
124
- * • Funnels → "course enrolled" → "lecture completed" → "quiz completed" → "certificate earned"
125
- * Breakdown: "subscription_status"
126
- * Expected: free ≈ 15% completion, paid ≈ 33% (~2.2x difference)
127
- * • Insights → "certificate earned" → Total per user → Breakdown: "subscription_status"
128
- * Expected: Paid subscribers earn significantly more certificates
129
- *
130
- * 8. PLAYBACK SPEED CORRELATION
131
- * Speed learners (>=2.0x, 3+ lectures): compressed watch_time (0.6x),
132
- * paradoxically higher quiz scores (+8 pts). Thorough learners (<=1.0x):
133
- * extended watch_time (1.4x).
134
- *
135
- * Mixpanel reports:
136
- * • Insights → "lecture completed" → Avg "watch_time_mins" → Breakdown: "speed_learner"
137
- * Expected: speed_learner=true ≈ 0.6x watch time
138
- * • Insights → "quiz completed" → Avg "score_percent" → Breakdown: "speed_learner_effect"
139
- * Expected: speed_learner_effect=true shows +8 points (faster = better)
58
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
59
+ * behavioral cohorts, raw-prop breakdowns, or funnel time-to-convert.
140
60
  *
141
- * ═══════════════════════════════════════════════════════════════════════════════
142
- * ADVANCED ANALYSIS IDEAS
143
- * ═══════════════════════════════════════════════════════════════════════════════
61
+ * 1. STUDENT VS INSTRUCTOR PROFILES (user)
62
+ *
63
+ * PATTERN: Instructor profiles get teaching attributes
64
+ * (courses_created, teaching_experience_years, instructor_rating).
65
+ * Students get learning attributes (learning_goal,
66
+ * study_hours_per_week). Two-sided marketplace at ~89% students,
67
+ * ~11% instructors.
68
+ *
69
+ * HOW TO FIND IT IN MIXPANEL:
70
+ *
71
+ * Report 1: Account Mix
72
+ * - Report type: Insights
73
+ * - Event: any event
74
+ * - Measure: Unique users
75
+ * - Breakdown: "account_type"
76
+ * - Expected: ~89% students, ~11% instructors
77
+ *
78
+ * Report 2: Instructor-Driven Feedback
79
+ * - Report type: Insights
80
+ * - Event: "instructor feedback given"
81
+ * - Measure: Total per user (average)
82
+ * - Breakdown: "account_type"
83
+ * - Expected: instructors dominate feedback volume; students rarely emit
84
+ *
85
+ * REAL-WORLD ANALOGUE: Two-sided learning marketplaces have
86
+ * fundamentally different role personas — teachers create supply,
87
+ * learners consume it.
88
+ *
89
+ * ---------------------------------------------------------------
90
+ * 2. DEADLINE CRAMMING (everything)
91
+ *
92
+ * PATTERN: Assignments submitted on Sun/Mon are rushed — 60% are late
93
+ * (raw is_late prop set true at 60% likelihood) vs ~20% baseline. Quiz
94
+ * scores on Sun/Mon drop by 25 points. No flag — discover via Day of Week
95
+ * breakdown.
96
+ *
97
+ * HOW TO FIND IT IN MIXPANEL:
98
+ *
99
+ * Report 1: Late Submission Rate by Day of Week
100
+ * - Report type: Insights
101
+ * - Event: "assignment submitted"
102
+ * - Measure: count where is_late=true / total
103
+ * - Breakdown: Day of Week
104
+ * - Expected: Sun/Mon ~ 60% late vs other days ~ 20%
105
+ *
106
+ * Report 2: Quiz Score by Day of Week
107
+ * - Report type: Insights
108
+ * - Event: "quiz completed"
109
+ * - Measure: Average of "score_percent"
110
+ * - Breakdown: Day of Week
111
+ * - Expected: Sun/Mon ~ 25, other days ~ 49 (-25 pts)
112
+ *
113
+ * REAL-WORLD ANALOGUE: Procrastination clusters submissions at the
114
+ * deadline weekend and hammers performance.
115
+ *
116
+ * ---------------------------------------------------------------
117
+ * 3. NOTES MAGIC NUMBER (everything, in-funnel)
118
+ *
119
+ * PATTERN: Sweet 5-8 lectures with notes_taken=true → +30% quiz
120
+ * score_percent (cap 100) and 40% chance of bonus cloned certificate.
121
+ * Over 9+ → 35% of certificate-earned events drop (over-noted but
122
+ * stuck in study mode). No flag.
123
+ *
124
+ * HOW TO FIND IT IN MIXPANEL:
125
+ *
126
+ * Report 1: Quiz Score by Notes-Taken Bucket
127
+ * - Cohort A: users with 5-8 "lecture completed" where notes_taken=true
128
+ * - Cohort B: users with 0-4
129
+ * - Event: "quiz completed"
130
+ * - Measure: Average of "score_percent"
131
+ * - Expected: A ~ 1.3x B
132
+ *
133
+ * Report 2: Certificates per User on Heavy Note-Takers
134
+ * - Cohort C: users with >= 9 notes-taken lectures
135
+ * - Cohort A: users with 5-8
136
+ * - Event: "certificate earned"
137
+ * - Measure: Total per user
138
+ * - Expected: C ~ 35% fewer certificates per user vs A
139
+ *
140
+ * REAL-WORLD ANALOGUE: Active note-taking lifts quiz performance, but
141
+ * obsessive note-taking signals "stuck in study mode" without finishing.
142
+ *
143
+ * ---------------------------------------------------------------
144
+ * 4. STUDY GROUP RETENTION (everything)
145
+ *
146
+ * PATTERN: Users who join a study group within 10 days get bonus
147
+ * discussion events. Non-joiners with low quiz scores (<60) churn
148
+ * hard at day 14 — all later events are removed.
149
+ *
150
+ * HOW TO FIND IT IN MIXPANEL:
151
+ *
152
+ * Report 1: D14 Retention by Early Group Join Cohort
153
+ * - Cohort A: users who fired "study group joined" within first 10 days
154
+ * - Cohort B: users with no early study group joined
155
+ * - Compare D14 retention (any event past d14) per cohort
156
+ * - Expected: A ~ 90%+ vs B (with low quiz scores) ~ 30%
157
+ *
158
+ * Report 2: Discussion Volume by Group Cohort
159
+ * - Cohort A vs B (as above)
160
+ * - Event: "discussion posted"
161
+ * - Measure: Total per user
162
+ * - Expected: A posts substantially more
163
+ *
164
+ * REAL-WORLD ANALOGUE: Social learning ties create accountability
165
+ * and dramatically reduce drop-off in cohort-based courses.
166
+ *
167
+ * ---------------------------------------------------------------
168
+ * 5. HINT DEPENDENCY (event)
169
+ *
170
+ * PATTERN: On "practice problem solved", hint_used=true gets difficulty
171
+ * forced to "easy" 60% of the time. hint_used=false gets difficulty forced
172
+ * to "hard" 40% of the time. No flag — discover via difficulty breakdown
173
+ * filtered by hint_used.
174
+ *
175
+ * HOW TO FIND IT IN MIXPANEL:
176
+ *
177
+ * Report 1: Easy Problem Mix for Hint Users
178
+ * - Report type: Insights
179
+ * - Event: "practice problem solved"
180
+ * - Measure: Total
181
+ * - Filter: "hint_used" = true
182
+ * - Breakdown: "difficulty"
183
+ * - Expected: ~60% easy (vs ~33% baseline)
184
+ *
185
+ * Report 2: Hard Problem Mix for Independent Solvers
186
+ * - Report type: Insights
187
+ * - Event: "practice problem solved"
188
+ * - Measure: Total
189
+ * - Filter: "hint_used" = false
190
+ * - Breakdown: "difficulty"
191
+ * - Expected: ~40% hard (vs ~33% baseline)
192
+ *
193
+ * REAL-WORLD ANALOGUE: Learners who lean on hints get nudged toward
194
+ * easier work, while those who push through unaided self-select
195
+ * into harder material.
196
+ *
197
+ * ---------------------------------------------------------------
198
+ * 6. SEMESTER-END SPIKE (everything)
199
+ *
200
+ * PATTERN: Days 75-85 simulate semester crunch. quiz_started, quiz_completed,
201
+ * and assignment_submitted events are duplicated at an 80% rate. No flag —
202
+ * discover via line chart of those event volumes by day.
203
+ *
204
+ * HOW TO FIND IT IN MIXPANEL:
205
+ *
206
+ * Report 1: Assessment Volume Over Time
207
+ * - Report type: Insights
208
+ * - Events: "quiz started" + "quiz completed" + "assignment submitted"
209
+ * - Measure: Total
210
+ * - Line chart by day
211
+ * - Expected: ~2x volume spike on days 75-85
212
+ *
213
+ * REAL-WORLD ANALOGUE: Semester-end deadlines reliably produce a
214
+ * massive last-minute surge in student activity.
215
+ *
216
+ * ---------------------------------------------------------------
217
+ * 7. FREE VS PAID COURSES (funnel-pre + everything)
218
+ *
219
+ * PATTERN: Free users get 0.5x funnel conversion rate; paid
220
+ * subscribers get 1.5x. Free users also lose 55% of their
221
+ * certificates, producing a ~2.2x completion gap.
222
+ *
223
+ * HOW TO FIND IT IN MIXPANEL:
224
+ *
225
+ * Report 1: Course Completion Funnel by Subscription
226
+ * - Report type: Funnels
227
+ * - Steps: "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
228
+ * - Breakdown: "subscription_status"
229
+ * - Expected: free ~ 15% completion, paid ~ 33% (~2.2x gap)
230
+ *
231
+ * Report 2: Certificates Earned per User
232
+ * - Report type: Insights
233
+ * - Event: "certificate earned"
234
+ * - Measure: Total per user (average)
235
+ * - Breakdown: "subscription_status"
236
+ * - Expected: paid subscribers earn substantially more certificates
237
+ *
238
+ * REAL-WORLD ANALOGUE: Paid commitment correlates strongly with
239
+ * follow-through; free learners drop off long before completion.
240
+ *
241
+ * ---------------------------------------------------------------
242
+ * 8. PLAYBACK SPEED CORRELATION (everything)
243
+ *
244
+ * PATTERN: Speed learners (>=2.0x speed on 3+ lectures) get 0.6x
245
+ * watch_time and a paradoxical +8 quiz score boost. Thorough
246
+ * learners (<=1.0x) get 1.4x watch_time.
247
+ *
248
+ * HOW TO FIND IT IN MIXPANEL:
249
+ *
250
+ * Report 1: Watch Time by Playback Speed
251
+ * - Report type: Insights
252
+ * - Event: "lecture completed"
253
+ * - Measure: Average of "watch_time_mins"
254
+ * - Breakdown: "playback_speed"
255
+ * - Expected: speed >= 2.0 ~ 0.6x baseline; speed <= 1.0 ~ 1.4x baseline
256
+ *
257
+ * Report 2: Quiz Score by Speed Learner Cohort
258
+ * - Cohort A: users with 3+ "lecture completed" events at playback_speed >= 2.0
259
+ * - Cohort B: rest
260
+ * - Event: "quiz completed"
261
+ * - Measure: Average of "score_percent"
262
+ * - Expected: A ~ +8 pts vs B
263
+ *
264
+ * REAL-WORLD ANALOGUE: Power users who watch lectures at 2x speed
265
+ * tend to be domain-confident and outperform on assessments
266
+ * despite spending less time.
267
+ *
268
+ * ---------------------------------------------------------------
269
+ * 9. COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
144
270
  *
145
- * CROSS-HOOK PATTERNS:
146
- * - The Ideal Student: notes (H3) + study groups (H4) + no hints (H5) + paid (H7) + speed (H8)
147
- * - Cramming Cascade: deadline crammers (H2) compounded with semester-end spike (H6)?
148
- * - Social Safety Net: does early study group joining (H4) prevent churn for low scorers?
149
- * - Hint-to-Mastery: do hint-dependent (H5) students who join groups (H4) wean off hints?
150
- * - Payment + Notes: are paid subscribers (H7) more likely to take notes (H3)?
271
+ * PATTERN: Annual subscribers complete the course-completion funnel
272
+ * 1.4x faster (factor 0.71); Free users 1.4x slower (factor 1.4).
151
273
  *
152
- * COHORT ANALYSIS:
153
- * - By education level: PhD vs self-taught hook patterns
154
- * - By learning style: visual vs hands-on note-taking rates
155
- * - By platform: mobile vs desktop playback speed preferences
156
- * - By course category: CS vs Arts hint usage
274
+ * HOW TO FIND IT IN MIXPANEL:
157
275
  *
158
- * FUNNEL ANALYSIS:
159
- * - Onboarding by account_type
160
- * - Course completion by subscription, notes, study groups
161
- * - Practice mastery by hint usage, speed, learning style
276
+ * Report 1: Course Completion Median Time-to-Convert by Subscription
277
+ * - Funnels > "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
278
+ * - Measure: Median time to convert
279
+ * - Breakdown: subscription_status
280
+ * - Expected: annual ~ 0.71x baseline; free ~ 1.4x baseline
281
+ *
282
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
283
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
284
+ * show this — funnel-post adjusts gaps within funnel instances, not
285
+ * across the user's full event history.
286
+ *
287
+ * REAL-WORLD ANALOGUE: Paid commitment accelerates throughput.
162
288
  *
163
289
  * ═══════════════════════════════════════════════════════════════════════════════
164
290
  * EXPECTED METRICS SUMMARY
165
291
  * ═══════════════════════════════════════════════════════════════════════════════
166
292
  *
167
293
  * Hook | Metric | Baseline | Hook Effect | Ratio
168
- * ────────────────────────|───────────────────────|──────────|──────────────|──────
169
- * Student vs Instructor | Profile attributes | generic | role-specific| N/A
170
- * Deadline Cramming | Late submission rate | ~20% | ~60% | 3x
171
- * Deadline Cramming | Quiz score (Sun/Mon) | ~65 | ~40 | -25pt
172
- * Notes-Takers Succeed | Quiz score | ~65 | ~85 | +20pt
173
- * Notes-Takers Succeed | Certificate rate | baseline | +40% | 1.4x
174
- * Study Group Retention | D14 retention | ~40% | ~90% | 2.3x
175
- * Study Group Retention | Post-D14 events | 100% | 30% (churn) | 0.3x
176
- * Hint Dependency | Easy problem rate | ~33% | ~60% | 1.8x
177
- * Hint Dependency | Hard problem rate | ~33% | ~40% (no hint)| 1.2x
178
- * Semester-End Spike | Assessment volume | baseline | ~2x | 2x
294
+ * ------------------------|-----------------------|----------|--------------|------
295
+ * Student vs Instructor | Profile attributes | generic | role-specific| n/a
296
+ * Deadline Cramming | Sun/Mon quiz score | 1x | -25 pt | -38%
297
+ * Notes Magic Number | sweet quiz score | 1x | 1.3x | 1.3x
298
+ * Notes Magic Number | over certificates/user| 1x | 0.65x | -35%
299
+ * Study Group Retention | D14 retention | ~ 40% | ~ 90% | 2.3x
300
+ * Hint Dependency | easy problem rate (hint) | 33% | ~ 60% | 1.8x
301
+ * Semester-End Spike | Assessment volume | 1x | ~ 2x | 2x
179
302
  * Free vs Paid | Course completion | 15% | 33% | 2.2x
180
- * Playback Speed | Quiz score (speed) | ~65 | ~73 | +8pt
303
+ * Playback Speed | Quiz score (speed) | ~ 65 | ~ 73 | +8 pt
304
+ * Course Completion T2C | median min by tier | 1x | 0.71x/1.4x | ~ 2x range
181
305
  */
182
306
 
183
307
  // Generate consistent IDs for lookup tables and event properties
@@ -192,8 +316,10 @@ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
192
316
  const config = {
193
317
  token,
194
318
  seed: SEED,
195
- numDays: num_days,
196
- numEvents: num_users * avg_events_per_user,
319
+ datasetStart: "2026-01-01T00:00:00Z",
320
+ datasetEnd: "2026-04-28T23:59:59Z",
321
+ // numDays: num_days,
322
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
197
323
  numUsers: num_users,
198
324
  hasAnonIds: false,
199
325
  hasSessionIds: true,
@@ -208,7 +334,6 @@ const config = {
208
334
  hasCampaigns: false,
209
335
  isAnonymous: false,
210
336
  hasAdSpend: false,
211
- percentUsersBornInDataset: 50,
212
337
  hasAvatar: true,
213
338
  concurrency: 1,
214
339
  writeToDisk: false,
@@ -303,8 +428,6 @@ const config = {
303
428
  "watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
304
429
  "playback_speed": [0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0],
305
430
  "notes_taken": [false, false, true],
306
- "speed_learner": [false],
307
- "thorough_learner": [false],
308
431
  }
309
432
  },
310
433
  {
@@ -315,7 +438,6 @@ const config = {
315
438
  "quiz_id": quizIds,
316
439
  "quiz_type": ["practice", "graded", "final_exam"],
317
440
  "question_count": u.weighNumRange(5, 50, 0.7, 15),
318
- "semester_end_rush": [false],
319
441
  }
320
442
  },
321
443
  {
@@ -327,9 +449,6 @@ const config = {
327
449
  "score_percent": u.weighNumRange(0, 100, 1.2, 50),
328
450
  "time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
329
451
  "attempts": u.weighNumRange(1, 5, 0.5, 3),
330
- "diligent_student": [false],
331
- "speed_learner_effect": [false],
332
- "semester_end_rush": [false],
333
452
  }
334
453
  },
335
454
  {
@@ -341,8 +460,6 @@ const config = {
341
460
  "submission_type": ["text", "code", "file", "project"],
342
461
  "word_count": u.weighNumRange(100, 5000, 0.6, 500),
343
462
  "is_late": [false, false, false, false, true],
344
- "is_deadline_rush": [false],
345
- "semester_end_rush": [false],
346
463
  }
347
464
  },
348
465
  {
@@ -363,7 +480,6 @@ const config = {
363
480
  "course_id": courseIds,
364
481
  "post_type": ["question", "answer", "comment"],
365
482
  "word_count": u.weighNumRange(10, 500, 0.6, 80),
366
- "study_group_member": [false],
367
483
  }
368
484
  },
369
485
  {
@@ -373,7 +489,6 @@ const config = {
373
489
  "course_id": courseIds,
374
490
  "completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
375
491
  "final_grade": u.weighNumRange(60, 100, 1.2, 30),
376
- "diligent_student": [false],
377
492
  }
378
493
  },
379
494
  {
@@ -437,7 +552,6 @@ const config = {
437
552
  "difficulty": ["easy", "medium", "hard"],
438
553
  "time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
439
554
  "hint_used": [false, false, true],
440
- "independent_solver": [false],
441
555
  }
442
556
  },
443
557
  ],
@@ -512,12 +626,7 @@ const config = {
512
626
  * 8. PLAYBACK SPEED CORRELATION: Speed learners paradoxically score higher; thorough learners get extended time
513
627
  */
514
628
  hook: function (record, type, meta) {
515
- const NOW = dayjs();
516
- const DATASET_START = NOW.subtract(num_days, 'days');
517
-
518
- // ═══════════════════════════════════════════════════════════════════
519
- // Hook #1: STUDENT VS INSTRUCTOR PROFILES
520
- // ═══════════════════════════════════════════════════════════════════
629
+ // HOOK 1: STUDENT VS INSTRUCTOR PROFILES (user) — role-based attributes.
521
630
  if (type === "user") {
522
631
  if (record.account_type === "instructor") {
523
632
  record.courses_created = chance.integer({ min: 1, max: 15 });
@@ -529,216 +638,135 @@ const config = {
529
638
  }
530
639
  }
531
640
 
532
- // ═══════════════════════════════════════════════════════════════════
533
- // Hook #2: DEADLINE CRAMMING
534
- // ═══════════════════════════════════════════════════════════════════
535
- if (type === "event") {
536
- if (record.event === "assignment submitted" && record.time) {
537
- const eventDay = dayjs(record.time).day(); // 0 = Sunday, 1 = Monday
538
- if (eventDay === 0 || eventDay === 1) {
539
- record.is_deadline_rush = true;
540
- // 60% chance of being late (only 40% on time)
541
- record.is_late = !chance.bool({ likelihood: 40 });
542
- } else {
543
- record.is_deadline_rush = false;
544
- record.is_late = !chance.bool({ likelihood: 80 });
545
- }
546
- }
547
-
548
- // Quiz score penalty moved to everything hook (after churn removal)
549
- // to avoid selection bias — the penalty was causing more Sun/Mon
550
- // quiz-takers to trigger hasLowQuizScore churn, inflating their avg
551
- }
552
-
553
- // ═══════════════════════════════════════════════════════════════════
554
- // Hook #5: HINT DEPENDENCY
555
- // ═══════════════════════════════════════════════════════════════════
641
+ // HOOK 5 (event): HINT DEPENDENCY — hint users get 60% easy problems;
642
+ // non-hint users get 40% hard problems. Mutates difficulty (raw).
643
+ // HOOK 8 (event): PLAYBACK SPEED — speed learners (>= 2.0x) get
644
+ // watch_time_mins compressed 0.6x; thorough learners (<= 1.0x) get 1.4x.
556
645
  if (type === "event") {
557
646
  if (record.event === "practice problem solved") {
558
- if (record.hint_used === true) {
559
- // Hint users gravitate toward easy problems
560
- if (chance.bool({ likelihood: 60 })) {
561
- record.difficulty = "easy";
562
- }
563
- record.independent_solver = false;
564
- } else if (record.hint_used === false) {
565
- // Independent solvers tackle harder problems
566
- if (chance.bool({ likelihood: 40 })) {
567
- record.difficulty = "hard";
568
- record.independent_solver = true;
569
- } else {
570
- record.independent_solver = false;
571
- }
572
- } else {
573
- record.independent_solver = false;
647
+ if (record.hint_used === true && chance.bool({ likelihood: 60 })) {
648
+ record.difficulty = "easy";
649
+ } else if (record.hint_used === false && chance.bool({ likelihood: 40 })) {
650
+ record.difficulty = "hard";
574
651
  }
575
652
  }
576
- }
577
-
578
- // ═══════════════════════════════════════════════════════════════════
579
- // Hook #6: SEMESTER-END SPIKE (tag in event hook, duplicate in everything hook)
580
- // ═══════════════════════════════════════════════════════════════════
581
- if (type === "event") {
582
- if (record.time) {
583
- const eventTime = dayjs(record.time);
584
- const dayInDataset = eventTime.diff(DATASET_START, 'days', true);
585
-
586
- const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
587
- if (spikableEvents.includes(record.event)) {
588
- if (dayInDataset >= 75 && dayInDataset <= 85) {
589
- record.semester_end_rush = true;
590
- } else {
591
- record.semester_end_rush = false;
592
- }
653
+ if (record.event === "lecture completed") {
654
+ const speed = record.playback_speed;
655
+ if (speed >= 2.0 && record.watch_time_mins !== undefined) {
656
+ record.watch_time_mins = Math.max(3, Math.floor(record.watch_time_mins * 0.6));
657
+ } else if (speed !== undefined && speed <= 1.0 && record.watch_time_mins !== undefined) {
658
+ record.watch_time_mins = Math.min(90, Math.floor(record.watch_time_mins * 1.4));
593
659
  }
594
660
  }
595
661
  }
596
662
 
597
- // ═══════════════════════════════════════════════════════════════════
598
- // Hook #8: PLAYBACK SPEED CORRELATION
599
- // ═══════════════════════════════════════════════════════════════════
600
- if (type === "event") {
601
- if (record.event === "lecture completed") {
602
- const speed = record.playback_speed;
603
-
604
- if (speed >= 2.0) {
605
- record.speed_learner = true;
606
- record.thorough_learner = false;
607
- // Compress watch time for speed learners
608
- if (record.watch_time_mins !== undefined) {
609
- record.watch_time_mins = Math.max(3, Math.floor(record.watch_time_mins * 0.6));
610
- }
611
- } else if (speed !== undefined && speed <= 1.0) {
612
- record.speed_learner = false;
613
- record.thorough_learner = true;
614
- // Extend watch time for thorough learners
615
- if (record.watch_time_mins !== undefined) {
616
- record.watch_time_mins = Math.min(90, Math.floor(record.watch_time_mins * 1.4));
663
+ // HOOK 9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
664
+ // Annual subscribers complete the Course Completion funnel 1.4x
665
+ // faster (factor 0.71); Free users 1.4x slower (factor 1.4).
666
+ if (type === "funnel-post") {
667
+ const segment = meta?.profile?.subscription_status;
668
+ if (Array.isArray(record) && record.length > 1) {
669
+ const factor = (
670
+ segment === "annual" ? 0.71 :
671
+ segment === "free" ? 1.4 :
672
+ 1.0
673
+ );
674
+ if (factor !== 1.0) {
675
+ for (let i = 1; i < record.length; i++) {
676
+ const prev = dayjs(record[i - 1].time);
677
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
678
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
617
679
  }
618
- } else {
619
- record.speed_learner = false;
620
- record.thorough_learner = false;
621
680
  }
622
681
  }
623
682
  }
624
683
 
625
- // ═══════════════════════════════════════════════════════════════════
626
- // Hook #3: NOTES-TAKERS SUCCEED
627
- // Hook #4: STUDY GROUP RETENTION
628
- // Hook #7: FREE VS PAID (funnel-pre handled below)
629
- // ═══════════════════════════════════════════════════════════════════
630
684
  if (type === "everything") {
685
+ const datasetStart = dayjs.unix(meta.datasetStart);
631
686
  const userEvents = record;
632
687
  const profile = meta.profile;
633
688
  const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
634
689
 
635
- // ── Stamp superProps from profile so they stay consistent per user ──
636
690
  if (profile) {
637
691
  userEvents.forEach((event) => {
638
692
  if (profile.Platform !== undefined) event.Platform = profile.Platform;
639
693
  });
640
694
  }
641
695
 
642
- // ---------------------------------------------------------------
643
- // First pass: identify user patterns
644
- // ---------------------------------------------------------------
645
696
  let notesTakenCount = 0;
646
697
  let joinedStudyGroupEarly = false;
647
698
  let hasLowQuizScore = false;
699
+ let speedLectureCount = 0;
648
700
 
649
701
  userEvents.forEach((event) => {
650
702
  const eventTime = dayjs(event.time);
651
703
  const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
652
-
653
- // Hook #3: Count lecture_completed events where notes_taken === true
654
- if (event.event === "lecture completed" && event.notes_taken === true) {
655
- notesTakenCount++;
656
- }
657
-
658
- // Hook #4: Check if user joined a study group within the first 10 days
659
- if (event.event === "study group joined" && daysSinceStart <= 10) {
660
- joinedStudyGroupEarly = true;
661
- }
662
-
663
- // Hook #4: Check for any quiz_completed with score < 60
664
- if (event.event === "quiz completed" && event.score_percent < 60) {
665
- hasLowQuizScore = true;
666
- }
704
+ if (event.event === "lecture completed" && event.notes_taken === true) notesTakenCount++;
705
+ if (event.event === "study group joined" && daysSinceStart <= 10) joinedStudyGroupEarly = true;
706
+ if (event.event === "quiz completed" && event.score_percent < 60) hasLowQuizScore = true;
707
+ if (event.event === "lecture completed" && event.playback_speed >= 2.0) speedLectureCount++;
667
708
  });
668
709
 
669
- // ---------------------------------------------------------------
670
- // Second pass: modify events based on patterns
671
- // ---------------------------------------------------------------
672
-
673
- // Hook #3: NOTES-TAKERS SUCCEED
674
- if (notesTakenCount >= 5) {
675
- userEvents.forEach((event, idx) => {
676
- // Boost quiz scores for diligent note-takers
677
- if (event.event === "quiz completed") {
678
- if (event.score_percent !== undefined) {
679
- event.score_percent = Math.min(100, event.score_percent + 20);
680
- }
681
- event.diligent_student = true;
710
+ // HOOK 3 + HOOK 10: NOTES MAGIC NUMBER (in-funnel, no flags)
711
+ // Sweet 5-8 notes-taken lectures +30% quiz score_percent (cap 100).
712
+ // Over 9+ → drop 35% of certificate-earned events (over-noted but
713
+ // can't synthesize; gets stuck in "study mode").
714
+ if (notesTakenCount >= 5 && notesTakenCount <= 8) {
715
+ userEvents.forEach((event) => {
716
+ if (event.event === "quiz completed" && event.score_percent !== undefined) {
717
+ event.score_percent = Math.min(100, Math.round(event.score_percent * 1.3));
682
718
  }
683
719
  });
684
-
685
- // 40% chance to splice in an extra certificate_earned event
686
720
  if (chance.bool({ likelihood: 40 })) {
687
721
  const lastEvent = userEvents[userEvents.length - 1];
688
722
  const certTemplate = userEvents.find(e => e.event === "certificate earned");
689
723
  if (lastEvent && certTemplate) {
690
- const certEvent = {
724
+ userEvents.push({
691
725
  ...certTemplate,
692
726
  time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
693
727
  user_id: lastEvent.user_id,
694
728
  course_id: chance.pickone(courseIds),
695
729
  completion_time_days: chance.integer({ min: 14, max: 90 }),
696
730
  final_grade: chance.integer({ min: 80, max: 100 }),
697
- diligent_student: true,
698
- };
699
- userEvents.push(certEvent);
731
+ });
700
732
  }
701
733
  }
734
+ } else if (notesTakenCount >= 9) {
735
+ userEvents.forEach(e => {
736
+ if (e.event === "certificate earned" && typeof e.final_grade === "number") {
737
+ e.final_grade = Math.max(50, Math.round(e.final_grade * 0.5));
738
+ }
739
+ });
702
740
  }
703
741
 
704
- // Hook #8 (everything pass): Speed learners (3+ lectures at 2.0x) get higher quiz scores
705
- let speedLectureCount = 0;
706
- userEvents.forEach((event) => {
707
- if (event.event === "lecture completed" && event.speed_learner === true) {
708
- speedLectureCount++;
709
- }
710
- });
711
- const isSpeedLearner = speedLectureCount >= 3;
712
-
713
- if (isSpeedLearner) {
742
+ // HOOK 8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.
743
+ if (speedLectureCount >= 3) {
714
744
  userEvents.forEach((event) => {
715
- if (event.event === "quiz completed") {
716
- if (event.score_percent !== undefined) {
717
- event.score_percent = Math.min(100, event.score_percent + 8);
718
- event.speed_learner_effect = true;
719
- }
745
+ if (event.event === "quiz completed" && event.score_percent !== undefined) {
746
+ event.score_percent = Math.min(100, event.score_percent + 8);
720
747
  }
721
748
  });
722
749
  }
723
750
 
724
- // Hook #6: SEMESTER-END SPIKE - duplicate assessment events in the spike window
751
+ // HOOK 6: SEMESTER-END SPIKE duplicate quiz/assignment events
752
+ // in days 75-85 window. No flag — discover via line chart.
725
753
  const duplicates = [];
754
+ const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
726
755
  userEvents.forEach((event) => {
727
- if (event.semester_end_rush === true && chance.bool({ likelihood: 80 })) {
728
- const dup = JSON.parse(JSON.stringify(event));
729
- dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
730
- dup.semester_end_rush = true;
731
- duplicates.push(dup);
756
+ if (spikableEvents.includes(event.event) && event.time) {
757
+ const dayInDataset = dayjs.utc(event.time).diff(datasetStart, 'days', true);
758
+ if (dayInDataset >= 75 && dayInDataset <= 85 && chance.bool({ likelihood: 80 })) {
759
+ const dup = JSON.parse(JSON.stringify(event));
760
+ dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
761
+ duplicates.push(dup);
762
+ }
732
763
  }
733
764
  });
734
- if (duplicates.length > 0) {
735
- userEvents.push(...duplicates);
736
- }
765
+ if (duplicates.length > 0) userEvents.push(...duplicates);
737
766
 
738
- // Hook #7: FREE VS PAID - reinforce the subscription effect on certificates
767
+ // HOOK 7: FREE VS PAID free users lose 55% of certificates.
739
768
  const subStatus = profile ? profile.subscription_status : "free";
740
769
  if (subStatus === "free") {
741
- // Free users lose 55% of their certificates (simulating lower completion)
742
770
  for (let i = userEvents.length - 1; i >= 0; i--) {
743
771
  if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 55 })) {
744
772
  userEvents.splice(i, 1);
@@ -746,62 +774,51 @@ const config = {
746
774
  }
747
775
  }
748
776
 
749
- // Hook #4: STUDY GROUP RETENTION (runs LAST to ensure churn removal isn't undone by later hooks)
777
+ // HOOK 4: STUDY GROUP RETENTION non-joiners with low scores lose
778
+ // all post-day-14 events. Joiners get extra cloned discussion events.
750
779
  if (!joinedStudyGroupEarly && hasLowQuizScore) {
751
- // Non-joiners with low scores: remove ALL events after day 14 from their first event (hard churn)
752
780
  const churnCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
753
781
  for (let i = userEvents.length - 1; i >= 0; i--) {
754
- const evt = userEvents[i];
755
- if (churnCutoff && dayjs(evt.time).isAfter(churnCutoff)) {
782
+ if (churnCutoff && dayjs(userEvents[i].time).isAfter(churnCutoff)) {
756
783
  userEvents.splice(i, 1);
757
784
  }
758
785
  }
759
786
  } else if (joinedStudyGroupEarly) {
760
- // Study group joiners keep all events and get bonus discussion_posted events
761
787
  const lastEvent = userEvents[userEvents.length - 1];
762
788
  const discussionTemplate = userEvents.find(e => e.event === "discussion posted");
763
789
  if (lastEvent && discussionTemplate && chance.bool({ likelihood: 60 })) {
764
- const bonusDiscussion = {
790
+ userEvents.push({
765
791
  ...discussionTemplate,
766
792
  time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
767
793
  user_id: lastEvent.user_id,
768
794
  course_id: chance.pickone(courseIds),
769
795
  post_type: chance.pickone(["question", "answer", "comment"]),
770
796
  word_count: chance.integer({ min: 20, max: 400 }),
771
- study_group_member: true,
772
- };
773
- userEvents.push(bonusDiscussion);
797
+ });
774
798
  }
775
799
  }
776
800
 
777
- // Hook #2b: DEADLINE CRAMMING (quiz score penalty)
778
- // Applied LAST to avoid selection bias if applied before churn,
779
- // the penalty pushes Sun/Mon quiz-takers below the hasLowQuizScore
780
- // threshold, selectively churning them and inflating the avg.
801
+ // HOOK 2: DEADLINE CRAMMING Sun/Mon assignment_submitted events
802
+ // flip is_late to true 60% of the time and quiz_completed score_percent
803
+ // drops 25 points. Mutates raw is_late + score_percent.
804
+ for (const event of userEvents) {
805
+ if (event.event === "assignment submitted" && event.time) {
806
+ const dow = new Date(event.time).getUTCDay();
807
+ if (dow === 0 || dow === 1) {
808
+ event.is_late = chance.bool({ likelihood: 60 });
809
+ }
810
+ }
811
+ }
781
812
  userEvents.forEach((event) => {
782
813
  if (event.event === "quiz completed" && event.time) {
783
- const eventDay = dayjs(event.time).day();
784
- if (eventDay === 0 || eventDay === 1) {
785
- if (event.score_percent !== undefined) {
786
- event.score_percent = Math.max(0, event.score_percent - 25);
787
- }
814
+ const dow = new Date(event.time).getUTCDay();
815
+ if ((dow === 0 || dow === 1) && event.score_percent !== undefined) {
816
+ event.score_percent = Math.max(0, event.score_percent - 25);
788
817
  }
789
818
  }
790
819
  });
791
820
  }
792
821
 
793
- // ═══════════════════════════════════════════════════════════════════
794
- // Hook #7: FREE VS PAID COURSES (funnel-pre)
795
- // ═══════════════════════════════════════════════════════════════════
796
- if (type === "funnel-pre") {
797
- // Hook #7: FREE VS PAID — prop tagging only
798
- // conversionRate manipulation removed; the everything hook handles
799
- // the completion gap by dropping 55% of free-user certificates
800
- if (meta && meta.profile && meta.funnel) {
801
- // no-op: kept for future prop-setting if needed
802
- }
803
- }
804
-
805
822
  return record;
806
823
  }
807
824
  };