@ak--47/dungeon-master 1.5.0 → 1.5.2

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 (66) hide show
  1. package/.claude/skills/create-dungeon/SKILL.md +139 -46
  2. package/.claude/skills/verify-dungeon/references/counting-semantics.md +31 -6
  3. package/.claude/skills/verify-dungeon/references/sql-recipes.md +44 -25
  4. package/.claude/skills/write-hooks/SKILL.md +31 -3
  5. package/CHANGELOG.md +85 -0
  6. package/HOOKS.md +13 -0
  7. package/dungeons/technical/ad-spend.js +41 -49
  8. package/dungeons/technical/anonymous-users.js +38 -36
  9. package/dungeons/technical/array-of-object-lookup.js +136 -153
  10. package/dungeons/technical/datagen-v15-verify.js +24 -11
  11. package/dungeons/technical/experiments.js +42 -40
  12. package/dungeons/technical/foobar.js +114 -118
  13. package/dungeons/technical/group-analytics.js +42 -40
  14. package/dungeons/technical/hook-helpers-verify.js +69 -50
  15. package/dungeons/technical/identity-model-verify.js +22 -12
  16. package/dungeons/technical/mirror-strategies.js +37 -39
  17. package/dungeons/technical/nested-objects.js +119 -118
  18. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  19. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  20. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  21. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  22. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  23. package/dungeons/technical/retention-cadence.js +115 -112
  24. package/dungeons/technical/sanity.js +86 -80
  25. package/dungeons/technical/scale-test.js +34 -38
  26. package/dungeons/technical/scd.js +111 -128
  27. package/dungeons/technical/simple.js +134 -141
  28. package/dungeons/technical/simplest.js +54 -62
  29. package/dungeons/technical/text-generation.js +110 -146
  30. package/dungeons/vertical/ai-platform.js +296 -333
  31. package/dungeons/vertical/community.js +284 -255
  32. package/dungeons/vertical/crypto.js +395 -391
  33. package/dungeons/vertical/dating.js +411 -378
  34. package/dungeons/vertical/devtools.js +336 -298
  35. package/dungeons/vertical/ecommerce.js +316 -394
  36. package/dungeons/vertical/education.js +369 -325
  37. package/dungeons/vertical/fintech.js +358 -325
  38. package/dungeons/vertical/fitness.js +335 -291
  39. package/dungeons/vertical/food-delivery.js +343 -307
  40. package/dungeons/vertical/gaming.js +480 -444
  41. package/dungeons/vertical/healthcare.js +306 -262
  42. package/dungeons/vertical/insurance-application.js +427 -409
  43. package/dungeons/vertical/logistics.js +271 -252
  44. package/dungeons/vertical/marketplace.js +333 -323
  45. package/dungeons/vertical/media.js +382 -335
  46. package/dungeons/vertical/real-estate.js +395 -346
  47. package/dungeons/vertical/sass.js +319 -333
  48. package/dungeons/vertical/social.js +368 -316
  49. package/dungeons/vertical/travel.js +297 -295
  50. package/index.js +46 -4
  51. package/lib/core/config-validator.js +126 -28
  52. package/lib/generators/funnels.js +4 -1
  53. package/lib/orchestrators/mixpanel-sender.js +7 -0
  54. package/lib/orchestrators/user-loop.js +132 -31
  55. package/lib/templates/defaults.js +59 -59
  56. package/lib/templates/macro-presets.js +14 -2
  57. package/lib/utils/dataset-context.js +103 -0
  58. package/lib/utils/retention-curve.js +140 -0
  59. package/lib/utils/utils.js +149 -38
  60. package/lib/verify/counting.js +40 -0
  61. package/lib/verify/emulate-breakdown.js +20 -1
  62. package/lib/verify/index.js +1 -0
  63. package/lib/verify/schema-validator.js +3 -1
  64. package/package.json +11 -2
  65. package/scripts/run-dungeon.mjs +12 -1
  66. package/types.d.ts +117 -1
