@ak--47/dungeon-master 1.2.3 → 1.3.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 (89) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +51 -13
  3. package/dungeons/technical/ad-spend.js +2 -2
  4. package/dungeons/technical/anonymous-users.js +2 -2
  5. package/dungeons/technical/array-of-object-lookup.js +2 -4
  6. package/dungeons/technical/experiments.js +2 -2
  7. package/dungeons/technical/foobar.js +2 -2
  8. package/dungeons/technical/group-analytics.js +2 -2
  9. package/dungeons/technical/mirror-strategies.js +2 -2
  10. package/dungeons/technical/nested-objects.js +2 -2
  11. package/dungeons/technical/retention-cadence.js +2 -3
  12. package/dungeons/technical/sanity.js +2 -2
  13. package/dungeons/technical/scale-test.js +2 -2
  14. package/dungeons/technical/scd.js +2 -2
  15. package/dungeons/technical/simple.js +5 -6
  16. package/dungeons/technical/simplest-schema.json +5 -0
  17. package/dungeons/technical/simplest.js +2 -2
  18. package/dungeons/technical/text-generation.js +3 -3
  19. package/dungeons/vertical/ai-platform.js +858 -0
  20. package/dungeons/vertical/community.js +84 -40
  21. package/dungeons/vertical/crypto.js +830 -0
  22. package/dungeons/vertical/dating.js +744 -0
  23. package/dungeons/vertical/devtools.js +175 -69
  24. package/dungeons/vertical/ecommerce.js +242 -94
  25. package/dungeons/vertical/education.js +330 -313
  26. package/dungeons/vertical/fintech.js +442 -313
  27. package/dungeons/vertical/fitness.js +143 -61
  28. package/dungeons/vertical/food-delivery.js +327 -353
  29. package/dungeons/vertical/gaming.js +912 -382
  30. package/dungeons/vertical/healthcare.js +142 -63
  31. package/dungeons/vertical/insurance-application.js +170 -76
  32. package/dungeons/vertical/logistics.js +115 -20
  33. package/dungeons/vertical/marketplace.js +152 -58
  34. package/dungeons/vertical/media.js +248 -384
  35. package/dungeons/vertical/real-estate.js +781 -0
  36. package/dungeons/vertical/sass.js +255 -266
  37. package/dungeons/vertical/social.js +264 -206
  38. package/dungeons/vertical/travel.js +117 -41
  39. package/index.js +17 -17
  40. package/lib/core/config-validator.js +159 -31
  41. package/lib/core/context.js +10 -24
  42. package/lib/core/storage.js +6 -1
  43. package/lib/generators/events.js +11 -14
  44. package/lib/generators/funnels.js +12 -4
  45. package/lib/generators/mirror.js +3 -2
  46. package/lib/generators/product-names.js +1 -1
  47. package/lib/generators/scd.js +2 -1
  48. package/lib/generators/text.js +1 -1
  49. package/lib/orchestrators/user-loop.js +81 -47
  50. package/lib/templates/macro-presets.js +111 -0
  51. package/lib/templates/soup-presets.js +19 -36
  52. package/lib/utils/utils.js +71 -39
  53. package/package.json +8 -2
  54. package/scripts/smoke-test-all.mjs +162 -0
  55. package/scripts/verify-runner.mjs +72 -24
  56. package/types.d.ts +251 -51
  57. package/dungeons/technical/ad-spend-schema.json +0 -128
  58. package/dungeons/technical/anonymous-users-schema.json +0 -92
  59. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  60. package/dungeons/technical/experiments-schema.json +0 -203
  61. package/dungeons/technical/foobar-schema.json +0 -362
  62. package/dungeons/technical/group-analytics-schema.json +0 -241
  63. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  64. package/dungeons/technical/nested-objects-schema.json +0 -145
  65. package/dungeons/technical/retention-cadence-schema.json +0 -37
  66. package/dungeons/technical/sanity-schema.json +0 -185
  67. package/dungeons/technical/scale-test-schema.json +0 -70
  68. package/dungeons/technical/scd-schema.json +0 -467
  69. package/dungeons/technical/simple-schema.json +0 -362
  70. package/dungeons/technical/text-generation-schema.json +0 -1062
  71. package/dungeons/user/.gitkeep +0 -0
  72. package/dungeons/vertical/community-schema.json +0 -579
  73. package/dungeons/vertical/devtools-schema.json +0 -601
  74. package/dungeons/vertical/ecommerce-schema.json +0 -604
  75. package/dungeons/vertical/education-schema.json +0 -5686
  76. package/dungeons/vertical/fintech-schema.json +0 -630
  77. package/dungeons/vertical/fitness-schema.json +0 -530
  78. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  79. package/dungeons/vertical/gaming-schema.json +0 -438
  80. package/dungeons/vertical/healthcare-schema.json +0 -549
  81. package/dungeons/vertical/insurance-application-schema.json +0 -485
  82. package/dungeons/vertical/logistics-schema.json +0 -574
  83. package/dungeons/vertical/marketplace-schema.json +0 -533
  84. package/dungeons/vertical/media-schema.json +0 -4749
  85. package/dungeons/vertical/rpg-schema.json +0 -2491
  86. package/dungeons/vertical/rpg.js +0 -976
  87. package/dungeons/vertical/sass-schema.json +0 -3128
  88. package/dungeons/vertical/social-schema.json +0 -620
  89. package/dungeons/vertical/travel-schema.json +0 -580
