@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.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- 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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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);
|