@ak--47/dungeon-master 1.0.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 (66) hide show
  1. package/README.md +518 -0
  2. package/dungeons/array-of-object-lookup-schema.json +327 -0
  3. package/dungeons/array-of-object-lookup.js +220 -0
  4. package/dungeons/ecommerce-schema.json +462 -0
  5. package/dungeons/ecommerce.js +447 -0
  6. package/dungeons/education-schema.json +2409 -0
  7. package/dungeons/education.js +768 -0
  8. package/dungeons/fintech-schema.json +14034 -0
  9. package/dungeons/fintech.js +696 -0
  10. package/dungeons/foobar-schema.json +403 -0
  11. package/dungeons/foobar.js +296 -0
  12. package/dungeons/food-delivery-schema.json +192 -0
  13. package/dungeons/food-delivery.js +602 -0
  14. package/dungeons/food-schema.json +1152 -0
  15. package/dungeons/food.js +754 -0
  16. package/dungeons/gaming-schema.json +1270 -0
  17. package/dungeons/gaming.js +508 -0
  18. package/dungeons/insurance-application-schema.json +204 -0
  19. package/dungeons/insurance-application.js +605 -0
  20. package/dungeons/media-schema.json +906 -0
  21. package/dungeons/media.js +790 -0
  22. package/dungeons/retention-cadence-schema.json +78 -0
  23. package/dungeons/retention-cadence.js +244 -0
  24. package/dungeons/rpg-schema.json +4526 -0
  25. package/dungeons/rpg.js +919 -0
  26. package/dungeons/sanity-schema.json +255 -0
  27. package/dungeons/sanity.js +152 -0
  28. package/dungeons/sass-schema.json +1291 -0
  29. package/dungeons/sass.js +795 -0
  30. package/dungeons/scd-schema.json +919 -0
  31. package/dungeons/scd.js +277 -0
  32. package/dungeons/simple-schema.json +608 -0
  33. package/dungeons/simple.js +285 -0
  34. package/dungeons/simplest-schema.json +1418 -0
  35. package/dungeons/simplest.js +392 -0
  36. package/dungeons/social-schema.json +1118 -0
  37. package/dungeons/social.js +686 -0
  38. package/dungeons/text-generation-schema.json +3096 -0
  39. package/dungeons/text-generation.js +812 -0
  40. package/index.js +567 -0
  41. package/lib/core/config-validator.js +395 -0
  42. package/lib/core/context.js +204 -0
  43. package/lib/core/dungeon-loader.js +337 -0
  44. package/lib/core/storage.js +379 -0
  45. package/lib/generators/adspend.js +132 -0
  46. package/lib/generators/events.js +271 -0
  47. package/lib/generators/funnels.js +407 -0
  48. package/lib/generators/mirror.js +167 -0
  49. package/lib/generators/product-lookup.js +262 -0
  50. package/lib/generators/product-names.js +195 -0
  51. package/lib/generators/profiles.js +93 -0
  52. package/lib/generators/scd.js +124 -0
  53. package/lib/generators/text.js +1192 -0
  54. package/lib/orchestrators/mixpanel-sender.js +266 -0
  55. package/lib/orchestrators/user-loop.js +335 -0
  56. package/lib/templates/abbreviated.d.ts +169 -0
  57. package/lib/templates/defaults.js +1405 -0
  58. package/lib/templates/phrases.js +2526 -0
  59. package/lib/templates/schema.d.ts +173 -0
  60. package/lib/templates/soup-presets.js +188 -0
  61. package/lib/utils/function-registry.js +302 -0
  62. package/lib/utils/json-evaluator.js +172 -0
  63. package/lib/utils/logger.js +34 -0
  64. package/lib/utils/utils.js +1490 -0
  65. package/package.json +89 -0
  66. package/types.d.ts +865 -0
