@ak--47/dungeon-master 1.2.0 → 1.2.2

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 (54) hide show
  1. package/dungeons/technical/ad-spend.js +15 -4
  2. package/dungeons/technical/anonymous-users.js +15 -5
  3. package/dungeons/technical/array-of-object-lookup.js +21 -5
  4. package/dungeons/technical/experiments.js +23 -5
  5. package/dungeons/technical/foobar.js +28 -12
  6. package/dungeons/technical/group-analytics.js +15 -5
  7. package/dungeons/technical/mirror-strategies.js +15 -5
  8. package/dungeons/technical/nested-objects.js +15 -4
  9. package/dungeons/technical/retention-cadence.js +13 -7
  10. package/dungeons/technical/sanity.js +25 -6
  11. package/dungeons/technical/scale-test.js +15 -4
  12. package/dungeons/technical/scd.js +24 -5
  13. package/dungeons/technical/simple.js +20 -5
  14. package/dungeons/technical/simplest.js +23 -6
  15. package/dungeons/technical/text-generation.js +22 -6
  16. package/dungeons/user/.gitkeep +0 -0
  17. package/dungeons/vertical/community-schema.json +1 -1
  18. package/dungeons/vertical/community.js +49 -28
  19. package/dungeons/vertical/devtools-schema.json +1 -1
  20. package/dungeons/vertical/devtools.js +50 -29
  21. package/dungeons/vertical/ecommerce.js +23 -6
  22. package/dungeons/vertical/education-schema.json +1 -1
  23. package/dungeons/vertical/education.js +29 -19
  24. package/dungeons/vertical/fintech-schema.json +1 -1
  25. package/dungeons/vertical/fintech.js +27 -8
  26. package/dungeons/vertical/fitness-schema.json +1 -1
  27. package/dungeons/vertical/fitness.js +48 -22
  28. package/dungeons/vertical/food-delivery-schema.json +1 -1
  29. package/dungeons/vertical/food-delivery.js +51 -12
  30. package/dungeons/vertical/gaming-schema.json +1 -1
  31. package/dungeons/vertical/gaming.js +41 -7
  32. package/dungeons/vertical/healthcare-schema.json +1 -1
  33. package/dungeons/vertical/healthcare.js +69 -40
  34. package/dungeons/vertical/insurance-application-schema.json +1 -1
  35. package/dungeons/vertical/insurance-application.js +26 -8
  36. package/dungeons/vertical/logistics-schema.json +1 -1
  37. package/dungeons/vertical/logistics.js +69 -41
  38. package/dungeons/vertical/marketplace-schema.json +1 -1
  39. package/dungeons/vertical/marketplace.js +44 -19
  40. package/dungeons/vertical/media.js +56 -15
  41. package/dungeons/vertical/rpg-schema.json +1 -1
  42. package/dungeons/vertical/rpg.js +38 -8
  43. package/dungeons/vertical/sass.js +24 -7
  44. package/dungeons/vertical/social.js +23 -7
  45. package/dungeons/vertical/travel-schema.json +1 -1
  46. package/dungeons/vertical/travel.js +50 -19
  47. package/index.js +3 -0
  48. package/lib/core/config-validator.js +13 -2
  49. package/lib/generators/events.js +3 -5
  50. package/lib/orchestrators/mixpanel-sender.js +64 -3
  51. package/lib/orchestrators/user-loop.js +20 -0
  52. package/lib/utils/utils.js +129 -9
  53. package/package.json +2 -2
  54. package/types.d.ts +5 -3
@@ -1,15 +1,22 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "dm4-fitness";
3
+ const num_days = 100;
4
+ const num_users = 5_000;
5
+ const avg_events_per_user = 120;
6
+ let token = "your-mixpanel-token";
7
+
8
+ // ── env overrides ──
9
+ if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
+
1
11
  import dayjs from "dayjs";
2
12
  import utc from "dayjs/plugin/utc.js";
3
13
  import "dotenv/config";
4
14
  import * as u from "../../lib/utils/utils.js";
5
15
 
6
- const SEED = "dm4-fitness";
7
16
  dayjs.extend(utc);
8
17
  const chance = u.initChance(SEED);
9
- const num_users = 5_000;
10
- const days = 100;
11
18
  const NOW = dayjs();
