@ak--47/dungeon-master 1.3.0 → 1.4.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 (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-healthcare";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -16,9 +16,6 @@ import * as v from "ak-tools";
16
16
 
17
17
  dayjs.extend(utc);
18
18
  const chance = u.initChance(SEED);
19
- const NOW = dayjs();
20
- const DATASET_START = NOW.subtract(num_days, "days");
21
-
22
19
  /** @typedef {import("../../types").Dungeon} Config */
23
20
 
24
21
  // Generate consistent doctor/clinic IDs at module level
@@ -54,7 +51,10 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
54
51
 
55
52
  /**
56
53
  * ═══════════════════════════════════════════════════════════════
57
- * ANALYTICS HOOKS (8 hooks)
54
+ * ANALYTICS HOOKS (10 hooks)
55
+ *
56
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
57
+ * via raw-prop breakdowns (HOD, day, tier) or behavioral cohorts.
58
58
  * ═══════════════════════════════════════════════════════════════
59
59
  *
60
60
  * ───────────────────────────────────────────────────────────────
@@ -70,9 +70,9 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
70
70
  * • Report type: Insights
71
71
  * • Event: "consultation completed"
72
72
  * • Measure: Average of "consultation_fee"
73
- * • Breakdown: "after_hours"
74
- * • Expected: after_hours=true should show ~1.5x avg fee vs false
75
- * (true ≈ $112, false ≈ $75)
73
+ * • Breakdown: hour of day
74
+ * • Expected: hours 19-06 UTC ~ 1.5x avg fee vs hours 07-18
75
+ * (after-hours ≈ $112, business ≈ $75)
76
76
  *
77
77
  * REAL-WORLD ANALOGUE: Telehealth platforms charge premiums for
78
78
  * after-hours urgent consultations, a key revenue driver.
@@ -226,30 +226,82 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
226
226
  * REAL-WORLD ANALOGUE: Free-tier patients face longer wait times
227
227
  * and limited scheduling, reducing completed consultations.
228
228
  *
229
+ * ───────────────────────────────────────────────────────────────
230
+ * 9. BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
231
+ *
232
+ * PATTERN: Premium tier completes Booking funnel 1.35x faster
233
+ * (factor 0.74); Free tier 1.33x slower (factor 1.33).
234
+ *
235
+ * HOW TO FIND IT IN MIXPANEL:
236
+ *
237
+ * Report 1: Booking Funnel Median Time-to-Convert by Tier
238
+ * - Funnels > "symptom search" -> "appointment booked" -> "consultation completed"
239
+ * - Measure: Median time to convert
240
+ * - Breakdown: subscription_tier
241
+ * - Expected: premium ~ 0.74x; free ~ 1.33x
242
+ *
243
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
244
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
245
+ * show this — funnel-post adjusts gaps within funnel instances, not
246
+ * across the user's full event history.
247
+ *
248
+ * ───────────────────────────────────────────────────────────────
249
+ * 10. CONSULTATION-COUNT MAGIC NUMBER (everything)
250
+ *
251
+ * PATTERN: Sweet 3-6 consultations → +25% on consultation_fee.
252
+ * Over 7+ → days_until_followup multiplied by 1.5 (over-consulted
253
+ * patients wait 50% longer for next visit). No flag.
254
+ *
255
+ * HOW TO FIND IT IN MIXPANEL:
256
+ *
257
+ * Report 1: Avg Consultation Fee by Consult-Count Bucket
258
+ * - Cohort A: users with 3-6 "consultation completed"
259
+ * - Cohort B: users with 0-2
260
+ * - Event: "consultation completed"
261
+ * - Measure: Average of "consultation_fee"
262
+ * - Expected: A ~ 1.25x B
263
+ *
264
+ * Report 2: Follow-Up Wait Time on Heavy Consulters
265
+ * - Cohort C: users with >= 7 consultations
266
+ * - Cohort A: users with 3-6
267
+ * - Event: "follow up scheduled"
268
+ * - Measure: Average of "days_until_followup"
269
+ * - Expected: C ~ 1.5x A (longer gap before next visit)
270
+ *
271
+ * REAL-WORLD ANALOGUE: Engaged patients pay more; over-engaged
272
+ * patients hit care-fatigue and stretch the gap to next visit.
273
+ *
229
274
  * ═══════════════════════════════════════════════════════════════
230
275
  * EXPECTED METRICS SUMMARY
231
276
  * ═══════════════════════════════════════════════════════════════
232
277
  *
233
- * Hook | Metric | Baseline | Effect | Ratio
234
- * ────────────────────────────|─────────────────────|──────────|─────────|──────
235
- * After-Hours Pricing | consultation_fee | $75 | $112 | 1.5x
236
- * Flu Season Spike | respiratory appts | ~15% | ~60% | 4x
237
- * Experienced Doctor Sat. | satisfaction_score | 3.5 | 4.2 | 1.2x
238
- * Video Follow-Up Lift | follow-ups/user | 1.0 | 2.0 | 2x
239
- * Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
240
- * Occasional No-Shows | booking→consult | 95% | 75% | 0.79x
241
- * Doctor Specialization | years_experience | 5 | 22 | 4.4x
242
- * Free-Tier Conversion Drop | funnel conversion | 40% | 28% | 0.7x
278
+ * Hook | Metric | Baseline | Effect | Ratio
279
+ * ----------------------------|---------------------|----------|-----------|------
280
+ * After-Hours Pricing | consultation_fee | 1x | 1.5x | 1.5x
281
+ * Flu Season Spike | respiratory share | ~ 15% | ~ 60% | 4x
282
+ * Experienced Doctor Sat. | satisfaction_score | 3.5 | 4.2 | 1.2x
283
+ * Video Follow-Up Lift | follow-ups/user | 1x | 2x | 2x
284
+ * Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
285
+ * Occasional No-Shows | booking→consult | 95% | 75% | -20%
286
+ * Doctor Specialization | years_experience | 5 | 22 | 4.4x
287
+ * Free-Tier Conversion Drop | funnel conversion | 40% | 28% | -30%
288
+ * Booking T2C | median min by tier | 1x | 0.74/1.33x| ~ 1.8x range
289
+ * Consult-Count Magic Number | sweet consult fee | 1x | 1.25x | 1.25x
290
+ * Consult-Count Magic Number | over days_until_fu | 1x | 1.5x | +50%
243
291
  */
244
292
 
245
293
  /** @type {Config} */
246
294
  const config = {
295
+ version: 2,
247
296
  token,
248
297
  seed: SEED,
249
- numDays: num_days,
298
+ datasetStart: "2026-01-01T00:00:00Z",
299
+ datasetEnd: "2026-05-01T23:59:59Z",
300
+ // numDays: num_days,
250
301
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
251
302
  numUsers: num_users,
252
- hasAnonIds: false,
303
+ hasAnonIds: true,
304
+ avgDevicePerUser: 2,
253
305
  hasSessionIds: true,
254
306
  format: "json",
255
307
  gzip: true,
@@ -282,6 +334,7 @@ const config = {
282
334
  event: "account created",
283
335
  weight: 1,
284
336
  isFirstEvent: true,
337
+ isAuthEvent: true,
285
338
  properties: {
286
339
  referral_source: ["organic", "doctor_referral", "insurance_partner", "social_media", "search"],
287
340
  },
@@ -303,7 +356,6 @@ const config = {
303
356
  condition_type: ["general", "general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
304
357
  wait_time_hours: u.weighNumRange(1, 72, 0.4),
305
358
  appointment_type: ["new_patient", "follow_up", "follow_up", "urgent", "routine", "routine"],
306
- after_hours: [false],
307
359
  },
308
360
  },
309
361
  {
@@ -311,12 +363,11 @@ const config = {
311
363
  weight: 5,
312
364
  properties: {
313
365
  doctor_id: chance.pickone.bind(chance, doctorIds),
314
- consultation_mode: ["phone"], // default; feature rollout changes to video
366
+ consultation_mode: ["phone", "phone", "video"],
315
367
  duration_minutes: u.weighNumRange(5, 60, 0.6, 15),
316
368
  consultation_fee: u.weighNumRange(25, 200, 0.4, 75),
317
369
  satisfaction_score: u.weighNumRange(1, 5, 0.8, 3),
318
370
  condition_type: ["general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
319
- after_hours: [false],
320
371
  },
321
372
  },
322
373
  {
@@ -346,7 +397,7 @@ const config = {
346
397
  doctor_id: chance.pickone.bind(chance, doctorIds),
347
398
  days_until_followup: u.weighNumRange(3, 30, 0.5, 7),
348
399
  condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
349
- consultation_mode: ["phone"],
400
+ consultation_mode: ["phone", "phone", "video"],
350
401
  },
351
402
  },
352
403
  {
@@ -577,8 +628,8 @@ const config = {
577
628
  { name: "premium", price: 29.99 },
578
629
  ],
579
630
  lifecycle: {
580
- trialToPayRate: 0.30,
581
- upgradeRate: 0.08,
631
+ trialToPayRate: 0.55,
632
+ upgradeRate: 0.20,
582
633
  downgradeRate: 0.03,
583
634
  churnRate: 0.05,
584
635
  winBackRate: 0.10,
@@ -655,44 +706,38 @@ const config = {
655
706
  }
656
707
  }
657
708
 
658
- // ── HOOK 8: PREMIUM BOOKING FUNNEL LIFT (funnel-pre)
659
- // (conversionRate boost removed filtering applied in everything hook instead)
660
- if (type === "funnel-pre") {
661
- // no-op: conversion differentiation handled via event filtering below
662
- }
663
-
664
- // ── HOOK 1: AFTER-HOURS SURGE PRICING (event) ────────
665
- // Consultations between 7PM-7AM get 1.5x fee.
666
- if (type === "event") {
667
- if (record.event === "consultation completed" || record.event === "appointment booked") {
668
- const hour = dayjs(record.time).hour();
669
- if (hour >= 19 || hour < 7) {
670
- record.after_hours = true;
671
- if (record.consultation_fee) {
672
- record.consultation_fee = Math.floor(record.consultation_fee * 1.5);
709
+ // HOOK 9 (T2C): BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
710
+ // Premium tier completes Booking funnel 1.35x faster (factor 0.74);
711
+ // Free tier 1.33x slower (factor 1.33).
712
+ if (type === "funnel-post") {
713
+ const segment = meta?.profile?.subscription_tier;
714
+ if (Array.isArray(record) && record.length > 1) {
715
+ const factor = (
716
+ segment === "premium" ? 0.74 :
717
+ segment === "free" ? 1.33 :
718
+ 1.0
719
+ );
720
+ if (factor !== 1.0) {
721
+ for (let i = 1; i < record.length; i++) {
722
+ const prev = dayjs(record[i - 1].time);
723
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
724
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
673
725
  }
674
726
  }
675
727
  }
676
-
677
- // ── HOOK 2: FLU SEASON SPIKE (event) ─────────────
678
- // Days 50-70: respiratory conditions dominate, wait times double.
679
- const FLU_START = DATASET_START.add(50, "days");
680
- const FLU_END = DATASET_START.add(70, "days");
681
- const eventTime = dayjs(record.time);
682
- if (record.event === "appointment booked" && eventTime.isAfter(FLU_START) && eventTime.isBefore(FLU_END)) {
683
- if (chance.bool({ likelihood: 60 })) {
684
- record.condition_type = "respiratory";
685
- }
686
- if (record.condition_type === "respiratory") {
687
- record.wait_time_hours = Math.floor((record.wait_time_hours || 12) * 2);
688
- }
689
- }
690
728
  }
691
729
 
730
+ // (HOOK 1: AFTER-HOURS SURGE PRICING moved to everything hook — hour checks
731
+ // must run after bunchIntoSessions redistributes timestamps)
732
+ // (HOOK 2: FLU SEASON SPIKE moved to everything hook — same reason)
733
+
692
734
  // ── EVERYTHING HOOKS ─────────────────────────────────
693
735
  if (type === "everything") {
694
736
  if (!record.length) return record;
695
737
  const profile = meta.profile;
738
+ const datasetStart = dayjs.unix(meta.datasetStart);
739
+ const FLU_START = datasetStart.add(50, "days");
740
+ const FLU_END = datasetStart.add(70, "days");
696
741
 
697
742
  // ── SUPER-PROP STAMPING ──────────────────────────
698
743
  // Stamp superProps from profile so they are consistent per-user.
@@ -705,17 +750,36 @@ const config = {
705
750
  });
706
751
  }
707
752
 
753
+ // HOOK 1: AFTER-HOURS SURGE PRICING — consultations 7PM-7AM
754
+ // UTC get consultation_fee 1.5x. No flag — discover via HOD chart.
755
+ record.forEach(e => {
756
+ if (e.event === "consultation completed" || e.event === "appointment booked") {
757
+ const hour = new Date(e.time).getUTCHours();
758
+ if ((hour >= 19 || hour < 7) && e.consultation_fee) {
759
+ e.consultation_fee = Math.floor(e.consultation_fee * 1.5);
760
+ }
761
+ }
762
+ });
763
+
764
+ // HOOK 2: FLU SEASON SPIKE — d50-70 respiratory dominates, wait_time doubles.
765
+ // Runs in everything hook so timestamp checks see post-bunchIntoSessions times.
766
+ record.forEach(e => {
767
+ if (e.event !== "appointment booked") return;
768
+ const t = dayjs(e.time);
769
+ if (t.isAfter(FLU_START) && t.isBefore(FLU_END)) {
770
+ if (chance.bool({ likelihood: 60 })) e.condition_type = "respiratory";
771
+ if (e.condition_type === "respiratory") {
772
+ e.wait_time_hours = Math.floor((e.wait_time_hours || 12) * 2);
773
+ }
774
+ }
775
+ });
776
+
708
777
  // ── HOOK 8: FREE-TIER CONVERSION DROP ────────────
709
778
  // Free-tier users lose ~30% of "consultation completed" events
710
779
  // (last step of Booking to Consultation funnel), simulating
711
780
  // lower conversion for non-paying patients.
712
- if (profile && profile.subscription_tier === "free") {
713
- record = record.filter(e => {
714
- if (e.event === "consultation completed" && chance.bool({ likelihood: 30 })) {
715
- return false;
716
- }
717
- return true;
718
- });
781
+ if (profile && profile.subscription_tier === "free" && chance.bool({ likelihood: 30 })) {
782
+ record = record.filter(e => e.event !== "consultation completed");
719
783
  }
720
784
 
721
785
  // ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
@@ -783,16 +847,35 @@ const config = {
783
847
  }
784
848
  }
785
849
 
786
- // ── HOOK 6: OCCASIONAL PATIENT NO-SHOWS ──────────
787
- // Low-activity patients (< 15 events) lose 25% of appointments.
850
+ // HOOK 6: OCCASIONAL PATIENT NO-SHOWS — low-activity patients
851
+ // (< 15 events) lose 25% of consultations (they booked but didn't show).
788
852
  if (record.length < 15) {
789
853
  for (let i = record.length - 1; i >= 0; i--) {
790
- if (record[i].event === "appointment booked" && chance.bool({ likelihood: 25 })) {
854
+ if (record[i].event === "consultation completed" && chance.bool({ likelihood: 25 })) {
791
855
  record.splice(i, 1);
792
856
  }
793
857
  }
794
858
  }
795
859
 
860
+ // HOOK 10: CONSULTATION-COUNT MAGIC NUMBER (no flags)
861
+ // Sweet 3-6 consultations → +25% on consultation_fee. Over 7+ →
862
+ // drop 30% of "follow up scheduled" events (over-consulted →
863
+ // follow-up fatigue).
864
+ const consultCt = record.filter(e => e.event === "consultation completed").length;
865
+ if (consultCt >= 3 && consultCt <= 6) {
866
+ record.forEach(e => {
867
+ if (e.event === "consultation completed" && typeof e.consultation_fee === "number") {
868
+ e.consultation_fee = Math.round(e.consultation_fee * 1.25);
869
+ }
870
+ });
871
+ } else if (consultCt >= 7) {
872
+ record.forEach(e => {
873
+ if (e.event === "follow up scheduled" && typeof e.days_until_followup === "number") {
874
+ e.days_until_followup = Math.round(e.days_until_followup * 1.5);
875
+ }
876
+ });
877
+ }
878
+
796
879
  return record;
797
880
  }
798
881