@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
@@ -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,7 +316,9 @@ 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,
319
+ datasetStart: "2026-01-01T00:00:00Z",
320
+ datasetEnd: "2026-04-28T23:59:59Z",
321
+ // numDays: num_days,
196
322
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
197
323
  numUsers: num_users,
198
324
  hasAnonIds: false,
@@ -302,8 +428,6 @@ const config = {
302
428
  "watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
303
429
  "playback_speed": [0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0],
304
430
  "notes_taken": [false, false, true],
305
- "speed_learner": [false],
306
- "thorough_learner": [false],
307
431
  }
308
432
  },
309
433
  {
@@ -314,7 +438,6 @@ const config = {
314
438
  "quiz_id": quizIds,
315
439
  "quiz_type": ["practice", "graded", "final_exam"],
316
440
  "question_count": u.weighNumRange(5, 50, 0.7, 15),
317
- "semester_end_rush": [false],
318
441
  }
319
442
  },
320
443
  {
@@ -326,9 +449,6 @@ const config = {
326
449
  "score_percent": u.weighNumRange(0, 100, 1.2, 50),
327
450
  "time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
328
451
  "attempts": u.weighNumRange(1, 5, 0.5, 3),
329
- "diligent_student": [false],
330
- "speed_learner_effect": [false],
331
- "semester_end_rush": [false],
332
452
  }
333
453
  },
334
454
  {
@@ -340,8 +460,6 @@ const config = {
340
460
  "submission_type": ["text", "code", "file", "project"],
341
461
  "word_count": u.weighNumRange(100, 5000, 0.6, 500),
342
462
  "is_late": [false, false, false, false, true],
343
- "is_deadline_rush": [false],
344
- "semester_end_rush": [false],
345
463
  }
346
464
  },
347
465
  {
@@ -362,7 +480,6 @@ const config = {
362
480
  "course_id": courseIds,
363
481
  "post_type": ["question", "answer", "comment"],
364
482
  "word_count": u.weighNumRange(10, 500, 0.6, 80),
365
- "study_group_member": [false],
366
483
  }
367
484
  },
368
485
  {
@@ -372,7 +489,6 @@ const config = {
372
489
  "course_id": courseIds,
373
490
  "completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
374
491
  "final_grade": u.weighNumRange(60, 100, 1.2, 30),
375
- "diligent_student": [false],
376
492
  }
377
493
  },
378
494
  {
@@ -436,7 +552,6 @@ const config = {
436
552
  "difficulty": ["easy", "medium", "hard"],
437
553
  "time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
438
554
  "hint_used": [false, false, true],
439
- "independent_solver": [false],
440
555
  }
441
556
  },
442
557
  ],
