@ak--47/dungeon-master 1.5.4 → 1.6.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 (146) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -12
  2. package/.claude/skills/create-dungeon/SKILL.md +65 -5
  3. package/.claude/skills/create-project/SKILL.md +1 -1
  4. package/.claude/skills/create-project/provision.mjs +57 -10
  5. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  6. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  7. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  8. package/.claude/skills/write-hooks/SKILL.md +156 -18
  9. package/CHANGELOG.md +183 -0
  10. package/HOOKS.md +566 -112
  11. package/README.md +28 -9
  12. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  13. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  14. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  15. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  16. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  17. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  18. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  19. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  20. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  21. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  22. package/dungeons/technical/stories-verify.js +152 -0
  23. package/dungeons/vertical/README.md +154 -0
  24. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  25. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  26. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  27. package/dungeons/vertical/community/community.js +1589 -0
  28. package/dungeons/vertical/community/community.sql +216 -0
  29. package/dungeons/vertical/community/community.verify.mjs +72 -0
  30. package/dungeons/vertical/crypto/crypto.js +1483 -0
  31. package/dungeons/vertical/crypto/crypto.sql +194 -0
  32. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  33. package/dungeons/vertical/dating/dating.js +1540 -0
  34. package/dungeons/vertical/dating/dating.sql +288 -0
  35. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  36. package/dungeons/vertical/devtools/devtools.js +1524 -0
  37. package/dungeons/vertical/devtools/devtools.sql +161 -0
  38. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  39. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  40. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  41. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  42. package/dungeons/vertical/education/education.js +1763 -0
  43. package/dungeons/vertical/education/education.sql +243 -0
  44. package/dungeons/vertical/education/education.verify.mjs +75 -0
  45. package/dungeons/vertical/fintech/fintech.js +1410 -0
  46. package/dungeons/vertical/fintech/fintech.sql +175 -0
  47. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  48. package/dungeons/vertical/fitness/fitness.js +1495 -0
  49. package/dungeons/vertical/fitness/fitness.sql +206 -0
  50. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  51. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  52. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  53. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  54. package/dungeons/vertical/gaming/gaming.js +1741 -0
  55. package/dungeons/vertical/gaming/gaming.sql +249 -0
  56. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  57. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  58. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  59. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  60. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  61. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  62. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  63. package/dungeons/vertical/logistics/logistics.js +1541 -0
  64. package/dungeons/vertical/logistics/logistics.sql +205 -0
  65. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  66. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  67. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  68. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  69. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  70. package/dungeons/vertical/media/media.sql +173 -0
  71. package/dungeons/vertical/media/media.verify.mjs +74 -0
  72. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  73. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  74. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  75. package/dungeons/vertical/sass/sass.js +1733 -0
  76. package/dungeons/vertical/sass/sass.sql +173 -0
  77. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  78. package/dungeons/vertical/social/social.js +1456 -0
  79. package/dungeons/vertical/social/social.sql +160 -0
  80. package/dungeons/vertical/social/social.verify.mjs +76 -0
  81. package/dungeons/vertical/streaming/streaming.js +677 -0
  82. package/dungeons/vertical/streaming/streaming.sql +101 -0
  83. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  84. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  85. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  86. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  87. package/dungeons/vertical/travel/travel.js +1603 -0
  88. package/dungeons/vertical/travel/travel.sql +236 -0
  89. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  90. package/index.js +2 -2
  91. package/lib/core/config-validator.js +37 -2
  92. package/lib/core/context.js +4 -2
  93. package/lib/core/dungeon-loader.js +20 -15
  94. package/lib/core/dungeon-to-json.js +22 -4
  95. package/lib/core/storage.js +3 -3
  96. package/lib/generators/funnels.js +36 -9
  97. package/lib/hook-helpers/cohort.js +53 -0
  98. package/lib/hook-helpers/index.js +8 -0
  99. package/lib/hook-helpers/mutate.js +1 -1
  100. package/lib/hook-helpers/shape.js +319 -0
  101. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  102. package/lib/hook-patterns/attributed-by-source.js +60 -58
  103. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  104. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  105. package/lib/hook-patterns/index.js +1 -1
  106. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  107. package/lib/orchestrators/user-loop.js +41 -0
  108. package/lib/templates/story-spec.schema.json +175 -0
  109. package/lib/utils/json-evaluator.js +1 -1
  110. package/lib/utils/utils.js +5 -4
  111. package/lib/verify/coerce.js +186 -0
  112. package/lib/verify/counting.js +270 -32
  113. package/lib/verify/emulate-breakdown.js +1285 -203
  114. package/lib/verify/first-time.js +89 -0
  115. package/lib/verify/flows.js +679 -0
  116. package/lib/verify/formula.js +259 -0
  117. package/lib/verify/funnel-engine.js +697 -113
  118. package/lib/verify/index.js +32 -4
  119. package/lib/verify/sessionize.js +275 -0
  120. package/lib/verify/story-runner.js +483 -0
  121. package/lib/verify/verify-dungeon.js +59 -37
  122. package/package.json +2 -1
  123. package/scripts/extract-dungeon-schema.mjs +2 -2
  124. package/scripts/run-many.mjs +2 -2
  125. package/scripts/verify-runner.mjs +2 -2
  126. package/scripts/verify-stories.mjs +244 -0
  127. package/types.d.ts +296 -30
  128. package/dungeons/vertical/ai-platform.js +0 -818
  129. package/dungeons/vertical/community.js +0 -966
  130. package/dungeons/vertical/crypto.js +0 -895
  131. package/dungeons/vertical/dating.js +0 -830
  132. package/dungeons/vertical/devtools.js +0 -1050
  133. package/dungeons/vertical/ecommerce.js +0 -811
  134. package/dungeons/vertical/education.js +0 -964
  135. package/dungeons/vertical/fintech.js +0 -946
  136. package/dungeons/vertical/fitness.js +0 -935
  137. package/dungeons/vertical/food-delivery.js +0 -934
  138. package/dungeons/vertical/gaming.js +0 -1120
  139. package/dungeons/vertical/healthcare.js +0 -945
  140. package/dungeons/vertical/insurance-application.js +0 -1039
  141. package/dungeons/vertical/logistics.js +0 -986
  142. package/dungeons/vertical/marketplace.js +0 -1019
  143. package/dungeons/vertical/real-estate.js +0 -872
  144. package/dungeons/vertical/sass.js +0 -995
  145. package/dungeons/vertical/social.js +0 -904
  146. package/dungeons/vertical/travel.js +0 -930