@@ -0,0 +1,768 @@
1
+ import dayjs from "dayjs";
2
+ import utc from "dayjs/plugin/utc.js";
3
+ import "dotenv/config";
4
+ import * as u from "../lib/utils/utils.js";
5
+ import * as v from "ak-tools";
6
+
7
+ const SEED = "harness-education";
8
+ dayjs.extend(utc);
9
+ const chance = u.initChance(SEED);
10
+ const num_users = 5_000;
11
+ const days = 100;
12
+
13
+ /** @typedef {import("../types.d.ts").Dungeon} Config */
14
+
15
+ /**
16
+ * ═══════════════════════════════════════════════════════════════════════════════
17
+ * DATASET OVERVIEW — LearnPath eLearning Platform
18
+ * ═══════════════════════════════════════════════════════════════════════════════
19
+ *
20
+ * An online learning platform modeled after Coursera, Khan Academy, and Udemy.
21
+ * Supports self-paced and cohort-based learning with courses, quizzes,
22
+ * assignments, and social study features.
23
+ *
24
+ * Scale: 5,000 users / 600K events / 100 days / 17 event types
25
+ *
26
+ * CORE LOOP:
27
+ * Register → browse/enroll in courses → watch lectures → practice problems →
28
+ * quizzes/assignments → certificate earned. Social layer (study groups,
29
+ * discussions) drives retention. Subscription tiers (free/monthly/annual)
30
+ * gate completion rates.
31
+ *
32
+ * FUNNELS:
33
+ * - Onboarding: account registered → course enrolled → lecture started
34
+ * - Learning loop: lecture started → lecture completed → practice problem solved
35
+ * - Assessment: quiz started → quiz completed → assignment submitted
36
+ * - Course completion: course enrolled → lecture completed → quiz completed → certificate earned
37
+ * - Social learning: discussion posted → study group joined → resource downloaded
38
+ * - Instructor interaction: assignment submitted → assignment graded → instructor feedback given
39
+ * - Support/monetization: help requested → subscription purchased → course reviewed
40
+ *
41
+ * GROUPS: course_id (150 courses), group_id (300 study groups)
42
+ * SUBSCRIPTIONS: free (~60%), monthly, annual
43
+ * ACCOUNT TYPES: ~89% students, ~11% instructors (two-sided marketplace)
44
+ */
45
+
46
+ /**
47
+ * ═══════════════════════════════════════════════════════════════════════════════
48
+ * ANALYTICS HOOKS (8 architected patterns)
49
+ * ═══════════════════════════════════════════════════════════════════════════════
50
+ *
51
+ * 1. STUDENT VS INSTRUCTOR PROFILES
52
+ * Instructor profiles get teaching attributes (courses_created,
53
+ * teaching_experience_years, instructor_rating). Students get learning
54
+ * attributes (learning_goal, study_hours_per_week).
55
+ *
56
+ * Mixpanel reports:
57
+ * • Insights → Any event → Unique users → Breakdown: "account_type"
58
+ * Expected: ~89% students, ~11% instructors
59
+ * • Insights → "instructor feedback given" → Total per user → Breakdown: "account_type"
60
+ * Expected: Instructors dominate feedback; students show learning_goal instead
61
+ *
62
+ * 2. DEADLINE CRAMMING
63
+ * Assignments submitted on Sun/Mon are rushed: 60% late (vs ~20% baseline),
64
+ * quiz scores drop by 25 points. Events carry is_deadline_rush: true.
65
+ *
66
+ * Mixpanel reports:
67
+ * • Insights → "assignment submitted" → Total → Breakdown: "is_deadline_rush"
68
+ * Expected: is_deadline_rush=true shows ~60% late rate vs ~20% baseline
69
+ * • Insights → "quiz completed" → Avg "score_percent" → Breakdown: Day of Week
70
+ * Expected: Sun/Mon scores ~25 points lower (~40 vs ~65)
71
+ *
72
+ * 3. NOTES-TAKERS SUCCEED
73
+ * Students with 5+ notes_taken=true lectures get +20 quiz score boost
74
+ * (capped at 100) and 40% chance of bonus certificate. Marked diligent_student: true.
75
+ *
76
+ * Mixpanel reports:
77
+ * • Insights → "quiz completed" → Avg "score_percent" → Breakdown: "diligent_student"
78
+ * Expected: diligent_student=true ≈ 85 avg vs ~65 baseline (+20 pts)
79
+ * • Insights → "certificate earned" → Total per user → Breakdown: "diligent_student"
80
+ * Expected: diligent_student=true earn ~40% more certificates
81
+ *
82
+ * 4. STUDY GROUP RETENTION
83
+ * Early study group joiners (within 10 days) retain and get bonus discussions.
84
+ * Non-joiners with low quiz scores (<60) churn hard at day 14 (all later events removed).
85
+ *
86
+ * Mixpanel reports:
87
+ * • Retention → A: "account registered" → B: Any event → Segment by early study group join
88
+ * Expected: Early joiners ~90% D14 retention; non-joiners with low scores ~30%
89
+ * • Insights → "discussion posted" → Total per user → Breakdown: "study_group_member"
90
+ * Expected: study_group_member=true users post more
91
+ *
92
+ * 5. HINT DEPENDENCY
93
+ * Hint users get 60% chance of easy problems; non-hint users get 40% chance of
94
+ * hard problems with independent_solver: true.
95
+ *
96
+ * Mixpanel reports:
97
+ * • Insights → "practice problem solved" → Total → Breakdown: "difficulty" → Filter: hint_used=true
98
+ * Expected: ~60% easy (vs ~33% baseline)
99
+ * • Insights → "practice problem solved" → Total → Breakdown: "difficulty" → Filter: hint_used=false
100
+ * Expected: ~40% hard (vs ~33% baseline)
101
+ *
102
+ * 6. SEMESTER-END SPIKE
103
+ * Days 75-85: quiz_started, quiz_completed, assignment_submitted events duplicated
104
+ * at 80% rate. Events carry semester_end_rush: true.
105
+ *
106
+ * Mixpanel reports:
107
+ * • Insights (line) → "quiz started" + "quiz completed" + "assignment submitted" → Daily
108
+ * Expected: ~2x volume spike during days 75-85
109
+ * • Insights → "quiz completed" → Total → Breakdown: "semester_end_rush"
110
+ * Expected: semester_end_rush=true clusters in days 75-85
111
+ *
112
+ * 7. FREE VS PAID COURSES
113
+ * Free users get 0.5x funnel conversion rate; paid subscribers get 1.5x.
114
+ * Free users also lose 55% of certificates. Creates ~2.2x completion gap.
115
+ *
116
+ * Mixpanel reports:
117
+ * • Funnels → "course enrolled" → "lecture completed" → "quiz completed" → "certificate earned"
118
+ * Breakdown: "subscription_status"
119
+ * Expected: free ≈ 15% completion, paid ≈ 33% (~2.2x difference)
120
+ * • Insights → "certificate earned" → Total per user → Breakdown: "subscription_status"
121
+ * Expected: Paid subscribers earn significantly more certificates
122
+ *
123
+ * 8. PLAYBACK SPEED CORRELATION
124
+ * Speed learners (>=2.0x, 3+ lectures): compressed watch_time (0.6x),
125
+ * paradoxically higher quiz scores (+8 pts). Thorough learners (<=1.0x):
126
+ * extended watch_time (1.4x).
127
+ *
128
+ * Mixpanel reports:
129
+ * • Insights → "lecture completed" → Avg "watch_time_mins" → Breakdown: "speed_learner"
130
+ * Expected: speed_learner=true ≈ 0.6x watch time
131
+ * • Insights → "quiz completed" → Avg "score_percent" → Breakdown: "speed_learner_effect"
132
+ * Expected: speed_learner_effect=true shows +8 points (faster = better)
133
+ *
134
+ * ═══════════════════════════════════════════════════════════════════════════════
135
+ * ADVANCED ANALYSIS IDEAS
136
+ * ═══════════════════════════════════════════════════════════════════════════════
137
+ *
138
+ * CROSS-HOOK PATTERNS:
139
+ * - The Ideal Student: notes (H3) + study groups (H4) + no hints (H5) + paid (H7) + speed (H8)
140
+ * - Cramming Cascade: deadline crammers (H2) compounded with semester-end spike (H6)?
141
+ * - Social Safety Net: does early study group joining (H4) prevent churn for low scorers?
142
+ * - Hint-to-Mastery: do hint-dependent (H5) students who join groups (H4) wean off hints?
143
+ * - Payment + Notes: are paid subscribers (H7) more likely to take notes (H3)?
144
+ *
145
+ * COHORT ANALYSIS:
146
+ * - By education level: PhD vs self-taught hook patterns
147
+ * - By learning style: visual vs hands-on note-taking rates
148
+ * - By platform: mobile vs desktop playback speed preferences
149
+ * - By course category: CS vs Arts hint usage
150
+ *
151
+ * FUNNEL ANALYSIS:
152
+ * - Onboarding by account_type
153
+ * - Course completion by subscription, notes, study groups
154
+ * - Practice mastery by hint usage, speed, learning style
155
+ *
156
+ * ═══════════════════════════════════════════════════════════════════════════════
157
+ * EXPECTED METRICS SUMMARY
158
+ * ═══════════════════════════════════════════════════════════════════════════════
159
+ *
160
+ * Hook | Metric | Baseline | Hook Effect | Ratio
161
+ * ────────────────────────|───────────────────────|──────────|──────────────|──────
162
+ * Student vs Instructor | Profile attributes | generic | role-specific| N/A
163
+ * Deadline Cramming | Late submission rate | ~20% | ~60% | 3x
164
+ * Deadline Cramming | Quiz score (Sun/Mon) | ~65 | ~40 | -25pt
165
+ * Notes-Takers Succeed | Quiz score | ~65 | ~85 | +20pt
166
+ * Notes-Takers Succeed | Certificate rate | baseline | +40% | 1.4x
167
+ * Study Group Retention | D14 retention | ~40% | ~90% | 2.3x
168
+ * Study Group Retention | Post-D14 events | 100% | 30% (churn) | 0.3x
169
+ * Hint Dependency | Easy problem rate | ~33% | ~60% | 1.8x
170
+ * Hint Dependency | Hard problem rate | ~33% | ~40% (no hint)| 1.2x
171
+ * Semester-End Spike | Assessment volume | baseline | ~2x | 2x
172
+ * Free vs Paid | Course completion | 15% | 33% | 2.2x
173
+ * Playback Speed | Quiz score (speed) | ~65 | ~73 | +8pt
174
+ */
175
+
176
+ // Generate consistent IDs for lookup tables and event properties
177
+ const courseIds = v.range(1, 151).map(n => `course_${v.uid(6)}`);
178
+ const quizIds = v.range(1, 401).map(n => `quiz_${v.uid(6)}`);
179
+ const groupIds = v.range(1, 301).map(n => `group_${v.uid(6)}`);
180
+ const lectureIds = v.range(1, 501).map(n => `lecture_${v.uid(6)}`);
181
+ const assignmentIds = v.range(1, 201).map(n => `assignment_${v.uid(6)}`);
182
+ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
183
+
184
+ /** @type {Config} */
185
+ const config = {
186
+ token: "",
187
+ seed: SEED,
188
+ numDays: days,
189
+ numEvents: num_users * 120,
190
+ numUsers: num_users,
191
+ hasAnonIds: false,
192
+ hasSessionIds: true,
193
+ format: "json",
194
+ gzip: true,
195
+ alsoInferFunnels: false,
196
+ hasLocation: true,
197
+ hasAndroidDevices: true,
198
+ hasIOSDevices: true,
199
+ hasDesktopDevices: true,
200
+ hasBrowser: false,
201
+ hasCampaigns: false,
202
+ isAnonymous: false,
203
+ hasAdSpend: false,
204
+ percentUsersBornInDataset: 50,
205
+ hasAvatar: true,
206
+ batchSize: 2_500_000,
207
+ concurrency: 1,
208
+ writeToDisk: false,
209
+
210
+ funnels: [
211
+ {
212
+ sequence: ["account registered", "course enrolled", "lecture started"],
213
+ isFirstFunnel: true,
214
+ conversionRate: 75,
215
+ timeToConvert: 1,
216
+ },
217
+ {
218
+ // Core learning loop: students watch lectures and do practice problems constantly
219
+ sequence: ["lecture started", "lecture completed", "practice problem solved"],
220
+ conversionRate: 70,
221
+ timeToConvert: 4,
222
+ weight: 5,
223
+ },
224
+ {
225
+ // Assessment flow: quizzes and assignments after studying
226
+ sequence: ["quiz started", "quiz completed", "assignment submitted"],
227
+ conversionRate: 55,
228
+ timeToConvert: 8,
229
+ weight: 3,
230
+ },
231
+ {
232
+ // Course completion journey: enroll → complete → earn certificate
233
+ sequence: ["course enrolled", "lecture completed", "quiz completed", "certificate earned"],
234
+ conversionRate: 30,
235
+ timeToConvert: 48,
236
+ weight: 2,
237
+ },
238
+ {
239
+ // Social learning: discussions and study groups
240
+ sequence: ["discussion posted", "study group joined", "resource downloaded"],
241
+ conversionRate: 50,
242
+ timeToConvert: 12,
243
+ weight: 2,
244
+ },
245
+ {
246
+ // Instructor interaction loop
247
+ sequence: ["assignment submitted", "assignment graded", "instructor feedback given"],
248
+ conversionRate: 45,
249
+ timeToConvert: 24,
250
+ weight: 2,
251
+ },
252
+ {
253
+ // Support and monetization
254
+ sequence: ["help requested", "subscription purchased", "course reviewed"],
255
+ conversionRate: 35,
256
+ timeToConvert: 24,
257
+ weight: 1,
258
+ },
259
+ ],
260
+
261
+ events: [
262
+ {
263
+ event: "account registered",
264
+ weight: 1,
265
+ isFirstEvent: true,
266
+ properties: {
267
+ "account_type": u.pickAWinner(["student", "instructor"], 0.15),
268
+ "signup_source": ["organic", "referral", "school_partnership", "social_ad"],
269
+ }
270
+ },
271
+ {
272
+ event: "course enrolled",
273
+ weight: 8,
274
+ properties: {
275
+ "course_id": u.pickAWinner(courseIds),
276
+ "course_category": ["CS", "Math", "Science", "Business", "Arts", "Languages"],
277
+ "difficulty": ["beginner", "intermediate", "advanced"],
278
+ "is_free": u.pickAWinner([true, false], 0.4),
279
+ }
280
+ },
281
+ {
282
+ event: "lecture started",
283
+ weight: 18,
284
+ properties: {
285
+ "course_id": u.pickAWinner(courseIds),
286
+ "lecture_id": u.pickAWinner(lectureIds),
287
+ "lecture_duration_mins": u.weighNumRange(5, 60, 0.8, 20),
288
+ "module_number": u.weighNumRange(1, 12),
289
+ }
290
+ },
291
+ {
292
+ event: "lecture completed",
293
+ weight: 14,
294
+ properties: {
295
+ "course_id": u.pickAWinner(courseIds),
296
+ "lecture_id": u.pickAWinner(lectureIds),
297
+ "watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
298
+ "playback_speed": u.pickAWinner([0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0]),
299
+ "notes_taken": u.pickAWinner([true, false], 0.35),
300
+ }
301
+ },
302
+ {
303
+ event: "quiz started",
304
+ weight: 10,
305
+ properties: {
306
+ "course_id": u.pickAWinner(courseIds),
307
+ "quiz_id": u.pickAWinner(quizIds),
308
+ "quiz_type": ["practice", "graded", "final_exam"],
309
+ "question_count": u.weighNumRange(5, 50, 0.7, 15),
310
+ }
311
+ },
312
+ {
313
+ event: "quiz completed",
314
+ weight: 8,
315
+ properties: {
316
+ "course_id": u.pickAWinner(courseIds),
317
+ "quiz_id": u.pickAWinner(quizIds),
318
+ "score_percent": u.weighNumRange(0, 100, 1.2, 50),
319
+ "time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
320
+ "attempts": u.weighNumRange(1, 5, 0.5, 3),
321
+ }
322
+ },
323
+ {
324
+ event: "assignment submitted",
325
+ weight: 6,
326
+ properties: {
327
+ "course_id": u.pickAWinner(courseIds),
328
+ "assignment_id": u.pickAWinner(assignmentIds),
329
+ "submission_type": ["text", "code", "file", "project"],
330
+ "word_count": u.weighNumRange(100, 5000, 0.6, 500),
331
+ "is_late": u.pickAWinner([true, false], 0.2),
332
+ }
333
+ },
334
+ {
335
+ event: "assignment graded",
336
+ weight: 5,
337
+ properties: {
338
+ "course_id": u.pickAWinner(courseIds),
339
+ "assignment_id": u.pickAWinner(assignmentIds),
340
+ "grade": ["A", "B", "C", "D", "F"],
341
+ "feedback_length": u.weighNumRange(0, 500, 0.5, 100),
342
+ "grader": ["instructor", "peer", "auto"],
343
+ }
344
+ },
345
+ {
346
+ event: "discussion posted",
347
+ weight: 7,
348
+ properties: {
349
+ "course_id": u.pickAWinner(courseIds),
350
+ "post_type": ["question", "answer", "comment"],
351
+ "word_count": u.weighNumRange(10, 500, 0.6, 80),
352
+ }
353
+ },
354
+ {
355
+ event: "certificate earned",
356
+ weight: 2,
357
+ properties: {
358
+ "course_id": u.pickAWinner(courseIds),
359
+ "completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
360
+ "final_grade": u.weighNumRange(60, 100, 1.2, 30),
361
+ }
362
+ },
363
+ {
364
+ event: "study group joined",
365
+ weight: 4,
366
+ properties: {
367
+ "group_id": u.pickAWinner(groupIds),
368
+ "group_size": u.weighNumRange(3, 20, 0.7, 8),
369
+ "group_type": ["study_circle", "project_team", "tutoring"],
370
+ }
371
+ },
372
+ {
373
+ event: "resource downloaded",
374
+ weight: 9,
375
+ properties: {
376
+ "resource_type": ["pdf", "slides", "code_sample", "dataset", "cheat_sheet"],
377
+ "course_id": u.pickAWinner(courseIds),
378
+ }
379
+ },
380
+ {
381
+ event: "instructor feedback given",
382
+ weight: 3,
383
+ properties: {
384
+ "course_id": u.pickAWinner(courseIds),
385
+ "feedback_type": ["written", "video", "rubric"],
386
+ "response_time_hours": u.weighNumRange(1, 72, 0.5, 15),
387
+ }
388
+ },
389
+ {
390
+ event: "course reviewed",
391
+ weight: 3,
392
+ properties: {
393
+ "course_id": u.pickAWinner(courseIds),
394
+ "rating": u.weighNumRange(1, 5, 1.5, 3),
395
+ "review_length": u.weighNumRange(10, 1000, 0.5, 100),
396
+ "would_recommend": u.pickAWinner([true, false], 0.7),
397
+ }
398
+ },
399
+ {
400
+ event: "subscription purchased",
401
+ weight: 2,
402
+ properties: {
403
+ "plan": ["monthly", "annual", "lifetime"],
404
+ "price": u.pickAWinner([19.99, 149.99, 499.99]),
405
+ }
406
+ },
407
+ {
408
+ event: "help requested",
409
+ weight: 4,
410
+ properties: {
411
+ "topic": ["technical", "content", "billing", "accessibility"],
412
+ "channel": ["chat", "email", "forum"],
413
+ }
414
+ },
415
+ {
416
+ event: "practice problem solved",
417
+ weight: 12,
418
+ properties: {
419
+ "course_id": u.pickAWinner(courseIds),
420
+ "problem_id": u.pickAWinner(problemIds),
421
+ "difficulty": ["easy", "medium", "hard"],
422
+ "time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
423
+ "hint_used": u.pickAWinner([true, false], 0.35),
424
+ }
425
+ },
426
+ ],
427
+
428
+ superProps: {
429
+ platform: ["Web", "iOS", "Android", "iPad"],
430
+ },
431
+
432
+ scdProps: {},
433
+
434
+ userProps: {
435
+ "account_type": u.pickAWinner(["student", "student", "student", "student", "student", "student", "student", "student", "instructor"]),
436
+ "subscription_status": u.pickAWinner(["free", "free", "free", "monthly", "annual"]),
437
+ "learning_style": ["visual", "reading", "hands_on", "auditory"],
438
+ "education_level": ["high_school", "bachelors", "masters", "phd", "self_taught"],
439
+ "timezone": ["US_Eastern", "US_Pacific", "US_Central", "Europe", "Asia"],
440
+ },
441
+
442
+ groupKeys: [
443
+ ["course_id", 150, ["course enrolled", "lecture started", "lecture completed", "quiz completed", "certificate earned"]],
444
+ ["group_id", 300, ["study group joined", "discussion posted"]],
445
+ ],
446
+
447
+ groupProps: {
448
+ course_id: {
449
+ "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"])}`,
450
+ "instructor_count": u.weighNumRange(1, 5, 0.5, 2),
451
+ "total_enrolled": u.weighNumRange(50, 5000, 0.6, 500),
452
+ "avg_rating": u.weighNumRange(3, 5, 1.5, 1),
453
+ },
454
+ group_id: {
455
+ "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 })}`,
456
+ "member_count": u.weighNumRange(3, 20, 0.7, 8),
457
+ "focus_area": ["CS", "Math", "Science", "Business", "Arts", "Languages"],
458
+ }
459
+ },
460
+
461
+ lookupTables: [],
462
+
463
+ /**
464
+ * ARCHITECTED ANALYTICS HOOKS
465
+ *
466
+ * This hook function creates 8 deliberate patterns in the data:
467
+ *
468
+ * 1. STUDENT VS INSTRUCTOR PROFILES: Instructor profiles get teaching attributes; students get learning attributes
469
+ * 2. DEADLINE CRAMMING: Assignments submitted on Sun/Mon are rushed and lower quality
470
+ * 3. NOTES-TAKERS SUCCEED: Students who take notes during lectures score higher on quizzes
471
+ * 4. STUDY GROUP RETENTION: Early study group joiners retain; non-joiners with low scores churn
472
+ * 5. HINT DEPENDENCY: Hint users get locked into easy problems; non-hint users tackle harder ones
473
+ * 6. SEMESTER-END SPIKE: Days 75-85 see doubled assessment activity (cramming period)
474
+ * 7. FREE VS PAID COURSES: Paid subscribers convert through Course Completion funnel at ~2.2x rate
475
+ * 8. PLAYBACK SPEED CORRELATION: Speed learners paradoxically score higher; thorough learners get extended time
476
+ */
477
+ hook: function (record, type, meta) {
478
+ const NOW = dayjs();
479
+ const DATASET_START = NOW.subtract(days, 'days');
480
+
481
+ // ═══════════════════════════════════════════════════════════════════
482
+ // Hook #1: STUDENT VS INSTRUCTOR PROFILES
483
+ // ═══════════════════════════════════════════════════════════════════
484
+ if (type === "user") {
485
+ if (record.account_type === "instructor") {
486
+ record.courses_created = chance.integer({ min: 1, max: 15 });
487
+ record.teaching_experience_years = chance.integer({ min: 1, max: 20 });
488
+ record.instructor_rating = Math.round((chance.floating({ min: 3.0, max: 5.0 }) + Number.EPSILON) * 100) / 100;
489
+ } else {
490
+ record.learning_goal = chance.pickone(["career_change", "skill_upgrade", "hobby", "degree_requirement"]);
491
+ record.study_hours_per_week = chance.integer({ min: 2, max: 30 });
492
+ }
493
+ }
494
+
495
+ // ═══════════════════════════════════════════════════════════════════
496
+ // Hook #2: DEADLINE CRAMMING
497
+ // ═══════════════════════════════════════════════════════════════════
498
+ if (type === "event") {
499
+ if (record.event === "assignment submitted" && record.time) {
500
+ const eventDay = dayjs(record.time).day(); // 0 = Sunday, 1 = Monday
501
+ if (eventDay === 0 || eventDay === 1) {
502
+ record.is_deadline_rush = true;
503
+ // 60% chance of being late (only 40% on time)
504
+ record.is_late = !chance.bool({ likelihood: 40 });
505
+ } else {
506
+ record.is_deadline_rush = false;
507
+ record.is_late = !chance.bool({ likelihood: 80 });
508
+ }
509
+ }
510
+
511
+ // Quiz score penalty moved to everything hook (after churn removal)
512
+ // to avoid selection bias — the penalty was causing more Sun/Mon
513
+ // quiz-takers to trigger hasLowQuizScore churn, inflating their avg
514
+ }
515
+
516
+ // ═══════════════════════════════════════════════════════════════════
517
+ // Hook #5: HINT DEPENDENCY
518
+ // ═══════════════════════════════════════════════════════════════════
519
+ if (type === "event") {
520
+ if (record.event === "practice problem solved") {
521
+ if (record.hint_used === true) {
522
+ // Hint users gravitate toward easy problems
523
+ if (chance.bool({ likelihood: 60 })) {
524
+ record.difficulty = "easy";
525
+ }
526
+ record.independent_solver = false;
527
+ } else if (record.hint_used === false) {
528
+ // Independent solvers tackle harder problems
529
+ if (chance.bool({ likelihood: 40 })) {
530
+ record.difficulty = "hard";
531
+ record.independent_solver = true;
532
+ } else {
533
+ record.independent_solver = false;
534
+ }
535
+ } else {
536
+ record.independent_solver = false;
537
+ }
538
+ }
539
+ }
540
+
541
+ // ═══════════════════════════════════════════════════════════════════
542
+ // Hook #6: SEMESTER-END SPIKE (tag in event hook, duplicate in everything hook)
543
+ // ═══════════════════════════════════════════════════════════════════
544
+ if (type === "event") {
545
+ if (record.time) {
546
+ const eventTime = dayjs(record.time);
547
+ const dayInDataset = eventTime.diff(DATASET_START, 'days', true);
548
+
549
+ const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
550
+ if (spikableEvents.includes(record.event)) {
551
+ if (dayInDataset >= 75 && dayInDataset <= 85) {
552
+ record.semester_end_rush = true;
553
+ } else {
554
+ record.semester_end_rush = false;
555
+ }
556
+ }
557
+ }
558
+ }
559
+
560
+ // ═══════════════════════════════════════════════════════════════════
561
+ // Hook #8: PLAYBACK SPEED CORRELATION
562
+ // ═══════════════════════════════════════════════════════════════════
563
+ if (type === "event") {
564
+ if (record.event === "lecture completed") {
565
+ const speed = record.playback_speed;
566
+
567
+ if (speed >= 2.0) {
568
+ record.speed_learner = true;
569
+ record.thorough_learner = false;
570
+ // Compress watch time for speed learners
571
+ if (record.watch_time_mins !== undefined) {
572
+ record.watch_time_mins = Math.max(3, Math.floor(record.watch_time_mins * 0.6));
573
+ }
574
+ } else if (speed !== undefined && speed <= 1.0) {
575
+ record.speed_learner = false;
576
+ record.thorough_learner = true;
577
+ // Extend watch time for thorough learners
578
+ if (record.watch_time_mins !== undefined) {
579
+ record.watch_time_mins = Math.min(90, Math.floor(record.watch_time_mins * 1.4));
580
+ }
581
+ } else {
582
+ record.speed_learner = false;
583
+ record.thorough_learner = false;
584
+ }
585
+ }
586
+ }
587
+
588
+ // ═══════════════════════════════════════════════════════════════════
589
+ // Hook #3: NOTES-TAKERS SUCCEED
590
+ // Hook #4: STUDY GROUP RETENTION
591
+ // Hook #7: FREE VS PAID (funnel-pre handled below)
592
+ // ═══════════════════════════════════════════════════════════════════
593
+ if (type === "everything") {
594
+ const userEvents = record;
595
+ const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
596
+
597
+ // ---------------------------------------------------------------
598
+ // First pass: identify user patterns
599
+ // ---------------------------------------------------------------
600
+ let notesTakenCount = 0;
601
+ let joinedStudyGroupEarly = false;
602
+ let hasLowQuizScore = false;
603
+
604
+ userEvents.forEach((event) => {
605
+ const eventTime = dayjs(event.time);
606
+ const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
607
+
608
+ // Hook #3: Count lecture_completed events where notes_taken === true
609
+ if (event.event === "lecture completed" && event.notes_taken === true) {
610
+ notesTakenCount++;
611
+ }
612
+
613
+ // Hook #4: Check if user joined a study group within the first 10 days
614
+ if (event.event === "study group joined" && daysSinceStart <= 10) {
615
+ joinedStudyGroupEarly = true;
616
+ }
617
+
618
+ // Hook #4: Check for any quiz_completed with score < 60
619
+ if (event.event === "quiz completed" && event.score_percent < 60) {
620
+ hasLowQuizScore = true;
621
+ }
622
+ });
623
+
624
+ // ---------------------------------------------------------------
625
+ // Second pass: modify events based on patterns
626
+ // ---------------------------------------------------------------
627
+
628
+ // Hook #3: NOTES-TAKERS SUCCEED
629
+ if (notesTakenCount >= 5) {
630
+ userEvents.forEach((event, idx) => {
631
+ // Boost quiz scores for diligent note-takers
632
+ if (event.event === "quiz completed") {
633
+ if (event.score_percent !== undefined) {
634
+ event.score_percent = Math.min(100, event.score_percent + 20);
635
+ }
636
+ event.diligent_student = true;
637
+ }
638
+ });
639
+
640
+ // 40% chance to splice in an extra certificate_earned event
641
+ if (chance.bool({ likelihood: 40 })) {
642
+ const lastEvent = userEvents[userEvents.length - 1];
643
+ if (lastEvent) {
644
+ const certEvent = {
645
+ event: "certificate earned",
646
+ time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
647
+ user_id: lastEvent.user_id,
648
+ course_id: chance.pickone(courseIds),
649
+ completion_time_days: chance.integer({ min: 14, max: 90 }),
650
+ final_grade: chance.integer({ min: 80, max: 100 }),
651
+ diligent_student: true,
652
+ };
653
+ userEvents.push(certEvent);
654
+ }
655
+ }
656
+ }
657
+
658
+ // Hook #8 (everything pass): Speed learners (3+ lectures at 2.0x) get higher quiz scores
659
+ let speedLectureCount = 0;
660
+ userEvents.forEach((event) => {
661
+ if (event.event === "lecture completed" && event.speed_learner === true) {
662
+ speedLectureCount++;
663
+ }
664
+ });
665
+ const isSpeedLearner = speedLectureCount >= 3;
666
+
667
+ if (isSpeedLearner) {
668
+ userEvents.forEach((event) => {
669
+ if (event.event === "quiz completed") {
670
+ if (event.score_percent !== undefined) {
671
+ event.score_percent = Math.min(100, event.score_percent + 8);
672
+ event.speed_learner_effect = true;
673
+ }
674
+ }
675
+ });
676
+ }
677
+
678
+ // Hook #6: SEMESTER-END SPIKE - duplicate assessment events in the spike window
679
+ const duplicates = [];
680
+ userEvents.forEach((event) => {
681
+ if (event.semester_end_rush === true && chance.bool({ likelihood: 80 })) {
682
+ const dup = JSON.parse(JSON.stringify(event));
683
+ dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
684
+ dup.semester_end_rush = true;
685
+ duplicates.push(dup);
686
+ }
687
+ });
688
+ if (duplicates.length > 0) {
689
+ userEvents.push(...duplicates);
690
+ }
691
+
692
+ // Hook #7: FREE VS PAID - reinforce the subscription effect on certificates
693
+ const subStatus = meta && meta.profile ? meta.profile.subscription_status : "free";
694
+ if (subStatus === "free") {
695
+ // Free users lose 55% of their certificates (simulating lower completion)
696
+ for (let i = userEvents.length - 1; i >= 0; i--) {
697
+ if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 55 })) {
698
+ userEvents.splice(i, 1);
699
+ }
700
+ }
701
+ }
702
+
703
+ // Hook #4: STUDY GROUP RETENTION (runs LAST to ensure churn removal isn't undone by later hooks)
704
+ if (!joinedStudyGroupEarly && hasLowQuizScore) {
705
+ // Non-joiners with low scores: remove ALL events after day 14 from their first event (hard churn)
706
+ const churnCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
707
+ for (let i = userEvents.length - 1; i >= 0; i--) {
708
+ const evt = userEvents[i];
709
+ if (churnCutoff && dayjs(evt.time).isAfter(churnCutoff)) {
710
+ userEvents.splice(i, 1);
711
+ }
712
+ }
713
+ } else if (joinedStudyGroupEarly) {
714
+ // Study group joiners keep all events and get bonus discussion_posted events
715
+ const lastEvent = userEvents[userEvents.length - 1];
716
+ if (lastEvent && chance.bool({ likelihood: 60 })) {
717
+ const bonusDiscussion = {
718
+ event: "discussion posted",
719
+ time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
720
+ user_id: lastEvent.user_id,
721
+ course_id: chance.pickone(courseIds),
722
+ post_type: chance.pickone(["question", "answer", "comment"]),
723
+ word_count: chance.integer({ min: 20, max: 400 }),
724
+ study_group_member: true,
725
+ };
726
+ userEvents.push(bonusDiscussion);
727
+ }
728
+ }
729
+
730
+ // Hook #2b: DEADLINE CRAMMING (quiz score penalty)
731
+ // Applied LAST to avoid selection bias — if applied before churn,
732
+ // the penalty pushes Sun/Mon quiz-takers below the hasLowQuizScore
733
+ // threshold, selectively churning them and inflating the avg.
734
+ userEvents.forEach((event) => {
735
+ if (event.event === "quiz completed" && event.time) {
736
+ const eventDay = dayjs(event.time).day();
737
+ if (eventDay === 0 || eventDay === 1) {
738
+ if (event.score_percent !== undefined) {
739
+ event.score_percent = Math.max(0, event.score_percent - 25);
740
+ }
741
+ }
742
+ }
743
+ });
744
+ }
745
+
746
+ // ═══════════════════════════════════════════════════════════════════
747
+ // Hook #7: FREE VS PAID COURSES (funnel-pre)
748
+ // ═══════════════════════════════════════════════════════════════════
749
+ if (type === "funnel-pre") {
750
+ // Target funnels containing course completion events
751
+ if (meta && meta.profile && meta.funnel) {
752
+ const subscriptionStatus = meta.profile.subscription_status;
753
+
754
+ if (subscriptionStatus === "free") {
755
+ // Free users convert at 0.5x rate
756
+ record.conversionRate = (record.conversionRate || 0.25) * 0.5;
757
+ } else if (subscriptionStatus === "monthly" || subscriptionStatus === "annual") {
758
+ // Paid subscribers convert at 1.5x rate
759
+ record.conversionRate = (record.conversionRate || 0.25) * 1.5;
760
+ }
761
+ }
762
+ }
763
+
764
+ return record;
765
+ }
766
+ };
767
+
768
+ export default config;