@ak--47/dungeon-master 1.4.4 → 1.5.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 (67) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +147 -0
  9. package/HOOKS.md +1243 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +1 -1
  12. package/dungeons/technical/anonymous-users.js +1 -1
  13. package/dungeons/technical/array-of-object-lookup.js +1 -1
  14. package/dungeons/technical/datagen-v15-verify.js +74 -0
  15. package/dungeons/technical/experiments.js +1 -1
  16. package/dungeons/technical/foobar.js +1 -1
  17. package/dungeons/technical/group-analytics.js +1 -1
  18. package/dungeons/technical/mirror-strategies.js +1 -1
  19. package/dungeons/technical/nested-objects.js +1 -1
  20. package/dungeons/technical/retention-cadence.js +1 -1
  21. package/dungeons/technical/sanity.js +1 -1
  22. package/dungeons/technical/scale-test.js +1 -1
  23. package/dungeons/technical/scd.js +1 -1
  24. package/dungeons/technical/simple.js +1 -1
  25. package/dungeons/technical/simplest.js +74 -20
  26. package/dungeons/technical/text-generation.js +1 -1
  27. package/dungeons/vertical/ai-platform.js +4 -0
  28. package/dungeons/vertical/community.js +9 -3
  29. package/dungeons/vertical/crypto.js +5 -0
  30. package/dungeons/vertical/dating.js +23 -10
  31. package/dungeons/vertical/devtools.js +10 -0
  32. package/dungeons/vertical/ecommerce.js +6 -0
  33. package/dungeons/vertical/education.js +11 -0
  34. package/dungeons/vertical/fintech.js +13 -0
  35. package/dungeons/vertical/fitness.js +10 -0
  36. package/dungeons/vertical/food-delivery.js +9 -0
  37. package/dungeons/vertical/gaming.js +10 -0
  38. package/dungeons/vertical/healthcare.js +5 -0
  39. package/dungeons/vertical/insurance-application.js +10 -0
  40. package/dungeons/vertical/logistics.js +8 -1
  41. package/dungeons/vertical/marketplace.js +7 -0
  42. package/dungeons/vertical/media.js +8 -0
  43. package/dungeons/vertical/real-estate.js +7 -1
  44. package/dungeons/vertical/sass.js +12 -0
  45. package/dungeons/vertical/social.js +9 -0
  46. package/dungeons/vertical/travel.js +5 -0
  47. package/index.js +45 -7
  48. package/lib/core/config-validator.js +270 -7
  49. package/lib/core/context.js +58 -0
  50. package/lib/core/dungeon-loader.js +2 -5
  51. package/lib/generators/events.js +12 -13
  52. package/lib/generators/funnels.js +72 -1
  53. package/lib/hook-helpers/index.js +1 -0
  54. package/lib/hook-helpers/inject.js +95 -0
  55. package/lib/orchestrators/mixpanel-sender.js +27 -1
  56. package/lib/orchestrators/user-loop.js +488 -29
  57. package/lib/templates/macro-presets.js +39 -9
  58. package/lib/utils/utils.js +16 -79
  59. package/lib/verify/counting.js +320 -0
  60. package/lib/verify/emulate-breakdown.js +512 -108
  61. package/lib/verify/funnel-engine.js +539 -0
  62. package/lib/verify/identity.js +78 -0
  63. package/lib/verify/index.js +19 -0
  64. package/lib/verify/verify-dungeon.js +58 -0
  65. package/package.json +4 -2
  66. package/types.d.ts +314 -4
  67. package/scripts/smoke-test-all.mjs +0 -162
@@ -3,7 +3,7 @@ const SEED = "simple is best";
3
3
  const num_days = 100;
4
4
  const num_users = 2_500;
5
5
  const avg_events_per_user_per_day = 1;
6
- let token = "your-mixpanel-token";
6
+ let token = "";
7
7
 
8
8
  // ── env overrides ──
9
9
  if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
@@ -14,7 +14,7 @@ import dayjs from "dayjs";
14
14
  import utc from "dayjs/plugin/utc.js";
15
15
  dayjs.extend(utc);
16
16
  import { uid, comma } from 'ak-tools';
