@ak--47/dungeon-master 1.3.0 → 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 (74) hide show
  1. package/dungeons/technical/array-of-object-lookup.js +0 -2
  2. package/dungeons/technical/simple.js +3 -4
  3. package/dungeons/technical/simplest-schema.json +5 -0
  4. package/dungeons/technical/text-generation.js +1 -1
  5. package/dungeons/vertical/ai-platform.js +192 -133
  6. package/dungeons/vertical/community.js +64 -34
  7. package/dungeons/vertical/crypto.js +333 -224
  8. package/dungeons/vertical/dating.js +228 -282
  9. package/dungeons/vertical/devtools.js +164 -62
  10. package/dungeons/vertical/ecommerce.js +240 -91
  11. package/dungeons/vertical/education.js +328 -310
  12. package/dungeons/vertical/fintech.js +437 -311
  13. package/dungeons/vertical/fitness.js +141 -58
  14. package/dungeons/vertical/food-delivery.js +298 -318
  15. package/dungeons/vertical/gaming.js +378 -231
  16. package/dungeons/vertical/healthcare.js +140 -60
  17. package/dungeons/vertical/insurance-application.js +168 -73
  18. package/dungeons/vertical/logistics.js +95 -6
  19. package/dungeons/vertical/marketplace.js +137 -51
  20. package/dungeons/vertical/media.js +241 -386
  21. package/dungeons/vertical/real-estate.js +322 -315
  22. package/dungeons/vertical/sass.js +253 -263
  23. package/dungeons/vertical/social.js +262 -206
  24. package/dungeons/vertical/travel.js +59 -16
  25. package/index.js +17 -17
  26. package/lib/core/config-validator.js +93 -21
  27. package/lib/core/context.js +10 -24
  28. package/lib/core/storage.js +6 -1
  29. package/lib/generators/events.js +11 -14
  30. package/lib/generators/funnels.js +12 -4
  31. package/lib/generators/mirror.js +3 -2
  32. package/lib/generators/product-names.js +1 -1
  33. package/lib/generators/scd.js +2 -1
  34. package/lib/generators/text.js +1 -1
  35. package/lib/orchestrators/user-loop.js +47 -47
  36. package/lib/utils/utils.js +71 -39
  37. package/package.json +1 -1
  38. package/scripts/smoke-test-all.mjs +162 -0
  39. package/scripts/verify-runner.mjs +72 -24
  40. package/types.d.ts +38 -15
  41. package/dungeons/technical/ad-spend-schema.json +0 -128
  42. package/dungeons/technical/anonymous-users-schema.json +0 -92
  43. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  44. package/dungeons/technical/experiments-schema.json +0 -203
  45. package/dungeons/technical/foobar-schema.json +0 -362
  46. package/dungeons/technical/group-analytics-schema.json +0 -241
  47. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  48. package/dungeons/technical/nested-objects-schema.json +0 -145
  49. package/dungeons/technical/retention-cadence-schema.json +0 -37
  50. package/dungeons/technical/sanity-schema.json +0 -185
  51. package/dungeons/technical/scale-test-schema.json +0 -70
  52. package/dungeons/technical/scd-schema.json +0 -467
  53. package/dungeons/technical/simple-schema.json +0 -362
  54. package/dungeons/technical/text-generation-schema.json +0 -1062
  55. package/dungeons/vertical/ai-platform-schema.json +0 -617
  56. package/dungeons/vertical/community-schema.json +0 -579
  57. package/dungeons/vertical/crypto-schema.json +0 -546
  58. package/dungeons/vertical/dating-schema.json +0 -401
  59. package/dungeons/vertical/devtools-schema.json +0 -601
  60. package/dungeons/vertical/ecommerce-schema.json +0 -604
  61. package/dungeons/vertical/education-schema.json +0 -5686
  62. package/dungeons/vertical/fintech-schema.json +0 -630
  63. package/dungeons/vertical/fitness-schema.json +0 -530
  64. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  65. package/dungeons/vertical/gaming-schema.json +0 -2703
  66. package/dungeons/vertical/healthcare-schema.json +0 -549
  67. package/dungeons/vertical/insurance-application-schema.json +0 -485
  68. package/dungeons/vertical/logistics-schema.json +0 -574
  69. package/dungeons/vertical/marketplace-schema.json +0 -533
  70. package/dungeons/vertical/media-schema.json +0 -4749
  71. package/dungeons/vertical/real-estate-schema.json +0 -527
  72. package/dungeons/vertical/sass-schema.json +0 -3128
  73. package/dungeons/vertical/social-schema.json +0 -620
  74. package/dungeons/vertical/travel-schema.json +0 -580
@@ -48,91 +48,204 @@ const chance = u.initChance(SEED);
48
48
  * - Seller funnel: property listed → open house attended → property sold (35%)
49
49
  *
50
50
  * ===================================================================
51
- * ANALYTICS HOOKS (8 architected patterns)
51
+ * ANALYTICS HOOKS (10 hooks)
52
+ *
53
+ * Adds 10. TOUR FUNNEL TIME-TO-CONVERT: Premier agents 0.71x faster, Standard
54
+ * 1.3x slower (funnel-post). Discover via Tour Funnel median TTC by agent_tier.
55
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
56
+ * Cross-event MIN→MIN SQL queries on raw events do NOT show this.
52
57
  * ===================================================================
53
58
  *
