@ak--47/dungeon-master 1.5.0 → 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 (62) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/HOOKS.md +13 -0
  3. package/dungeons/technical/ad-spend.js +41 -49
  4. package/dungeons/technical/anonymous-users.js +38 -36
  5. package/dungeons/technical/array-of-object-lookup.js +136 -153
  6. package/dungeons/technical/datagen-v15-verify.js +24 -11
  7. package/dungeons/technical/experiments.js +42 -40
  8. package/dungeons/technical/foobar.js +114 -118
  9. package/dungeons/technical/group-analytics.js +42 -40
  10. package/dungeons/technical/hook-helpers-verify.js +69 -50
  11. package/dungeons/technical/identity-model-verify.js +22 -12
  12. package/dungeons/technical/mirror-strategies.js +37 -39
  13. package/dungeons/technical/nested-objects.js +119 -118
  14. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  15. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  16. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  17. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  18. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  19. package/dungeons/technical/retention-cadence.js +115 -112
  20. package/dungeons/technical/sanity.js +86 -80
  21. package/dungeons/technical/scale-test.js +34 -38
  22. package/dungeons/technical/scd.js +111 -128
  23. package/dungeons/technical/simple.js +134 -141
  24. package/dungeons/technical/simplest.js +54 -62
  25. package/dungeons/technical/text-generation.js +110 -146
  26. package/dungeons/vertical/ai-platform.js +296 -333
  27. package/dungeons/vertical/community.js +284 -255
  28. package/dungeons/vertical/crypto.js +395 -391
  29. package/dungeons/vertical/dating.js +411 -378
  30. package/dungeons/vertical/devtools.js +336 -298
  31. package/dungeons/vertical/ecommerce.js +316 -394
  32. package/dungeons/vertical/education.js +369 -325
  33. package/dungeons/vertical/fintech.js +358 -325
  34. package/dungeons/vertical/fitness.js +335 -291
  35. package/dungeons/vertical/food-delivery.js +343 -307
  36. package/dungeons/vertical/gaming.js +480 -444
  37. package/dungeons/vertical/healthcare.js +306 -262
  38. package/dungeons/vertical/insurance-application.js +427 -409
  39. package/dungeons/vertical/logistics.js +271 -252
  40. package/dungeons/vertical/marketplace.js +333 -323
  41. package/dungeons/vertical/media.js +382 -335
  42. package/dungeons/vertical/real-estate.js +395 -346
  43. package/dungeons/vertical/sass.js +319 -333
  44. package/dungeons/vertical/social.js +368 -316
  45. package/dungeons/vertical/travel.js +297 -295
  46. package/index.js +46 -4
  47. package/lib/core/config-validator.js +126 -28
  48. package/lib/generators/funnels.js +4 -1
  49. package/lib/orchestrators/mixpanel-sender.js +7 -0
  50. package/lib/orchestrators/user-loop.js +132 -31
  51. package/lib/templates/defaults.js +59 -59
  52. package/lib/templates/macro-presets.js +14 -2
  53. package/lib/utils/dataset-context.js +103 -0
  54. package/lib/utils/retention-curve.js +140 -0
  55. package/lib/utils/utils.js +149 -38
  56. package/lib/verify/counting.js +40 -0
  57. package/lib/verify/emulate-breakdown.js +20 -1
  58. package/lib/verify/index.js +1 -0
  59. package/lib/verify/schema-validator.js +3 -1
  60. package/package.json +11 -2
  61. package/scripts/run-dungeon.mjs +12 -1
  62. package/types.d.ts +117 -1
@@ -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 = "";
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,