@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,60 +1,54 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "harness-fintech";
3
- const num_days = 120;
4
- const num_users = 10_000;
5
- const avg_events_per_user_per_day = 1.2;
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";
4
+ dayjs.extend(utc);
13
5
  import "dotenv/config";
14
6
  import * as u from "../../lib/utils/utils.js";
15
7
  import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
16
-
17
- dayjs.extend(utc);
18
- const chance = u.initChance(SEED);
19
8
  /** @typedef {import("../../types").Dungeon} Config */
20
9
 
21
- /**
22
- * ===================================================================
23
- * DATASET OVERVIEW
24
- * ===================================================================
25
- *
26
- * NexBank a Chime/Revolut-style neobank app. Users open accounts
27
- * (personal or business), transact across 7 merchant categories,
28
- * send transfers, pay bills, set budgets, invest, apply for loans,
29
- * and earn tier-scaled rewards.
30
- *
31
- * Scale: 5,000 users · 600K events · 100 days · 18 event types
32
- * Groups: 500 households
33
- * Tiers: Basic (free) / Plus ($4.99/mo) / Premium ($14.99/mo)
34
- *
35
- * Core loop: onboarding daily banking financial planning
36
- * budgets & savings investments rewards & monetization
37
- *
38
- * Funnels:
39
- * - Onboarding: account opened → app session → balance checked
40
- * - Daily banking: app session → balance checked → transaction
41
- * - Transfers: app session → transfer sent → notification opened
42
- * - Bill payment: app session → bill paidnotification opened
43
- * - Financial planning: budget created budget alertsavings goal
44
- * - Investment: balance checked investment madereward redeemed
45
- * - Support: support contacted card lockeddispute filed
46
- * - Lending: loan applied loan approvedpremium upgraded
10
+ // ── OVERVIEW ──
11
+ /*
12
+ * NAME: NexBank
13
+ * APP: Chime/Revolut-style neobank app. Users open accounts (personal or
14
+ * business), transact across 7 merchant categories, send transfers,
15
+ * pay bills, set budgets, invest, apply for loans, and earn
16
+ * tier-scaled rewards. Core loop runs from onboarding through daily
17
+ * banking, financial planning, investments, and rewards.
18
+ * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
19
+ * CORE LOOP: account opened → app session → balance checked → transaction completed
20
+ *
21
+ * EVENTS (19):
22
+ * app session (20) > transaction completed (18) > balance checked (15)
23
+ * > notification opened (10) > transfer sent (8) > bill paid (6)
24
+ * > investment made (4) > reward redeemed (4) > budget alert (4)
25
+ * > budget created (3) > savings goal set (3) > support contacted (3)
26
+ * > card locked (2) > dispute filed (2) > loan applied (2) > premium upgraded (2)
27
+ * > account opened (1) > loan approved (1) > bill payment missed (1)
28
+ *
29
+ * FUNNELS (8):
30
+ * - Onboarding: account opened → app session → balance checked (85%)
31
+ * - Daily Banking: app session → balance checkedtransaction completed (80%)
32
+ * - Transfers: app sessiontransfer sentnotification opened (50%)
33
+ * - Bill Payment: app sessionbill paidnotification opened (60%)
34
+ * - Financial Planning: budget createdbudget alertsavings goal set (40%)
35
+ * - Investment: balance checkedinvestment madereward redeemed (30%)
36
+ * - Support: support contacted → card locked → dispute filed (35%)
37
+ * - Lending: loan applied → loan approved → premium upgraded (25%)
38
+ *
39
+ * USER PROPS: account_tier, Platform, credit_score_range, income_bracket, account_age_months, total_balance, has_direct_deposit, account_segment, employee_count, annual_revenue, industry, age_range, life_stage
40
+ * SUPER PROPS: account_tier, Platform
41
+ * SCD PROPS: account_tier (basic/plus/premium, monthly fuzzy, max 6), risk_category (low/medium/high/critical, household_id-scoped, monthly fixed, max 8)
42
+ * GROUPS: household_id (500 households)
47
43
  */
48
44
 
