@ak--47/dungeon-master 1.4.5 → 1.5.1

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 (78) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +182 -0
  9. package/HOOKS.md +1256 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +41 -49
  12. package/dungeons/technical/anonymous-users.js +38 -36
  13. package/dungeons/technical/array-of-object-lookup.js +136 -153
  14. package/dungeons/technical/datagen-v15-verify.js +87 -0
  15. package/dungeons/technical/experiments.js +42 -40
  16. package/dungeons/technical/foobar.js +114 -118
  17. package/dungeons/technical/group-analytics.js +42 -40
  18. package/dungeons/technical/hook-helpers-verify.js +69 -50
  19. package/dungeons/technical/identity-model-verify.js +22 -12
  20. package/dungeons/technical/mirror-strategies.js +37 -39
  21. package/dungeons/technical/nested-objects.js +119 -118
  22. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  23. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  24. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  25. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  26. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  27. package/dungeons/technical/retention-cadence.js +115 -112
  28. package/dungeons/technical/sanity.js +86 -80
  29. package/dungeons/technical/scale-test.js +34 -38
  30. package/dungeons/technical/scd.js +111 -128
  31. package/dungeons/technical/simple.js +134 -141
  32. package/dungeons/technical/simplest.js +111 -65
  33. package/dungeons/technical/text-generation.js +110 -146
  34. package/dungeons/vertical/ai-platform.js +300 -333
  35. package/dungeons/vertical/community.js +290 -255
  36. package/dungeons/vertical/crypto.js +400 -391
  37. package/dungeons/vertical/dating.js +421 -375
  38. package/dungeons/vertical/devtools.js +346 -298
  39. package/dungeons/vertical/ecommerce.js +322 -394
  40. package/dungeons/vertical/education.js +380 -325
  41. package/dungeons/vertical/fintech.js +371 -325
  42. package/dungeons/vertical/fitness.js +345 -291
  43. package/dungeons/vertical/food-delivery.js +352 -307
  44. package/dungeons/vertical/gaming.js +490 -444
  45. package/dungeons/vertical/healthcare.js +311 -262
  46. package/dungeons/vertical/insurance-application.js +437 -409
  47. package/dungeons/vertical/logistics.js +278 -252
  48. package/dungeons/vertical/marketplace.js +340 -323
  49. package/dungeons/vertical/media.js +390 -335
  50. package/dungeons/vertical/real-estate.js +402 -347
  51. package/dungeons/vertical/sass.js +331 -333
  52. package/dungeons/vertical/social.js +377 -316
  53. package/dungeons/vertical/travel.js +302 -295
  54. package/index.js +64 -7
  55. package/lib/core/config-validator.js +378 -17
  56. package/lib/core/dungeon-loader.js +2 -5
  57. package/lib/generators/events.js +12 -13
  58. package/lib/generators/funnels.js +76 -2
  59. package/lib/hook-helpers/index.js +1 -0
  60. package/lib/hook-helpers/inject.js +95 -0
  61. package/lib/orchestrators/mixpanel-sender.js +7 -0
  62. package/lib/orchestrators/user-loop.js +598 -48
  63. package/lib/templates/defaults.js +59 -59
  64. package/lib/templates/macro-presets.js +53 -11
  65. package/lib/utils/dataset-context.js +103 -0
  66. package/lib/utils/retention-curve.js +140 -0
  67. package/lib/utils/utils.js +157 -109
  68. package/lib/verify/counting.js +360 -0
  69. package/lib/verify/emulate-breakdown.js +531 -108
  70. package/lib/verify/funnel-engine.js +539 -0
  71. package/lib/verify/identity.js +78 -0
  72. package/lib/verify/index.js +20 -0
  73. package/lib/verify/schema-validator.js +3 -1
  74. package/lib/verify/verify-dungeon.js +58 -0
  75. package/package.json +14 -3
  76. package/scripts/run-dungeon.mjs +12 -1
  77. package/types.d.ts +353 -4
  78. package/scripts/smoke-test-all.mjs +0 -162
