@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,50 +1,52 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "experiments";
3
- const num_days = 60;
4
- const num_users = 2_000;
5
- const avg_events_per_user_per_day = 0.83;
6
- let token = "";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
1
+ // ── IMPORTS ──
2
+ import Chance from "chance";
3
+ import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
4
+ /** @typedef {import("../../types").Dungeon} Config */
10
5
 
11
- /**
12
- * Experiments — tests funnel experiments with A/B/C variants.
13
- *
14
- * Exercises: experiment: true for variant generation, bindPropsIndex
15
- * for property binding, and multiple funnel ordering modes
16
- * (sequential, random, first-and-last-fixed).
17
- *
18
- * - 2000 users, 100K events, 60 days
19
- * - 7 events, 4 funnels with different experiment/order configs
20
- * - No hooks
6
+ // ── OVERVIEW ──
7
+ /*
8
+ * NAME: experiments
9
+ * PURPOSE: Exercises funnel experiments (A/B/C variants), bindPropsIndex,
10
+ * and multiple funnel ordering modes (sequential, first-and-last-fixed, random).
11
+ * SCALE: 2,000 users, ~100K events, 60 days
12
+ * EVENTS (7): page view, signup, feature viewed, action taken, checkout, button click, help viewed
13
+ * FUNNELS (4): Onboarding Flow (sequential, experiment), Purchase Funnel (first-and-last-fixed, experiment),
14
+ * Feature Adoption (random, experiment, bindPropsIndex), Quick Signup (sequential, control)
21
15
  */
22
16
 
23
- import Chance from 'chance';
24
- let chance = new Chance();
25
- import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
17
+ // ── SCALE ──
18
+ const SEED = "experiments";
19
+ const NUM_DAYS = 60;
20
+ const NUM_USERS = 2_000;
21
+ const EVENTS_PER_DAY = 0.83;
22
+ const token = process.env.MP_TOKEN || "";
26
23
 