54
- * 1. SPRING BUYING SEASON (event hookTIME-BASED)
55
- * Days 30-60: spring_season=true on tours/offers. Tour events get
56
- * duration_mins 3x. Offer events get offer_price 2.5x.
57
- * → Insights: "tour scheduled" total over time, breakdown spring_season
58
- * (expect: 3x volume spike during days 30-60)
59
- * → Insights: "offer submitted" avg offer_price, breakdown spring_season
60
- * (expect: 2.5x average during spring)
61
- *
62
- * 2. MORTGAGE RATE SHOCK (event + everything hook — TIME-BASED)
63
- * Day 75: mortgage_rate jumps from 6.5 to 7.5. After day 75 and
64
- * before day 89, mortgage_rate forced to 7.5 on pre-approval events.
65
- * In everything hook, 45% of offer_submitted events after day 75
66
- * are removed (buyer pullback).
67
- * → Insights: "mortgage pre-approval" avg mortgage_rate over time
68
- * (expect: step change from ~6.5 to 7.5 at day 75)
69
- * → Insights: "offer submitted" total over time
70
- * (expect: ~45% volume drop after day 75)
71
- *
72
- * 3. SAVED-SEARCH RETENTION (everything hook — RETENTION)
73
- * Users who create a "saved search created" within first 7 days
74
- * get extra property_viewed and tour_scheduled events injected.
75
- * Non-savers lose 80% of events after day 30.
76
- * → Retention: any → any, segment has_saved_search = true vs false
77
- * (expect: dramatically higher retention for saved-search users)
78
- * → Insights: "property viewed" total, breakdown has_saved_search
79
- *
80
- * 4. PRE-APPROVED BUYER CONVERSION (everything hook — CONVERSION)
81
- * Users with a "mortgage pre-approval" event complete offer_submitted
82
- * 5x more often. Extra offer events injected, pre_approved=true.
83
- * → Insights: "offer submitted" total, breakdown pre_approved
84
- * (expect: 5x volume for pre_approved=true)
85
- * → Funnels: property viewed → tour scheduled → offer submitted,
86
- * segment pre_approved = true vs false
87
- *
88
- * 5. TOP-TIER AGENT ADVANTAGE (everything hook — SUBSCRIPTION TIER)
89
- * Users with agent_tier="Premier" from profile get 3x property_listed
90
- * events and 2x property_sold events. premier_agent=true.
91
- * → Insights: "property listed" total, breakdown premier_agent
92
- * (expect: Premier agents ~3x listings)
93
- * → Insights: "property sold" total, breakdown premier_agent
94
- * (expect: Premier agents ~2x sales)
95
- *
96
- * 6. TOUR-TO-OFFER POWER USERS (everything hook — BEHAVIORS TOGETHER)
97
- * Users who did BOTH "virtual tour" AND "in-person tour" get 6x
98
- * offer_submitted events injected. dual_tour=true on offers.
99
- * → Insights: "offer submitted" total, breakdown dual_tour
100
- * (expect: dual_tour=true ~6x volume)
101
- * → Funnels: virtual tour → in-person tour → offer submitted,
102
- * filter dual_tour = true
103
- *
104
- * 7. LUXURY LISTING RELEASE (event + everything hook — TIMED RELEASE)
105
- * Day 50: luxury listings appear. After day 50, 3% of property_listed
106
- * events get listing_price set to $5M+ and luxury=true. ~3% of users
107
- * (by hash) get extra luxury property_viewed events.
108
- * → Insights: "property listed" avg listing_price, filter luxury=true
109
- * (expect: $5M+ only after day 50)
110
- * → Insights: "property viewed" total, breakdown luxury
111
- * (expect: luxury=true cluster starting day 50)
112
- *
113
- * 8. COLD-LEAD CHURN (everything hook — CHURN)
114
- * Users who browse (property viewed) but never save (property saved)
115
- * in the first 14 days lose 90% of events after day 14.
116
- * → Retention: any → any, segment cold_lead = true vs false
117
- * (expect: cold_lead=true drops to near zero after day 14)
118
- * → Insights: any event total, filter cold_lead = true
59
+ * NOTE: All cohort effects are HIDDENno flag stamping. Discoverable
60
+ * only via behavioral cohorts (count event per user, then measure
61
+ * downstream) or raw-prop breakdowns (date, agent_tier).
119
62
  *