@@ -511,12 +626,7 @@ const config = {
511
626
  * 8. PLAYBACK SPEED CORRELATION: Speed learners paradoxically score higher; thorough learners get extended time
512
627
  */
513
628
  hook: function (record, type, meta) {
514
- const NOW = dayjs.utc();
515
- const DATASET_START = NOW.subtract(num_days, 'days');
516
-
517
- // ═══════════════════════════════════════════════════════════════════
518
- // Hook #1: STUDENT VS INSTRUCTOR PROFILES
519
- // ═══════════════════════════════════════════════════════════════════
629
+ // HOOK 1: STUDENT VS INSTRUCTOR PROFILES (user) — role-based attributes.
520
630
  if (type === "user") {
521
631
  if (record.account_type === "instructor") {
522
632
  record.courses_created = chance.integer({ min: 1, max: 15 });
@@ -528,216 +638,135 @@ const config = {
528
638
  }
529
639
  }
530
640
 
531
- // ═══════════════════════════════════════════════════════════════════
532
- // Hook #2: DEADLINE CRAMMING
533
- // ═══════════════════════════════════════════════════════════════════
534
- if (type === "event") {
535
- if (record.event === "assignment submitted" && record.time) {
536
- const eventDay = dayjs(record.time).day(); // 0 = Sunday, 1 = Monday
537
- if (eventDay === 0 || eventDay === 1) {
538
- record.is_deadline_rush = true;
539
- // 60% chance of being late (only 40% on time)
540
- record.is_late = !chance.bool({ likelihood: 40 });
541
- } else {
542
- record.is_deadline_rush = false;
543
- record.is_late = !chance.bool({ likelihood: 80 });
544
- }
545
- }
546
-
547
- // Quiz score penalty moved to everything hook (after churn removal)
548
- // to avoid selection bias — the penalty was causing more Sun/Mon
549
- // quiz-takers to trigger hasLowQuizScore churn, inflating their avg
550
- }
551
-
552
- // ═══════════════════════════════════════════════════════════════════
553
- // Hook #5: HINT DEPENDENCY
554
- // ═══════════════════════════════════════════════════════════════════
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.
555
645
  if (type === "event") {
556
646
  if (record.event === "practice problem solved") {
557
- if (record.hint_used === true) {
558
- // Hint users gravitate toward easy problems
559
- if (chance.bool({ likelihood: 60 })) {
560
- record.difficulty = "easy";
561
- }
562
- record.independent_solver = false;
563
- } else if (record.hint_used === false) {
564
- // Independent solvers tackle harder problems
565
- if (chance.bool({ likelihood: 40 })) {
566
- record.difficulty = "hard";
567
- record.independent_solver = true;
568
- } else {
569
- record.independent_solver = false;
570
- }
571
- } else {
572
- 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";
573
651
  }
574
652
  }
575
- }
576
-
577
- // ═══════════════════════════════════════════════════════════════════
578
- // Hook #6: SEMESTER-END SPIKE (tag in event hook, duplicate in everything hook)
579
- // ═══════════════════════════════════════════════════════════════════
580
- if (type === "event") {
581
- if (record.time) {
582
- const eventTime = dayjs.utc(record.time);
583
- const dayInDataset = eventTime.diff(DATASET_START, 'days', true);
584
-
585
- const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
586
- if (spikableEvents.includes(record.event)) {
587
- if (dayInDataset >= 75 && dayInDataset <= 85) {
588
- record.semester_end_rush = true;
589
- } else {
590
- record.semester_end_rush = false;
591
- }
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));
592
659
  }
593
660
  }
594
661
  }
595
662
 
596
- // ═══════════════════════════════════════════════════════════════════
597
- // Hook #8: PLAYBACK SPEED CORRELATION
598
- // ═══════════════════════════════════════════════════════════════════
599
- if (type === "event") {
600
- if (record.event === "lecture completed") {
601
- const speed = record.playback_speed;
602
-
603
- if (speed >= 2.0) {
604
- record.speed_learner = true;
605
- record.thorough_learner = false;
606
- // Compress watch time for speed learners
607
- if (record.watch_time_mins !== undefined) {
608
- record.watch_time_mins = Math.max(3, Math.floor(record.watch_time_mins * 0.6));
609
- }
610
- } else if (speed !== undefined && speed <= 1.0) {
611
- record.speed_learner = false;
612
- record.thorough_learner = true;
613
- // Extend watch time for thorough learners
614
- if (record.watch_time_mins !== undefined) {
615
- 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();
616
679
  }
617
- } else {
618
- record.speed_learner = false;
619
- record.thorough_learner = false;
620
680
  }
621
681
  }
622
682
  }
623
683
 
624
- // ═══════════════════════════════════════════════════════════════════
625
- // Hook #3: NOTES-TAKERS SUCCEED
626
- // Hook #4: STUDY GROUP RETENTION
627
- // Hook #7: FREE VS PAID (funnel-pre handled below)
628
- // ═══════════════════════════════════════════════════════════════════
629
684
  if (type === "everything") {
685
+ const datasetStart = dayjs.unix(meta.datasetStart);
630
686
  const userEvents = record;
631
687
  const profile = meta.profile;
632
688
  const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
633
689
 
634
- // ── Stamp superProps from profile so they stay consistent per user ──
635
690
  if (profile) {
636
691
  userEvents.forEach((event) => {
637
692
  if (profile.Platform !== undefined) event.Platform = profile.Platform;
638
693
  });
639
694
  }
640
695
 
641
- // ---------------------------------------------------------------
642
- // First pass: identify user patterns
643
- // ---------------------------------------------------------------
644
696
  let notesTakenCount = 0;
645
697
  let joinedStudyGroupEarly = false;
646
698
  let hasLowQuizScore = false;
699
+ let speedLectureCount = 0;
647
700
 
648
701
  userEvents.forEach((event) => {
649
702
  const eventTime = dayjs(event.time);
650
703
  const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
651
-
652
- // Hook #3: Count lecture_completed events where notes_taken === true
653
- if (event.event === "lecture completed" && event.notes_taken === true) {
654
- notesTakenCount++;
655
- }
656
-
657
- // Hook #4: Check if user joined a study group within the first 10 days
658
- if (event.event === "study group joined" && daysSinceStart <= 10) {
659
- joinedStudyGroupEarly = true;
660
- }
661
-
662
- // Hook #4: Check for any quiz_completed with score < 60
663
- if (event.event === "quiz completed" && event.score_percent < 60) {
664
- hasLowQuizScore = true;
665
- }
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++;
666
708
  });
667
709
 
668
- // ---------------------------------------------------------------
669
- // Second pass: modify events based on patterns
670
- // ---------------------------------------------------------------
671
-
672
- // Hook #3: NOTES-TAKERS SUCCEED
673
- if (notesTakenCount >= 5) {
674
- userEvents.forEach((event, idx) => {
675
- // Boost quiz scores for diligent note-takers
676
- if (event.event === "quiz completed") {
677
- if (event.score_percent !== undefined) {
678
- event.score_percent = Math.min(100, event.score_percent + 20);
679
- }
680
- 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));
681
718
  }
682
719
  });
683
-
684
- // 40% chance to splice in an extra certificate_earned event
685
720
  if (chance.bool({ likelihood: 40 })) {
686
721
  const lastEvent = userEvents[userEvents.length - 1];
687
722
  const certTemplate = userEvents.find(e => e.event === "certificate earned");
688
723
  if (lastEvent && certTemplate) {
689
- const certEvent = {
724
+ userEvents.push({
690
725
  ...certTemplate,
691
726
  time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
692
727
  user_id: lastEvent.user_id,
693
728
  course_id: chance.pickone(courseIds),
694
729
  completion_time_days: chance.integer({ min: 14, max: 90 }),
695
730
  final_grade: chance.integer({ min: 80, max: 100 }),
696
- diligent_student: true,
697
- };
698
- userEvents.push(certEvent);
731
+ });
699
732
  }
700
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
+ });
701
740
  }