17
- import { pickAWinner, weighNumRange, date, integer, weighChoices } from "../../lib/utils/utils.js";
17
+ import { pickAWinner, weighNumRange, date, integer, weighChoices, dateRange, listOf, objectList } from "../../lib/utils/utils.js";
18
18
 
19
19
  /** @typedef {import("../../types").Dungeon} Config */
20
20
  const itemCategories = ["Books", "Movies", "Music", "Games", "Electronics", "Computers", "Smart Home", "Home", "Garden", "Pet", "Beauty", "Health", "Toys", "Kids", "Baby", "Handmade", "Sports", "Outdoors", "Automotive", "Industrial", "Entertainment", "Art", "Food", "Appliances", "Office", "Wedding", "Software"];
@@ -26,20 +26,29 @@ const videoCategories = ["funny", "educational", "inspirational", "music", "news
26
26
  * DATASET OVERVIEW
27
27
  * ═══════════════════════════════════════════════════════════════
28
28
  *
29
- * Simplest E-Commerce App — the minimal dungeon with no hooks.
29
+ * Simplest E-Commerce App — the canonical no-hook baseline dungeon.
30
30
  * - 2,500 users over 100 days, ~250K events
31
31
  * - 25 event types covering browse, search, cart, checkout,
32
32
  * notifications, reviews, wishlists, rewards
33
- * - 11 funnels (signup, purchase, content, browse-to-cart, etc.)
33
+ * - 11 funnels with diverse weights (1-10) covering signup, purchase,
34
+ * content engagement, browse-to-cart, post-purchase advocacy, etc.
35
+ * - At least one property of every Mixpanel data type:
36
+ * string, numeric, boolean, date, list, object, list-of-objects
37
+ * on both event properties and user properties.
34
38
  * - No device/location/campaign data — pure event stream
39
+ * - **No hooks.** This is the engine-validation baseline; engine + TimeSoup
40
+ * alone produce the trend shape. Hook authors should reach for vertical
41
+ * dungeons or write hooks against this template; do NOT add hooks here.
35
42
  *
36
43
  * ═══════════════════════════════════════════════════════════════
37
- * ANALYTICS HOOKS
44
+ * USE CASES
38
45
  * ═══════════════════════════════════════════════════════════════
39
46
  *
40
- * None this is a baseline dungeon with a no-op hook function.
41
- * Use it for testing core generation, funnel inference, or as a
42
- * starting template for new dungeons.
47
+ * - Engine sweep harness baseline (`scripts/sweep-engine.mjs`). Strict-bar
48
+ * regression testing across the 184-combo macro/born/rate/active-day matrix.
49
+ * - Reference for "what an average dungeon looks like" — diverse events
50
+ * + funnels + user props with no hook architecting on top.
51
+ * - Starting template for new dungeons before adding bespoke hooks.
43
52
  */
44
53
 
45
54
  /** @type {import('../types.js').Dungeon} */
@@ -69,6 +78,7 @@ const config = {
69
78
  {
70
79
  event: "page view",
71
80
  weight: 10,
81
+ isStrictEvent: false,
72
82
  properties: {
73
83
  page: ["/", "/", "/help", "/account", "/pricing", "/product", "/about", "/blog"],
74
84
  utm_source: ["$organic", "$organic", "$organic", "$organic", "google", "google", "google", "facebook", "facebook", "twitter", "linkedin"],
@@ -77,6 +87,7 @@ const config = {
77
87
  {
78
88
  event: "sign up",
79
89
  weight: 1,
90
+ isStrictEvent: false,
80
91
  isFirstEvent: true,
81
92
  properties: {
82
93
  signupMethod: ["email", "google", "facebook", "github"],
@@ -86,6 +97,7 @@ const config = {
86
97
  {
87
98
  event: "login",
88
99
  weight: 8,
100
+ isStrictEvent: false,
89
101
  properties: {
90
102
  method: ["password", "google", "facebook", "github"],
91
103
  }
@@ -93,6 +105,7 @@ const config = {
93
105
  {
94
106
  event: "search",
95
107
  weight: 7,
108
+ isStrictEvent: false,
96
109
  properties: {
97
110
  query_length: weighNumRange(1, 50),
98
111
  resultsReturned: weighNumRange(0, 100, .25),
@@ -102,16 +115,22 @@ const config = {
102
115
  {
103
116
  event: "view item",
104
117
  weight: 9,
118
+ isStrictEvent: false,
105
119
  properties: {
106
120
  isFeaturedItem: [true, false, false],
107
121
  itemCategory: pickAWinner(itemCategories, integer(0, 27)),
108
122
  price: weighNumRange(5, 500, .25),
109
123
  rating: weighNumRange(1, 5),
124
+ // list (array of strings) — Mixpanel "List" property type
125
+ tags: listOf(["new", "sale", "limited", "popular", "trending", "exclusive", "clearance", "preorder"], { min: 1, max: 3 }),
126
+ // date — Mixpanel "Date" property type, default ISO string
127
+ lastRestockedAt: dateRange(),
110
128
  }
111
129
  },
112
130
  {
113
131
  event: "add to cart",
114
132
  weight: 5,
133
+ isStrictEvent: false,
115
134
  properties: {
116
135
  amount: weighNumRange(5, 500, .25),
117
136
  itemCategory: pickAWinner(itemCategories, integer(0, 27)),
@@ -121,16 +140,27 @@ const config = {
121
140
  {
122
141
  event: "checkout",
123
142
  weight: 3,
143
+ isStrictEvent: false,
124
144
  properties: {
125
145
  amount: weighNumRange(10, 500, .25),
126
146
  currency: ["USD", "CAD", "EUR", "JPY"],
127
147
  coupon: weighChoices(["none", "none", "none", "none", "10%OFF", "20%OFF", "30%OFF"]),
128
148
  numItems: weighNumRange(1, 10),
149
+ // object — Mixpanel "Object" property type (plain object, returned as-is)
150
+ billingAddress: { country: "US", region: "CA", postalCode: "94016" },
151
+ // list of objects — Mixpanel "List of Objects" property type
152
+ lineItems: objectList({
153
+ sku: weighNumRange(10000, 99999),
154
+ name: pickAWinner(itemCategories, integer(0, 27)),
155
+ qty: [1, 1, 1, 2, 2, 3],
156
+ price: weighNumRange(5, 200, .25),
157
+ }, { min: 1, max: 5 }),
129
158
  }
130
159
  },
131
160
  {
132
161
  event: "watch video",
133
162
  weight: 6,
163
+ isStrictEvent: false,
134
164
  properties: {
135
165
  videoCategory: pickAWinner(videoCategories, integer(0, 9)),
136
166
  watchTimeSec: weighNumRange(10, 600, .25),
@@ -140,6 +170,7 @@ const config = {
140
170
  {
141
171
  event: "share content",
142
172
  weight: 2,
173
+ isStrictEvent: false,
143
174
  properties: {
144
175
  platform: ["twitter", "facebook", "linkedin", "email", "link"],
145
176
  contentType: ["video", "product", "article"],
@@ -148,6 +179,7 @@ const config = {
148
179
  {
149
180
  event: "rate item",
150
181
  weight: 4,
182
+ isStrictEvent: false,
151
183
  properties: {
152
184
  rating: weighNumRange(1, 5),
153
185
  itemCategory: pickAWinner(itemCategories, integer(0, 27)),
@@ -160,6 +192,7 @@ const config = {
160
192
  {
161
193
  event: "support ticket",
162
194
  weight: 2,
195
+ isStrictEvent: false,
163
196
  properties: {
164
197
  priority: weighChoices(["low", "low", "medium", "medium", "medium", "high"]),
165
198
  category: ["billing", "technical", "account", "shipping", "returns"],
@@ -168,6 +201,7 @@ const config = {
168
201
  {
169
202
  event: "add to wishlist",
170
203
  weight: 4,
204
+ isStrictEvent: false,
171
205
  properties: {
172
206
  itemCategory: pickAWinner(itemCategories, integer(0, 27)),
173
207
  price: weighNumRange(5, 500, .25),
@@ -176,6 +210,7 @@ const config = {
176
210
  {
177
211
  event: "remove from cart",
178
212
  weight: 3,
213
+ isStrictEvent: false,
179
214
  properties: {
180
215
  reason: weighChoices(["changed mind", "too expensive", "found better", "duplicate", "changed mind", "changed mind"]),
181
216
  }
@@ -183,6 +218,7 @@ const config = {
183
218
  {
184
219
  event: "apply coupon",
185
220
  weight: 2,
221
+ isStrictEvent: false,
186
222
  properties: {
187
223
  couponCode: weighChoices(["SAVE10", "SAVE20", "WELCOME", "FREESHIP", "VIP30", "SAVE10", "SAVE10"]),
188
224
  discountPercent: weighNumRange(5, 50),
@@ -191,6 +227,7 @@ const config = {
191
227
  {
192
228
  event: "notification received",
193
229
  weight: 7,
230
+ isStrictEvent: false,
194
231
  properties: {
195
232
  channel: ["push", "email", "in-app", "sms"],
196
233
  type: ["promo", "order update", "recommendation", "reminder"],
@@ -199,6 +236,7 @@ const config = {
199
236
  {
200
237
  event: "notification clicked",
201
238
  weight: 5,
239
+ isStrictEvent: false,
202
240
  properties: {
203
241
  channel: ["push", "email", "in-app", "sms"],
204
242
  type: ["promo", "order update", "recommendation", "reminder"],
@@ -207,6 +245,7 @@ const config = {
207
245
  {
208
246
  event: "add payment method",
209
247
  weight: 1,
248
+ isStrictEvent: false,
210
249
  properties: {
211
250
  type: ["credit card", "debit card", "paypal", "apple pay", "google pay"],
212
251
  }
@@ -214,6 +253,7 @@ const config = {
214
253
  {
215
254
  event: "update profile",
216
255
  weight: 3,
256
+ isStrictEvent: false,
217
257
  properties: {
218
258
  field: ["avatar", "name", "email", "address", "phone", "preferences"],
219
259
  }
@@ -221,6 +261,7 @@ const config = {
221
261
  {
222
262
  event: "invite friend",
223
263
  weight: 1,
264
+ isStrictEvent: false,
224
265
  properties: {
225
266
  method: ["email", "link", "sms"],
226
267
  }
@@ -228,6 +269,7 @@ const config = {
228
269
  {
229
270
  event: "view category",
230
271
  weight: 8,
272
+ isStrictEvent: false,
231
273
  properties: {
232
274
  category: pickAWinner(itemCategories, integer(0, 27)),
233
275
  sortBy: ["popular", "newest", "price low", "price high", "rating"],
@@ -236,6 +278,7 @@ const config = {
236
278
  {
237
279
  event: "save address",
238
280
  weight: 1,
281
+ isStrictEvent: false,
239
282
  properties: {
240
283
  type: ["home", "work", "other"],
241
284
  }
@@ -243,6 +286,7 @@ const config = {
243
286
  {
244
287
  event: "compare items",
245
288
  weight: 3,
289
+ isStrictEvent: false,
246
290
  properties: {
247
291
  numItems: weighNumRange(2, 5),
248
292
  itemCategory: pickAWinner(itemCategories, integer(0, 27)),
@@ -251,6 +295,7 @@ const config = {
251
295
  {
252
296
  event: "subscribe newsletter",
253
297
  weight: 1,
298
+ isStrictEvent: false,
254
299
  properties: {
255
300
  frequency: ["daily", "weekly", "monthly"],
256
301
  topics: ["deals", "new arrivals", "recommendations", "deals", "deals"],
@@ -259,6 +304,7 @@ const config = {
259
304
  {
260
305
  event: "leave review",
261
306
  weight: 2,
307
+ isStrictEvent: false,
262
308
  properties: {
263
309
  rating: weighNumRange(1, 5),
264
310
  wordCount: weighNumRange(10, 200, .25),
@@ -268,6 +314,7 @@ const config = {
268
314
  {
269
315
  event: "redeem reward",
270
316
  weight: 1,
317
+ isStrictEvent: false,
271
318
  properties: {
272
319
  rewardType: ["discount", "free shipping", "free item", "points bonus"],
273
320
  pointsUsed: weighNumRange(100, 5000, .25),
@@ -375,10 +422,26 @@ const config = {
375
422
  each key should be an array or function reference
376
423
  */
377
424
  userProps: {
425
+ // string
378
426
  theme: ["light", "dark", "custom", "light", "dark"],
379
427
  title: chance.profession.bind(chance),
428
+ spiritAnimal: ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"],
429
+ // numeric
380
430
  luckyNumber: weighNumRange(42, 420, .3),
381
- spiritAnimal: ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"]
431
+ // boolean
432
+ emailOptIn: [true, true, false],
433
+ // date
434
+ signupDate: dateRange(),
435
+ // list
436
+ favoriteCategories: listOf(itemCategories, { min: 1, max: 4 }),
437
+ // object
438
+ preferences: { notifications: true, currency: "USD", language: "en-US" },
439
+ // list of objects
440
+ recentOrders: objectList({
441
+ orderId: weighNumRange(100000, 999999),
442
+ total: weighNumRange(10, 500, .25),
443
+ itemCount: [1, 1, 2, 2, 3],
444
+ }, { min: 0, max: 3 }),
382
445
  },
383
446
  scdProps: {},
384
447
  mirrorProps: {},
@@ -390,17 +453,8 @@ const config = {
390
453
  groupKeys: [],
391
454
  groupProps: {},
392
455
  lookupTables: [],
393
- hook: function (record, type, meta) {
394
-
395
- if (type === "everything") {
396
- const profile = meta.profile;
397
- record.forEach(e => {
398
- e.theme = profile.theme;
399
- });
400
- }
401
-
402
- return record;
403
- }
456
+ // No hook by design this is the engine-validation baseline. Engine + TimeSoup
457
+ // alone produce the trend shape. To experiment with hooks, fork this file.
404
458
  };
405
459
 
406
460
 
@@ -3,7 +3,7 @@ const SEED = "SUPER DUPER DANGEROUS BROOO";
3
3
  const num_days = 92;
4
4
  const num_users = 8_000;
5
5
  const avg_events_per_user_per_day = 1.3;
6
- let token = "your-mixpanel-token";
6
+ let token = "";
7
7
 
8
8
  // ── env overrides ──
9
9
  if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
@@ -390,6 +390,7 @@ const config = {
390
390
  {
391
391
  event: "api call",
392
392
  weight: 10,
393
+ isStrictEvent: false,
393
394
  properties: {
394
395
  model: ["sonnet-4", "sonnet-4", "sonnet-4", "haiku-4", "haiku-4", "opus-4-6"],
395
396
  input_tokens: u.weighNumRange(50, 8000, 0.4, 2000),
@@ -410,6 +411,7 @@ const config = {
410
411
  {
411
412
  event: "tool use call",
412
413
  weight: 4,
414
+ isStrictEvent: false,
413
415
  properties: {
414
416
  tool_name: ["web_search", "code_interpreter", "file_reader", "calculator", "database_query", "api_connector"],
415
417
  execution_time_ms: u.weighNumRange(50, 10000, 0.4, 800),
@@ -441,6 +443,7 @@ const config = {
441
443
  {
442
444
  event: "eval job",
443
445
  weight: 3,
446
+ isStrictEvent: false,
444
447
  properties: {
445
448
  eval_type: ["accuracy", "relevance", "safety", "latency", "cost", "custom"],
446
449
  num_test_cases: u.weighNumRange(10, 1000, 0.3, 100),
@@ -472,6 +475,7 @@ const config = {
472
475
  {
473
476
  event: "billing payment",
474
477
  weight: 2,
478
+ isStrictEvent: false,
475
479
  properties: {
476
480
  amount_usd: u.weighNumRange(5, 50000, 0.2, 500),
477
481
  payment_method: ["credit_card", "credit_card", "credit_card", "invoice", "wire_transfer"],
@@ -381,6 +381,7 @@ const config = {
381
381
  {
382
382
  event: "article viewed",
383
383
  weight: 9,
384
+ isStrictEvent: false,
384
385
  properties: {
385
386
  wiki_id: chance.pickone.bind(chance, wikiIds),
386
387
  content_hub: ["gaming", "anime", "movies", "tv", "comics", "music"],
@@ -391,6 +392,7 @@ const config = {
391
392
  {
392
393
  event: "article published",
393
394
  weight: 3,
395
+ isStrictEvent: false,
394
396
  properties: {
395
397
  wiki_id: chance.pickone.bind(chance, wikiIds),
396
398
  content_hub: ["gaming", "anime", "movies", "tv", "comics", "music"],
@@ -413,6 +415,7 @@ const config = {
413
415
  {
414
416
  event: "discussion posted",
415
417
  weight: 5,
418
+ isStrictEvent: false,
416
419
  properties: {
417
420
  community_id: chance.pickone.bind(chance, communityIds),
418
421
  content_hub: ["gaming", "anime", "movies", "tv", "comics", "music"],
@@ -424,6 +427,7 @@ const config = {
424
427
  {
425
428
  event: "comment posted",
426
429
  weight: 6,
430
+ isStrictEvent: false,
427
431
  properties: {
428
432
  community_id: chance.pickone.bind(chance, communityIds),
429
433
  content_hub: ["gaming", "anime", "movies", "tv", "comics", "music"],
@@ -435,6 +439,7 @@ const config = {
435
439
  {
436
440
  event: "upvote given",
437
441
  weight: 7,
442
+ isStrictEvent: false,
438
443
  properties: {
439
444
  content_type: ["article", "article", "discussion", "comment"],
440
445
  content_hub: ["gaming", "anime", "movies", "tv", "comics", "music"],
@@ -565,6 +570,7 @@ const config = {
565
570
  order: "sequential",
566
571
  timeToConvert: 96,
567
572
  weight: 4,
573
+ reentry: true,
568
574
  },
569
575
  {
570
576
  name: "Creator to Supporter",
@@ -837,11 +843,11 @@ const config = {
837
843
  }
838
844
 
839
845
  // -- HOOK 8: PRO SUBSCRIBER CONTENT CREATION LIFT ---------
840
- // Free-tier users drop 50% of comment events to widen
841
- // the gap to ~2.0x vs paid subscribers.
846
+ // Free-tier users drop 65% of comment events to widen the funnel
847
+ // conversion gap to ~2x vs paid subscribers.
842
848
  if (profile.subscription_tier !== "pro" && profile.subscription_tier !== "supporter") {
843
849
  events = events.filter(e => {
844
- if (e.event === "comment posted" && chance.bool({ likelihood: 50 })) return false;
850
+ if (e.event === "comment posted" && chance.bool({ likelihood: 65 })) return false;
845
851
  return true;
846
852
  });
847
853
  }
@@ -426,6 +426,7 @@ const config = {
426
426
  {
427
427
  event: "deposit",
428
428
  weight: 5,
429
+ isStrictEvent: false,
429
430
  properties: {
430
431
  token: ["ETH", "USDC", "USDT", "SOL", "BTC"],
431
432
  deposit_amount_usd: u.weighNumRange(10, 5000, 0.3, 500),
@@ -436,6 +437,7 @@ const config = {
436
437
  {
437
438
  event: "withdrawal",
438
439
  weight: 3,
440
+ isStrictEvent: false,
439
441
  properties: {
440
442
  token: ["ETH", "USDC", "USDT", "SOL", "BTC"],
441
443
  amount_usd: u.weighNumRange(10, 5000, 0.3, 400),
@@ -446,6 +448,7 @@ const config = {
446
448
  {
447
449
  event: "swap",
448
450
  weight: 10,
451
+ isStrictEvent: false,
449
452
  properties: {
450
453
  token_pair: TOKEN_PAIRS,
451
454
  trade_amount_usd: u.weighNumRange(10, 10000, 0.3, 200),
@@ -458,6 +461,7 @@ const config = {
458
461
  {
459
462
  event: "stake",
460
463
  weight: 4,
464
+ isStrictEvent: false,
461
465
  properties: {
462
466
  token: ["ETH", "SOL", "MATIC", "AVAX", "ATOM", "DOT"],
463
467
  amount_usd: u.weighNumRange(50, 10000, 0.3, 500),
@@ -478,6 +482,7 @@ const config = {
478
482
  {
479
483
  event: "claim airdrop",
480
484
  weight: 2,
485
+ isStrictEvent: false,
481
486
  properties: {
482
487
  airdrop_name: AIRDROP_NAMES,
483
488
  token_amount: u.weighNumRange(10, 10000, 0.3, 500),
@@ -277,6 +277,7 @@ const config = {
277
277
  {
278
278
  event: "photo uploaded",
279
279
  weight: 12,
280
+ isStrictEvent: false,
280
281
  properties: {
281
282
  photo_number: u.weighNumRange(1, 6, 0.5, 2),
282
283
  has_face: [true, true, true, true, false],
@@ -300,6 +301,7 @@ const config = {
300
301
  {
301
302
  event: "swipe right",
302
303
  weight: 10,
304
+ isStrictEvent: false,
303
305
  properties: {
304
306
  is_super_like: [false, false, false, false, false, false, false, false, false, true],
305
307
  swipe_source: ["feed", "feed", "feed", "discover", "boost", "nearby"],
@@ -315,6 +317,7 @@ const config = {
315
317
  {
316
318
  event: "match received",
317
319
  weight: 4,
320
+ isStrictEvent: false,
318
321
  properties: {
319
322
  match_score: u.weighNumRange(50, 100, 0.5, 75),
320
323
  },
@@ -322,6 +325,7 @@ const config = {
322
325
  {
323
326
  event: "message sent",
324
327
  weight: 6,
328
+ isStrictEvent: false,
325
329
  properties: {
326
330
  message_length: u.weighNumRange(1, 500, 0.3, 40),
327
331
  has_emoji: [false, false, true, true, true],
@@ -338,6 +342,7 @@ const config = {
338
342
  {
339
343
  event: "phone number exchanged",
340
344
  weight: 1,
345
+ isStrictEvent: false,
341
346
  properties: {
342
347
  exchange_method: ["in_chat", "in_chat", "voice_call", "video_call"],
343
348
  },
@@ -345,6 +350,7 @@ const config = {
345
350
  {
346
351
  event: "date scheduled",
347
352
  weight: 1,
353
+ isStrictEvent: false,
348
354
  properties: {
349
355
  venue_type: ["coffee", "dinner", "drinks", "activity", "virtual"],
350
356
  },
@@ -359,6 +365,7 @@ const config = {
359
365
  {
360
366
  event: "premium upgrade",
361
367
  weight: 1,
368
+ isStrictEvent: false,
362
369
  properties: {
363
370
  plan: ["Premium", "Premium", "Premium", "Elite"],
364
371
  price_usd: [14.99, 14.99, 14.99, 29.99],
@@ -392,6 +399,7 @@ const config = {
392
399
  {
393
400
  event: "app opened",
394
401
  weight: 8,
402
+ isStrictEvent: false,
395
403
  properties: {
396
404
  session_duration_mins: u.weighNumRange(1, 120, 0.3, 8),
397
405
  },
@@ -416,6 +424,7 @@ const config = {
416
424
  order: "sequential",
417
425
  timeToConvert: 24,
418
426
  weight: 6,
427
+ reentry: true,
419
428
  },
420
429
  {
421
430
  name: "Date Funnel",
@@ -424,6 +433,7 @@ const config = {
424
433
  order: "sequential",
425
434
  timeToConvert: 72,
426
435
  weight: 3,
436
+ reentry: true,
427
437
  },
428
438
  {
429
439
  name: "Monetization",
@@ -555,10 +565,9 @@ const config = {
555
565
  });
556
566
  }
557
567
 
558
- // HOOK 1 + HOOK 9: PHOTO MAGIC NUMBER (no flags)
559
- // Sweet 2-5 photos clone 2-4 extra match events per existing.
560
- // Over 6+ photos → drop match_score by 35% on match received events
561
- // (over-curated profile reads as fake/staged).
568
+ // HOOK 1: PHOTO MAGIC NUMBER (sweet 2-5 photos → clone 2-4 extra matches)
569
+ // Over-6 score reduction is applied AT THE END of this hook so it also
570
+ // affects matches injected by HOOK 4 (premium boost).
562
571
  if (photoUploadCount >= 2 && photoUploadCount <= 5 && matchEvents.length > 0) {
563
572
  const matchTemplate = matchEvents[0];
564
573
  matchEvents.forEach(m => {
@@ -572,12 +581,6 @@ const config = {
572
581
  });
573
582
  }
574
583
  });
575
- } else if (photoUploadCount >= 6) {
576
- events.forEach(e => {
577
- if (e.event === "match received" && typeof e.match_score === "number") {
578
- e.match_score = Math.max(20, Math.round(e.match_score * 0.65));
579
- }
580
- });
581
584
  }
582
585
 
583
586
  // HOOK 2: WEEKEND SWIPE SURGE — Sunday swipes get heavy cloning
@@ -742,6 +745,16 @@ const config = {
742
745
  });
743
746
  }
744
747
 
748
+ // HOOK 1b: PHOTO MAGIC NUMBER — over-6 score reduction (applied LAST so
749
+ // it also affects matches injected by HOOK 4 premium boost).
750
+ if (photoUploadCount >= 6) {
751
+ events.forEach(e => {
752
+ if (e.event === "match received" && typeof e.match_score === "number") {
753
+ e.match_score = Math.max(20, Math.round(e.match_score * 0.65));
754
+ }
755
+ });
756
+ }
757
+
745
758
  // HOOK 8: OFF-APP RETENTION — users with phone-exchanged or
746
759
  // date-scheduled in first 14 days get extra cloned app-open + swipe
747
760
  // events past day 30. Non-milestone users lose 80% of post-day-30
@@ -356,6 +356,7 @@ const config = {
356
356
  {
357
357
  event: "build completed",
358
358
  weight: 8,
359
+ isStrictEvent: false,
359
360
  properties: {
360
361
  pipeline_id: chance.pickone.bind(chance, pipelineIds),
361
362
  repo_id: chance.pickone.bind(chance, repoIds),
@@ -369,6 +370,7 @@ const config = {
369
370
  {
370
371
  event: "deployment completed",
371
372
  weight: 5,
373
+ isStrictEvent: false,
372
374
  properties: {
373
375
  pipeline_id: chance.pickone.bind(chance, pipelineIds),
374
376
  repo_id: chance.pickone.bind(chance, repoIds),
@@ -381,6 +383,7 @@ const config = {
381
383
  {
382
384
  event: "pull request created",
383
385
  weight: 6,
386
+ isStrictEvent: false,
384
387
  properties: {
385
388
  repo_id: chance.pickone.bind(chance, repoIds),
386
389
  pr_size: ["small", "small", "medium", "medium", "large", "xlarge"],
@@ -393,6 +396,7 @@ const config = {
393
396
  {
394
397
  event: "code review completed",
395
398
  weight: 5,
399
+ isStrictEvent: false,
396
400
  properties: {
397
401
  repo_id: chance.pickone.bind(chance, repoIds),
398
402
  review_result: ["approved", "approved", "approved", "changes_requested", "commented"],
@@ -404,6 +408,7 @@ const config = {
404
408
  {
405
409
  event: "alert triggered",
406
410
  weight: 4,
411
+ isStrictEvent: false,
407
412
  properties: {
408
413
  alert_type: ["error_rate", "latency", "cpu", "memory", "disk", "custom_metric"],
409
414
  severity: ["info", "warning", "warning", "critical", "critical"],
@@ -414,6 +419,7 @@ const config = {
414
419
  {
415
420
  event: "incident created",
416
421
  weight: 2,
422
+ isStrictEvent: false,
417
423
  properties: {
418
424
  severity: ["sev1", "sev2", "sev2", "sev3", "sev3", "sev3"],
419
425
  service: ["api", "web", "worker", "database", "cdn", "auth"],
@@ -424,6 +430,7 @@ const config = {
424
430
  {
425
431
  event: "incident resolved",
426
432
  weight: 2,
433
+ isStrictEvent: false,
427
434
  properties: {
428
435
  severity: ["sev1", "sev2", "sev2", "sev3", "sev3", "sev3"],
429
436
  resolution_time_minutes: u.weighNumRange(5, 480, 0.3, 60),
@@ -461,6 +468,7 @@ const config = {
461
468
  {
462
469
  event: "monitoring dashboard viewed",
463
470
  weight: 5,
471
+ isStrictEvent: false,
464
472
  properties: {
465
473
  dashboard_type: ["overview", "performance", "errors", "deploys", "custom"],
466
474
  time_range: ["1h", "6h", "24h", "7d", "30d"],
@@ -541,6 +549,7 @@ const config = {
541
549
  order: "sequential",
542
550
  timeToConvert: 48,
543
551
  weight: 5,
552
+ reentry: true,
544
553
  },
545
554
  {
546
555
  name: "PR Review Flow",
@@ -557,6 +566,7 @@ const config = {
557
566
  order: "sequential",
558
567
  timeToConvert: 24,
559
568
  weight: 3,
569
+ reentry: true,
560
570
  },
561
571
  {
562
572
  name: "Upgrade Path",