@ak--47/dungeon-master 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  All notable changes to `@ak--47/dungeon-master`.
4
4
 
5
+ ## 1.4.0 — 2026-05-03
6
+
7
+ The "identity model + hook verification" release. Users get multi-device identity, declarative experiments, and a complete hook authoring pipeline with verification. All 20 vertical dungeons upgraded and verified.
8
+
9
+ ### Added
10
+
11
+ - **Identity model.** Three additive knobs for realistic user/device identity:
12
+ - `avgDevicePerUser: N` — per-user device pool sized by normal distribution; sessions are sticky to a device drawn from the pool.
13
+ - `EventConfig.isAuthEvent: true` — marks the sign-up/login event as the identity stitch point. Pre-auth funnel steps get `device_id` only; the stitch event gets both; post-auth gets `user_id` only.
14
+ - `Funnel.attempts: { min, max, conversionRate? }` — failed-prior-attempt retries before final conversion. Failed attempts truncate before `isAuthEvent`.
15
+ - `EventConfig.isAttributionEvent: true` — opt-in UTM stamping on specific events (replaces blanket 25% of all events).
16
+ - **Declarative experiments.** `Funnel.experiment: { name, variants, startDaysBeforeEnd }` on any funnel. Engine handles variant assignment (deterministic hash), `$experiment_started` events, and conversion/TTC multipliers. Hooks read `meta.experiment` for variant-specific downstream effects.
17
+ - **Hook helpers** (`@ak--47/dungeon-master/hook-helpers`). 14 composable atoms across 5 modules: cohort binning, event mutation, timing manipulation, event injection, and identity partitioning. Full JSDoc on each atom.
18
+ - **Hook patterns** (`@ak--47/dungeon-master/hook-patterns`). 5 high-level recipes mapping 1:1 to Mixpanel analysis types: frequency-by-frequency, funnel-frequency breakdown, aggregate-by-bin, TTC-by-segment, attributed-by-source.
19
+ - **Verification pipeline** (`@ak--47/dungeon-master/verify`). `emulateBreakdown` re-derives Mixpanel breakdown tables from raw events. `verifyDungeon` runs CI-style assertions with NAILED/STRONG/WEAK/NONE/INVERSE scoring.
20
+ - **`HOOKS.md` encyclopedia.** 23 production-proven hook recipes with code, Mixpanel report instructions, and adaptation notes. 14 core principles. Phase 3/4 atom and pattern reference tables.
21
+ - **`EmulateOptions` type.** Previously referenced but undefined in `types.d.ts`. Now fully typed with per-analysis-type field documentation.
22
+ - **Skills pipeline.** `/create-dungeon` (schema only) → `/write-hooks` (engineer patterns) → `/verify-hooks` (DuckDB verification with 5-tier scoring). Each skill is self-contained with reference examples.
23
+
24
+ ### Changed
25
+
26
+ - **Legacy 42% per-event `user_id` dice removed.** Every event now gets `user_id` by default (unless in a pre-auth funnel step). More correct for Mixpanel identity.
27
+ - **`isStrictEvent` events excluded from standalone generation.** They appear only in explicitly-defined funnels — cleaner data for events like "application approved" that shouldn't exist outside funnel context.
28
+ - **`insert_id` uses `crypto.randomUUID()`.** Eliminates hash collisions. Non-deterministic but irrelevant for analytics.
29
+ - **Funnel-pre hooks have final authority.** Persona and world-event modifiers apply before the hook — hooks can override everything.
30
+ - **Experiment variant assignment is deterministic per user** (hash-based, not random per funnel run).
31
+ - **All 20 vertical dungeons upgraded to version 2.** Identity model adopted, dates standardized (120-day window), temporal hooks migrated to `everything`, deprecated features replaced in hooks, all hooks verified STRONG or NAILED.
32
+ - **`types.d.ts` expanded.** JSDoc warnings on `HookMetaEvent` (temporal unreliability), hook ordering note on `HookMetaEverything`, `numDays` 3-mode resolution docs, `isStrictEvent` clarification.
33
+
34
+ ### Removed (silently ignored)
35
+
36
+ - `subscription`, `attribution`, `geo`, `features`, `anomalies` config keys. Engine strips them with one deprecation warning per dungeon and continues. Recreate these patterns via hooks — see `HOOKS.md` recipes 3.22 (deprecated feature replacement) and the `write-hooks` skill.
37
+
38
+ ### Key Learnings from 20-Dungeon Eval
39
+
40
+ These patterns are documented in `HOOKS.md` principles 7-14 and the skill REV 10 sections:
41
+
42
+ 1. **Temporal hooks belong in `everything`, not `event`.** The event hook's `meta.datasetStart`/`meta.datasetEnd` are in a different time frame than `record.time`. Move any day-in-dataset check to `everything`.
43
+ 2. **Temporal mutations run AFTER all cloning.** If Hook A clones events into a time window and Hook B mutates events in that window, B must run after A or clones miss the mutation.
44
+ 3. **Cohort detection must survive downstream filtering.** If a churn hook removes the marker events used to detect a cohort, require 3+ markers instead of 1+ so survivors still identify the group.
45
+ 4. **Deprecated feature replacement.** Add equivalent property assignments in `user`/`everything` hooks and `superProps`/`userProps`. See recipe 3.22.
46
+ 5. **Unseeded Chance breaks determinism.** Replace `new Chance()` with `initChance(SEED)`.
47
+
48
+ ### Documentation
49
+
50
+ - **`research/FINALIZE-REPORT.md`** — full eval report: 19 dungeons, 191 hooks, all STRONG/NAILED.
51
+ - **`research/1.4.0-upgrade-guide.md`** — migration checklist and hook-writing guide.
52
+ - **`HOOKS.md`** — 23 recipes, 14 principles, atom/pattern reference.
53
+ - **`.claude/skills/verify-hooks/SKILL.md`** — REV 10: clone dilution, cohort filtering, deprecated features, dynamic date window derivation.
54
+ - **`.claude/skills/write-hooks/SKILL.md`** — REV 10: hook ordering within `everything`, deprecated feature replacement, cohort sizing guidelines.
55
+
56
+ ### Backward Compatibility
57
+
58
+ - **No breaking changes to the public API.** `DUNGEON_MASTER(config)` signature unchanged. All named exports unchanged.
59
+ - Existing dungeons run without modification. New features are opt-in.
60
+ - Dungeons that relied on the legacy 42% `user_id` dice will see more consistent identity (every event gets `user_id`). This is more correct for Mixpanel.
61
+ - `numDays`-only configs continue to work (window anchors to today). Pin `datasetStart`+`datasetEnd` for deterministic runs.
62
+
5
63
  ## 1.3.0 — 2026-04-28