702
741
 
703
- // Hook #8 (everything pass): Speed learners (3+ lectures at 2.0x) get higher quiz scores
704
- let speedLectureCount = 0;
705
- userEvents.forEach((event) => {
706
- if (event.event === "lecture completed" && event.speed_learner === true) {
707
- speedLectureCount++;
708
- }
709
- });
710
- const isSpeedLearner = speedLectureCount >= 3;
711
-
712
- if (isSpeedLearner) {
742
+ // HOOK 8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.
743
+ if (speedLectureCount >= 3) {
713
744
  userEvents.forEach((event) => {
714
- if (event.event === "quiz completed") {
715
- if (event.score_percent !== undefined) {
716
- event.score_percent = Math.min(100, event.score_percent + 8);
717
- event.speed_learner_effect = true;
718
- }
745
+ if (event.event === "quiz completed" && event.score_percent !== undefined) {
746
+ event.score_percent = Math.min(100, event.score_percent + 8);
719
747
  }
720
748
  });
721
749
  }
722
750
 
723
- // 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.
724
753
  const duplicates = [];
754
+ const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
725
755
  userEvents.forEach((event) => {
726
- if (event.semester_end_rush === true && chance.bool({ likelihood: 80 })) {
727
- const dup = JSON.parse(JSON.stringify(event));
728
- dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
729
- dup.semester_end_rush = true;
730
- 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
+ }
731
763
  }
732
764
  });
733
- if (duplicates.length > 0) {
734
- userEvents.push(...duplicates);
735
- }
765
+ if (duplicates.length > 0) userEvents.push(...duplicates);
736
766
 
737
- // Hook #7: FREE VS PAID - reinforce the subscription effect on certificates
767
+ // HOOK 7: FREE VS PAID free users lose 55% of certificates.
738
768
  const subStatus = profile ? profile.subscription_status : "free";
739
769
  if (subStatus === "free") {
740
- // Free users lose 55% of their certificates (simulating lower completion)
741
770
  for (let i = userEvents.length - 1; i >= 0; i--) {
742
771
  if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 55 })) {
743
772
  userEvents.splice(i, 1);
@@ -745,62 +774,51 @@ const config = {
745
774
  }
746
775
  }