49
- /**
50
- * ===================================================================
51
- * ANALYTICS HOOKS (10 hooks)
52
- * ===================================================================
53
- *
45
+ // ── HOOK STORIES ──
46
+ /*
54
47
  * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
55
48
  * via behavioral cohorts, raw-prop breakdowns (date, account_tier),
56
49
  * or funnel time-to-convert.
57
50
  *
51
+ * ---------------------------------------------------------------
58
52
  * 1. PERSONAL VS BUSINESS ACCOUNTS (user)
59
53
  *
60
54
  * PATTERN: 20% of accounts are business (employee_count, revenue,
@@ -214,7 +208,7 @@ const chance = u.initChance(SEED);
214
208
  * payment cadence that virtually eliminates missed bills.
215
209
  *
216
210
  * ---------------------------------------------------------------
217
- * 7. PREMIUM TIER VALUE (event)
211
+ * 7. PREMIUM TIER VALUE (everything)
218
212
  *
219
213
  * PATTERN: Premium-tier users get 3x reward values and 2x sell
220
214
  * returns on investments. Plus tier gets 1.5x rewards. No flag —
@@ -266,23 +260,6 @@ const chance = u.initChance(SEED);
266
260
  * REAL-WORLD ANALOGUE: Users obsessively check balances at month
267
261
  * end as bills hit and runway tightens.
268
262
  *
269
- * ===================================================================
270
- * ADVANCED ANALYSIS IDEAS
271
- * ===================================================================
272
- *
273
- * Cross-hook patterns:
274
- * - Budget + Low Balance: Do budget creators avoid low-balance churn?
275
- * - Premium + Auto-Pay: Do premium users adopt auto-pay more?
276
- * - Fraud + Churn: Do fraud victims churn more? Does resolution help?
277
- * - Payday + Month-End: Do payday spenders run out by month-end?
278
- * - Business vs Personal Fraud: Are business accounts more targeted?
279
- *
280
- * Cohort analysis:
281
- * - By account_tier: upgrade paths, value realization
282
- * - By signup_channel: referral retention vs organic
283
- * - By income_bracket: feature adoption by income
284
- * - By credit_score_range: loan approvals, tier adoption
285
- *
286
263
  * ---------------------------------------------------------------
287
264
  * 9. ONBOARDING TIME-TO-CONVERT (everything)
288
265
  *
@@ -345,33 +322,327 @@ const chance = u.initChance(SEED);
345
322
  * Txn-Count Magic Num | over premium upgrades | 1x | 0.8x | -20%
346
323
  */
347
324
 
