@ak--47/dungeon-master 1.5.0 → 1.5.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 (66) hide show
  1. package/.claude/skills/create-dungeon/SKILL.md +139 -46
  2. package/.claude/skills/verify-dungeon/references/counting-semantics.md +31 -6
  3. package/.claude/skills/verify-dungeon/references/sql-recipes.md +44 -25
  4. package/.claude/skills/write-hooks/SKILL.md +31 -3
  5. package/CHANGELOG.md +85 -0
  6. package/HOOKS.md +13 -0
  7. package/dungeons/technical/ad-spend.js +41 -49
  8. package/dungeons/technical/anonymous-users.js +38 -36
  9. package/dungeons/technical/array-of-object-lookup.js +136 -153
  10. package/dungeons/technical/datagen-v15-verify.js +24 -11
  11. package/dungeons/technical/experiments.js +42 -40
  12. package/dungeons/technical/foobar.js +114 -118
  13. package/dungeons/technical/group-analytics.js +42 -40
  14. package/dungeons/technical/hook-helpers-verify.js +69 -50
  15. package/dungeons/technical/identity-model-verify.js +22 -12
  16. package/dungeons/technical/mirror-strategies.js +37 -39
  17. package/dungeons/technical/nested-objects.js +119 -118
  18. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  19. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  20. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  21. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  22. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  23. package/dungeons/technical/retention-cadence.js +115 -112
  24. package/dungeons/technical/sanity.js +86 -80
  25. package/dungeons/technical/scale-test.js +34 -38
  26. package/dungeons/technical/scd.js +111 -128
  27. package/dungeons/technical/simple.js +134 -141
  28. package/dungeons/technical/simplest.js +54 -62
  29. package/dungeons/technical/text-generation.js +110 -146
  30. package/dungeons/vertical/ai-platform.js +296 -333
  31. package/dungeons/vertical/community.js +284 -255
  32. package/dungeons/vertical/crypto.js +395 -391
  33. package/dungeons/vertical/dating.js +411 -378
  34. package/dungeons/vertical/devtools.js +336 -298
  35. package/dungeons/vertical/ecommerce.js +316 -394
  36. package/dungeons/vertical/education.js +369 -325
  37. package/dungeons/vertical/fintech.js +358 -325
  38. package/dungeons/vertical/fitness.js +335 -291
  39. package/dungeons/vertical/food-delivery.js +343 -307
  40. package/dungeons/vertical/gaming.js +480 -444
  41. package/dungeons/vertical/healthcare.js +306 -262
  42. package/dungeons/vertical/insurance-application.js +427 -409
  43. package/dungeons/vertical/logistics.js +271 -252
  44. package/dungeons/vertical/marketplace.js +333 -323
  45. package/dungeons/vertical/media.js +382 -335
  46. package/dungeons/vertical/real-estate.js +395 -346
  47. package/dungeons/vertical/sass.js +319 -333
  48. package/dungeons/vertical/social.js +368 -316
  49. package/dungeons/vertical/travel.js +297 -295
  50. package/index.js +46 -4
  51. package/lib/core/config-validator.js +126 -28
  52. package/lib/generators/funnels.js +4 -1
  53. package/lib/orchestrators/mixpanel-sender.js +7 -0
  54. package/lib/orchestrators/user-loop.js +132 -31
  55. package/lib/templates/defaults.js +59 -59
  56. package/lib/templates/macro-presets.js +14 -2
  57. package/lib/utils/dataset-context.js +103 -0
  58. package/lib/utils/retention-curve.js +140 -0
  59. package/lib/utils/utils.js +149 -38
  60. package/lib/verify/counting.js +40 -0
  61. package/lib/verify/emulate-breakdown.js +20 -1
  62. package/lib/verify/index.js +1 -0
  63. package/lib/verify/schema-validator.js +3 -1
  64. package/package.json +11 -2
  65. package/scripts/run-dungeon.mjs +12 -1
  66. package/types.d.ts +117 -1
@@ -1,78 +1,40 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "simple is best";
3
- const num_days = 120;
4
- const num_users = 42_000;
5
- const avg_events_per_user_per_day = 0.37;
6
- let token = "your-mixpanel-token";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
1
+ // ── IMPORTS ──
11
2
  import dayjs from "dayjs";
12
3
  import utc from "dayjs/plugin/utc.js";
13
4
  dayjs.extend(utc);