@@ -1,66 +1,44 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "dm4-logistics";
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 * as v from "ak-tools";
16
-
17
- dayjs.extend(utc);
18
- const chance = u.initChance(SEED);
19
8
  /** @typedef {import("../../types").Dungeon} Config */
20
9
 
21
- // Generate consistent warehouse/supplier IDs at module level
22
- const warehouseIds = v.range(1, 80).map(() => `WH_${v.uid(6)}`);
23
- const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
24
-
25
- /**
26
- * ===================================================================
27
- * DATASET OVERVIEW
28
- * ===================================================================
29
- *
30
- * SupplyStack -- a B2B warehouse/inventory management SaaS for
31
- * businesses to track stock levels, purchase orders, suppliers,
32
- * shipments, and quality inspections across multiple warehouses.
33
- *
34
- * - 10,000 users over 120 days
35
- * - Multi-tier system: enterprise (15%), mid-market (35%), small business (40%), trial (10%)
36
- * - Core loop: sign up -> check inventory -> create PO -> receive order -> track shipment
37
- * - Revenue: free_trial / starter ($49) / professional ($199) / enterprise ($599)
38
- *
39
- * Advanced Features:
40
- * - Personas: 4 archetypes (enterprise_ops, mid_market, small_business, trial_explorer)
41
- * - World Events: supply_chain_disruption (day 35), holiday_surge (day 70)
42
- * - Subscription: 4-tier revenue lifecycle with 30-day trial
43
- * - Geo: US/EU/APAC with currency and regional properties
44
- * - Features: predictive_reorder (day 40) and supplier_portal (day 65)
45
- * - Anomalies: extreme stockout spikes, coordinated holiday prep burst
46
- *
47
- * Key entities:
48
- * - warehouse_id: specific warehouse location
49
- * - supplier_id: vendor supplying goods
50
- * - sku_category: product category being managed
51
- * - priority: urgency level for orders and alerts
10
+ // ── OVERVIEW ──
11
+ /*
12
+ * NAME: SupplyStack
13
+ * APP: B2B warehouse/inventory management SaaS for businesses to track
14
+ * stock levels, purchase orders, suppliers, shipments, and quality
15
+ * inspections across multiple warehouses. Multi-tier system with
16
+ * enterprise, mid-market, small business, and trial customers.
17
+ * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
18
+ * CORE LOOP: account created → inventory checked → purchase order → order received → shipment tracked
19
+ *
20
+ * EVENTS (17):
21
+ * inventory checked (8) > app session (7) > shipment tracked (6) > notification received (6)
22
+ * > stockout alert (5) > purchase order created (5) > order received (4) > report generated (4)
23
+ * > supplier contacted (3) > warehouse transfer (3) > invoice processed (3)
24
+ * > integration connected (2) > alert configured (2) > quality inspection (2)
25
+ * > account created (1) > support ticket (1) > account deactivated (1)
26
+ *
27
+ * FUNNELS (5):
28
+ * - Onboarding: account created → inventory checked → integration connected → report generated (40%)
29
+ * - Order Fulfillment: inventory checked purchase order created → order received → shipment tracked (35%, reentry)
30
+ * - Supplier Management: supplier contacted purchase order created → invoice processed (45%)
31
+ * - Integration Setup: integration connected report generated → alert configured (30%)
32
+ * - Alert Response: stockout alert purchase order created → order received (50%)
33
+ *
34
+ * USER PROPS: company_tier, warehouse_count, employee_count, industry, integration_count, Platform, subscription_plan
35
+ * SUPER PROPS: Platform, subscription_plan
36
+ * SCD PROPS: company_tier (trial/starter/professional/enterprise, monthly fuzzy, max 6)
37
+ * GROUPS: none
52
38
  */
53
39
 