12
- const DATASET_START = NOW.subtract(days, "days");
19
+ const DATASET_START = NOW.subtract(num_days, "days");
13
20
 
14
21
  /** @typedef {import("../../types").Dungeon} Config */
15
22
 
@@ -219,12 +226,12 @@ const DATASET_START = NOW.subtract(days, "days");
219
226
  * consistency to build credibility with their clients.
220
227
  *
221
228
  * ───────────────────────────────────────────────────────────────
222
- * 8. ANNUAL SUBSCRIBER WORKOUT FUNNEL LIFT (funnel-pre hook)
229
+ * 8. ANNUAL SUBSCRIBER WORKOUT FUNNEL LIFT (everything hook)
223
230
  * ───────────────────────────────────────────────────────────────
224
231
  *
225
- * PATTERN: Users with subscription_tier "annual" or "family"
226
- * get 1.4x conversion rate through the Workout Loop funnel.
227
- * Annual commitment correlates with follow-through.
232
+ * PATTERN: Free/monthly-tier users lose ~30% of "progress checked"
233
+ * events (last step of the Workout Loop funnel), simulating lower
234
+ * follow-through. Annual/family subscribers retain all events.
228
235
  *
229
236
  * HOW TO FIND IT IN MIXPANEL:
230
237
  *
@@ -255,10 +262,10 @@ const DATASET_START = NOW.subtract(days, "days");
255
262
 
256
263
  /** @type {Config} */
