@ak--47/dungeon-master 1.4.5 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) 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 +182 -0
  9. package/HOOKS.md +1256 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +41 -49
  12. package/dungeons/technical/anonymous-users.js +38 -36
  13. package/dungeons/technical/array-of-object-lookup.js +136 -153
  14. package/dungeons/technical/datagen-v15-verify.js +87 -0
  15. package/dungeons/technical/experiments.js +42 -40
  16. package/dungeons/technical/foobar.js +114 -118
  17. package/dungeons/technical/group-analytics.js +42 -40
  18. package/dungeons/technical/hook-helpers-verify.js +69 -50
  19. package/dungeons/technical/identity-model-verify.js +22 -12
  20. package/dungeons/technical/mirror-strategies.js +37 -39
  21. package/dungeons/technical/nested-objects.js +119 -118
  22. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  23. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  24. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  25. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  26. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  27. package/dungeons/technical/retention-cadence.js +115 -112
  28. package/dungeons/technical/sanity.js +86 -80
  29. package/dungeons/technical/scale-test.js +34 -38
  30. package/dungeons/technical/scd.js +111 -128
  31. package/dungeons/technical/simple.js +134 -141
  32. package/dungeons/technical/simplest.js +111 -65
  33. package/dungeons/technical/text-generation.js +110 -146
  34. package/dungeons/vertical/ai-platform.js +300 -333
  35. package/dungeons/vertical/community.js +290 -255
  36. package/dungeons/vertical/crypto.js +400 -391
  37. package/dungeons/vertical/dating.js +421 -375
  38. package/dungeons/vertical/devtools.js +346 -298
  39. package/dungeons/vertical/ecommerce.js +322 -394
  40. package/dungeons/vertical/education.js +380 -325
  41. package/dungeons/vertical/fintech.js +371 -325
  42. package/dungeons/vertical/fitness.js +345 -291
  43. package/dungeons/vertical/food-delivery.js +352 -307
  44. package/dungeons/vertical/gaming.js +490 -444
  45. package/dungeons/vertical/healthcare.js +311 -262
  46. package/dungeons/vertical/insurance-application.js +437 -409
  47. package/dungeons/vertical/logistics.js +278 -252
  48. package/dungeons/vertical/marketplace.js +340 -323
  49. package/dungeons/vertical/media.js +390 -335
  50. package/dungeons/vertical/real-estate.js +402 -347
  51. package/dungeons/vertical/sass.js +331 -333
  52. package/dungeons/vertical/social.js +377 -316
  53. package/dungeons/vertical/travel.js +302 -295
  54. package/index.js +64 -7
  55. package/lib/core/config-validator.js +378 -17
  56. package/lib/core/dungeon-loader.js +2 -5
  57. package/lib/generators/events.js +12 -13
  58. package/lib/generators/funnels.js +76 -2
  59. package/lib/hook-helpers/index.js +1 -0
  60. package/lib/hook-helpers/inject.js +95 -0
  61. package/lib/orchestrators/mixpanel-sender.js +7 -0
  62. package/lib/orchestrators/user-loop.js +598 -48
  63. package/lib/templates/defaults.js +59 -59
  64. package/lib/templates/macro-presets.js +53 -11
  65. package/lib/utils/dataset-context.js +103 -0
  66. package/lib/utils/retention-curve.js +140 -0
  67. package/lib/utils/utils.js +157 -109
  68. package/lib/verify/counting.js +360 -0
  69. package/lib/verify/emulate-breakdown.js +531 -108
  70. package/lib/verify/funnel-engine.js +539 -0
  71. package/lib/verify/identity.js +78 -0
  72. package/lib/verify/index.js +20 -0
  73. package/lib/verify/schema-validator.js +3 -1
  74. package/lib/verify/verify-dungeon.js +58 -0
  75. package/package.json +14 -3
  76. package/scripts/run-dungeon.mjs +12 -1
  77. package/types.d.ts +353 -4
  78. package/scripts/smoke-test-all.mjs +0 -162
@@ -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 = "your-mixpanel-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 = "your-mixpanel-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,
@@ -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 = "your-mixpanel-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