54
- /**
55
- * ===================================================================
56
- * ANALYTICS HOOKS (10 hooks)
57
- *
58
- * Adds 10. ONBOARDING TIME-TO-CONVERT: enterprise 0.71x faster, small_business
59
- * 1.3x slower (funnel-post). Discover via Onboarding funnel median TTC by company_tier.
60
- * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
61
- * Cross-event MIN→MIN SQL queries on raw events do NOT show this.
62
- * ===================================================================
63
- *
40
+ // ── HOOK STORIES ──
41
+ /*
64
42
  * -------------------------------------------------------------------
65
43
  * 1. MONTH-END REPORTING SURGE (event hook)
66
44
  * -------------------------------------------------------------------
@@ -339,31 +317,246 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
339
317
  * Onboarding TTC | funnel median TTC | 1x | 0.71x | 1.4x faster (enterprise)
340
318
  */
341
319
 
320
+ // ── SCALE ──
321
+ const SEED = "dm4-logistics";
322
+ const NUM_USERS = 10_000;
323
+ const DATASET_START = "2026-01-01T00:00:00Z";
324
+ const DATASET_END = "2026-05-01T23:59:59Z";
325
+ const EVENTS_PER_DAY = 1.2;
326
+ const token = process.env.MP_TOKEN || "your-mixpanel-token";
327
+
328
+ const chance = u.initChance(SEED);
329
+
330
+ // ── KNOBS (tweak these to reshape stories) ──
331
+ const MONTH_END_DAY_THRESHOLD = 28;
332
+ const MONTH_END_PAGES_MULT = 2.5;
333
+
334
+ const RUSH_ORDER_COST_MULT = 1.5;
335
+
336
+ const ENTERPRISE_STOCKOUT_DROP_LIKELIHOOD = 10;
337
+
338
+ const INTEGRATION_THRESHOLD = 3;
339
+ const INTEGRATION_REPORT_CLONE_LIKELIHOOD = 65;
340
+
341
+ const ALERT_FATIGUE_THRESHOLD = 30;
342
+ const ALERT_FATIGUE_START_IDX = 20;
343
+ const ALERT_FATIGUE_BASE_MULT = 1.5;
344
+ const ALERT_FATIGUE_RAMP_MULT = 1.5;
345
+
346
+ const TRIAL_CHURN_EVENT_THRESHOLD = 10;
347
+ const TRIAL_CHURN_CUTOFF_DAYS = 7;
348
+ const TRIAL_CHURN_DROP_LIKELIHOOD = 50;
349
+
350
+ const SMB_ALERT_DROP_LIKELIHOOD = 35;
351
+
352
+ const INVENTORY_SWEET_MIN = 5;
353
+ const INVENTORY_SWEET_MAX = 15;
354
+ const INVENTORY_OVER_THRESHOLD = 16;
355
+ const INVENTORY_PO_QUANTITY_BOOST = 1.4;
356
+ const INVENTORY_OVER_PO_DROP_LIKELIHOOD = 60;
357
+
358
+ const TTC_ENTERPRISE_FACTOR = 0.71;
359
+ const TTC_SMB_FACTOR = 1.3;
360
+
361
+ // ── DATA ARRAYS ──
362
+ const warehouseIds = v.range(1, 80).map(() => `WH_${v.uid(6)}`);
363
+ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
364
+
365
+ // ── HELPER FUNCTIONS ──
366
+ function handleUserHooks(record) {
367
+ // H7: ENTERPRISE PROFILES — large warehouse_count + employee_count by tier.
368
+ if (record.company_tier === "enterprise") {
369
+ record.warehouse_count = chance.integer({ min: 5, max: 15 });
370
+ record.employee_count = chance.integer({ min: 200, max: 2000 });
371
+ } else if (record.company_tier === "mid_market") {
372
+ record.warehouse_count = chance.integer({ min: 2, max: 6 });
373
+ record.employee_count = chance.integer({ min: 20, max: 200 });
374
+ } else if (record.company_tier === "small_business") {
375
+ record.warehouse_count = chance.integer({ min: 1, max: 3 });
376
+ record.employee_count = chance.integer({ min: 5, max: 80 });
377
+ } else if (record.company_tier === "trial") {
378
+ record.warehouse_count = 1;
379
+ record.employee_count = chance.integer({ min: 1, max: 10 });
380
+ }
381
+ return record;
382
+ }
383
+
384
+ function handleEventHooks(record) {
385
+ // H2: RUSH ORDER PREMIUM — urgent purchase orders get 1.5x unit_cost.
386
+ if (record.event === "purchase order created" && record.priority === "urgent") {
387
+ record.unit_cost = Math.floor((record.unit_cost || 50) * RUSH_ORDER_COST_MULT);
388
+ }
389
+ return record;
390
+ }
391
+
392
+ function handleFunnelPostHooks(record, meta) {
393
+ // H10: ONBOARDING TIME-TO-CONVERT — enterprise 1.4x faster (0.71);
394
+ // small_business + trial 1.3x slower (1.3).
395
+ const segment = meta?.profile?.company_tier;
396
+ if (Array.isArray(record) && record.length > 1) {
397
+ const factor = (
398
+ segment === "enterprise" ? TTC_ENTERPRISE_FACTOR :
399
+ segment === "small_business" || segment === "trial" ? TTC_SMB_FACTOR :
400
+ 1.0
401
+ );
402
+ if (factor !== 1.0) {
403
+ for (let i = 1; i < record.length; i++) {
404
+ const prev = dayjs(record[i - 1].time);
405
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
406
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
407
+ }
408
+ }
409
+ }
410
+ return record;
411
+ }
412
+
413
+ function handleEverythingHooks(record, meta) {
414
+ if (!record.length) return record;
415
+ const profile = meta.profile;
416
+
417
+ // ── SUPER-PROP STAMPING ──
418
+ // Stamp superProps from profile so they are consistent per-user.
419
+ if (profile) {
420
+ const plat = profile.Platform;
421
+ const plan = profile.subscription_plan;
422
+ record.forEach(e => {
423
+ if (plat) e.Platform = plat;
424
+ if (plan) e.subscription_plan = plan;
425
+ });
426
+ }
427
+
428
+ // H1: MONTH-END REPORTING SURGE — reports on calendar days 28-31 get
429
+ // 2.5x report_pages. Runs in everything (post-sessionization) so the
430
+ // day-of-month tag matches the final timestamp.
431
+ for (const e of record) {
432
+ if (e.event === "report generated") {
433
+ const dayOfMonth = new Date(e.time).getUTCDate();
434
+ if (dayOfMonth >= MONTH_END_DAY_THRESHOLD) {
435
+ e.report_pages = Math.floor((e.report_pages || 20) * MONTH_END_PAGES_MULT);
436
+ }
437
+ }
438
+ }
439
+
440
+ // H8: SMALL-BUSINESS CONVERSION DROP — small_business users lose
441
+ // ~35% of "alert configured" events (last step of Integration Setup
442
+ // funnel), simulating lower conversion without dedicated IT.
443
+ if (profile && profile.company_tier === "small_business") {
444
+ record = record.filter(e => {
445
+ if (e.event === "alert configured" && chance.bool({ likelihood: SMB_ALERT_DROP_LIKELIHOOD })) {
446
+ return false;
447
+ }
448
+ return true;
449
+ });
450
+ }
451
+
452
+ // H3: REORDER ACCURACY BY TIER — enterprise users get 10% of
453
+ // stockout alerts removed (better forecasting + safety stock).
454
+ if (profile && profile.company_tier === "enterprise") {
455
+ for (let i = record.length - 1; i >= 0; i--) {
456
+ if (record[i].event === "stockout alert" && chance.bool({ likelihood: ENTERPRISE_STOCKOUT_DROP_LIKELIHOOD })) {
457
+ record.splice(i, 1);
458
+ }
459
+ }
460
+ }
461
+
462
+ // H4: INTEGRATION COMPLETION DRIVES RETENTION — users with 3+
463
+ // "integration connected" events get cloned "report generated" events.
464
+ const integrationCount = record.filter(e => e.event === "integration connected").length;
465
+ if (integrationCount >= INTEGRATION_THRESHOLD) {
466
+ const templateReport = record.find(e => e.event === "report generated");
467
+ if (templateReport) {
468
+ const integrationEvents = record.filter(e => e.event === "integration connected");
469
+ integrationEvents.forEach(ie => {
470
+ if (chance.bool({ likelihood: INTEGRATION_REPORT_CLONE_LIKELIHOOD })) {
471
+ record.push({
472
+ ...templateReport,
473
+ time: dayjs(ie.time).add(chance.integer({ min: 1, max: 5 }), "days").toISOString(),
474
+ user_id: ie.user_id,
475
+ report_type: "integration_summary",
476
+ report_pages: chance.integer({ min: 5, max: 25 }),
477
+ });
478
+ }
479
+ });
480
+ }
481
+ }
482
+
483
+ // H5: ALERT FATIGUE — users with >30 stockout alerts get increasing
484
+ // response_time_hours starting at the 20th alert.
485
+ const alertEvents = record.filter(e => e.event === "stockout alert");
486
+ if (alertEvents.length > ALERT_FATIGUE_THRESHOLD) {
487
+ alertEvents.forEach((alert, idx) => {
488
+ if (idx >= ALERT_FATIGUE_START_IDX) {
489
+ const fatigueMultiplier = ALERT_FATIGUE_BASE_MULT + ((idx - ALERT_FATIGUE_START_IDX) / alertEvents.length) * ALERT_FATIGUE_RAMP_MULT;
490
+ alert.response_time_hours = Math.floor((alert.response_time_hours || 4) * fatigueMultiplier);
491
+ }
492
+ });
493
+ }
494
+
495
+ // H6: TRIAL CHURN — users with <10 total events lose 50% after day 7.
496
+ if (record.length < TRIAL_CHURN_EVENT_THRESHOLD) {
497
+ const userFirstEvent = record[0];
498
+ if (userFirstEvent) {
499
+ const firstTime = dayjs(userFirstEvent.time);
500
+ const cutoff = firstTime.add(TRIAL_CHURN_CUTOFF_DAYS, "days");
501
+ for (let i = record.length - 1; i >= 0; i--) {
502
+ if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: TRIAL_CHURN_DROP_LIKELIHOOD })) {
503
+ record.splice(i, 1);
504
+ }
505
+ }
506
+ }
507
+ }
508
+
509
+ // H9: INVENTORY-CHECK MAGIC NUMBER (no flags) — sweet 5-15 inventory
510
+ // checks → +25% PO quantity (1.4x to overcome dilution); over 16+ →
511
+ // drop 60% of PO created events.
512
+ const invCheckCount = record.filter(e => e.event === "inventory checked").length;
513
+ if (invCheckCount >= INVENTORY_SWEET_MIN && invCheckCount <= INVENTORY_SWEET_MAX) {
514
+ record.forEach(e => {
515
+ if (e.event === "purchase order created" && typeof e.quantity === "number") {
516
+ e.quantity = Math.round(e.quantity * INVENTORY_PO_QUANTITY_BOOST);
517
+ }
518
+ });
519
+ } else if (invCheckCount >= INVENTORY_OVER_THRESHOLD) {
520
+ for (let i = record.length - 1; i >= 0; i--) {
521
+ if (record[i].event === "purchase order created" && chance.bool({ likelihood: INVENTORY_OVER_PO_DROP_LIKELIHOOD })) {
522
+ record.splice(i, 1);
523
+ }
524
+ }
525
+ }
526
+
527
+ return record;
528
+ }
529
+
530
+ // ── CONFIG ──
342
531
  /** @type {Config} */