120
- * ===================================================================
121
- * ADVANCED ANALYSIS IDEAS
122
- * ===================================================================
63
+ * -------------------------------------------------------------------
64
+ * 1. SPRING BUYING SEASON (event)
65
+ * -------------------------------------------------------------------
66
+ *
67
+ * PATTERN: Days 30-60, tour-scheduled duration_mins boosted 3x and
68
+ * offer-submitted offer_price boosted 2.5x. Mutates raw props. No flag.
69
+ *
70
+ * HOW TO FIND IT IN MIXPANEL:
71
+ *
72
+ * Report 1: Avg Offer Price by Day
73
+ * - Report type: Insights
74
+ * - Event: "offer submitted"
75
+ * - Measure: Average of "offer_price"
76
+ * - Line chart by day
77
+ * - Expected: visible bulge days 30-60 (~ 2.5x baseline)
78
+ *
79
+ * REAL-WORLD ANALOGUE: Spring buying season elevates prices and tour intent.
80
+ *
81
+ * -------------------------------------------------------------------
82
+ * 2. MORTGAGE RATE SHOCK (event + everything)
83
+ * -------------------------------------------------------------------
84
+ *
85
+ * PATTERN: Days 75-89, mortgage_rate forced to 7.5 on pre-approval
86
+ * events. In everything hook, 45% of post-day-75 offer-submitted events
87
+ * dropped. No flag.
88
+ *
89
+ * HOW TO FIND IT IN MIXPANEL:
90
+ *
91
+ * Report 1: Mortgage Rate Step Change
92
+ * - Report type: Insights
93
+ * - Event: "mortgage pre-approval"
94
+ * - Measure: Average of "mortgage_rate"
95
+ * - Line chart by day
96
+ * - Expected: clear step change from ~ 6.5 to 7.5 at day 75
97
+ *
98
+ * Report 2: Offer Volume Drop After Rate Hike
99
+ * - Report type: Insights
100
+ * - Event: "offer submitted"
101
+ * - Measure: Total
102
+ * - Line chart by day
103
+ * - Expected: ~ 45% volume drop after day 75
104
+ *
105
+ * REAL-WORLD ANALOGUE: Buyer demand is sensitive to mortgage rates.
106
+ *
107
+ * -------------------------------------------------------------------
108
+ * 3. SAVED-SEARCH RETENTION (everything)
109
+ * -------------------------------------------------------------------
110
+ *
111
+ * PATTERN: Users who create saved-search-created in first 7 days get
112
+ * extra cloned property-viewed + tour-scheduled events. Non-savers lose
113
+ * 80% of post-day-30 events. No flag — discover via cohort builder.
114
+ *
115
+ * HOW TO FIND IT IN MIXPANEL:
116
+ *
117
+ * Report 1: Retention by Saved-Search Cohort
118
+ * - Report type: Retention
119
+ * - Cohort A: users with >= 1 "saved search created" in first 7 days
120
+ * - Cohort B: rest
121
+ * - Expected: A sustains higher retention through dataset
122
+ *
123
+ * REAL-WORLD ANALOGUE: Saved searches indicate buyer intent.
124
+ *
125
+ * -------------------------------------------------------------------
126
+ * 4. PRE-APPROVED BUYER CONVERSION (everything)
127
+ * -------------------------------------------------------------------
128
+ *
129
+ * PATTERN: Users with a mortgage-pre-approval event get 4-6 extra
130
+ * cloned offer-submitted events. No flag — discover via cohort.
131
+ *
132
+ * HOW TO FIND IT IN MIXPANEL:
133
+ *
134
+ * Report 1: Offers per User by Pre-Approval
135
+ * - Report type: Insights (with cohort)
136
+ * - Cohort A: users with >= 1 "mortgage pre-approval"
137
+ * - Cohort B: rest
138
+ * - Event: "offer submitted"
139
+ * - Measure: Total per user
140
+ * - Expected: A ~ 5x B
141
+ *
142
+ * REAL-WORLD ANALOGUE: Pre-approval is the strongest buyer indicator.
143
+ *
144
+ * -------------------------------------------------------------------
145
+ * 5. TOP-TIER AGENT ADVANTAGE (everything)
146
+ * -------------------------------------------------------------------
147
+ *
148
+ * PATTERN: Premier-tier agents get 2 extra cloned listings per existing
149
+ * (3x rate) and 1 extra cloned sale per existing (2x rate). Mutates
150
+ * cloned events with raw values; reads agent_tier from profile (existing
151
+ * SCD prop). Discover via agent_tier breakdown.
152
+ *
153
+ * HOW TO FIND IT IN MIXPANEL:
154
+ *
155
+ * Report 1: Listings per Agent by Tier
156
+ * - Report type: Insights
157
+ * - Event: "property listed"
158
+ * - Measure: Total per user
159
+ * - Breakdown: user "agent_tier"
160
+ * - Expected: Premier ~ 3x Standard
123
161
  *
124
- * Cross-hook patterns:
125
- * - Spring + Rate Shock: Does the spring surge survive the rate hike?
126
- * - Saved Search + Cold Lead: Are saved-search users immune to churn?
127
- * - Pre-Approved + Dual Tour: Do pre-approved dual-tourers dominate offers?
128
- * - Premier Agent + Luxury: Do Premier agents list more luxury properties?
129
- * - Rate Shock + Pre-Approved: Do pre-approved buyers still submit after rate jump?
162
+ * REAL-WORLD ANALOGUE: Top producers list and close more.
130
163
  *
131
- * Cohort analysis:
132
- * - By user_type: Buyer vs Agent vs Both engagement
133
- * - By agent_tier: Standard vs Premier listing volume and close rate
134
- * - By property_preference: Which housing type converts best?
135
- * - By preferred_location: Urban vs Suburban vs Rural tour rates
164
+ * -------------------------------------------------------------------
165
+ * 6. TOUR-TO-OFFER POWER USERS (everything)
166
+ * -------------------------------------------------------------------
167
+ *
168
+ * PATTERN: Users with both virtual-tour AND in-person-tour events get
169
+ * 5-7 extra cloned offer-submitted events. No flag — discover via
170
+ * compound cohort builder.
171
+ *
172
+ * HOW TO FIND IT IN MIXPANEL:
173
+ *
174
+ * Report 1: Offers per User by Dual-Tour Cohort
175
+ * - Report type: Insights (with cohort)
176
+ * - Cohort A: users with both >= 1 "virtual tour" AND >= 1 "in-person tour"
177
+ * - Cohort B: rest
178
+ * - Event: "offer submitted"
179
+ * - Measure: Total per user
180
+ * - Expected: A ~ 6x B
181
+ *
182
+ * REAL-WORLD ANALOGUE: Dual-tour buyers self-qualify into serious bidders.
183
+ *
184
+ * -------------------------------------------------------------------
185
+ * 7. LUXURY LISTING RELEASE (event + everything)
186
+ * -------------------------------------------------------------------
187
+ *
188
+ * PATTERN: After day 50, 3% of property-listed events get listing_price
189
+ * set to $5M-$15M (raw mutation). ~3% of users (by id hash) get extra
190
+ * luxury cloned property-viewed events. No flag — discover via line
191
+ * chart by day on listing_price.
192
+ *
193
+ * HOW TO FIND IT IN MIXPANEL:
194
+ *
195
+ * Report 1: Listing Price Distribution Over Time
196
+ * - Report type: Insights
197
+ * - Event: "property listed"
198
+ * - Measure: Distribution of "listing_price"
199
+ * - Line chart by week
200
+ * - Expected: $5M+ outliers appear only after day 50
201
+ *
202
+ * REAL-WORLD ANALOGUE: Luxury inventory drop attracts HNW shoppers.
203
+ *
204
+ * -------------------------------------------------------------------
205
+ * 8. COLD-LEAD CHURN (everything)
206
+ * -------------------------------------------------------------------
207
+ *
208
+ * PATTERN: Users who view (property viewed) but never save (property
209
+ * saved) in the first 14 days lose 90% of events after day 14. No flag.
210
+ *
211
+ * HOW TO FIND IT IN MIXPANEL:
212
+ *
213
+ * Report 1: Cold-Lead Retention
214
+ * - Report type: Retention
215
+ * - Cohort A: users with >= 1 "property viewed" but 0 "property saved" in first 14 days
216
+ * - Cohort B: rest
217
+ * - Expected: A retention drops to near-zero after day 14
218
+ *
219
+ * REAL-WORLD ANALOGUE: Browsers who never save are window-shoppers.
220
+ *
221
+ * -------------------------------------------------------------------
222
+ * 9. PROPERTY-VIEWED MAGIC NUMBER (everything)
223
+ * -------------------------------------------------------------------
224
+ *
225
+ * PATTERN: Users in the 6-12 property-viewed sweet spot get +30% on
226
+ * offer_price for offer-submitted events. Users with 13+ views are
227
+ * tire-kickers; 35% of their offer-submitted events drop. No flag.
228
+ *
229
+ * HOW TO FIND IT IN MIXPANEL:
230
+ *
231
+ * Report 1: Avg Offer Price by View Bucket
232
+ * - Report type: Insights (with cohort)
233
+ * - Cohort A: users with 6-12 "property viewed"
234
+ * - Cohort B: users with 0-5
235
+ * - Event: "offer submitted"
236
+ * - Measure: Average of "offer_price"
237
+ * - Expected: A ~ 1.3x B
238
+ *
239
+ * Report 2: Offers per User on Heavy Viewers
240
+ * - Report type: Insights (with cohort)
241
+ * - Cohort C: users with >= 13 "property viewed"
242
+ * - Cohort A: users with 6-12
243
+ * - Event: "offer submitted"
244
+ * - Measure: Total per user
245
+ * - Expected: C ~ 35% fewer offers per user vs A
246
+ *
247
+ * REAL-WORLD ANALOGUE: Focused buyers commit; obsessive browsers
248
+ * tire-kick and never make the leap.
136
249
  *
