@ak--47/dungeon-master 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/create-dungeon/SKILL.md +139 -46
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +31 -6
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +44 -25
- package/.claude/skills/write-hooks/SKILL.md +31 -3
- package/CHANGELOG.md +85 -0
- package/HOOKS.md +13 -0
- package/dungeons/technical/ad-spend.js +41 -49
- package/dungeons/technical/anonymous-users.js +38 -36
- package/dungeons/technical/array-of-object-lookup.js +136 -153
- package/dungeons/technical/datagen-v15-verify.js +24 -11
- package/dungeons/technical/experiments.js +42 -40
- package/dungeons/technical/foobar.js +114 -118
- package/dungeons/technical/group-analytics.js +42 -40
- package/dungeons/technical/hook-helpers-verify.js +69 -50
- package/dungeons/technical/identity-model-verify.js +22 -12
- package/dungeons/technical/mirror-strategies.js +37 -39
- package/dungeons/technical/nested-objects.js +119 -118
- package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
- package/dungeons/technical/pattern-attributed-by-source.js +23 -9
- package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
- package/dungeons/technical/pattern-funnel-frequency.js +30 -15
- package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
- package/dungeons/technical/retention-cadence.js +115 -112
- package/dungeons/technical/sanity.js +86 -80
- package/dungeons/technical/scale-test.js +34 -38
- package/dungeons/technical/scd.js +111 -128
- package/dungeons/technical/simple.js +134 -141
- package/dungeons/technical/simplest.js +54 -62
- package/dungeons/technical/text-generation.js +110 -146
- package/dungeons/vertical/ai-platform.js +296 -333
- package/dungeons/vertical/community.js +284 -255
- package/dungeons/vertical/crypto.js +395 -391
- package/dungeons/vertical/dating.js +411 -378
- package/dungeons/vertical/devtools.js +336 -298
- package/dungeons/vertical/ecommerce.js +316 -394
- package/dungeons/vertical/education.js +369 -325
- package/dungeons/vertical/fintech.js +358 -325
- package/dungeons/vertical/fitness.js +335 -291
- package/dungeons/vertical/food-delivery.js +343 -307
- package/dungeons/vertical/gaming.js +480 -444
- package/dungeons/vertical/healthcare.js +306 -262
- package/dungeons/vertical/insurance-application.js +427 -409
- package/dungeons/vertical/logistics.js +271 -252
- package/dungeons/vertical/marketplace.js +333 -323
- package/dungeons/vertical/media.js +382 -335
- package/dungeons/vertical/real-estate.js +395 -346
- package/dungeons/vertical/sass.js +319 -333
- package/dungeons/vertical/social.js +368 -316
- package/dungeons/vertical/travel.js +297 -295
- package/index.js +46 -4
- package/lib/core/config-validator.js +126 -28
- package/lib/generators/funnels.js +4 -1
- package/lib/orchestrators/mixpanel-sender.js +7 -0
- package/lib/orchestrators/user-loop.js +132 -31
- package/lib/templates/defaults.js +59 -59
- package/lib/templates/macro-presets.js +14 -2
- package/lib/utils/dataset-context.js +103 -0
- package/lib/utils/retention-curve.js +140 -0
- package/lib/utils/utils.js +149 -38
- package/lib/verify/counting.js +40 -0
- package/lib/verify/emulate-breakdown.js +20 -1
- package/lib/verify/index.js +1 -0
- package/lib/verify/schema-validator.js +3 -1
- package/package.json +11 -2
- package/scripts/run-dungeon.mjs +12 -1
- package/types.d.ts +117 -1
|
@@ -28,9 +28,18 @@ In scope here:
|
|
|
28
28
|
`isFirstFunnel`, `attempts`
|
|
29
29
|
- Event flags: `isAuthEvent`, `isAttributionEvent`, `isFirstEvent`,
|
|
30
30
|
`isStrictEvent`, `isChurnEvent`, `isSessionStartEvent`
|
|
31
|
-
- Top-level: `datasetStart`, `datasetEnd`, `numUsers`,
|
|
32
|
-
`seed`, `format`,
|
|
33
|
-
`
|
|
31
|
+
- Top-level scale + data model: `datasetStart`, `datasetEnd`, `numUsers`,
|
|
32
|
+
`avgEventsPerUserPerDay`, `seed`, `userSeed`, `format`, `macro`, `soup`,
|
|
33
|
+
`retentionCurve`, `avgActiveDaysPerUser`, `maxTouchpointsPerUser`
|
|
34
|
+
- **Sub-object API (v1.5+):** group related keys into:
|
|
35
|
+
- `credentials: { token, region, serviceAccount, serviceSecret, projectId }`
|
|
36
|
+
- `switches: { hasLocation, hasCampaigns, hasSessionIds, hasAvatar,
|
|
37
|
+
hasIOSDevices, hasAndroidDevices, hasDesktopDevices, hasBrowser,
|
|
38
|
+
isAnonymous, alsoInferFunnels, hasAdSpend, hasAttributionFlags }`
|
|
39
|
+
- `identity: { avgDevicePerUser, sessionTimeout }`
|
|
40
|
+
|
|
41
|
+
Old top-level keys keep working (verbose warn nudges migration), but new
|
|
42
|
+
dungeons should ship the sub-object shape.
|
|
34
43
|
- Surviving advanced entities: `personas`, `worldEvents`, `engagementDecay`,
|
|
35
44
|
`dataQuality` — use sparingly
|
|
36
45
|
|
|
@@ -59,58 +68,78 @@ Before writing any code, scan:
|
|
|
59
68
|
|
|
60
69
|
## File structure
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const num_days = 120;
|
|
66
|
-
const num_users = 5_000;
|
|
67
|
-
const avg_events_per_user_per_day = 1.2;
|
|
68
|
-
let token = "your-mixpanel-token";
|
|
69
|
-
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
71
|
+
Use the canonical layout — sections in this fixed order. Skip any section
|
|
72
|
+
that doesn't apply (e.g., schema-only dungeons omit HOOK STORIES and KNOBS).
|
|
73
|
+
Section delimiter: `// ── SECTION NAME ──` (box-drawing chars).
|
|
70
74
|
|
|
75
|
+
```javascript
|
|
76
|
+
// ── IMPORTS ──
|
|
71
77
|
import dayjs from "dayjs";
|
|
78
|
+
import utc from "dayjs/plugin/utc.js";
|
|
79
|
+
dayjs.extend(utc);
|
|
72
80
|
import "dotenv/config";
|
|
73
81
|
import * as u from "../../lib/utils/utils.js";
|
|
74
82
|
import * as v from "ak-tools";
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* ═══════════════════════════════════════════════════════════════
|
|
85
|
-
* DATASET OVERVIEW
|
|
86
|
-
* ═══════════════════════════════════════════════════════════════
|
|
83
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
84
|
+
|
|
85
|
+
// ── OVERVIEW ──
|
|
86
|
+
/*
|
|
87
|
+
* NAME: <BrandName>
|
|
88
|
+
* APP: <2-4 line description: what users do, core flow, monetization>
|
|
89
|
+
* SCALE: <numUsers> users, ~<numEvents> events, <numDays> days (<start> → <end>)
|
|
90
|
+
* CORE LOOP: <event1> → <event2> → <event3> → ...
|
|
87
91
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* - Key entities and relationships
|
|
91
|
-
* - Why these events/properties were chosen
|
|
92
|
+
* EVENTS (N):
|
|
93
|
+
* <event name (weight)> > ... (sorted by weight desc)
|
|
92
94
|
*
|
|
93
|
-
*
|
|
95
|
+
* FUNNELS (N):
|
|
96
|
+
* - <Funnel name>: <step> → <step> (N%)
|
|
97
|
+
*
|
|
98
|
+
* USER PROPS: <prop1, prop2, ...>
|
|
99
|
+
* SUPER PROPS: <prop1, prop2, ...>
|
|
100
|
+
* SCD PROPS: <prop (values, freq, max)>
|
|
101
|
+
* GROUPS: <key1, key2 | none>
|
|
94
102
|
*/
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
// ── SCALE ──
|
|
105
|
+
const SEED = "dm4-VERTICAL";
|
|
106
|
+
const NUM_USERS = 5_000;
|
|
107
|
+
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
108
|
+
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
109
|
+
const EVENTS_PER_DAY = 1.2;
|
|
110
|
+
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
102
111
|
|
|
103
|
-
|
|
104
|
-
hasAnonIds: true,
|
|
105
|
-
avgDevicePerUser: 2,
|
|
106
|
-
hasSessionIds: true,
|
|
112
|
+
const chance = u.initChance(SEED);
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
// ── DATA ARRAYS ── (omit if none)
|
|
115
|
+
const productIds = v.range(1, 200).map(n => `prod_${v.uid(8)}`);
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
// ── CONFIG ──
|
|
118
|
+
/** @type {Config} */
|
|
119
|
+
const config = {
|
|
120
|
+
seed: SEED,
|
|
121
|
+
datasetStart: DATASET_START,
|
|
122
|
+
datasetEnd: DATASET_END,
|
|
123
|
+
numUsers: NUM_USERS,
|
|
124
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
125
|
+
format: "json",
|
|
126
|
+
gzip: true,
|
|
127
|
+
writeToDisk: false,
|
|
128
|
+
concurrency: 1,
|
|
129
|
+
macro: "flat", // optional — see "Trend shape" below
|
|
130
|
+
soup: "growth", // optional
|
|
131
|
+
|
|
132
|
+
credentials: { token },
|
|
133
|
+
switches: {
|
|
134
|
+
hasLocation: true,
|
|
135
|
+
hasAndroidDevices: false,
|
|
136
|
+
hasIOSDevices: false,
|
|
137
|
+
hasDesktopDevices: true,
|
|
138
|
+
hasBrowser: true,
|
|
139
|
+
hasAvatar: true,
|
|
140
|
+
hasSessionIds: true,
|
|
141
|
+
},
|
|
142
|
+
identity: { avgDevicePerUser: 2 },
|
|
114
143
|
|
|
115
144
|
funnels: [ /* see "Funnels" below */ ],
|
|
116
145
|
events: [ /* see "Events" below */ ],
|
|
@@ -125,6 +154,14 @@ const config = {
|
|
|
125
154
|
export default config;
|
|
126
155
|
```
|
|
127
156
|
|
|
157
|
+
When the dungeon has hooks (added later by `/write-hooks`), the layout
|
|
158
|
+
extends with HOOK STORIES (full per-hook docs with Mixpanel report blocks),
|
|
159
|
+
KNOBS (extracted tunable constants — timing, thresholds, multipliers),
|
|
160
|
+
HOOK STATE (module-level Maps/Sets used across users), and HELPER FUNCTIONS
|
|
161
|
+
(per-type handlers like `handleEventHooks`, `handleEverythingHooks`).
|
|
162
|
+
`config.hook` becomes a thin dispatcher delegating to the helpers. See
|
|
163
|
+
`dungeons/vertical/ecommerce.js` as the canonical exemplar.
|
|
164
|
+
|
|
128
165
|
## Required components
|
|
129
166
|
|
|
130
167
|
### 1. Events (~15–20)
|
|
@@ -234,7 +271,9 @@ If you absolutely need a stub for downstream stamping consistency, leave a
|
|
|
234
271
|
|
|
235
272
|
The identity model has three knobs:
|
|
236
273
|
|
|
237
|
-
### `avgDevicePerUser` (whole number, default 0)
|
|
274
|
+
### `identity.avgDevicePerUser` (whole number, default 0)
|
|
275
|
+
|
|
276
|
+
Place inside the `identity` sub-object: `identity: { avgDevicePerUser: 2 }`.
|
|
238
277
|
|
|
239
278
|
| App type | Recommended | Why |
|
|
240
279
|
|----------|-------------|-----|
|
|
@@ -243,7 +282,11 @@ The identity model has three knobs:
|
|
|
243
282
|
| Multi-device-heavy product (streaming, fitness) | 2–3 | TV + phone + tablet sessions distinguishable |
|
|
244
283
|
| Server / API-only product | 0 | No client device concept |
|
|
245
284
|
|
|
246
|
-
|
|
285
|
+
**`hasAnonIds: true` is deprecated.** Use `identity.avgDevicePerUser: 1`
|
|
286
|
+
directly. The deprecated alias still works through 1.5.x — when
|
|
287
|
+
`hasAnonIds: true` is set without an explicit `avgDevicePerUser`, the
|
|
288
|
+
validator promotes to `identity.avgDevicePerUser: 1` and emits a verbose
|
|
289
|
+
warning.
|
|
247
290
|
|
|
248
291
|
### `isAuthEvent` placement
|
|
249
292
|
|
|
@@ -257,6 +300,15 @@ Flag the event that represents "user becomes identified". Put it in the
|
|
|
257
300
|
The engine stamps user_id+device_id on this event; pre-auth funnel steps get
|
|
258
301
|
device_id only; post-auth funnel steps get user_id only.
|
|
259
302
|
|
|
303
|
+
**Anonymous non-converters get `_drop: true` on their profile (v1.5.1).**
|
|
304
|
+
Born-in-dataset users who never reach an `isAuthEvent` step are anonymous —
|
|
305
|
+
their events still flow (tied to `device_id`), but `mixpanel-sender` filters
|
|
306
|
+
`_drop:true` profiles before `/engage` push. `result.profilesPushed` reports
|
|
307
|
+
actual push count vs `result.userProfilesData.size` (full population). The
|
|
308
|
+
`everything` hook can rescue a profile via `delete meta.profile._drop`.
|
|
309
|
+
Pre-existing users (born outside window) are always considered identified
|
|
310
|
+
and never get `_drop`.
|
|
311
|
+
|
|
260
312
|
### `attempts` (per-funnel, optional)
|
|
261
313
|
|
|
262
314
|
```js
|
|
@@ -406,13 +458,54 @@ skill handles this).
|
|
|
406
458
|
### `maxTouchpointsPerUser` (attribution cap)
|
|
407
459
|
|
|
408
460
|
Top-level optional knob. Caps UTM stamping at this many events per user
|
|
409
|
-
(default 10, matching Mixpanel `TOUCHPOINTS_LIMIT`). When `hasCampaigns: true`
|
|
461
|
+
(default 10, matching Mixpanel `TOUCHPOINTS_LIMIT`). When `switches.hasCampaigns: true`
|
|
410
462
|
and a user has more eligible events than the cap, the engine takes a
|
|
411
463
|
uniform-random sample across the user's lifetime and stamps UTMs on the
|
|
412
464
|
sampled events only. Sampling across lifetime (NOT first-N) preserves
|
|
413
465
|
realistic touch shape — Mixpanel's last-10-window then gives meaningful
|
|
414
466
|
first/last-touch attribution. Set to `Infinity` to disable the cap.
|
|
415
467
|
|
|
468
|
+
**Generator/verifier asymmetry to know about:** the generator samples
|
|
469
|
+
uniformly across user lifetime; the verifier (`emulateBreakdown` with
|
|
470
|
+
`attributedBy`) and real Mixpanel attribution both read the **last N
|
|
471
|
+
touchpoints before each conversion** (per `attributed_value_reader.cpp`).
|
|
472
|
+
For users with ≤10 attribution-eligible events lifetime, no divergence
|
|
473
|
+
(cap is a no-op). For users with >10 eligible events and multiple
|
|
474
|
+
conversions, generator stamps may not align with Mixpanel's per-conversion
|
|
475
|
+
last-10 window. Real-world impact: minor for first-touch, occasional
|
|
476
|
+
divergence for last-touch in multi-conversion users. Tracked for 1.6.
|
|
477
|
+
|
|
478
|
+
### `retentionCurve` (generator-side retention shape, v1.5+)
|
|
479
|
+
|
|
480
|
+
Top-level optional knob. Shape retention via log-linear interpolation
|
|
481
|
+
between waypoints. Independent of `engagementDecay`.
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
retentionCurve: [
|
|
485
|
+
{ day: 0, retention: 1.0 },
|
|
486
|
+
{ day: 1, retention: 0.80 },
|
|
487
|
+
{ day: 7, retention: 0.50 },
|
|
488
|
+
{ day: 30, retention: 0.20 },
|
|
489
|
+
]
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Each born-in-dataset user's events get filtered based on the interpolated
|
|
493
|
+
retention at the event's age-from-first-event-day. Use when you want a
|
|
494
|
+
declarative retention shape at config level (analytical-style D1/D7/D30
|
|
495
|
+
targets) instead of writing hook logic.
|
|
496
|
+
|
|
497
|
+
### `userSeed` (separate distinct_id RNG seed, v1.5+)
|
|
498
|
+
|
|
499
|
+
Top-level optional knob. Separates the distinct_id RNG seed from the main
|
|
500
|
+
`seed`. Lets you regenerate a dataset with a different event distribution
|
|
501
|
+
while keeping the user pool stable across runs — useful for incremental
|
|
502
|
+
data layering.
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
seed: "v2", // event-stream RNG (different distribution each version)
|
|
506
|
+
userSeed: "users-v1", // user-pool RNG (stable across versions)
|
|
507
|
+
```
|
|
508
|
+
|
|
416
509
|
## SuperProp consistency rule
|
|
417
510
|
|
|
418
511
|
If `superProps` and `userProps` both define a property like `Plan`, the
|
|
@@ -7,10 +7,25 @@ Mixpanel does NOT count the way naive SQL does. The verifier (and any DuckDB que
|
|
|
7
7
|
| Concept | Mixpanel rule | Wrong SQL → Right SQL |
|
|
8
8
|
|---------|--------------|----------------------|
|
|
9
9
|
| Frequency / cohort by event count | Distinct calendar days, NOT total events | `COUNT(*)` → `COUNT(DISTINCT date_trunc('day', time::TIMESTAMP))` |
|
|
10
|
-
| Funnels | Greedy single-pass, strict order, 2-second grace | NEVER hand-roll funnel SQL — use `emulateBreakdown` |
|
|
11
|
-
| AVG / SUM / MIN / MAX | Skip null and non-numeric from BOTH num and denom | Always wrap in `TRY_CAST(prop AS DOUBLE)` |
|
|
12
|
-
| Attribution | Cap at 10 touchpoints in lookback | Use `emulateBreakdown` with `attributedBy` |
|
|
13
|
-
| Conversion window | Strict `<` boundary | Read `Funnel.conversionWindowDays` and respect it |
|
|
10
|
+
| Funnels | Greedy single-pass, strict order, 2-second grace (`history.cpp` `OUT_OF_ORDER_MILLISECONDS = 2000`) | NEVER hand-roll funnel SQL — use `emulateBreakdown` |
|
|
11
|
+
| AVG / SUM / MIN / MAX | Skip null and non-numeric from BOTH num and denom (`normal_query.cpp:1718-1733`) | Always wrap in `TRY_CAST(prop AS DOUBLE)` |
|
|
12
|
+
| Attribution | Cap at 10 touchpoints in lookback (`attributed_value_reader.cpp:16` `TOUCHPOINTS_LIMIT`) | Use `emulateBreakdown` with `attributedBy` |
|
|
13
|
+
| Conversion window | Strict `<` boundary (`conversion_window.cpp:48` `t1 < t2 + 1000*len`) | Read `Funnel.conversionWindowDays` and respect it |
|
|
14
|
+
| Sessions | 3-trigger split: timeout `>`, max duration `>`, day-idx change (`session_query.cpp:906-911`) | Trust pre-stamped `session_id`; group by `(user, session_id)` |
|
|
15
|
+
| Retention | Birth-anchored, ms-strict gate (default `birth_can_retain=false` → `<`), bucketed by `floor((ret−birth)/unit)` (`retention_query.cpp:1097-1109,1228-1231`) | Use `emulateBreakdown` with `retention` |
|
|
16
|
+
|
|
17
|
+
**Known divergences from Mixpanel C++** (1.5.1):
|
|
18
|
+
- `countDistinctPeriods` default = `algorithm: 'calendar'` (UTC bucket).
|
|
19
|
+
Mixpanel C++ (`addiction_query.cpp:359`) uses ROLLING window. Pass
|
|
20
|
+
`algorithm: 'rolling'` for exact Frequency-Distribution parity.
|
|
21
|
+
- COMPOUNDED retention is NOT implemented — verifier silently ignores
|
|
22
|
+
`compounded: true`. Use DuckDB or query Mixpanel directly for "DAU
|
|
23
|
+
coming back" reports.
|
|
24
|
+
- Touchpoint sampling: generator stamps uniform-random across user
|
|
25
|
+
lifetime; verifier reads last-N before conversion (matches C++).
|
|
26
|
+
For users with ≤10 attribution events lifetime, no divergence.
|
|
27
|
+
- List-typed property AVG/SUM: C++ auto-flattens lists per item; our
|
|
28
|
+
`nullAwareAvg` requires pre-flattened input.
|
|
14
29
|
|
|
15
30
|
Full rules: see [HOOKS.md Section 2](../../../../HOOKS.md#2-how-mixpanel-counts-things).
|
|
16
31
|
|
|
@@ -63,7 +78,7 @@ For CI-style assertions, use `verifyDungeon` with a checks array; see `tests/e2e
|
|
|
63
78
|
- **`Funnel.order` auto-dispatched.** For `sequential` / `interrupt` funnels, the emulator runs the greedy single-pass engine. For other order modes (`first-fixed`, `last-fixed`, `random`, etc.), it dispatches to `evaluateAnyOrderCompletion` (set-membership check). For `random` mode, results are `verificationKind: "informational"` — Mixpanel funnel shape doesn't apply; do not assert PASS/FAIL.
|
|
64
79
|
- **Auto-sort means custom DuckDB queries can trust event order.** Per-user events arrive sorted ascending by time (default; opt out via `autoSortAfterEverything: false`). `LAG`/`LEAD` window functions work without explicit `ORDER BY time` in the partition.
|
|
65
80
|
- **Auto-promote `isStrictEvent` is silent healing — not a regression.** If a stale dungeon's funnel-step events also live in `events[]`, the validator stamps `isStrictEvent: true` and warns. Verification of those dungeons may show CHANGED standalone-event counts vs older runs — that is correct behavior, not a bug to chase.
|
|
66
|
-
- **Touchpoint cap = 10 enforced at generation.** `hasCampaigns: true` users get up to `maxTouchpointsPerUser` (default 10) UTM-stamped events, sampled across lifetime. Attribution checks via `attributedBy` should see realistic first/last-touch shapes, not all-stamps-at-birth.
|
|
81
|
+
- **Touchpoint cap = 10 enforced at generation.** `switches.hasCampaigns: true` users get up to `maxTouchpointsPerUser` (default 10) UTM-stamped events, sampled across lifetime. Attribution checks via `attributedBy` should see realistic first/last-touch shapes, not all-stamps-at-birth. The verifier's `attributedBy` reads last-N before conversion (matches Mixpanel); the generator samples uniform across lifetime. For users with >10 attribution events lifetime, expect minor divergence on multi-conversion users.
|
|
67
82
|
|
|
68
83
|
## Hook awareness for verification
|
|
69
84
|
|
|
@@ -128,7 +143,17 @@ When the dungeon's `Funnel` config sets these fields, `verifyDungeon` auto-appli
|
|
|
128
143
|
|
|
129
144
|
## Identity-model dungeons — pass profiles
|
|
130
145
|
|
|
131
|
-
When `avgDevicePerUser > 0` or `hasAnonIds: true
|
|
146
|
+
When `identity.avgDevicePerUser > 0` (or the deprecated `hasAnonIds: true`),
|
|
147
|
+
ALWAYS pass `profiles` to `emulateBreakdown`. Without it, pre-auth
|
|
148
|
+
`device_id` events bucket as separate "users" and your funnel/retention/
|
|
149
|
+
attribution numbers all deflate.
|
|
150
|
+
|
|
151
|
+
**v1.5.1 anonymous non-converter semantic:** born-in-dataset users who
|
|
152
|
+
never reach an `isAuthEvent` get `_drop: true` on their profile.
|
|
153
|
+
`result.profilesPushed` reports the actual push count;
|
|
154
|
+
`result.userProfilesData.size` reports full population (including dropped).
|
|
155
|
+
Don't be surprised if profile-count assertions show
|
|
156
|
+
`profilesPushed < userProfilesData.size` — that's correct.
|
|
132
157
|
|
|
133
158
|
```js
|
|
134
159
|
const events = Array.from(result.eventData);
|
|
@@ -36,14 +36,14 @@ The expected set of columns per event type is derived from config:
|
|
|
36
36
|
| Source | Keys | Condition |
|
|
37
37
|
|--------|------|-----------|
|
|
38
38
|
| Core | `event`, `time`, `insert_id`, `user_id` | Always |
|
|
39
|
-
| Identity | `device_id` | `avgDevicePerUser > 0` |
|
|
40
|
-
| Identity | `session_id` | `hasSessionIds` |
|
|
39
|
+
| Identity | `device_id` | `identity.avgDevicePerUser > 0` |
|
|
40
|
+
| Identity | `session_id` | `switches.hasSessionIds` |
|
|
41
41
|
| Event config | `events[i].properties` keys | Per event type |
|
|
42
42
|
| Super props | `superProps` keys | All event types |
|
|
43
|
-
| Location | `city`, `region`, `country`, `country_code` | `hasLocation` |
|
|
44
|
-
| Browser | `browser` | `hasBrowser` |
|
|
45
|
-
| Device | `model`, `screen_height`, `screen_width`, `os`, `
|
|
46
|
-
| Campaigns | `utm_source`, `utm_campaign`, `utm_medium`, `utm_content`, `utm_term` | `hasCampaigns` |
|
|
43
|
+
| Location | `city`, `region`, `country`, `country_code` | `switches.hasLocation` |
|
|
44
|
+
| Browser | `browser` | `switches.hasBrowser` |
|
|
45
|
+
| Device | `model`, `screen_height`, `screen_width`, `os`, `carrier`, `radio` | `switches.hasAndroidDevices`/`hasIOSDevices`/`hasDesktopDevices`. **`Platform` removed in 1.5.1** — `os` covers the signal. Hooks/dungeons may opt back in by declaring `Platform` in event `properties`. |
|
|
46
|
+
| Campaigns | `utm_source`, `utm_campaign`, `utm_medium`, `utm_content`, `utm_term` | `switches.hasCampaigns` |
|
|
47
47
|
| Group keys | group key name | Per event type from `groupKeys[i][2]`, or all if empty |
|
|
48
48
|
| Funnel props | `funnel.props` keys | Events in funnel sequence |
|
|
49
49
|
| Experiment | `Experiment name`, `Variant name` | `$experiment_started` event |
|
|
@@ -60,7 +60,7 @@ If any event type has SCHEMA-FAIL, flag it prominently and include specific reme
|
|
|
60
60
|
|
|
61
61
|
## Standard identity-model invariants
|
|
62
62
|
|
|
63
|
-
Run these for every dungeon that uses the identity model (`isAuthEvent` + `attempts` + `avgDevicePerUser`), BEFORE per-pattern checks:
|
|
63
|
+
Run these for every dungeon that uses the identity model (`isAuthEvent` + `attempts` + `identity.avgDevicePerUser`), BEFORE per-pattern checks:
|
|
64
64
|
|
|
65
65
|
```sql
|
|
66
66
|
-- Stitch event count must match converted-born count, exactly one per user.
|
|
@@ -420,7 +420,8 @@ When verifying `everything` hooks, you often MUST join events with user profiles
|
|
|
420
420
|
|
|
421
421
|
## Advanced feature verification
|
|
422
422
|
|
|
423
|
-
|
|
423
|
+
Supported advanced features (still active in 1.5.x): `personas`,
|
|
424
|
+
`worldEvents`, `engagementDecay`, `dataQuality`. When verifying:
|
|
424
425
|
|
|
425
426
|
```sql
|
|
426
427
|
-- Personas: check distribution matches configured weights
|
|
@@ -432,27 +433,36 @@ SELECT promo, count(*) FROM read_json_auto('./data/verify-dungeon-EVENTS.json')
|
|
|
432
433
|
-- Data Quality: verify bots, nulls, empty events
|
|
433
434
|
SELECT 'bots' as metric, count(*) FROM read_json_auto('./data/verify-dungeon-USERS.json') WHERE is_bot = true
|
|
434
435
|
UNION ALL SELECT 'null_props', count(*) FROM read_json_auto('./data/verify-dungeon-EVENTS.json') WHERE category IS NULL;
|
|
436
|
+
```
|
|
435
437
|
|
|
436
|
-
|
|
437
|
-
SELECT event, count(*) FROM read_json_auto('./data/verify-dungeon-EVENTS.json')
|
|
438
|
-
WHERE event IN ('trial started','subscription started','plan upgraded','subscription cancelled') GROUP BY 1;
|
|
438
|
+
Advanced feature patterns should ALWAYS be present (deterministic from config), unlike hooks which may have statistical variance.
|
|
439
439
|
|
|
440
|
-
|
|
441
|
-
|
|
440
|
+
**Deprecated config blocks (silently stripped by validator since 1.4):**
|
|
441
|
+
`subscription`, `attribution`, `geo`, `features`, `anomalies`. If a
|
|
442
|
+
dungeon still references these, properties they used to generate
|
|
443
|
+
(`subscription_plan`, `_region`, `theme`, `_anomaly`, etc.) will be
|
|
444
|
+
missing from the output. Migration: add equivalents to `superProps` /
|
|
445
|
+
`userProps` and drive downstream effects in `user` or `everything` hooks.
|
|
442
446
|
|
|
443
|
-
|
|
444
|
-
SELECT _region, count(*) FROM read_json_auto('./data/verify-dungeon-USERS.json') WHERE _region IS NOT NULL GROUP BY 1;
|
|
447
|
+
## Standard verification checks (run for every dungeon)
|
|
445
448
|
|
|
446
|
-
|
|
447
|
-
SELECT theme, count(*) FROM read_json_auto('./data/verify-dungeon-EVENTS.json') WHERE theme IS NOT NULL GROUP BY 1;
|
|
449
|
+
### 0. Anonymous non-converter `_drop` audit (v1.5.1, identity-model dungeons)
|
|
448
450
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
451
|
+
Born-in-dataset users who never reach an `isAuthEvent` get `_drop: true`
|
|
452
|
+
stamped on their profile. Real Mixpanel `/engage` skips these — the
|
|
453
|
+
verifier's profile-count assertions should mirror that. Quick check:
|
|
452
454
|
|
|
453
|
-
|
|
455
|
+
```sql
|
|
456
|
+
SELECT
|
|
457
|
+
COUNT(*) AS total_profiles,
|
|
458
|
+
SUM(CASE WHEN _drop = true THEN 1 ELSE 0 END) AS dropped,
|
|
459
|
+
SUM(CASE WHEN _drop IS NULL OR _drop = false THEN 1 ELSE 0 END) AS would_push
|
|
460
|
+
FROM read_json_auto('./data/verify-dungeon-USERS.json');
|
|
461
|
+
```
|
|
454
462
|
|
|
455
|
-
|
|
463
|
+
`would_push` should equal `result.profilesPushed` from the run output.
|
|
464
|
+
For pre-existing-only dungeons (`percentUsersBornInDataset: 0`) expect
|
|
465
|
+
`dropped = 0`.
|
|
456
466
|
|
|
457
467
|
### 1. SuperProp Consistency
|
|
458
468
|
Verify each user has exactly 1 value per superProp:
|
|
@@ -476,9 +486,18 @@ Verdict: **STRONG** ≥99% consistent, **WEAK** 90-99%, **FAIL** <90%.
|
|
|
476
486
|
Every superProp key should also appear on user profiles. Compare the dungeon's `superProps` keys against columns in the USERS file. Any superProp not mirrored in `userProps` means the stamping fix is incomplete.
|
|
477
487
|
|
|
478
488
|
### 3. Mixpanel Default Property Casing Check
|
|
479
|
-
The system generates device properties with Mixpanel's standard casing
|
|
480
|
-
|
|
481
|
-
|
|
489
|
+
The system generates device properties with Mixpanel's standard casing
|
|
490
|
+
(`os`, `model`, `screen_height`, `screen_width`, `carrier`, `radio`,
|
|
491
|
+
`browser`) and location properties (`city`, `region`, `country`,
|
|
492
|
+
`country_code`). If a dungeon defines a superProp with conflicting casing
|
|
493
|
+
(e.g., capitalized `City` vs system `city`), both properties appear on
|
|
494
|
+
events — confusing in Mixpanel. Check for:
|
|
495
|
+
- `City`, `Region`, `Country` (caps) vs system `city`, `region`, `country` — verdict **FAIL** if dungeon uses caps for these
|
|
496
|
+
- `Browser` (caps) vs system `browser` — verdict **FAIL** if mismatched
|
|
497
|
+
|
|
498
|
+
**Note:** `Platform` was REMOVED from default device props in 1.5.1.
|
|
499
|
+
If a dungeon explicitly declares `Platform` in event `properties`, that's
|
|
500
|
+
intentional opt-in — not a casing conflict.
|
|
482
501
|
|
|
483
502
|
### 4. funnel-pre Dilution Check
|
|
484
503
|
For any dungeon with `funnel-pre` conversionRate modifications, verify the actual visible effect:
|
|
@@ -78,6 +78,10 @@ Inside `funnel-pre` and `funnel-post`:
|
|
|
78
78
|
Inside `everything`:
|
|
79
79
|
- `meta.authTime: number | null` — unix-ms of the stitch event, null if never authed
|
|
80
80
|
- `meta.isPreAuth(event): boolean` — convenience predicate
|
|
81
|
+
- `meta.profile` — full profile object. Mutate or rescue here.
|
|
82
|
+
- `meta.userIsBornInDataset: boolean` — true when user was born inside the dataset window
|
|
83
|
+
- `meta.scd: { <key>: SCDEntry[] }` — SCD entries per key
|
|
84
|
+
- `meta.datasetStart, meta.datasetEnd: number` — unix-seconds bounds
|
|
81
85
|
|
|
82
86
|
Pattern: gate trend logic on `meta.isFinalAttempt` so failed prior attempts
|
|
83
87
|
don't get the same treatment as the converted attempt.
|
|
@@ -196,9 +200,10 @@ writing a custom hook:
|
|
|
196
200
|
- **Time-series trends** ("conversion rises week over week") — wrap any
|
|
197
201
|
breakdown with `timeBucket: 'week'`. Engineer via temporal-windowed hooks
|
|
198
202
|
using `DATASET_START.add(N, 'days')`.
|
|
199
|
-
- **Identity-model dungeons** — when `avgDevicePerUser > 0`
|
|
200
|
-
`hasAnonIds: true
|
|
201
|
-
identity map merging pre-auth `device_id`
|
|
203
|
+
- **Identity-model dungeons** — when `identity.avgDevicePerUser > 0`
|
|
204
|
+
(or the deprecated `hasAnonIds: true`), ALWAYS pass `profiles` to
|
|
205
|
+
verification. Auto-builds identity map merging pre-auth `device_id`
|
|
206
|
+
events with post-auth `user_id`.
|
|
202
207
|
|
|
203
208
|
**Schema-first reminder:** exclusion events must be declared in `events[]`
|
|
204
209
|
before referencing them as `Funnel.exclusionEvents` — the validator throws
|
|
@@ -234,6 +239,10 @@ record.was_dropped = false; // ❌ flag-stamping
|
|
|
234
239
|
event.engineered_pattern_id = 5; // ❌ flag-stamping
|
|
235
240
|
```
|
|
236
241
|
|
|
242
|
+
(One narrow exception: `meta.profile._drop` is engine-recognized — see
|
|
243
|
+
the "Anonymous non-converter `_drop` rescue" section above. All OTHER
|
|
244
|
+
flags must live in `userProps`/event `properties` with a declared default.)
|
|
245
|
+
|
|
237
246
|
DO WRITE:
|
|
238
247
|
```js
|
|
239
248
|
record.amount *= 3; // ✅ scale existing numeric prop
|
|
@@ -259,6 +268,25 @@ hook: function(record, type, meta) {
|
|
|
259
268
|
}
|
|
260
269
|
```
|
|
261
270
|
|
|
271
|
+
### Anonymous non-converter `_drop` rescue (v1.5.1)
|
|
272
|
+
|
|
273
|
+
Born-in-dataset users who never reach an `isAuthEvent` step get
|
|
274
|
+
`_drop: true` stamped on their profile BEFORE the `everything` hook fires.
|
|
275
|
+
`mixpanel-sender` filters those before pushing to `/engage`. Hooks can
|
|
276
|
+
rescue a profile by deleting the flag:
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
if (type === 'everything' && meta.profile) {
|
|
280
|
+
// Rescue: keep some anonymous power-users in /engage even without sign_up.
|
|
281
|
+
const eventCount = record.length;
|
|
282
|
+
if (eventCount >= 20) delete meta.profile._drop;
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`_drop` is the ONE engine-recognized flag a hook may set/clear on a
|
|
287
|
+
profile — every other property must be declared in `userProps` first per
|
|
288
|
+
the anti-flag-stamping rule below.
|
|
289
|
+
|
|
262
290
|
When a dungeon uses funnel `attempts`, hooks can reach into individual attempts
|
|
263
291
|
via funnel-post meta:
|
|
264
292
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,91 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@ak--47/dungeon-master`.
|
|
4
4
|
|
|
5
|
+
## 1.5.2 — 2026-05-21
|
|
6
|
+
|
|
7
|
+
Docs-only patch. Aligns the `.claude/skills/` authoring + verification
|
|
8
|
+
guides with the 1.5.1 engine + config API. No runtime changes.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **`create-dungeon` skill** now emits the canonical dungeon layout
|
|
13
|
+
(IMPORTS / OVERVIEW / SCALE / DATA ARRAYS / CONFIG sections) and the
|
|
14
|
+
sub-object config API (`credentials` / `switches` / `identity`).
|
|
15
|
+
Removed the old `// ── TWEAK THESE ──` template + flat-key example.
|
|
16
|
+
- **`create-dungeon` skill** documents `hasAnonIds` as deprecated; nudges
|
|
17
|
+
authors to write `identity.avgDevicePerUser: 1` directly.
|
|
18
|
+
- **`create-dungeon` skill** adds sections for `retentionCurve`,
|
|
19
|
+
`userSeed`, anonymous-non-converter `_drop: true` semantics, and
|
|
20
|
+
flags the touchpoint-sampling generator/verifier asymmetry.
|
|
21
|
+
- **`write-hooks` skill** documents the `meta.profile._drop` rescue
|
|
22
|
+
pattern (the one engine-recognized flag a hook may set/clear on a
|
|
23
|
+
profile). Expanded `meta` interface listing for the `everything` hook.
|
|
24
|
+
- **`verify-dungeon/references/counting-semantics.md`** notes known
|
|
25
|
+
divergences from Mixpanel C++ (calendar vs rolling distinct-period
|
|
26
|
+
default, COMPOUNDED retention not implemented, touchpoint sampling
|
|
27
|
+
asymmetry, list-typed AVG/SUM no auto-flatten). All references to
|
|
28
|
+
`hasAnonIds: true` updated to the new `identity.avgDevicePerUser` shape.
|
|
29
|
+
- **`verify-dungeon/references/sql-recipes.md`** drops `Platform` from
|
|
30
|
+
the expected device-keys table (removed in 1.5.1; `os` covers the
|
|
31
|
+
signal). Updates casing check to drop `Platform`-vs-`platform` rule.
|
|
32
|
+
Adds an anonymous-non-converter `_drop` audit query as standard check
|
|
33
|
+
#0 for identity-model dungeons. Updates "Advanced feature verification"
|
|
34
|
+
to list only currently-supported features (`personas`, `worldEvents`,
|
|
35
|
+
`engagementDecay`, `dataQuality`); calls out the deprecated config
|
|
36
|
+
blocks (`subscription`, `attribution`, `geo`, `features`, `anomalies`)
|
|
37
|
+
the validator silently strips.
|
|
38
|
+
|
|
39
|
+
### Why
|
|
40
|
+
|
|
41
|
+
Skills are how most dungeons get authored. Drifting between skill-emitted
|
|
42
|
+
output and 1.5.1 engine behavior would silently produce stale-shape
|
|
43
|
+
dungeons + missed coverage of new features (`retentionCurve`, `userSeed`,
|
|
44
|
+
`_drop` semantics, sub-object API). Patch keeps skill output and engine
|
|
45
|
+
behavior synchronized.
|
|
46
|
+
|
|
47
|
+
## 1.5.1 — 2026-05-20
|
|
48
|
+
|
|
49
|
+
Quality + ergonomics release. No new analytical capabilities — fixes accumulated rough edges around concurrency, accuracy, profiles, and config ergonomics that surfaced after 1.5.0 shipped. Adds a generator-side retention shaper, exposes a config sub-object API for cleaner dungeon files, and restructures all 48 shipped dungeons to a canonical layout. Top-level keys keep working for back-compat.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **`credentials` / `switches` / `identity` config sub-objects.** New ergonomic shape for grouping related dungeon keys: `credentials: { token, region, serviceAccount, serviceSecret, projectId }`, `switches: { hasLocation, hasCampaigns, hasSessionIds, hasAvatar, isAnonymous, ... }`, `identity: { avgDevicePerUser, sessionTimeout }`. `mergeConfigSubObjects` hoists sub-object values into top-level keys at validation time; top-level still wins when both set (with a verbose warn). Old flat top-level keys keep working — back-compat suite in `tests/unit/config-restructure.test.js`.
|
|
54
|
+
- **`retentionCurve` config knob.** Generator-side retention shaper. Accepts an array of `{day, retention}` waypoints; the engine interpolates log-linearly to drop late events per user based on first-event age. Independent of `engagementDecay`. Enables analytical-style retention shapes (D1 80% → D7 50% → D30 20%) at dungeon-config level instead of via hooks.
|
|
55
|
+
- **Per-macro `avgActiveDaysPerUser` defaults.** When `avgActiveDaysPerUser` is unset, defaults derived per macro: steady=15, growth=10, viral=20, decline=5, flat=20 (numDays/4 cap). Removes the need to hand-tune for every macro.
|
|
56
|
+
- **`COUNT_DISTINCT` aggregation in `emulateBreakdown`** (`type: 'distinctCount'`). Mirrors Mixpanel's count-distinct measure for cohort sizing / unique-user breakdowns.
|
|
57
|
+
- **`userSeed` config knob.** Separate distinct_id seed from the main `seed`. Lets you regenerate a dataset with a different event distribution while keeping the user pool stable across runs — useful for incremental data layering.
|
|
58
|
+
- **`result.profilesPushed`** count exposed on the `Result` object. Reports how many profiles actually got pushed to `/engage` after `_drop` filtering.
|
|
59
|
+
- **`runWithDataset(begin, now, fn)`** API for explicit dataset-window scoping (rare — most callers don't need this; in-process `generate()` calls now auto-scope via AsyncLocalStorage).
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **Anonymous non-converters get `_drop: true` stamped on their profile.** Real-world Mixpanel `$identify` semantics — profiles only exist for users who actually identified. Born-in-dataset users who never reach an `isAuthEvent` step in their first funnel are anonymous: events still flow (tied to `device_id`), but no profile is pushed to `/engage`. `userProfilesData` still contains every profile object; `mixpanel-sender` filters `_drop:true` before push. Pre-existing users are considered already-identified and never get `_drop`. The `everything` hook can rescue a profile by `delete meta.profile._drop`.
|
|
64
|
+
- **`numEvents` more accurate.** Removed dice rolls + `0.714` magic dampening from per-user budget computation; replaced with a clean `chance.normal(mean=budget, dev=budget/3)`. Old behavior overshot the target by 1.6-2x; new behavior matches the configured rate within ±3% across all 5 macros at 50K target. **If you previously tuned `avgEventsPerUserPerDay` around the old overshoot, expect ~40-60% fewer events at the same rate.** Recompute targets.
|
|
65
|
+
- **Default `Platform` device property removed.** 59 entries commented out in `lib/templates/defaults.js` (15 iOS + 15 Android + 29 Desktop). `os` already carries the platform signal. If a dungeon reads `Platform` in hooks or downstream, you'll see undefined — define `Platform` explicitly in your event properties to opt back in.
|
|
66
|
+
- **`hasAnonIds` deprecated.** Use `identity.avgDevicePerUser: 1` instead. The deprecated alias still works through 1.5.x: when `hasAnonIds: true` is set without an explicit `avgDevicePerUser`, the validator promotes to `avgDevicePerUser: 1`.
|
|
67
|
+
- **`DATASET_NOW` / `DATASET_BEGIN` scoped via AsyncLocalStorage.** No more module-level mutable globals. In-process concurrent `generate()` calls with different `datasetStart`/`datasetEnd` windows now produce isolated, in-window output. Legacy `setDatasetNow` / `setDatasetBegin` setters remain as back-compat shims.
|
|
68
|
+
- **UTC bare-date parsing** for `datasetStart` / `datasetEnd`. `"2026-01-01"` parses as `2026-01-01T00:00:00Z`, not as local-midnight (which shifted the window by UTC offset).
|
|
69
|
+
- **Quiet by default.** `verbose: false` (default) now gates all warnings, info logs, and dataset-context messages. Set `verbose: true` to opt back into the chatty output.
|
|
70
|
+
- **GCS upload retry hardening.** 10 retries with exponential backoff, 10-minute total budget. Handles transient cloud upload failures without giving up.
|
|
71
|
+
- **>64K user runs no longer crash V8.** Internal data structure swap (`Array` for sparse-keyed maps) eliminates V8 limit hit at high user counts.
|
|
72
|
+
- **All 48 shipped dungeons restructured to canonical layout.** Sections in fixed order: IMPORTS → OVERVIEW → HOOK STORIES → SCALE → KNOBS → DATA ARRAYS → HOOK STATE → HELPER FUNCTIONS → CONFIG. Hook stories preserve full per-hook Mixpanel report docs; `config.hook` becomes a thin dispatcher delegating to per-type helpers (`handleEventHooks`, `handleEverythingHooks`, etc.). Zero behavioral changes — every dungeon's seed-pinned output is unchanged.
|
|
73
|
+
- **All 49 dungeons + ~18 test fixtures migrated to the new sub-object API.** Cosmetic adoption only — `mergeConfigSubObjects` already supported both shapes since Phase 1.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- **Standalone events now stamp `config.superProps`.** Was silently `{}` before — masked by the validator's auto-funnel pre-fix in 1.5.0. Surfaced by the `numEvents` overshoot fix when the `useFunnel` gate started routing more users to the standalone path.
|
|
78
|
+
- **Born-late funnel auth events past `FIXED_NOW` no longer set `userAuthTimeMs`.** Engine drops the event at storage time (`funnels.js:640`) but previously still recorded the auth-time, marking the user as authed without a real sign_up event. Fix gates `authTimeMs` on `!_drop` (`funnels.js:328`). Affected 1-2% of users in test runs.
|
|
79
|
+
- **Pre-existing user events strict-clamp at `FIXED_BEGIN`.** Born-outside-window users no longer leak events into the pre-dataset window via TimeSoup's sub-window distribution.
|
|
80
|
+
|
|
81
|
+
### Docs
|
|
82
|
+
|
|
83
|
+
- **HOOKS.md targeted edits.** Recipe 4.25 (First-Touch Attribution Bias) gains a v1.5 note pointing to Recipe 4.26's OVERWRITE pattern when `hasCampaigns: true`. Atom catalog gains a footnote that `injectBetween` / `injectBurst` / `injectAfterEvent` / `injectOnNewDays` no longer require trailing `record.sort(...)` calls — covered by `autoSortAfterEverything: true` default since 1.5.0.
|
|
84
|
+
- **`docs/guides/1.5.1-upgrade-guide.md`** — TL;DR + per-change action items for existing dungeon authors.
|
|
85
|
+
|
|
86
|
+
### Infra
|
|
87
|
+
|
|
88
|
+
- 95+ commits across the branch; 1269 vitest tests pass; engine canary 10/10; engine-shape full sweep 194/194; smoke test 20/20 verticals; 5-vertical hook verifier matches Sprint 1 baseline (ecommerce 10/10, fitness 12/12, sass 10/10, social 11/11, dating 11/13 pre-existing small-mode artifacts).
|
|
89
|
+
|
|
5
90
|
## 1.5.0 — 2026-05-08
|
|
6
91
|
|
|
7
92
|
The "count and verify like Mixpanel does" release. Aligns BOTH the data generation engine AND the verifier with Mixpanel's actual counting semantics — greedy single-pass funnels, distinct-period frequency counting, null-aware aggregation, touchpoint-capped attribution, identity merge, retention, sessions, time-bucketed trends. Removes `bunchIntoSessions`, the root cause of funnel ordering corruption since 1.0.
|
package/HOOKS.md
CHANGED
|
@@ -1183,6 +1183,13 @@ first-touch result as stamping 10 — Mixpanel's attribution module
|
|
|
1183
1183
|
(`attributed_value_reader.cpp`) only considers `TOUCHPOINTS_LIMIT = 10`. Aim
|
|
1184
1184
|
for sparse, deterministic touches.
|
|
1185
1185
|
|
|
1186
|
+
**v1.5 with `hasCampaigns: true`:** when the engine has already stamped UTMs
|
|
1187
|
+
on up to `maxTouchpointsPerUser` events per user (default 10), DO NOT stamp
|
|
1188
|
+
fresh touches in your hook — they'd push the user past the cap and fall
|
|
1189
|
+
outside Mixpanel's last-10 window. Use [Recipe 4.26](#426-bias-engine-stamped-touches-v15)
|
|
1190
|
+
to OVERWRITE the engine's `utm_source` on the existing stamped events
|
|
1191
|
+
instead. See [§2.4](#24-attribution-caps-at-10-touchpoints).
|
|
1192
|
+
|
|
1186
1193
|
---
|
|
1187
1194
|
|
|
1188
1195
|
#### 4.26 Bias Engine-Stamped Touches (v1.5)
|
|
@@ -1326,6 +1333,12 @@ Import from `@ak--47/dungeon-master/hook-helpers`:
|
|
|
1326
1333
|
| `isPreAuthEvent` | identity | `(event, authTime) -> boolean` | Check if before user's stitch |
|
|
1327
1334
|
| `splitByAuth` | identity | `(events, authTime) -> { preAuth, postAuth, stitch }` | Partition by auth boundary |
|
|
1328
1335
|
|
|
1336
|
+
**Inject atoms + v1.5:** the engine auto-sorts events by time after the
|
|
1337
|
+
`everything` hook (`autoSortAfterEverything: true` default — see Principle
|
|
1338
|
+
26). Hooks using `injectBetween` / `injectBurst` / `injectAfterEvent` /
|
|
1339
|
+
`injectOnNewDays` no longer need a trailing `record.sort(...)` to keep the
|
|
1340
|
+
greedy funnel engine happy.
|
|
1341
|
+
|
|
1329
1342
|
Full JSDoc in [`lib/hook-helpers/*.js`](lib/hook-helpers/).
|
|
1330
1343
|
|
|
1331
1344
|
---
|