27
- /** @typedef {import("../../types").Dungeon} Config */
28
- /** @type {import('../../types').Dungeon} */
24
+ const chance = new Chance();
25
+
26
+ // ── CONFIG ──
27
+ /** @type {Config} */
29
28
  const config = {
30
- token,
31
29
  seed: SEED,
32
- numDays: num_days,
33
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
34
- numUsers: num_users,
30
+ numDays: NUM_DAYS,
31
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
32
+ numUsers: NUM_USERS,
35
33
  format: "json",
36
- region: "US",
37
- hasAnonIds: false,
38
- hasSessionIds: true,
39
- hasAdSpend: false,
40
- hasLocation: false,
41
- hasAndroidDevices: true,
42
- hasIOSDevices: true,
43
- hasDesktopDevices: true,
44
- hasBrowser: true,
45
- hasCampaigns: false,
46
- isAnonymous: false,
47
- alsoInferFunnels: false,
34
+ credentials: {
35
+ token,
36
+ region: "US",
37
+ },
38
+ switches: {
39
+ hasSessionIds: true,
40
+ hasAdSpend: false,
41
+ hasLocation: false,
42
+ hasAndroidDevices: true,
43
+ hasIOSDevices: true,
44
+ hasDesktopDevices: true,
45
+ hasBrowser: true,
46
+ hasCampaigns: false,
47
+ isAnonymous: false,
48
+ alsoInferFunnels: false,
49
+ },
48
50
  concurrency: 1,
49
51
  writeToDisk: false,
50
52
 
@@ -1,63 +1,39 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "dm4-foobar";
3
- const num_days = 30;
4
- const num_users = 4_000_000;
5
- const avg_events_per_user_per_day = 16.67;
6
- let token = "";
1
+ // ── IMPORTS ──
2
+ /** @typedef {import("../../types").Dungeon} Config */
7
3
 
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
4
+ // ── OVERVIEW ──
5
+ /*
6
+ * NAME: foobar
7
+ * PURPOSE: minimal stress/sanity dungeon — abstract event names, weight-driven distribution, no funnels, no event properties
8
+ * SCALE: 4,000,000 users, ~2B events, 30 days (configurable for stress testing)
9
+ * EVENTS (10): foo, bar, baz, qux, garply, durtle, linny, fonk, crumn, yak
10
+ * FUNNELS (0): none
11
+ */
10
12
 
11
- /**
12
- * ═══════════════════════════════════════════════════════════════
13
- * DATASET OVERVIEW
14
- * ═══════════════════════════════════════════════════════════════
15
- *
16
- * Foobar — minimal test/sanity dungeon with abstract event names.
17
- * - Configurable scale (default 2 billion events for stress testing)
18
- * - 10 events: foo, bar, baz, qux, garply, durtle, linny, fonk, crumn, yak
19
- * - Super props: string, number, boolean, date
20
- * - No funnels, no properties on events — pure weight-driven distribution
21
- *
22
- * ═══════════════════════════════════════════════════════════════
23
- * ANALYTICS HOOKS (2 patterns)
24
- * ═══════════════════════════════════════════════════════════════
25
- *
13
+ // ── HOOK STORIES ──
14
+ /*
26
15
  * 1. TEMPERATURE TAGGING (event hook)
27
16
  * foo/bar/baz = "hot", crumn/yak = "cold", everything else = "warm".
28
- *
29
- * Mixpanel Report:
30
- * - Insights: any event, total events, breakdown by temperature
31
- * Expected: "hot" dominates (highest-weight events)
17
+ * Mixpanel report: Insights, any event, total events, breakdown by temperature.
18
+ * Expected: "hot" dominates (highest-weight events).
32
19
  *
33
20
  * 2. HASH-BASED POWER USERS (everything hook)
34
- * ~10% of users (by distinct_id hash) get 3 extra duplicate events.
35
- *
36
- * Mixpanel Report:
37
- * - Insights: any event, total per user, breakdown by is_duplicate
38
- * Expected: ~10% of users have extra events tagged is_duplicate=true
21
+ * ~10% of users (by distinct_id charCode mod 10) get 3 extra duplicate events
22
+ * tagged is_duplicate=true.
23
+ * Mixpanel report: Insights, any event, total per user, breakdown by is_duplicate.
24
+ * Expected: ~10% of users have extra events tagged is_duplicate=true.
39
25
  */
40
26
 
27
+ // ── SCALE ──
28
+ const SEED = "dm4-foobar";
29
+ const NUM_DAYS = 30;
30
+ const NUM_USERS = 4_000_000;
31
+ const EVENTS_PER_DAY = 16.67;
32
+ const token = process.env.MP_TOKEN || "";
41
33
 
42
-
43
-
44
- function integer(min = 1, max = 100) {
45
- // If min equals max, just return the value
46
- if (min === max) {
47
- return min;
48
- }
49
-
50
- // Swap min and max if min is greater than max
51
- if (min > max) {
52
- [min, max] = [max, min];
53
- }
54
-
55
- // Generate a random integer between min and max (inclusive)
56
- return Math.floor(Math.random() * (max - min + 1)) + min;
57
- }
58
-
59
- /** @typedef {import("../../types").Dungeon} Config */
60
-
34
+ // ── DATA ARRAYS ──
35
+ const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
36
+ const spiritAnimals = ["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"];
61
37
  const dates = [
62
38
  "2024-09-18T21:48:28.173Z",
63
39
  "2024-12-09T03:17:46.661Z",
@@ -161,26 +137,88 @@ const dates = [
161
137
  "2024-06-03T06:52:21.652Z"
162
138
  ];
163
139
 
164
- /** @type {import('../../types').Dungeon} */
140
+ // ── HELPER FUNCTIONS ──
141
+ function integer(min = 1, max = 100) {
142
+ // If min equals max, just return the value
143
+ if (min === max) {
144
+ return min;
145
+ }
146
+
147
+ // Swap min and max if min is greater than max
148
+ if (min > max) {
149
+ [min, max] = [max, min];
150
+ }
151
+
152
+ // Generate a random integer between min and max (inclusive)
153
+ return Math.floor(Math.random() * (max - min + 1)) + min;
154
+ }
155
+
156
+ function handleEventHook(record) {
157
+ // H1: temperature tagging — high-weight events "hot", low-weight "cold", rest "warm"
158
+ const hotEvents = ["foo", "bar", "baz"];
159
+ const coldEvents = ["crumn", "yak"];
160
+ if (hotEvents.includes(record.event)) {
161
+ record.temperature = "hot";
162
+ } else if (coldEvents.includes(record.event)) {
163
+ record.temperature = "cold";
164
+ } else {
165
+ record.temperature = "warm";
166
+ }
167
+ return record;
168
+ }
169
+
170
+ function handleEverythingHook(record, meta) {
171
+ // stamp profile-derived superProps for consistency
172
+ const profile = meta.profile;
173
+ record.forEach(e => {
174
+ e.string = profile.string;
175
+ e.number = profile.number;
176
+ e.boolean = profile.boolean;
177
+ e.date = profile.date;
178
+ e.temperature = profile.temperature;
179
+ });
180
+
181
+ // H2: hash-based power users — ~10% of users (distinct_id charCode mod 10) get 3 extra duplicate events
182
+ if (record.length > 0) {
183
+ const userId = record[0].user_id || record[0].distinct_id || "";
184
+ if (userId && userId.charCodeAt(0) % 10 === 0) {
185
+ const extras = record.slice(0, 3).map(e => ({
186
+ ...e,
187
+ event: e.event,
188
+ user_id: e.user_id,
189
+ time: e.time,
190
+ is_duplicate: true,
191
+ }));
192
+ return record.concat(extras);
193
+ }
194
+ }
195
+ return record;
196
+ }
197
+
198
+ // ── CONFIG ──
199
+ /** @type {Config} */
165
200
  const config = {
166
- token,
167
201
  seed: SEED,
168
- numDays: num_days,
169
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
170
- numUsers: num_users,
202
+ numDays: NUM_DAYS,
203
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
204
+ numUsers: NUM_USERS,
171
205
  format: 'json', //csv or json
172
- region: "US",
173
- hasAnonIds: false, //if true, anonymousIds are created for each user
174
- hasSessionIds: false, //if true, hasSessionIds are created for each user
175
- hasAdSpend: false,
176
- hasAvatar: false,
177
- hasBrowser: false,
178
- hasCampaigns: false,
179
- hasIOSDevices: false,
180
- hasLocation: false,
181
- isAnonymous: true,
182
- hasAndroidDevices: false,
183
- hasDesktopDevices: false,
206
+ credentials: {
207
+ token,
208
+ region: "US",
209
+ },
210
+ switches: {
211
+ hasSessionIds: false, //if true, hasSessionIds are created for each user
212
+ hasAdSpend: false,
213
+ hasAvatar: false,
214
+ hasBrowser: false,
215
+ hasCampaigns: false,
216
+ hasIOSDevices: false,
217
+ hasLocation: false,
218
+ isAnonymous: true,
219
+ hasAndroidDevices: false,
220
+ hasDesktopDevices: false,
221
+ },
184
222
  writeToDisk: false,
185
223
  concurrency: 1,
186
224
 
@@ -237,7 +275,7 @@ const config = {
237
275
  }
238
276
  ],
239
277
  superProps: {
240
- string: ["red", "orange", "yellow", "green", "blue", "indigo", "violet"],
278
+ string: colors,
241
279
  number: integer,
242
280
  boolean: [true, false],
243
281
  date: dates,
@@ -245,9 +283,9 @@ const config = {
245
283
  },
246
284
  userProps: {
247
285
  luckyNumber: integer,
248
- 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"],
286
+ spiritAnimal: spiritAnimals,
249
287
  created: dates,
250
- string: ["red", "orange", "yellow", "green", "blue", "indigo", "violet"],
288
+ string: colors,
251
289
  number: integer,
252
290
  boolean: [true, false],
253
291
  date: dates,
@@ -257,56 +295,14 @@ const config = {
257
295
  scdProps: {},
258
296
  mirrorProps: {},
259
297
  lookupTables: [],
260
- groupKeys: [
261
- ],
262
- groupProps: {
263
- },
298
+ groupKeys: [],
299
+ groupProps: {},
264
300
 
265
301
  hook: function (record, type, meta) {
266
- // event hook: high-weight events get a "hot" flag, low-weight get "cold"
267
- if (type === "event") {
268
- const hotEvents = ["foo", "bar", "baz"];
269
- const coldEvents = ["crumn", "yak"];
270
- if (hotEvents.includes(record.event)) {
271
- record.temperature = "hot";
272
- } else if (coldEvents.includes(record.event)) {
273
- record.temperature = "cold";
274
- } else {
275
- record.temperature = "warm";
276
- }
277
- }
278
-
279
- // everything hook: hash-based cohort — 10% of users (by distinct_id) get doubled events
280
- if (type === "everything") {
281
- const profile = meta.profile;
282
- record.forEach(e => {
283
- e.string = profile.string;
284
- e.number = profile.number;
285
- e.boolean = profile.boolean;
286
- e.date = profile.date;
287
- e.temperature = profile.temperature;
288
- });
289
- if (record.length > 0) {
290
- const userId = record[0].user_id || record[0].distinct_id || "";
291
- if (userId && userId.charCodeAt(0) % 10 === 0) {
292
- // power user: duplicate each event with a slight time offset
293
- const extras = record.slice(0, 3).map(e => ({
294
- ...e,
295
- event: e.event,
296
- user_id: e.user_id,
297
- time: e.time,
298
- is_duplicate: true,
299
- }));
300
- return record.concat(extras);
301
- }
302
- }
303
- return record;
304
- }
305
-
302
+ if (type === "event") return handleEventHook(record);
303
+ if (type === "everything") return handleEverythingHook(record, meta);
306
304
  return record;
307
305
  }
308
306
  };
309
307
 
310
-
311
-
312
- export default config;
308
+ export default config;
@@ -1,50 +1,52 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "group-analytics";
3
- const num_days = 60;
4
- const num_users = 500;
5
- const avg_events_per_user_per_day = 1;
6
- let token = "";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
1
+ // ── IMPORTS ──
2
+ import Chance from 'chance';
3
+ import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
4
+ /** @typedef {import("../../types").Dungeon} Config */
10
5
 
11
- /**
12
- * Group Analytics — tests multiple group keys with varied entity counts.
13
- *
14
- * Exercises: 3 group keys (company, team, project) at different scales,
15
- * each with distinct event associations and group-level properties.
16
- *
17
- * - 500 users, 30K events, 60 days
18
- * - 3 group keys: company_id (50), team_id (200), project_id (500)
19
- * - groupProps with varied property types per key
20
- * - No hooks
6
+ // ── OVERVIEW ──
7
+ /*
8
+ * NAME: group-analytics
9
+ * PURPOSE: exercises 3 groupKeys (company/team/project) at varied scales with per-key groupProps
10
+ * SCALE: 500 users, ~30K events, 60 days
11
+ * EVENTS (8): feature used, subscription change, support ticket, task completed,
12
+ * meeting scheduled, code committed, deployment run, login
13
+ * FUNNELS (0): none
14
+ * GROUPS: company_id (50), team_id (200), project_id (500)
21
15
  */
22
16
 
23
- import Chance from 'chance';
24
- let chance = new Chance();
25
- import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
17
+ // ── SCALE ──
18
+ const SEED = "group-analytics";
19
+ const NUM_DAYS = 60;
20
+ const NUM_USERS = 500;
21
+ const EVENTS_PER_DAY = 1;
22
+ const token = process.env.MP_TOKEN || "";
26
23
 
27
- /** @typedef {import("../../types").Dungeon} Config */
28
- /** @type {import('../../types').Dungeon} */
24
+ const chance = new Chance();
25
+
26
+ // ── CONFIG ──
27
+ /** @type {Config} */
29
28
  const config = {
30
- token,
31
29
  seed: SEED,
32
- numDays: num_days,
33
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
34
- numUsers: num_users,
30
+ numDays: NUM_DAYS,
31
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
32
+ numUsers: NUM_USERS,
35
33
  format: "json",
36
- region: "US",
37
- hasAnonIds: false,
38
- hasSessionIds: false,
39
- hasAdSpend: false,
40
- hasLocation: false,
41
- hasAndroidDevices: false,
42
- hasIOSDevices: false,
43
- hasDesktopDevices: true,
44
- hasBrowser: true,
45
- hasCampaigns: false,
46
- isAnonymous: false,
47
- alsoInferFunnels: false,
34
+ credentials: {
35
+ token,
36
+ region: "US",
37
+ },
38
+ switches: {
39
+ hasSessionIds: false,
40
+ hasAdSpend: false,
41
+ hasLocation: false,
42
+ hasAndroidDevices: false,
43
+ hasIOSDevices: false,
44
+ hasDesktopDevices: true,
45
+ hasBrowser: true,
46
+ hasCampaigns: false,
47
+ isAnonymous: false,
48
+ alsoInferFunnels: false,
49
+ },
48
50
  concurrency: 1,
49
51
  writeToDisk: false,
50
52
 
@@ -1,28 +1,40 @@
1
- /**
2
- * Phase 3 hook-helpers verification fixture.
1
+ // ── IMPORTS ──
2
+ import dayjs from 'dayjs';
3
+ import {
4
+ binUsersByEventCount,
5
+ scalePropertyValue,
6
+ injectBurst,
7
+ cloneEvent,
8
+ } from '../../lib/hook-helpers/index.js';
9
+ /** @typedef {import("../../types").Dungeon} Config */
10
+
11
+ // ── OVERVIEW ──
12
+ /*
13
+ * NAME: hook-helpers-verify
14
+ * PURPOSE: Phase 3 fixture — exercises binUsersByEventCount + scalePropertyValue + injectBurst atoms
15
+ * SCALE: 1,000 users, ~120K events, 30 days
16
+ * EVENTS (2): Browse, Purchase
17
+ * FUNNELS (0): none
3
18
  *
4
- * Exercises three atoms inside one `everything` hook:
19
+ * Verified by DuckDB queries against the events output (see
20
+ * scripts/verify-runner.mjs + the Phase 3 verification gate notes in the plan).
21
+ */
22
+
23
+ // ── HOOK STORIES ──
24
+ /*
25
+ * Inside one `everything` hook:
5
26
  * 1. `binUsersByEventCount` — classify users into "casual" / "engaged" / "power"
6
27
  * based on their count of `Browse` events.
7
28
  * 2. `scalePropertyValue` — engaged users get 2x `amount` on Purchase events;
8
29
  * power users get 3x.
9
30
  * 3. `injectBurst` — power users also get a burst of 5 cloned `Browse` events
10
31
  * around the midpoint of their stream.
11
- *
12
- * Verified by DuckDB queries against the events output (see
13
- * scripts/verify-runner.mjs + the Phase 3 verification gate notes in the plan).
14
32
  */
15
33
 
16
- import dayjs from 'dayjs';
17
- import {
18
- binUsersByEventCount,
19
- scalePropertyValue,
20
- injectBurst,
21
- cloneEvent,
22
- } from '../../lib/hook-helpers/index.js';
23
-
34
+ // ── SCALE ──
24
35
  const FIXED_NOW = dayjs('2024-02-02').unix();
25
36
 
37
+ // ── KNOBS (tweak these to reshape stories) ──
26
38
  const BINS = {
27
39
  casual: [0, 5],
28
40
  engaged: [5, 15],
@@ -32,7 +44,46 @@ const BINS = {
32
44
  const SCALE_BY_BIN = { casual: 1, engaged: 2, power: 3 };
33
45
  const BURST_COUNT_BY_BIN = { casual: 0, engaged: 0, power: 5 };
34
46
 
35
- export default {
47
+ // ── HELPER FUNCTIONS ──
48
+ function handleEverythingHooks(record, meta) {
49
+ // 1. Classify the user via cohort atom
50
+ const bin = binUsersByEventCount(record, 'Browse', BINS);
51
+ if (!bin) return record;
52
+
53
+ // Stamp the bin onto each Purchase so verification queries can group by it
54
+ // without re-deriving. Non-flag-stamping: this property is config-defined when
55
+ // dungeons add `cohort` to Purchase.properties; here we accept the small bend
56
+ // for the verification fixture so DuckDB queries are clean.
57
+ for (const ev of record) {
58
+ if (ev.event === 'Purchase') ev.cohort = bin;
59
+ }
60
+
61
+ // 2. Scale Purchase amounts via mutate atom (engaged 2x, power 3x)
62
+ const factor = SCALE_BY_BIN[bin] || 1;
63
+ if (factor !== 1) {
64
+ scalePropertyValue(record, e => e.event === 'Purchase', 'amount', factor);
65
+ }
66
+
67
+ // 3. Power users get a Browse burst clustered at the dataset midpoint
68
+ const burstCount = BURST_COUNT_BY_BIN[bin] || 0;
69
+ if (burstCount > 0 && record.length) {
70
+ const midUnixMs = (meta.datasetStart + (meta.datasetEnd - meta.datasetStart) / 2) * 1000;
71
+ // Use the user's first Browse as a clone template so we honor the schema.
72
+ const template = record.find(e => e.event === 'Browse');
73
+ if (template) {
74
+ const tpl = cloneEvent(template, { cohort: bin });
75
+ injectBurst(record, tpl, burstCount, midUnixMs, 60 * 60 * 1000); // ±1h spread
76
+ }
77
+ }
78
+
79
+ // Re-sort by time after burst injection so downstream pipeline sees ordered events.
80
+ record.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0));
81
+ return record;
82
+ }
83
+
84
+ // ── CONFIG ──
85
+ /** @type {Config} */
86
+ const config = {
36
87
  name: 'hook-helpers-verify',
37
88
  seed: 'phase3-helpers-verify',
38
89
  datasetStart: FIXED_NOW - 30 * 86400,
@@ -40,7 +91,6 @@ export default {
40
91
  numUsers: 1_000,
41
92
  avgEventsPerUserPerDay: 4,
42
93
  percentUsersBornInDataset: 30,
43
- hasAnonIds: false,
44
94
  format: 'json',
45
95
  concurrency: 1,
46
96
  writeToDisk: true,
@@ -51,39 +101,8 @@ export default {
51
101
  ],
52
102
  hook: function (record, type, meta) {
53
103
  if (type !== 'everything' || !Array.isArray(record)) return record;
54
-
55
- // 1. Classify the user via cohort atom
56
- const bin = binUsersByEventCount(record, 'Browse', BINS);
57
- if (!bin) return record;
58
-
59
- // Stamp the bin onto each Purchase so verification queries can group by it
60
- // without re-deriving. Non-flag-stamping: this property is config-defined when
61
- // dungeons add `cohort` to Purchase.properties; here we accept the small bend
62
- // for the verification fixture so DuckDB queries are clean.
63
- for (const ev of record) {
64
- if (ev.event === 'Purchase') ev.cohort = bin;
65
- }
66
-
67
- // 2. Scale Purchase amounts via mutate atom (engaged 2x, power 3x)
68
- const factor = SCALE_BY_BIN[bin] || 1;
69
- if (factor !== 1) {
70
- scalePropertyValue(record, e => e.event === 'Purchase', 'amount', factor);
71
- }
72
-
73
- // 3. Power users get a Browse burst clustered at the dataset midpoint
74
- const burstCount = BURST_COUNT_BY_BIN[bin] || 0;
75
- if (burstCount > 0 && record.length) {
76
- const midUnixMs = (meta.datasetStart + (meta.datasetEnd - meta.datasetStart) / 2) * 1000;
77
- // Use the user's first Browse as a clone template so we honor the schema.
78
- const template = record.find(e => e.event === 'Browse');
79
- if (template) {
80
- const tpl = cloneEvent(template, { cohort: bin });
81
- injectBurst(record, tpl, burstCount, midUnixMs, 60 * 60 * 1000); // ±1h spread
82
- }
83
- }
84
-
85
- // Re-sort by time after burst injection so downstream pipeline sees ordered events.
86
- record.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0));
87
- return record;
104
+ return handleEverythingHooks(record, meta);
88
105
  },
89
106
  };
107
+
108
+ export default config;
@@ -1,16 +1,23 @@
1
- /**
2
- * Phase 2 identity model — verification fixture (intentionally simple).
3
- *
4
- * 1K born-in-dataset users with isAuthEvent on `Sign Up` (the stitch step) and a
5
- * 3-step first funnel. Dataset window pinned for determinism. avgDevicePerUser:2
6
- * exercises the multi-device sticky-per-session model. Used by phase 2 verification
7
- * gate (DuckDB queries against the events output).
8
- */
9
-
1
+ // ── IMPORTS ──
10
2
  import dayjs from 'dayjs';
11
3
 
4
+ // ── OVERVIEW ──
5
+ /*
6
+ * NAME: identity-model-verify
7
+ * PURPOSE: Phase 2 identity model verification fixture (intentionally simple).
8
+ * 1K born-in-dataset users with isAuthEvent on `Sign Up` (the stitch
9
+ * step) and a 3-step first funnel. avgDevicePerUser:2 exercises the
10
+ * multi-device sticky-per-session model. Used by the phase 2
11
+ * verification gate (DuckDB queries against the events output).
12
+ * SCALE: 1,000 users, ~90K events, 30 days
13
+ * EVENTS (6): Browse (5) > Save Item (2) > Land (1) > View Pricing (1) > Sign Up (1) > Onboarding (1)
14
+ * FUNNELS (1): Acquisition: Land → View Pricing → Sign Up → Onboarding (80%, attempts 0-2)
15
+ */
16
+
17
+ // ── SCALE ──
12
18
  const FIXED_NOW = dayjs('2024-02-02').unix();
13
19
 
20
+ // ── CONFIG ──
14
21
  export default {
15
22
  name: 'identity-model-verify',
16
23
  seed: 'phase2-identity-verify',
@@ -19,9 +26,12 @@ export default {
19
26
  numUsers: 1_000,
20
27
  avgEventsPerUserPerDay: 3,
21
28
  percentUsersBornInDataset: 100,
22
- hasAnonIds: true,
23
- avgDevicePerUser: 2,
24
- hasSessionIds: true,
29
+ switches: {
30
+ hasSessionIds: true,
31
+ },
32
+ identity: {
33
+ avgDevicePerUser: 2,
34
+ },
25
35
  format: 'json',
26
36
  concurrency: 1,
27
37
  writeToDisk: true,