747
776
 
748
- // 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.
749
779
  if (!joinedStudyGroupEarly && hasLowQuizScore) {
750
- // Non-joiners with low scores: remove ALL events after day 14 from their first event (hard churn)
751
780
  const churnCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
752
781
  for (let i = userEvents.length - 1; i >= 0; i--) {
753
- const evt = userEvents[i];
754
- if (churnCutoff && dayjs(evt.time).isAfter(churnCutoff)) {
782
+ if (churnCutoff && dayjs(userEvents[i].time).isAfter(churnCutoff)) {
755
783
  userEvents.splice(i, 1);
756
784
  }
757
785
  }
758
786
  } else if (joinedStudyGroupEarly) {
759
- // Study group joiners keep all events and get bonus discussion_posted events
760
787
  const lastEvent = userEvents[userEvents.length - 1];
761
788
  const discussionTemplate = userEvents.find(e => e.event === "discussion posted");
762
789
  if (lastEvent && discussionTemplate && chance.bool({ likelihood: 60 })) {
763
- const bonusDiscussion = {
790
+ userEvents.push({
764
791
  ...discussionTemplate,
765
792
  time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
766
793
  user_id: lastEvent.user_id,
767
794
  course_id: chance.pickone(courseIds),
768
795
  post_type: chance.pickone(["question", "answer", "comment"]),
769
796
  word_count: chance.integer({ min: 20, max: 400 }),
770
- study_group_member: true,
771
- };
772
- userEvents.push(bonusDiscussion);
797
+ });
773
798
  }
774
799
  }
775
800
 
776
- // Hook #2b: DEADLINE CRAMMING (quiz score penalty)
777
- // Applied LAST to avoid selection bias if applied before churn,
778
- // the penalty pushes Sun/Mon quiz-takers below the hasLowQuizScore
779
- // 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
+ }
780
812
  userEvents.forEach((event) => {
781
813
  if (event.event === "quiz completed" && event.time) {
782
- const eventDay = dayjs(event.time).day();
783
- if (eventDay === 0 || eventDay === 1) {
784
- if (event.score_percent !== undefined) {
785
- event.score_percent = Math.max(0, event.score_percent - 25);
786
- }
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);
787
817
  }
788
818
  }
789
819
  });
790
820
  }
791
821
 
792
- // ═══════════════════════════════════════════════════════════════════
793
- // Hook #7: FREE VS PAID COURSES (funnel-pre)
794
- // ═══════════════════════════════════════════════════════════════════
795
- if (type === "funnel-pre") {
796
- // Hook #7: FREE VS PAID — prop tagging only
797
- // conversionRate manipulation removed; the everything hook handles
798
- // the completion gap by dropping 55% of free-user certificates
799
- if (meta && meta.profile && meta.funnel) {
800
- // no-op: kept for future prop-setting if needed
801
- }
802
- }
803
-
804
822
  return record;
805
823
  }
806
824
  };