@ak--47/dungeon-master 1.3.1 → 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 (51) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/hook-helpers-verify.js +89 -0
  3. package/dungeons/technical/identity-model-verify.js +47 -0
  4. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  5. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  6. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  7. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  8. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  9. package/dungeons/vertical/ai-platform.js +45 -52
  10. package/dungeons/vertical/community.js +11 -8
  11. package/dungeons/vertical/crypto.js +25 -24
  12. package/dungeons/vertical/dating.js +56 -48
  13. package/dungeons/vertical/devtools.js +25 -18
  14. package/dungeons/vertical/ecommerce.js +42 -38
  15. package/dungeons/vertical/education.js +24 -9
  16. package/dungeons/vertical/fintech.js +13 -8
  17. package/dungeons/vertical/fitness.js +73 -122
  18. package/dungeons/vertical/food-delivery.js +18 -19
  19. package/dungeons/vertical/gaming.js +19 -20
  20. package/dungeons/vertical/healthcare.js +11 -8
  21. package/dungeons/vertical/insurance-application.js +6 -3
  22. package/dungeons/vertical/logistics.js +15 -9
  23. package/dungeons/vertical/marketplace.js +36 -27
  24. package/dungeons/vertical/media.js +27 -25
  25. package/dungeons/vertical/real-estate.js +18 -7
  26. package/dungeons/vertical/sass.js +84 -68
  27. package/dungeons/vertical/social.js +46 -47
  28. package/dungeons/vertical/travel.js +8 -5
  29. package/lib/core/config-validator.js +136 -157
  30. package/lib/generators/events.js +49 -93
  31. package/lib/generators/funnels.js +202 -91
  32. package/lib/hook-helpers/_internal.js +23 -0
  33. package/lib/hook-helpers/cohort.js +124 -0
  34. package/lib/hook-helpers/identity.js +56 -0
  35. package/lib/hook-helpers/index.js +44 -0
  36. package/lib/hook-helpers/inject.js +99 -0
  37. package/lib/hook-helpers/mutate.js +151 -0
  38. package/lib/hook-helpers/timing.js +99 -0
  39. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  40. package/lib/hook-patterns/attributed-by-source.js +72 -0
  41. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  42. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  43. package/lib/hook-patterns/index.js +14 -0
  44. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  45. package/lib/orchestrators/user-loop.js +119 -269
  46. package/lib/utils/utils.js +29 -16
  47. package/lib/verify/emulate-breakdown.js +281 -0
  48. package/lib/verify/index.js +12 -0
  49. package/lib/verify/verify-dungeon.js +61 -0
  50. package/package.json +6 -4
  51. package/types.d.ts +397 -211
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-community";
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
 
@@ -31,7 +31,7 @@ const communityIds = v.range(1, 30).map(() => `COMM_${v.uid(4)}`);
31
31
  * users create articles, discuss topics, moderate content, and
32
32
  * build collaborative knowledge bases across fandoms.
33
33
  *
34
- * - 5,000 users over 100 days, ~600K events
34
+ * - 10,000 users over 120 days
35
35
  * - Multi-role system: power creators (5%), moderators (8%),
36
36
  * active contributors (25%), readers (45%), lurkers (17%)
37
37
  * - Core loop: sign up -> search -> read articles -> contribute -> discuss
@@ -262,14 +262,16 @@ const communityIds = v.range(1, 30).map(() => `COMM_${v.uid(4)}`);
262
262
 
263
263
  /** @type {Config} */