257
264
  const config = {
258
- token: "",
265
+ token,
259
266
  seed: SEED,
260
- numDays: days,
261
- numEvents: num_users * 120,
267
+ numDays: num_days,
268
+ numEvents: num_users * avg_events_per_user,
262
269
  numUsers: num_users,
263
270
  hasAnonIds: false,
264
271
  hasSessionIds: true,
@@ -500,7 +507,7 @@ const config = {
500
507
 
501
508
  // ── SuperProps ──────────────────────────────────────────
502
509
  superProps: {
503
- platform: ["ios", "ios", "android"],
510
+ Platform: ["ios", "ios", "android"],
504
511
  workout_type: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
505
512
  },
506
513
 
@@ -512,6 +519,8 @@ const config = {
512
519
  total_workouts: u.weighNumRange(0, 0, 0.5),
513
520
  preferred_workout: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
514
521
  goal: ["weight_loss", "muscle_gain", "endurance", "flexibility", "general_health"],
522
+ Platform: ["ios", "ios", "android"],
523
+ workout_type: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
515
524
  },
516
525
 
517
526
  // ── Personas ──────────────────────────────────
@@ -679,16 +688,9 @@ const config = {
679
688
  }
680
689
 
681
690
  // ── HOOK 8: ANNUAL SUBSCRIBER WORKOUT FUNNEL LIFT (funnel-pre) ─
682
- // Annual and family subscribers convert 1.4x through workout loop.
691
+ // (conversionRate filtering moved to everything hook)
683
692
  if (type === "funnel-pre") {
684
- if (meta && meta.profile) {
685
- const tier = meta.profile.subscription_tier;
686
- if (tier === "annual" || tier === "family") {
687
- record.conversionRate = Math.min(record.conversionRate * 1.4, 95);
688
- } else if (tier === "monthly") {
689
- record.conversionRate = Math.min(record.conversionRate * 1.15, 90);
690
- }
691
- }
693
+ // no-op: conversion filtering handled via event dropping in everything hook
692
694
  }
693
695
 
694
696
  // ── HOOK 1: MORNING WORKOUT BOOST (event) ────────────
@@ -722,9 +724,33 @@ const config = {
722
724
 
723
725
  // ── EVERYTHING HOOKS ─────────────────────────────────
724
726
  if (type === "everything") {
725
- const events = record;
727
+ let events = record;
726
728
  if (!events.length) return record;
727
729
 
730
+ // ── SUPERPROP STAMPING ──────────────────────────
731
+ // Stamp superProps from profile so they are consistent per user.
732
+ if (meta && meta.profile) {
733
+ const p = meta.profile;
734
+ events.forEach(e => {
735
+ if (p.Platform) e.Platform = p.Platform;
736
+ if (p.workout_type) e.workout_type = p.workout_type;
737
+ });
738
+ }
739
+
740
+ // ── HOOK 8: ANNUAL SUBSCRIBER CONVERSION FILTER ─
741
+ // Free/monthly-tier users drop ~30% of "progress checked"
742
+ // (last step of Workout Loop funnel) to simulate lower conversion.
743
+ if (meta && meta.profile) {
744
+ const tier = meta.profile.subscription_tier;
745
+ if (tier !== "annual" && tier !== "family") {
746
+ record = record.filter(e => {
747
+ if (e.event === "progress checked" && chance.bool({ likelihood: 30 })) return false;
748
+ return true;
749
+ });
750
+ events = record;
751
+ }
752
+ }
753
+
728
754
  // ── HOOK 3: STREAK RETENTION ─────────────────────
729
755
  // Users with >10 workouts get streak_days updated and
730
756
  // cloned achievement events for milestones.
@@ -36600,7 +36600,7 @@
36600
36600
  }
36601
36601
  ],
36602
36602
  "superProps": {
36603
- "platform": [
36603
+ "Platform": [
36604
36604
  "iOS",
36605
36605
  "Android",
36606
36606
  "Web"
@@ -1,14 +1,21 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "harness-food";
3
+ const num_days = 100;
4
+ const num_users = 5_000;
5
+ const avg_events_per_user = 120;
6
+ let token = "your-mixpanel-token";
7
+
8
+ // ── env overrides ──
9
+ if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
+
1
11
  import dayjs from "dayjs";
2
12
  import utc from "dayjs/plugin/utc.js";
3
13
  import "dotenv/config";
4
14
  import * as u from "../../lib/utils/utils.js";
5
15
  import * as v from "ak-tools";
6
16
 
7
- const SEED = "harness-food";
8
17
  dayjs.extend(utc);
9
18
  const chance = u.initChance(SEED);
10
- const num_users = 5_000;
11
- const days = 100;
12
19
 
13
20
  /** @typedef {import("../../types").Dungeon} Config */
14
21
 
@@ -156,10 +163,10 @@ const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
156
163
 
157
164
  /** @type {Config} */
158
165
  const config = {
159
- token: "",
166
+ token,
160
167
  seed: SEED,
161
- numDays: days,
162
- numEvents: num_users * 120,
168
+ numDays: num_days,
169
+ numEvents: num_users * avg_events_per_user,
163
170
  numUsers: num_users,
164
171
  hasAnonIds: false,
165
172
  hasSessionIds: true,
@@ -379,6 +386,9 @@ const config = {
379
386
  "order_id": orderIds,
380
387
  "actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
381
388
  "on_time": [false, false, false, true, true, true, true, true, true, true],
389
+ "lunch_rush": [false],
390
+ "dinner_rush": [false],
391
+ "first_order_bonus": [false],
382
392
  "trial_retained": [false],
383
393
  }
384
394
  },
@@ -459,7 +469,7 @@ const config = {
459
469
  ],
460
470
 
461
471
  superProps: {
462
- platform: ["iOS", "Android", "Web"],
472
+ Platform: ["iOS", "Android", "Web"],
463
473
  subscription_tier: ["Free", "Free", "Free", "Free", "QuickBite+"],
464
474
  city: ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
465
475
  },
@@ -480,6 +490,9 @@ const config = {
480
490
  "favorite_restaurant_count": u.weighNumRange(1, 10),
481
491
  "is_high_risk": [false],
482
492
  "churn_risk_score": [0],
493
+ "Platform": ["iOS", "Android", "Web"],
494
+ "subscription_tier": ["Free", "Free", "Free", "Free", "QuickBite+"],
495
+ "city": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
483
496
  },
484
497
 
485
498
  groupKeys: [
@@ -522,7 +535,7 @@ const config = {
522
535
  */
523
536
  hook: function (record, type, meta) {
524
537
  const NOW = dayjs();
525
- const DATASET_START = NOW.subtract(days, 'days');
538
+ const DATASET_START = NOW.subtract(num_days, 'days');
526
539
  const RAINY_WEEK_START = DATASET_START.add(20, 'days');
527
540
  const RAINY_WEEK_END = DATASET_START.add(27, 'days');
528
541
 
@@ -539,13 +552,11 @@ const config = {
539
552
 
540
553
  // Lunch rush: 11AM - 1PM
541
554
  if (hour >= 11 && hour <= 13) {
542
- record.conversionRate = record.conversionRate * 1.4;
543
555
  record.props.lunch_rush = true;
544
556
  record.props.dinner_rush = false;
545
557
  }
546
558
  // Dinner rush: 5PM - 8PM
547
559
  else if (hour >= 17 && hour <= 20) {
548
- record.conversionRate = record.conversionRate * 1.2;
549
560
  record.props.lunch_rush = false;
550
561
  record.props.dinner_rush = true;
551
562
  } else {
@@ -568,7 +579,6 @@ const config = {
568
579
  const userId = meta && meta.user && (meta.user.distinct_id || String(meta.user));
569
580
  const isNewUser = userId && userId.charCodeAt(0) % 2 === 0;
570
581
  if (isNewUser) {
571
- record.conversionRate = Math.min(98, record.conversionRate * 1.4);
572
582
  record.props.first_order_bonus = true;
573
583
  } else {
574
584
  record.props.first_order_bonus = false;
@@ -605,7 +615,7 @@ const config = {
605
615
  time: midTime,
606
616
  user_id: firstEvent.user_id,
607
617
  subscription_tier: firstEvent.subscription_tier,
608
- platform: firstEvent.platform,
618
+ Platform: firstEvent.Platform,
609
619
  city: firstEvent.city,
610
620
  coupon_code: chance.pickone(couponCodes),
611
621
  discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
@@ -673,6 +683,35 @@ const config = {
673
683
  // ═══════════════════════════════════════════════════════════════════
674
684
  if (type === "everything") {
675
685
  const userEvents = record;
686
+ const profile = meta.profile;
687
+
688
+ // ── Stamp superProps from profile so they stay consistent per user ──
689
+ if (profile) {
690
+ userEvents.forEach((event) => {
691
+ if (profile.Platform !== undefined) event.Platform = profile.Platform;
692
+ if (profile.subscription_tier !== undefined) event.subscription_tier = profile.subscription_tier;
693
+ if (profile.city !== undefined) event.city = profile.city;
694
+ });
695
+ }
696
+
697
+ // ── Bug 2 fix: Conversion filtering for lunch rush + first order ──
698
+ // Hook 1: Non-lunch/dinner-hour events → drop ~30% of final funnel step
699
+ // Hook 8: Returning users (no first_order_bonus) → drop ~30% of final funnel step
700
+ for (let i = userEvents.length - 1; i >= 0; i--) {
701
+ const event = userEvents[i];
702
+ if (event.event === "order delivered") {
703
+ // Hook 1: if not during lunch or dinner rush, drop 30%
704
+ if (!event.lunch_rush && !event.dinner_rush && chance.bool({ likelihood: 30 })) {
705
+ userEvents.splice(i, 1);
706
+ continue;
707
+ }
708
+ // Hook 8: returning users (no first_order_bonus) drop 30%
709
+ if (event.first_order_bonus === false && chance.bool({ likelihood: 30 })) {
710
+ userEvents.splice(i, 1);
711
+ continue;
712
+ }
713
+ }
714
+ }
676
715
 
677
716
  // First pass: identify user patterns
678
717
  let isReferralUser = false;
@@ -296,7 +296,7 @@
296
296
  }
297
297
  ],
298
298
  "superProps": {
299
- "platform": [
299
+ "Platform": [
300
300
  "Mobile",
301
301
  "Xbox",
302
302
  "Playstation",
@@ -1,3 +1,12 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "my-seed";
3
+ const num_days = 180;
4
+ const num_users = 12_000;
5
+ const avg_events_per_user = 90;
6
+ let token = "your-mixpanel-token";
7
+
8
+ // ── env overrides ──
9
+ if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
1
10
 
2
11
  import dayjs from "dayjs";
3
12
  import utc from "dayjs/plugin/utc.js";
@@ -5,11 +14,8 @@ import "dotenv/config";
5
14
  import * as u from "../../lib/utils/utils.js";
6
15
  import * as v from "ak-tools";
7
16
 
8
- const SEED = "my-seed";
9
17
  dayjs.extend(utc);
10
18
  const chance = u.initChance(SEED);
11
- const num_users = 12_000;
12
- const days = 180;
13
19
 
14
20
  /** @typedef {import("../../types").Dungeon} Config */
15
21
 
@@ -154,10 +160,10 @@ const days = 180;
154
160
 
155
161
  /** @type {Config} */
156
162
  const config = {
157
- token: "",
163
+ token,
158
164
  seed: "i am gamer face",
159
- numDays: days,
160
- numEvents: num_users * 90,
165
+ numDays: num_days,
166
+ numEvents: num_users * avg_events_per_user,
161
167
  numUsers: num_users,
162
168
  hasAnonIds: false,
163
169
  hasSessionIds: false,
@@ -360,7 +366,7 @@ const config = {
360
366
  },
361
367
  ],
362
368
  superProps: {
363
- platform: [
369
+ Platform: [
364
370
  "Mobile",
365
371
  "Xbox",
366
372
  "Playstation",
@@ -404,6 +410,26 @@ const config = {
404
410
  }
405
411
  },
406
412
  userProps: {
413
+ Platform: [
414
+ "Mobile",
415
+ "Xbox",
416
+ "Playstation",
417
+ "Switch",
418
+ "Web"
419
+ ],
420
+ "game mode": u.pickAWinner([
421
+ "Single Player",
422
+ "Multiplayer",
423
+ ], 1),
424
+ language: [
425
+ "English",
426
+ "Spanish",
427
+ "French",
428
+ "German",
429
+ "Japanese",
430
+ "Korean",
431
+ "Chinese",
432
+ ],
407
433
  experiment: [
408
434
  "fast leveling",
409
435
  "tension economy",
@@ -511,7 +537,15 @@ const config = {
511
537
  }
512
538
 
513
539
  if (type === "everything") {
540
+ // Stamp superProps from profile for consistency
541
+ const profile = meta.profile;
542
+ record.forEach(e => {
543
+ e.Platform = profile.Platform;
544
+ e["game mode"] = profile["game mode"];
545
+ e.language = profile.language;
546
+ });
514
547
 
548
+ return record;
515
549
  }
516
550
 
517
551
  return record;
@@ -491,7 +491,7 @@
491
491
  "basic",
492
492
  "premium"
493
493
  ],
494
- "platform": [
494
+ "Platform": [
495
495
  "ios",
496
496
  "android",
497
497
  "web"
@@ -1,16 +1,23 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "dm4-healthcare";
3
+ const num_days = 100;
4
+ const num_users = 5_000;
5
+ const avg_events_per_user = 120;
6
+ let token = "your-mixpanel-token";
7
+
8
+ // ── env overrides ──
9
+ if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
+
1
11
  import dayjs from "dayjs";
2
12
  import utc from "dayjs/plugin/utc.js";
3
13
  import "dotenv/config";
4
14
  import * as u from "../../lib/utils/utils.js";
5
15
  import * as v from "ak-tools";
6
16
 
7
- const SEED = "dm4-healthcare";
8
17
  dayjs.extend(utc);
9
18
  const chance = u.initChance(SEED);
10
- const num_users = 5_000;
11
- const days = 100;
12
19
  const NOW = dayjs();
13
- const DATASET_START = NOW.subtract(days, "days");
20
+ const DATASET_START = NOW.subtract(num_days, "days");
14
21
 
15
22
  /** @typedef {import("../../types").Dungeon} Config */
16
23
 
@@ -199,22 +206,25 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
199
206
  * and experience levels that affect patient matching.
200
207
  *
201
208
  * ───────────────────────────────────────────────────────────────
202
- * 8. PREMIUM BOOKING FUNNEL LIFT (funnel-pre hook)
209
+ * 8. FREE-TIER CONVERSION DROP (everything hook)
203
210
  * ───────────────────────────────────────────────────────────────
204
211
  *
205
- * PATTERN: Users with subscription_tier "premium" get 1.4x conversion
206
- * rate on the booking funnel. Premium patients have priority scheduling.
212
+ * PATTERN: Free-tier users lose ~30% of "consultation completed"
213
+ * events (last step of the Booking to Consultation funnel).
214
+ * This is implemented via event filtering in the everything hook
215
+ * rather than conversionRate modification in funnel-pre, so the
216
+ * effect is not diluted by organic (non-funnel) events.
207
217
  *
208
218
  * HOW TO FIND IT IN MIXPANEL:
209
219
  *
210
220
  * Report 1: Booking Conversion by Tier
211
221
  * • Report type: Funnels
212
222
  * • Steps: "symptom search" → "appointment booked" → "consultation completed"
213
- * • Breakdown: "subscription_tier" (superProp)
214
- * • Expected: premium56% vs free ≈ 40% conversion
223
+ * • Breakdown: "subscription_tier"
224
+ * • Expected: free28% vs basic/premium ≈ 40% conversion
215
225
  *
216
- * REAL-WORLD ANALOGUE: Premium telehealth subscribers get priority
217
- * scheduling and shorter wait times, improving conversion.
226
+ * REAL-WORLD ANALOGUE: Free-tier patients face longer wait times
227
+ * and limited scheduling, reducing completed consultations.
218
228
  *
219
229
  * ═══════════════════════════════════════════════════════════════
220
230
  * EXPECTED METRICS SUMMARY
@@ -229,15 +239,15 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
229
239
  * Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
230
240
  * Occasional No-Shows | booking→consult | 95% | 75% | 0.79x
231
241
  * Doctor Specialization | years_experience | 5 | 22 | 4.4x
232
- * Premium Booking Lift | funnel conversion | 40% | 56% | 1.4x
242
+ * Free-Tier Conversion Drop | funnel conversion | 40% | 28% | 0.7x
233
243
  */
234
244
 
235
245
  /** @type {Config} */
236
246
  const config = {
237
- token: "",
247
+ token,
238
248
  seed: SEED,
239
- numDays: days,
240
- numEvents: num_users * 120,
249
+ numDays: num_days,
250
+ numEvents: num_users * avg_events_per_user,
241
251
  numUsers: num_users,
242
252
  hasAnonIds: false,
243
253
  hasSessionIds: true,
@@ -485,7 +495,7 @@ const config = {
485
495
  // ── SuperProps ──────────────────────────────────────────
486
496
  superProps: {
487
497
  subscription_tier: ["free", "free", "free", "basic", "basic", "premium"],
488
- platform: ["ios", "android", "web"],
498
+ Platform: ["ios", "android", "web"],
489
499
  },
490
500
 
491
501
  // ── UserProps ──────────────────────────────────────────
@@ -496,6 +506,8 @@ const config = {
496
506
  preferred_language: ["en", "en", "en", "en", "es", "pt", "de", "fr"],
497
507
  has_chronic_condition: [false, false, false, true],
498
508
  age_range: ["18-25", "26-35", "26-35", "36-45", "36-45", "46-55", "56-65", "65+"],
509
+ subscription_tier: ["free", "free", "free", "basic", "basic", "premium"],
510
+ Platform: ["ios", "android", "web"],
499
511
  },
500
512
 
501
513
  // ── Personas ──────────────────────────────────
@@ -645,16 +657,9 @@ const config = {
645
657
  }
646
658
 
647
659
  // ── HOOK 8: PREMIUM BOOKING FUNNEL LIFT (funnel-pre) ─
648
- // Premium subscribers convert 1.4x better through booking funnel.
660
+ // (conversionRate boost removed filtering applied in everything hook instead)
649
661
  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
- }
662
+ // no-op: conversion differentiation handled via event filtering below
658
663
  }
659
664
 
660
665
  // ── HOOK 1: AFTER-HOURS SURGE PRICING (event) ────────
@@ -687,18 +692,42 @@ const config = {
687
692
 
688
693
  // ── EVERYTHING HOOKS ─────────────────────────────────
689
694
  if (type === "everything") {
690
- const events = record;
691
- if (!events.length) return record;
695
+ if (!record.length) return record;
696
+ const profile = meta.profile;
697
+
698
+ // ── SUPER-PROP STAMPING ──────────────────────────
699
+ // Stamp superProps from profile so they are consistent per-user.
700
+ if (profile) {
701
+ const tier = profile.subscription_tier;
702
+ const plat = profile.Platform;
703
+ record.forEach(e => {
704
+ if (tier) e.subscription_tier = tier;
705
+ if (plat) e.Platform = plat;
706
+ });
707
+ }
708
+
709
+ // ── HOOK 8: FREE-TIER CONVERSION DROP ────────────
710
+ // Free-tier users lose ~30% of "consultation completed" events
711
+ // (last step of Booking to Consultation funnel), simulating
712
+ // lower conversion for non-paying patients.
713
+ if (profile && profile.subscription_tier === "free") {
714
+ record = record.filter(e => {
715
+ if (e.event === "consultation completed" && chance.bool({ likelihood: 30 })) {
716
+ return false;
717
+ }
718
+ return true;
719
+ });
720
+ }
692
721
 
693
722
  // ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
694
723
  // Users with >50 consultation events get boosted satisfaction scores.
695
724
  let consultCount = 0;
696
- events.forEach(e => {
725
+ record.forEach(e => {
697
726
  if (e.event === "consultation completed") consultCount++;
698
727
  });
699
728
 
700
729
  if (consultCount > 50) {
701
- events.forEach(e => {
730
+ record.forEach(e => {
702
731
  if (e.event === "consultation completed") {
703
732
  e.satisfaction_score = chance.floating({ min: 4.0, max: 5.0, fixed: 1 });
704
733
  }
@@ -707,18 +736,18 @@ const config = {
707
736
 
708
737
  // ── HOOK 4: VIDEO CONSULTATION FOLLOW-UP LIFT ────
709
738
  // Patients with video consultations get 2x follow-up events.
710
- const hasVideoConsult = events.some(e =>
739
+ const hasVideoConsult = record.some(e =>
711
740
  e.event === "consultation completed" && e.consultation_mode === "video"
712
741
  );
713
742
  if (hasVideoConsult) {
714
- const templateFollowUp = events.find(e => e.event === "follow up scheduled");
743
+ const templateFollowUp = record.find(e => e.event === "follow up scheduled");
715
744
  if (templateFollowUp) {
716
- const videoConsults = events.filter(e =>
745
+ const videoConsults = record.filter(e =>
717
746
  e.event === "consultation completed" && e.consultation_mode === "video"
718
747
  );
719
748
  videoConsults.forEach(vc => {
720
749
  if (chance.bool({ likelihood: 60 })) {
721
- events.push({
750
+ record.push({
722
751
  ...templateFollowUp,
723
752
  time: dayjs(vc.time).add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
724
753
  user_id: vc.user_id,
@@ -732,17 +761,17 @@ const config = {
732
761
 
733
762
  // ── HOOK 5: CHRONIC CONDITION REFILL CHAIN ───────
734
763
  // Patients with chronic prescriptions get refills every ~30 days.
735
- const chronicRxs = events.filter(e =>
764
+ const chronicRxs = record.filter(e =>
736
765
  e.event === "prescription issued" && e.condition_type === "chronic"
737
766
  );
738
767
  if (chronicRxs.length > 0) {
739
- const templateRefill = events.find(e => e.event === "prescription refill");
768
+ const templateRefill = record.find(e => e.event === "prescription refill");
740
769
  if (templateRefill) {
741
770
  chronicRxs.forEach(rx => {
742
771
  const rxTime = dayjs(rx.time);
743
772
  const refillsToAdd = chance.integer({ min: 2, max: 4 });
744
773
  for (let i = 1; i <= refillsToAdd; i++) {
745
- events.push({
774
+ record.push({
746
775
  ...templateRefill,
747
776
  time: rxTime.add(30 * i + chance.integer({ min: -3, max: 3 }), "days").toISOString(),
748
777
  user_id: rx.user_id,
@@ -757,10 +786,10 @@ const config = {
757
786
 
758
787
  // ── HOOK 6: OCCASIONAL PATIENT NO-SHOWS ──────────
759
788
  // 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);
789
+ if (record.length < 15) {
790
+ for (let i = record.length - 1; i >= 0; i--) {
791
+ if (record[i].event === "appointment booked" && chance.bool({ likelihood: 25 })) {
792
+ record.splice(i, 1);
764
793
  }
765
794
  }
766
795
  }
@@ -423,7 +423,7 @@
423
423
  }
424
424
  ],
425
425
  "superProps": {
426
- "platform": [
426
+ "Platform": [
427
427
  "web",
428
428
  "ios",
429
429
  "android"