14
- import { uid, comma } from 'ak-tools';
15
- import { weighNumRange, date, integer, weighChoices, decimal, initChance } from "../../lib/utils/utils.js";
5
+ import { uid } from "ak-tools";
6
+ import { weighNumRange, integer, weighChoices, decimal, initChance } from "../../lib/utils/utils.js";
16
7
  import { scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
17
- const chance = initChance(SEED);
18
-
19
8
  /** @typedef {import("../../types").Dungeon} Config */
20
- 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"];
21
- const videoCategories = ["funny", "educational", "inspirational", "music", "news", "sports", "cooking", "DIY", "travel", "gaming"];
22
- const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"];
23
9
 
10
+ // ── OVERVIEW ──
24
11
  /*
25
- * ============================================================================
26
- * DATASET OVERVIEW
27
- * ============================================================================
28
- *
29
- * App: eCommerce marketplace with integrated video content
30
- * Scale: 50K users, 2M events, 108 days
31
- *
32
- * Core loop: Users browse products, watch videos, build carts, and check out.
33
- * A signup funnel converts browsers into registered users. Video engagement
34
- * (likes/dislikes) runs alongside the shopping flow.
35
- *
36
- * Events:
37
- * page view (10) > view item (8) > watch video (8) > like video (6)
38
- * > save item (5) > add to cart (4) > dislike video (4) > checkout (2) > sign up (1)
39
- *
40
- * Funnels:
41
- * - Signup Flow: page view → view item → save item → page viewsign up (50%)
42
- * - Video Likes: watch → like → watch → like (60%)
43
- * - Video Dislikes: watch dislike → watch → dislike (20%)
44
- * - eCommerce Purchase: view → view → add to cart → view → add to cart → checkout (15%)
45
- *
46
- * User props: title, luckyNumber, spiritAnimal
47
- * Super props: theme (light/dark/custom)
48
- * ============================================================================
12
+ * NAME: ShopStream
13
+ * APP: eCommerce marketplace with integrated video content. Users browse
14
+ * products, watch videos, build carts, and check out. A signup
15
+ * funnel converts anonymous browsers into registered users; video
16
+ * engagement (like/dislike) runs alongside the shopping flow.
17
+ * SCALE: 42,000 users, ~2M events, 121 days (2026-01-01 → 2026-05-01)
18
+ * CORE LOOP: page view → view item → save/add to cart → checkout (+ video like/dislike side loop)
19
+ *
20
+ * EVENTS (9):
21
+ * page view (10) > view item (8) > watch video (8) > like video (6) > save item (5)
22
+ * > add to cart (4) > dislike video (4) > checkout (2) > sign up (1)
23
+ *
24
+ * FUNNELS (4):
25
+ * - Signup Flow: page view view item save item page view sign up (50%)
26
+ * - Video Likes: watch video → like video → watch video → like video (60%)
27
+ * - Video Dislikes: watch video → dislike video → watch video → dislike video (20%)
28
+ * - eCommerce Purchase: view item → view item → add to cart → view item → add to cart checkout (15%, A/B/C experiment)
29
+ *
30
+ * USER PROPS: title, luckyNumber, spiritAnimal, theme
31
+ * SUPER PROPS: theme
32
+ * SCD PROPS: loyalty_tier (bronze/silver/gold/platinum, monthly fuzzy, max 8)
33
+ * GROUPS: none
49
34
  */
50
35
 
36
+ // ── HOOK STORIES ──
51
37
  /*
52
- * ============================================================================
53
- * ANALYTICS HOOKS (10 hooks)
54
- *
55
- * Adds 7. SIGNUP FLOW TIME-TO-CONVERT: gold/platinum loyalty 0.67x faster,
56
- * bronze 1.33x slower (everything hook via scaleFunnelTTC on the page-view-
57
- * to-sign-up window). Loyalty tier is determined from SCD data
58
- * (meta.scd.loyalty_tier) with deterministic hash fallback. Discover via
59
- * bound-sequence funnel TTC breakdown by loyalty tier.
60
- *
61
- * Adds 8. CHECKOUT FLOW EXPERIMENT: A/B/C experiment on the eCommerce
62
- * Purchase funnel — "Control", "Express Checkout" (1.25x conversion),
63
- * "Social Proof" (1.15x conversion, 0.9x TTC). Engine-managed via funnel
64
- * experiment config; starts 30 days before dataset end.
65
- *
66
- * Adds 9. DARK THEME POWER USERS: funnel-pre hook that boosts purchase
67
- * funnel conversion 1.3x for dark-theme users and penalizes light-theme
68
- * users to 0.85x. Discoverable by breakdown of funnel conversion by theme.
69
- *
70
- * Adds 10. SAVE-ITEM RETENTION: born-in-dataset users who save 2+ items
71
- * in their first 10 days retain long-term; those below threshold lose ~70%
72
- * of post-day-25 events. Discoverable by retention or frequency analysis
73
- * segmented by early save-item count.
74
- * ============================================================================
75
- *
76
38
  * ----------------------------------------------------------------------------
77
39
  * Hook 1: Signup Flow Improvement (event + everything)
78
40
  * ----------------------------------------------------------------------------
@@ -386,30 +348,69 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
386
348
  * ============================================================================
387
349
  */
388
350
 
351
+ // ── SCALE ──
352
+ const SEED = "simple is best";
353
+ const NUM_USERS = 42_000;
354
+ const DATASET_START = "2026-01-01T00:00:00Z";
355
+ const DATASET_END = "2026-05-01T23:59:59Z";
356
+ const EVENTS_PER_DAY = 0.37;
357
+ const token = process.env.MP_TOKEN || "your-mixpanel-token";
358
+
359
+ const chance = initChance(SEED);
360
+
361
+ // ── KNOBS (tweak these to reshape stories) ──
362
+ const SIGNUP_FIX_DAYS_AGO = 7;
363
+ const WATCH_INFLECTION_DAYS_AGO = 30;
364
+ const WATCH_FACTOR_MIN = 0.25;
365
+ const WATCH_FACTOR_MAX = 0.79;
366
+ const VIEW_SWEET_MIN = 3;
367
+ const VIEW_SWEET_MAX = 8;
368
+ const VIEW_OVER_THRESHOLD = 9;
369
+ const SWEET_CART_BOOST = 1.25;
370
+ const SWEET_CLONE_LIKELIHOOD = 45;
371
+ const OVER_CHECKOUT_DROP_LIKELIHOOD = 30;
372
+ const LOYALTY_TTC_FAST = 0.67;
373
+ const LOYALTY_TTC_SLOW = 1.33;
374
+ const LOYALTY_LOOKBACK_DAYS = 2;
375
+ const SAVE_RETENTION_MIN = 2;
376
+ const SAVE_RETENTION_WINDOW_DAYS = 10;
377
+ const SAVE_RETENTION_CUTOFF_DAYS = 25;
378
+ const SAVE_RETENTION_DROP_LIKELIHOOD = 70;
379
+ const QUALITY_FACTORS = {
380
+ "2160p": 1.5,
381
+ "1440p": 1.4,
382
+ "1080p": 1.3,
383
+ "720p": 1.15,
384
+ "480p": 1.0,
385
+ "360p": 0.85,
386
+ "240p": 0.7,
387
+ };
389
388
 
389
+ // ── DATA ARRAYS ──
390
+ const videoCategories = ["funny", "educational", "inspirational", "music", "news", "sports", "cooking", "DIY", "travel", "gaming"];
391
+ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"];
392
+ const productCategories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"];
393
+ const productDescriptors = ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"];
394
+ const productSuffixes = ["item", "product", "good", "merchandise", "thing", "object", "widget", "gadget", "device", "apparatus", "contraption", "instrument", "tool", "implement", "utensil", "appliance", "machine", "equipment", "gear", "kit", "set", "package"];
395
+ const assetPreview = [".png", ".jpg", ".jpeg", ".heic", ".mp4", ".mov", ".avi"];
396
+
397
+ // ── HELPER FUNCTIONS ──
390
398
  function makeProducts(maxItems = 5) {
391
399
  return function () {
392
- const categories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"];
393
- const descriptors = ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"];
394
- const suffix = ["item", "product", "good", "merchandise", "thing", "object", "widget", "gadget", "device", "apparatus", "contraption", "instrument", "tool", "implement", "utensil", "appliance", "machine", "equipment", "gear", "kit", "set", "package"];
395
- const assetPreview = ['.png', '.jpg', '.jpeg', '.heic', '.mp4', '.mov', '.avi'];
396
400
  const data = [];
397
401
  const numOfItems = integer(1, maxItems);
398
402
 
399
- for (var i = 0; i < numOfItems; i++) {
400
- const category = chance.pickone(categories);
401
- const descriptor = chance.pickone(descriptors);
402
- const suffixWord = chance.pickone(suffix);
403
- const slug = `${descriptor.replace(/\s+/g, '-').toLowerCase()}-${suffixWord.replace(/\s+/g, '-').toLowerCase()}`;
403
+ for (let i = 0; i < numOfItems; i++) {
404
+ const category = chance.pickone(productCategories);
405
+ const descriptor = chance.pickone(productDescriptors);
406
+ const suffixWord = chance.pickone(productSuffixes);
407
+ const slug = `${descriptor.replace(/\s+/g, "-").toLowerCase()}-${suffixWord.replace(/\s+/g, "-").toLowerCase()}`;
404
408
  const asset = chance.pickone(assetPreview);
405
409
 
406
- // const product_id = chance.guid();
407
410
  const price = integer(1, 100);
408
411
  const quantity = integer(1, 5);
409
412
 
410
- const item = {
411
- // product_id: product_id,
412
- // sku: integer(11111, 99999),
413
+ data.push({
413
414
  amount: price,
414
415
  quantity: quantity,
415
416
  total_value: price * quantity,
@@ -418,42 +419,219 @@ function makeProducts(maxItems = 5) {
418
419
  descriptor: descriptor,
419
420
  slug: slug,
420
421
  assetPreview: `https://example.com/assets/${slug}${asset}`,
421
- assetType: asset
422
-
423
- };
424
-
425
- data.push(item);
422
+ assetType: asset,
423
+ });
426
424
  }
427
425
 
428
426
  return () => [data];
429
427
  };
430
- };
428
+ }
429
+
430
+ function handleFunnelPreHooks(record, meta) {
431
+ // H9: Dark Theme Power Users — dark +30%, light -15%, custom unchanged
432
+ const isPurchaseFunnel = meta.funnel?.sequence?.includes("checkout");
433
+ if (isPurchaseFunnel) {
434
+ const theme = meta.profile?.theme;
435
+ if (theme === "dark") record.conversionRate = Math.min(95, Math.round(record.conversionRate * 1.3));
436
+ else if (theme === "light") record.conversionRate = Math.round(record.conversionRate * 0.85);
437
+ }
438
+ return record;
439
+ }
431
440
 
441
+ function handleEventHooks(record) {
442
+ // H5: Item flattening — promote first item's fields to top level
443
+ if (record.item && Array.isArray(record.item)) {
444
+ record = { ...record, ...record.item[0] };
445
+ delete record.item;
446
+ }
447
+
448
+ // H3: Toys + Shoes cart correlation
449
+ if (record.event === "checkout" && Array.isArray(record.cart)) {
450
+ const hasToys = record.cart.some(item => item.category === "toys");
451
+ const hasShoes = record.cart.some(item => item.category === "shoes");
452
+ if (hasToys && !hasShoes) {
453
+ const bigCart = makeProducts(20)()()[0];
454
+ const shoeItems = bigCart.filter(item => item.category === "shoes");
455
+ if (shoeItems.length > 0) record.cart.push(shoeItems[0]);
456
+ }
457
+ if (hasShoes && !hasToys) {
458
+ const bigCart = makeProducts(20)()()[0];
459
+ const toyItems = bigCart.filter(item => item.category === "toys");
460
+ if (toyItems.length > 0) record.cart.push(toyItems[0]);
461
+ }
462
+ if (!hasToys && !hasShoes) {
463
+ const cheapFactor = decimal(0.75, 0.9);
464
+ record.cart = record.cart.map(item => ({
465
+ ...item,
466
+ amount: Math.round(item.amount * cheapFactor),
467
+ total_value: Math.round(item.total_value * cheapFactor),
468
+ }));
469
+ }
470
+ }
432
471
 
433
- /** @type {import('../types.d.ts').Dungeon} */
472
+ // H4: Quality → watch time correlation
473
+ if (record.event === "watch video") {
474
+ const quality = record.quality || "480p";
475
+ const factor = QUALITY_FACTORS[quality] ?? 1.0;
476
+ record.watchTimeSec = Math.round(record.watchTimeSec * factor);
477
+ }
478
+
479
+ return record;
480
+ }
481
+
482
+ function handleEverythingHooks(record, meta) {
483
+ const profile = meta.profile;
484
+ record.forEach(e => {
485
+ e.theme = profile.theme;
486
+ });
487
+
488
+ const datasetEnd = dayjs.unix(meta.datasetEnd);
489
+ const DAY_SIGNUPS_IMPROVED = datasetEnd.subtract(SIGNUP_FIX_DAYS_AGO, "day");
490
+ const DAY_WATCH_TIME_WENT_UP = datasetEnd.subtract(WATCH_INFLECTION_DAYS_AGO, "day");
491
+
492
+ // H1: Signup flow v1 → v2; pre-fix v1 signups lose 50% to _drop
493
+ record.forEach(e => {
494
+ if (e.event !== "sign up") return;
495
+ const eventTime = dayjs(e.time);
496
+ e.signup_flow = "v1";
497
+ if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
498
+ if (chance.bool({ likelihood: 50 })) e._drop = true;
499
+ }
500
+ if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
501
+ e.signup_flow = "v2";
502
+ }
503
+ });
504
+
505
+ // H2: Watch time inflection — before: shrink, after: grow
506
+ record.forEach(e => {
507
+ if (e.event !== "watch video") return;
508
+ const eventTime = dayjs(e.time);
509
+ const factor = decimal(WATCH_FACTOR_MIN, WATCH_FACTOR_MAX);
510
+ if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
511
+ e.watchTimeSec = Math.round(e.watchTimeSec * (1 - factor));
512
+ }
513
+ if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
514
+ e.watchTimeSec = Math.round(e.watchTimeSec * (1 + factor));
515
+ }
516
+ });
517
+
518
+ // H6: View-item magic number — sweet spot (3-8) boost; over (9+) checkout drop
519
+ const viewItemCount = record.filter(e => e.event === "view item").length;
520
+ if (viewItemCount >= VIEW_SWEET_MIN && viewItemCount <= VIEW_SWEET_MAX) {
521
+ record.forEach(e => {
522
+ if (e.event === "checkout" && Array.isArray(e.cart)) {
523
+ e.cart = e.cart.map(it => ({
524
+ ...it,
525
+ amount: typeof it.amount === "number" ? Math.round(it.amount * SWEET_CART_BOOST) : it.amount,
526
+ total_value: typeof it.total_value === "number" ? Math.round(it.total_value * SWEET_CART_BOOST) : it.total_value,
527
+ }));
528
+ }
529
+ });
530
+ const addToCartEvents = record.filter(e => e.event === "add to cart");
531
+ const clones = [];
532
+ addToCartEvents.forEach(e => {
533
+ if (chance.bool({ likelihood: SWEET_CLONE_LIKELIHOOD })) {
534
+ const offsetMs = integer(60_000, 600_000);
535
+ clones.push({
536
+ ...e,
537
+ insert_id: uid(),
538
+ time: dayjs(e.time).add(offsetMs, "milliseconds").toISOString(),
539
+ });
540
+ }
541
+ });
542
+ if (clones.length > 0) record.push(...clones);
543
+ } else if (viewItemCount >= VIEW_OVER_THRESHOLD) {
544
+ for (let i = record.length - 1; i >= 0; i--) {
545
+ if (record[i].event === "checkout" && chance.bool({ likelihood: OVER_CHECKOUT_DROP_LIKELIHOOD })) {
546
+ record.splice(i, 1);
547
+ }
548
+ }
549
+ }
550
+
551
+ // H7: Signup-flow TTC scaled by loyalty tier (gold/plat fast, bronze slow)
552
+ {
553
+ let loyaltyTier = "silver";
554
+ const scdEntries = meta?.scd?.loyalty_tier;
555
+ if (Array.isArray(scdEntries) && scdEntries.length > 0) {
556
+ const latest = scdEntries.reduce((a, b) => (a.time > b.time ? a : b));
557
+ loyaltyTier = latest.loyalty_tier || "silver";
558
+ } else {
559
+ const uidStr = record[0]?.user_id || "";
560
+ const hash = uidStr.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
561
+ const bucket = hash % 100;
562
+ loyaltyTier = bucket < 20 ? "gold" : bucket < 50 ? "silver" : "bronze";
563
+ }
564
+ const ttcFactor = (
565
+ loyaltyTier === "gold" || loyaltyTier === "platinum" ? LOYALTY_TTC_FAST :
566
+ loyaltyTier === "bronze" ? LOYALTY_TTC_SLOW :
567
+ 1.0
568
+ );
569
+ if (ttcFactor !== 1.0) {
570
+ const funnelSteps = new Set(["page view", "view item", "save item", "sign up"]);
571
+ const sorted = record.slice().sort((a, b) => new Date(a.time) - new Date(b.time));
572
+ const signupEvent = sorted.find(e => e.event === "sign up");
573
+ if (signupEvent) {
574
+ const signupMs = new Date(signupEvent.time).getTime();
575
+ const windowMs = LOYALTY_LOOKBACK_DAYS * 24 * 60 * 60 * 1000;
576
+ const funnelEvents = sorted.filter(e =>
577
+ funnelSteps.has(e.event) &&
578
+ new Date(e.time).getTime() >= signupMs - windowMs &&
579
+ new Date(e.time).getTime() <= signupMs
580
+ );
581
+ if (funnelEvents.length >= 2) scaleFunnelTTC(funnelEvents, ttcFactor);
582
+ }
583
+ }
584
+ }
585
+
586
+ // H10: Save-item retention — born-in users below threshold churn after day 25
587
+ if (meta.userIsBornInDataset) {
588
+ const firstT = record[0]?.time;
589
+ if (firstT) {
590
+ const window10 = dayjs(firstT).add(SAVE_RETENTION_WINDOW_DAYS, "days").toISOString();
591
+ const saves = record.filter(e => e.event === "save item" && e.time <= window10).length;
592
+ if (saves < SAVE_RETENTION_MIN) {
593
+ const cutoff = dayjs(firstT).add(SAVE_RETENTION_CUTOFF_DAYS, "days");
594
+ for (let i = record.length - 1; i >= 0; i--) {
595
+ if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: SAVE_RETENTION_DROP_LIKELIHOOD })) {
596
+ record.splice(i, 1);
597
+ }
598
+ }
599
+ }
600
+ }
601
+ }
602
+
603
+ return record.filter(e => !e._drop);
604
+ }
605
+
606
+ // ── CONFIG ──
607
+ /** @type {Config} */
434
608
  const config = {
435
609
  version: 2,
436
- token,
437
610
  seed: SEED,
438
- datasetStart: "2026-01-01T00:00:00Z",
439
- datasetEnd: "2026-05-01T23:59:59Z",
440
- // numDays: num_days,
441
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
442
- numUsers: num_users,
443
- format: 'json', //csv or json
444
- region: "US",
445
- hasAnonIds: true,
446
- avgDevicePerUser: 2,
447
- hasSessionIds: true,
448
- hasAdSpend: false,
449
- hasLocation: true,
450
- hasAndroidDevices: true,
451
- hasIOSDevices: true,
452
- hasDesktopDevices: true,
453
- hasBrowser: true,
454
- hasCampaigns: false,
455
- isAnonymous: false,
456
- alsoInferFunnels: false,
611
+ datasetStart: DATASET_START,
612
+ datasetEnd: DATASET_END,
613
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
614
+ numUsers: NUM_USERS,
615
+ format: "json",
616
+ credentials: {
617
+ token,
618
+ region: "US",
619
+ },
620
+ switches: {
621
+ hasSessionIds: true,
622
+ hasAdSpend: false,
623
+ hasLocation: true,
624
+ hasAndroidDevices: true,
625
+ hasIOSDevices: true,
626
+ hasDesktopDevices: true,
627
+ hasBrowser: true,
628
+ hasCampaigns: false,
629
+ isAnonymous: false,
630
+ alsoInferFunnels: false,
631
+ },
632
+ identity: {
633
+ avgDevicePerUser: 2,
634
+ },
457
635
  concurrency: 1,
458
636
  events: [
459
637
  {
@@ -464,7 +642,7 @@ const config = {
464
642
  currency: ["USD", "CAD", "EUR", "BTC", "ETH", "JPY"],
465
643
  coupon: weighChoices(["none", "none", "none", "none", "10%OFF", "20%OFF", "10%OFF", "20%OFF", "30%OFF", "40%OFF", "50%OFF"]),
466
644
  cart: makeProducts(),
467
- }
645
+ },
468
646
  },
469
647
  {
470
648
  event: "add to cart",
@@ -476,12 +654,12 @@ const config = {
476
654
  quantity: weighNumRange(1, 5, 0.3),
477
655
  total_value: weighNumRange(1, 500, 0.3),
478
656
  featured: [true, false, false],
479
- category: ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"],
480
- descriptor: ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"],
657
+ category: productCategories,
658
+ descriptor: productDescriptors,
481
659
  slug: ["item"],
482
660
  assetPreview: ["https://example.com/assets/item.png"],
483
- assetType: [".png", ".jpg", ".jpeg", ".heic", ".mp4", ".mov", ".avi"],
484
- }
661
+ assetType: assetPreview,
662
+ },
485
663
  },
