@ak--47/dungeon-master 1.0.0 → 1.2.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 (104) hide show
  1. package/README.md +7 -7
  2. package/dungeons/technical/ad-spend-schema.json +128 -0
  3. package/dungeons/technical/ad-spend.js +116 -0
  4. package/dungeons/technical/anonymous-users-schema.json +92 -0
  5. package/dungeons/technical/anonymous-users.js +102 -0
  6. package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
  7. package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
  8. package/dungeons/technical/experiments-schema.json +203 -0
  9. package/dungeons/technical/experiments.js +168 -0
  10. package/dungeons/technical/foobar-schema.json +362 -0
  11. package/dungeons/{foobar.js → technical/foobar.js} +3 -3
  12. package/dungeons/technical/group-analytics-schema.json +241 -0
  13. package/dungeons/technical/group-analytics.js +151 -0
  14. package/dungeons/technical/mirror-strategies-schema.json +84 -0
  15. package/dungeons/technical/mirror-strategies.js +126 -0
  16. package/dungeons/technical/nested-objects-schema.json +145 -0
  17. package/dungeons/technical/nested-objects.js +207 -0
  18. package/dungeons/technical/retention-cadence-schema.json +37 -0
  19. package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
  20. package/dungeons/technical/sanity-schema.json +185 -0
  21. package/dungeons/{sanity.js → technical/sanity.js} +4 -2
  22. package/dungeons/technical/scale-test-schema.json +70 -0
  23. package/dungeons/technical/scale-test.js +90 -0
  24. package/dungeons/technical/scd-schema.json +467 -0
  25. package/dungeons/{scd.js → technical/scd.js} +20 -10
  26. package/dungeons/technical/simple-schema.json +362 -0
  27. package/dungeons/{simple.js → technical/simple.js} +7 -7
  28. package/dungeons/technical/simplest-schema.json +724 -0
  29. package/dungeons/{simplest.js → technical/simplest.js} +7 -7
  30. package/dungeons/technical/text-generation-schema.json +1062 -0
  31. package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
  32. package/dungeons/vertical/community-schema.json +579 -0
  33. package/dungeons/vertical/community.js +794 -0
  34. package/dungeons/vertical/devtools-schema.json +601 -0
  35. package/dungeons/vertical/devtools.js +843 -0
  36. package/dungeons/vertical/ecommerce-schema.json +604 -0
  37. package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
  38. package/dungeons/vertical/education-schema.json +5686 -0
  39. package/dungeons/{education.js → vertical/education.js} +70 -39
  40. package/dungeons/vertical/fintech-schema.json +630 -0
  41. package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
  42. package/dungeons/vertical/fitness-schema.json +530 -0
  43. package/dungeons/vertical/fitness.js +808 -0
  44. package/dungeons/vertical/food-delivery-schema.json +36728 -0
  45. package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
  46. package/dungeons/vertical/gaming-schema.json +438 -0
  47. package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
  48. package/dungeons/vertical/healthcare-schema.json +549 -0
  49. package/dungeons/vertical/healthcare.js +775 -0
  50. package/dungeons/vertical/insurance-application-schema.json +485 -0
  51. package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
  52. package/dungeons/vertical/logistics-schema.json +574 -0
  53. package/dungeons/vertical/logistics.js +799 -0
  54. package/dungeons/vertical/marketplace-schema.json +533 -0
  55. package/dungeons/vertical/marketplace.js +850 -0
  56. package/dungeons/vertical/media-schema.json +4749 -0
  57. package/dungeons/{media.js → vertical/media.js} +103 -19
  58. package/dungeons/vertical/rpg-schema.json +2491 -0
  59. package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
  60. package/dungeons/vertical/sass-schema.json +3128 -0
  61. package/dungeons/{sass.js → vertical/sass.js} +65 -26
  62. package/dungeons/vertical/social-schema.json +620 -0
  63. package/dungeons/{social.js → vertical/social.js} +76 -17
  64. package/dungeons/vertical/travel-schema.json +580 -0
  65. package/dungeons/vertical/travel.js +742 -0
  66. package/index.js +20 -16
  67. package/lib/core/config-validator.js +286 -1
  68. package/lib/core/context.js +16 -5
  69. package/lib/core/storage.js +2 -0
  70. package/lib/generators/adspend.js +3 -3
  71. package/lib/generators/events.js +141 -14
  72. package/lib/generators/funnels.js +67 -7
  73. package/lib/generators/text.js +2 -2
  74. package/lib/orchestrators/user-loop.js +444 -22
  75. package/lib/templates/soup-presets.js +2 -2
  76. package/lib/utils/utils.js +4 -5
  77. package/package.json +4 -3
  78. package/scripts/dungeon-to-json.mjs +163 -0
  79. package/scripts/extract-dungeon-schema.mjs +343 -0
  80. package/scripts/json-to-dungeon.mjs +200 -0
  81. package/scripts/run-dungeon.mjs +76 -0
  82. package/scripts/run-many.mjs +118 -0
  83. package/scripts/verify-runner.mjs +42 -0
  84. package/types.d.ts +316 -1
  85. package/dungeons/array-of-object-lookup-schema.json +0 -327
  86. package/dungeons/ecommerce-schema.json +0 -462
  87. package/dungeons/education-schema.json +0 -2409
  88. package/dungeons/fintech-schema.json +0 -14034
  89. package/dungeons/foobar-schema.json +0 -403
  90. package/dungeons/food-delivery-schema.json +0 -192
  91. package/dungeons/food-delivery.js +0 -602
  92. package/dungeons/food-schema.json +0 -1152
  93. package/dungeons/gaming-schema.json +0 -1270
  94. package/dungeons/insurance-application-schema.json +0 -204
  95. package/dungeons/media-schema.json +0 -906
  96. package/dungeons/retention-cadence-schema.json +0 -78
  97. package/dungeons/rpg-schema.json +0 -4526
  98. package/dungeons/sanity-schema.json +0 -255
  99. package/dungeons/sass-schema.json +0 -1291
  100. package/dungeons/scd-schema.json +0 -919
  101. package/dungeons/simple-schema.json +0 -608
  102. package/dungeons/simplest-schema.json +0 -1418
  103. package/dungeons/social-schema.json +0 -1118
  104. package/dungeons/text-generation-schema.json +0 -3096