137
250
  * ===================================================================
138
251
  * EXPECTED METRICS SUMMARY
@@ -140,21 +253,25 @@ const chance = u.initChance(SEED);
140
253
  *
141
254
  * Hook | Metric | Baseline | Effect | Ratio
142
255
  * -------------------------|------------------------|----------|---------|------
143
- * Spring Buying Season | Tour volume | 1x | 3x | 3x
144
- * Mortgage Rate Shock | Offer volume post D75 | 100% | 55% | 0.55x
145
- * Saved-Search Retention | D30+ events (non-saver)| 100% | 20% | 0.2x
146
- * Pre-Approved Conversion | Offer count | 1x | 5x | 5x
147
- * Premier Agent Advantage | Listings | 1x | 3x | 3x
148
- * Dual Tour Power Users | Offer count | 1x | 6x | 6x
149
- * Luxury Listing Release | Luxury listings | 0% | 3% | D50+
150
- * Cold-Lead Churn | D14+ events (cold lead)| 100% | 10% | 0.1x
256
+ * Spring Buying Season | Avg offer_price d30-60 | 1x | 2.5x | 2.5x
257
+ * Mortgage Rate Shock | Offer vol post-day-75 | 1x | 0.55x | -45%
258
+ * Saved-Search Retention | non-saver post-day-30 | 1x | 0.2x | -80%
259
+ * Pre-Approved Conversion | offers/user | 1x | ~ 5x | 5x
260
+ * Premier Agent Advantage | listings/user | 1x | ~ 3x | 3x
261
+ * Dual-Tour Power Users | offers/user | 1x | ~ 6x | 6x
262
+ * Luxury Listing Release | $5M+ listings | 0% | ~ 3% | d50+
263
+ * Cold-Lead Churn | non-save post-day-14 | 1x | 0.1x | -90%
264
+ * View-Count Magic Number | sweet offer_price | 1x | 1.3x | 1.3x
265
+ * View-Count Magic Number | over offers/user | 1x | 0.65x | -35%
151
266
  */
152
267
 
153
268
  /** @type {Config} */
154
269
  const config = {
155
270
  token,
156
271
  seed: SEED,
157
- numDays: num_days,
272
+ datasetStart: "2026-01-01T00:00:00Z",
273
+ datasetEnd: "2026-04-28T23:59:59Z",
274
+ // numDays: num_days,
158
275
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
159
276
  numUsers: num_users,
160
277
  hasAnonIds: false,
@@ -240,8 +357,6 @@ const config = {
240
357
  property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
241
358
  bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
242
359
  square_feet: u.weighNumRange(600, 5000, 0.5, 50),
243
- spring_season: [false],
244
- luxury: [false],
245
360
  }
246
361
  },