486
664
  {
487
665
  event: "view item",
@@ -493,12 +671,12 @@ const config = {
493
671
  quantity: weighNumRange(1, 5, 0.3),
494
672
  total_value: weighNumRange(1, 500, 0.3),
495
673
  featured: [true, false, false],
496
- category: ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"],
497
- descriptor: ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"],
674
+ category: productCategories,
675
+ descriptor: productDescriptors,
498
676
  slug: ["item"],
499
677
  assetPreview: ["https://example.com/assets/item.png"],
500
- assetType: [".png", ".jpg", ".jpeg", ".heic", ".mp4", ".mov", ".avi"],
501
- }
678
+ assetType: assetPreview,
679
+ },
502
680
  },
503
681
  {
504
682
  event: "save item",
@@ -510,12 +688,12 @@ const config = {
510
688
  quantity: weighNumRange(1, 5, 0.3),
511
689
  total_value: weighNumRange(1, 500, 0.3),
512
690
  featured: [true, false, false],
513
- category: ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"],
514
- descriptor: ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"],
691
+ category: productCategories,
692
+ descriptor: productDescriptors,
515
693
  slug: ["item"],
516
694
  assetPreview: ["https://example.com/assets/item.png"],
517
- assetType: [".png", ".jpg", ".jpeg", ".heic", ".mp4", ".mov", ".avi"],
518
- }
695
+ assetType: assetPreview,
696
+ },
519
697
  },
