@ak--47/dungeon-master 1.2.2 → 1.3.0

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 (52) 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 -2
  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 +2 -2
  16. package/dungeons/technical/simplest.js +2 -2
  17. package/dungeons/technical/text-generation.js +2 -2
  18. package/dungeons/vertical/ai-platform-schema.json +617 -0
  19. package/dungeons/vertical/ai-platform.js +799 -0
  20. package/dungeons/vertical/community.js +40 -26
  21. package/dungeons/vertical/crypto-schema.json +546 -0
  22. package/dungeons/vertical/crypto.js +721 -0
  23. package/dungeons/vertical/dating-schema.json +401 -0
  24. package/dungeons/vertical/dating.js +798 -0
  25. package/dungeons/vertical/devtools.js +13 -9
  26. package/dungeons/vertical/ecommerce.js +2 -3
  27. package/dungeons/vertical/education.js +4 -5
  28. package/dungeons/vertical/fintech.js +32 -29
  29. package/dungeons/vertical/fitness.js +2 -3
  30. package/dungeons/vertical/food-delivery.js +37 -43
  31. package/dungeons/vertical/gaming-schema.json +2495 -230
  32. package/dungeons/vertical/gaming.js +771 -388
  33. package/dungeons/vertical/healthcare.js +2 -3
  34. package/dungeons/vertical/insurance-application.js +2 -3
  35. package/dungeons/vertical/logistics.js +20 -14
  36. package/dungeons/vertical/marketplace.js +22 -14
  37. package/dungeons/vertical/media.js +39 -30
  38. package/dungeons/vertical/real-estate-schema.json +527 -0
  39. package/dungeons/vertical/real-estate.js +774 -0
  40. package/dungeons/vertical/sass.js +2 -3
  41. package/dungeons/vertical/social.js +15 -13
  42. package/dungeons/vertical/travel.js +59 -26
  43. package/lib/core/config-validator.js +71 -15
  44. package/lib/core/storage.js +14 -4
  45. package/lib/orchestrators/user-loop.js +39 -5
  46. package/lib/templates/macro-presets.js +111 -0
  47. package/lib/templates/soup-presets.js +19 -36
  48. package/package.json +8 -2
  49. package/types.d.ts +219 -42
  50. package/dungeons/user/.gitkeep +0 -0
  51. package/dungeons/vertical/rpg-schema.json +0 -2491
  52. package/dungeons/vertical/rpg.js +0 -976