@@ -2,7 +2,7 @@
2
2
  const SEED = "dm4-logistics";
3
3
  const num_days = 100;
4
4
  const num_users = 5_000;
5
- const avg_events_per_user = 120;
5
+ const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
8
8
  // ── env overrides ──
@@ -16,9 +16,6 @@ import * as v from "ak-tools";
16
16
 
17
17
  dayjs.extend(utc);
18
18
  const chance = u.initChance(SEED);
19
- const NOW = dayjs();
20
- const DATASET_START = NOW.subtract(num_days, "days");
21
-
22
19
  /** @typedef {import("../../types").Dungeon} Config */
23
20
 
24
21
  // Generate consistent warehouse/supplier IDs at module level
@@ -56,7 +53,12 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
56
53
 
57
54
  /**
58
55
  * ===================================================================
59
- * ANALYTICS HOOKS (8 hooks)
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.
60
62
  * ===================================================================
61
63
  *
62
64
  * -------------------------------------------------------------------
@@ -123,6 +125,15 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
123
125
  * - Breakdown: user property "company_tier"
124
126
  * - Expected: enterprise ratio ~0.9x vs small_business baseline
125
127
  *
128
+ * Report 3: Stockout-to-Inventory-Check Ratio (NORMALIZED — recommended)
129
+ * - Report type: Insights (formula)
130
+ * - A = "stockout alert" total, B = "inventory checked" total
131
+ * - Formula: A / B
132
+ * - Breakdown: user property "company_tier"
133
+ * - Expected: enterprise ratio ~ 0.9x SMB ratio (10% reduction visible)
134
+ * - WHY THIS METRIC: per-user counts are dominated by persona event multipliers
135
+ * (enterprise has 5x baseline activity); ratio normalizes that out.
136
+ *
126
137
  * REAL-WORLD ANALOGUE: Enterprise operations have dedicated
127
138
  * procurement teams and predictive analytics reducing stockouts.
128
139
  *
@@ -243,6 +254,41 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
243
254
  * REAL-WORLD ANALOGUE: Small businesses lack dedicated IT teams,
244
255
  * leading to incomplete integration setup and lower alert adoption.
245
256
  *
257
+ * -------------------------------------------------------------------
258
+ * 9. INVENTORY-CHECK MAGIC NUMBER (everything hook)
259
+ * -------------------------------------------------------------------
260
+ *
261
+ * PATTERN: Users with 5-15 "inventory checked" events sit in the
262
+ * "engaged-but-focused" sweet spot — every "purchase order created"
263
+ * event gets quantity boosted ~25%. Users with 16 or more inventory
264
+ * checks are over-engaged (paralysis); ~30% of their "purchase order
265
+ * created" events are dropped. No flag is stamped — discoverable only
266
+ * by binning users on inventory-check COUNT and comparing PO totals.
267
+ *
268
+ * HOW TO FIND IT IN MIXPANEL:
269
+ *
270
+ * Report 1: PO Quantity by Inventory-Check Bucket
271
+ * - Report type: Insights (with cohort)
272
+ * - Cohort A: users with 5-15 "inventory checked" events
273
+ * - Cohort B: users with 0-4 "inventory checked" events
274
+ * - Event: "purchase order created"
275
+ * - Measure: Average of "quantity"
276
+ * - Compare cohort A vs cohort B
277
+ * - Expected: cohort A ~ 1.25x higher quantity than B
278
+ *
279
+ * Report 2: POs per User by Browse Intensity
280
+ * - Report type: Insights (with cohort)
281
+ * - Cohort C: users with >= 16 "inventory checked" events
282
+ * - Cohort A: users with 5-15 "inventory checked" events
283
+ * - Event: "purchase order created"
284
+ * - Measure: Total events per user
285
+ * - Compare cohort C vs cohort A
286
+ * - Expected: cohort C has ~ 30% fewer POs per user
287
+ *
288
+ * REAL-WORLD ANALOGUE: A focused operations team that monitors
289
+ * stock just enough places larger, more confident orders; an
290
+ * obsessive checker is paralysed and orders less.
291
+ *
246
292
  * ===================================================================
247
293
  * EXPECTED METRICS SUMMARY
248
294
  * ===================================================================
@@ -256,15 +302,19 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
256
302
  * Alert Fatigue | response_time_hours | 4h | 8-12h | 2-3x
257
303
  * Trial Churn | events after wk 1 | 5 | 2.5 | 0.5x
258
304
  * Enterprise Profiles | warehouse_count | 3 | 10 | 3.3x
259
- * Small-Biz Conversion Drop | funnel conversion | 30% | 20% | 0.65x
305
+ * Small-Biz Conversion Drop | funnel conversion | 30% | 20% | 0.65x
306
+ * Inventory-Check Magic Num | sweet PO quantity | 1x | 1.25x | 1.25x
307
+ * Inventory-Check Magic Num | over POs/user | 1x | 0.7x | -30%
260
308
  */