520
698
  {
521
699
  event: "page view",
@@ -523,33 +701,21 @@ const config = {
523
701
  isStrictEvent: false,
524
702
  properties: {
525
703
  page: ["/", "/help", "/account", "/watch", "/listen", "/product", "/people", "/peace"],
526
- }
704
+ },
527
705
  },
528
706
  {
529
707
  event: "watch video",
530
708
  weight: 8,
531
709
  isStrictEvent: false,
532
710
  properties: {
533
- watchTimeSec: weighNumRange(10, 600, .25),
711
+ watchTimeSec: weighNumRange(10, 600, 0.25),
534
712
  quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
535
713
  format: ["mp4", "avi", "mov", "mpg"],
536
714
  uploader_id: chance.guid.bind(chance),
537
- }
538
- },
539
- {
540
- event: "like video",
541
- weight: 6,
542
- properties: {
543
-
544
- }
545
- },
546
- {
547
- event: "dislike video",
548
- weight: 4,
549
- properties: {
550
-
551
- }
715
+ },
552
716
  },
717
+ { event: "like video", weight: 6, properties: {} },
718
+ { event: "dislike video", weight: 4, properties: {} },
553
719
  {
554
720
  event: "sign up",
555
721
  weight: 1,
@@ -559,9 +725,8 @@ const config = {
559
725
  signupMethod: ["email", "google", "facebook", "twitter", "linkedin", "github"],
560
726
  referral: weighChoices(["none", "none", "none", "friend", "ad", "ad", "ad", "friend", "friend", "friend", "friend"]),
561
727
  signup_flow: ["v1"],
562
- }
728
+ },
563
729
  },