@@ -1,49 +1,47 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "mirror-strategies";
3
- const num_days = 30;
4
- const num_users = 500;
5
- const avg_events_per_user_per_day = 2;
6
- let token = "";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
1
+ // ── IMPORTS ──
2
+ import { weighNumRange } from "../../lib/utils/utils.js";
3
+ /** @typedef {import("../../types").Dungeon} Config */
10
4
 
11
- /**
12
- * Mirror Strategies — tests all 4 mirror data strategies.
13
- *
14
- * Exercises: create, update, fill, delete mirror transformations.
15
- * Each mirror prop targets a different event and uses a different
16
- * strategy so the output can be diffed against the original data.
17
- *
18
- * - 500 users, 30K events, 30 days
19
- * - 5 simple events, no funnels, no hooks
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: mirror-strategies
8
+ * PURPOSE: exercises all 4 mirrorProps strategies (create, update, fill, delete)
9
+ * SCALE: 500 users, ~30K events, 30 days
10
+ * EVENTS (5): page view, checkout, profile update, subscription change, login
11
+ * FUNNELS (0): none
20
12
  */
21
13
 
22
- import Chance from 'chance';
23
- let chance = new Chance();
24
- import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
14
+ // ── SCALE ──
15
+ const SEED = "mirror-strategies";
16
+ const NUM_DAYS = 30;
17
+ const NUM_USERS = 500;
18
+ const EVENTS_PER_DAY = 2;
19
+ const token = process.env.MP_TOKEN || "";
25
20
 