264
264
  const config = {
265
+ version: 2,
265
266
  token,
266
267
  seed: SEED,
267
268
  datasetStart: "2026-01-01T00:00:00Z",
268
- datasetEnd: "2026-04-28T23:59:59Z",
269
+ datasetEnd: "2026-05-01T23:59:59Z",
269
270
  // numDays: num_days,
270
271
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
271
272
  numUsers: num_users,
272
- hasAnonIds: false,
273
+ hasAnonIds: true,
274
+ avgDevicePerUser: 2,
273
275
  hasSessionIds: true,
274
276
  format: "json",
275
277
  gzip: true,
@@ -302,6 +304,7 @@ const config = {
302
304
  event: "account created",
303
305
  weight: 1,
304
306
  isFirstEvent: true,
307
+ isAuthEvent: true,
305
308
  properties: {
306
309
  referral_source: ["organic", "google_search", "reddit_referral", "youtube_link", "friend_invite"],
307
310
  },
@@ -618,7 +621,7 @@ const config = {
618
621
  name: "google_search",
619
622
  source: "google",
620
623
  medium: "organic",
621
- activeDays: [0, 100],
624
+ activeDays: [0, 120],
622
625
  dailyBudget: [100, 400],
623
626
  acquisitionRate: 0.03,
624
627
  userPersonaBias: { reader: 0.5, lurker: 0.3 },
@@ -627,7 +630,7 @@ const config = {
627
630
  name: "reddit_referral",
628
631
  source: "reddit",
629
632
  medium: "referral",
630
- activeDays: [0, 100],
633
+ activeDays: [0, 120],
631
634
  dailyBudget: [50, 200],
632
635
  acquisitionRate: 0.02,
633
636
  userPersonaBias: { active_contributor: 0.5, moderator: 0.3 },
@@ -636,7 +639,7 @@ const config = {
636
639
  name: "youtube_link",
637
640
  source: "youtube",
638
641
  medium: "referral",
639
- activeDays: [0, 100],
642
+ activeDays: [0, 120],
640
643
  dailyBudget: [75, 250],
641
644
  acquisitionRate: 0.02,
642
645
  userPersonaBias: { lurker: 0.5, reader: 0.3 },
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "coinnest";
3
3
  const num_days = 120;
4
- const num_users = 6_000;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 0.83;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -200,7 +200,7 @@ const chance = u.initChance(SEED);
200
200
  * ---------------------------------------------------------------
201
201
  * 8. RUG-PULL AFTERMATH (everything)
202
202
  *
203
- * PATTERN: ~8% of pre-day-70 swaps get token_pair flipped to a SCAM
203
+ * PATTERN: ~2% of pre-day-70 swaps get token_pair flipped to a SCAM
204
204
  * pair. Users who held SCAM lose 80% of post-day-70 events. No flag.
205
205
  *
206
206
  * HOW TO FIND IT IN MIXPANEL:
@@ -307,14 +307,16 @@ const NFT_COLLECTIONS = [
307
307
 
308
308
  /** @type {Config} */
309
309
  const config = {
310
+ version: 2,
310
311
  token,
311
312
  seed: SEED,
312
313
  datasetStart: "2026-01-01T00:00:00Z",
313
- datasetEnd: "2026-04-28T23:59:59Z",
314
+ datasetEnd: "2026-05-01T23:59:59Z",
314
315
  // numDays: num_days,
315
316
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
316
317
  numUsers: num_users,
317
- hasAnonIds: false,
318
+ hasAnonIds: true,
319
+ avgDevicePerUser: 2,
318
320
  hasSessionIds: true,
319
321
  format: "json",
320
322
  gzip: true,
@@ -369,6 +371,7 @@ const config = {
369
371
  event: "wallet connected",
370
372
  weight: 1,
371
373
  isFirstEvent: true,
374
+ isAuthEvent: true,
372
375
  properties: {
373
376
  wallet_type: ["MetaMask", "Phantom", "Coinbase Wallet", "Rainbow", "WalletConnect"],
374
377
  chain: ["Ethereum", "Solana", "Base", "Arbitrum", "Polygon"],
@@ -575,21 +578,7 @@ const config = {
575
578
  });
576
579
  }
577
580
 
578
- // HOOK 2: GAS PRICE SPIKE days 35-37 swap+transfer gas_fee_usd 10x,
579
- // 40% of swaps get swap_status=failed. Runs in everything hook so
580
- // timestamp checks see post-bunchIntoSessions times.
581
- const day35 = datasetStart.add(35, "days");
582
- const day38 = datasetStart.add(38, "days");
583
- userEvents.forEach(e => {
584
- if (e.event !== "swap" && e.event !== "transfer") return;
585
- const t = dayjs(e.time);
586
- if (t.isAfter(day35) && t.isBefore(day38)) {
587
- e.gas_fee_usd = Math.round((e.gas_fee_usd || 5) * 10);
588
- if (e.event === "swap" && chance.bool({ likelihood: 40 })) {
589
- e.swap_status = "failed";
590
- }
591
- }
592
- });
581
+ // HOOK 2 moved to end (after all cloning) for full coverage
593
582
 
594
583
  // HOOK 3: TOKEN LAUNCH SURGE — after d50, 25% of swaps flip token_pair to MOON.
595
584
  const day50 = datasetStart.add(50, "days");
@@ -767,15 +756,11 @@ const config = {
767
756
  // -----------------------------------------------------------
768
757
  const day70 = datasetStart.add(70, "days");
769
758
 
770
- // Check if user traded SCAM before day 70
771
- // ~10% of pre-day-70 swaps naturally get SCAM token pair
772
- // (injected here since SCAM isn't in the default TOKEN_PAIRS list)
773
759
  let hadScam = false;
774
760
  userEvents.forEach(e => {
775
761
  if (e.event === "swap") {
776
762
  const swapDay = dayjs(e.time).diff(datasetStart, "day");
777
- // Before day 70, ~8% of swaps randomly become SCAM trades
778
- if (swapDay >= 10 && swapDay < 70 && chance.bool({ likelihood: 8 })) {
763
+ if (swapDay >= 10 && swapDay < 70 && chance.bool({ likelihood: 2 })) {
779
764
  e.token_pair = chance.pickone(["SCAM/USDC", "SCAM/ETH", "ETH/SCAM"]);
780
765
  hadScam = true;
781
766
  }
@@ -820,6 +805,22 @@ const config = {
820
805
  }
821
806
  }
822
807
 
808
+ // HOOK 2: GAS PRICE SPIKE — days 35-37 swap+transfer gas_fee_usd 10x,
809
+ // 40% of swaps get swap_status=failed. Runs AFTER all cloning to
810
+ // catch events that land in the spike window from clone offsets.
811
+ const day35 = datasetStart.add(35, "days");
812
+ const day38 = datasetStart.add(38, "days");
813
+ userEvents.forEach(e => {
814
+ if (e.event !== "swap" && e.event !== "transfer") return;
815
+ const t = dayjs(e.time);
816
+ if (t.isAfter(day35) && t.isBefore(day38)) {
817
+ e.gas_fee_usd = Math.round((e.gas_fee_usd || 5) * 10);
818
+ if (e.event === "swap" && chance.bool({ likelihood: 40 })) {
819
+ e.swap_status = "failed";
820
+ }
821
+ }
822
+ });
823
+
823
824
  userEvents.sort((a, b) => new Date(a.time) - new Date(b.time));
824
825
  }
825
826
 
@@ -81,7 +81,8 @@ const chance = u.initChance(SEED);
81
81
  * -------------------------------------------------------------------------------------
82
82
  * 2. WEEKEND SWIPE SURGE (everything)
83
83
  * -------------------------------------------------------------------------------------
84
- * PATTERN: Sunday 18-23 UTC swipes get 1.5x duplication. No flag.
84
+ * PATTERN: Sunday swipes get heavy cloning (evening 4x, daytime 2x)
85
+ * to overcome soup DOW weight deficit. No flag.
85
86
  *
86
87
  * HOW TO FIND IT IN MIXPANEL:
87
88
  * Report 1: Swipe Volume by Day of Week
@@ -215,14 +216,16 @@ const chance = u.initChance(SEED);
215
216
 
216
217
  /** @type {Config} */
217
218
  const config = {
219
+ version: 2,
218
220
  token,
219
221
  seed: SEED,
220
222
  datasetStart: "2026-01-01T00:00:00Z",
221
- datasetEnd: "2026-04-28T23:59:59Z",
223
+ datasetEnd: "2026-05-01T23:59:59Z",
222
224
  // numDays: num_days,
223
225
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
224
226
  numUsers: num_users,
225
- hasAnonIds: false,
227
+ hasAnonIds: true,
228
+ avgDevicePerUser: 2,
226
229
  hasSessionIds: true,
227
230
  format: "json",
228
231
  gzip: true,
@@ -246,6 +249,7 @@ const config = {
246
249
  event: "profile created",
247
250
  weight: 1,
248
251
  isFirstEvent: true,
252
+ isAuthEvent: true,
249
253
  properties: {
250
254
  age_range: ["18-24", "25-29", "30-34", "35-39", "40+"],
251
255
  gender: ["Male", "Male", "Female", "Female", "Non-binary"],
@@ -544,24 +548,22 @@ const config = {
544
548
  });
545
549
  }
546
550
 
547
- // HOOK 2: WEEKEND SWIPE SURGE — Sunday 18-23 UTC swipes get cloned
548
- // 1.5x. No flag discover via day-of-week chart.
551
+ // HOOK 2: WEEKEND SWIPE SURGE — Sunday swipes get heavy cloning
552
+ // to overcome the soup DOW weight deficit. Evening swipes (18-23)
553
+ // get 4 clones; daytime Sunday swipes get 2 clones.
554
+ // No flag — discover via day-of-week chart.
549
555
  for (let idx = events.length - 1; idx >= 0; idx--) {
550
556
  const event = events[idx];
551
557
  if (event.event === "swipe right") {
552
558
  const dow = new Date(event.time).getUTCDay();
553
- const hr = new Date(event.time).getUTCHours();
554
- if (dow === 0 && hr >= 18 && hr <= 23) {
559
+ if (dow === 0) {
560
+ const hr = new Date(event.time).getUTCHours();
561
+ const clones = (hr >= 18 && hr <= 23) ? 4 : 2;
555
562
  const etime = dayjs(event.time);
556
- events.push({
557
- ...event,
558
- time: etime.add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
559
- user_id: event.user_id,
560
- });
561
- if (chance.bool({ likelihood: 50 })) {
563
+ for (let c = 0; c < clones; c++) {
562
564
  events.push({
563
565
  ...event,
564
- time: etime.add(chance.integer({ min: 5, max: 60 }), "minutes").toISOString(),
566
+ time: etime.add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
565
567
  user_id: event.user_id,
566
568
  });
567
569
  }
@@ -587,15 +589,50 @@ const config = {
587
589
  });
588
590
  }
589
591
 
592
+ // HOOK 5: GHOSTING CHURN — users with match but no message within
593
+ // 48hrs lose 80% of post-match events. No flag.
594
+ // (runs BEFORE premium boost so injected premium matches survive)
595
+ if (matchEvents.length > 0) {
596
+ let hasTimely = false;
597
+ for (const m of matchEvents) {
598
+ const matchTime = dayjs(m.time);
599
+ const deadline = matchTime.add(48, "hours");
600
+ for (const msg of messageSentEvents) {
601
+ const msgTime = dayjs(msg.time);
602
+ if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
603
+ hasTimely = true;
604
+ break;
605
+ }
606
+ }
607
+ if (hasTimely) break;
608
+ }
609
+ if (!hasTimely) {
610
+ const earliestMatch = matchEvents.reduce((min, m) =>
611
+ dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
612
+ );
613
+ const churnAfter = dayjs(earliestMatch.time);
614
+ for (let i = events.length - 1; i >= 0; i--) {
615
+ if (dayjs(events[i].time).isAfter(churnAfter) && chance.bool({ likelihood: 80 })) {
616
+ events.splice(i, 1);
617
+ }
618
+ }
619
+ }
620
+ }
621
+
590
622
  // HOOK 4: PREMIUM MATCH BOOST — Premium 2x, Elite 4x match events.
591
623
  // Elite users also get profile-viewed events injected (see-who-liked-you).
592
- // Reads subscription from profile.
624
+ // Reads subscription from profile. Runs AFTER ghosting churn so
625
+ // injected matches are not culled.
593
626
  const sub = profile.subscription;
594
627
  if ((sub === "Premium" || sub === "Elite") && matchEvents.length > 0) {
595
- const multiplier = sub === "Elite" ? 3 : 1;
628
+ // Count surviving match events post-churn
629
+ const survivingMatches = events.filter(e => e.event === "match received");
630
+ const baseCount = survivingMatches.length || 1;
631
+ const targetMultiplier = sub === "Elite" ? 4 : 2;
632
+ const toAdd = Math.max(0, baseCount * targetMultiplier - baseCount);
596
633
  const matchTemplate = matchEvents[0];
597
- for (let i = 0; i < matchEvents.length * multiplier; i++) {
598
- const sourceMatch = matchEvents[i % matchEvents.length];
634
+ for (let i = 0; i < toAdd; i++) {
635
+ const sourceMatch = survivingMatches[i % survivingMatches.length] || matchTemplate;
599
636
  events.push({
600
637
  ...matchTemplate,
601
638
  time: dayjs(sourceMatch.time).add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
@@ -605,7 +642,7 @@ const config = {
605
642
  }
606
643
  if (sub === "Elite") {
607
644
  const viewTemplate = events.find(e => e.event === "profile viewed") || matchTemplate;
608
- matchEvents.forEach(m => {
645
+ survivingMatches.forEach(m => {
609
646
  events.push({
610
647
  ...viewTemplate,
611
648
  event: "profile viewed",
@@ -705,35 +742,6 @@ const config = {
705
742
  }
706
743
  }
707
744
 
708
- // HOOK 5: GHOSTING CHURN — users with match but no message within
709
- // 48hrs lose 80% of post-match events. No flag.
710
- if (matchEvents.length > 0) {
711
- let hasTimely = false;
712
- for (const m of matchEvents) {
713
- const matchTime = dayjs(m.time);
714
- const deadline = matchTime.add(48, "hours");
715
- for (const msg of messageSentEvents) {
716
- const msgTime = dayjs(msg.time);
717
- if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
718
- hasTimely = true;
719
- break;
720
- }
721
- }
722
- if (hasTimely) break;
723
- }
724
- if (!hasTimely) {
725
- const earliestMatch = matchEvents.reduce((min, m) =>
726
- dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
727
- );
728
- const churnAfter = dayjs(earliestMatch.time);
729
- for (let i = events.length - 1; i >= 0; i--) {
730
- if (dayjs(events[i].time).isAfter(churnAfter) && chance.bool({ likelihood: 80 })) {
731
- events.splice(i, 1);
732
- }
733
- }
734
- }
735
- }
736
-
737
745
  return record;
738
746
  }
739
747
 
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-devtools";
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
 
@@ -281,14 +281,16 @@ const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
281
281
 
282
282
  /** @type {Config} */
283
283
  const config = {
284
+ version: 2,
284
285
  token,
285
286
  seed: SEED,
286
287
  datasetStart: "2026-01-01T00:00:00Z",
287
- datasetEnd: "2026-04-28T23:59:59Z",
288
+ datasetEnd: "2026-05-01T23:59:59Z",
288
289
  // numDays: num_days,
289
290
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
290
291
  numUsers: num_users,
291
- hasAnonIds: false,
292
+ hasAnonIds: true,
293
+ avgDevicePerUser: 2,
292
294
  hasSessionIds: true,
293
295
  format: "json",
294
296
  gzip: true,
@@ -321,6 +323,7 @@ const config = {
321
323
  event: "account created",
322
324
  weight: 1,
323
325
  isFirstEvent: true,
326
+ isAuthEvent: true,
324
327
  properties: {
325
328
  referral_source: ["organic", "github", "conference", "blog_post", "colleague", "search"],
326
329
  },
@@ -841,11 +844,16 @@ const config = {
841
844
  }
842
845
 
843
846
  // -- HOOK 3: COPILOT PR VELOCITY ----------------------
844
- // Users with ai_assist="copilot" on any PR get 1.5x more PRs.
845
- const hasCopilot = events.some(e =>
846
- (e.event === "pull request created" || e.event === "code review completed") && e.ai_assist === "copilot"
847
- );
848
- if (hasCopilot) {
847
+ // ~30% of users are copilot adopters (hash-based cohort).
848
+ // Copilot users get ai_assist="copilot" stamped and 1.5x more PRs.
849
+ const uid = events[0]?.user_id || "";
850
+ const isCopilotUser = (typeof uid === "string" ? uid.charCodeAt(0) : uid) % 10 < 3;
851
+ if (isCopilotUser) {
852
+ events.forEach(e => {
853
+ if (e.event === "pull request created" || e.event === "code review completed") {
854
+ e.ai_assist = "copilot";
855
+ }
856
+ });
849
857
  const prEvents = events.filter(e => e.event === "pull request created");
850
858
  const extraCount = Math.floor(prEvents.length * 0.5);
851
859
  for (let i = 0; i < extraCount; i++) {
@@ -909,16 +917,15 @@ const config = {
909
917
  }
910
918
 
911
919
  // -- HOOK 9: BUILD-COUNT MAGIC NUMBER (no flags) ------
912
- // Sweet 15-30 builds → +30% deploys (clone with unique offset).
913
- // Over 31+ → drop 25% of deploys (flaky CI burnout).
920
+ // Sweet 15-30 builds → +50% deploys (clone with unique offset).
921
+ // Over 31+ → drop 40% of deploys (flaky CI burnout).
914
922
  const buildCount = events.filter(e => e.event === "build completed").length;
915
923
  if (buildCount >= 15 && buildCount <= 30) {
916
- const deployTemplate = events.find(e => e.event === "deployment completed");
917
- if (deployTemplate) {
918
- const deploys = events.filter(e => e.event === "deployment completed");
919
- const extras = Math.floor(deploys.length * 0.3);
920
- for (let k = 0; k < extras; k++) {
921
- const tpl = deploys[k % deploys.length];
924
+ const deploys = events.filter(e => e.event === "deployment completed");
925
+ const extras = Math.max(Math.floor(deploys.length * 0.5), 1);
926
+ for (let k = 0; k < extras; k++) {
927
+ const tpl = deploys[k % deploys.length];
928
+ if (tpl) {
922
929
  events.push({
923
930
  ...tpl,
924
931
  time: dayjs(tpl.time).add(chance.integer({ min: 5, max: 360 }), "minutes").toISOString(),
@@ -928,7 +935,7 @@ const config = {
928
935
  }
929
936
  } else if (buildCount >= 31) {
930
937
  for (let i = events.length - 1; i >= 0; i--) {
931
- if (events[i].event === "deployment completed" && chance.bool({ likelihood: 25 })) {
938
+ if (events[i].event === "deployment completed" && chance.bool({ likelihood: 40 })) {
932
939
  events.splice(i, 1);
933
940
  }
934
941
  }
@@ -1,20 +1,19 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "simple is best";
3
- const num_days = 108;
4
- const num_users = 50_000;
3
+ const num_days = 120;
4
+ const num_users = 42_000;
5
5
  const avg_events_per_user_per_day = 0.37;
6
6
  let token = "your-mixpanel-token";
7
7
 
8
8
  // ── env overrides ──
9
9
  if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
10
 
11
- import Chance from 'chance';
12
- let chance = new Chance();
13
11
  import dayjs from "dayjs";
14
12
  import utc from "dayjs/plugin/utc.js";
15
13
  dayjs.extend(utc);
16
14
  import { uid, comma } from 'ak-tools';
17
- import { weighNumRange, date, integer, weighChoices, decimal } from "../../lib/utils/utils.js";
15
+ import { weighNumRange, date, integer, weighChoices, decimal, initChance } from "../../lib/utils/utils.js";
16
+ const chance = initChance(SEED);
18
17
 
19
18
  /** @typedef {import("../../types").Dungeon} Config */
20
19
  const itemCategories = ["Books", "Movies", "Music", "Games", "Electronics", "Computers", "Smart Home", "Home", "Garden", "Pet", "Beauty", "Health", "Toys", "Kids", "Baby", "Handmade", "Sports", "Outdoors", "Automotive", "Industrial", "Entertainment", "Art", "Food", "Appliances", "Office", "Wedding", "Software"];
@@ -292,17 +291,19 @@ function makeProducts(maxItems = 5) {
292
291
 
293
292
  /** @type {import('../types.d.ts').Dungeon} */
294
293
  const config = {
294
+ version: 2,
295
295
  token,
296
296
  seed: SEED,
297
297
  datasetStart: "2026-01-01T00:00:00Z",
298
- datasetEnd: "2026-04-28T23:59:59Z",
298
+ datasetEnd: "2026-05-01T23:59:59Z",
299
299
  // numDays: num_days,
300
300
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
301
301
  numUsers: num_users,
302
302
  format: 'json', //csv or json
303
303
  region: "US",
304
- hasAnonIds: false, //if true, anonymousIds are created for each user
305
- hasSessionIds: false, //if true, hasSessionIds are created for each user
304
+ hasAnonIds: true,
305
+ avgDevicePerUser: 2,
306
+ hasSessionIds: true,
306
307
  hasAdSpend: false,
307
308
  hasLocation: true,
308
309
  hasAndroidDevices: true,
@@ -406,6 +407,7 @@ const config = {
406
407
  event: "sign up",
407
408
  weight: 1,
408
409
  isFirstEvent: true,
410
+ isAuthEvent: true,
409
411
  properties: {
410
412
  signupMethod: ["email", "google", "facebook", "twitter", "linkedin", "github"],
411
413
  referral: weighChoices(["none", "none", "none", "friend", "ad", "ad", "ad", "friend", "friend", "friend", "friend"]),
@@ -512,42 +514,12 @@ const config = {
512
514
  }
513
515
 
514
516
  if (type === "event") {
515
- const datasetEnd = dayjs.unix(meta.datasetEnd);
516
- const DAY_SIGNUPS_IMPROVED = datasetEnd.subtract(7, 'day');
517
- const DAY_WATCH_TIME_WENT_UP = datasetEnd.subtract(30, 'day');
518
- const eventTime = dayjs(record.time);
519
-
520
517
  // unflattering 'items'
521
518
  if (record.item && Array.isArray(record.item)) {
522
519
  record = { ...record, ...record.item[0] };
523
520
  delete record.item;
524
521
  }
525
522
 
526
- if (record.event === 'sign up') {
527
- record.signup_flow = "v1";
528
- if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
529
- // tag 50% for removal (filtered in "everything" hook)
530
- if (chance.bool({ likelihood: 50 })) {
531
- record._drop = true;
532
- }
533
- }
534
- if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
535
- record.signup_flow = "v2";
536
- }
537
- }
538
-
539
- if (record.event === 'watch video') {
540
- const factor = decimal(0.25, 0.79);
541
- if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
542
- record.watchTimeSec = Math.round(record.watchTimeSec * (1 - factor));
543
- }
544
- if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
545
- // increase watch time by 33%
546
- record.watchTimeSec = Math.round(record.watchTimeSec * (1 + factor));
547
- }
548
-
549
- }
550
-
551
523
  // toys + shoes frequently purchases together (and are higher cart values)
552
524
  if (record.event === 'checkout' && Array.isArray(record.cart)) {
553
525
  const hasToys = record.cart.some(item => item.category === 'toys');
@@ -609,6 +581,38 @@ const config = {
609
581
  e.theme = profile.theme;
610
582
  });
611
583
 
584
+ const datasetEnd = dayjs.unix(meta.datasetEnd);
585
+ const DAY_SIGNUPS_IMPROVED = datasetEnd.subtract(7, 'day');
586
+ const DAY_WATCH_TIME_WENT_UP = datasetEnd.subtract(30, 'day');
587
+
588
+ // Hook 1: Signup flow improvement — pre-fix signups (v1) get 50% dropped
589
+ record.forEach(e => {
590
+ if (e.event !== 'sign up') return;
591
+ const eventTime = dayjs(e.time);
592
+ e.signup_flow = "v1";
593
+ if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
594
+ if (chance.bool({ likelihood: 50 })) {
595
+ e._drop = true;
596
+ }
597
+ }
598
+ if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
599
+ e.signup_flow = "v2";
600
+ }
601
+ });
602
+
603
+ // Hook 2: Watch time inflection — before 30 days ago: reduce, after: increase
604
+ record.forEach(e => {
605
+ if (e.event !== 'watch video') return;
606
+ const eventTime = dayjs(e.time);
607
+ const factor = decimal(0.25, 0.79);
608
+ if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
609
+ e.watchTimeSec = Math.round(e.watchTimeSec * (1 - factor));
610
+ }
611
+ if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
612
+ e.watchTimeSec = Math.round(e.watchTimeSec * (1 + factor));
613
+ }
614
+ });
615
+
612
616
  // Hook 6: View-Item Magic Number (behavioral, no flags)
613
617
  // Users in 3-8 view-item sweet spot show ~25% higher cart amounts.
614
618
  // Users >=9 are over-engaged window-shoppers; ~30% of their checkouts drop.
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "harness-education";
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
 
@@ -28,7 +28,7 @@ const chance = u.initChance(SEED);
28
28
  * Supports self-paced and cohort-based learning with courses, quizzes,
29
29
  * assignments, and social study features.
30
30
  *
31
- * Scale: 5,000 users / 600K events / 100 days / 17 event types
31
+ * Scale: 10,000 users / ~1.4M events / 120 days / 17 event types
32
32
  *
33
33
  * CORE LOOP:
34
34
  * Register → browse/enroll in courses → watch lectures → practice problems →
@@ -314,14 +314,16 @@ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
314
314
 
315
315
  /** @type {Config} */
316
316
  const config = {
317
+ version: 2,
317
318
  token,
318
319
  seed: SEED,
319
320
  datasetStart: "2026-01-01T00:00:00Z",
320
- datasetEnd: "2026-04-28T23:59:59Z",
321
+ datasetEnd: "2026-05-01T23:59:59Z",
321
322
  // numDays: num_days,
322
323
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
323
324
  numUsers: num_users,
324
- hasAnonIds: false,
325
+ hasAnonIds: true,
326
+ avgDevicePerUser: 2,
325
327
  hasSessionIds: true,
326
328
  format: "json",
327
329
  gzip: true,
@@ -394,6 +396,7 @@ const config = {
394
396
  event: "account registered",
395
397
  weight: 1,
396
398
  isFirstEvent: true,
399
+ isAuthEvent: true,
397
400
  properties: {
398
401
  "account_type": ["instructor", "instructor", "instructor", "instructor", "instructor", "instructor", "student"],
399
402
  "signup_source": ["organic", "referral", "school_partnership", "social_ad"],
@@ -660,6 +663,17 @@ const config = {
660
663
  }
661
664
  }
662
665
 
666
+ // HOOK 7 (funnel-pre): FREE VS PAID — free users get 0.5x conversion rate;
667
+ // paid subscribers get 1.5x. Applies to course-completion funnel.
668
+ if (type === "funnel-pre") {
669
+ const subStatus = meta?.profile?.subscription_status;
670
+ if (subStatus === "free") {
671
+ record.conversionRate = Math.round(record.conversionRate * 0.5);
672
+ } else if (subStatus === "monthly" || subStatus === "annual") {
673
+ record.conversionRate = Math.min(100, Math.round(record.conversionRate * 1.5));
674
+ }
675
+ }
676
+
663
677
  // HOOK 9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
664
678
  // Annual subscribers complete the Course Completion funnel 1.4x
665
679
  // faster (factor 0.71); Free users 1.4x slower (factor 1.4).
@@ -732,11 +746,12 @@ const config = {
732
746
  }
733
747
  }
734
748
  } else if (notesTakenCount >= 9) {
735
- userEvents.forEach(e => {
736
- if (e.event === "certificate earned" && typeof e.final_grade === "number") {
737
- e.final_grade = Math.max(50, Math.round(e.final_grade * 0.5));
749
+ // Over-noters: drop 35% of certificates (stuck in study mode)
750
+ for (let i = userEvents.length - 1; i >= 0; i--) {
751
+ if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 35 })) {
752
+ userEvents.splice(i, 1);
738
753
  }
739
- });
754
+ }
740
755
  }
741
756
 
742
757
  // HOOK 8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.