564
-
565
730
  ],
566
731
  funnels: [
567
732
  {
@@ -572,8 +737,7 @@ const config = {
572
737
  isFirstFunnel: true,
573
738
  timeToConvert: 2,
574
739
  experiment: false,
575
- name: "Signup Flow"
576
-
740
+ name: "Signup Flow",
577
741
  },
578
742
  {
579
743
  sequence: ["watch video", "like video", "watch video", "like video"],
@@ -583,8 +747,8 @@ const config = {
583
747
  videoCategory: videoCategories,
584
748
  quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
585
749
  format: ["mp4", "avi", "mov", "mpg"],
586
- uploader_id: chance.guid.bind(chance)
587
- }
750
+ uploader_id: chance.guid.bind(chance),
751
+ },
588
752
  },
589
753
  {
590
754
  name: "Video Dislikes",
@@ -594,8 +758,8 @@ const config = {
594
758
  videoCategory: videoCategories,
595
759
  quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
596
760
  format: ["mp4", "avi", "mov", "mpg"],
597
- uploader_id: chance.guid.bind(chance)
598
- }
761
+ uploader_id: chance.guid.bind(chance),
762
+ },
599
763
  },
600
764
  {
601
765
  name: "eCommerce Purchase",
@@ -613,19 +777,14 @@ const config = {
613
777
  ],
614
778
  startDaysBeforeEnd: 30,
615
779
  },