261
309
 
262
310
  /** @type {Config} */
263
311
  const config = {
264
312
  token,
265
313
  seed: SEED,
266
- numDays: num_days,
267
- numEvents: num_users * avg_events_per_user,
314
+ datasetStart: "2026-01-01T00:00:00Z",
315
+ datasetEnd: "2026-04-28T23:59:59Z",
316
+ // numDays: num_days,
317
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
268
318
  numUsers: num_users,
269
319
  hasAnonIds: false,
270
320
  hasSessionIds: true,
@@ -279,7 +329,6 @@ const config = {
279
329
  hasCampaigns: false,
280
330
  isAnonymous: false,
281
331
  hasAdSpend: false,
282
- percentUsersBornInDataset: 35,
283
332
  hasAvatar: true,
284
333
  concurrency: 1,
285
334
  writeToDisk: false,
@@ -692,6 +741,27 @@ const config = {
692
741
 
693
742
  // -- Hook Function ------------------------------------------------
694
743
  hook: function (record, type, meta) {
744
+ // HOOK 10 (T2C): ONBOARDING TIME-TO-CONVERT (funnel-post)
745
+ // Enterprise tier completes Onboarding funnel 1.4x faster (factor 0.71);
746
+ // small_business 1.3x slower (factor 1.3).
747
+ if (type === "funnel-post") {
748
+ const segment = meta?.profile?.company_tier;
749
+ if (Array.isArray(record) && record.length > 1) {
750
+ const factor = (
751
+ segment === "enterprise" ? 0.71 :
752
+ segment === "small_business" || segment === "trial" ? 1.3 :
753
+ 1.0
754
+ );
755
+ if (factor !== 1.0) {
756
+ for (let i = 1; i < record.length; i++) {
757
+ const prev = dayjs(record[i - 1].time);
758
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
759
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
760
+ }
761
+ }
762
+ }
763
+ }
764
+
695
765
  // -- HOOK 7: ENTERPRISE PROFILES (user) -----------------------
696
766
  // Enterprise ops get large warehouse_count and employee_count.
697
767
  if (type === "user") {
@@ -713,18 +783,13 @@ const config = {
713
783
  // no-op: conversion differentiation handled via event filtering below
714
784
  }
715
785
 
716
- // -- HOOK 1: MONTH-END REPORTING SURGE (event) ----------------
717
- // Reports on days 28-31 get 2x report_pages.
718
- if (type === "event") {
719
- if (record.event === "report generated") {
720
- const dayOfMonth = dayjs(record.time).date();
721
- if (dayOfMonth >= 28) {
722
- record.report_pages = Math.floor((record.report_pages || 20) * 2);
723
- }
724
- }
786
+ // -- HOOK 1: MONTH-END REPORTING SURGE ------------------------
787
+ // Moved to everything hook (after sessionization) so day-of-month
788
+ // tags match final timestamps. See everything hook below.
725
789
 
726
- // -- HOOK 2: RUSH ORDER PREMIUM (event) -------------------
727
- // Urgent purchase orders get 1.5x unit_cost.
790
+ // -- HOOK 2: RUSH ORDER PREMIUM (event) -------------------
791
+ // Urgent purchase orders get 1.5x unit_cost.
792
+ if (type === "event") {
728
793
  if (record.event === "purchase order created" && record.priority === "urgent") {
729
794
  record.unit_cost = Math.floor((record.unit_cost || 50) * 1.5);
730
795
  }
@@ -746,6 +811,18 @@ const config = {
746
811
  });
747
812
  }
748
813
 
814
+ // -- HOOK 1: MONTH-END REPORTING SURGE --------------------
815
+ // Reports on days 28-31 get 2x report_pages.
816
+ // Runs after sessionization so day-of-month matches final timestamps.
817
+ for (const e of record) {
818
+ if (e.event === 'report generated') {
819
+ const dayOfMonth = new Date(e.time).getUTCDate();
820
+ if (dayOfMonth >= 28) {
821
+ e.report_pages = Math.floor((e.report_pages || 20) * 2);
822
+ }
823
+ }
824
+ }
825
+
749
826
  // -- HOOK 8: SMALL-BUSINESS CONVERSION DROP ---------------
750
827
  // Small-business users lose ~35% of "alert configured" events
751
828
  // (last step of Integration Setup funnel), simulating lower
@@ -817,6 +894,24 @@ const config = {
817
894
  }
818
895
  }
819
896
 
897
+ // -- HOOK 9: INVENTORY-CHECK MAGIC NUMBER (no flags) ------
898
+ // Sweet 5-15 inventory checks → +25% PO quantity.
899
+ // Over 16+ → drop 30% of PO created events.
900
+ const invCheckCount = record.filter(e => e.event === 'inventory checked').length;
901
+ if (invCheckCount >= 5 && invCheckCount <= 15) {
902
+ record.forEach(e => {
903
+ if (e.event === 'purchase order created' && typeof e.quantity === 'number') {
904
+ e.quantity = Math.round(e.quantity * 1.25);
905
+ }
906
+ });
907
+ } else if (invCheckCount >= 16) {
908
+ for (let i = record.length - 1; i >= 0; i--) {
909
+ if (record[i].event === 'purchase order created' && chance.bool({ likelihood: 30 })) {
910
+ record.splice(i, 1);
911
+ }
912
+ }
913
+ }
914
+
820
915
  return record;
821
916
  }
822
917
 
@@ -2,7 +2,7 @@
2
2
  const SEED = "dm4-marketplace";
3
3
  const num_days = 100;
4
4
  const num_users = 5_000;
5
- const avg_events_per_user = 120;
5
+ const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
8
8
  // ── env overrides ──
@@ -16,9 +16,6 @@ import * as v from "ak-tools";
16
16
 
17
17
  dayjs.extend(utc);
18
18
  const chance = u.initChance(SEED);
19
- const NOW = dayjs();
20
- const DATASET_START = NOW.subtract(num_days, "days");
21
-
22
19
  /** @typedef {import("../../types").Dungeon} Config */
23
20
 
24
21
  // Generate consistent seller store and listing IDs at module level
@@ -56,33 +53,33 @@ const listingIds = v.range(1, 500).map(() => `LST_${v.uid(8)}`);
56
53
 
57
54
  /**
58
55
  * ═══════════════════════════════════════════════════════════════
59
- * ANALYTICS HOOKS (8 hooks)
56
+ * ANALYTICS HOOKS (10 hooks)
57
+ *
58
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
59
+ * raw-prop breakdowns (segment, day, category) or behavioral cohorts.
60
+ *
61
+ * Adds 9. BROWSE TO PURCHASE TIME-TO-CONVERT and 10. MESSAGE-COUNT MAGIC
62
+ * NUMBER on top of original 8.
60
63
  * ═══════════════════════════════════════════════════════════════
61
64
  *
62
65
  * ───────────────────────────────────────────────────────────────
63
- * 1. FEE CHANGE IMPACT (event hook)
66
+ * 1. FEE CHANGE IMPACT (everything hook)
64
67
  * ───────────────────────────────────────────────────────────────
65
68
  *
66
69
  * PATTERN: After day 45 (permanent marketplace fee increase), all
67
70
  * "listing created" events get listing_fee multiplied by 1.3x.
68
- * Simulates the revenue impact of a platform fee adjustment.
71
+ * Simulates the revenue impact of a platform fee adjustment. No flag —
72
+ * discover via line chart of avg listing_fee over time.
69
73
  *
70
74
  * HOW TO FIND IT IN MIXPANEL:
71
75
  *
72
- * Report 1: Listing Fee Before vs After Fee Change
76
+ * Report 1: Listing Fee Over Time
73
77
  * • Report type: Insights
74
78
  * • Event: "listing created"
75
79
  * • Measure: Average of "listing_fee"
76
80
  * • Line chart by week
77
81
  * • Expected: Clear step-up around day 45 from ~$15 avg to ~$20 avg
78
82
  *
79
- * Report 2: Fee Change Flag Distribution
80
- * • Report type: Insights
81
- * • Event: "listing created"
82
- * • Measure: Total
83
- * • Breakdown: "fee_change"
84
- * • Expected: "increased" appears only after day 45
85
- *
86
83
  * REAL-WORLD ANALOGUE: Marketplace platforms periodically adjust
87
84
  * commission/listing fees, impacting seller economics and behavior.
88
85
  *
@@ -238,28 +235,80 @@ const listingIds = v.range(1, 500).map(() => `LST_${v.uid(8)}`);
238
235
  * REAL-WORLD ANALOGUE: Returning buyers have established trust
239
236
  * and familiarity with the platform, converting at higher rates.
240
237
  *
238
+ * ───────────────────────────────────────────────────────────────
239
+ * 9. BROWSE TO PURCHASE TIME-TO-CONVERT (funnel-post)
240
+ *
241
+ * PATTERN: Power_seller and frequent_buyer users complete the
242
+ * Browse to Purchase funnel 1.4x faster (factor 0.71); window_shopper
243
+ * 1.4x slower (factor 1.4).
244
+ *
245
+ * HOW TO FIND IT IN MIXPANEL:
246
+ *
247
+ * Report 1: Browse to Purchase Median Time-to-Convert by Segment
248
+ * - Funnels > "item searched" -> "item viewed" -> "add to cart" -> "purchase completed"
249
+ * - Measure: Median time to convert
250
+ * - Breakdown: segment
251
+ * - Expected: power/frequent ~ 0.71x; window ~ 1.4x
252
+ *
253
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
254
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
255
+ * show this — funnel-post adjusts gaps within funnel instances, not
256
+ * across the user's full event history.
257
+ *
258
+ * ───────────────────────────────────────────────────────────────
259
+ * 10. MESSAGE-COUNT MAGIC NUMBER (in-funnel, everything)
260
+ *
261
+ * PATTERN: Sweet 2-5 message-sent events between item-viewed and
262
+ * offer-received → +35% on offer_amount of offer-received events.
263
+ * Over 6+ → drop 25% of purchase-completed events (haggling deadlock).
264
+ * No flag.
265
+ *
266
+ * HOW TO FIND IT IN MIXPANEL:
267
+ *
268
+ * Report 1: Avg Offer Amount by Message Bucket
269
+ * - Cohort A: users with 2-5 messages between item-viewed and offer-received
270
+ * - Cohort B: users with 0-1
271
+ * - Event: "offer received"
272
+ * - Measure: Average of "offer_amount"
273
+ * - Expected: A ~ 1.35x B
274
+ *
275
+ * Report 2: Purchase Rate on Heavy Messengers
276
+ * - Cohort C: users with >= 6 messages
277
+ * - Cohort A: users with 2-5
278
+ * - Event: "purchase completed"
279
+ * - Measure: Total per user
280
+ * - Expected: C ~ 25% fewer purchases per user
281
+ *
282
+ * REAL-WORLD ANALOGUE: A few quick clarifying messages close deals;
283
+ * extended haggling kills conversion.
284
+ *
241
285
  * ═══════════════════════════════════════════════════════════════
242
286
  * EXPECTED METRICS SUMMARY
243
287
  * ═══════════════════════════════════════════════════════════════
244
288
  *
245
- * Hook | Metric | Baseline | Effect | Ratio
246
- * ────────────────────────────|─────────────────────|──────────|─────────|──────
247
- * Fee Change Impact | listing_fee | $15 | $20 | 1.3x
248
- * Weekend Shopping Surge | total_amount | $60 | $72 | 1.2x
249
- * Seller Success → Trust | purchases/user | 3 | 6 | 2x
250
- * Electronics Category Lift | electronics purch. | baseline | 1.5x | 1.5x
251
- * Response Time → Conversion | offer_accepted/user | 1 | 2 | 2x
252
- * New Seller Churn | events post-day-14 | 100% | 60% | 0.6x
253
- * Power Seller Profiles | total_transactions | 0 | 100-500 | --
254
- * Frequent Buyer Funnel | funnel conversion | 30% | 39% | 1.3x
289
+ * Hook | Metric | Baseline | Effect | Ratio
290
+ * ----------------------------|---------------------|----------|-----------|------
291
+ * Fee Change Impact | listing_fee post-D45| 1x | 1.3x | 1.3x
292
+ * Weekend Shopping Surge | total_amount Sat/Sun| 1x | 1.2x | 1.2x
293
+ * Seller Success → Trust | purchases/user | 1x | 2x | 2x
294
+ * Electronics Category Lift | electronics purch. | 1x | 1.5x | 1.5x
295
+ * Response Time → Conversion | offer_accepted/user | 1x | 2x | 2x
296
+ * New Seller Churn | events post-D14 | 1x | 0.6x | -40%
297
+ * Power Seller Profiles | total_transactions | 0 | 100-500 | --
298
+ * Frequent Buyer Funnel | funnel conversion | 30% | 39% | 1.3x
299
+ * Browse to Purchase T2C | median min by segment| 1x | 0.71/1.4x | ~ 2x range
300
+ * Message Magic Number | sweet offer_amount | 1x | 1.35x | 1.35x
301
+ * Message Magic Number | over purchases/user | 1x | 0.75x | -25%
255
302
  */
256
303
 
257
304
  /** @type {Config} */
258
305
  const config = {
259
306
  token,
260
307
  seed: SEED,
261
- numDays: num_days,
262
- numEvents: num_users * avg_events_per_user,
308
+ datasetStart: "2026-01-01T00:00:00Z",
309
+ datasetEnd: "2026-04-28T23:59:59Z",
310
+ // numDays: num_days,
311
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
263
312
  numUsers: num_users,
264
313
  hasAnonIds: false,
265
314
  hasSessionIds: true,
@@ -274,7 +323,6 @@ const config = {
274
323
  hasCampaigns: false,
275
324
  isAnonymous: false,
276
325
  hasAdSpend: false,
277
- percentUsersBornInDataset: 35,
278
326
  hasAvatar: true,
279
327
  concurrency: 1,
280
328
  writeToDisk: false,
@@ -337,7 +385,6 @@ const config = {
337
385
  item_count: u.weighNumRange(1, 5, 0.3),
338
386
  payment_method: ["credit_card", "credit_card", "paypal", "apple_pay", "debit"],
339
387
  shipping_method: ["standard", "standard", "express", "pickup"],
340
- is_whale_purchase: [false],
341
388
  },
342
389
  },
343
390
  {
@@ -349,7 +396,6 @@ const config = {
349
396
  asking_price: u.weighNumRange(5, 500, 0.3, 50),
350
397
  condition: ["new", "new", "like_new", "good", "fair"],
351
398
  listing_fee: u.weighNumRange(5, 30, 0.5, 15),
352
- fee_change: ["none"],
353
399
  },
354
400
  },
355
401
  {
@@ -729,37 +775,34 @@ const config = {
729
775
  }
730
776
  }
731
777
 
732
- // ── HOOK 8: FREQUENT BUYER FUNNEL LIFT (funnel-pre) ──
733
- // (conversionRate filtering moved to everything hook)
734
- if (type === "funnel-pre") {
735
- // no-op: conversion filtering handled via event dropping in everything hook
736
- }
737
-
738
- // ── HOOK 1: FEE CHANGE IMPACT (event) ────────────────
739
- // Listings after day 45 get 1.3x higher listing_fee.
740
- if (type === "event") {
741
- const FEE_CHANGE_DAY = DATASET_START.add(45, "days");
742
- if (record.event === "listing created") {
743
- const eventTime = dayjs(record.time);
744
- if (eventTime.isAfter(FEE_CHANGE_DAY)) {
745
- record.listing_fee = Math.floor((record.listing_fee || 15) * 1.3);
746
- record.fee_change = "increased";
747
- }
748
- }
749
-
750
- // ── HOOK 2: WEEKEND SHOPPING SURGE (event) ───────
751
- // Purchases on Sat/Sun get 1.2x total_amount.
752
- if (record.event === "purchase completed") {
753
- const dayOfWeek = dayjs(record.time).day();
754
- // Saturday=6, Sunday=0
755
- if (dayOfWeek === 0 || dayOfWeek === 6) {
756
- record.total_amount = Math.floor((record.total_amount || 60) * 1.2);
778
+ // HOOK 9 (T2C): BROWSE TO PURCHASE TIME-TO-CONVERT (funnel-post)
779
+ // Power_seller users complete Browse to Purchase funnel 1.4x faster
780
+ // (factor 0.71); window_shopper 1.4x slower (factor 1.4).
781
+ if (type === "funnel-post") {
782
+ const segment = meta?.profile?.segment;
783
+ if (Array.isArray(record) && record.length > 1) {
784
+ const factor = (
785
+ segment === "power_seller" || segment === "frequent_buyer" ? 0.71 :
786
+ segment === "window_shopper" ? 1.4 :
787
+ 1.0
788
+ );
789
+ if (factor !== 1.0) {
790
+ for (let i = 1; i < record.length; i++) {
791
+ const prev = dayjs(record[i - 1].time);
792
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
793
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
794
+ }
757
795
  }
758
796
  }
759
797
  }
760
798
 
799
+ // HOOK 1: FEE CHANGE IMPACT — moved to everything hook below.
800
+ // (event hook fires before bunchIntoSessions reshuffles timestamps)
801
+
761
802
  // ── EVERYTHING HOOKS ─────────────────────────────────
762
803
  if (type === "everything") {
804
+ const datasetStart = dayjs.unix(meta.datasetStart);
805
+ const FEE_CHANGE_DAY = datasetStart.add(45, "days");
763
806
  let events = record;
764
807
  if (!events.length) return record;
765
808
 
@@ -774,6 +817,14 @@ const config = {
774
817
  });
775
818
  }
776
819
 
820
+ // HOOK 1: FEE CHANGE IMPACT — listings after d45 get listing_fee 1.3x.
821
+ // In everything hook so timestamp comparison sees post-bunchIntoSessions times.
822
+ events.forEach(e => {
823
+ if (e.event === "listing created" && dayjs(e.time).isAfter(FEE_CHANGE_DAY)) {
824
+ e.listing_fee = Math.floor((e.listing_fee || 15) * 1.3);
825
+ }
826
+ });
827
+
777
828
  // ── HOOK 8: FREQUENT BUYER CONVERSION FILTER ────
778
829
  // Non-frequent-buyer users drop ~25% of "purchase completed"
779
830
  // (last step of Browse to Purchase funnel) to simulate lower conversion.
@@ -831,11 +882,13 @@ const config = {
831
882
  }
832
883
 
833
884
  // ── HOOK 5: RESPONSE TIME → CONVERSION ───────────
834
- // Sellers with avg response_time < 2 hours get more offer_accepted.
885
+ // Sellers with avg response_time < 12 hours get more offer_accepted.
886
+ // (response_time_hours centers around ~6h via weighNumRange, so a
887
+ // threshold of 2h was too tight — almost no users qualified.)
835
888
  const messages = events.filter(e => e.event === "message sent" && e.response_time_hours);
836
889
  if (messages.length > 0) {
837
890
  const avgResponseTime = messages.reduce((sum, m) => sum + m.response_time_hours, 0) / messages.length;
838
- if (avgResponseTime < 2) {
891
+ if (avgResponseTime < 12) {
839
892
  const templateOffer = events.find(e => e.event === "offer accepted");
840
893
  if (templateOffer) {
841
894
  const offers = events.filter(e => e.event === "offer received");
@@ -856,7 +909,7 @@ const config = {
856
909
  // ── HOOK 6: NEW SELLER CHURN ─────────────────────
857
910
  // New sellers with <10 events lose 40% of events after day 14.
858
911
  if (profile && profile.segment === "new_seller" && events.length < 10) {
859
- const DAY_14 = DATASET_START.add(14, "days");
912
+ const DAY_14 = datasetStart.add(14, "days");
860
913
  for (let i = events.length - 1; i >= 0; i--) {
861
914
  const eventTime = dayjs(events[i].time);
862
915
  if (eventTime.isAfter(DAY_14) && chance.bool({ likelihood: 40 })) {
@@ -865,6 +918,47 @@ const config = {
865
918
  }
866
919
  }
867
920
 
921
+ // HOOK 2: WEEKEND SHOPPING SURGE — Sat/Sun purchases get
922
+ // total_amount 1.2x. Mutates raw prop. No flag.
923
+ events.forEach(e => {
924
+ if (e.event === "purchase completed") {
925
+ const dow = new Date(e.time).getUTCDay();
926
+ if (dow === 0 || dow === 6) {
927
+ e.total_amount = Math.floor((e.total_amount || 60) * 1.2);
928
+ }
929
+ }
930
+ });
931
+
932
+ // HOOK 10: MESSAGE-COUNT MAGIC NUMBER (in-funnel, no flags)
933
+ // Sweet 2-5 message-sent events between item-viewed and
934
+ // offer-received → +35% on offer-received offer_amount/asking_price.
935
+ // Over 6+ → drop 25% of purchase-completed events (over-message
936
+ // signals haggling deadlock).
937
+ const itemViewed = events.find(e => e.event === "item viewed");
938
+ const offerReceived = events.find(e => e.event === "offer received");
939
+ if (itemViewed && offerReceived) {
940
+ const aTime = dayjs(itemViewed.time);
941
+ const bTime = dayjs(offerReceived.time);
942
+ const msgBetween = events.filter(e =>
943
+ e.event === "message sent" &&
944
+ dayjs(e.time).isAfter(aTime) &&
945
+ dayjs(e.time).isBefore(bTime)
946
+ ).length;
947
+ if (msgBetween >= 2 && msgBetween <= 5) {
948
+ events.forEach(e => {
949
+ if (e.event === "offer received" && typeof e.offer_amount === "number") {
950
+ e.offer_amount = Math.round(e.offer_amount * 1.35);
951
+ }
952
+ });
953
+ } else if (msgBetween >= 6) {
954
+ for (let i = events.length - 1; i >= 0; i--) {
955
+ if (events[i].event === "purchase completed" && chance.bool({ likelihood: 25 })) {
956
+ events.splice(i, 1);
957
+ }
958
+ }
959
+ }
960
+ }
961
+
868
962
  return record;
869
963
  }
870
964