325
+ // ── SCALE ──
326
+ const SEED = "harness-fintech";
327
+ const NUM_USERS = 10_000;
328
+ const DATASET_START = "2026-01-01T00:00:00Z";
329
+ const DATASET_END = "2026-05-01T23:59:59Z";
330
+ const EVENTS_PER_DAY = 1.2;
331
+ const token = process.env.MP_TOKEN || "your-mixpanel-token";
332
+
333
+ const chance = u.initChance(SEED);
334
+
335
+ // ── KNOBS (tweak these to reshape stories) ──
336
+ // H1: Personal vs Business
337
+ const BUSINESS_LIKELIHOOD = 20;
338
+ const BUSINESS_TXN_MULT = 4;
339
+
340
+ // H2: Payday Patterns
341
+ const PAYDAY_DEPOSIT_MULT = 3;
342
+ const PAYDAY_TRANSFER_MULT = 2.0;
343
+ const PAYDAY_TRANSFER_LIKELIHOOD = 60;
344
+
345
+ // H3: Fraud Detection
346
+ const FRAUD_LIKELIHOOD = 15;
347
+ const FRAUD_BURST_MIN = 3;
348
+ const FRAUD_BURST_MAX = 5;
349
+ const FRAUD_AMOUNT_MIN = 500;
350
+ const FRAUD_AMOUNT_MAX = 3000;
351
+
352
+ // H4: Low Balance Churn
353
+ const LOW_BALANCE_THRESHOLD = 15000;
354
+ const LOW_BALANCE_CHECK_THRESHOLD = 3;
355
+ const LOW_BALANCE_CHURN_CUTOFF_DAYS = 30;
356
+ const LOW_BALANCE_DROP_LIKELIHOOD = 50;
357
+
358
+ // H5: Budget Discipline
359
+ const BUDGET_SAVINGS_MULT = 2;
360
+ const BUDGET_INVESTMENT_MULT = 1.5;
361
+ const BUDGET_CLONE_LIKELIHOOD = 50;
362
+
363
+ // H6: Auto-Pay Loyalty
364
+ const MISSED_BILL_LIKELIHOOD = 30;
365
+
366
+ // H7: Premium Tier Value
367
+ const PREMIUM_REWARD_MULT = 3;
368
+ const PLUS_REWARD_MULT = 1.5;
369
+ const PREMIUM_INVEST_SELL_MULT = 2;
370
+
371
+ // H8: Month-End Anxiety
372
+ const MONTH_END_DAY_THRESHOLD = 28;
373
+ const MONTH_END_SESSION_MULT = 1.4;
374
+ const MONTH_END_BALANCE_MULT = 0.7;
375
+
376
+ // H9: Onboarding TTC
377
+ const TTC_PREMIUM_FACTOR = 0.67;
378
+ const TTC_BASIC_FACTOR = 1.33;
379
+ const TTC_MAX_GAP_MINUTES = 60 * 24 * 30; // 30-day max gap between steps
380
+
381
+ // H10: Transaction-Count Magic Number
382
+ const TXN_SWEET_MIN = 6;
383
+ const TXN_SWEET_MAX = 10;
384
+ const TXN_OVER_THRESHOLD = 11;
385
+ const TXN_INVESTMENT_BOOST = 1.4;
386
+ const TXN_PREMIUM_DROP_LIKELIHOOD = 20;
387
+
388
+ // ── HELPER FUNCTIONS ──
389
+ function handleUserHooks(record) {
390
+ // H1: PERSONAL VS BUSINESS ACCOUNTS — role-based attrs.
391
+ const isBusiness = chance.bool({ likelihood: BUSINESS_LIKELIHOOD });
392
+ if (isBusiness) {
393
+ record.account_segment = "business";
394
+ record.employee_count = chance.integer({ min: 5, max: 500 });
395
+ record.annual_revenue = chance.integer({ min: 100000, max: 10000000 });
396
+ record.industry = chance.pickone(["tech", "retail", "food", "services", "healthcare"]);
397
+ } else {
398
+ record.account_segment = "personal";
399
+ record.age_range = `${chance.pickone([18, 25, 35, 45, 55])}-${chance.pickone([24, 34, 44, 54, 65])}`;
400
+ record.life_stage = chance.pickone(["student", "early_career", "established", "pre_retirement", "retired"]);
401
+ }
402
+ return record;
403
+ }
404
+
405
+ function handleEventHooks(record) {
406
+ // H6: AUTO-PAY LOYALTY — manual bill-paid events have 30% chance of
407
+ // becoming "bill payment missed". Mutates event name.
408
+ if (record.event === "bill paid" && record.auto_pay !== true && chance.bool({ likelihood: MISSED_BILL_LIKELIHOOD })) {
409
+ record.event = "bill payment missed";
410
+ }
411
+ return record;
412
+ }
413
+
414
+ function handleEverythingHooks(record, meta) {
415
+ const datasetStart = dayjs.unix(meta.datasetStart);
416
+ const userEvents = record;
417
+ const profile = meta.profile;
418
+
419
+ userEvents.forEach(e => {
420
+ e.account_tier = profile.account_tier;
421
+ e.Platform = profile.Platform;
422
+ });
423
+
424
+ // H9: ONBOARDING TIME-TO-CONVERT — Premium 1.5x faster (factor 0.67);
425
+ // Basic 1.33x slower (factor 1.33). Finds first onboarding sequence
426
+ // and scales the inter-step gaps.
427
+ {
428
+ const ttcFactor = (
429
+ profile.account_tier === "premium" ? TTC_PREMIUM_FACTOR :
430
+ profile.account_tier === "basic" ? TTC_BASIC_FACTOR :
431
+ 1.0
432
+ );
433
+ if (ttcFactor !== 1.0) {
434
+ const onboardingSeq = findFirstSequence(
435
+ userEvents,
436
+ ["account opened", "app session", "balance checked"],
437
+ TTC_MAX_GAP_MINUTES
438
+ );
439
+ if (onboardingSeq) {
440
+ scaleFunnelTTC(onboardingSeq, ttcFactor);
441
+ }
442
+ }
443
+ }
444
+
445
+ // H1B: PERSONAL VS BUSINESS — business segment txns 4x larger
446
+ // (per Report 2 in JSDoc: business ~ $200, personal ~ $50).
447
+ if (profile.account_segment === "business") {
448
+ userEvents.forEach(e => {
449
+ if (e.event === "transaction completed" && typeof e.amount === "number") {
450
+ e.amount = Math.floor(e.amount * BUSINESS_TXN_MULT);
451
+ }
452
+ });
453
+ }
454
+
455
+ // H2: PAYDAY PATTERNS — 1st & 15th: direct_deposit amount 3x.
456
+ // Days 1-3 and 15-17: 60% of transfers get amount 2x. No flag.
457
+ for (const e of userEvents) {
458
+ const dayOfMonth = new Date(e.time).getUTCDate();
459
+ if (e.event === "transaction completed" && e.transaction_type === "direct_deposit") {
460
+ if (dayOfMonth === 1 || dayOfMonth === 15) {
461
+ e.amount = Math.floor((e.amount || 50) * PAYDAY_DEPOSIT_MULT);
462
+ }
463
+ }
464
+ if (e.event === "transfer sent") {
465
+ const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
466
+ if (isPaydayWindow && chance.bool({ likelihood: PAYDAY_TRANSFER_LIKELIHOOD })) {
467
+ e.amount = Math.floor((e.amount || 200) * PAYDAY_TRANSFER_MULT);
468
+ }
469
+ }
470
+ }
471
+
472
+ // H8: MONTH-END ANXIETY — days >= 28: app_session duration 1.4x;
473
+ // balance_checked account_balance 0.7x. Mutates raw props.
474
+ for (const e of userEvents) {
475
+ const dayOfMonth = new Date(e.time).getUTCDate();
476
+ if (dayOfMonth >= MONTH_END_DAY_THRESHOLD) {
477
+ if (e.event === "app session") {
478
+ e.session_duration_sec = Math.floor((e.session_duration_sec || 60) * MONTH_END_SESSION_MULT);
479
+ }
480
+ if (e.event === "balance checked") {
481
+ e.account_balance = Math.floor((e.account_balance || 2500) * MONTH_END_BALANCE_MULT);
482
+ }
483
+ }
484
+ }
485
+
486
+ // H7: PREMIUM TIER VALUE — Premium 3x reward value + 2x investment-sell
487
+ // amount; Plus 1.5x reward value. Reads tier from profile. No flag.
488
+ const tier = profile.account_tier;
489
+ userEvents.forEach(e => {
490
+ if (e.event === "reward redeemed") {
491
+ if (tier === "premium") e.value = Math.floor((e.value || 10) * PREMIUM_REWARD_MULT);
492
+ else if (tier === "plus") e.value = Math.floor((e.value || 10) * PLUS_REWARD_MULT);
493
+ }
494
+ if (e.event === "investment made" && e.action === "sell" && tier === "premium") {
495
+ e.amount = Math.floor((e.amount || 250) * PREMIUM_INVEST_SELL_MULT);
496
+ }
497
+ });
498
+
499
+ // H3: FRAUD DETECTION — 15% of users get fraud burst (3-5 rapid
500
+ // high-value transactions + card locked + dispute + support contacted)
501
+ // at timeline midpoint. No flag — discover via cohort builder on users
502
+ // with card-locked + dispute-filed.
503
+ if (chance.bool({ likelihood: FRAUD_LIKELIHOOD }) && userEvents.length >= 2) {
504
+ const midIdx = Math.floor(userEvents.length / 2);
505
+ const midEvent = userEvents[midIdx];
506
+ const midTime = dayjs(midEvent.time);
507
+ const distinctId = midEvent.user_id;
508
+ const burstCount = chance.integer({ min: FRAUD_BURST_MIN, max: FRAUD_BURST_MAX });
509
+ const fraudEvents = [];
510
+ const txnTemplate = userEvents.find(e => e.event === "transaction completed");
511
+ const cardTemplate = userEvents.find(e => e.event === "card locked");
512
+ const disputeTemplate = userEvents.find(e => e.event === "dispute filed");
513
+ const supportTemplate = userEvents.find(e => e.event === "support contacted");
514
+
515
+ for (let i = 0; i < burstCount; i++) {
516
+ if (txnTemplate) {
517
+ fraudEvents.push({
518
+ ...txnTemplate,
519
+ time: midTime.add(i * 10, "minutes").toISOString(),
520
+ user_id: distinctId,
521
+ transaction_type: "purchase",
522
+ amount: chance.integer({ min: FRAUD_AMOUNT_MIN, max: FRAUD_AMOUNT_MAX }),
523
+ merchant_category: chance.pickone(["online", "retail"]),
524
+ payment_method: "credit",
525
+ });
526
+ }
527
+ }
528
+ if (cardTemplate) fraudEvents.push({
529
+ ...cardTemplate,
530
+ time: midTime.add(burstCount * 10 + 5, "minutes").toISOString(),
531
+ user_id: distinctId,
532
+ reason: "suspicious_activity",
533
+ });
534
+ if (disputeTemplate) fraudEvents.push({
535
+ ...disputeTemplate,
536
+ time: midTime.add(burstCount * 10 + 30, "minutes").toISOString(),
537
+ user_id: distinctId,
538
+ dispute_amount: chance.integer({ min: FRAUD_AMOUNT_MIN, max: FRAUD_AMOUNT_MAX }),
539
+ reason: "unauthorized",
540
+ });
541
+ if (supportTemplate) fraudEvents.push({
542
+ ...supportTemplate,
543
+ time: midTime.add(burstCount * 10 + 45, "minutes").toISOString(),
544
+ user_id: distinctId,
545
+ channel: "phone",
546
+ issue_type: "card",
547
+ resolved: true,
548
+ });
549
+ userEvents.splice(midIdx + 1, 0, ...fraudEvents);
550
+ }
551
+
552
+ // H4: LOW BALANCE CHURN — users with 3+ balance checks under $15K lose
553
+ // 50% of post-day-30 events. No flag.
554
+ const lowBalanceChecks = userEvents.filter(e =>
555
+ e.event === "balance checked" && (e.account_balance || 0) < LOW_BALANCE_THRESHOLD
556
+ ).length;
557
+ if (lowBalanceChecks >= LOW_BALANCE_CHECK_THRESHOLD) {
558
+ const dayCutoff = datasetStart.add(LOW_BALANCE_CHURN_CUTOFF_DAYS, "days");
559
+ for (let i = userEvents.length - 1; i >= 0; i--) {
560
+ if (dayjs(userEvents[i].time).isAfter(dayCutoff) && chance.bool({ likelihood: LOW_BALANCE_DROP_LIKELIHOOD })) {
561
+ userEvents.splice(i, 1);
562
+ }
563
+ }
564
+ }
565
+
566
+ // H5: BUDGET DISCIPLINE — users with any budget-created event get
567
+ // savings 2x, investment amounts 1.5x, and extra cloned savings-goal
568
+ // events. No flag.
569
+ const hasBudget = userEvents.some(e => e.event === "budget created");
570
+ if (hasBudget) {
571
+ userEvents.forEach((event, idx) => {
572
+ const eventTime = dayjs(event.time);
573
+ if (event.event === "savings goal set") {
574
+ event.monthly_contribution = Math.floor((event.monthly_contribution || 200) * BUDGET_SAVINGS_MULT);
575
+ }
576
+ if (event.event === "investment made") {
577
+ event.amount = Math.floor((event.amount || 250) * BUDGET_INVESTMENT_MULT);
578
+ }
579
+ if (event.event === "budget created" && chance.bool({ likelihood: BUDGET_CLONE_LIKELIHOOD })) {
580
+ const savingsTemplate = userEvents.find(e => e.event === "savings goal set");
581
+ if (savingsTemplate) {
582
+ userEvents.splice(idx + 1, 0, {
583
+ ...savingsTemplate,
584
+ time: eventTime.add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
585
+ user_id: event.user_id,
586
+ goal_type: chance.pickone(["emergency", "vacation", "car", "home"]),
587
+ target_amount: chance.integer({ min: 1000, max: 20000 }),
588
+ monthly_contribution: chance.integer({ min: 100, max: 800 }),
589
+ });
590
+ }
591
+ }
592
+ });
593
+ }
594
+
595
+ // H10: TRANSACTION-COUNT MAGIC NUMBER (no flags)
596
+ // Sweet 6-10 transactions/user → +40% on investment_made amount.
597
+ // Over 11+ → drop 20% of premium-upgraded events.
598
+ const txnCount = userEvents.filter(e => e.event === "transaction completed").length;
599
+ if (txnCount >= TXN_SWEET_MIN && txnCount <= TXN_SWEET_MAX) {
600
+ userEvents.forEach(e => {
601
+ if (e.event === "investment made" && typeof e.amount === "number") {
602
+ e.amount = Math.round(e.amount * TXN_INVESTMENT_BOOST);
603
+ }
604
+ });
605
+ } else if (txnCount >= TXN_OVER_THRESHOLD) {
606
+ for (let i = userEvents.length - 1; i >= 0; i--) {
607
+ if (userEvents[i].event === "premium upgraded" && chance.bool({ likelihood: TXN_PREMIUM_DROP_LIKELIHOOD })) {
608
+ userEvents.splice(i, 1);
609
+ }
610
+ }
611
+ }
612
+
613
+ return record;
614
+ }
615
+
616
+ // ── CONFIG ──
348
617
  /** @type {Config} */