6
64
 
7
65
  The "no more end-of-dataset blowup" release. Rewrote how per-user event budgets and big-picture trends are decided so the default chart looks flat with a weekly cycle, instead of meteoric ramp + cliff in the final ~14 days.
@@ -147,8 +147,6 @@ const config = {
147
147
  }],
148
148
  hook: function (record, type, meta) {
149
149
 
150
- const NOW = dayjs();
151
-
152
150
  if (type === "event") {
153
151
  // Pattern 1: Checkouts with coupons get a discount_applied flag and adjusted total
154
152
  if (record.event === "checkout" && record.coupon && record.coupon !== "none") {
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Phase 3 hook-helpers verification fixture.
3
+ *
4
+ * Exercises three atoms inside one `everything` hook:
5
+ * 1. `binUsersByEventCount` — classify users into "casual" / "engaged" / "power"
6
+ * based on their count of `Browse` events.
7
+ * 2. `scalePropertyValue` — engaged users get 2x `amount` on Purchase events;
8
+ * power users get 3x.
9
+ * 3. `injectBurst` — power users also get a burst of 5 cloned `Browse` events
10
+ * 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
+ */
15
+
16
+ import dayjs from 'dayjs';
17
+ import {
18
+ binUsersByEventCount,
19
+ scalePropertyValue,
20
+ injectBurst,
21
+ cloneEvent,
22
+ } from '../../lib/hook-helpers/index.js';
23
+
24
+ const FIXED_NOW = dayjs('2024-02-02').unix();
25
+
26
+ const BINS = {
27
+ casual: [0, 5],
28
+ engaged: [5, 15],
29
+ power: [15, Infinity],
30
+ };
31
+
32
+ const SCALE_BY_BIN = { casual: 1, engaged: 2, power: 3 };
33
+ const BURST_COUNT_BY_BIN = { casual: 0, engaged: 0, power: 5 };
34
+
35
+ export default {
36
+ name: 'hook-helpers-verify',
37
+ seed: 'phase3-helpers-verify',
38
+ datasetStart: FIXED_NOW - 30 * 86400,
39
+ datasetEnd: FIXED_NOW,
40
+ numUsers: 1_000,
41
+ avgEventsPerUserPerDay: 4,
42
+ percentUsersBornInDataset: 30,
43
+ hasAnonIds: false,
44
+ format: 'json',
45
+ concurrency: 1,
46
+ writeToDisk: true,
47
+ verbose: false,
48
+ events: [
49
+ { event: 'Browse', weight: 8, properties: { surface: ['home', 'category', 'search'] } },
50
+ { event: 'Purchase', weight: 2, properties: { amount: [10, 20, 30, 40, 50] } },
51
+ ],
52
+ hook: function (record, type, meta) {
53
+ 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;
88
+ },
89
+ };
@@ -0,0 +1,47 @@
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
+
10
+ import dayjs from 'dayjs';
11
+
12
+ const FIXED_NOW = dayjs('2024-02-02').unix();
13
+
14
+ export default {
15
+ name: 'identity-model-verify',
16
+ seed: 'phase2-identity-verify',
17
+ datasetStart: FIXED_NOW - 30 * 86400,
18
+ datasetEnd: FIXED_NOW,
19
+ numUsers: 1_000,
20
+ avgEventsPerUserPerDay: 3,
21
+ percentUsersBornInDataset: 100,
22
+ hasAnonIds: true,
23
+ avgDevicePerUser: 2,
24
+ hasSessionIds: true,
25
+ format: 'json',
26
+ concurrency: 1,
27
+ writeToDisk: true,
28
+ verbose: false,
29
+ events: [
30
+ { event: 'Land', isFirstEvent: true, isStrictEvent: true },
31
+ { event: 'View Pricing', isStrictEvent: true },
32
+ { event: 'Sign Up', isAuthEvent: true, isStrictEvent: true },
33
+ { event: 'Onboarding', isStrictEvent: true },
34
+ { event: 'Browse', weight: 5 },
35
+ { event: 'Save Item', weight: 2 },
36
+ ],
37
+ funnels: [
38
+ {
39
+ name: 'Acquisition',
40
+ sequence: ['Land', 'View Pricing', 'Sign Up', 'Onboarding'],
41
+ conversionRate: 80,
42
+ isFirstFunnel: true,
43
+ timeToConvert: 2,
44
+ attempts: { min: 0, max: 2 },
45
+ },
46
+ ],
47
+ };
@@ -0,0 +1,41 @@
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
+
8
+ import dayjs from 'dayjs';
9
+ import { applyAggregateByBin } from '../../lib/hook-patterns/index.js';
10
+
11
+ const FIXED_NOW = dayjs('2024-02-02').unix();
12
+
13
+ export default {
14
+ name: 'pattern-agg-by-bin',
15
+ seed: 'phase4-aggbybin',
16
+ datasetStart: FIXED_NOW - 30 * 86400,
17
+ datasetEnd: FIXED_NOW,
18
+ numUsers: 1_000,
19
+ avgEventsPerUserPerDay: 6,
20
+ percentUsersBornInDataset: 30,
21
+ hasAnonIds: false,
22
+ format: 'json',
23
+ concurrency: 1,
24
+ writeToDisk: true,
25
+ verbose: false,
26
+ events: [
27
+ { event: 'Browse', weight: 6 },
28
+ { event: 'Purchase', weight: 2, properties: { amount: [10, 20, 30] } },
29
+ ],
30
+ hook: function (record, type) {
31
+ if (type !== 'everything' || !Array.isArray(record)) return record;
32
+ applyAggregateByBin(record, null, {
33
+ cohortEvent: 'Browse',
34
+ bins: { low: [0, 5], mid: [5, 15], high: [15, Infinity] },
35
+ event: 'Purchase',
36
+ propertyName: 'amount',
37
+ deltas: { low: 1, mid: 2, high: 4 },
38
+ });
39
+ return record;
40
+ },
41
+ };
@@ -0,0 +1,42 @@
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
+
9
+ import dayjs from 'dayjs';
10
+ import { applyAttributedBySource } from '../../lib/hook-patterns/index.js';
11
+
12
+ const FIXED_NOW = dayjs('2024-02-02').unix();
13
+
14
+ export default {
15
+ name: 'pattern-attributed-by-source',
16
+ seed: 'phase4-attrib',
17
+ datasetStart: FIXED_NOW - 30 * 86400,
18
+ datasetEnd: FIXED_NOW,
19
+ numUsers: 1_000,
20
+ avgEventsPerUserPerDay: 4,
21
+ percentUsersBornInDataset: 50,
22
+ hasAnonIds: false,
23
+ format: 'json',
24
+ concurrency: 1,
25
+ writeToDisk: true,
26
+ verbose: false,
27
+ events: [
28
+ { event: 'Touch', weight: 5, properties: { source: ['google', 'facebook', 'twitter'] } },
29
+ { event: 'Convert', weight: 2, properties: { source: ['unknown'] } },
30
+ ],
31
+ hook: function (record, type) {
32
+ if (type !== 'everything' || !Array.isArray(record)) return record;
33
+ applyAttributedBySource(record, null, {
34
+ sourceEvent: 'Touch',
35
+ sourceProperty: 'source',
36
+ downstreamEvent: 'Convert',
37
+ weights: { google: 10, facebook: 5, twitter: 1 },
38
+ model: 'firstTouch',
39
+ });
40
+ return record;
41
+ },
42
+ };
@@ -0,0 +1,40 @@
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
+
8
+ import dayjs from 'dayjs';
9
+ import { applyFrequencyByFrequency } from '../../lib/hook-patterns/index.js';
10
+
11
+ const FIXED_NOW = dayjs('2024-02-02').unix();
12
+
13
+ export default {
14
+ name: 'pattern-freq-by-freq',
15
+ seed: 'phase4-freqxfreq',
16
+ datasetStart: FIXED_NOW - 30 * 86400,
17
+ datasetEnd: FIXED_NOW,
18
+ numUsers: 1_000,
19
+ avgEventsPerUserPerDay: 6,
20
+ percentUsersBornInDataset: 30,
21
+ hasAnonIds: false,
22
+ format: 'json',
23
+ concurrency: 1,
24
+ writeToDisk: true,
25
+ verbose: false,
26
+ events: [
27
+ { event: 'Browse', weight: 6 },
28
+ { event: 'Purchase', weight: 2, properties: { amount: [10, 20, 30, 40, 50] } },
29
+ ],
30
+ hook: function (record, type) {
31
+ if (type !== 'everything' || !Array.isArray(record)) return record;
32
+ applyFrequencyByFrequency(record, null, {
33
+ cohortEvent: 'Browse',
34
+ bins: { low: [0, 5], mid: [5, 15], high: [15, Infinity] },
35
+ targetEvent: 'Purchase',
36
+ multipliers: { low: 1, mid: 2, high: 3 },
37
+ });
38
+ return record;
39
+ },
40
+ };
@@ -0,0 +1,54 @@
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.
7
+ *
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
10
+ * conversion-per-step lift in the high cohort.
11
+ */
12
+
13
+ import dayjs from 'dayjs';
14
+ import { applyFunnelFrequencyBreakdown } from '../../lib/hook-patterns/index.js';
15
+
16
+ const FIXED_NOW = dayjs('2024-02-02').unix();
17
+
18
+ export default {
19
+ name: 'pattern-funnel-freq',
20
+ seed: 'phase4-funnel-freq',
21
+ datasetStart: FIXED_NOW - 30 * 86400,
22
+ datasetEnd: FIXED_NOW,
23
+ numUsers: 1_000,
24
+ avgEventsPerUserPerDay: 4,
25
+ percentUsersBornInDataset: 100,
26
+ hasAnonIds: false,
27
+ format: 'json',
28
+ concurrency: 1,
29
+ writeToDisk: true,
30
+ verbose: false,
31
+ events: [
32
+ { event: 'Land', isFirstEvent: true, isStrictEvent: true },
33
+ { event: 'Sign Up', isAuthEvent: true, isStrictEvent: true },
34
+ { event: 'Activate', isStrictEvent: true },
35
+ { event: 'Browse', weight: 5 },
36
+ ],
37
+ funnels: [{
38
+ sequence: ['Land', 'Sign Up', 'Activate'],
39
+ conversionRate: 70, isFirstFunnel: true, timeToConvert: 4,
40
+ }],
41
+ hook: function (record, type, meta) {
42
+ if (type !== 'funnel-post' || !Array.isArray(record)) return;
43
+ 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
+ applyFunnelFrequencyBreakdown(null, meta.profile || {}, record, {
48
+ cohortEvent: 'Browse',
49
+ bins: { low: [0, 3], high: [3, Infinity] },
50
+ dropMultipliers: { low: 0.5, high: 0.95 },
51
+ finalStep: 'Activate',
52
+ });
53
+ },
54
+ };
@@ -0,0 +1,45 @@
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
+
8
+ import dayjs from 'dayjs';
9
+ import { applyTTCBySegment } from '../../lib/hook-patterns/index.js';
10
+
11
+ const FIXED_NOW = dayjs('2024-02-02').unix();
12
+
13
+ export default {
14
+ name: 'pattern-ttc-by-segment',
15
+ seed: 'phase4-ttc-seg',
16
+ datasetStart: FIXED_NOW - 30 * 86400,
17
+ datasetEnd: FIXED_NOW,
18
+ numUsers: 1_000,
19
+ avgEventsPerUserPerDay: 4,
20
+ percentUsersBornInDataset: 100,
21
+ hasAnonIds: false,
22
+ format: 'json',
23
+ concurrency: 1,
24
+ writeToDisk: true,
25
+ verbose: false,
26
+ userProps: { tier: ['trial', 'trial', 'enterprise'] },
27
+ events: [
28
+ { event: 'Land', isFirstEvent: true, isStrictEvent: true },
29
+ { event: 'Sign Up', isAuthEvent: true, isStrictEvent: true },
30
+ { event: 'Activate', isStrictEvent: true },
31
+ { event: 'Browse', weight: 5 },
32
+ ],
33
+ funnels: [{
34
+ sequence: ['Land', 'Sign Up', 'Activate'],
35
+ conversionRate: 100, isFirstFunnel: true, timeToConvert: 4,
36
+ }],
37
+ hook: function (record, type, meta) {
38
+ if (type !== 'funnel-post' || !Array.isArray(record)) return;
39
+ if (!meta.isFirstFunnel) return;
40
+ applyTTCBySegment(record, meta.profile || {}, {
41
+ segmentKey: 'tier',
42
+ factors: { trial: 4, enterprise: 0.5 },
43
+ });
44
+ },
45
+ };
@@ -218,10 +218,9 @@ const config = {
218
218
  lookupTables: [],
219
219
  hook: function (record, type, meta) {
220
220
 
221
- const NOW = dayjs();
222
- // const DATE_HOMEGROWN_LAUNCH = NOW.subtract(25, 'day');
223
- // const DATE_HOMEGROWN_IMPROVEMENT = NOW.subtract(10, 'day');
224
- const OVER_THINGS_GET_BETTER = NOW.subtract(15, 'day');
221
+ // Anchor relative dates to the resolved dataset window — never wall-clock.
222
+ const datasetEnd = dayjs.unix(meta.datasetEnd);
223
+ const OVER_THINGS_GET_BETTER = datasetEnd.subtract(15, 'day');
225
224
 
226
225
  if (type === "event") {
227
226
  const EVENT_TIME = dayjs(record.time);
@@ -622,6 +622,11 @@
622
622
  ]
623
623
  },
624
624
  "userProps": {
625
+ "theme": [
626
+ "light",
627
+ "dark",
628
+ "custom"
629
+ ],
625
630
  "title": {
626
631
  "$type": "string"
627
632
  },
@@ -240,7 +240,7 @@ const socialTweetGen = createTextGenerator({
240
240
  typoRate: 0.03,
241
241
  sentimentDrift: 0.3,
242
242
  min: 10,
243
- max: 280, // Twitter character limit
243
+ max: 254, // capped at Mixpanel string property limit
244
244
  includeMetadata: false
245
245
  });
246
246