@@ -0,0 +1,774 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "homenest";
3
+ const num_days = 150;
4
+ const num_users = 6_000;
5
+ const avg_events_per_user_per_day = 0.53;
6
+ let token = "your-mixpanel-token";
7
+
8
+ // ── env overrides ──
9
+ if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
+
11
+ import dayjs from "dayjs";
12
+ import utc from "dayjs/plugin/utc.js";
13
+ import "dotenv/config";
14
+ import * as u from "../../lib/utils/utils.js";
15
+ import * as v from "ak-tools";
16
+
17
+ dayjs.extend(utc);
18
+ const chance = u.initChance(SEED);
19
+
20
+ /** @typedef {import("../../types").Dungeon} Config */
21
+
22
+ /**
23
+ * ===================================================================
24
+ * DATASET OVERVIEW
25
+ * ===================================================================
26
+ *
27
+ * HomeNest — a property listings and agent CRM platform
28
+ * (Zillow meets Compass). Buyers search listings, save properties,
29
+ * schedule tours, get pre-approved for mortgages, and submit offers.
30
+ * Agents list properties, manage clients, and close deals.
31
+ *
32
+ * Scale: 6,000 users · 480K events · 150 days · 17 event types
33
+ * Groups: 200 brokerages
34
+ * User types: Buyer (60%), Agent (20%), Both (20%)
35
+ * Agent tiers: Standard (75%) / Premier (25%)
36
+ *
37
+ * Core loops:
38
+ * Buyer: account created → property search → property viewed →
39
+ * property saved → tour scheduled → offer submitted →
40
+ * offer accepted / offer rejected
41
+ *
42
+ * Agent: account created → property listed → open house attended →
43
+ * agent contacted → property sold
44
+ *
45
+ * Funnels:
46
+ * - Buyer journey: account created → property search → property viewed (80%)
47
+ * - Tour funnel: property viewed → tour scheduled → offer submitted (40%)
48
+ * - Seller funnel: property listed → open house attended → property sold (35%)
49
+ *
50
+ * ===================================================================
51
+ * ANALYTICS HOOKS (8 architected patterns)
52
+ * ===================================================================
53
+ *
54
+ * 1. SPRING BUYING SEASON (event hook — TIME-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
119
+ *
120
+ * ===================================================================
121
+ * ADVANCED ANALYSIS IDEAS
122
+ * ===================================================================
123
+ *
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?
130
+ *
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
136
+ *
137
+ * ===================================================================
138
+ * EXPECTED METRICS SUMMARY
139
+ * ===================================================================
140
+ *
141
+ * Hook | Metric | Baseline | Effect | Ratio
142
+ * -------------------------|------------------------|----------|---------|------
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
151
+ */
152
+
153
+ /** @type {Config} */
154
+ const config = {
155
+ token,
156
+ seed: SEED,
157
+ numDays: num_days,
158
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
159
+ numUsers: num_users,
160
+ hasAnonIds: false,
161
+ hasSessionIds: true,
162
+ format: "json",
163
+ gzip: true,
164
+ alsoInferFunnels: false,
165
+ hasLocation: true,
166
+ hasAndroidDevices: true,
167
+ hasIOSDevices: true,
168
+ hasDesktopDevices: true,
169
+ hasBrowser: true,
170
+ hasCampaigns: false,
171
+ isAnonymous: false,
172
+ hasAdSpend: false,
173
+
174
+ hasAvatar: true,
175
+
176
+ concurrency: 1,
177
+ writeToDisk: false,
178
+
179
+ soup: "growth",
180
+
181
+ scdProps: {
182
+ agent_tier: {
183
+ values: ["Standard", "Standard", "Standard", "Premier"],
184
+ frequency: "month",
185
+ timing: "fuzzy",
186
+ max: 4
187
+ }
188
+ },
189
+
190
+ funnels: [
191
+ {
192
+ sequence: ["account created", "property search", "property viewed"],
193
+ isFirstFunnel: true,
194
+ conversionRate: 80,
195
+ timeToConvert: 0.5,
196
+ name: "Buyer Journey"
197
+ },
198
+ {
199
+ sequence: ["property viewed", "tour scheduled", "offer submitted"],
200
+ conversionRate: 40,
201
+ timeToConvert: 24,
202
+ weight: 5,
203
+ name: "Tour Funnel"
204
+ },
205
+ {
206
+ sequence: ["property listed", "open house attended", "property sold"],
207
+ conversionRate: 35,
208
+ timeToConvert: 48,
209
+ weight: 3,
210
+ name: "Seller Funnel"
211
+ }
212
+ ],
213
+
214
+ events: [
215
+ {
216
+ event: "account created",
217
+ weight: 1,
218
+ isFirstEvent: true,
219
+ properties: {
220
+ signup_method: ["email", "google", "apple", "facebook"],
221
+ }
222
+ },
223
+ {
224
+ event: "property search",
225
+ weight: 10,
226
+ properties: {
227
+ location: ["Manhattan", "Brooklyn", "Austin", "Miami", "San Francisco", "Denver", "Seattle", "Chicago", "Nashville", "Portland"],
228
+ price_range_min: u.weighNumRange(100000, 500000, 0.5, 40),
229
+ price_range_max: u.weighNumRange(300000, 2000000, 0.5, 40),
230
+ bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
231
+ property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
232
+ }
233
+ },
234
+ {
235
+ event: "property viewed",
236
+ weight: 12,
237
+ properties: {
238
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
239
+ listing_price: u.weighNumRange(150000, 1500000, 0.4, 60),
240
+ property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
241
+ bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
242
+ square_feet: u.weighNumRange(600, 5000, 0.5, 50),
243
+ spring_season: [false],
244
+ luxury: [false],
245
+ }
246
+ },
247
+ {
248
+ event: "property saved",
249
+ weight: 5,
250
+ properties: {
251
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
252
+ listing_price: u.weighNumRange(150000, 1500000, 0.4, 40),
253
+ }
254
+ },
255
+ {
256
+ event: "saved search created",
257
+ weight: 3,
258
+ properties: {
259
+ location: ["Manhattan", "Brooklyn", "Austin", "Miami", "San Francisco", "Denver", "Seattle", "Chicago", "Nashville", "Portland"],
260
+ criteria_count: u.weighNumRange(2, 8, 0.8, 10),
261
+ }
262
+ },
263
+ {
264
+ event: "virtual tour",
265
+ weight: 4,
266
+ properties: {
267
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
268
+ duration_mins: u.weighNumRange(5, 45, 0.5, 30),
269
+ }
270
+ },
271
+ {
272
+ event: "in-person tour",
273
+ weight: 3,
274
+ properties: {
275
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
276
+ agent_id: () => `AGT-${chance.integer({ min: 1000, max: 9999 })}`,
277
+ }
278
+ },
279
+ {
280
+ event: "tour scheduled",
281
+ weight: 4,
282
+ properties: {
283
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
284
+ agent_id: () => `AGT-${chance.integer({ min: 1000, max: 9999 })}`,
285
+ spring_season: [false],
286
+ }
287
+ },
288
+ {
289
+ event: "mortgage pre-approval",
290
+ weight: 2,
291
+ properties: {
292
+ lender: ["Chase", "Wells Fargo", "Bank of America", "Rocket Mortgage", "United Wholesale", "loanDepot"],
293
+ approved_amount: u.weighNumRange(200000, 1500000, 0.4, 40),
294
+ mortgage_rate: u.weighNumRange(5.5, 7.5, 0.8, 20),
295
+ }
296
+ },
297
+ {
298
+ event: "offer submitted",
299
+ weight: 2,
300
+ properties: {
301
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
302
+ offer_price: u.weighNumRange(200000, 1500000, 0.4, 40),
303
+ listing_price: u.weighNumRange(200000, 1500000, 0.4, 40),
304
+ pre_approved: [false],
305
+ spring_season: [false],
306
+ dual_tour: [false],
307
+ }
308
+ },
309
+ {
310
+ event: "offer accepted",
311
+ weight: 1,
312
+ properties: {
313
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
314
+ final_price: u.weighNumRange(200000, 1500000, 0.4, 40),
315
+ }
316
+ },
317
+ {
318
+ event: "offer rejected",
319
+ weight: 1,
320
+ properties: {
321
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
322
+ reason: ["price too low", "competing offer", "seller changed mind", "inspection issues", "financing fell through"],
323
+ }
324
+ },
325
+ {
326
+ event: "property listed",
327
+ weight: 3,
328
+ properties: {
329
+ listing_price: u.weighNumRange(200000, 1500000, 0.4, 50),
330
+ property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
331
+ bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
332
+ listing_status: ["active", "pending", "coming soon"],
333
+ luxury: [false],
334
+ premier_agent: [false],
335
+ }
336
+ },
337
+ {
338
+ event: "property sold",
339
+ weight: 1,
340
+ properties: {
341
+ sale_price: u.weighNumRange(200000, 1500000, 0.4, 40),
342
+ days_on_market: u.weighNumRange(5, 180, 0.4, 30),
343
+ premier_agent: [false],
344
+ }
345
+ },
346
+ {
347
+ event: "agent contacted",
348
+ weight: 4,
349
+ properties: {
350
+ contact_method: ["phone", "email", "in-app message", "text"],
351
+ }
352
+ },
353
+ {
354
+ event: "open house attended",
355
+ weight: 2,
356
+ properties: {
357
+ listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
358
+ attendees: u.weighNumRange(2, 30, 0.5, 15),
359
+ }
360
+ },
361
+ {
362
+ event: "review submitted",
363
+ weight: 2,
364
+ properties: {
365
+ rating: [1, 2, 3, 3, 4, 4, 4, 5, 5, 5],
366
+ review_type: ["agent", "property", "platform"],
367
+ }
368
+ }
369
+ ],
370
+
371
+ superProps: {
372
+ user_type: ["Buyer", "Buyer", "Buyer", "Agent", "Both"],
373
+ preferred_location: ["Urban", "Suburban", "Rural"],
374
+ property_preference: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
375
+ has_saved_search: [false],
376
+ cold_lead: [false],
377
+ },
378
+
379
+ userProps: {
380
+ user_type: ["Buyer", "Buyer", "Buyer", "Agent", "Both"],
381
+ preferred_location: ["Urban", "Suburban", "Rural"],
382
+ property_preference: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
383
+ budget_max: u.weighNumRange(200000, 2000000, 0.4, 50),
384
+ agent_tier: ["Standard", "Standard", "Standard", "Premier"],
385
+ total_properties_viewed: u.weighNumRange(0, 100, 0.5, 30),
386
+ pre_approval_status: ["none"],
387
+ has_saved_search: [false],
388
+ cold_lead: [false],
389
+ },
390
+
391
+ groupKeys: [
392
+ ["brokerage_id", 200, ["property listed", "property sold", "agent contacted", "open house attended"]],
393
+ ],
394
+
395
+ groupProps: {
396
+ brokerage_id: {
397
+ brokerage_name: ["Compass", "RE/MAX", "Keller Williams", "Coldwell Banker", "Century 21", "Sotheby's", "eXp Realty", "Redfin"],
398
+ agent_count: u.weighNumRange(5, 200, 0.4, 30),
399
+ region: ["Northeast", "Southeast", "Midwest", "Southwest", "West Coast", "Pacific Northwest"],
400
+ avg_listing_price: u.weighNumRange(200000, 1500000, 0.4, 30),
401
+ }
402
+ },
403
+
404
+ lookupTables: [],
405
+
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
+ 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;
480
+ }
481
+ }
482
+ }
483
+
484
+ // ===============================================================
485
+ // EVERYTHING HOOK — Complex behavioral patterns
486
+ // ===============================================================
487
+ if (type === "everything") {
488
+ const userEvents = record;
489
+ const profile = meta.profile;
490
+
491
+ // Stamp superProps from profile for consistency
492
+ userEvents.forEach(e => {
493
+ e.user_type = profile.user_type;
494
+ e.preferred_location = profile.preferred_location;
495
+ e.property_preference = profile.property_preference;
496
+ e.has_saved_search = profile.has_saved_search;
497
+ e.cold_lead = profile.cold_lead;
498
+ });
499
+
500
+ const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
501
+
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");
508
+ for (let i = userEvents.length - 1; i >= 0; i--) {
509
+ 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
+ }
514
+ }
515
+ }
516
+
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
+ // -----------------------------------------------------------
523
+ 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
+ }
533
+
534
+ 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
+ const viewTemplate = userEvents.find(e => e.event === "property viewed");
541
+ const tourTemplate = userEvents.find(e => e.event === "tour scheduled");
542
+
543
+ if (viewTemplate) {
544
+ const extraCount = chance.integer({ min: 3, max: 8 });
545
+ for (let i = 0; i < extraCount; i++) {
546
+ const offset = chance.integer({ min: 10, max: num_days - 10 });
547
+ userEvents.push({
548
+ ...viewTemplate,
549
+ time: DATASET_START.add(offset, "days").add(chance.integer({ min: 0, max: 23 }), "hours").toISOString(),
550
+ user_id: viewTemplate.user_id,
551
+ listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
552
+ listing_price: chance.integer({ min: 200000, max: 1200000 }),
553
+ has_saved_search: true,
554
+ });
555
+ }
556
+ }
557
+ if (tourTemplate) {
558
+ const extraTours = chance.integer({ min: 1, max: 3 });
559
+ for (let i = 0; i < extraTours; i++) {
560
+ const offset = chance.integer({ min: 15, max: num_days - 10 });
561
+ userEvents.push({
562
+ ...tourTemplate,
563
+ time: DATASET_START.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
564
+ user_id: tourTemplate.user_id,
565
+ listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
566
+ has_saved_search: true,
567
+ });
568
+ }
569
+ }
570
+ } else {
571
+ // Non-savers: remove 80% of events after day 30
572
+ for (let i = userEvents.length - 1; i >= 0; i--) {
573
+ const evt = userEvents[i];
574
+ if (dayjs(evt.time).isAfter(day30)) {
575
+ if (chance.bool({ likelihood: 80 })) {
576
+ userEvents.splice(i, 1);
577
+ }
578
+ }
579
+ }
580
+ }
581
+
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
+ // -----------------------------------------------------------
587
+ const hasPreApproval = userEvents.some(e => e.event === "mortgage pre-approval");
588
+
589
+ if (hasPreApproval) {
590
+ profile.pre_approval_status = "approved";
591
+ const offerTemplate = userEvents.find(e => e.event === "offer submitted");
592
+ if (offerTemplate) {
593
+ const extraOffers = chance.integer({ min: 4, max: 6 });
594
+ for (let i = 0; i < extraOffers; i++) {
595
+ const offset = chance.integer({ min: 20, max: num_days - 5 });
596
+ userEvents.push({
597
+ ...offerTemplate,
598
+ time: DATASET_START.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
599
+ user_id: offerTemplate.user_id,
600
+ listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
601
+ offer_price: chance.integer({ min: 250000, max: 1200000 }),
602
+ listing_price: chance.integer({ min: 250000, max: 1200000 }),
603
+ pre_approved: true,
604
+ spring_season: false,
605
+ dual_tour: false,
606
+ });
607
+ }
608
+ }
609
+
610
+ // Also tag existing offers
611
+ userEvents.forEach(e => {
612
+ if (e.event === "offer submitted") {
613
+ e.pre_approved = true;
614
+ }
615
+ });
616
+ }
617
+
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
+ // -----------------------------------------------------------
623
+ if (profile.agent_tier === "Premier") {
624
+ const listTemplate = userEvents.find(e => e.event === "property listed");
625
+ const soldTemplate = userEvents.find(e => e.event === "property sold");
626
+
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
+ if (listTemplate) {
636
+ const existingListings = userEvents.filter(e => e.event === "property listed").length;
637
+ const extraListings = existingListings * 2;
638
+ for (let i = 0; i < extraListings; i++) {
639
+ const offset = chance.integer({ min: 5, max: num_days - 5 });
640
+ userEvents.push({
641
+ ...listTemplate,
642
+ time: DATASET_START.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
643
+ user_id: listTemplate.user_id,
644
+ listing_price: chance.integer({ min: 300000, max: 1500000 }),
645
+ listing_status: chance.pickone(["active", "pending", "coming soon"]),
646
+ luxury: false,
647
+ premier_agent: true,
648
+ });
649
+ }
650
+ }
651
+
652
+ // Inject extra sales (2x = 1 more copy per existing)
653
+ if (soldTemplate) {
654
+ const existingSales = userEvents.filter(e => e.event === "property sold").length;
655
+ for (let i = 0; i < existingSales; i++) {
656
+ const offset = chance.integer({ min: 10, max: num_days - 5 });
657
+ userEvents.push({
658
+ ...soldTemplate,
659
+ time: DATASET_START.add(offset, "days").add(chance.integer({ min: 9, max: 17 }), "hours").toISOString(),
660
+ user_id: soldTemplate.user_id,
661
+ sale_price: chance.integer({ min: 300000, max: 1500000 }),
662
+ days_on_market: chance.integer({ min: 5, max: 90 }),
663
+ premier_agent: true,
664
+ });
665
+ }
666
+ }
667
+ }
668
+
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
+ // -----------------------------------------------------------
674
+ const hasVirtualTour = userEvents.some(e => e.event === "virtual tour");
675
+ const hasInPersonTour = userEvents.some(e => e.event === "in-person tour");
676
+ const isDualTourer = hasVirtualTour && hasInPersonTour;
677
+
678
+ if (isDualTourer) {
679
+ const offerTemplate = userEvents.find(e => e.event === "offer submitted");
680
+ if (offerTemplate) {
681
+ const extraOffers = chance.integer({ min: 5, max: 7 });
682
+ for (let i = 0; i < extraOffers; i++) {
683
+ const offset = chance.integer({ min: 15, max: num_days - 5 });
684
+ userEvents.push({
685
+ ...offerTemplate,
686
+ time: DATASET_START.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
687
+ user_id: offerTemplate.user_id,
688
+ listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
689
+ offer_price: chance.integer({ min: 300000, max: 1500000 }),
690
+ listing_price: chance.integer({ min: 300000, max: 1500000 }),
691
+ dual_tour: true,
692
+ pre_approved: false,
693
+ spring_season: false,
694
+ });
695
+ }
696
+ }
697
+
698
+ // Tag existing offers
699
+ userEvents.forEach(e => {
700
+ if (e.event === "offer submitted") {
701
+ e.dual_tour = true;
702
+ }
703
+ });
704
+ }
705
+
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");
712
+ if (userEvents.length > 0) {
713
+ const userId = userEvents[0].user_id || "";
714
+ const isLuxuryBrowser = typeof userId === "string" && userId.length > 0 && userId.charCodeAt(0) % 33 === 0;
715
+
716
+ if (isLuxuryBrowser) {
717
+ const viewTemplate = userEvents.find(e => e.event === "property viewed");
718
+ if (viewTemplate) {
719
+ const extraViews = chance.integer({ min: 3, max: 6 });
720
+ for (let i = 0; i < extraViews; i++) {
721
+ const offset = chance.integer({ min: 50, max: num_days - 5 });
722
+ userEvents.push({
723
+ ...viewTemplate,
724
+ time: DATASET_START.add(offset, "days").add(chance.integer({ min: 9, max: 21 }), "hours").toISOString(),
725
+ user_id: viewTemplate.user_id,
726
+ listing_id: `LST-${chance.integer({ min: 90000, max: 99999 })}`,
727
+ listing_price: chance.integer({ min: 5000000, max: 15000000 }),
728
+ property_type: chance.pickone(["Single Family", "Condo"]),
729
+ bedrooms: chance.integer({ min: 4, max: 8 }),
730
+ square_feet: chance.integer({ min: 4000, max: 15000 }),
731
+ luxury: true,
732
+ spring_season: false,
733
+ });
734
+ }
735
+ }
736
+ }
737
+ }
738
+
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
+ // -----------------------------------------------------------
744
+ const day14 = firstEventTime ? firstEventTime.add(14, "days") : null;
745
+
746
+ if (day14) {
747
+ const earlyEvents = userEvents.filter(e => dayjs(e.time).isBefore(day14));
748
+ const hasView = earlyEvents.some(e => e.event === "property viewed");
749
+ const hasSave = earlyEvents.some(e => e.event === "property saved");
750
+
751
+ if (hasView && !hasSave) {
752
+ // Cold lead: remove 90% of events after day 14
753
+ profile.cold_lead = true;
754
+ for (let i = userEvents.length - 1; i >= 0; i--) {
755
+ 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
+ }
762
+ }
763
+ }
764
+ // Tag surviving events
765
+ userEvents.forEach(e => { e.cold_lead = true; });
766
+ }
767
+ }
768
+ }
769
+
770
+ return record;
771
+ }
772
+ };
773
+
774
+ export default config;