@@ -0,0 +1,775 @@
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 = "dm4-healthcare";
8
+ dayjs.extend(utc);
9
+ const chance = u.initChance(SEED);
10
+ const num_users = 5_000;
11
+ const days = 100;
12
+ const NOW = dayjs();
13
+ const DATASET_START = NOW.subtract(days, "days");
14
+
15
+ /** @typedef {import("../../types").Dungeon} Config */
16
+
17
+ // Generate consistent doctor/clinic IDs at module level
18
+ const doctorIds = v.range(1, 120).map(() => `DR_${v.uid(6)}`);
19
+ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
20
+
21
+ /**
22
+ * ═══════════════════════════════════════════════════════════════
23
+ * DATASET OVERVIEW
24
+ * ═══════════════════════════════════════════════════════════════
25
+ *
26
+ * MedConnect — a telehealth platform connecting doctors, nurses,
27
+ * and patients through virtual consultations, prescriptions,
28
+ * and secure messaging.
29
+ *
30
+ * - 5,000 users over 100 days, ~600K events
31
+ * - Multi-role system: doctors (5%), nurses (10%), patients (85%)
32
+ * - Core loop: sign up → book appointment → consultation → prescription → follow-up
33
+ * - Revenue: free (2 visits/month), basic ($9.99, 14-day trial), premium ($29.99 unlimited)
34
+ *
35
+ * Advanced Features:
36
+ * - Personas: 5 archetypes (doctor, nurse, patient_active, patient_occasional, patient_churner)
37
+ * - Subscription: 3-tier revenue lifecycle with trial conversion
38
+ * - Geo: US/EU/LATAM with timezone-aware scheduling and regional compliance
39
+ * - Features: video_consultation (day 30) and ai_symptom_checker (day 60)
40
+ *
41
+ * Key entities:
42
+ * - doctor_id: assigned doctor for a consultation
43
+ * - clinic_id: virtual clinic grouping
44
+ * - condition_type: medical specialty area
45
+ * - consultation_mode: phone vs video (driven by Feature rollout)
46
+ */
47
+
48
+ /**
49
+ * ═══════════════════════════════════════════════════════════════
50
+ * ANALYTICS HOOKS (8 hooks)
51
+ * ═══════════════════════════════════════════════════════════════
52
+ *
53
+ * ───────────────────────────────────────────────────────────────
54
+ * 1. AFTER-HOURS SURGE PRICING (event hook)
55
+ * ───────────────────────────────────────────────────────────────
56
+ *
57
+ * PATTERN: Consultations between 7PM-7AM (after-hours) have 1.5x
58
+ * higher consultation_fee. Simulates urgent care premium pricing.
59
+ *
60
+ * HOW TO FIND IT IN MIXPANEL:
61
+ *
62
+ * Report 1: After-Hours Fee Premium
63
+ * • Report type: Insights
64
+ * • Event: "consultation completed"
65
+ * • Measure: Average of "consultation_fee"
66
+ * • Breakdown: "after_hours"
67
+ * • Expected: after_hours=true should show ~1.5x avg fee vs false
68
+ * (true ≈ $112, false ≈ $75)
69
+ *
70
+ * REAL-WORLD ANALOGUE: Telehealth platforms charge premiums for
71
+ * after-hours urgent consultations, a key revenue driver.
72
+ *
73
+ * ───────────────────────────────────────────────────────────────
74
+ * 2. FLU SEASON SPIKE (event hook)
75
+ * ───────────────────────────────────────────────────────────────
76
+ *
77
+ * PATTERN: During days 50-70 (flu season window), appointments
78
+ * with condition_type "respiratory" get 2x the wait_time and the
79
+ * condition is forced to "respiratory" 60% of the time.
80
+ *
81
+ * HOW TO FIND IT IN MIXPANEL:
82
+ *
83
+ * Report 1: Flu Season Volume
84
+ * • Report type: Insights
85
+ * • Event: "appointment booked"
86
+ * • Measure: Total
87
+ * • Filter: condition_type = "respiratory"
88
+ * • Line chart by week
89
+ * • Expected: Clear spike during flu season window (days 50-70)
90
+ *
91
+ * Report 2: Wait Time During Flu Season
92
+ * • Report type: Insights
93
+ * • Event: "appointment booked"
94
+ * • Measure: Average of "wait_time_hours"
95
+ * • Breakdown: "condition_type"
96
+ * • Filter: time within flu season
97
+ * • Expected: respiratory ~2x wait vs other conditions
98
+ *
99
+ * REAL-WORLD ANALOGUE: Seasonal illness creates predictable surges
100
+ * in appointment demand and wait times.
101
+ *
102
+ * ───────────────────────────────────────────────────────────────
103
+ * 3. EXPERIENCED DOCTOR SATISFACTION (everything hook)
104
+ * ───────────────────────────────────────────────────────────────
105
+ *
106
+ * PATTERN: Users who had >50 consultation events get higher avg
107
+ * satisfaction_score (boosted to 4.0-5.0 range) vs baseline 1-5.
108
+ * Simulates experienced doctors earning better reviews.
109
+ *
110
+ * HOW TO FIND IT IN MIXPANEL:
111
+ *
112
+ * Report 1: Satisfaction by Consultation Volume
113
+ * • Report type: Insights
114
+ * • Event: "consultation completed"
115
+ * • Measure: Average of "satisfaction_score"
116
+ * • Breakdown: user property "role"
117
+ * • Expected: Doctors (high volume users) show ~4.2 avg vs ~3.5 baseline
118
+ *
119
+ * REAL-WORLD ANALOGUE: Experienced providers develop better bedside
120
+ * manner and patient communication skills over time.
121
+ *
122
+ * ───────────────────────────────────────────────────────────────
123
+ * 4. VIDEO CONSULTATION FOLLOW-UP LIFT (everything hook)
124
+ * ───────────────────────────────────────────────────────────────
125
+ *
126
+ * PATTERN: Patients whose consultations used video mode (vs phone)
127
+ * get 2x more follow-up appointment events injected. Cloned from
128
+ * existing "follow up scheduled" events.
129
+ *
130
+ * HOW TO FIND IT IN MIXPANEL:
131
+ *
132
+ * Report 1: Follow-Up Rate by Consultation Mode
133
+ * • Report type: Insights
134
+ * • Event: "follow up scheduled"
135
+ * • Measure: Total per user
136
+ * • Breakdown: "consultation_mode" (from consultation completed)
137
+ * • Expected: video users ~2x more follow-ups than phone users
138
+ *
139
+ * REAL-WORLD ANALOGUE: Face-to-face (video) consultations build
140
+ * stronger patient-doctor rapport, increasing follow-up compliance.
141
+ *
142
+ * ───────────────────────────────────────────────────────────────
143
+ * 5. CHRONIC CONDITION REFILL CHAIN (everything hook)
144
+ * ───────────────────────────────────────────────────────────────
145
+ *
146
+ * PATTERN: Patients with condition_type "chronic" on any prescription
147
+ * event get additional cloned prescription_refill events injected
148
+ * every ~30 days after the original. Creates periodic refill cadence.
149
+ *
150
+ * HOW TO FIND IT IN MIXPANEL:
151
+ *
152
+ * Report 1: Refill Volume by Condition
153
+ * • Report type: Insights
154
+ * • Event: "prescription refill"
155
+ * • Measure: Total
156
+ * • Breakdown: "condition_type"
157
+ * • Expected: "chronic" should have ~3-4x more refills than others
158
+ *
159
+ * REAL-WORLD ANALOGUE: Chronic conditions (diabetes, hypertension)
160
+ * require ongoing prescriptions creating predictable refill revenue.
161
+ *
162
+ * ───────────────────────────────────────────────────────────────
163
+ * 6. OCCASIONAL PATIENT NO-SHOWS (everything hook)
164
+ * ───────────────────────────────────────────────────────────────
165
+ *
166
+ * PATTERN: Users with role "patient" and low event count (<15 events)
167
+ * have 25% of their "appointment booked" events dropped. Simulates
168
+ * occasional patients who book but don't show up.
169
+ *
170
+ * HOW TO FIND IT IN MIXPANEL:
171
+ *
172
+ * Report 1: Appointment-to-Consultation Ratio
173
+ * • Report type: Funnels
174
+ * • Steps: "appointment booked" → "consultation completed"
175
+ * • Breakdown: user property "role"
176
+ * • Expected: patients with fewer events convert ~75% vs ~95% for active
177
+ *
178
+ * REAL-WORLD ANALOGUE: Infrequent patients have higher no-show rates,
179
+ * a major operational cost for healthcare providers.
180
+ *
181
+ * ───────────────────────────────────────────────────────────────
182
+ * 7. DOCTOR PROFILE SPECIALIZATION (user hook)
183
+ * ───────────────────────────────────────────────────────────────
184
+ *
185
+ * PATTERN: Users with role "doctor" get specialty set to a specific
186
+ * value (from the existing array) and years_experience boosted to
187
+ * senior range (15-30). Nurses get years_experience in mid range.
188
+ *
189
+ * HOW TO FIND IT IN MIXPANEL:
190
+ *
191
+ * Report 1: Experience Distribution by Role
192
+ * • Report type: Insights
193
+ * • Event: "consultation completed"
194
+ * • Measure: Average of user property "years_experience"
195
+ * • Breakdown: user property "role"
196
+ * • Expected: doctors ≈ 22 years, nurses ≈ 8, patients ≈ 0
197
+ *
198
+ * REAL-WORLD ANALOGUE: Provider profiles have specialized expertise
199
+ * and experience levels that affect patient matching.
200
+ *
201
+ * ───────────────────────────────────────────────────────────────
202
+ * 8. PREMIUM BOOKING FUNNEL LIFT (funnel-pre hook)
203
+ * ───────────────────────────────────────────────────────────────
204
+ *
205
+ * PATTERN: Users with subscription_tier "premium" get 1.4x conversion
206
+ * rate on the booking funnel. Premium patients have priority scheduling.
207
+ *
208
+ * HOW TO FIND IT IN MIXPANEL:
209
+ *
210
+ * Report 1: Booking Conversion by Tier
211
+ * • Report type: Funnels
212
+ * • Steps: "symptom search" → "appointment booked" → "consultation completed"
213
+ * • Breakdown: "subscription_tier" (superProp)
214
+ * • Expected: premium ≈ 56% vs free ≈ 40% conversion
215
+ *
216
+ * REAL-WORLD ANALOGUE: Premium telehealth subscribers get priority
217
+ * scheduling and shorter wait times, improving conversion.
218
+ *
219
+ * ═══════════════════════════════════════════════════════════════
220
+ * EXPECTED METRICS SUMMARY
221
+ * ═══════════════════════════════════════════════════════════════
222
+ *
223
+ * Hook | Metric | Baseline | Effect | Ratio
224
+ * ────────────────────────────|─────────────────────|──────────|─────────|──────
225
+ * After-Hours Pricing | consultation_fee | $75 | $112 | 1.5x
226
+ * Flu Season Spike | respiratory appts | ~15% | ~60% | 4x
227
+ * Experienced Doctor Sat. | satisfaction_score | 3.5 | 4.2 | 1.2x
228
+ * Video Follow-Up Lift | follow-ups/user | 1.0 | 2.0 | 2x
229
+ * Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
230
+ * Occasional No-Shows | booking→consult | 95% | 75% | 0.79x
231
+ * Doctor Specialization | years_experience | 5 | 22 | 4.4x
232
+ * Premium Booking Lift | funnel conversion | 40% | 56% | 1.4x
233
+ */
234
+
235
+ /** @type {Config} */
236
+ const config = {
237
+ token: "",
238
+ seed: SEED,
239
+ numDays: days,
240
+ numEvents: num_users * 120,
241
+ numUsers: num_users,
242
+ hasAnonIds: false,
243
+ hasSessionIds: true,
244
+ format: "json",
245
+ gzip: true,
246
+ alsoInferFunnels: false,
247
+ hasLocation: true,
248
+ hasAndroidDevices: true,
249
+ hasIOSDevices: true,
250
+ hasDesktopDevices: true,
251
+ hasBrowser: false,
252
+ hasCampaigns: false,
253
+ isAnonymous: false,
254
+ hasAdSpend: false,
255
+ percentUsersBornInDataset: 35,
256
+ hasAvatar: true,
257
+ concurrency: 1,
258
+ writeToDisk: false,
259
+ scdProps: {
260
+ care_plan: {
261
+ values: ["preventive", "routine", "chronic", "acute"],
262
+ frequency: "month",
263
+ timing: "fuzzy",
264
+ max: 8
265
+ }
266
+ },
267
+ mirrorProps: {},
268
+ lookupTables: [],
269
+
270
+ // ── Events (18) ──────────────────────────────────────────
271
+ events: [
272
+ {
273
+ event: "account created",
274
+ weight: 1,
275
+ isFirstEvent: true,
276
+ properties: {
277
+ referral_source: ["organic", "doctor_referral", "insurance_partner", "social_media", "search"],
278
+ },
279
+ },
280
+ {
281
+ event: "symptom search",
282
+ weight: 7,
283
+ properties: {
284
+ search_term: ["headache", "fever", "cough", "back pain", "fatigue", "anxiety", "rash", "nausea", "chest pain", "joint pain"],
285
+ results_count: u.weighNumRange(0, 25, 0.5),
286
+ },
287
+ },
288
+ {
289
+ event: "appointment booked",
290
+ weight: 6,
291
+ properties: {
292
+ doctor_id: chance.pickone.bind(chance, doctorIds),
293
+ clinic_id: chance.pickone.bind(chance, clinicIds),
294
+ condition_type: ["general", "general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
295
+ wait_time_hours: u.weighNumRange(1, 72, 0.4),
296
+ appointment_type: ["new_patient", "follow_up", "follow_up", "urgent", "routine", "routine"],
297
+ after_hours: [false],
298
+ },
299
+ },
300
+ {
301
+ event: "consultation completed",
302
+ weight: 5,
303
+ properties: {
304
+ doctor_id: chance.pickone.bind(chance, doctorIds),
305
+ consultation_mode: ["phone"], // default; feature rollout changes to video
306
+ duration_minutes: u.weighNumRange(5, 60, 0.6, 15),
307
+ consultation_fee: u.weighNumRange(25, 200, 0.4, 75),
308
+ satisfaction_score: u.weighNumRange(1, 5, 0.8, 3),
309
+ condition_type: ["general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
310
+ after_hours: [false],
311
+ },
312
+ },
313
+ {
314
+ event: "prescription issued",
315
+ weight: 4,
316
+ properties: {
317
+ medication_type: ["antibiotic", "antiviral", "painkiller", "anti_inflammatory", "antidepressant", "inhaler", "topical", "chronic_maintenance"],
318
+ quantity: u.weighNumRange(1, 90, 0.3, 30),
319
+ condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "chronic", "pediatric"],
320
+ refill_count: u.weighNumRange(0, 3),
321
+ },
322
+ },
323
+ {
324
+ event: "prescription refill",
325
+ weight: 3,
326
+ properties: {
327
+ medication_type: ["antibiotic", "antiviral", "painkiller", "anti_inflammatory", "antidepressant", "inhaler", "topical", "chronic_maintenance"],
328
+ quantity: u.weighNumRange(1, 90, 0.3, 30),
329
+ condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "chronic", "pediatric"],
330
+ refill_count: u.weighNumRange(1, 6),
331
+ },
332
+ },
333
+ {
334
+ event: "follow up scheduled",
335
+ weight: 3,
336
+ properties: {
337
+ doctor_id: chance.pickone.bind(chance, doctorIds),
338
+ days_until_followup: u.weighNumRange(3, 30, 0.5, 7),
339
+ condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
340
+ consultation_mode: ["phone"],
341
+ },
342
+ },
343
+ {
344
+ event: "message sent",
345
+ weight: 5,
346
+ properties: {
347
+ message_type: ["question", "question", "update", "result_inquiry", "prescription_question", "scheduling"],
348
+ recipient_role: ["doctor", "doctor", "nurse", "support"],
349
+ response_time_hours: u.weighNumRange(0.1, 48, 0.3, 4),
350
+ },
351
+ },
352
+ {
353
+ event: "lab results viewed",
354
+ weight: 3,
355
+ properties: {
356
+ test_type: ["blood_panel", "urinalysis", "imaging", "allergy_test", "metabolic_panel", "thyroid"],
357
+ result_status: ["normal", "normal", "normal", "abnormal", "pending"],
358
+ },
359
+ },
360
+ {
361
+ event: "health record accessed",
362
+ weight: 4,
363
+ properties: {
364
+ record_type: ["visit_summary", "lab_results", "prescriptions", "immunizations", "billing"],
365
+ access_method: ["app", "app", "web_portal"],
366
+ },
367
+ },
368
+ {
369
+ event: "insurance verified",
370
+ weight: 2,
371
+ properties: {
372
+ insurance_type: ["private", "private", "employer", "medicare", "medicaid", "self_pay"],
373
+ verification_status: ["approved", "approved", "approved", "pending", "denied"],
374
+ copay_amount: u.weighNumRange(0, 75, 0.5, 20),
375
+ },
376
+ },
377
+ {
378
+ event: "payment processed",
379
+ weight: 3,
380
+ properties: {
381
+ amount: u.weighNumRange(10, 500, 0.3, 75),
382
+ payment_method: ["credit_card", "credit_card", "insurance_claim", "hsa_fsa", "debit"],
383
+ payment_status: ["success", "success", "success", "success", "failed"],
384
+ },
385
+ },
386
+ {
387
+ event: "notification received",
388
+ weight: 6,
389
+ properties: {
390
+ notification_type: ["appointment_reminder", "appointment_reminder", "lab_ready", "prescription_ready", "message_received", "billing"],
391
+ channel: ["push", "push", "email", "sms"],
392
+ opened: [true, true, true, false],
393
+ },
394
+ },
395
+ {
396
+ event: "provider rated",
397
+ weight: 2,
398
+ properties: {
399
+ doctor_id: chance.pickone.bind(chance, doctorIds),
400
+ rating: u.weighNumRange(1, 5, 0.7, 4),
401
+ would_recommend: [true, true, true, true, false],
402
+ },
403
+ },
404
+ {
405
+ event: "support ticket created",
406
+ weight: 1,
407
+ properties: {
408
+ category: ["billing", "technical", "scheduling", "prescription", "insurance", "other"],
409
+ priority: ["low", "low", "medium", "medium", "high"],
410
+ resolution_hours: u.weighNumRange(1, 96, 0.4, 24),
411
+ },
412
+ },
413
+ {
414
+ event: "profile updated",
415
+ weight: 2,
416
+ properties: {
417
+ field_updated: ["insurance", "address", "phone", "emergency_contact", "allergies", "medications"],
418
+ },
419
+ },
420
+ {
421
+ event: "app session",
422
+ weight: 8,
423
+ properties: {
424
+ session_duration_sec: u.weighNumRange(10, 1800, 0.4, 120),
425
+ pages_viewed: u.weighNumRange(1, 15, 0.5, 3),
426
+ },
427
+ },
428
+ {
429
+ event: "account deactivated",
430
+ weight: 1,
431
+ isChurnEvent: true,
432
+ returnLikelihood: 0.15,
433
+ isStrictEvent: true,
434
+ properties: {
435
+ reason: ["switched_provider", "cost", "no_longer_needed", "poor_experience", "insurance_change"],
436
+ },
437
+ },
438
+ ],
439
+
440
+ // ── Funnels (5) ──────────────────────────────────────────
441
+ funnels: [
442
+ {
443
+ name: "Onboarding Flow",
444
+ sequence: ["account created", "insurance verified", "symptom search", "appointment booked"],
445
+ conversionRate: 45,
446
+ order: "sequential",
447
+ isFirstFunnel: true,
448
+ timeToConvert: 72,
449
+ weight: 3,
450
+ },
451
+ {
452
+ name: "Booking to Consultation",
453
+ sequence: ["symptom search", "appointment booked", "consultation completed"],
454
+ conversionRate: 40,
455
+ order: "sequential",
456
+ timeToConvert: 48,
457
+ weight: 5,
458
+ },
459
+ {
460
+ name: "Full Care Journey",
461
+ sequence: ["appointment booked", "consultation completed", "prescription issued", "follow up scheduled"],
462
+ conversionRate: 30,
463
+ order: "sequential",
464
+ timeToConvert: 168,
465
+ weight: 3,
466
+ },
467
+ {
468
+ name: "Prescription Lifecycle",
469
+ sequence: ["prescription issued", "prescription refill", "payment processed"],
470
+ conversionRate: 55,
471
+ order: "sequential",
472
+ timeToConvert: 720,
473
+ weight: 2,
474
+ },
475
+ {
476
+ name: "Patient Satisfaction",
477
+ sequence: ["consultation completed", "provider rated", "follow up scheduled"],
478
+ conversionRate: 25,
479
+ order: "sequential",
480
+ timeToConvert: 72,
481
+ weight: 2,
482
+ },
483
+ ],
484
+
485
+ // ── SuperProps ──────────────────────────────────────────
486
+ superProps: {
487
+ subscription_tier: ["free", "free", "free", "basic", "basic", "premium"],
488
+ platform: ["ios", "android", "web"],
489
+ },
490
+
491
+ // ── UserProps ──────────────────────────────────────────
492
+ userProps: {
493
+ role: ["patient", "patient", "patient", "patient", "patient", "patient", "patient", "patient", "nurse", "doctor"],
494
+ specialty: ["none"],
495
+ years_experience: u.weighNumRange(0, 5, 0.5),
496
+ preferred_language: ["en", "en", "en", "en", "es", "pt", "de", "fr"],
497
+ has_chronic_condition: [false, false, false, true],
498
+ age_range: ["18-25", "26-35", "26-35", "36-45", "36-45", "46-55", "56-65", "65+"],
499
+ },
500
+
501
+ // ── Personas ──────────────────────────────────
502
+ personas: [
503
+ {
504
+ name: "doctor",
505
+ weight: 5,
506
+ eventMultiplier: 5.0,
507
+ conversionModifier: 1.8,
508
+ churnRate: 0.01,
509
+ properties: {
510
+ role: "doctor",
511
+ segment: "provider",
512
+ },
513
+ },
514
+ {
515
+ name: "nurse",
516
+ weight: 10,
517
+ eventMultiplier: 3.0,
518
+ conversionModifier: 1.5,
519
+ churnRate: 0.03,
520
+ properties: {
521
+ role: "nurse",
522
+ segment: "provider",
523
+ },
524
+ },
525
+ {
526
+ name: "patient_active",
527
+ weight: 40,
528
+ eventMultiplier: 1.0,
529
+ conversionModifier: 1.0,
530
+ churnRate: 0.05,
531
+ properties: {
532
+ role: "patient",
533
+ segment: "active_patient",
534
+ },
535
+ },
536
+ {
537
+ name: "patient_occasional",
538
+ weight: 30,
539
+ eventMultiplier: 0.5,
540
+ conversionModifier: 0.7,
541
+ churnRate: 0.12,
542
+ properties: {
543
+ role: "patient",
544
+ segment: "occasional_patient",
545
+ },
546
+ },
547
+ {
548
+ name: "patient_churner",
549
+ weight: 15,
550
+ eventMultiplier: 0.3,
551
+ conversionModifier: 0.3,
552
+ churnRate: 0.4,
553
+ properties: {
554
+ role: "patient",
555
+ segment: "churner",
556
+ },
557
+ activeWindow: { maxDays: 21 },
558
+ },
559
+ ],
560
+
561
+ // ── Subscription ──────────────────────────────
562
+ subscription: {
563
+ plans: [
564
+ { name: "free", price: 0, default: true },
565
+ { name: "basic", price: 9.99, trialDays: 14 },
566
+ { name: "premium", price: 29.99 },
567
+ ],
568
+ lifecycle: {
569
+ trialToPayRate: 0.30,
570
+ upgradeRate: 0.08,
571
+ downgradeRate: 0.03,
572
+ churnRate: 0.05,
573
+ winBackRate: 0.10,
574
+ winBackDelay: 21,
575
+ paymentFailureRate: 0.02,
576
+ },
577
+ },
578
+
579
+ // ── Geo ──────────────────────────────────────
580
+ geo: {
581
+ sticky: true,
582
+ regions: [
583
+ {
584
+ name: "us",
585
+ countries: ["US"],
586
+ weight: 50,
587
+ timezoneOffset: -5,
588
+ properties: { currency: "USD", locale: "en-US" },
589
+ },
590
+ {
591
+ name: "eu",
592
+ countries: ["GB", "DE", "FR"],
593
+ weight: 30,
594
+ timezoneOffset: 1,
595
+ properties: { currency: "EUR", locale: "en-EU", gdpr_consent: true },
596
+ },
597
+ {
598
+ name: "latam",
599
+ countries: ["BR", "MX", "AR"],
600
+ weight: 20,
601
+ timezoneOffset: -3,
602
+ properties: { currency: "BRL", locale: "pt-BR" },
603
+ },
604
+ ],
605
+ },
606
+
607
+ // ── Features ──────────────────────────────────
608
+ features: [
609
+ {
610
+ name: "video_consultation",
611
+ launchDay: 30,
612
+ adoptionCurve: "fast",
613
+ property: "consultation_mode",
614
+ values: ["phone", "video"],
615
+ defaultBefore: "phone",
616
+ affectsEvents: ["consultation completed", "follow up scheduled"],
617
+ },
618
+ {
619
+ name: "ai_symptom_checker",
620
+ launchDay: 60,
621
+ adoptionCurve: { k: 0.08, midpoint: 25 },
622
+ property: "symptom_source",
623
+ values: ["manual", "ai_assisted"],
624
+ defaultBefore: "manual",
625
+ affectsEvents: ["symptom search"],
626
+ conversionLift: 1.12,
627
+ },
628
+ ],
629
+
630
+ // ── Hook Function ──────────────────────────────────────
631
+ hook: function (record, type, meta) {
632
+ // ── HOOK 7: DOCTOR PROFILE SPECIALIZATION (user) ─────
633
+ // Doctors get a real specialty and senior years_experience.
634
+ // Nurses get mid-range experience. Patients stay at defaults.
635
+ if (type === "user") {
636
+ if (record.role === "doctor") {
637
+ record.specialty = chance.pickone(["cardiology", "dermatology", "pediatrics", "psychiatry", "general_practice", "pulmonology", "endocrinology"]);
638
+ record.years_experience = chance.integer({ min: 15, max: 30 });
639
+ } else if (record.role === "nurse") {
640
+ record.specialty = chance.pickone(["general_practice", "pediatrics", "emergency"]);
641
+ record.years_experience = chance.integer({ min: 3, max: 15 });
642
+ } else {
643
+ record.years_experience = 0;
644
+ }
645
+ }
646
+
647
+ // ── HOOK 8: PREMIUM BOOKING FUNNEL LIFT (funnel-pre) ─
648
+ // Premium subscribers convert 1.4x better through booking funnel.
649
+ if (type === "funnel-pre") {
650
+ if (meta && meta.profile) {
651
+ const tier = meta.profile.subscription_tier;
652
+ if (tier === "premium") {
653
+ record.conversionRate = Math.min(record.conversionRate * 1.4, 95);
654
+ } else if (tier === "basic") {
655
+ record.conversionRate = Math.min(record.conversionRate * 1.15, 90);
656
+ }
657
+ }
658
+ }
659
+
660
+ // ── HOOK 1: AFTER-HOURS SURGE PRICING (event) ────────
661
+ // Consultations between 7PM-7AM get 1.5x fee.
662
+ if (type === "event") {
663
+ if (record.event === "consultation completed" || record.event === "appointment booked") {
664
+ const hour = dayjs(record.time).hour();
665
+ if (hour >= 19 || hour < 7) {
666
+ record.after_hours = true;
667
+ if (record.consultation_fee) {
668
+ record.consultation_fee = Math.floor(record.consultation_fee * 1.5);
669
+ }
670
+ }
671
+ }
672
+
673
+ // ── HOOK 2: FLU SEASON SPIKE (event) ─────────────
674
+ // Days 50-70: respiratory conditions dominate, wait times double.
675
+ const FLU_START = DATASET_START.add(50, "days");
676
+ const FLU_END = DATASET_START.add(70, "days");
677
+ const eventTime = dayjs(record.time);
678
+ if (record.event === "appointment booked" && eventTime.isAfter(FLU_START) && eventTime.isBefore(FLU_END)) {
679
+ if (chance.bool({ likelihood: 60 })) {
680
+ record.condition_type = "respiratory";
681
+ }
682
+ if (record.condition_type === "respiratory") {
683
+ record.wait_time_hours = Math.floor((record.wait_time_hours || 12) * 2);
684
+ }
685
+ }
686
+ }
687
+
688
+ // ── EVERYTHING HOOKS ─────────────────────────────────
689
+ if (type === "everything") {
690
+ const events = record;
691
+ if (!events.length) return record;
692
+
693
+ // ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
694
+ // Users with >50 consultation events get boosted satisfaction scores.
695
+ let consultCount = 0;
696
+ events.forEach(e => {
697
+ if (e.event === "consultation completed") consultCount++;
698
+ });
699
+
700
+ if (consultCount > 50) {
701
+ events.forEach(e => {
702
+ if (e.event === "consultation completed") {
703
+ e.satisfaction_score = chance.floating({ min: 4.0, max: 5.0, fixed: 1 });
704
+ }
705
+ });
706
+ }
707
+
708
+ // ── HOOK 4: VIDEO CONSULTATION FOLLOW-UP LIFT ────
709
+ // Patients with video consultations get 2x follow-up events.
710
+ const hasVideoConsult = events.some(e =>
711
+ e.event === "consultation completed" && e.consultation_mode === "video"
712
+ );
713
+ if (hasVideoConsult) {
714
+ const templateFollowUp = events.find(e => e.event === "follow up scheduled");
715
+ if (templateFollowUp) {
716
+ const videoConsults = events.filter(e =>
717
+ e.event === "consultation completed" && e.consultation_mode === "video"
718
+ );
719
+ videoConsults.forEach(vc => {
720
+ if (chance.bool({ likelihood: 60 })) {
721
+ events.push({
722
+ ...templateFollowUp,
723
+ time: dayjs(vc.time).add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
724
+ user_id: vc.user_id,
725
+ consultation_mode: "video",
726
+ days_until_followup: chance.integer({ min: 3, max: 14 }),
727
+ });
728
+ }
729
+ });
730
+ }
731
+ }
732
+
733
+ // ── HOOK 5: CHRONIC CONDITION REFILL CHAIN ───────
734
+ // Patients with chronic prescriptions get refills every ~30 days.
735
+ const chronicRxs = events.filter(e =>
736
+ e.event === "prescription issued" && e.condition_type === "chronic"
737
+ );
738
+ if (chronicRxs.length > 0) {
739
+ const templateRefill = events.find(e => e.event === "prescription refill");
740
+ if (templateRefill) {
741
+ chronicRxs.forEach(rx => {
742
+ const rxTime = dayjs(rx.time);
743
+ const refillsToAdd = chance.integer({ min: 2, max: 4 });
744
+ for (let i = 1; i <= refillsToAdd; i++) {
745
+ events.push({
746
+ ...templateRefill,
747
+ time: rxTime.add(30 * i + chance.integer({ min: -3, max: 3 }), "days").toISOString(),
748
+ user_id: rx.user_id,
749
+ condition_type: "chronic",
750
+ medication_type: "chronic_maintenance",
751
+ refill_count: i,
752
+ });
753
+ }
754
+ });
755
+ }
756
+ }
757
+
758
+ // ── HOOK 6: OCCASIONAL PATIENT NO-SHOWS ──────────
759
+ // Low-activity patients (< 15 events) lose 25% of appointments.
760
+ if (events.length < 15) {
761
+ for (let i = events.length - 1; i >= 0; i--) {
762
+ if (events[i].event === "appointment booked" && chance.bool({ likelihood: 25 })) {
763
+ events.splice(i, 1);
764
+ }
765
+ }
766
+ }
767
+
768
+ return record;
769
+ }
770
+
771
+ return record;
772
+ },
773
+ };
774
+
775
+ export default config;