@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,945 +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
- import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
9
- /** @typedef {import("../../types").Dungeon} Config */
10
-
11
- // ── OVERVIEW ──
12
- /*
13
- * NAME: MedConnect
14
- * APP: Telehealth platform connecting doctors, nurses, and patients
15
- * through virtual consultations, prescriptions, and secure
16
- * messaging. Multi-role system; subscription tiers (free/basic/
17
- * premium); feature rollouts for video consultation and AI
18
- * symptom checker; geo-aware (US/EU/LATAM).
19
- * SCALE: 10,000 users, ~1.2M events, 121 days (2026-01-01 → 2026-05-01)
20
- * CORE LOOP: sign up → symptom search → book appointment → consultation → prescription → follow-up
21
- *
22
- * EVENTS (18):
23
- * app session (8) > symptom search (7) > appointment booked (6) > notification received (6)
24
- * > consultation completed (5) > message sent (5) > prescription issued (4)
25
- * > health record accessed (4) > prescription refill (3) > follow up scheduled (3)
26
- * > lab results viewed (3) > payment processed (3) > insurance verified (2)
27
- * > provider rated (2) > profile updated (2) > account created (1)
28
- * > support ticket created (1) > account deactivated (1)
29
- *
30
- * FUNNELS (5):
31
- * - Onboarding Flow: account created → insurance verified → symptom search → appointment booked (45%)
32
- * - Booking to Consultation: symptom search → appointment booked → consultation completed (40%)
33
- * - Full Care Journey: appointment booked → consultation completed → prescription issued → follow up scheduled (30%)
34
- * - Prescription Lifecycle: prescription issued → prescription refill → payment processed (55%)
35
- * - Patient Satisfaction: consultation completed → provider rated → follow up scheduled (25%)
36
- *
37
- * USER PROPS: role, specialty, years_experience, preferred_language, has_chronic_condition, age_range, subscription_tier, Platform
38
- * SUPER PROPS: subscription_tier, Platform
39
- * SCD PROPS: care_plan (preventive/routine/chronic/acute, monthly fuzzy, max 8)
40
- * GROUPS: none
41
- */
42
-
43
- // ── HOOK STORIES ──
44
- /*
45
- * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
46
- * via raw-prop breakdowns (HOD, day, tier) or behavioral cohorts.
47
- *
48
- * ───────────────────────────────────────────────────────────────
49
- * 1. AFTER-HOURS SURGE PRICING (event hook)
50
- * ───────────────────────────────────────────────────────────────
51
- *
52
- * PATTERN: Consultations between 7PM-7AM (after-hours) have 1.5x
53
- * higher consultation_fee. Simulates urgent care premium pricing.
54
- *
55
- * HOW TO FIND IT IN MIXPANEL:
56
- *
57
- * Report 1: After-Hours Fee Premium
58
- * • Report type: Insights
59
- * • Event: "consultation completed"
60
- * • Measure: Average of "consultation_fee"
61
- * • Breakdown: hour of day
62
- * • Expected: hours 19-06 UTC ~ 1.5x avg fee vs hours 07-18
63
- * (after-hours ≈ $112, business ≈ $75)
64
- *
65
- * REAL-WORLD ANALOGUE: Telehealth platforms charge premiums for
66
- * after-hours urgent consultations, a key revenue driver.
67
- *
68
- * ───────────────────────────────────────────────────────────────
69
- * 2. FLU SEASON SPIKE (event hook)
70
- * ───────────────────────────────────────────────────────────────
71
- *
72
- * PATTERN: During days 50-70 (flu season window), appointments
73
- * with condition_type "respiratory" get 2x the wait_time and the
74
- * condition is forced to "respiratory" 60% of the time.
75
- *
76
- * HOW TO FIND IT IN MIXPANEL:
77
- *
78
- * Report 1: Flu Season Volume
79
- * • Report type: Insights
80
- * • Event: "appointment booked"
81
- * • Measure: Total
82
- * • Filter: condition_type = "respiratory"
83
- * • Line chart by week
84
- * • Expected: Clear spike during flu season window (days 50-70)
85
- *
86
- * Report 2: Wait Time During Flu Season
87
- * • Report type: Insights
88
- * • Event: "appointment booked"
89
- * • Measure: Average of "wait_time_hours"
90
- * • Breakdown: "condition_type"
91
- * • Filter: time within flu season
92
- * • Expected: respiratory ~2x wait vs other conditions
93
- *
94
- * REAL-WORLD ANALOGUE: Seasonal illness creates predictable surges
95
- * in appointment demand and wait times.
96
- *
97
- * ───────────────────────────────────────────────────────────────
98
- * 3. EXPERIENCED DOCTOR SATISFACTION (everything hook)
99
- * ───────────────────────────────────────────────────────────────
100
- *
101
- * PATTERN: Users who had >12 consultation events get higher avg
102
- * satisfaction_score (boosted to 4.0-5.0 range) vs baseline 1-5.
103
- * Simulates experienced doctors earning better reviews.
104
- *
105
- * HOW TO FIND IT IN MIXPANEL:
106
- *
107
- * Report 1: Satisfaction by Consultation Volume
108
- * • Report type: Insights
109
- * • Event: "consultation completed"
110
- * • Measure: Average of "satisfaction_score"
111
- * • Breakdown: user property "role"
112
- * • Expected: Doctors (high volume users) show ~4.2 avg vs ~3.5 baseline
113
- *
114
- * REAL-WORLD ANALOGUE: Experienced providers develop better bedside
115
- * manner and patient communication skills over time.
116
- *
117
- * ───────────────────────────────────────────────────────────────
118
- * 4. VIDEO CONSULTATION FOLLOW-UP LIFT (everything hook)
119
- * ───────────────────────────────────────────────────────────────
120
- *
121
- * PATTERN: Patients whose consultations used video mode (vs phone)
122
- * get 2x more follow-up appointment events injected. Cloned from
123
- * existing "follow up scheduled" events.
124
- *
125
- * HOW TO FIND IT IN MIXPANEL:
126
- *
127
- * Report 1: Follow-Up Rate by Consultation Mode
128
- * • Report type: Insights
129
- * • Event: "follow up scheduled"
130
- * • Measure: Total per user
131
- * • Breakdown: "consultation_mode" (from consultation completed)
132
- * • Expected: video users ~2x more follow-ups than phone users
133
- *
134
- * REAL-WORLD ANALOGUE: Face-to-face (video) consultations build
135
- * stronger patient-doctor rapport, increasing follow-up compliance.
136
- *
137
- * ───────────────────────────────────────────────────────────────
138
- * 5. CHRONIC CONDITION REFILL CHAIN (everything hook)
139
- * ───────────────────────────────────────────────────────────────
140
- *
141
- * PATTERN: Patients with condition_type "chronic" on any prescription
142
- * event get additional cloned prescription_refill events injected
143
- * every ~30 days after the original. Creates periodic refill cadence.
144
- *
145
- * HOW TO FIND IT IN MIXPANEL:
146
- *
147
- * Report 1: Refill Volume by Condition
148
- * • Report type: Insights
149
- * • Event: "prescription refill"
150
- * • Measure: Total
151
- * • Breakdown: "condition_type"
152
- * • Expected: "chronic" should have ~3-4x more refills than others
153
- *
154
- * REAL-WORLD ANALOGUE: Chronic conditions (diabetes, hypertension)
155
- * require ongoing prescriptions creating predictable refill revenue.
156
- *
157
- * ───────────────────────────────────────────────────────────────
158
- * 6. OCCASIONAL PATIENT NO-SHOWS (everything hook)
159
- * ───────────────────────────────────────────────────────────────
160
- *
161
- * PATTERN: Users with role "patient" and low event count (<15 events)
162
- * have 25% of their "appointment booked" events dropped. Simulates
163
- * occasional patients who book but don't show up.
164
- *
165
- * HOW TO FIND IT IN MIXPANEL:
166
- *
167
- * Report 1: Appointment-to-Consultation Ratio
168
- * • Report type: Funnels
169
- * • Steps: "appointment booked" → "consultation completed"
170
- * • Breakdown: user property "role"
171
- * • Expected: patients with fewer events convert ~75% vs ~95% for active
172
- *
173
- * REAL-WORLD ANALOGUE: Infrequent patients have higher no-show rates,
174
- * a major operational cost for healthcare providers.
175
- *
176
- * ───────────────────────────────────────────────────────────────
177
- * 7. DOCTOR PROFILE SPECIALIZATION (user hook)
178
- * ───────────────────────────────────────────────────────────────
179
- *
180
- * PATTERN: Users with role "doctor" get specialty set to a specific
181
- * value (from the existing array) and years_experience boosted to
182
- * senior range (15-30). Nurses get years_experience in mid range.
183
- *
184
- * HOW TO FIND IT IN MIXPANEL:
185
- *
186
- * Report 1: Experience Distribution by Role
187
- * • Report type: Insights
188
- * • Event: "consultation completed"
189
- * • Measure: Average of user property "years_experience"
190
- * • Breakdown: user property "role"
191
- * • Expected: doctors ≈ 22 years, nurses ≈ 8, patients ≈ 0
192
- *
193
- * REAL-WORLD ANALOGUE: Provider profiles have specialized expertise
194
- * and experience levels that affect patient matching.
195
- *
196
- * ───────────────────────────────────────────────────────────────
197
- * 8. FREE-TIER CONVERSION DROP (everything hook)
198
- * ───────────────────────────────────────────────────────────────
199
- *
200
- * PATTERN: Free-tier users lose ~30% of "consultation completed"
201
- * events (last step of the Booking to Consultation funnel).
202
- * This is implemented via event filtering in the everything hook
203
- * rather than conversionRate modification in funnel-pre, so the
204
- * effect is not diluted by organic (non-funnel) events.
205
- *
206
- * HOW TO FIND IT IN MIXPANEL:
207
- *
208
- * Report 1: Booking Conversion by Tier
209
- * • Report type: Funnels
210
- * • Steps: "symptom search" → "appointment booked" → "consultation completed"
211
- * • Breakdown: "subscription_tier"
212
- * • Expected: free ≈ 28% vs basic/premium ≈ 40% conversion
213
- *
214
- * REAL-WORLD ANALOGUE: Free-tier patients face longer wait times
215
- * and limited scheduling, reducing completed consultations.
216
- *
217
- * ───────────────────────────────────────────────────────────────
218
- * 9. BOOKING FUNNEL TTC BY TIER (everything hook — property scaling)
219
- *
220
- * PATTERN: Premium users get shorter wait times and consultation
221
- * durations (0.67x); Free users get longer (1.4x); Basic at 1.0x.
222
- * Scales `wait_time_hours` on "appointment booked" and
223
- * `duration_minutes` on "consultation completed".
224
- *
225
- * HOW TO FIND IT IN MIXPANEL:
226
- *
227
- * Report 1: Wait Time by Subscription Tier
228
- * - Insights > "appointment booked"
229
- * - Measure: Average of "wait_time_hours"
230
- * - Breakdown: subscription_tier
231
- * - Expected: premium ~ 0.67x baseline; free ~ 1.4x baseline
232
- *
233
- * Report 2: Consultation Duration by Tier
234
- * - Insights > "consultation completed"
235
- * - Measure: Average of "duration_minutes"
236
- * - Breakdown: subscription_tier
237
- * - Expected: premium ~ 0.67x baseline; free ~ 1.4x baseline
238
- *
239
- * ───────────────────────────────────────────────────────────────
240
- * 10. CONSULTATION-COUNT MAGIC NUMBER (everything)
241
- *
242
- * PATTERN: Sweet 3-6 consultations → +25% on consultation_fee.
243
- * Over 7+ → days_until_followup multiplied by 1.5 (over-consulted
244
- * patients wait 50% longer for next visit). No flag.
245
- *
246
- * HOW TO FIND IT IN MIXPANEL:
247
- *
248
- * Report 1: Avg Consultation Fee by Consult-Count Bucket
249
- * - Cohort A: users with 3-6 "consultation completed"
250
- * - Cohort B: users with 0-2
251
- * - Event: "consultation completed"
252
- * - Measure: Average of "consultation_fee"
253
- * - Expected: A ~ 1.25x B
254
- *
255
- * Report 2: Follow-Up Wait Time on Heavy Consulters
256
- * - Cohort C: users with >= 7 consultations
257
- * - Cohort A: users with 3-6
258
- * - Event: "follow up scheduled"
259
- * - Measure: Average of "days_until_followup"
260
- * - Expected: C ~ 1.5x A (longer gap before next visit)
261
- *
262
- * REAL-WORLD ANALOGUE: Engaged patients pay more; over-engaged
263
- * patients hit care-fatigue and stretch the gap to next visit.
264
- *
265
- * ═══════════════════════════════════════════════════════════════
266
- * EXPECTED METRICS SUMMARY
267
- * ═══════════════════════════════════════════════════════════════
268
- *
269
- * Hook | Metric | Baseline | Effect | Ratio
270
- * ----------------------------|---------------------|----------|-----------|------
271
- * After-Hours Pricing | consultation_fee | 1x | 1.5x | 1.5x
272
- * Flu Season Spike | respiratory share | ~ 15% | ~ 60% | 4x
273
- * Experienced Doctor Sat. | satisfaction_score | ~2.0 | 4.0-5.0 | ~2x
274
- * Video Follow-Up Lift | follow-ups/user | 1x | 2x | 2x
275
- * Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
276
- * Occasional No-Shows | booking→consult | 95% | 75% | -20%
277
- * Doctor Specialization | years_experience | 5 | 22 | 4.4x
278
- * Free-Tier Conversion Drop | funnel conversion | 40% | 28% | -30%
279
- * Booking TTC by Tier | wait_time/duration | 1x | 0.67/1.4x | ~ 2.1x range
280
- * Consult-Count Magic Number | sweet consult fee | 1x | 1.25x | 1.25x
281
- * Consult-Count Magic Number | over days_until_fu | 1x | 1.5x | +50%
282
- */
283
-
284
- // ── SCALE ──
285
- const SEED = "dm4-healthcare";
286
- const NUM_USERS = 10_000;
287
- const DATASET_START = "2026-01-01T00:00:00Z";
288
- const DATASET_END = "2026-05-01T23:59:59Z";
289
- const EVENTS_PER_DAY = 1.2;
290
- const token = process.env.MP_TOKEN || "your-mixpanel-token";
291
-
292
- const chance = u.initChance(SEED);
293
-
294
- // ── KNOBS (tweak these to reshape stories) ──
295
- const AFTER_HOURS_START = 19;
296
- const AFTER_HOURS_END = 7;
297
- const AFTER_HOURS_FEE_MULT = 1.5;
298
-
299
- const FLU_START_DAY = 50;
300
- const FLU_END_DAY = 70;
301
- const FLU_RESPIRATORY_LIKELIHOOD = 60;
302
- const FLU_WAIT_MULT = 2;
303
-
304
- const EXPERIENCED_CONSULT_THRESHOLD = 12;
305
- const EXPERIENCED_SATISFACTION_MIN = 4.0;
306
- const EXPERIENCED_SATISFACTION_MAX = 5.0;
307
-
308
- const VIDEO_FOLLOWUP_LIKELIHOOD = 60;
309
-
310
- const CHRONIC_REFILL_MIN = 2;
311
- const CHRONIC_REFILL_MAX = 4;
312
- const CHRONIC_REFILL_INTERVAL_DAYS = 30;
313
-
314
- const NO_SHOW_EVENT_THRESHOLD = 15;
315
- const NO_SHOW_DROP_LIKELIHOOD = 25;
316
-
317
- const DOCTOR_EXPERIENCE_MIN = 15;
318
- const DOCTOR_EXPERIENCE_MAX = 30;
319
- const NURSE_EXPERIENCE_MIN = 3;
320
- const NURSE_EXPERIENCE_MAX = 15;
321
-
322
- const FREE_TIER_DROP_LIKELIHOOD = 30;
323
-
324
- const TTC_PREMIUM_FACTOR = 0.67;
325
- const TTC_FREE_FACTOR = 1.4;
326
-
327
- const CONSULT_SWEET_MIN = 3;
328
- const CONSULT_SWEET_MAX = 6;
329
- const CONSULT_OVER_THRESHOLD = 7;
330
- const CONSULT_FEE_BOOST = 1.25;
331
- const CONSULT_FOLLOWUP_STRETCH = 1.5;
332
-
333
- // ── DATA ARRAYS ──
334
- // Generate consistent doctor/clinic IDs at module level
335
- const doctorIds = v.range(1, 120).map(() => `DR_${v.uid(6)}`);
336
- const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
337
-
338
- // ── HELPER FUNCTIONS ──
339
- function handleUserHooks(record) {
340
- // H7: DOCTOR PROFILE SPECIALIZATION — doctors get a real specialty and
341
- // senior years_experience. Nurses get mid-range experience. Patients
342
- // stay at defaults.
343
- if (record.role === "doctor") {
344
- record.specialty = chance.pickone(["cardiology", "dermatology", "pediatrics", "psychiatry", "general_practice", "pulmonology", "endocrinology"]);
345
- record.years_experience = chance.integer({ min: DOCTOR_EXPERIENCE_MIN, max: DOCTOR_EXPERIENCE_MAX });
346
- } else if (record.role === "nurse") {
347
- record.specialty = chance.pickone(["general_practice", "pediatrics", "emergency"]);
348
- record.years_experience = chance.integer({ min: NURSE_EXPERIENCE_MIN, max: NURSE_EXPERIENCE_MAX });
349
- } else {
350
- record.years_experience = 0;
351
- }
352
- return record;
353
- }
354
-
355
- function handleEverythingHooks(record, meta) {
356
- if (!record.length) return record;
357
- const profile = meta.profile;
358
- const datasetStart = dayjs.unix(meta.datasetStart);
359
- const FLU_START = datasetStart.add(FLU_START_DAY, "days");
360
- const FLU_END = datasetStart.add(FLU_END_DAY, "days");
361
-
362
- // ── SUPER-PROP STAMPING ──────────────────────────
363
- // Stamp superProps from profile so they are consistent per-user.
364
- if (profile) {
365
- const tier = profile.subscription_tier;
366
- const plat = profile.Platform;
367
- record.forEach(e => {
368
- if (tier) e.subscription_tier = tier;
369
- if (plat) e.Platform = plat;
370
- });
371
- }
372
-
373
- // HOOK 9: BOOKING FUNNEL TTC BY TIER (property scaling)
374
- // Premium users get shorter wait_time_hours (0.67x) and duration_minutes (0.67x).
375
- // Free users get longer wait_time_hours (1.4x) and duration_minutes (1.4x).
376
- // Basic users stay at baseline. SQL-measurable via AVG(wait_time_hours) broken by tier.
377
- if (profile) {
378
- const userTier = profile.subscription_tier;
379
- const ttcFactor = userTier === "premium" ? TTC_PREMIUM_FACTOR : userTier === "free" ? TTC_FREE_FACTOR : 1.0;
380
- if (ttcFactor !== 1.0) {
381
- // Timestamp shift: affects Mixpanel funnel TTC
382
- const bookingSeq = findFirstSequence(
383
- record,
384
- ["appointment booked", "consultation completed", "follow up scheduled"],
385
- 60 * 24 * 30
386
- );
387
- if (bookingSeq) scaleFunnelTTC(bookingSeq, ttcFactor);
388
- // Property scale: affects Insights AVG reports
389
- record.forEach(e => {
390
- if (e.event === "appointment booked" && typeof e.wait_time_hours === "number") {
391
- e.wait_time_hours = Math.round(e.wait_time_hours * ttcFactor * 10) / 10;
392
- }
393
- if (e.event === "consultation completed" && typeof e.duration_minutes === "number") {
394
- e.duration_minutes = Math.round(e.duration_minutes * ttcFactor);
395
- }
396
- });
397
- }
398
- }
399
-
400
- // HOOK 1: AFTER-HOURS SURGE PRICING — consultations 7PM-7AM
401
- // UTC get consultation_fee 1.5x. No flag — discover via HOD chart.
402
- record.forEach(e => {
403
- if (e.event === "consultation completed" || e.event === "appointment booked") {
404
- const hour = new Date(e.time).getUTCHours();
405
- if ((hour >= AFTER_HOURS_START || hour < AFTER_HOURS_END) && e.consultation_fee) {
406
- e.consultation_fee = Math.floor(e.consultation_fee * AFTER_HOURS_FEE_MULT);
407
- }
408
- }
409
- });
410
-
411
- // HOOK 2: FLU SEASON SPIKE — d50-70 respiratory dominates, wait_time doubles.
412
- // Runs in everything hook so timestamp checks see post-bunchIntoSessions times.
413
- record.forEach(e => {
414
- if (e.event !== "appointment booked") return;
415
- const t = dayjs(e.time);
416
- if (t.isAfter(FLU_START) && t.isBefore(FLU_END)) {
417
- if (chance.bool({ likelihood: FLU_RESPIRATORY_LIKELIHOOD })) e.condition_type = "respiratory";
418
- if (e.condition_type === "respiratory") {
419
- e.wait_time_hours = Math.floor((e.wait_time_hours || 12) * FLU_WAIT_MULT);
420
- }
421
- }
422
- });
423
-
424
- // ── HOOK 8: FREE-TIER CONVERSION DROP ────────────
425
- // Free-tier users lose ~30% of "consultation completed" events
426
- // (last step of Booking to Consultation funnel), simulating
427
- // lower conversion for non-paying patients.
428
- if (profile && profile.subscription_tier === "free" && chance.bool({ likelihood: FREE_TIER_DROP_LIKELIHOOD })) {
429
- record = record.filter(e => e.event !== "consultation completed");
430
- }
431
-
432
- // ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
433
- // Users with >12 consultation events get boosted satisfaction scores.
434
- let consultCount = 0;
435
- record.forEach(e => {
436
- if (e.event === "consultation completed") consultCount++;
437
- });
438
-
439
- if (consultCount > EXPERIENCED_CONSULT_THRESHOLD) {
440
- record.forEach(e => {
441
- if (e.event === "consultation completed") {
442
- e.satisfaction_score = chance.floating({ min: EXPERIENCED_SATISFACTION_MIN, max: EXPERIENCED_SATISFACTION_MAX, fixed: 1 });
443
- }
444
- });
445
- }
446
-
447
- // ── HOOK 4: VIDEO CONSULTATION FOLLOW-UP LIFT ────
448
- // Patients with video consultations get 2x follow-up events.
449
- const hasVideoConsult = record.some(e =>
450
- e.event === "consultation completed" && e.consultation_mode === "video"
451
- );
452
- if (hasVideoConsult) {
453
- const templateFollowUp = record.find(e => e.event === "follow up scheduled");
454
- if (templateFollowUp) {
455
- const videoConsults = record.filter(e =>
456
- e.event === "consultation completed" && e.consultation_mode === "video"
457
- );
458
- videoConsults.forEach(vc => {
459
- if (chance.bool({ likelihood: VIDEO_FOLLOWUP_LIKELIHOOD })) {
460
- record.push({
461
- ...templateFollowUp,
462
- time: dayjs(vc.time).add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
463
- user_id: vc.user_id,
464
- consultation_mode: "video",
465
- days_until_followup: chance.integer({ min: 3, max: 14 }),
466
- });
467
- }
468
- });
469
- }
470
- }
471
-
472
- // ── HOOK 5: CHRONIC CONDITION REFILL CHAIN ───────
473
- // Patients with chronic prescriptions get refills every ~30 days.
474
- const chronicRxs = record.filter(e =>
475
- e.event === "prescription issued" && e.condition_type === "chronic"
476
- );
477
- if (chronicRxs.length > 0) {
478
- const templateRefill = record.find(e => e.event === "prescription refill");
479
- if (templateRefill) {
480
- chronicRxs.forEach(rx => {
481
- const rxTime = dayjs(rx.time);
482
- const refillsToAdd = chance.integer({ min: CHRONIC_REFILL_MIN, max: CHRONIC_REFILL_MAX });
483
- for (let i = 1; i <= refillsToAdd; i++) {
484
- record.push({
485
- ...templateRefill,
486
- time: rxTime.add(CHRONIC_REFILL_INTERVAL_DAYS * i + chance.integer({ min: -3, max: 3 }), "days").toISOString(),
487
- user_id: rx.user_id,
488
- condition_type: "chronic",
489
- medication_type: "chronic_maintenance",
490
- refill_count: i,
491
- });
492
- }
493
- });
494
- }
495
- }
496
-
497
- // HOOK 6: OCCASIONAL PATIENT NO-SHOWS — low-activity patients
498
- // (< 15 events) lose 25% of consultations (they booked but didn't show).
499
- if (record.length < NO_SHOW_EVENT_THRESHOLD) {
500
- for (let i = record.length - 1; i >= 0; i--) {
501
- if (record[i].event === "consultation completed" && chance.bool({ likelihood: NO_SHOW_DROP_LIKELIHOOD })) {
502
- record.splice(i, 1);
503
- }
504
- }
505
- }
506
-
507
- // HOOK 10: CONSULTATION-COUNT MAGIC NUMBER (no flags)
508
- // Sweet 3-6 consultations → +25% on consultation_fee. Over 7+ →
509
- // drop 30% of "follow up scheduled" events (over-consulted →
510
- // follow-up fatigue).
511
- const consultCt = record.filter(e => e.event === "consultation completed").length;
512
- if (consultCt >= CONSULT_SWEET_MIN && consultCt <= CONSULT_SWEET_MAX) {
513
- record.forEach(e => {
514
- if (e.event === "consultation completed" && typeof e.consultation_fee === "number") {
515
- e.consultation_fee = Math.round(e.consultation_fee * CONSULT_FEE_BOOST);
516
- }
517
- });
518
- } else if (consultCt >= CONSULT_OVER_THRESHOLD) {
519
- record.forEach(e => {
520
- if (e.event === "follow up scheduled" && typeof e.days_until_followup === "number") {
521
- e.days_until_followup = Math.round(e.days_until_followup * CONSULT_FOLLOWUP_STRETCH);
522
- }
523
- });
524
- }
525
-
526
- return record;
527
- }
528
-
529
- // ── CONFIG ──
530
- /** @type {Config} */
531
- const config = {
532
- version: 2,
533
- seed: SEED,
534
- datasetStart: DATASET_START,
535
- datasetEnd: DATASET_END,
536
- avgEventsPerUserPerDay: EVENTS_PER_DAY,
537
- numUsers: NUM_USERS,
538
- format: "json",
539
- gzip: true,
540
- credentials: {
541
- token,
542
- },
543
- switches: {
544
- hasSessionIds: true,
545
- alsoInferFunnels: false,
546
- hasLocation: true,
547
- hasAndroidDevices: true,
548
- hasIOSDevices: true,
549
- hasDesktopDevices: true,
550
- hasBrowser: false,
551
- hasCampaigns: false,
552
- isAnonymous: false,
553
- hasAdSpend: false,
554
- hasAvatar: true,
555
- },
556
- identity: {
557
- avgDevicePerUser: 2,
558
- },
559
- concurrency: 1,
560
- writeToDisk: false,
561
- scdProps: {
562
- care_plan: {
563
- values: ["preventive", "routine", "chronic", "acute"],
564
- frequency: "month",
565
- timing: "fuzzy",
566
- max: 8
567
- }
568
- },
569
- mirrorProps: {},
570
- lookupTables: [],
571
-
572
- // ── Events (18) ──────────────────────────────────────────
573
- events: [
574
- {
575
- event: "account created",
576
- weight: 1,
577
- isFirstEvent: true,
578
- isAuthEvent: true,
579
- properties: {
580
- referral_source: ["organic", "doctor_referral", "insurance_partner", "social_media", "search"],
581
- },
582
- },
583
- {
584
- event: "symptom search",
585
- weight: 7,
586
- properties: {
587
- search_term: ["headache", "fever", "cough", "back pain", "fatigue", "anxiety", "rash", "nausea", "chest pain", "joint pain"],
588
- results_count: u.weighNumRange(0, 25, 0.5),
589
- },
590
- },
591
- {
592
- event: "appointment booked",
593
- weight: 6,
594
- isStrictEvent: false,
595
- properties: {
596
- doctor_id: chance.pickone.bind(chance, doctorIds),
597
- clinic_id: chance.pickone.bind(chance, clinicIds),
598
- condition_type: ["general", "general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
599
- wait_time_hours: u.weighNumRange(1, 72, 0.4),
600
- appointment_type: ["new_patient", "follow_up", "follow_up", "urgent", "routine", "routine"],
601
- },
602
- },
603
- {
604
- event: "consultation completed",
605
- weight: 5,
606
- isStrictEvent: false,
607
- properties: {
608
- doctor_id: chance.pickone.bind(chance, doctorIds),
609
- consultation_mode: ["phone", "phone", "video"],
610
- duration_minutes: u.weighNumRange(5, 60, 0.6, 15),
611
- consultation_fee: u.weighNumRange(25, 200, 0.4, 75),
612
- satisfaction_score: u.weighNumRange(1, 5, 0.8, 3),
613
- condition_type: ["general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
614
- },
615
- },
616
- {
617
- event: "prescription issued",
618
- weight: 4,
619
- isStrictEvent: false,
620
- properties: {
621
- medication_type: ["antibiotic", "antiviral", "painkiller", "anti_inflammatory", "antidepressant", "inhaler", "topical", "chronic_maintenance"],
622
- quantity: u.weighNumRange(1, 90, 0.3, 30),
623
- condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "chronic", "pediatric"],
624
- refill_count: u.weighNumRange(0, 3),
625
- },
626
- },
627
- {
628
- event: "prescription refill",
629
- weight: 3,
630
- isStrictEvent: false,
631
- properties: {
632
- medication_type: ["antibiotic", "antiviral", "painkiller", "anti_inflammatory", "antidepressant", "inhaler", "topical", "chronic_maintenance"],
633
- quantity: u.weighNumRange(1, 90, 0.3, 30),
634
- condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "chronic", "pediatric"],
635
- refill_count: u.weighNumRange(1, 6),
636
- },
637
- },
638
- {
639
- event: "follow up scheduled",
640
- weight: 3,
641
- isStrictEvent: false,
642
- properties: {
643
- doctor_id: chance.pickone.bind(chance, doctorIds),
644
- days_until_followup: u.weighNumRange(3, 30, 0.5, 7),
645
- condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
646
- consultation_mode: ["phone", "phone", "video"],
647
- },
648
- },
649
- {
650
- event: "message sent",
651
- weight: 5,
652
- properties: {
653
- message_type: ["question", "question", "update", "result_inquiry", "prescription_question", "scheduling"],
654
- recipient_role: ["doctor", "doctor", "nurse", "support"],
655
- response_time_hours: u.weighNumRange(0.1, 48, 0.3, 4),
656
- },
657
- },
658
- {
659
- event: "lab results viewed",
660
- weight: 3,
661
- properties: {
662
- test_type: ["blood_panel", "urinalysis", "imaging", "allergy_test", "metabolic_panel", "thyroid"],
663
- result_status: ["normal", "normal", "normal", "abnormal", "pending"],
664
- },
665
- },
666
- {
667
- event: "health record accessed",
668
- weight: 4,
669
- properties: {
670
- record_type: ["visit_summary", "lab_results", "prescriptions", "immunizations", "billing"],
671
- access_method: ["app", "app", "web_portal"],
672
- },
673
- },
674
- {
675
- event: "insurance verified",
676
- weight: 2,
677
- properties: {
678
- insurance_type: ["private", "private", "employer", "medicare", "medicaid", "self_pay"],
679
- verification_status: ["approved", "approved", "approved", "pending", "denied"],
680
- copay_amount: u.weighNumRange(0, 75, 0.5, 20),
681
- },
682
- },
683
- {
684
- event: "payment processed",
685
- weight: 3,
686
- properties: {
687
- amount: u.weighNumRange(10, 500, 0.3, 75),
688
- payment_method: ["credit_card", "credit_card", "insurance_claim", "hsa_fsa", "debit"],
689
- payment_status: ["success", "success", "success", "success", "failed"],
690
- },
691
- },
692
- {
693
- event: "notification received",
694
- weight: 6,
695
- properties: {
696
- notification_type: ["appointment_reminder", "appointment_reminder", "lab_ready", "prescription_ready", "message_received", "billing"],
697
- channel: ["push", "push", "email", "sms"],
698
- opened: [true, true, true, false],
699
- },
700
- },
701
- {
702
- event: "provider rated",
703
- weight: 2,
704
- properties: {
705
- doctor_id: chance.pickone.bind(chance, doctorIds),
706
- rating: u.weighNumRange(1, 5, 0.7, 4),
707
- would_recommend: [true, true, true, true, false],
708
- },
709
- },
710
- {
711
- event: "support ticket created",
712
- weight: 1,
713
- properties: {
714
- category: ["billing", "technical", "scheduling", "prescription", "insurance", "other"],
715
- priority: ["low", "low", "medium", "medium", "high"],
716
- resolution_hours: u.weighNumRange(1, 96, 0.4, 24),
717
- },
718
- },
719
- {
720
- event: "profile updated",
721
- weight: 2,
722
- properties: {
723
- field_updated: ["insurance", "address", "phone", "emergency_contact", "allergies", "medications"],
724
- },
725
- },
726
- {
727
- event: "app session",
728
- weight: 8,
729
- properties: {
730
- session_duration_sec: u.weighNumRange(10, 1800, 0.4, 120),
731
- pages_viewed: u.weighNumRange(1, 15, 0.5, 3),
732
- },
733
- },
734
- {
735
- event: "account deactivated",
736
- weight: 1,
737
- isChurnEvent: true,
738
- returnLikelihood: 0.15,
739
- isStrictEvent: true,
740
- properties: {
741
- reason: ["switched_provider", "cost", "no_longer_needed", "poor_experience", "insurance_change"],
742
- },
743
- },
744
- ],
745
-
746
- // ── Funnels (5) ──────────────────────────────────────────
747
- funnels: [
748
- {
749
- name: "Onboarding Flow",
750
- sequence: ["account created", "insurance verified", "symptom search", "appointment booked"],
751
- conversionRate: 45,
752
- order: "sequential",
753
- isFirstFunnel: true,
754
- timeToConvert: 72,
755
- weight: 3,
756
- },
757
- {
758
- name: "Booking to Consultation",
759
- sequence: ["symptom search", "appointment booked", "consultation completed"],
760
- conversionRate: 40,
761
- order: "sequential",
762
- timeToConvert: 48,
763
- weight: 5,
764
- },
765
- {
766
- name: "Full Care Journey",
767
- sequence: ["appointment booked", "consultation completed", "prescription issued", "follow up scheduled"],
768
- conversionRate: 30,
769
- order: "sequential",
770
- timeToConvert: 168,
771
- weight: 3,
772
- },
773
- {
774
- name: "Prescription Lifecycle",
775
- sequence: ["prescription issued", "prescription refill", "payment processed"],
776
- conversionRate: 55,
777
- order: "sequential",
778
- timeToConvert: 720,
779
- weight: 2,
780
- },
781
- {
782
- name: "Patient Satisfaction",
783
- sequence: ["consultation completed", "provider rated", "follow up scheduled"],
784
- conversionRate: 25,
785
- order: "sequential",
786
- timeToConvert: 72,
787
- weight: 2,
788
- },
789
- ],
790
-
791
- // ── SuperProps ──────────────────────────────────────────
792
- superProps: {
793
- subscription_tier: ["free", "free", "free", "basic", "basic", "premium"],
794
- Platform: ["ios", "android", "web"],
795
- },
796
-
797
- // ── UserProps ──────────────────────────────────────────
798
- userProps: {
799
- role: ["patient", "patient", "patient", "patient", "patient", "patient", "patient", "patient", "nurse", "doctor"],
800
- specialty: ["none"],
801
- years_experience: u.weighNumRange(0, 5, 0.5),
802
- preferred_language: ["en", "en", "en", "en", "es", "pt", "de", "fr"],
803
- has_chronic_condition: [false, false, false, true],
804
- age_range: ["18-25", "26-35", "26-35", "36-45", "36-45", "46-55", "56-65", "65+"],
805
- subscription_tier: ["free", "free", "free", "basic", "basic", "premium"],
806
- Platform: ["ios", "android", "web"],
807
- },
808
-
809
- // ── Personas ──────────────────────────────────
810
- personas: [
811
- {
812
- name: "doctor",
813
- weight: 5,
814
- eventMultiplier: 5.0,
815
- conversionModifier: 1.8,
816
- churnRate: 0.01,
817
- properties: {
818
- role: "doctor",
819
- segment: "provider",
820
- },
821
- },
822
- {
823
- name: "nurse",
824
- weight: 10,
825
- eventMultiplier: 3.0,
826
- conversionModifier: 1.5,
827
- churnRate: 0.03,
828
- properties: {
829
- role: "nurse",
830
- segment: "provider",
831
- },
832
- },
833
- {
834
- name: "patient_active",
835
- weight: 40,
836
- eventMultiplier: 1.0,
837
- conversionModifier: 1.0,
838
- churnRate: 0.05,
839
- properties: {
840
- role: "patient",
841
- segment: "active_patient",
842
- },
843
- },
844
- {
845
- name: "patient_occasional",
846
- weight: 30,
847
- eventMultiplier: 0.5,
848
- conversionModifier: 0.7,
849
- churnRate: 0.12,
850
- properties: {
851
- role: "patient",
852
- segment: "occasional_patient",
853
- },
854
- },
855
- {
856
- name: "patient_churner",
857
- weight: 15,
858
- eventMultiplier: 0.3,
859
- conversionModifier: 0.3,
860
- churnRate: 0.4,
861
- properties: {
862
- role: "patient",
863
- segment: "churner",
864
- },
865
- activeWindow: { maxDays: 21 },
866
- },
867
- ],
868
-
869
- // ── Subscription ──────────────────────────────
870
- subscription: {
871
- plans: [
872
- { name: "free", price: 0, default: true },
873
- { name: "basic", price: 9.99, trialDays: 14 },
874
- { name: "premium", price: 29.99 },
875
- ],
876
- lifecycle: {
877
- trialToPayRate: 0.55,
878
- upgradeRate: 0.20,
879
- downgradeRate: 0.03,
880
- churnRate: 0.05,
881
- winBackRate: 0.10,
882
- winBackDelay: 21,
883
- paymentFailureRate: 0.02,
884
- },
885
- },
886
-
887
- // ── Geo ──────────────────────────────────────
888
- geo: {
889
- sticky: true,
890
- regions: [
891
- {
892
- name: "us",
893
- countries: ["US"],
894
- weight: 50,
895
- timezoneOffset: -5,
896
- properties: { currency: "USD", locale: "en-US" },
897
- },
898
- {
899
- name: "eu",
900
- countries: ["GB", "DE", "FR"],
901
- weight: 30,
902
- timezoneOffset: 1,
903
- properties: { currency: "EUR", locale: "en-EU", gdpr_consent: true },
904
- },
905
- {
906
- name: "latam",
907
- countries: ["BR", "MX", "AR"],
908
- weight: 20,
909
- timezoneOffset: -3,
910
- properties: { currency: "BRL", locale: "pt-BR" },
911
- },
912
- ],
913
- },
914
-
915
- // ── Features ──────────────────────────────────
916
- features: [
917
- {
918
- name: "video_consultation",
919
- launchDay: 30,
920
- adoptionCurve: "fast",
921
- property: "consultation_mode",
922
- values: ["phone", "video"],
923
- defaultBefore: "phone",
924
- affectsEvents: ["consultation completed", "follow up scheduled"],
925
- },
926
- {
927
- name: "ai_symptom_checker",
928
- launchDay: 60,
929
- adoptionCurve: { k: 0.08, midpoint: 25 },
930
- property: "symptom_source",
931
- values: ["manual", "ai_assisted"],
932
- defaultBefore: "manual",
933
- affectsEvents: ["symptom search"],
934
- conversionLift: 1.12,
935
- },
936
- ],
937
-
938
- hook(record, type, meta) {
939
- if (type === "user") return handleUserHooks(record);
940
- if (type === "everything") return handleEverythingHooks(record, meta);
941
- return record;
942
- },
943
- };
944
-
945
- export default config;