26
- /** @typedef {import("../../types").Dungeon} Config */
27
- /** @type {import('../../types').Dungeon} */
21
+ // ── CONFIG ──
22
+ /** @type {Config} */
28
23
  const config = {
29
- token,
30
24
  seed: SEED,
31
- numDays: num_days,
32
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
33
- numUsers: num_users,
25
+ numDays: NUM_DAYS,
26
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
27
+ numUsers: NUM_USERS,
34
28
  format: "json",
35
- region: "US",
36
- hasAnonIds: false,
37
- hasSessionIds: false,
38
- hasAdSpend: false,
39
- hasLocation: false,
40
- hasAndroidDevices: false,
41
- hasIOSDevices: false,
42
- hasDesktopDevices: false,
43
- hasBrowser: false,
44
- hasCampaigns: false,
45
- isAnonymous: false,
46
- alsoInferFunnels: false,
29
+ credentials: {
30
+ token,
31
+ region: "US",
32
+ },
33
+ switches: {
34
+ hasSessionIds: false,
35
+ hasAdSpend: false,
36
+ hasLocation: false,
37
+ hasAndroidDevices: false,
38
+ hasIOSDevices: false,
39
+ hasDesktopDevices: false,
40
+ hasBrowser: false,
41
+ hasCampaigns: false,
42
+ isAnonymous: false,
43
+ alsoInferFunnels: false,
44
+ },
47
45
  concurrency: 1,
48
46
  writeToDisk: false,
49
47
 
@@ -1,35 +1,31 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "dm4-nested-objects";
3
- const num_days = 30;
4
- const num_users = 500;
5
- const avg_events_per_user_per_day = 2;
6
- let token = "";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
11
- import Chance from 'chance';
12
- let chance = new Chance();
1
+ // ── IMPORTS ──
2
+ import Chance from "chance";
13
3
  import dayjs from "dayjs";
14
4
  import utc from "dayjs/plugin/utc.js";
15
5
  dayjs.extend(utc);
16
- import { uid, comma } from 'ak-tools';
17
- import { weighNumRange, date, integer, weighChoices } from "../../lib/utils/utils.js";
18
-
6
+ import { weighNumRange, integer } from "../../lib/utils/utils.js";
19
7
  /** @typedef {import("../../types").Dungeon} Config */
20
- /**
21
- * ═══════════════════════════════════════════════════════════════
22
- * TECHNICAL TEST: Nested Objects & High Cardinality
23
- * ═══════════════════════════════════════════════════════════════
24
- *
25
- * Tests Mixpanel's handling of:
26
- * - Array-of-object event properties (cart items, search results, form fields)
27
- * - Deeply nested object properties (metadata.browser.name, etc.)
28
- * - High cardinality string properties (10K product IDs, unique session IDs)
29
- *
30
- * 500 users, 30K events, 30 days. No hooks.
8
+
9
+ // ── OVERVIEW ──
10
+ /*
11
+ * NAME: nested-objects
12
+ * PURPOSE: Exercises array-of-object event properties, deeply nested object
13
+ * properties, and high-cardinality string properties.
14
+ * SCALE: 500 users, ~30K events, 30 days
15
+ * EVENTS (4): checkout, search performed, form submitted, page view
16
+ * FUNNELS (0): none
31
17
  */
32
18
 
19
+ // ── SCALE ──
20
+ const SEED = "dm4-nested-objects";
21
+ const NUM_DAYS = 30;
22
+ const NUM_USERS = 500;
23
+ const EVENTS_PER_DAY = 2;
24
+ const token = process.env.MP_TOKEN || "";
25
+
26
+ const chance = new Chance();
27
+
28
+ // ── DATA ARRAYS ──
33
29
  const productCategories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office"];
34
30
  const browsers = ["Chrome", "Firefox", "Safari", "Edge", "Opera", "Brave"];
35
31
  const browserVersions = ["120.0", "121.0", "119.0", "118.0", "117.0", "116.0", "115.0"];
@@ -38,98 +34,7 @@ const osList = ["Windows 11", "macOS 14", "iOS 17", "Android 14", "Linux", "Chro
38
34
  const cities = ["New York", "London", "Tokyo", "Berlin", "Sydney", "Toronto", "Paris", "Mumbai", "Seoul", "Mexico City", "Cairo", "Lagos", "Dubai", "Bangkok", "Istanbul"];
39
35
  const countries = ["US", "UK", "JP", "DE", "AU", "CA", "FR", "IN", "KR", "MX", "EG", "NG", "AE", "TH", "TR"];
40
36
 
41
- /** @type {import('../../types').Dungeon} */
42
- const config = {
43
- token,
44
- seed: SEED,
45
- name: "nested-objects",
46
- numDays: num_days,
47
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
48
- numUsers: num_users,
49
- format: 'json',
50
- region: "US",
51
- hasAnonIds: false,
52
- hasSessionIds: false,
53
- hasAdSpend: false,
54
- hasLocation: false,
55
- hasAndroidDevices: false,
56
- hasIOSDevices: false,
57
- hasDesktopDevices: true,
58
- hasBrowser: false,
59
- hasCampaigns: false,
60
- isAnonymous: false,
61
- alsoInferFunnels: false,
62
- concurrency: 1,
63
- writeToDisk: false,
64
-
65
- events: [
66
- {
67
- event: "checkout",
68
- weight: 3,
69
- properties: {
70
- // Array of objects: cart items
71
- cart: makeCartItems(),
72
- total_amount: weighNumRange(10, 2000, .25),
73
- currency: ["USD", "EUR", "GBP", "JPY", "CAD"],
74
- // High cardinality: unique session per event
75
- session_id: () => chance.guid(),
76
- // Deeply nested metadata
77
- metadata: makeMetadata(),
78
- }
79
- },
80
- {
81
- event: "search performed",
82
- weight: 5,
83
- properties: {
84
- query: ["laptop", "shoes", "headphones", "jacket", "phone case", "coffee maker", "backpack", "monitor", "keyboard", "mouse"],
85
- // Array of objects: search results
86
- results: makeSearchResults(),
87
- result_count: weighNumRange(0, 50),
88
- // High cardinality product ID
89
- top_result_id: () => `prod_${integer(1, 10000)}`,
90
- session_id: () => chance.guid(),
91
- metadata: makeMetadata(),
92
- }
93
- },
94
- {
95
- event: "form submitted",
96
- weight: 4,
97
- properties: {
98
- form_name: ["checkout", "signup", "contact", "feedback", "settings", "profile"],
99
- // Array of objects: form fields
100
- fields: makeFormFields(),
101
- submission_valid: [true, true, true, false],
102
- session_id: () => chance.guid(),
103
- metadata: makeMetadata(),
104
- }
105
- },
106
- {
107
- event: "page view",
108
- weight: 8,
109
- properties: {
110
- page: ["/", "/products", "/cart", "/search", "/account", "/help", "/about"],
111
- // High cardinality
112
- product_id: () => `prod_${integer(1, 10000)}`,
113
- session_id: () => chance.guid(),
114
- metadata: makeMetadata(),
115
- }
116
- },
117
- ],
118
- funnels: [],
119
- superProps: {},
120
- userProps: {
121
- plan: ["free", "basic", "pro", "enterprise"],
122
- },
123
- scdProps: {},
124
- mirrorProps: {},
125
- groupKeys: [],
126
- groupProps: {},
127
- lookupTables: [],
128
- hook: function (record, type, meta) {
129
- return record;
130
- }
131
- };
132
-
37
+ // ── HELPER FUNCTIONS ──
133
38
  /**
134
39
  * Generates an array-of-objects property for cart items.
135
40
  * Each cart has 1-5 items with product_id, name, price, quantity, category.
@@ -215,4 +120,100 @@ function makeMetadata() {
215
120
  };
216
121
  }
217
122
 
123
+ // ── CONFIG ──
124
+ /** @type {Config} */
125
+ const config = {
126
+ seed: SEED,
127
+ name: "nested-objects",
128
+ numDays: NUM_DAYS,
129
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
130
+ numUsers: NUM_USERS,
131
+ format: 'json',
132
+ credentials: {
133
+ token,
134
+ region: "US",
135
+ },
136
+ switches: {
137
+ hasSessionIds: false,
138
+ hasAdSpend: false,
139
+ hasLocation: false,
140
+ hasAndroidDevices: false,
141
+ hasIOSDevices: false,
142
+ hasDesktopDevices: true,
143
+ hasBrowser: false,
144
+ hasCampaigns: false,
145
+ isAnonymous: false,
146
+ alsoInferFunnels: false,
147
+ },
148
+ concurrency: 1,
149
+ writeToDisk: false,
150
+
151
+ events: [
152
+ {
153
+ event: "checkout",
154
+ weight: 3,
155
+ properties: {
156
+ // Array of objects: cart items
157
+ cart: makeCartItems(),
158
+ total_amount: weighNumRange(10, 2000, .25),
159
+ currency: ["USD", "EUR", "GBP", "JPY", "CAD"],
160
+ // High cardinality: unique session per event
161
+ session_id: () => chance.guid(),
162
+ // Deeply nested metadata
163
+ metadata: makeMetadata(),
164
+ }
165
+ },
166
+ {
167
+ event: "search performed",
168
+ weight: 5,
169
+ properties: {
170
+ query: ["laptop", "shoes", "headphones", "jacket", "phone case", "coffee maker", "backpack", "monitor", "keyboard", "mouse"],
171
+ // Array of objects: search results
172
+ results: makeSearchResults(),
173
+ result_count: weighNumRange(0, 50),
174
+ // High cardinality product ID
175
+ top_result_id: () => `prod_${integer(1, 10000)}`,
176
+ session_id: () => chance.guid(),
177
+ metadata: makeMetadata(),
178
+ }
179
+ },
180
+ {
181
+ event: "form submitted",
182
+ weight: 4,
183
+ properties: {
184
+ form_name: ["checkout", "signup", "contact", "feedback", "settings", "profile"],
185
+ // Array of objects: form fields
186
+ fields: makeFormFields(),
187
+ submission_valid: [true, true, true, false],
188
+ session_id: () => chance.guid(),
189
+ metadata: makeMetadata(),
190
+ }
191
+ },
192
+ {
193
+ event: "page view",
194
+ weight: 8,
195
+ properties: {
196
+ page: ["/", "/products", "/cart", "/search", "/account", "/help", "/about"],
197
+ // High cardinality
198
+ product_id: () => `prod_${integer(1, 10000)}`,
199
+ session_id: () => chance.guid(),
200
+ metadata: makeMetadata(),
201
+ }
202
+ },
203
+ ],
204
+ funnels: [],
205
+ superProps: {},
206
+ userProps: {
207
+ plan: ["free", "basic", "pro", "enterprise"],
208
+ },
209
+ scdProps: {},
210
+ mirrorProps: {},
211
+ groupKeys: [],
212
+ groupProps: {},
213
+ lookupTables: [],
214
+ hook: function (record, type, meta) {
215
+ return record;
216
+ }
217
+ };
218
+
218
219
  export default config;
@@ -1,15 +1,29 @@
1
- /**
2
- * Phase 4 reference dungeon — aggregate per user, by bin.
3
- *
4
- * Users in the high-Browse cohort have 4× higher Purchase amounts on average;
5
- * mid cohort 2×; low cohort 1×. Verified via the aggregatePerUser emulator.
6
- */
7
-
1
+ // ── IMPORTS ──
8
2
  import dayjs from 'dayjs';
9
3
  import { applyAggregateByBin } from '../../lib/hook-patterns/index.js';
10
4
 
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: pattern-aggregate-by-bin
8
+ * PURPOSE: Phase 4 reference fixture — aggregate per user, by bin. Users in
9
+ * the high-Browse cohort have 4× higher Purchase amounts on average;
10
+ * mid 2×; low 1×. Verified via the aggregatePerUser emulator.
11
+ * SCALE: 1,000 users, ~180K events, 30 days
12
+ * EVENTS (2): Browse (6) > Purchase (2)
13
+ * FUNNELS (0): none
14
+ */
15
+
16
+ // ── HOOK STORIES ──
17
+ /*
18
+ * PATTERN: Bin users by Browse count (low 0-5, mid 5-15, high 15+);
19
+ * scale Purchase `amount` by {low:1, mid:2, high:4}. Verified via
20
+ * the aggregatePerUser emulator.
21
+ */
22
+
23
+ // ── SCALE ──
11
24
  const FIXED_NOW = dayjs('2024-02-02').unix();
12
25
 
26
+ // ── CONFIG ──
13
27
  export default {
14
28
  name: 'pattern-agg-by-bin',
15
29
  seed: 'phase4-aggbybin',
@@ -18,7 +32,6 @@ export default {
18
32
  numUsers: 1_000,
19
33
  avgEventsPerUserPerDay: 6,
20
34
  percentUsersBornInDataset: 30,
21
- hasAnonIds: false,
22
35
  format: 'json',
23
36
  concurrency: 1,
24
37
  writeToDisk: true,
@@ -1,16 +1,31 @@
1
- /**
2
- * Phase 4 reference dungeon — conversions attributed by source.
3
- *
4
- * Touch events carry one of three random sources; conversions are stamped with
5
- * the user's first-touch source weighted google:facebook:twitter = 10:5:1.
6
- * Verified via the attributedBy emulator.
7
- */
8
-
1
+ // ── IMPORTS ──
9
2
  import dayjs from 'dayjs';
10
3
  import { applyAttributedBySource } from '../../lib/hook-patterns/index.js';
11
4
 
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: pattern-attributed-by-source
8
+ * PURPOSE: Phase 4 reference fixture — conversions attributed by source.
9
+ * Touch events carry one of three random sources; conversions are
10
+ * stamped with the user's first-touch source weighted
11
+ * google:facebook:twitter = 10:5:1. Verified via the attributedBy
12
+ * emulator.
13
+ * SCALE: 1,000 users, ~120K events, 30 days
14
+ * EVENTS (2): Touch (5) > Convert (2)
15
+ * FUNNELS (0): none
16
+ */
17
+
18
+ // ── HOOK STORIES ──
19
+ /*
20
+ * PATTERN: First-touch attribution — stamp each Convert event with the
21
+ * source of the user's first Touch, weighted google:facebook:twitter =
22
+ * 10:5:1. Verified via the attributedBy emulator.
23
+ */
24
+
25
+ // ── SCALE ──
12
26
  const FIXED_NOW = dayjs('2024-02-02').unix();
13
27
 
28
+ // ── CONFIG ──
14
29
  export default {
15
30
  name: 'pattern-attributed-by-source',
16
31
  seed: 'phase4-attrib',
@@ -19,7 +34,6 @@ export default {
19
34
  numUsers: 1_000,
20
35
  avgEventsPerUserPerDay: 4,
21
36
  percentUsersBornInDataset: 50,
22
- hasAnonIds: false,
23
37
  format: 'json',
24
38
  concurrency: 1,
25
39
  writeToDisk: true,
@@ -1,15 +1,29 @@
1
- /**
2
- * Phase 4 reference dungeon — frequency × frequency.
3
- *
4
- * Engages users into low / mid / high "Browse" cohorts; high-cohort users get 3×
5
- * Purchase events. Verified by the Phase 4 patterns test + verify-runner.
6
- */
7
-
1
+ // ── IMPORTS ──
8
2
  import dayjs from 'dayjs';
9
3
  import { applyFrequencyByFrequency } from '../../lib/hook-patterns/index.js';
10
4
 
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: pattern-frequency-by-frequency
8
+ * PURPOSE: Phase 4 reference fixture — frequency × frequency. Engages users
9
+ * into low/mid/high "Browse" cohorts; high-cohort users get 3×
10
+ * Purchase events. Verified by the Phase 4 patterns test + verify-runner.
11
+ * SCALE: 1,000 users, ~180K events, 30 days
12
+ * EVENTS (2): Browse (6) > Purchase (2)
13
+ * FUNNELS (0): none
14
+ */
15
+
16
+ // ── HOOK STORIES ──
17
+ /*
18
+ * PATTERN: Bin users by Browse count (low 0-5, mid 5-15, high 15+);
19
+ * scale Purchase event count by {low:1, mid:2, high:3}. Verified via
20
+ * the frequencyByFrequency emulator.
21
+ */
22
+
23
+ // ── SCALE ──
11
24
  const FIXED_NOW = dayjs('2024-02-02').unix();
12
25
 
26
+ // ── CONFIG ──
13
27
  export default {
14
28
  name: 'pattern-freq-by-freq',
15
29
  seed: 'phase4-freqxfreq',
@@ -18,7 +32,6 @@ export default {
18
32
  numUsers: 1_000,
19
33
  avgEventsPerUserPerDay: 6,
20
34
  percentUsersBornInDataset: 30,
21
- hasAnonIds: false,
22
35
  format: 'json',
23
36
  concurrency: 1,
24
37
  writeToDisk: true,
@@ -1,20 +1,39 @@
1
- /**
2
- * Phase 4 reference dungeon — funnel frequency breakdown.
3
- *
4
- * Engaged-cohort users (high count of `Browse`) are 1.5× more likely to complete
5
- * the activation funnel's last step than low-cohort users. Verified via the
6
- * funnelFrequency emulator.
1
+ // ── IMPORTS ──
2
+ import dayjs from 'dayjs';
3
+ import { applyFunnelFrequencyBreakdown } from '../../lib/hook-patterns/index.js';
4
+
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: pattern-funnel-frequency
8
+ * PURPOSE: Phase 4 reference fixture — funnel frequency breakdown.
9
+ * Engaged-cohort users (high count of `Browse`) are 1.5× more likely
10
+ * to complete the activation funnel's last step than low-cohort
11
+ * users. Verified via the funnelFrequency emulator.
12
+ * SCALE: 1,000 users, ~120K events, 30 days
13
+ * EVENTS (4): Browse (5) > Land (1) > Sign Up (1) > Activate (1)
14
+ * FUNNELS (1): Activation: Land → Sign Up → Activate (70%)
15
+ */
16
+
17
+ // ── HOOK STORIES ──
18
+ /*
19
+ * PATTERN: Bin users by Browse count (low 0-3, high 3+); within funnel-post,
20
+ * drop final-step `Activate` events at rates {low:0.5, high:0.95}, lifting
21
+ * the high cohort's conversion-per-step.
7
22
  *
8
- * Note: this pattern is funnel-post; it edits the funnel's own events. Combined
9
- * with the per-user funnel attempts, the breakdown should show a stronger
23
+ * Note: this pattern is funnel-post; it edits the funnel's own events.
24
+ * Combined with per-user funnel attempts, the breakdown shows a stronger
10
25
  * conversion-per-step lift in the high cohort.
26
+ *
27
+ * In funnel-post we don't have the user's full event stream — the pattern
28
+ * accepts null for `allUserEvents` and falls back to counting cohortEvent
29
+ * inside the funnel-only events. Real dungeons usually wire this via
30
+ * `everything` instead.
11
31
  */
12
32
 
13
- import dayjs from 'dayjs';
14
- import { applyFunnelFrequencyBreakdown } from '../../lib/hook-patterns/index.js';
15
-
33
+ // ── SCALE ──
16
34
  const FIXED_NOW = dayjs('2024-02-02').unix();
17
35
 
36
+ // ── CONFIG ──
18
37
  export default {
19
38
  name: 'pattern-funnel-freq',
20
39
  seed: 'phase4-funnel-freq',
@@ -23,7 +42,6 @@ export default {
23
42
  numUsers: 1_000,
24
43
  avgEventsPerUserPerDay: 4,
25
44
  percentUsersBornInDataset: 100,
26
- hasAnonIds: false,
27
45
  format: 'json',
28
46
  concurrency: 1,
29
47
  writeToDisk: true,
@@ -41,9 +59,6 @@ export default {
41
59
  hook: function (record, type, meta) {
42
60
  if (type !== 'funnel-post' || !Array.isArray(record)) return;
43
61
  if (!meta.isFirstFunnel) return;
44
- // In funnel-post we don't have the user's full event stream — pattern accepts
45
- // null for `allUserEvents` and falls back to counting cohortEvent inside the
46
- // funnel-only events. Real dungeons usually wire this via `everything` instead.
47
62
  applyFunnelFrequencyBreakdown(null, meta.profile || {}, record, {
48
63
  cohortEvent: 'Browse',
49
64
  bins: { low: [0, 3], high: [3, Infinity] },
@@ -1,15 +1,29 @@
1
- /**
2
- * Phase 4 reference dungeon — time to convert by segment.
3
- *
4
- * Trial users take 4× longer to complete the activation funnel than enterprise
5
- * users (0.5× faster). Verified via the timeToConvert emulator.
6
- */
7
-
1
+ // ── IMPORTS ──
8
2
  import dayjs from 'dayjs';
9
3
  import { applyTTCBySegment } from '../../lib/hook-patterns/index.js';
10
4
 
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: pattern-ttc-by-segment
8
+ * PURPOSE: Phase 4 reference fixture — time-to-convert by segment. Trial
9
+ * users take 4× longer to complete the activation funnel; enterprise
10
+ * 0.5× faster. Verified via the timeToConvert emulator.
11
+ * SCALE: 1,000 users, ~120K events, 30 days
12
+ * EVENTS (4): Browse (5) > Land (1) > Sign Up (1) > Activate (1)
13
+ * FUNNELS (1): Activation: Land → Sign Up → Activate (100%)
14
+ */
15
+
16
+ // ── HOOK STORIES ──
17
+ /*
18
+ * PATTERN: Scale the activation funnel's time-to-convert by user tier:
19
+ * trial users 4× slower, enterprise 0.5× faster. Verified via the
20
+ * timeToConvert emulator.
21
+ */
22
+
23
+ // ── SCALE ──
11
24
  const FIXED_NOW = dayjs('2024-02-02').unix();
12
25
 
26
+ // ── CONFIG ──
13
27
  export default {
14
28
  name: 'pattern-ttc-by-segment',
15
29
  seed: 'phase4-ttc-seg',
@@ -18,7 +32,6 @@ export default {
18
32
  numUsers: 1_000,
19
33
  avgEventsPerUserPerDay: 4,
20
34
  percentUsersBornInDataset: 100,
21
- hasAnonIds: false,
22
35
  format: 'json',
23
36
  concurrency: 1,
24
37
  writeToDisk: true,