@@ -1,964 +0,0 @@
1
- // ── IMPORTS ──
2
- import dayjs from "dayjs";
3
- import utc from "dayjs/plugin/utc.js";
4
- dayjs.extend(utc);
5
- import "dotenv/config";
6
- import * as u from "../../lib/utils/utils.js";
7
- import * as v from "ak-tools";
8
- /** @typedef {import("../../types").Dungeon} Config */
9
-
10
- // ── OVERVIEW ──
11
- /*
12
- * NAME: LearnPath
13
- * APP: Online learning platform modeled after Coursera, Khan Academy,
14
- * and Udemy. Self-paced and cohort-based courses with quizzes,
15
- * assignments, certificates, and a social study layer. Two-sided
16
- * marketplace: ~89% students, ~11% instructors.
17
- * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
18
- * CORE LOOP: account registered → course enrolled → lecture started/completed → quiz → certificate
19
- *
20
- * EVENTS (17):
21
- * lecture started (18) > lecture completed (14) > practice problem solved (12)
22
- * > quiz started (10) > resource downloaded (9) > course enrolled (8)
23
- * > quiz completed (8) > discussion posted (7) > assignment submitted (6)
24
- * > assignment graded (5) > help requested (4) > study group joined (4)
25
- * > instructor feedback given (3) > course reviewed (3) > certificate earned (2)
26
- * > subscription purchased (2) > account registered (1)
27
- *
28
- * FUNNELS (7):
29
- * - Onboarding: account registered → course enrolled → lecture started (75%)
30
- * - Learning Loop: lecture started → lecture completed → practice problem solved (70%, reentry)
31
- * - Assessment: quiz started → quiz completed → assignment submitted (55%, reentry)
32
- * - Course Completion: course enrolled → lecture completed → quiz completed → certificate earned (30%)
33
- * - Social Learning: discussion posted → study group joined → resource downloaded (50%, AI Study Buddy A/B)
34
- * - Instructor Interaction: assignment submitted → assignment graded → instructor feedback given (45%)
35
- * - Support/Monetization: help requested → subscription purchased → course reviewed (35%)
36
- *
37
- * USER PROPS: account_type, subscription_status, learning_style, education_level, timezone, courses_created, teaching_experience_years, instructor_rating, learning_goal, study_hours_per_week, Platform
38
- * SUPER PROPS: Platform
39
- * SCD PROPS: enrollment_status (enrolled/active/completed/dropped, monthly fuzzy, max 6), course_status (draft/published/archived/deprecated, monthly fixed, max 6, type: course_id)
40
- * GROUPS: course_id (150 courses), group_id (300 study groups)
41
- */
42
-
43
- // ── HOOK STORIES ──
44
- /*
45
- * ---------------------------------------------------------------
46
- * 1. STUDENT VS INSTRUCTOR PROFILES (user)
47
- * ---------------------------------------------------------------
48
- *
49
- * PATTERN: Instructor profiles get teaching attributes
50
- * (courses_created, teaching_experience_years, instructor_rating).
51
- * Students get learning attributes (learning_goal,
52
- * study_hours_per_week). Two-sided marketplace at ~89% students,
53
- * ~11% instructors.
54
- *
55
- * HOW TO FIND IT IN MIXPANEL:
56
- *
57
- * Report 1: Account Mix
58
- * - Report type: Insights
59
- * - Event: any event
60
- * - Measure: Unique users
61
- * - Breakdown: "account_type"
62
- * - Expected: ~89% students, ~11% instructors
63
- *
64
- * Report 2: Instructor-Driven Feedback
65
- * - Report type: Insights
66
- * - Event: "instructor feedback given"
67
- * - Measure: Total per user (average)
68
- * - Breakdown: "account_type"
69
- * - Expected: instructors dominate feedback volume; students rarely emit
70
- *
71
- * REAL-WORLD ANALOGUE: Two-sided learning marketplaces have
72
- * fundamentally different role personas — teachers create supply,
73
- * learners consume it.
74
- *
75
- * ---------------------------------------------------------------
76
- * 2. DEADLINE CRAMMING (everything)
77
- * ---------------------------------------------------------------
78
- *
79
- * PATTERN: Assignments submitted on Sun/Mon are rushed — 60% are late
80
- * (raw is_late prop set true at 60% likelihood) vs ~20% baseline. Quiz
81
- * scores on Sun/Mon drop by 25 points. No flag — discover via Day of Week
82
- * breakdown.
83
- *
84
- * HOW TO FIND IT IN MIXPANEL:
85
- *
86
- * Report 1: Late Submission Rate by Day of Week
87
- * - Report type: Insights
88
- * - Event: "assignment submitted"
89
- * - Measure: count where is_late=true / total
90
- * - Breakdown: Day of Week
91
- * - Expected: Sun/Mon ~ 60% late vs other days ~ 20%
92
- *
93
- * Report 2: Quiz Score by Day of Week
94
- * - Report type: Insights
95
- * - Event: "quiz completed"
96
- * - Measure: Average of "score_percent"
97
- * - Breakdown: Day of Week
98
- * - Expected: Sun/Mon ~ 25, other days ~ 49 (-25 pts)
99
- *
100
- * REAL-WORLD ANALOGUE: Procrastination clusters submissions at the
101
- * deadline weekend and hammers performance.
102
- *
103
- * ---------------------------------------------------------------
104
- * 3. NOTES MAGIC NUMBER (everything, in-funnel)
105
- * ---------------------------------------------------------------
106
- *
107
- * PATTERN: Sweet 5-8 lectures with notes_taken=true → +30% quiz
108
- * score_percent (cap 100) and 40% chance of bonus cloned certificate.
109
- * Over 9+ → 35% of certificate-earned events drop (over-noted but
110
- * stuck in study mode). No flag.
111
- *
112
- * HOW TO FIND IT IN MIXPANEL:
113
- *
114
- * Report 1: Quiz Score by Notes-Taken Bucket
115
- * - Cohort A: users with 5-8 "lecture completed" where notes_taken=true
116
- * - Cohort B: users with 0-4
117
- * - Event: "quiz completed"
118
- * - Measure: Average of "score_percent"
119
- * - Expected: A ~ 1.3x B
120
- *
121
- * Report 2: Certificates per User on Heavy Note-Takers
122
- * - Cohort C: users with >= 9 notes-taken lectures
123
- * - Cohort A: users with 5-8
124
- * - Event: "certificate earned"
125
- * - Measure: Total per user
126
- * - Expected: C ~ 35% fewer certificates per user vs A
127
- *
128
- * REAL-WORLD ANALOGUE: Active note-taking lifts quiz performance, but
129
- * obsessive note-taking signals "stuck in study mode" without finishing.
130
- *
131
- * ---------------------------------------------------------------
132
- * 4. STUDY GROUP RETENTION (everything)
133
- * ---------------------------------------------------------------
134
- *
135
- * PATTERN: Users who join a study group within 10 days get bonus
136
- * discussion events. Non-joiners with low quiz scores (<60) churn
137
- * hard at day 14 — all later events are removed.
138
- *
139
- * HOW TO FIND IT IN MIXPANEL:
140
- *
141
- * Report 1: D14 Retention by Early Group Join Cohort
142
- * - Cohort A: users who fired "study group joined" within first 10 days
143
- * - Cohort B: users with no early study group joined
144
- * - Compare D14 retention (any event past d14) per cohort
145
- * - Expected: A ~ 90%+ vs B (with low quiz scores) ~ 30%
146
- *
147
- * Report 2: Discussion Volume by Group Cohort
148
- * - Cohort A vs B (as above)
149
- * - Event: "discussion posted"
150
- * - Measure: Total per user
151
- * - Expected: A posts substantially more
152
- *
153
- * REAL-WORLD ANALOGUE: Social learning ties create accountability
154
- * and dramatically reduce drop-off in cohort-based courses.
155
- *
156
- * ---------------------------------------------------------------
157
- * 5. HINT DEPENDENCY (event)
158
- * ---------------------------------------------------------------
159
- *
160
- * PATTERN: On "practice problem solved", hint_used=true gets difficulty
161
- * forced to "easy" 60% of the time. hint_used=false gets difficulty forced
162
- * to "hard" 40% of the time. No flag — discover via difficulty breakdown
163
- * filtered by hint_used.
164
- *
165
- * HOW TO FIND IT IN MIXPANEL:
166
- *
167
- * Report 1: Easy Problem Mix for Hint Users
168
- * - Report type: Insights
169
- * - Event: "practice problem solved"
170
- * - Measure: Total
171
- * - Filter: "hint_used" = true
172
- * - Breakdown: "difficulty"
173
- * - Expected: ~60% easy (vs ~33% baseline)
174
- *
175
- * Report 2: Hard Problem Mix for Independent Solvers
176
- * - Report type: Insights
177
- * - Event: "practice problem solved"
178
- * - Measure: Total
179
- * - Filter: "hint_used" = false
180
- * - Breakdown: "difficulty"
181
- * - Expected: ~40% hard (vs ~33% baseline)
182
- *
183
- * REAL-WORLD ANALOGUE: Learners who lean on hints get nudged toward
184
- * easier work, while those who push through unaided self-select
185
- * into harder material.
186
- *
187
- * ---------------------------------------------------------------
188
- * 6. SEMESTER-END SPIKE (everything)
189
- * ---------------------------------------------------------------
190
- *
191
- * PATTERN: Days 75-85 simulate semester crunch. quiz_started, quiz_completed,
192
- * and assignment_submitted events are duplicated at an 80% rate. No flag —
193
- * discover via line chart of those event volumes by day.
194
- *
195
- * HOW TO FIND IT IN MIXPANEL:
196
- *
197
- * Report 1: Assessment Volume Over Time
198
- * - Report type: Insights
199
- * - Events: "quiz started" + "quiz completed" + "assignment submitted"
200
- * - Measure: Total
201
- * - Line chart by day
202
- * - Expected: ~2x volume spike on days 75-85
203
- *
204
- * REAL-WORLD ANALOGUE: Semester-end deadlines reliably produce a
205
- * massive last-minute surge in student activity.
206
- *
207
- * ---------------------------------------------------------------
208
- * 7. FREE VS PAID COURSES (funnel-pre + everything)
209
- * ---------------------------------------------------------------
210
- *
211
- * PATTERN: Free users get 0.5x funnel conversion rate on the cert funnel only; paid
212
- * subscribers get 1.5x. Free users also lose 55% of their
213
- * certificates, producing a ~2.2x completion gap.
214
- *
215
- * HOW TO FIND IT IN MIXPANEL:
216
- *
217
- * Report 1: Course Completion Funnel by Subscription
218
- * - Report type: Funnels
219
- * - Steps: "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
220
- * - Breakdown: "subscription_status"
221
- * - Expected: free ~ 15% completion, paid ~ 33% (~2.2x gap)
222
- *
223
- * Report 2: Certificates Earned per User
224
- * - Report type: Insights
225
- * - Event: "certificate earned"
226
- * - Measure: Total per user (average)
227
- * - Breakdown: "subscription_status"
228
- * - Expected: paid subscribers earn substantially more certificates
229
- *
230
- * REAL-WORLD ANALOGUE: Paid commitment correlates strongly with
231
- * follow-through; free learners drop off long before completion.
232
- *
233
- * ---------------------------------------------------------------
234
- * 8. PLAYBACK SPEED CORRELATION (event + everything)
235
- * ---------------------------------------------------------------
236
- *
237
- * PATTERN: Speed learners (>=2.0x speed on 3+ lectures) get 0.6x
238
- * watch_time and a paradoxical +8 quiz score boost. Thorough
239
- * learners (<=1.0x) get 1.4x watch_time.
240
- *
241
- * HOW TO FIND IT IN MIXPANEL:
242
- *
243
- * Report 1: Watch Time by Playback Speed
244
- * - Report type: Insights
245
- * - Event: "lecture completed"
246
- * - Measure: Average of "watch_time_mins"
247
- * - Breakdown: "playback_speed"
248
- * - Expected: speed >= 2.0 ~ 0.6x baseline; speed <= 1.0 ~ 1.4x baseline
249
- *
250
- * Report 2: Quiz Score by Speed Learner Cohort
251
- * - Cohort A: users with 3+ "lecture completed" events at playback_speed >= 2.0
252
- * - Cohort B: rest
253
- * - Event: "quiz completed"
254
- * - Measure: Average of "score_percent"
255
- * - Expected: A ~ +8 pts vs B
256
- *
257
- * REAL-WORLD ANALOGUE: Power users who watch lectures at 2x speed
258
- * tend to be domain-confident and outperform on assessments
259
- * despite spending less time.
260
- *
261
- * ---------------------------------------------------------------
262
- * 9. COURSE COMPLETION TIME-TO-CONVERT (everything)
263
- * ---------------------------------------------------------------
264
- *
265
- * PATTERN: Annual subscribers complete the course-completion funnel
266
- * 2x faster (factor 0.5); Free users 1.8x slower (factor 1.8).
267
- * Applied in the everything hook by scaling the enrolled-to-cert
268
- * gap on the raw events. Stronger factors compensate for the
269
- * composition effect from H7's conversion-rate gating.
270
- *
271
- * HOW TO FIND IT IN MIXPANEL:
272
- *
273
- * Report 1: Course Completion Median Time-to-Convert by Subscription
274
- * - Funnels > "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
275
- * - Measure: Median time to convert
276
- * - Breakdown: subscription_status
277
- * - Expected: annual < monthly < free (direction)
278
- *
279
- * Also visible via cross-event SQL: MIN("course enrolled" time) to
280
- * MIN("certificate earned" time) per user, broken down by
281
- * subscription_status. Annual < monthly < free.
282
- *
283
- * REAL-WORLD ANALOGUE: Paid commitment accelerates throughput.
284
- *
285
- * ---------------------------------------------------------------
286
- * 10. SOCIAL LEARNING EXPERIMENT (funnel experiment)
287
- * ---------------------------------------------------------------
288
- *
289
- * PATTERN: A/B experiment on the Social Learning funnel (discussion
290
- * posted → study group joined → resource downloaded). "AI Study
291
- * Buddy" variant boosts conversion 1.4x and speeds TTC to 0.85x.
292
- * Activates 30 days before dataset end.
293
- *
294
- * HOW TO FIND IT IN MIXPANEL:
295
- *
296
- * Report 1: $experiment_started by Variant
297
- * - Report type: Insights
298
- * - Event: "$experiment_started"
299
- * - Measure: Total
300
- * - Breakdown: "$experiment_name" and "Variant"
301
- * - Expected: ~50% Control, ~50% AI Study Buddy
302
- *
303
- * Report 2: Social Learning Funnel by Variant
304
- * - Report type: Funnels
305
- * - Steps: "discussion posted" → "study group joined" → "resource downloaded"
306
- * - Breakdown: Variant
307
- * - Expected: AI Study Buddy ~ 1.4x conversion vs Control
308
- *
309
- * REAL-WORLD ANALOGUE: AI-powered study companions boost social
310
- * engagement and resource discovery in cohort-based courses.
311
- *
312
- * ===============================================================
313
- * EXPECTED METRICS SUMMARY
314
- * ===============================================================
315
- *
316
- * Hook | Metric | Baseline | Hook Effect | Ratio
317
- * ------------------------|-----------------------|----------|--------------|------
318
- * Student vs Instructor | Profile attributes | generic | role-specific| n/a
319
- * Deadline Cramming | Sun/Mon quiz score | 1x | -25 pt | -38%
320
- * Notes Magic Number | sweet quiz score | 1x | 1.3x | 1.3x
321
- * Notes Magic Number | over certificates/user| 1x | 0.65x | -35%
322
- * Study Group Retention | D14 retention | ~ 40% | ~ 90% | 2.3x
323
- * Hint Dependency | easy problem rate (hint) | 33% | ~ 60% | 1.8x
324
- * Semester-End Spike | Assessment volume | 1x | ~ 2x | 2x
325
- * Free vs Paid | Course completion | 15% | 33% | 2.2x
326
- * Playback Speed | Quiz score (speed) | ~ 65 | ~ 73 | +8 pt
327
- * Course Completion T2C | median min by tier | 1x | 0.5x/1.8x | ~ 3.6x range
328
- * Social Learning Exp | AI variant conversion | 1x | 1.4x | 1.4x
329
- * Social Learning Exp | AI variant TTC | 1x | 0.85x | -15%
330
- */
331
-
332
- // ── SCALE ──
333
- const SEED = "harness-education";
334
- const NUM_USERS = 10_000;
335
- const DATASET_START = "2026-01-01T00:00:00Z";
336
- const DATASET_END = "2026-05-01T23:59:59Z";
337
- const EVENTS_PER_DAY = 1.2;
338
- const token = process.env.MP_TOKEN || "your-mixpanel-token";
339
-
340
- const chance = u.initChance(SEED);
341
-
342
- // ── KNOBS (tweak these to reshape stories) ──
343
- const HINT_EASY_LIKELIHOOD = 60;
344
- const HINT_HARD_LIKELIHOOD = 40;
345
-
346
- const SPEED_FAST_THRESHOLD = 2.0;
347
- const SPEED_FAST_WATCH_FACTOR = 0.6;
348
- const SPEED_FAST_WATCH_MIN = 3;
349
- const SPEED_SLOW_THRESHOLD = 1.0;
350
- const SPEED_SLOW_WATCH_FACTOR = 1.4;
351
- const SPEED_SLOW_WATCH_MAX = 90;
352
- const SPEED_LECTURE_COUNT_THRESHOLD = 3;
353
- const SPEED_QUIZ_BOOST_POINTS = 8;
354
-
355
- const FREE_FUNNEL_CONV_FACTOR = 0.5;
356
- const PAID_FUNNEL_CONV_FACTOR = 1.5;
357
- const FREE_CERT_DROP_LIKELIHOOD = 55;
358
-
359
- const NOTES_SWEET_MIN = 5;
360
- const NOTES_SWEET_MAX = 8;
361
- const NOTES_OVER_THRESHOLD = 9;
362
- const NOTES_QUIZ_BOOST = 1.3;
363
- const NOTES_BONUS_CERT_LIKELIHOOD = 40;
364
- const NOTES_OVER_CERT_DROP_LIKELIHOOD = 35;
365
-
366
- const SEMESTER_SPIKE_START_DAY = 75;
367
- const SEMESTER_SPIKE_END_DAY = 85;
368
- const SEMESTER_SPIKE_LIKELIHOOD = 80;
369
-
370
- const TTC_ANNUAL_FACTOR = 0.5;
371
- const TTC_FREE_FACTOR = 1.8;
372
-
373
- const STUDY_GROUP_EARLY_DAYS = 10;
374
- const STUDY_GROUP_LOW_QUIZ_THRESHOLD = 60;
375
- const STUDY_GROUP_CHURN_CUTOFF_DAYS = 14;
376
- const STUDY_GROUP_DISCUSSION_CLONE_LIKELIHOOD = 60;
377
-
378
- const DEADLINE_LATE_LIKELIHOOD = 60;
379
- const DEADLINE_QUIZ_PENALTY = 25;
380
-
381
- // ── DATA ARRAYS ──
382
- // Generate consistent IDs for lookup tables and event properties
383
- const courseIds = v.range(1, 151).map(n => `course_${v.uid(6)}`);
384
- const quizIds = v.range(1, 401).map(n => `quiz_${v.uid(6)}`);
385
- const groupIds = v.range(1, 301).map(n => `group_${v.uid(6)}`);
386
- const lectureIds = v.range(1, 501).map(n => `lecture_${v.uid(6)}`);
387
- const assignmentIds = v.range(1, 201).map(n => `assignment_${v.uid(6)}`);
388
- const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
389
-
390
- // ── HELPER FUNCTIONS ──
391
- function handleUserHooks(record) {
392
- // H1: STUDENT VS INSTRUCTOR PROFILES — role-based attributes.
393
- if (record.account_type === "instructor") {
394
- record.courses_created = chance.integer({ min: 1, max: 15 });
395
- record.teaching_experience_years = chance.integer({ min: 1, max: 20 });
396
- record.instructor_rating = Math.round((chance.floating({ min: 3.0, max: 5.0 }) + Number.EPSILON) * 100) / 100;
397
- } else {
398
- record.learning_goal = chance.pickone(["career_change", "skill_upgrade", "hobby", "degree_requirement"]);
399
- record.study_hours_per_week = chance.integer({ min: 2, max: 30 });
400
- }
401
- return record;
402
- }
403
-
404
- function handleEventHooks(record) {
405
- // H5: HINT DEPENDENCY — hint users get 60% easy problems; non-hint
406
- // users get 40% hard problems. Mutates difficulty (raw).
407
- if (record.event === "practice problem solved") {
408
- if (record.hint_used === true && chance.bool({ likelihood: HINT_EASY_LIKELIHOOD })) {
409
- record.difficulty = "easy";
410
- } else if (record.hint_used === false && chance.bool({ likelihood: HINT_HARD_LIKELIHOOD })) {
411
- record.difficulty = "hard";
412
- }
413
- }
414
- // H8 (event): PLAYBACK SPEED — speed learners (>= 2.0x) get
415
- // watch_time_mins compressed 0.6x; thorough learners (<= 1.0x) get 1.4x.
416
- if (record.event === "lecture completed") {
417
- const speed = record.playback_speed;
418
- if (speed >= SPEED_FAST_THRESHOLD && record.watch_time_mins !== undefined) {
419
- record.watch_time_mins = Math.max(SPEED_FAST_WATCH_MIN, Math.floor(record.watch_time_mins * SPEED_FAST_WATCH_FACTOR));
420
- } else if (speed !== undefined && speed <= SPEED_SLOW_THRESHOLD && record.watch_time_mins !== undefined) {
421
- record.watch_time_mins = Math.min(SPEED_SLOW_WATCH_MAX, Math.floor(record.watch_time_mins * SPEED_SLOW_WATCH_FACTOR));
422
- }
423
- }
424
- return record;
425
- }
426
-
427
- function handleFunnelPreHooks(record, meta) {
428
- // H7: FREE VS PAID — free users get 0.5x conversion rate; paid
429
- // subscribers get 1.5x. Scoped to the course-completion funnel ONLY
430
- // (sequence ending in "certificate earned") to avoid displacing standalone
431
- // events for paid users and triggering unintended churn in H4.
432
- const isCertFunnel = Array.isArray(meta?.funnel?.sequence) &&
433
- meta.funnel.sequence.includes("certificate earned");
434
- if (isCertFunnel) {
435
- const subStatus = meta?.profile?.subscription_status;
436
- if (subStatus === "free") {
437
- record.conversionRate = Math.round(record.conversionRate * FREE_FUNNEL_CONV_FACTOR);
438
- } else if (subStatus === "monthly" || subStatus === "annual") {
439
- record.conversionRate = Math.min(100, Math.round(record.conversionRate * PAID_FUNNEL_CONV_FACTOR));
440
- }
441
- }
442
- return record;
443
- }
444
-
445
- function handleEverythingHooks(record, meta) {
446
- const datasetStart = dayjs.unix(meta.datasetStart);
447
- const userEvents = record;
448
- const profile = meta.profile;
449
- const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
450
-
451
- if (profile) {
452
- userEvents.forEach((event) => {
453
- if (profile.Platform !== undefined) event.Platform = profile.Platform;
454
- });
455
- }
456
-
457
- let notesTakenCount = 0;
458
- let joinedStudyGroupEarly = false;
459
- let hasLowQuizScore = false;
460
- let speedLectureCount = 0;
461
-
462
- userEvents.forEach((event) => {
463
- const eventTime = dayjs(event.time);
464
- const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
465
- if (event.event === "lecture completed" && event.notes_taken === true) notesTakenCount++;
466
- if (event.event === "study group joined" && daysSinceStart <= STUDY_GROUP_EARLY_DAYS) joinedStudyGroupEarly = true;
467
- if (event.event === "quiz completed" && event.score_percent < STUDY_GROUP_LOW_QUIZ_THRESHOLD) hasLowQuizScore = true;
468
- if (event.event === "lecture completed" && event.playback_speed >= SPEED_FAST_THRESHOLD) speedLectureCount++;
469
- });
470
-
471
- // H3 + H10: NOTES MAGIC NUMBER (in-funnel, no flags)
472
- // Sweet 5-8 notes-taken lectures → +30% quiz score_percent (cap 100).
473
- // Over 9+ → drop 35% of certificate-earned events (over-noted but
474
- // can't synthesize; gets stuck in "study mode").
475
- if (notesTakenCount >= NOTES_SWEET_MIN && notesTakenCount <= NOTES_SWEET_MAX) {
476
- userEvents.forEach((event) => {
477
- if (event.event === "quiz completed" && event.score_percent !== undefined) {
478
- event.score_percent = Math.min(100, Math.round(event.score_percent * NOTES_QUIZ_BOOST));
479
- }
480
- });
481
- if (chance.bool({ likelihood: NOTES_BONUS_CERT_LIKELIHOOD })) {
482
- const lastEvent = userEvents[userEvents.length - 1];
483
- const certTemplate = userEvents.find(e => e.event === "certificate earned");
484
- if (lastEvent && certTemplate) {
485
- userEvents.push({
486
- ...certTemplate,
487
- time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
488
- user_id: lastEvent.user_id,
489
- course_id: chance.pickone(courseIds),
490
- completion_time_days: chance.integer({ min: 14, max: 90 }),
491
- final_grade: chance.integer({ min: 80, max: 100 }),
492
- });
493
- }
494
- }
495
- } else if (notesTakenCount >= NOTES_OVER_THRESHOLD) {
496
- // Over-noters: drop 35% of certificates (stuck in study mode)
497
- for (let i = userEvents.length - 1; i >= 0; i--) {
498
- if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: NOTES_OVER_CERT_DROP_LIKELIHOOD })) {
499
- userEvents.splice(i, 1);
500
- }
501
- }
502
- }
503
-
504
- // H8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.
505
- if (speedLectureCount >= SPEED_LECTURE_COUNT_THRESHOLD) {
506
- userEvents.forEach((event) => {
507
- if (event.event === "quiz completed" && event.score_percent !== undefined) {
508
- event.score_percent = Math.min(100, event.score_percent + SPEED_QUIZ_BOOST_POINTS);
509
- }
510
- });
511
- }
512
-
513
- // H6: SEMESTER-END SPIKE — duplicate quiz/assignment events
514
- // in days 75-85 window. No flag — discover via line chart.
515
- const duplicates = [];
516
- const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
517
- userEvents.forEach((event) => {
518
- if (spikableEvents.includes(event.event) && event.time) {
519
- const dayInDataset = dayjs.utc(event.time).diff(datasetStart, 'days', true);
520
- if (dayInDataset >= SEMESTER_SPIKE_START_DAY && dayInDataset <= SEMESTER_SPIKE_END_DAY && chance.bool({ likelihood: SEMESTER_SPIKE_LIKELIHOOD })) {
521
- const dup = JSON.parse(JSON.stringify(event));
522
- dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
523
- duplicates.push(dup);
524
- }
525
- }
526
- });
527
- if (duplicates.length > 0) userEvents.push(...duplicates);
528
-
529
- const subStatus = profile ? profile.subscription_status : "free";
530
-
531
- // H9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (everything)
532
- // Annual subscribers complete the cert funnel 2x faster (factor 0.5);
533
- // Free users 1.8x slower (factor 1.8). For each "certificate earned"
534
- // event, find the nearest preceding "course enrolled" and scale the gap.
535
- // Runs BEFORE cert-dropping (H7) so TTC adjustments aren't masked by
536
- // survivorship bias from the 55% free cert removal.
537
- {
538
- const ttcFactor = (
539
- subStatus === "annual" ? TTC_ANNUAL_FACTOR :
540
- subStatus === "free" ? TTC_FREE_FACTOR :
541
- 1.0
542
- );
543
- if (ttcFactor !== 1.0) {
544
- // Collect all "course enrolled" times (sorted) for binary lookup
545
- const enrolledTimes = userEvents
546
- .filter(e => e.event === "course enrolled")
547
- .map(e => dayjs(e.time))
548
- .sort((a, b) => a.valueOf() - b.valueOf());
549
-
550
- if (enrolledTimes.length > 0) {
551
- for (const event of userEvents) {
552
- if (event.event === "certificate earned") {
553
- const certTime = dayjs(event.time);
554
- // Find the latest enrolled time before this cert
555
- let anchor = null;
556
- for (let k = enrolledTimes.length - 1; k >= 0; k--) {
557
- if (enrolledTimes[k].isBefore(certTime)) {
558
- anchor = enrolledTimes[k];
559
- break;
560
- }
561
- }
562
- if (anchor) {
563
- const gap = certTime.diff(anchor);
564
- const newGap = Math.round(gap * ttcFactor);
565
- event.time = anchor.add(newGap, "milliseconds").toISOString();
566
- }
567
- }
568
- }
569
- }
570
- }
571
- }
572
-
573
- // H7: FREE VS PAID — free users lose 55% of certificates.
574
- if (subStatus === "free") {
575
- for (let i = userEvents.length - 1; i >= 0; i--) {
576
- if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: FREE_CERT_DROP_LIKELIHOOD })) {
577
- userEvents.splice(i, 1);
578
- }
579
- }
580
- }
581
-
582
- // H4: STUDY GROUP RETENTION — non-joiners with low scores lose
583
- // all post-day-14 events. Joiners get extra cloned discussion events.
584
- if (!joinedStudyGroupEarly && hasLowQuizScore) {
585
- const churnCutoff = firstEventTime ? firstEventTime.add(STUDY_GROUP_CHURN_CUTOFF_DAYS, 'days') : null;
586
- for (let i = userEvents.length - 1; i >= 0; i--) {
587
- if (churnCutoff && dayjs(userEvents[i].time).isAfter(churnCutoff)) {
588
- userEvents.splice(i, 1);
589
- }
590
- }
591
- } else if (joinedStudyGroupEarly) {
592
- const lastEvent = userEvents[userEvents.length - 1];
593
- const discussionTemplate = userEvents.find(e => e.event === "discussion posted");
594
- if (lastEvent && discussionTemplate && chance.bool({ likelihood: STUDY_GROUP_DISCUSSION_CLONE_LIKELIHOOD })) {
595
- userEvents.push({
596
- ...discussionTemplate,
597
- time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
598
- user_id: lastEvent.user_id,
599
- course_id: chance.pickone(courseIds),
600
- post_type: chance.pickone(["question", "answer", "comment"]),
601
- word_count: chance.integer({ min: 20, max: 400 }),
602
- });
603
- }
604
- }
605
-
606
- // H2: DEADLINE CRAMMING — Sun/Mon assignment_submitted events
607
- // flip is_late to true 60% of the time and quiz_completed score_percent
608
- // drops 25 points. Mutates raw is_late + score_percent.
609
- for (const event of userEvents) {
610
- if (event.event === "assignment submitted" && event.time) {
611
- const dow = new Date(event.time).getUTCDay();
612
- if (dow === 0 || dow === 1) {
613
- event.is_late = chance.bool({ likelihood: DEADLINE_LATE_LIKELIHOOD });
614
- }
615
- }
616
- }
617
- userEvents.forEach((event) => {
618
- if (event.event === "quiz completed" && event.time) {
619
- const dow = new Date(event.time).getUTCDay();
620
- if ((dow === 0 || dow === 1) && event.score_percent !== undefined) {
621
- event.score_percent = Math.max(0, event.score_percent - DEADLINE_QUIZ_PENALTY);
622
- }
623
- }
624
- });
625
-
626
- return record;
627
- }
628
-
629
- // ── CONFIG ──
630
- /** @type {Config} */
631
- const config = {
632
- version: 2,
633
- seed: SEED,
634
- datasetStart: DATASET_START,
635
- datasetEnd: DATASET_END,
636
- avgEventsPerUserPerDay: EVENTS_PER_DAY,
637
- numUsers: NUM_USERS,
638
- format: "json",
639
- gzip: true,
640
- credentials: {
641
- token,
642
- },
643
- switches: {
644
- hasSessionIds: true,
645
- alsoInferFunnels: false,
646
- hasLocation: true,
647
- hasAndroidDevices: true,
648
- hasIOSDevices: true,
649
- hasDesktopDevices: true,
650
- hasBrowser: false,
651
- hasCampaigns: false,
652
- isAnonymous: false,
653
- hasAdSpend: false,
654
- hasAvatar: true,
655
- },
656
- identity: {
657
- avgDevicePerUser: 2,
658
- },
659
- concurrency: 1,
660
- writeToDisk: false,
661
-
662
- funnels: [
663
- {
664
- sequence: ["account registered", "course enrolled", "lecture started"],
665
- isFirstFunnel: true,
666
- conversionRate: 75,
667
- timeToConvert: 1,
668
- },
669
- {
670
- // Core learning loop: students watch lectures and do practice problems constantly
671
- sequence: ["lecture started", "lecture completed", "practice problem solved"],
672
- conversionRate: 70,
673
- timeToConvert: 4,
674
- weight: 5,
675
- reentry: true,
676
- },
677
- {
678
- // Assessment flow: quizzes and assignments after studying
679
- sequence: ["quiz started", "quiz completed", "assignment submitted"],
680
- conversionRate: 55,
681
- timeToConvert: 8,
682
- weight: 3,
683
- reentry: true,
684
- },
685
- {
686
- // Course completion journey: enroll → complete → earn certificate
687
- sequence: ["course enrolled", "lecture completed", "quiz completed", "certificate earned"],
688
- conversionRate: 30,
689
- timeToConvert: 48,
690
- weight: 2,
691
- },
692
- {
693
- // Social learning: discussions and study groups
694
- sequence: ["discussion posted", "study group joined", "resource downloaded"],
695
- conversionRate: 50,
696
- timeToConvert: 12,
697
- weight: 2,
698
- experiment: {
699
- name: "AI Study Buddy",
700
- variants: [
701
- { name: "Control" },
702
- { name: "AI Study Buddy", conversionMultiplier: 1.4, ttcMultiplier: 0.85 },
703
- ],
704
- startDaysBeforeEnd: 30,
705
- },
706
- },
707
- {
708
- // Instructor interaction loop
709
- sequence: ["assignment submitted", "assignment graded", "instructor feedback given"],
710
- conversionRate: 45,
711
- timeToConvert: 24,
712
- weight: 2,
713
- },
714
- {
715
- // Support and monetization
716
- sequence: ["help requested", "subscription purchased", "course reviewed"],
717
- conversionRate: 35,
718
- timeToConvert: 24,
719
- weight: 1,
720
- },
721
- ],
722
-
723
- events: [
724
- {
725
- event: "account registered",
726
- weight: 1,
727
- isFirstEvent: true,
728
- isAuthEvent: true,
729
- properties: {
730
- "account_type": ["instructor", "instructor", "instructor", "instructor", "instructor", "instructor", "student"],
731
- "signup_source": ["organic", "referral", "school_partnership", "social_ad"],
732
- }
733
- },
734
- {
735
- event: "course enrolled",
736
- weight: 8,
737
- isStrictEvent: false,
738
- properties: {
739
- "course_id": courseIds,
740
- "course_category": ["CS", "Math", "Science", "Business", "Arts", "Languages"],
741
- "difficulty": ["beginner", "intermediate", "advanced"],
742
- "is_free": [false, false, false, true, true],
743
- }
744
- },
745
- {
746
- event: "lecture started",
747
- weight: 18,
748
- properties: {
749
- "course_id": courseIds,
750
- "lecture_id": lectureIds,
751
- "lecture_duration_mins": u.weighNumRange(5, 60, 0.8, 20),
752
- "module_number": u.weighNumRange(1, 12),
753
- }
754
- },
755
- {
756
- event: "lecture completed",
757
- weight: 14,
758
- isStrictEvent: false,
759
- properties: {
760
- "course_id": courseIds,
761
- "lecture_id": lectureIds,
762
- "watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
763
- "playback_speed": [0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0],
764
- "notes_taken": [false, false, true],
765
- }
766
- },
767
- {
768
- event: "quiz started",
769
- weight: 10,
770
- isStrictEvent: false,
771
- properties: {
772
- "course_id": courseIds,
773
- "quiz_id": quizIds,
774
- "quiz_type": ["practice", "graded", "final_exam"],
775
- "question_count": u.weighNumRange(5, 50, 0.7, 15),
776
- }
777
- },
778
- {
779
- event: "quiz completed",
780
- weight: 8,
781
- isStrictEvent: false,
782
- properties: {
783
- "course_id": courseIds,
784
- "quiz_id": quizIds,
785
- "score_percent": u.weighNumRange(0, 100, 1.2, 50),
786
- "time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
787
- "attempts": u.weighNumRange(1, 5, 0.5, 3),
788
- }
789
- },
790
- {
791
- event: "assignment submitted",
792
- weight: 6,
793
- isStrictEvent: false,
794
- properties: {
795
- "course_id": courseIds,
796
- "assignment_id": assignmentIds,
797
- "submission_type": ["text", "code", "file", "project"],
798
- "word_count": u.weighNumRange(100, 5000, 0.6, 500),
799
- "is_late": [false, false, false, false, true],
800
- }
801
- },
802
- {
803
- event: "assignment graded",
804
- weight: 5,
805
- properties: {
806
- "course_id": courseIds,
807
- "assignment_id": assignmentIds,
808
- "grade": ["A", "B", "C", "D", "F"],
809
- "feedback_length": u.weighNumRange(0, 500, 0.5, 100),
810
- "grader": ["instructor", "peer", "auto"],
811
- }
812
- },
813
- {
814
- event: "discussion posted",
815
- weight: 7,
816
- isStrictEvent: false,
817
- properties: {
818
- "course_id": courseIds,
819
- "post_type": ["question", "answer", "comment"],
820
- "word_count": u.weighNumRange(10, 500, 0.6, 80),
821
- }
822
- },
823
- {
824
- event: "certificate earned",
825
- weight: 2,
826
- isStrictEvent: false,
827
- properties: {
828
- "course_id": courseIds,
829
- "completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
830
- "final_grade": u.weighNumRange(60, 100, 1.2, 30),
831
- }
832
- },
833
- {
834
- event: "study group joined",
835
- weight: 4,
836
- isStrictEvent: false,
837
- properties: {
838
- "group_id": groupIds,
839
- "group_size": u.weighNumRange(3, 20, 0.7, 8),
840
- "group_type": ["study_circle", "project_team", "tutoring"],
841
- }
842
- },
843
- {
844
- event: "resource downloaded",
845
- weight: 9,
846
- properties: {
847
- "resource_type": ["pdf", "slides", "code_sample", "dataset", "cheat_sheet"],
848
- "course_id": courseIds,
849
- }
850
- },
851
- {
852
- event: "instructor feedback given",
853
- weight: 3,
854
- properties: {
855
- "course_id": courseIds,
856
- "feedback_type": ["written", "video", "rubric"],
857
- "response_time_hours": u.weighNumRange(1, 72, 0.5, 15),
858
- }
859
- },
860
- {
861
- event: "course reviewed",
862
- weight: 3,
863
- properties: {
864
- "course_id": courseIds,
865
- "rating": u.weighNumRange(1, 5, 1.5, 3),
866
- "review_length": u.weighNumRange(10, 1000, 0.5, 100),
867
- "would_recommend": [false, false, false, true, true, true, true, true, true, true],
868
- }
869
- },
870
- {
871
- event: "subscription purchased",
872
- weight: 2,
873
- properties: {
874
- "plan": ["monthly", "annual", "lifetime"],
875
- "price": [19.99, 149.99, 499.99],
876
- }
877
- },
878
- {
879
- event: "help requested",
880
- weight: 4,
881
- properties: {
882
- "topic": ["technical", "content", "billing", "accessibility"],
883
- "channel": ["chat", "email", "forum"],
884
- }
885
- },
886
- {
887
- event: "practice problem solved",
888
- weight: 12,
889
- isStrictEvent: false,
890
- properties: {
891
- "course_id": courseIds,
892
- "problem_id": problemIds,
893
- "difficulty": ["easy", "medium", "hard"],
894
- "time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
895
- "hint_used": [false, false, true],
896
- }
897
- },
898
- ],
899
-
900
- superProps: {
901
- Platform: ["Web", "iOS", "Android", "iPad"],
902
- },
903
-
904
- scdProps: {
905
- enrollment_status: {
906
- values: ["enrolled", "active", "completed", "dropped"],
907
- frequency: "month",
908
- timing: "fuzzy",
909
- max: 6
910
- },
911
- course_status: {
912
- values: ["draft", "published", "archived", "deprecated"],
913
- frequency: "month",
914
- timing: "fixed",
915
- max: 6,
916
- type: "course_id"
917
- }
918
- },
919
-
920
- userProps: {
921
- "account_type": ["student", "student", "student", "student", "student", "student", "student", "student", "instructor"],
922
- "subscription_status": ["free", "free", "free", "monthly", "annual"],
923
- "learning_style": ["visual", "reading", "hands_on", "auditory"],
924
- "education_level": ["high_school", "bachelors", "masters", "phd", "self_taught"],
925
- "timezone": ["US_Eastern", "US_Pacific", "US_Central", "Europe", "Asia"],
926
- "courses_created": [0],
927
- "teaching_experience_years": [0],
928
- "instructor_rating": [0],
929
- "learning_goal": ["none"],
930
- "study_hours_per_week": [0],
931
- "Platform": ["Web", "iOS", "Android", "iPad"],
932
- },
933
-
934
- groupKeys: [
935
- ["course_id", 150, ["course enrolled", "lecture started", "lecture completed", "quiz completed", "certificate earned"]],
936
- ["group_id", 300, ["study group joined", "discussion posted"]],
937
- ],
938
-
939
- groupProps: {
940
- course_id: {
941
- "title": () => `${chance.pickone(["Introduction to", "Advanced", "Mastering", "Fundamentals of", "Applied"])} ${chance.pickone(["Algorithms", "Data Science", "Machine Learning", "Statistics", "Web Development", "Calculus", "Biology", "Economics", "Design Thinking", "Creative Writing"])}`,
942
- "instructor_count": u.weighNumRange(1, 5, 0.5, 2),
943
- "total_enrolled": u.weighNumRange(50, 5000, 0.6, 500),
944
- "avg_rating": u.weighNumRange(3, 5, 1.5, 1),
945
- },
946
- group_id: {
947
- "name": () => `${chance.pickone(["Study", "Learning", "Focus", "Peer", "Cohort"])} ${chance.pickone(["Circle", "Squad", "Team", "Hub", "Group"])} ${chance.character({ alpha: true, casing: "upper" })}${chance.integer({ min: 1, max: 99 })}`,
948
- "member_count": u.weighNumRange(3, 20, 0.7, 8),
949
- "focus_area": ["CS", "Math", "Science", "Business", "Arts", "Languages"],
950
- }
951
- },
952
-
953
- lookupTables: [],
954
-
955
- hook(record, type, meta) {
956
- if (type === "user") return handleUserHooks(record);
957
- if (type === "event") return handleEventHooks(record);
958
- if (type === "funnel-pre") return handleFunnelPreHooks(record, meta);
959
- if (type === "everything") return handleEverythingHooks(record, meta);
960
- return record;
961
- }
962
- };
963
-
964
- export default config;