247
362
  {
@@ -282,7 +397,7 @@ const config = {
282
397
  properties: {
283
398
  listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
284
399
  agent_id: () => `AGT-${chance.integer({ min: 1000, max: 9999 })}`,
285
- spring_season: [false],
400
+ duration_mins: u.weighNumRange(5, 90, 0.5, 30),
286
401
  }
287
402
  },
288
403
  {
@@ -301,9 +416,6 @@ const config = {
301
416
  listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
302
417
  offer_price: u.weighNumRange(200000, 1500000, 0.4, 40),
303
418
  listing_price: u.weighNumRange(200000, 1500000, 0.4, 40),
304
- pre_approved: [false],
305
- spring_season: [false],
306
- dual_tour: [false],
307
419
  }
308
420
  },
309
421
  {
@@ -330,8 +442,6 @@ const config = {
330
442
  property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
331
443
  bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
332
444
  listing_status: ["active", "pending", "coming soon"],
333
- luxury: [false],
334
- premier_agent: [false],
335
445
  }
336
446
  },
337
447
  {
@@ -340,7 +450,6 @@ const config = {
340
450
  properties: {
341
451
  sale_price: u.weighNumRange(200000, 1500000, 0.4, 40),
342
452
  days_on_market: u.weighNumRange(5, 180, 0.4, 30),
343
- premier_agent: [false],
344
453
  }
345
454
  },
346
455
  {
@@ -372,8 +481,6 @@ const config = {
372
481
  user_type: ["Buyer", "Buyer", "Buyer", "Agent", "Both"],
373
482
  preferred_location: ["Urban", "Suburban", "Rural"],
374
483
  property_preference: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
375
- has_saved_search: [false],
376
- cold_lead: [false],
377
484
  },
378
485
 
379
486
  userProps: {
@@ -384,8 +491,6 @@ const config = {
384
491
  agent_tier: ["Standard", "Standard", "Standard", "Premier"],
385
492
  total_properties_viewed: u.weighNumRange(0, 100, 0.5, 30),
386
493
  pre_approval_status: ["none"],
387
- has_saved_search: [false],
388
- cold_lead: [false],
389
494
  },
390
495
 
391
496
  groupKeys: [
@@ -403,88 +508,34 @@ const config = {
403
508
 
404
509
  lookupTables: [],
405
510
 
406
- /**
407
- * ARCHITECTED ANALYTICS HOOKS
408
- *
409
- * This hook function creates 8 deliberate patterns in the data:
410
- *
411
- * 1. SPRING BUYING SEASON: Days 30-60, tours get 3x duration_mins, offers get 2.5x offer_price, spring_season=true
412
- * 2. MORTGAGE RATE SHOCK: Day 75, rate jumps to 7.5; 45% of post-day-75 offers removed
413
- * 3. SAVED-SEARCH RETENTION: First-7-day savers get extra engagement; non-savers lose 80% after day 30
414
- * 4. PRE-APPROVED BUYER CONVERSION: Users with pre-approval get 5x offer events, pre_approved=true
415
- * 5. TOP-TIER AGENT ADVANTAGE: Premier agents get 3x listings, 2x sales, premier_agent=true
416
- * 6. TOUR-TO-OFFER POWER USERS: Users with both virtual + in-person tours get 6x offers, dual_tour=true
417
- * 7. LUXURY LISTING RELEASE: Day 50+, 3% of listings become $5M+ luxury; ~3% of users browse luxury
418
- * 8. COLD-LEAD CHURN: Browsers who never save in first 14 days lose 90% of events after day 14
419
- */
420
511
  hook: function (record, type, meta) {
421
- const NOW = dayjs.utc();
422
- const DATASET_START = NOW.subtract(num_days, "days");
423
-
424
- // ===============================================================
425
- // Hook #1: SPRING BUYING SEASON (event)
426
- // Days 30-60: spring market heats up. Scheduled tours get
427
- // duration_mins 3x (more serious buyers). Offers get offer_price
428
- // 2.5x (bidding wars). spring_season=true flag set on both.
429
- // ===============================================================
430
- if (type === "event") {
431
- const EVENT_TIME = dayjs.utc(record.time);
432
- const dayInDataset = EVENT_TIME.diff(DATASET_START, "day");
433
-
434
- const isSpring = dayInDataset >= 30 && dayInDataset <= 60;
435
-
436
- if (record.event === "tour scheduled") {
437
- if (isSpring) {
438
- record.spring_season = true;
439
- } else {
440
- record.spring_season = false;
441
- }
442
- }
443
-
444
- if (record.event === "offer submitted") {
445
- if (isSpring) {
446
- record.offer_price = Math.floor((record.offer_price || 400000) * 2.5);
447
- record.spring_season = true;
448
- } else {
449
- record.spring_season = false;
450
- }
451
- }
452
-
453
- if (record.event === "property viewed" && isSpring) {
454
- record.spring_season = true;
455
- }
456
-
457
- // ===============================================================
458
- // Hook #2 (event part): MORTGAGE RATE SHOCK
459
- // Day 75-89: mortgage rates forced to 7.5 on pre-approval events
460
- // (up from baseline ~6.5).
461
- // ===============================================================
462
- if (record.event === "mortgage pre-approval") {
463
- if (dayInDataset >= 75 && dayInDataset < 89) {
464
- record.mortgage_rate = 7.5;
465
- }
466
- }
467
-
468
- // ===============================================================
469
- // Hook #7 (event part): LUXURY LISTING RELEASE
470
- // After day 50, 3% of property_listed events become luxury
471
- // ($5M-$15M) with luxury=true. Before day 50, no luxury.
472
- // ===============================================================
473
- if (record.event === "property listed") {
474
- if (dayInDataset >= 50 && chance.bool({ likelihood: 3 })) {
475
- record.listing_price = chance.integer({ min: 5000000, max: 15000000 });
476
- record.luxury = true;
477
- record.premier_agent = false;
478
- } else {
479
- record.luxury = false;
512
+ // HOOK 10 (T2C): TOUR FUNNEL TIME-TO-CONVERT (funnel-post)
513
+ // Premier agents move users through Tour funnel 1.4x faster
514
+ // (factor 0.71); Standard agents 1.3x slower (factor 1.3).
515
+ if (type === "funnel-post") {
516
+ const segment = meta?.profile?.agent_tier;
517
+ if (Array.isArray(record) && record.length > 1) {
518
+ const factor = (
519
+ segment === "Premier" ? 0.71 :
520
+ segment === "Standard" ? 1.3 :
521
+ 1.0
522
+ );
523
+ if (factor !== 1.0) {
524
+ for (let i = 1; i < record.length; i++) {
525
+ const prev = dayjs(record[i - 1].time);
526
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
527
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
528
+ }
480
529
  }
481
530
  }
482
531
  }
483
532
 
484
- // ===============================================================
485
- // EVERYTHING HOOK Complex behavioral patterns
486
- // ===============================================================
533
+ // HOOKS 1, 2, 7 (time-window event-level effects) moved to everything hook —
534
+ // event hook fires before bunchIntoSessions reshuffles timestamps, so day-window
535
+ // tags leak across boundaries.
536
+
487
537
  if (type === "everything") {
538
+ const datasetStart = dayjs.unix(meta.datasetStart);
488
539
  const userEvents = record;
489
540
  const profile = meta.profile;
490
541
 
@@ -493,276 +544,232 @@ const config = {
493
544
  e.user_type = profile.user_type;
494
545
  e.preferred_location = profile.preferred_location;
495
546
  e.property_preference = profile.property_preference;
496
- e.has_saved_search = profile.has_saved_search;
497
- e.cold_lead = profile.cold_lead;
547
+ });
548
+
549
+ // HOOK 1: SPRING BUYING SEASON — d30-60 tour duration 3x, offer_price 2.5x
550
+ // HOOK 2: MORTGAGE RATE SHOCK — d75-89 mortgage_rate pinned 7.5
551
+ // HOOK 7: LUXURY LISTING RELEASE — post-d50, 3% of listings priced $5M-$15M
552
+ const springStart = datasetStart.add(30, "days");
553
+ const springEnd = datasetStart.add(60, "days");
554
+ const shockStart = datasetStart.add(75, "days");
555
+ const shockEnd = datasetStart.add(89, "days");
556
+ const luxuryStart = datasetStart.add(50, "days");
557
+ userEvents.forEach(e => {
558
+ const t = dayjs.utc(e.time);
559
+ const inSpring = (t.isAfter(springStart) || t.isSame(springStart)) && t.isBefore(springEnd);
560
+ if (inSpring && e.event === "tour scheduled" && typeof e.duration_mins === "number") {
561
+ e.duration_mins = Math.round(e.duration_mins * 3);
562
+ }
563
+ if (inSpring && e.event === "offer submitted") {
564
+ e.offer_price = Math.floor((e.offer_price || 400000) * 2.5);
565
+ }
566
+ if (e.event === "mortgage pre-approval") {
567
+ if ((t.isAfter(shockStart) || t.isSame(shockStart)) && t.isBefore(shockEnd)) {
568
+ e.mortgage_rate = 7.5;
569
+ }
570
+ }
571
+ if (e.event === "property listed" && t.isAfter(luxuryStart) && chance.bool({ likelihood: 3 })) {
572
+ e.listing_price = chance.integer({ min: 5000000, max: 15000000 });
573
+ }
498
574
  });
499
575
 
500
576
  const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
501
577
 
502
- // -----------------------------------------------------------
503
- // Hook #2 (everything part): MORTGAGE RATE SHOCK
504
- // After day 75, 45% of offer_submitted events are removed
505
- // (buyer pullback due to higher rates).
506
- // -----------------------------------------------------------
507
- const day75 = DATASET_START.add(75, "days");
578
+ // HOOK 2 (cont): MORTGAGE RATE SHOCK — drop 45% of post-day-75
579
+ // offer-submitted events. No flag.
580
+ const day75 = datasetStart.add(75, "days");
508
581
  for (let i = userEvents.length - 1; i >= 0; i--) {
509
582
  const evt = userEvents[i];
510
- if (evt.event === "offer submitted" && dayjs(evt.time).isAfter(day75)) {
511
- if (chance.bool({ likelihood: 45 })) {
512
- userEvents.splice(i, 1);
513
- }
583
+ if (evt.event === "offer submitted" && dayjs(evt.time).isAfter(day75) && chance.bool({ likelihood: 45 })) {
584
+ userEvents.splice(i, 1);
514
585
  }
515
586
  }
516
587
 
517
- // -----------------------------------------------------------
518
- // Hook #3: SAVED-SEARCH RETENTION
519
- // Users who create a "saved search created" within first 7
520
- // days get extra property_viewed and tour_scheduled events.
521
- // Non-savers lose 80% of events after day 30.
522
- // -----------------------------------------------------------
588
+ // HOOK 3: SAVED-SEARCH RETENTION — early savers (saved-search-created
589
+ // in first 7 days) get extra cloned view + tour events. Non-savers
590
+ // lose 80% of events after day 30. No flag.
523
591
  const day7 = firstEventTime ? firstEventTime.add(7, "days") : null;
524
- const day30 = DATASET_START.add(30, "days");
525
-
526
- let hasSavedSearch = false;
527
- if (day7) {
528
- hasSavedSearch = userEvents.some(e =>
529
- e.event === "saved search created" &&
530
- dayjs(e.time).isBefore(day7)
531
- );
532
- }
592
+ const day30 = datasetStart.add(30, "days");
593
+ const hasSavedSearch = day7 ? userEvents.some(e =>
594
+ e.event === "saved search created" && dayjs(e.time).isBefore(day7)
595
+ ) : false;
533
596
 
534
597
  if (hasSavedSearch) {
535
- // Mark the user and inject extra engagement events
536
- profile.has_saved_search = true;
537
- userEvents.forEach(e => { e.has_saved_search = true; });
538
-
539
- // Inject extra property_viewed and tour_scheduled events
540
598
  const viewTemplate = userEvents.find(e => e.event === "property viewed");
541
599
  const tourTemplate = userEvents.find(e => e.event === "tour scheduled");
542
-
543
600
  if (viewTemplate) {
544
- const extraCount = chance.integer({ min: 3, max: 8 });
545
- for (let i = 0; i < extraCount; i++) {
601
+ const extras = chance.integer({ min: 3, max: 8 });
602
+ for (let i = 0; i < extras; i++) {
546
603
  const offset = chance.integer({ min: 10, max: num_days - 10 });
547
604
  userEvents.push({
548
605
  ...viewTemplate,
549
- time: DATASET_START.add(offset, "days").add(chance.integer({ min: 0, max: 23 }), "hours").toISOString(),
606
+ time: datasetStart.add(offset, "days").add(chance.integer({ min: 0, max: 23 }), "hours").toISOString(),
550
607
  user_id: viewTemplate.user_id,
551
608
  listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
552
609
  listing_price: chance.integer({ min: 200000, max: 1200000 }),
553
- has_saved_search: true,
554
610
  });
555
611
  }
556
612
  }
557
613
  if (tourTemplate) {
558
- const extraTours = chance.integer({ min: 1, max: 3 });
559
- for (let i = 0; i < extraTours; i++) {
614
+ const extras = chance.integer({ min: 1, max: 3 });
615
+ for (let i = 0; i < extras; i++) {
560
616
  const offset = chance.integer({ min: 15, max: num_days - 10 });
561
617
  userEvents.push({
562
618
  ...tourTemplate,
563
- time: DATASET_START.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
619
+ time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
564
620
  user_id: tourTemplate.user_id,
565
621
  listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
566
- has_saved_search: true,
567
622
  });
568
623
  }
569
624
  }
570
625
  } else {
571
- // Non-savers: remove 80% of events after day 30
572
626
  for (let i = userEvents.length - 1; i >= 0; i--) {
573
627
  const evt = userEvents[i];
574
- if (dayjs(evt.time).isAfter(day30)) {
575
- if (chance.bool({ likelihood: 80 })) {
576
- userEvents.splice(i, 1);
577
- }
628
+ if (dayjs(evt.time).isAfter(day30) && chance.bool({ likelihood: 80 })) {
629
+ userEvents.splice(i, 1);
578
630
  }
579
631
  }
580
632
  }
581
633
 
582
- // -----------------------------------------------------------
583
- // Hook #4: PRE-APPROVED BUYER CONVERSION
584
- // Users with a "mortgage pre-approval" event get 5x more
585
- // offer_submitted events injected. pre_approved=true.
586
- // -----------------------------------------------------------
634
+ // HOOK 4: PRE-APPROVED BUYER CONVERSION — clone 4-6 extra
635
+ // offer-submitted events for users with a mortgage pre-approval
636
+ // event. No flag.
587
637
  const hasPreApproval = userEvents.some(e => e.event === "mortgage pre-approval");
588
-
589
638
  if (hasPreApproval) {
590
639
  profile.pre_approval_status = "approved";
591
640
  const offerTemplate = userEvents.find(e => e.event === "offer submitted");
592
641
  if (offerTemplate) {
593
- const extraOffers = chance.integer({ min: 4, max: 6 });
594
- for (let i = 0; i < extraOffers; i++) {
642
+ const extras = chance.integer({ min: 4, max: 6 });
643
+ for (let i = 0; i < extras; i++) {
595
644
  const offset = chance.integer({ min: 20, max: num_days - 5 });
596
645
  userEvents.push({
597
646
  ...offerTemplate,
598
- time: DATASET_START.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
647
+ time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
599
648
  user_id: offerTemplate.user_id,
600
649
  listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
601
650
  offer_price: chance.integer({ min: 250000, max: 1200000 }),
602
651
  listing_price: chance.integer({ min: 250000, max: 1200000 }),
603
- pre_approved: true,
604
- spring_season: false,
605
- dual_tour: false,
606
652
  });
607
653
  }
608
654
  }
609
-
610
- // Also tag existing offers
611
- userEvents.forEach(e => {
612
- if (e.event === "offer submitted") {
613
- e.pre_approved = true;
614
- }
615
- });
616
655
  }
617
656
 
618
- // -----------------------------------------------------------
619
- // Hook #5: TOP-TIER AGENT ADVANTAGE
620
- // Premier agents get 3x property_listed and 2x property_sold
621
- // events. premier_agent=true on all their listing/sale events.
622
- // -----------------------------------------------------------
657
+ // HOOK 5: TOP-TIER AGENT ADVANTAGE — Premier agents get 2 extra
658
+ // cloned listings per existing (3x rate) and 1 extra clone per
659
+ // sale (2x rate). Reads agent_tier from profile. No flag.
623
660
  if (profile.agent_tier === "Premier") {
624
661
  const listTemplate = userEvents.find(e => e.event === "property listed");
625
662
  const soldTemplate = userEvents.find(e => e.event === "property sold");
626
663
 
627
- // Tag existing events
628
- userEvents.forEach(e => {
629
- if (e.event === "property listed" || e.event === "property sold") {
630
- e.premier_agent = true;
631
- }
632
- });
633
-
634
- // Inject extra listings (3x = 2 more copies per existing)
635
664
  if (listTemplate) {
636
665
  const existingListings = userEvents.filter(e => e.event === "property listed").length;
637
- const extraListings = existingListings * 2;
638
- for (let i = 0; i < extraListings; i++) {
666
+ const extras = existingListings * 2;
667
+ for (let i = 0; i < extras; i++) {
639
668
  const offset = chance.integer({ min: 5, max: num_days - 5 });
640
669
  userEvents.push({
641
670
  ...listTemplate,
642
- time: DATASET_START.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
671
+ time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
643
672
  user_id: listTemplate.user_id,
644
673
  listing_price: chance.integer({ min: 300000, max: 1500000 }),
645
674
  listing_status: chance.pickone(["active", "pending", "coming soon"]),
646
- luxury: false,
647
- premier_agent: true,
648
675
  });
649
676
  }
650
677
  }
651
-
652
- // Inject extra sales (2x = 1 more copy per existing)
653
678
  if (soldTemplate) {
654
679
  const existingSales = userEvents.filter(e => e.event === "property sold").length;
655
680
  for (let i = 0; i < existingSales; i++) {
656
681
  const offset = chance.integer({ min: 10, max: num_days - 5 });
657
682
  userEvents.push({
658
683
  ...soldTemplate,
659
- time: DATASET_START.add(offset, "days").add(chance.integer({ min: 9, max: 17 }), "hours").toISOString(),
684
+ time: datasetStart.add(offset, "days").add(chance.integer({ min: 9, max: 17 }), "hours").toISOString(),
660
685
  user_id: soldTemplate.user_id,
661
686
  sale_price: chance.integer({ min: 300000, max: 1500000 }),
662
687
  days_on_market: chance.integer({ min: 5, max: 90 }),
663
- premier_agent: true,
664
688
  });
665
689
  }
666
690
  }
667
691
  }
668
692
 
669
- // -----------------------------------------------------------
670
- // Hook #6: TOUR-TO-OFFER POWER USERS
671
- // Users who did BOTH "virtual tour" AND "in-person tour"
672
- // get 6x offer_submitted events. dual_tour=true on offers.
673
- // -----------------------------------------------------------
693
+ // HOOK 6: TOUR-TO-OFFER POWER USERS — users with both virtual
694
+ // tour AND in-person tour get 5-7 extra cloned offers. No flag.
674
695
  const hasVirtualTour = userEvents.some(e => e.event === "virtual tour");
675
696
  const hasInPersonTour = userEvents.some(e => e.event === "in-person tour");
676
- const isDualTourer = hasVirtualTour && hasInPersonTour;
677
-
678
- if (isDualTourer) {
697
+ if (hasVirtualTour && hasInPersonTour) {
679
698
  const offerTemplate = userEvents.find(e => e.event === "offer submitted");
680
699
  if (offerTemplate) {
681
- const extraOffers = chance.integer({ min: 5, max: 7 });
682
- for (let i = 0; i < extraOffers; i++) {
700
+ const extras = chance.integer({ min: 5, max: 7 });
701
+ for (let i = 0; i < extras; i++) {
683
702
  const offset = chance.integer({ min: 15, max: num_days - 5 });
684
703
  userEvents.push({
685
704
  ...offerTemplate,
686
- time: DATASET_START.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
705
+ time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
687
706
  user_id: offerTemplate.user_id,
688
707
  listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
689
708
  offer_price: chance.integer({ min: 300000, max: 1500000 }),
690
709
  listing_price: chance.integer({ min: 300000, max: 1500000 }),
691
- dual_tour: true,
692
- pre_approved: false,
693
- spring_season: false,
694
710
  });
695
711
  }
696
712
  }
697
-
698
- // Tag existing offers
699
- userEvents.forEach(e => {
700
- if (e.event === "offer submitted") {
701
- e.dual_tour = true;
702
- }
703
- });
704
713
  }
705
714
 
706
- // -----------------------------------------------------------
707
- // Hook #7 (everything part): LUXURY LISTING RELEASE
708
- // ~3% of users (by hash) get extra luxury property_viewed
709
- // events after day 50. Deterministic via character code.
710
- // -----------------------------------------------------------
711
- const day50 = DATASET_START.add(50, "days");
715
+ // HOOK 7 (cont): LUXURY LISTING RELEASE — ~3% of users (by hash)
716
+ // get 3-6 extra luxury property-viewed events after day 50. No flag.
712
717
  if (userEvents.length > 0) {
713
718
  const userId = userEvents[0].user_id || "";
714
719
  const isLuxuryBrowser = typeof userId === "string" && userId.length > 0 && userId.charCodeAt(0) % 33 === 0;
715
-
716
720
  if (isLuxuryBrowser) {
717
721
  const viewTemplate = userEvents.find(e => e.event === "property viewed");
718
722
  if (viewTemplate) {
719
- const extraViews = chance.integer({ min: 3, max: 6 });
720
- for (let i = 0; i < extraViews; i++) {
723
+ const extras = chance.integer({ min: 3, max: 6 });
724
+ for (let i = 0; i < extras; i++) {
721
725
  const offset = chance.integer({ min: 50, max: num_days - 5 });
722
726
  userEvents.push({
723
727
  ...viewTemplate,
724
- time: DATASET_START.add(offset, "days").add(chance.integer({ min: 9, max: 21 }), "hours").toISOString(),
728
+ time: datasetStart.add(offset, "days").add(chance.integer({ min: 9, max: 21 }), "hours").toISOString(),
725
729
  user_id: viewTemplate.user_id,
726
730
  listing_id: `LST-${chance.integer({ min: 90000, max: 99999 })}`,
727
731
  listing_price: chance.integer({ min: 5000000, max: 15000000 }),
728
732
  property_type: chance.pickone(["Single Family", "Condo"]),
729
733
  bedrooms: chance.integer({ min: 4, max: 8 }),
730
734
  square_feet: chance.integer({ min: 4000, max: 15000 }),
731
- luxury: true,
732
- spring_season: false,
733
735
  });
734
736
  }
735
737
  }
736
738
  }
737
739
  }
738
740
 
739
- // -----------------------------------------------------------
740
- // Hook #8: COLD-LEAD CHURN
741
- // Users who browse (property viewed) but never save (property
742
- // saved) in the first 14 days lose 90% of events after day 14.
743
- // -----------------------------------------------------------
741
+ // HOOK 8: COLD-LEAD CHURN — users who view but never save in
742
+ // first 14 days lose 90% of post-day-14 events. No flag.
744
743
  const day14 = firstEventTime ? firstEventTime.add(14, "days") : null;
745
-
746
744
  if (day14) {
747
745
  const earlyEvents = userEvents.filter(e => dayjs(e.time).isBefore(day14));
748
746
  const hasView = earlyEvents.some(e => e.event === "property viewed");
749
747
  const hasSave = earlyEvents.some(e => e.event === "property saved");
750
-
751
748
  if (hasView && !hasSave) {
752
- // Cold lead: remove 90% of events after day 14
753
- profile.cold_lead = true;
754
749
  for (let i = userEvents.length - 1; i >= 0; i--) {
755
750
  const evt = userEvents[i];
756
- if (dayjs(evt.time).isAfter(day14)) {
757
- if (chance.bool({ likelihood: 90 })) {
758
- userEvents.splice(i, 1);
759
- } else {
760
- evt.cold_lead = true;
761
- }
751
+ if (dayjs(evt.time).isAfter(day14) && chance.bool({ likelihood: 90 })) {
752
+ userEvents.splice(i, 1);
762
753
  }
763
754
  }
764
- // Tag surviving events
765
- userEvents.forEach(e => { e.cold_lead = true; });
755
+ }
756
+ }
757
+
758
+ // HOOK 9: PROPERTY-VIEWED MAGIC NUMBER (no flags)
759
+ // Sweet 6-12 views → +30% offer_price on offer submitted events.
760
+ // Over 13+ → drop 35% of offer submitted events (tire-kickers).
761
+ const viewCount = userEvents.filter(e => e.event === "property viewed").length;
762
+ if (viewCount >= 6 && viewCount <= 12) {
763
+ userEvents.forEach(e => {
764
+ if (e.event === "offer submitted" && typeof e.offer_price === "number") {
765
+ e.offer_price = Math.round(e.offer_price * 1.3);
766
+ }
767
+ });
768
+ } else if (viewCount >= 13) {
769
+ for (let i = userEvents.length - 1; i >= 0; i--) {
770
+ if (userEvents[i].event === "offer submitted" && chance.bool({ likelihood: 35 })) {
771
+ userEvents.splice(i, 1);
772
+ }
766
773
  }
767
774
  }
768
775
  }