343
532
  const config = {
344
533
  version: 2,
345
- token,
346
534
  seed: SEED,
347
- datasetStart: "2026-01-01T00:00:00Z",
348
- datasetEnd: "2026-05-01T23:59:59Z",
349
- // numDays: num_days,
350
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
351
- numUsers: num_users,
352
- hasAnonIds: true,
353
- avgDevicePerUser: 2,
354
- hasSessionIds: true,
535
+ datasetStart: DATASET_START,
536
+ datasetEnd: DATASET_END,
537
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
538
+ numUsers: NUM_USERS,
355
539
  format: "json",
356
540
  gzip: true,
357
- alsoInferFunnels: false,
358
- hasLocation: true,
359
- hasAndroidDevices: false,
360
- hasIOSDevices: false,
361
- hasDesktopDevices: true,
362
- hasBrowser: true,
363
- hasCampaigns: false,
364
- isAnonymous: false,
365
- hasAdSpend: false,
366
- hasAvatar: true,
541
+ credentials: {
542
+ token,
543
+ },
544
+ switches: {
545
+ hasSessionIds: true,
546
+ alsoInferFunnels: false,
547
+ hasLocation: true,
548
+ hasAndroidDevices: false,
549
+ hasIOSDevices: false,
550
+ hasDesktopDevices: true,
551
+ hasBrowser: true,
552
+ hasCampaigns: false,
553
+ isAnonymous: false,
554
+ hasAdSpend: false,
555
+ hasAvatar: true,
556
+ },
557
+ identity: {
558
+ avgDevicePerUser: 2,
559
+ },
367
560
  concurrency: 1,
368
561
  writeToDisk: false,
369
562
  scdProps: {
@@ -391,6 +584,7 @@ const config = {
391
584
  {
392
585
  event: "inventory checked",
393
586
  weight: 8,
587
+ isStrictEvent: false,
394
588
  properties: {
395
589
  warehouse_id: chance.pickone.bind(chance, warehouseIds),
396
590
  sku_category: ["electronics", "apparel", "food_beverage", "pharma", "raw_materials", "packaging", "automotive_parts"],
@@ -402,6 +596,7 @@ const config = {
402
596
  {
403
597
  event: "stockout alert",
404
598
  weight: 5,
599
+ isStrictEvent: false,
405
600
  properties: {
406
601
  warehouse_id: chance.pickone.bind(chance, warehouseIds),
407
602
  sku_category: ["electronics", "apparel", "food_beverage", "pharma", "raw_materials", "packaging"],
@@ -413,6 +608,7 @@ const config = {
413
608
  {
414
609
  event: "purchase order created",
415
610
  weight: 5,
611
+ isStrictEvent: false,
416
612
  properties: {
417
613
  supplier_id: chance.pickone.bind(chance, supplierIds),
418
614
  sku_category: ["electronics", "apparel", "food_beverage", "pharma", "raw_materials", "packaging", "automotive_parts"],
@@ -457,6 +653,7 @@ const config = {
457
653
  {
458
654
  event: "integration connected",
459
655
  weight: 2,
656
+ isStrictEvent: false,
460
657
  properties: {
461
658
  integration_type: ["erp", "accounting", "shipping", "ecommerce", "crm", "bi_tool"],
462
659
  integration_name: ["SAP", "QuickBooks", "ShipStation", "Shopify", "Salesforce", "NetSuite", "Xero"],
@@ -466,6 +663,7 @@ const config = {
466
663
  {
467
664
  event: "report generated",
468
665
  weight: 4,
666
+ isStrictEvent: false,
469
667
  properties: {
470
668
  report_type: ["inventory_summary", "order_history", "supplier_performance", "cost_analysis", "forecast", "compliance"],
471
669
  report_pages: u.weighNumRange(1, 50, 0.4, 20),
@@ -476,6 +674,7 @@ const config = {
476
674
  {
477
675
  event: "alert configured",
478
676
  weight: 2,
677
+ isStrictEvent: false,
479
678
  properties: {
480
679
  alert_type: ["low_stock", "delivery_delay", "price_change", "quality_threshold", "expiry_warning"],
481
680
  threshold_value: u.weighNumRange(1, 500, 0.4, 50),
@@ -569,6 +768,7 @@ const config = {
569
768
  order: "sequential",
570
769
  timeToConvert: 168,
571
770
  weight: 5,
771
+ reentry: true,
572
772
  },
573
773
  {
574
774
  name: "Supplier Management",
@@ -774,185 +974,11 @@ const config = {
774
974
  },
775
975
  ],
776
976
 
777
- // -- Hook Function ------------------------------------------------
778
- hook: function (record, type, meta) {
779
- // HOOK 10 (T2C): ONBOARDING TIME-TO-CONVERT (funnel-post)
780
- // Enterprise tier completes Onboarding funnel 1.4x faster (factor 0.71);
781
- // small_business 1.3x slower (factor 1.3).
782
- if (type === "funnel-post") {
783
- const segment = meta?.profile?.company_tier;
784
- if (Array.isArray(record) && record.length > 1) {
785
- const factor = (
786
- segment === "enterprise" ? 0.71 :
787
- segment === "small_business" || segment === "trial" ? 1.3 :
788
- 1.0
789
- );
790
- if (factor !== 1.0) {
791
- for (let i = 1; i < record.length; i++) {
792
- const prev = dayjs(record[i - 1].time);
793
- const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
794
- record[i].time = prev.add(newGap, "milliseconds").toISOString();
795
- }
796
- }
797
- }
798
- }
799
-
800
- // -- HOOK 7: ENTERPRISE PROFILES (user) -----------------------
801
- // Enterprise ops get large warehouse_count and employee_count.
802
- if (type === "user") {
803
- if (record.company_tier === "enterprise") {
804
- record.warehouse_count = chance.integer({ min: 5, max: 15 });
805
- record.employee_count = chance.integer({ min: 200, max: 2000 });
806
- } else if (record.company_tier === "mid_market") {
807
- record.warehouse_count = chance.integer({ min: 2, max: 6 });
808
- record.employee_count = chance.integer({ min: 20, max: 200 });
809
- } else if (record.company_tier === "small_business") {
810
- record.warehouse_count = chance.integer({ min: 1, max: 3 });
811
- record.employee_count = chance.integer({ min: 5, max: 80 });
812
- } else if (record.company_tier === "trial") {
813
- record.warehouse_count = 1;
814
- record.employee_count = chance.integer({ min: 1, max: 10 });
815
- }
816
- }
817
-
818
- // -- HOOK 8: ENTERPRISE INTEGRATION FUNNEL LIFT (funnel-pre) --
819
- // (conversionRate boost removed — filtering applied in everything hook instead)
820
- if (type === "funnel-pre") {
821
- // no-op: conversion differentiation handled via event filtering below
822
- }
823
-
824
- // -- HOOK 1: MONTH-END REPORTING SURGE ------------------------
825
- // Moved to everything hook (after sessionization) so day-of-month
826
- // tags match final timestamps. See everything hook below.
827
-
828
- // -- HOOK 2: RUSH ORDER PREMIUM (event) -------------------
829
- // Urgent purchase orders get 1.5x unit_cost.
830
- if (type === "event") {
831
- if (record.event === "purchase order created" && record.priority === "urgent") {
832
- record.unit_cost = Math.floor((record.unit_cost || 50) * 1.5);
833
- }
834
- }
835
-
836
- // -- EVERYTHING HOOKS -----------------------------------------
837
- if (type === "everything") {
838
- if (!record.length) return record;
839
- const profile = meta.profile;
840
-
841
- // -- SUPER-PROP STAMPING ----------------------------------
842
- // Stamp superProps from profile so they are consistent per-user.
843
- if (profile) {
844
- const plat = profile.Platform;
845
- const plan = profile.subscription_plan;
846
- record.forEach(e => {
847
- if (plat) e.Platform = plat;
848
- if (plan) e.subscription_plan = plan;
849
- });
850
- }
851
-
852
- // -- HOOK 1: MONTH-END REPORTING SURGE --------------------
853
- // Reports on days 28-31 get 2x report_pages.
854
- // Runs after sessionization so day-of-month matches final timestamps.
855
- for (const e of record) {
856
- if (e.event === 'report generated') {
857
- const dayOfMonth = new Date(e.time).getUTCDate();
858
- if (dayOfMonth >= 28) {
859
- e.report_pages = Math.floor((e.report_pages || 20) * 2.5);
860
- }
861
- }
862
- }
863
-
864
- // -- HOOK 8: SMALL-BUSINESS CONVERSION DROP ---------------
865
- // Small-business users lose ~35% of "alert configured" events
866
- // (last step of Integration Setup funnel), simulating lower
867
- // conversion for smaller teams without dedicated IT resources.
868
- if (profile && profile.company_tier === "small_business") {
869
- record = record.filter(e => {
870
- if (e.event === "alert configured" && chance.bool({ likelihood: 35 })) {
871
- return false;
872
- }
873
- return true;
874
- });
875
- }
876
-
877
- // -- HOOK 3: REORDER ACCURACY BY TIER ---------------------
878
- // Enterprise users get 10% of stockout alerts removed.
879
- if (profile && profile.company_tier === "enterprise") {
880
- for (let i = record.length - 1; i >= 0; i--) {
881
- if (record[i].event === "stockout alert" && chance.bool({ likelihood: 10 })) {
882
- record.splice(i, 1);
883
- }
884
- }
885
- }
886
-
887
- // -- HOOK 4: INTEGRATION COMPLETION DRIVES RETENTION ------
888
- // Users with 3+ integration events get cloned report events.
889
- const integrationCount = record.filter(e => e.event === "integration connected").length;
890
- if (integrationCount >= 3) {
891
- const templateReport = record.find(e => e.event === "report generated");
892
- if (templateReport) {
893
- const integrationEvents = record.filter(e => e.event === "integration connected");
894
- integrationEvents.forEach(ie => {
895
- if (chance.bool({ likelihood: 65 })) {
896
- record.push({
897
- ...templateReport,
898
- time: dayjs(ie.time).add(chance.integer({ min: 1, max: 5 }), "days").toISOString(),
899
- user_id: ie.user_id,
900
- report_type: "integration_summary",
901
- report_pages: chance.integer({ min: 5, max: 25 }),
902
- });
903
- }
904
- });
905
- }
906
- }
907
-
908
- // -- HOOK 5: ALERT FATIGUE --------------------------------
909
- // Users with >30 stockout alerts get increasing response times.
910
- const alertEvents = record.filter(e => e.event === "stockout alert");
911
- if (alertEvents.length > 30) {
912
- alertEvents.forEach((alert, idx) => {
913
- if (idx >= 20) {
914
- const fatigueMultiplier = 1.5 + ((idx - 20) / alertEvents.length) * 1.5;
915
- alert.response_time_hours = Math.floor((alert.response_time_hours || 4) * fatigueMultiplier);
916
- }
917
- });
918
- }
919
-
920
- // -- HOOK 6: TRIAL CHURN ----------------------------------
921
- // Users with <10 events lose 50% after day 7.
922
- if (record.length < 10) {
923
- const userFirstEvent = record[0];
924
- if (userFirstEvent) {
925
- const firstTime = dayjs(userFirstEvent.time);
926
- const cutoff = firstTime.add(7, "days");
927
- for (let i = record.length - 1; i >= 0; i--) {
928
- if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: 50 })) {
929
- record.splice(i, 1);
930
- }
931
- }
932
- }
933
- }
934
-
935
- // -- HOOK 9: INVENTORY-CHECK MAGIC NUMBER (no flags) ------
936
- // Sweet 5-15 inventory checks → +25% PO quantity.
937
- // Over 16+ → drop 60% of PO created events (high rate to overcome persona event multiplier dilution).
938
- const invCheckCount = record.filter(e => e.event === 'inventory checked').length;
939
- if (invCheckCount >= 5 && invCheckCount <= 15) {
940
- record.forEach(e => {
941
- if (e.event === 'purchase order created' && typeof e.quantity === 'number') {
942
- e.quantity = Math.round(e.quantity * 1.25);
943
- }
944
- });
945
- } else if (invCheckCount >= 16) {
946
- for (let i = record.length - 1; i >= 0; i--) {
947
- if (record[i].event === 'purchase order created' && chance.bool({ likelihood: 60 })) {
948
- record.splice(i, 1);
949
- }
950
- }
951
- }
952
-
953
- return record;
954
- }
955
-
977
+ hook(record, type, meta) {
978
+ if (type === "user") return handleUserHooks(record);
979
+ if (type === "event") return handleEventHooks(record);
980
+ if (type === "funnel-post") return handleFunnelPostHooks(record, meta);
981
+ if (type === "everything") return handleEverythingHooks(record, meta);
956
982
  return record;
957
983
  },
958
984
  };