616
- }
617
-
780
+ },
618
781
  ],
619
782
  superProps: {
620
783
  theme: ["light", "dark", "custom", "light", "dark"],
621
784
  },
622
- /*
623
- user properties work the same as event properties
624
- each key should be an array or function reference
625
- */
626
785
  userProps: {
627
786
  title: chance.profession.bind(chance),
628
- luckyNumber: weighNumRange(42, 420, .3),
787
+ luckyNumber: weighNumRange(42, 420, 0.3),
629
788
  spiritAnimal: spiritAnimals,
630
789
  theme: ["light", "dark", "custom", "light", "dark"],
631
790
  },
@@ -634,256 +793,19 @@ const config = {
634
793
  values: ["bronze", "silver", "gold", "platinum"],
635
794
  frequency: "month",
636
795
  timing: "fuzzy",
637
- max: 8
638
- }
796
+ max: 8,
797
+ },
639
798
  },
640
799
  mirrorProps: {},
641
-
642
- /*
643
- for group analytics keys, we need an array of arrays [[],[],[]]
644
- each pair represents a group_key and the number of profiles for that key
645
- */
646
800
  groupKeys: [],
647
801
  groupProps: {},
648
802
  lookupTables: [],
649
- hook: function (record, type, meta) {
650
-
651
- // H9: Dark Theme Power Users (funnel-pre)
652
- // Dark-theme users convert 1.3x better on the purchase funnel;
653
- // light-theme users convert 0.85x. Custom-theme is unchanged.
654
- if (type === "funnel-pre") {
655
- const isPurchaseFunnel = meta.funnel?.sequence?.includes("checkout");
656
- if (isPurchaseFunnel) {
657
- const theme = meta.profile?.theme;
658
- if (theme === "dark") record.conversionRate = Math.min(95, Math.round(record.conversionRate * 1.3));
659
- else if (theme === "light") record.conversionRate = Math.round(record.conversionRate * 0.85);
660
- }
661
- }
662
-
663
- if (type === "event") {
664
- // unflattering 'items'
665
- if (record.item && Array.isArray(record.item)) {
666
- record = { ...record, ...record.item[0] };
667
- delete record.item;
668
- }
669
-
670
- // toys + shoes frequently purchases together (and are higher cart values)
671
- if (record.event === 'checkout' && Array.isArray(record.cart)) {
672
- const hasToys = record.cart.some(item => item.category === 'toys');
673
- const hasShoes = record.cart.some(item => item.category === 'shoes');
674
- if (hasToys && !hasShoes) {
675
- const bigCart = makeProducts(20)()()[0];
676
- const shoeItems = bigCart.filter(item => item.category === 'shoes');
677
- if (shoeItems.length > 0) {
678
- record.cart.push(shoeItems[0]);
679
- }
680
- }
681
-
682
- if (hasShoes && !hasToys) {
683
- const bigCart = makeProducts(20)()()[0];
684
- const toyItems = bigCart.filter(item => item.category === 'toys');
685
- if (toyItems.length > 0) {
686
- record.cart.push(toyItems[0]);
687
- }
688
- }
689
-
690
- if (!hasToys && !hasShoes) {
691
- const cheapFactor = decimal(.75, 0.9);
692
- // make every item a bit cheaper
693
- record.cart = record.cart.map(item => {
694
- return {
695
- ...item,
696
- amount: Math.round(item.amount * cheapFactor),
697
- total_value: Math.round(item.total_value * cheapFactor)
698
- };
699
- });
700
- }
701
- }
702
- // high quality video means longer watch times (lower quality shorter watch times)
703
- if (record.event === 'watch video') {
704
- const qualityFactors = {
705
- "2160p": 1.5,
706
- "1440p": 1.4,
707
- "1080p": 1.3,
708
- "720p": 1.15,
709
- "480p": 1.0,
710
- "360p": 0.85,
711
- "240p": 0.7
712
- };
713
- const quality = record.quality || "480p";
714
- const factor = qualityFactors[quality] || 1.0;
715
- record.watchTimeSec = Math.round(record.watchTimeSec * factor);
716
- }
717
-
718
-
719
-
720
- }
721
-
722
-
723
-
724
- if (type === "everything") {
725
- // Stamp superProps from profile for consistency
726
- const profile = meta.profile;
727
- record.forEach(e => {
728
- e.theme = profile.theme;
729
- });
730
-
731
- const datasetEnd = dayjs.unix(meta.datasetEnd);
732
- const DAY_SIGNUPS_IMPROVED = datasetEnd.subtract(7, 'day');
733
- const DAY_WATCH_TIME_WENT_UP = datasetEnd.subtract(30, 'day');
734
-
735
- // Hook 1: Signup flow improvement — pre-fix signups (v1) get 50% dropped
736
- record.forEach(e => {
737
- if (e.event !== 'sign up') return;
738
- const eventTime = dayjs(e.time);
739
- e.signup_flow = "v1";
740
- if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
741
- if (chance.bool({ likelihood: 50 })) {
742
- e._drop = true;
743
- }
744
- }
745
- if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
746
- e.signup_flow = "v2";
747
- }
748
- });
749
-
750
- // Hook 2: Watch time inflection — before 30 days ago: reduce, after: increase
751
- record.forEach(e => {
752
- if (e.event !== 'watch video') return;
753
- const eventTime = dayjs(e.time);
754
- const factor = decimal(0.25, 0.79);
755
- if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
756
- e.watchTimeSec = Math.round(e.watchTimeSec * (1 - factor));
757
- }
758
- if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
759
- e.watchTimeSec = Math.round(e.watchTimeSec * (1 + factor));
760
- }
761
- });
762
-
763
- // Hook 6: View-Item Magic Number (behavioral, no flags)
764
- // Users in 3-8 view-item sweet spot show ~25% higher cart amounts
765
- // AND elevated add-to-cart rate (~45% boost via cloned events).
766
- // Users >=9 are over-engaged window-shoppers; ~30% of their checkouts drop.
767
- const viewItemCount = record.filter(e => e.event === 'view item').length;
768
- if (viewItemCount >= 3 && viewItemCount <= 8) {
769
- record.forEach(e => {
770
- if (e.event === 'checkout' && Array.isArray(e.cart)) {
771
- e.cart = e.cart.map(it => ({
772
- ...it,
773
- amount: typeof it.amount === 'number' ? Math.round(it.amount * 1.25) : it.amount,
774
- total_value: typeof it.total_value === 'number' ? Math.round(it.total_value * 1.25) : it.total_value
775
- }));
776
- }
777
- });
778
- // Clone ~45% of add-to-cart events to boost cart add rate
779
- const addToCartEvents = record.filter(e => e.event === 'add to cart');
780
- const clones = [];
781
- addToCartEvents.forEach(e => {
782
- if (chance.bool({ likelihood: 45 })) {
783
- const offsetMs = integer(60_000, 600_000); // 1-10 min offset
784
- clones.push({
785
- ...e,
786
- insert_id: uid(),
787
- time: dayjs(e.time).add(offsetMs, 'milliseconds').toISOString()
788
- });
789
- }
790
- });
791
- if (clones.length > 0) record.push(...clones);
792
- } else if (viewItemCount >= 9) {
793
- for (let i = record.length - 1; i >= 0; i--) {
794
- if (record[i].event === 'checkout' && chance.bool({ likelihood: 30 })) {
795
- record.splice(i, 1);
796
- }
797
- }
798
- }
799
-
800
- // Hook 7 (TTC): SIGNUP FLOW TIME-TO-CONVERT (everything)
801
- // Gold/platinum loyalty users complete the Signup Flow funnel
802
- // 0.67x faster; bronze users 1.33x slower. Loyalty tier is read
803
- // from SCD data (meta.scd.loyalty_tier); falls back to a
804
- // deterministic hash of user_id for consistent assignment.
805
- //
806
- // The funnel uses order:"first-and-last-fixed" so middle steps
807
- // can reorder. We anchor on the first "sign up" event, look back
808
- // up to 2 days for all funnel-step events in that window, and
809
- // scale the whole cluster via scaleFunnelTTC.
810
- {
811
- // Determine loyalty tier from SCD (latest entry) or hash fallback
812
- let loyaltyTier = "silver"; // default baseline
813
- const scdEntries = meta?.scd?.loyalty_tier;
814
- if (Array.isArray(scdEntries) && scdEntries.length > 0) {
815
- // Pick the latest SCD entry
816
- const latest = scdEntries.reduce((a, b) =>
817
- (a.time > b.time ? a : b));
818
- loyaltyTier = latest.loyalty_tier || "silver";
819
- } else {
820
- // Deterministic hash fallback: gold ~20%, silver ~30%, bronze ~50%
821
- const uid = record[0]?.user_id || "";
822
- const hash = uid.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
823
- const bucket = hash % 100;
824
- loyaltyTier = bucket < 20 ? "gold" : bucket < 50 ? "silver" : "bronze";
825
- }
826
-
827
- const ttcFactor = (
828
- loyaltyTier === "gold" || loyaltyTier === "platinum" ? 0.67 :
829
- loyaltyTier === "bronze" ? 1.33 :
830
- 1.0
831
- );
832
- if (ttcFactor !== 1.0) {
833
- // Find signup funnel: anchor on first "sign up", look back 2 days
834
- // for the cluster of funnel-step events
835
- const funnelSteps = new Set(["page view", "view item", "save item", "sign up"]);
836
- const sorted = record.slice().sort((a, b) =>
837
- new Date(a.time) - new Date(b.time));
838
- const signupEvent = sorted.find(e => e.event === "sign up");
839
- if (signupEvent) {
840
- const signupMs = new Date(signupEvent.time).getTime();
841
- const windowMs = 2 * 24 * 60 * 60 * 1000; // 2-day lookback
842
- const funnelEvents = sorted.filter(e =>
843
- funnelSteps.has(e.event) &&
844
- new Date(e.time).getTime() >= signupMs - windowMs &&
845
- new Date(e.time).getTime() <= signupMs
846
- );
847
- if (funnelEvents.length >= 2) {
848
- scaleFunnelTTC(funnelEvents, ttcFactor);
849
- }
850
- }
851
- }
852
- }
853
-
854
- }
855
-
856
- // H10: Save-Item Retention (everything — retention magic number)
857
- // Born-in-dataset users with 2+ "save item" in their first 10 days
858
- // retain; users below the threshold lose ~70% of events after day 25.
859
- if (type === "everything" && meta.userIsBornInDataset) {
860
- const firstT = record[0]?.time;
861
- if (firstT) {
862
- const window10 = dayjs(firstT).add(10, 'days').toISOString();
863
- const saves = record.filter(e => e.event === 'save item' && e.time <= window10).length;
864
- if (saves < 2) {
865
- const cutoff = dayjs(firstT).add(25, 'days');
866
- for (let i = record.length - 1; i >= 0; i--) {
867
- if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: 70 })) {
868
- record.splice(i, 1);
869
- }
870
- }
871
- }
872
- }
873
- }
874
-
875
- // Filter out events tagged for removal in the event hook
876
- if (type === "everything") {
877
- record = record.filter(e => !e._drop);
878
- }
879
-
803
+ hook(record, type, meta) {
804
+ if (type === "funnel-pre") return handleFunnelPreHooks(record, meta);
805
+ if (type === "event") return handleEventHooks(record);
806
+ if (type === "everything") return handleEverythingHooks(record, meta);
880
807
  return record;
881
- }
808
+ },
882
809
  };
883
810
 
884
- function flip(likelihood = 50) {
885
- return chance.bool({ likelihood });
886
- }
887
-
888
-
889
811
  export default config;