349
618
  const config = {
350
619
  version: 2,
351
- token,
352
620
  seed: SEED,
353
- datasetStart: "2026-01-01T00:00:00Z",
354
- datasetEnd: "2026-05-01T23:59:59Z",
355
- // numDays: num_days,
356
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
357
- numUsers: num_users,
358
- hasAnonIds: true,
359
- avgDevicePerUser: 2,
360
- hasSessionIds: true,
621
+ datasetStart: DATASET_START,
622
+ datasetEnd: DATASET_END,
623
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
624
+ numUsers: NUM_USERS,
361
625
  format: "json",
362
626
  gzip: true,
363
- alsoInferFunnels: false,
364
- hasLocation: true,
365
- hasAndroidDevices: true,
366
- hasIOSDevices: true,
367
- hasDesktopDevices: true,
368
- hasBrowser: false,
369
- hasCampaigns: false,
370
- isAnonymous: false,
371
- hasAdSpend: false,
372
-
373
- hasAvatar: true,
374
-
627
+ credentials: {
628
+ token,
629
+ },
630
+ switches: {
631
+ hasSessionIds: true,
632
+ alsoInferFunnels: false,
633
+ hasLocation: true,
634
+ hasAndroidDevices: true,
635
+ hasIOSDevices: true,
636
+ hasDesktopDevices: true,
637
+ hasBrowser: false,
638
+ hasCampaigns: false,
639
+ isAnonymous: false,
640
+ hasAdSpend: false,
641
+ hasAvatar: true,
642
+ },
643
+ identity: {
644
+ avgDevicePerUser: 2,
645
+ },
375
646
  concurrency: 1,
376
647
  writeToDisk: false,
377
648
 
@@ -664,248 +935,10 @@ const config = {
664
935
 
665
936
  lookupTables: [],
666
937
 
667
- /**
668
- * ARCHITECTED ANALYTICS HOOKS
669
- *
670
- * This hook function creates 8 deliberate patterns in the data:
671
- *
672
- * 1. PERSONAL VS BUSINESS: Business accounts get employee_count, revenue; personal get age_range, life_stage
673
- * 2. PAYDAY PATTERNS: Transactions spike on 1st/15th with bigger deposits and post-payday spending (everything hook — runs after sessionization)
674
- * 3. FRAUD DETECTION: 3% of users experience a fraud burst (rapid high-value txns -> card lock -> dispute -> support)
675
- * 4. LOW BALANCE CHURN: Users with chronic low balances (<$15K) lose 50% of activity after day 30
676
- * 5. BUDGET DISCIPLINE: Budget creators save 2x more and invest 1.5x more
677
- * 6. AUTO-PAY LOYALTY: Auto-pay users never miss bills; manual payers miss 30%
678
- * 7. PREMIUM TIER VALUE: Premium users get 3x rewards; Plus users get 1.5x; Premium investors get 2x returns
679
- * 8. MONTH-END ANXIETY: Last 3 days of month see 40% longer sessions and 30% lower balances (everything hook — runs after sessionization)
680
- */
681
- hook: function (record, type, meta) {
682
- // HOOK 1: PERSONAL VS BUSINESS ACCOUNTS (user) — role-based attrs.
683
- if (type === "user") {
684
- const isBusiness = chance.bool({ likelihood: 20 });
685
- if (isBusiness) {
686
- record.account_segment = "business";
687
- record.employee_count = chance.integer({ min: 5, max: 500 });
688
- record.annual_revenue = chance.integer({ min: 100000, max: 10000000 });
689
- record.industry = chance.pickone(["tech", "retail", "food", "services", "healthcare"]);
690
- } else {
691
- record.account_segment = "personal";
692
- record.age_range = `${chance.pickone([18, 25, 35, 45, 55])}-${chance.pickone([24, 34, 44, 54, 65])}`;
693
- record.life_stage = chance.pickone(["student", "early_career", "established", "pre_retirement", "retired"]);
694
- }
695
- }
696
-
697
- // HOOK 6: AUTO-PAY LOYALTY (event) — manual bill-paid events have
698
- // 30% chance of becoming "bill payment missed". Mutates event name.
699
- if (type === "event") {
700
- if (record.event === "bill paid" && record.auto_pay !== true && chance.bool({ likelihood: 30 })) {
701
- record.event = "bill payment missed";
702
- }
703
- }
704
-
705
- if (type === "everything") {
706
- const datasetStart = dayjs.unix(meta.datasetStart);
707
- const userEvents = record;
708
- const profile = meta.profile;
709
-
710
- userEvents.forEach(e => {
711
- e.account_tier = profile.account_tier;
712
- e.Platform = profile.Platform;
713
- });
714
-
715
- // HOOK 9 (T2C): ONBOARDING TIME-TO-CONVERT (everything)
716
- // Premium tier completes Onboarding funnel 1.5x faster (factor 0.67);
717
- // Basic users 1.33x slower (factor 1.33). Finds the first occurrence
718
- // of the onboarding sequence in the user's events and scales the gaps.
719
- {
720
- const ttcFactor = (
721
- profile.account_tier === "premium" ? 0.67 :
722
- profile.account_tier === "basic" ? 1.33 :
723
- 1.0
724
- );
725
- if (ttcFactor !== 1.0) {
726
- const onboardingSeq = findFirstSequence(
727
- userEvents,
728
- ["account opened", "app session", "balance checked"],
729
- 60 * 24 * 30 // 30-day max gap between steps
730
- );
731
- if (onboardingSeq) {
732
- scaleFunnelTTC(onboardingSeq, ttcFactor);
733
- }
734
- }
735
- }
736
-
737
- // HOOK 1B: PERSONAL VS BUSINESS — business segment txns 4x larger
738
- // (per Report 2 in JSDoc: business ~ $200, personal ~ $50).
739
- if (profile.account_segment === "business") {
740
- userEvents.forEach(e => {
741
- if (e.event === "transaction completed" && typeof e.amount === "number") {
742
- e.amount = Math.floor(e.amount * 4);
743
- }
744
- });
745
- }
746
-
747
- // HOOK 2: PAYDAY PATTERNS — 1st & 15th: direct_deposit amount 3x.
748
- // Days 1-3 and 15-17: 60% of transfers get amount 2x. No flag.
749
- for (const e of userEvents) {
750
- const dayOfMonth = new Date(e.time).getUTCDate();
751
- if (e.event === "transaction completed" && e.transaction_type === "direct_deposit") {
752
- if (dayOfMonth === 1 || dayOfMonth === 15) {
753
- e.amount = Math.floor((e.amount || 50) * 3);
754
- }
755
- }
756
- if (e.event === "transfer sent") {
757
- const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
758
- if (isPaydayWindow && chance.bool({ likelihood: 60 })) {
759
- e.amount = Math.floor((e.amount || 200) * 2.0);
760
- }
761
- }
762
- }
763
-
764
- // HOOK 8: MONTH-END ANXIETY — days >= 28: app_session duration
765
- // 1.4x; balance_checked account_balance 0.7x. Mutates raw props.
766
- for (const e of userEvents) {
767
- const dayOfMonth = new Date(e.time).getUTCDate();
768
- if (dayOfMonth >= 28) {
769
- if (e.event === "app session") {
770
- e.session_duration_sec = Math.floor((e.session_duration_sec || 60) * 1.4);
771
- }
772
- if (e.event === "balance checked") {
773
- e.account_balance = Math.floor((e.account_balance || 2500) * 0.7);
774
- }
775
- }
776
- }
777
-
778
- // HOOK 7: PREMIUM TIER VALUE — Premium 3x reward value + 2x
779
- // investment-sell amount; Plus 1.5x reward value. Reads tier
780
- // from profile. No flag.
781
- const tier = profile.account_tier;
782
- userEvents.forEach(e => {
783
- if (e.event === "reward redeemed") {
784
- if (tier === "premium") e.value = Math.floor((e.value || 10) * 3);
785
- else if (tier === "plus") e.value = Math.floor((e.value || 10) * 1.5);
786
- }
787
- if (e.event === "investment made" && e.action === "sell" && tier === "premium") {
788
- e.amount = Math.floor((e.amount || 250) * 2);
789
- }
790
- });
791
-
792
- // HOOK 3: FRAUD DETECTION — 3% of users get fraud burst
793
- // (3-5 rapid high-value transactions + card locked + dispute +
794
- // support contacted) at timeline midpoint. No flag — discover
795
- // via cohort builder on users with card-locked + dispute-filed.
796
- if (chance.bool({ likelihood: 15 }) && userEvents.length >= 2) {
797
- const midIdx = Math.floor(userEvents.length / 2);
798
- const midEvent = userEvents[midIdx];
799
- const midTime = dayjs(midEvent.time);
800
- const distinctId = midEvent.user_id;
801
- const burstCount = chance.integer({ min: 3, max: 5 });
802
- const fraudEvents = [];
803
- const txnTemplate = userEvents.find(e => e.event === "transaction completed");
804
- const cardTemplate = userEvents.find(e => e.event === "card locked");
805
- const disputeTemplate = userEvents.find(e => e.event === "dispute filed");
806
- const supportTemplate = userEvents.find(e => e.event === "support contacted");
807
-
808
- for (let i = 0; i < burstCount; i++) {
809
- if (txnTemplate) {
810
- fraudEvents.push({
811
- ...txnTemplate,
812
- time: midTime.add(i * 10, "minutes").toISOString(),
813
- user_id: distinctId,
814
- transaction_type: "purchase",
815
- amount: chance.integer({ min: 500, max: 3000 }),
816
- merchant_category: chance.pickone(["online", "retail"]),
817
- payment_method: "credit",
818
- });
819
- }
820
- }
821
- if (cardTemplate) fraudEvents.push({
822
- ...cardTemplate,
823
- time: midTime.add(burstCount * 10 + 5, "minutes").toISOString(),
824
- user_id: distinctId,
825
- reason: "suspicious_activity",
826
- });
827
- if (disputeTemplate) fraudEvents.push({
828
- ...disputeTemplate,
829
- time: midTime.add(burstCount * 10 + 30, "minutes").toISOString(),
830
- user_id: distinctId,
831
- dispute_amount: chance.integer({ min: 500, max: 3000 }),
832
- reason: "unauthorized",
833
- });
834
- if (supportTemplate) fraudEvents.push({
835
- ...supportTemplate,
836
- time: midTime.add(burstCount * 10 + 45, "minutes").toISOString(),
837
- user_id: distinctId,
838
- channel: "phone",
839
- issue_type: "card",
840
- resolved: true,
841
- });
842
- userEvents.splice(midIdx + 1, 0, ...fraudEvents);
843
- }
844
-
845
- // HOOK 4: LOW BALANCE CHURN — users with 3+ balance checks
846
- // under $15K lose 50% of post-day-30 events. No flag.
847
- const lowBalanceChecks = userEvents.filter(e =>
848
- e.event === "balance checked" && (e.account_balance || 0) < 15000
849
- ).length;
850
- if (lowBalanceChecks >= 3) {
851
- const day30 = datasetStart.add(30, "days");
852
- for (let i = userEvents.length - 1; i >= 0; i--) {
853
- if (dayjs(userEvents[i].time).isAfter(day30) && chance.bool({ likelihood: 50 })) {
854
- userEvents.splice(i, 1);
855
- }
856
- }
857
- }
858
-
859
- // HOOK 5: BUDGET DISCIPLINE — users with any budget-created event
860
- // get savings 2x, investment amounts 1.5x, and extra cloned
861
- // savings-goal events. No flag.
862
- const hasBudget = userEvents.some(e => e.event === "budget created");
863
- if (hasBudget) {
864
- userEvents.forEach((event, idx) => {
865
- const eventTime = dayjs(event.time);
866
- if (event.event === "savings goal set") {
867
- event.monthly_contribution = Math.floor((event.monthly_contribution || 200) * 2);
868
- }
869
- if (event.event === "investment made") {
870
- event.amount = Math.floor((event.amount || 250) * 1.5);
871
- }
872
- if (event.event === "budget created" && chance.bool({ likelihood: 50 })) {
873
- const savingsTemplate = userEvents.find(e => e.event === "savings goal set");
874
- if (savingsTemplate) {
875
- userEvents.splice(idx + 1, 0, {
876
- ...savingsTemplate,
877
- time: eventTime.add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
878
- user_id: event.user_id,
879
- goal_type: chance.pickone(["emergency", "vacation", "car", "home"]),
880
- target_amount: chance.integer({ min: 1000, max: 20000 }),
881
- monthly_contribution: chance.integer({ min: 100, max: 800 }),
882
- });
883
- }
884
- }
885
- });
886
- }
887
-
888
- // HOOK 10: TRANSACTION-COUNT MAGIC NUMBER (no flags)
889
- // Sweet 6-10 transactions/user → +40% on investment_made amount
890
- // (engaged transactor compounds wealth). Over 11+ → drop 20% of
891
- // premium-upgraded events (already engaged; less upgrade pressure).
892
- const txnCount = userEvents.filter(e => e.event === "transaction completed").length;
893
- if (txnCount >= 6 && txnCount <= 10) {
894
- userEvents.forEach(e => {
895
- if (e.event === "investment made" && typeof e.amount === "number") {
896
- e.amount = Math.round(e.amount * 1.4);
897
- }
898
- });
899
- } else if (txnCount >= 11) {
900
- // Drop 20% of premium-upgraded events for heavy transactors
901
- for (let i = userEvents.length - 1; i >= 0; i--) {
902
- if (userEvents[i].event === "premium upgraded" && chance.bool({ likelihood: 20 })) {
903
- userEvents.splice(i, 1);
904
- }
905
- }
906
- }
907
- }
908
-
938
+ hook(record, type, meta) {
939
+ if (type === "user") return handleUserHooks(record);
940
+ if (type === "event") return handleEventHooks(record);
941
+ if (type === "everything") return handleEverythingHooks(record, meta);
909
942
  return record;
910
943
  }
911
944
  };