@ak--47/dungeon-master 1.4.4 → 1.5.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/.claude/skills/analyze-soup/SKILL.md +158 -0
- package/.claude/skills/create-dungeon/SKILL.md +464 -0
- package/.claude/skills/verify-dungeon/SKILL.md +157 -0
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
- package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
- package/.claude/skills/write-hooks/SKILL.md +468 -0
- package/CHANGELOG.md +147 -0
- package/HOOKS.md +1243 -597
- package/README.md +140 -5
- package/dungeons/technical/ad-spend.js +1 -1
- package/dungeons/technical/anonymous-users.js +1 -1
- package/dungeons/technical/array-of-object-lookup.js +1 -1
- package/dungeons/technical/datagen-v15-verify.js +74 -0
- package/dungeons/technical/experiments.js +1 -1
- package/dungeons/technical/foobar.js +1 -1
- package/dungeons/technical/group-analytics.js +1 -1
- package/dungeons/technical/mirror-strategies.js +1 -1
- package/dungeons/technical/nested-objects.js +1 -1
- package/dungeons/technical/retention-cadence.js +1 -1
- package/dungeons/technical/sanity.js +1 -1
- package/dungeons/technical/scale-test.js +1 -1
- package/dungeons/technical/scd.js +1 -1
- package/dungeons/technical/simple.js +1 -1
- package/dungeons/technical/simplest.js +74 -20
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +4 -0
- package/dungeons/vertical/community.js +9 -3
- package/dungeons/vertical/crypto.js +5 -0
- package/dungeons/vertical/dating.js +23 -10
- package/dungeons/vertical/devtools.js +10 -0
- package/dungeons/vertical/ecommerce.js +6 -0
- package/dungeons/vertical/education.js +11 -0
- package/dungeons/vertical/fintech.js +13 -0
- package/dungeons/vertical/fitness.js +10 -0
- package/dungeons/vertical/food-delivery.js +9 -0
- package/dungeons/vertical/gaming.js +10 -0
- package/dungeons/vertical/healthcare.js +5 -0
- package/dungeons/vertical/insurance-application.js +10 -0
- package/dungeons/vertical/logistics.js +8 -1
- package/dungeons/vertical/marketplace.js +7 -0
- package/dungeons/vertical/media.js +8 -0
- package/dungeons/vertical/real-estate.js +7 -1
- package/dungeons/vertical/sass.js +12 -0
- package/dungeons/vertical/social.js +9 -0
- package/dungeons/vertical/travel.js +5 -0
- package/index.js +45 -7
- package/lib/core/config-validator.js +270 -7
- package/lib/core/context.js +58 -0
- package/lib/core/dungeon-loader.js +2 -5
- package/lib/generators/events.js +12 -13
- package/lib/generators/funnels.js +72 -1
- package/lib/hook-helpers/index.js +1 -0
- package/lib/hook-helpers/inject.js +95 -0
- package/lib/orchestrators/mixpanel-sender.js +27 -1
- package/lib/orchestrators/user-loop.js +488 -29
- package/lib/templates/macro-presets.js +39 -9
- package/lib/utils/utils.js +16 -79
- package/lib/verify/counting.js +320 -0
- package/lib/verify/emulate-breakdown.js +512 -108
- package/lib/verify/funnel-engine.js +539 -0
- package/lib/verify/identity.js +78 -0
- package/lib/verify/index.js +19 -0
- package/lib/verify/verify-dungeon.js +58 -0
- package/package.json +4 -2
- package/types.d.ts +314 -4
- package/scripts/smoke-test-all.mjs +0 -162
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-hooks
|
|
3
|
+
description: Use when an existing dungeon needs engineered story trends or "magic number" patterns — writes the `hook` function using atom helpers and high-level patterns. Adds no new event flags; never mutates the schema.
|
|
4
|
+
argument-hint: [path/to/dungeon.js] [free-text story / trend description]
|
|
5
|
+
model: claude-opus-4-6
|
|
6
|
+
effort: max
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Write Hooks
|
|
10
|
+
|
|
11
|
+
Engineer story trends into the dungeon at `$ARGUMENTS` (first positional arg)
|
|
12
|
+
based on the story description (remaining args).
|
|
13
|
+
|
|
14
|
+
## Scope
|
|
15
|
+
|
|
16
|
+
This skill writes the `hook` function ONLY. It assumes the dungeon's schema is
|
|
17
|
+
already complete (produced by `create-dungeon`). After writing, hand off to
|
|
18
|
+
`/verify-dungeon` to confirm the engineered patterns actually appear.
|
|
19
|
+
|
|
20
|
+
In scope:
|
|
21
|
+
- The `hook: function(record, type, meta) { ... }` body
|
|
22
|
+
- Documentation comments above the hook explaining each engineered pattern,
|
|
23
|
+
including a reference Mixpanel report block per pattern
|
|
24
|
+
|
|
25
|
+
Out of scope:
|
|
26
|
+
- Schema changes (events, properties, funnels, superProps, userProps).
|
|
27
|
+
Only modify schema if the hook can't possibly work without a new field —
|
|
28
|
+
and even then, prefer changing the value enumeration over adding a new field.
|
|
29
|
+
- New top-level config knobs.
|
|
30
|
+
- Removing the `hook: function...` body to start over with a new schema.
|
|
31
|
+
|
|
32
|
+
## Reference reading
|
|
33
|
+
|
|
34
|
+
- `lib/hook-helpers/index.js` — atoms (cohort, mutate, timing, inject,
|
|
35
|
+
identity). One file per group; full JSDoc on each atom.
|
|
36
|
+
- `lib/hook-patterns/index.js` — high-level recipes (one per Mixpanel
|
|
37
|
+
analysis type).
|
|
38
|
+
- `lib/verify/emulate-breakdown.js` — what `verify-dungeon` will check.
|
|
39
|
+
- `dungeons/user/my-buddy.js` — reference dungeon using a mix of atoms and
|
|
40
|
+
hand-rolled logic.
|
|
41
|
+
- `dungeons/technical/pattern-*.js` — five minimal pattern fixtures, one per
|
|
42
|
+
recipe.
|
|
43
|
+
- `HOOKS.md` — encyclopedia of hook recipes organized by story pattern. Contains
|
|
44
|
+
17+ worked examples with code snippets, Mixpanel report instructions, and
|
|
45
|
+
adaptation notes. **Start here** to find the right pattern for your story.
|
|
46
|
+
|
|
47
|
+
## Hook execution model
|
|
48
|
+
|
|
49
|
+
Hooks fire in this order per user (see `CLAUDE.md` for the canonical reference):
|
|
50
|
+
|
|
51
|
+
1. `"user"` — profile created. Mutate in place; return ignored.
|
|
52
|
+
2. `"scd-pre"` — SCD entries created. Mutate in place OR return new array.
|
|
53
|
+
3. For each funnel: `"funnel-pre"` → `"event"` (per step) → `"funnel-post"`.
|
|
54
|
+
|
|
55
|
+
**`funnel-pre` is now reliable for temporal patterns.** Usage funnels advance a
|
|
56
|
+
cursor after each run, so successive `meta.firstEventTime` values spread across
|
|
57
|
+
the user's active window. Persona and world-event modifiers apply BEFORE the
|
|
58
|
+
hook — the hook has final authority on `conversionRate`, `timeToConvert`, and
|
|
59
|
+
`props`.
|
|
60
|
+
4. `"event"` — for non-funnel standalone events. Return value REPLACES the event.
|
|
61
|
+
5. `"everything"` — array of ALL the user's events. Return array to replace.
|
|
62
|
+
|
|
63
|
+
**Most engineered trends belong in `everything`.** It sees the full user stream,
|
|
64
|
+
has access to `meta.profile` / `meta.scd` / `meta.authTime` / `meta.isPreAuth`,
|
|
65
|
+
and you can mutate freely.
|
|
66
|
+
|
|
67
|
+
Storage-only hooks (`ad-spend`, `group`, `mirror`, `lookup`) fire later in the
|
|
68
|
+
pipeline and don't see the same `meta` shape.
|
|
69
|
+
|
|
70
|
+
## Hook meta — identity context
|
|
71
|
+
|
|
72
|
+
Inside `funnel-pre` and `funnel-post`:
|
|
73
|
+
- `meta.isFirstFunnel: boolean`
|
|
74
|
+
- `meta.isBorn: boolean` (user born inside dataset window)
|
|
75
|
+
- `meta.attemptsConfig: { min, max, conversionRate? } | null`
|
|
76
|
+
- `meta.attemptNumber, meta.totalAttempts, meta.isFinalAttempt`
|
|
77
|
+
|
|
78
|
+
Inside `everything`:
|
|
79
|
+
- `meta.authTime: number | null` — unix-ms of the stitch event, null if never authed
|
|
80
|
+
- `meta.isPreAuth(event): boolean` — convenience predicate
|
|
81
|
+
|
|
82
|
+
Pattern: gate trend logic on `meta.isFinalAttempt` so failed prior attempts
|
|
83
|
+
don't get the same treatment as the converted attempt.
|
|
84
|
+
|
|
85
|
+
Inside `funnel-pre` and `funnel-post` (when experiment is active):
|
|
86
|
+
- `meta.experiment.name: string` — experiment name
|
|
87
|
+
- `meta.experiment.variantName: string` — assigned variant
|
|
88
|
+
- `meta.experiment.variantIndex: number` — 0-based index
|
|
89
|
+
- `meta.experiment.conversionMultiplier: number`
|
|
90
|
+
- `meta.experiment.ttcMultiplier: number`
|
|
91
|
+
- `meta.experiment` is `null` when no experiment or funnel run is before start date
|
|
92
|
+
|
|
93
|
+
Pattern: use `funnel-post` + `meta.experiment` to inject variant-specific
|
|
94
|
+
downstream effects:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
if (type === 'funnel-post' && meta.experiment) {
|
|
98
|
+
if (meta.experiment.variantName === 'Variant B') {
|
|
99
|
+
// Winner variant: inject downstream engagement event
|
|
100
|
+
const last = record[record.length - 1];
|
|
101
|
+
record.push(cloneEvent(last, {
|
|
102
|
+
event: 'Agenda Generated',
|
|
103
|
+
time: dayjs(last.time).add(5, 'minutes').toISOString(),
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Atom + pattern catalog
|
|
110
|
+
|
|
111
|
+
### Atoms (`@ak--47/dungeon-master/hook-helpers`)
|
|
112
|
+
|
|
113
|
+
| File | Atom | Purpose |
|
|
114
|
+
|------|------|---------|
|
|
115
|
+
| cohort | `binUsersByEventCount(events, eventName, bins)` | Classify by per-user event count (Insights total events) |
|
|
116
|
+
| cohort | `binUsersByEventInRange(events, eventName, start, end, bins)` | Same, time-windowed |
|
|
117
|
+
| cohort | `countEventsBetween(events, eventA, eventB)` | Count between first A and first B |
|
|
118
|
+
| cohort | `userInProfileSegment(profile, key, values)` | Profile-property cohort check |
|
|
119
|
+
| mutate | `cloneEvent(template, overrides)` | Spread+override a template event |
|
|
120
|
+
| mutate | `dropEventsWhere(events, predicate)` | In-place filter with count |
|
|
121
|
+
| mutate | `scaleEventCount(events, eventName, factor)` | >1 clones, <1 drops. **Targets Insights Total reports.** For frequency-distribution movement use `injectOnNewDays` |
|
|
122
|
+
| mutate | `scalePropertyValue(events, predicate, prop, factor)` | Multiply numeric prop |
|
|
123
|
+
| mutate | `shiftEventTime(event, deltaMs)` | Shift one event's time |
|
|
124
|
+
| timing | `scaleTimingBetween(events, A, B, factor)` | Scale gap between first A and next B |
|
|
125
|
+
| timing | `scaleFunnelTTC(funnelEvents, factor)` | Scale all step offsets from anchor |
|
|
126
|
+
| timing | `findFirstSequence(events, [names], maxGapMin)` | Detect ordered run within window |
|
|
127
|
+
| inject | `injectAfterEvent(events, source, template, gapMs, overrides)` | Splice clone after source |
|
|
128
|
+
| inject | `injectBetween(events, A, B, template, overrides)` | Splice at midpoint |
|
|
129
|
+
| inject | `injectBurst(events, template, count, anchorTime, spreadMs, overrides)` | Burst around anchor |
|
|
130
|
+
| inject | `injectOnNewDays(events, eventName, targetDistinctDays)` | **Cohort-only.** Spreads injections across previously-empty days. Use for cohort-conditional active-day boosts; for global active-day shape, use `Dungeon.avgActiveDaysPerUser` config knob. |
|
|
131
|
+
| identity | `isPreAuthEvent(event, authTime)` | Standalone variant of meta.isPreAuth |
|
|
132
|
+
| identity | `splitByAuth(events, authTime)` | { preAuth, postAuth, stitch } partition |
|
|
133
|
+
|
|
134
|
+
### Hook anti-patterns
|
|
135
|
+
|
|
136
|
+
- **DO NOT engineer global active-day distribution in hooks.** Use the
|
|
137
|
+
`Dungeon.avgActiveDaysPerUser` config knob — it's a concentrator that
|
|
138
|
+
preserves total event count while clustering events onto fewer days.
|
|
139
|
+
Hooks own cohort-conditional patterns ("premium users get 7+ days") only.
|
|
140
|
+
|
|
141
|
+
### Intentional strict-bar deviation is OK
|
|
142
|
+
|
|
143
|
+
The engine guarantees the no-hook baseline (`dungeons/technical/simplest.js`)
|
|
144
|
+
satisfies the per-macro strict bar across the 194-combo sweep matrix
|
|
145
|
+
(see [CLAUDE.md "Engine guarantees"](../../../CLAUDE.md#engine-guarantees)).
|
|
146
|
+
**Hooks can intentionally violate the strict bar** for legitimate stories:
|
|
147
|
+
|
|
148
|
+
- **Decline + churn cohort** (engagementDecay or `everything`-hook event-drop)
|
|
149
|
+
produces tail_ratio < 0.4 — well below the decline bar's 0.4 floor. This is
|
|
150
|
+
the design intent of a sunset story.
|
|
151
|
+
- **Viral hook + persona-driven late-cohort lift** can push the spike above
|
|
152
|
+
the viral preset's 7.0 cap. Hockey-stick stories are louder than the engine
|
|
153
|
+
baseline.
|
|
154
|
+
- **World-event spike** (e.g., a launch-day burst of 5x normal volume) creates
|
|
155
|
+
a single-day right-edge spike above the spike cap.
|
|
156
|
+
|
|
157
|
+
When you write a hook that intentionally violates the strict bar, document the
|
|
158
|
+
deviation in the dungeon's overview JSDoc + the hook's pattern documentation
|
|
159
|
+
block. Engine-validation guarantees apply to **no-hook configs only**; hooks
|
|
160
|
+
own their shape.
|
|
161
|
+
- **DO NOT hand-sort `everything` hook output.** The engine auto-sorts events
|
|
162
|
+
ascending by time after `everything` returns (default ON; opt out via
|
|
163
|
+
`autoSortAfterEverything: false`). Cloned events with arbitrary timestamps
|
|
164
|
+
no longer need explicit sort calls.
|
|
165
|
+
- **DO NOT stamp UTM properties from scratch in attribution hooks.** The
|
|
166
|
+
engine caps UTM stamping at `maxTouchpointsPerUser` (default 10) per user,
|
|
167
|
+
sampled across lifetime. Stamping fresh would push users past the cap; your
|
|
168
|
+
stamps would land outside Mixpanel's last-10 lookback window. OVERWRITE
|
|
169
|
+
engine-stamped values instead (e.g., `event.utm_source = "google"` on
|
|
170
|
+
already-stamped touches).
|
|
171
|
+
|
|
172
|
+
### When to use the verifier primitives
|
|
173
|
+
|
|
174
|
+
When designing a story, check if any of the new primitives match before
|
|
175
|
+
writing a custom hook:
|
|
176
|
+
|
|
177
|
+
- **Retention curves** ("70% retain at day 1, 30% at day 7") — verify with
|
|
178
|
+
`emulateBreakdown({ type: 'retention', cohortEvent, returnEvent, dayBuckets })`.
|
|
179
|
+
No special hook needed; engineer cohort behavior via `engagementDecay`,
|
|
180
|
+
`dropEventsWhere`, or per-user filtering in `everything`.
|
|
181
|
+
- **Session metrics** ("avg session has 6 events, lasts 4 minutes") — verify
|
|
182
|
+
with `emulateBreakdown({ type: 'sessionMetrics' })`. Trust pre-stamped
|
|
183
|
+
`session_id`. Engineer via `avgEventsPerUserPerDay` + `engagementDecay`.
|
|
184
|
+
- **Reentry funnels** ("power users complete the funnel 3+ times") — set
|
|
185
|
+
`Funnel.reentry: true` (verifier hint). Engineer multiple completions via
|
|
186
|
+
`funnel-post` injecting cloned funnel sequences for that cohort.
|
|
187
|
+
- **Exclusion patterns** ("rage-clickers never convert") — declare an event
|
|
188
|
+
in `events[]` (e.g., `rage_click`), set `Funnel.exclusionEvents: ['rage_click']`.
|
|
189
|
+
The generator stamps it on non-converters; the verifier terminates the
|
|
190
|
+
attempt when it sees one.
|
|
191
|
+
- **HPC / per-cart funnels** ("checkout completion per item type") — use
|
|
192
|
+
`evaluateFunnelHPC(events, steps, holdProperty)` directly (not auto-routed
|
|
193
|
+
through `funnelFrequency`).
|
|
194
|
+
- **Step filters** ("only iOS users complete step 2") — set
|
|
195
|
+
`Funnel.stepFilters: { 1: { prop: 'platform', op: 'eq', value: 'iOS' }}`.
|
|
196
|
+
- **Time-series trends** ("conversion rises week over week") — wrap any
|
|
197
|
+
breakdown with `timeBucket: 'week'`. Engineer via temporal-windowed hooks
|
|
198
|
+
using `DATASET_START.add(N, 'days')`.
|
|
199
|
+
- **Identity-model dungeons** — when `avgDevicePerUser > 0` or
|
|
200
|
+
`hasAnonIds: true`, ALWAYS pass `profiles` to verification. Auto-builds
|
|
201
|
+
identity map merging pre-auth `device_id` events with post-auth `user_id`.
|
|
202
|
+
|
|
203
|
+
**Schema-first reminder:** exclusion events must be declared in `events[]`
|
|
204
|
+
before referencing them as `Funnel.exclusionEvents` — the validator throws
|
|
205
|
+
on undeclared entries.
|
|
206
|
+
|
|
207
|
+
### Patterns (`@ak--47/dungeon-master/hook-patterns`)
|
|
208
|
+
|
|
209
|
+
Higher-level recipes. Each maps to ONE Mixpanel analysis the verify-dungeon
|
|
210
|
+
emulator can re-derive.
|
|
211
|
+
|
|
212
|
+
| Pattern | Mixpanel analysis | Hook type |
|
|
213
|
+
|---------|-------------------|-----------|
|
|
214
|
+
| `applyFrequencyByFrequency` | Insights — count(A) by per-user count(B) | everything |
|
|
215
|
+
| `applyFunnelFrequencyBreakdown` | Funnels — completion by per-user count(X) | funnel-post |
|
|
216
|
+
| `applyAggregateByBin` | Insights — avg(prop X) by per-user count(B) | everything |
|
|
217
|
+
| `applyTTCBySegment` | Funnel TTC — broken down by user-property segment | funnel-post |
|
|
218
|
+
| `applyAttributedBySource` | Conversions by Source (first/last touch) | everything |
|
|
219
|
+
|
|
220
|
+
Use a pattern when the trend matches its analysis 1:1. Drop down to atoms when
|
|
221
|
+
the trend is bespoke or composite.
|
|
222
|
+
|
|
223
|
+
## Anti-flag-stamping rule (HARD WALL)
|
|
224
|
+
|
|
225
|
+
Hooks MUST NOT add new properties to records. The schema (config) defines what
|
|
226
|
+
properties exist; hooks modify VALUES of existing properties or inject events
|
|
227
|
+
cloned from existing ones.
|
|
228
|
+
|
|
229
|
+
DO NOT WRITE:
|
|
230
|
+
```js
|
|
231
|
+
record.is_whale = true; // ❌ flag-stamping
|
|
232
|
+
record.cohort = "engaged"; // ❌ flag-stamping
|
|
233
|
+
record.was_dropped = false; // ❌ flag-stamping
|
|
234
|
+
event.engineered_pattern_id = 5; // ❌ flag-stamping
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
DO WRITE:
|
|
238
|
+
```js
|
|
239
|
+
record.amount *= 3; // ✅ scale existing numeric prop
|
|
240
|
+
record.payday = true; // ✅ ONLY if `payday: [false]` exists in event config
|
|
241
|
+
const clone = cloneEvent(template, {time, user_id}); // ✅ clone existing event
|
|
242
|
+
events.push(clone); // ✅ inject from template
|
|
243
|
+
return events.filter(e => !shouldDrop(e)); // ✅ filter inside `everything`
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
If a trend genuinely needs a new property and the schema doesn't have it, add
|
|
247
|
+
the property to the EVENT CONFIG with a default value (typically `[null]` or
|
|
248
|
+
`[false]`), not via the hook.
|
|
249
|
+
|
|
250
|
+
## Identity-aware hook patterns
|
|
251
|
+
|
|
252
|
+
When a dungeon uses `isAuthEvent`, hooks can branch on auth state:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
hook: function(record, type, meta) {
|
|
256
|
+
if (type !== 'everything' || !Array.isArray(record)) return record;
|
|
257
|
+
// Drop pre-auth funnel attempts that fired errors — analytics cleanup
|
|
258
|
+
return record.filter(e => !(e.event === 'API Error' && meta.isPreAuth(e)));
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
When a dungeon uses funnel `attempts`, hooks can reach into individual attempts
|
|
263
|
+
via funnel-post meta:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
if (type === 'funnel-post' && meta.isFirstFunnel && !meta.isFinalAttempt) {
|
|
267
|
+
// Failed prior attempt — reduce its event count to model "abandoned quickly"
|
|
268
|
+
scaleEventCount(record, record[0].event, 0.5);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Pattern documentation block
|
|
273
|
+
|
|
274
|
+
Above the `hook` function (or in the dataset overview comment), document each
|
|
275
|
+
engineered pattern with a Mixpanel report block. This is what verify-dungeon
|
|
276
|
+
checks against and what consumers read to understand the dataset.
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
280
|
+
* 1. POWER USERS BUY 3X MORE (everything, applyFrequencyByFrequency)
|
|
281
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
282
|
+
*
|
|
283
|
+
* PATTERN: Users with 15+ Browse events buy 3× as often as users with <5 Browse.
|
|
284
|
+
*
|
|
285
|
+
* MIXPANEL REPORT:
|
|
286
|
+
* Type: Insights
|
|
287
|
+
* Event: "Purchase"
|
|
288
|
+
* Measure: Frequency Distribution
|
|
289
|
+
* Breakdown: per-user count of "Browse"
|
|
290
|
+
* Expected ratio: bin>=15 / bin<5 ≈ 3x (within ±15%)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Hook Ordering Within `everything`
|
|
294
|
+
|
|
295
|
+
The order of operations inside the everything hook matters when hooks interact:
|
|
296
|
+
|
|
297
|
+
1. **SuperProp stamping** — stamp profile values onto events (always first)
|
|
298
|
+
2. **Temporal value mutations that DON'T need cloned events** — e.g., version stamping
|
|
299
|
+
3. **Behavioral detection + event cloning** — agentic detection, KYC clones, pro clones, magic number clones
|
|
300
|
+
4. **Event filtering** — churn, retention, rate-limit drops
|
|
301
|
+
5. **Temporal value mutations that NEED cloned events** — e.g., spring price boost, gas spike, outage errors (always LAST before sort)
|
|
302
|
+
6. **Sort** — `userEvents.sort((a, b) => new Date(a.time) - new Date(b.time))`
|
|
303
|
+
|
|
304
|
+
**Why:** If a temporal mutation runs before cloning, cloned events that land in
|
|
305
|
+
the temporal window miss the mutation. Moving temporal value mutations to the
|
|
306
|
+
end ensures ALL events in the window — original and cloned — receive the effect.
|
|
307
|
+
|
|
308
|
+
## Deprecated Feature Replacement
|
|
309
|
+
|
|
310
|
+
When a dungeon relied on deprecated config blocks (`subscription`, `attribution`,
|
|
311
|
+
`features`, `geo`, `anomalies`) for properties that hooks depend on, those
|
|
312
|
+
properties no longer appear in the data. Replace them:
|
|
313
|
+
|
|
314
|
+
1. Add the property to `superProps` and `userProps` with default values
|
|
315
|
+
2. Assign meaningful values in the `user` hook (based on hash, persona, or profile)
|
|
316
|
+
3. Use the assigned values in `everything` to drive downstream effects
|
|
317
|
+
|
|
318
|
+
Example: deprecated `subscription` → add `subscription_tier` to superProps/userProps,
|
|
319
|
+
assign tiers by hash in user hook, gate conversion/feature effects on tier in everything.
|
|
320
|
+
|
|
321
|
+
## Cohort Sizing Guidelines
|
|
322
|
+
|
|
323
|
+
Cohort detection conditions must be selective enough to create a meaningful
|
|
324
|
+
control group, but not so broad they catch everyone:
|
|
325
|
+
|
|
326
|
+
| Detection | Problem | Fix |
|
|
327
|
+
|-----------|---------|-----|
|
|
328
|
+
| `events.some(e => e.event === X)` with common X | 90%+ of users qualify | Require 3+ events: `events.filter(...).length >= 3` |
|
|
329
|
+
| `charCodeAt(0) % 50 === 0` | Only 2% of users | Increase modulus denominator or use `% 10` for 10% |
|
|
330
|
+
| `profile.tier === "premium"` | Fixed by config distribution | Adjust userProps distribution if cohort too small |
|
|
331
|
+
| `earlyEvents.length >= 5` for a low-weight event | 0% qualify (impossible threshold) | Check actual distribution first, set at ~80th percentile |
|
|
332
|
+
|
|
333
|
+
Target: 10-30% of users in the affected cohort for clean signal at 10K users.
|
|
334
|
+
|
|
335
|
+
### Threshold Calibration
|
|
336
|
+
|
|
337
|
+
When a hook gates on "N+ events of type X in first Y days," check the actual
|
|
338
|
+
distribution BEFORE choosing the threshold. With 200 event types and 2.5
|
|
339
|
+
events/user/day, a weight-7 event might produce only ~0.2 per user per day.
|
|
340
|
+
Setting threshold=5 for 7 days means ~0% of users qualify. Run this check
|
|
341
|
+
in your smoke test:
|
|
342
|
+
|
|
343
|
+
```sql
|
|
344
|
+
SELECT n, COUNT(*) FROM (
|
|
345
|
+
SELECT user_id, COUNT(*) as n FROM events WHERE event = 'X' GROUP BY user_id
|
|
346
|
+
) GROUP BY n ORDER BY n LIMIT 15;
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Set the threshold at approximately the 80th percentile of the distribution.
|
|
350
|
+
|
|
351
|
+
### Compounding Drop Hooks
|
|
352
|
+
|
|
353
|
+
Use at most ONE drop-based retention hook per dungeon. Multiple hooks that
|
|
354
|
+
each drop events after the same day threshold compound destructively:
|
|
355
|
+
|
|
356
|
+
- Hook A: drop 40% after day 21 for non-loyal users
|
|
357
|
+
- Hook B: drop 60% after day 21 for non-streak users
|
|
358
|
+
- Combined: 76% drop for users in both groups (which is 95% of users)
|
|
359
|
+
|
|
360
|
+
The control group barely exists. Fix: use boost-based patterns
|
|
361
|
+
(`scaleEventCount(events, "X", 1.8)`) for positive cohorts instead of drops
|
|
362
|
+
for negative cohorts. Boosts are additive and don't interact destructively.
|
|
363
|
+
Reserve drops for a single churn/retention effect per dungeon.
|
|
364
|
+
|
|
365
|
+
## Common Hook Pitfalls
|
|
366
|
+
|
|
367
|
+
Apply these BEFORE handing off to `/verify-dungeon`. See HOOKS.md §9 for full recipes.
|
|
368
|
+
|
|
369
|
+
### isStrictEvent: false is NOT optional for hook-read events
|
|
370
|
+
|
|
371
|
+
If your hook reads `event === 'X'` and `X` is also a funnel-step event, the
|
|
372
|
+
validator auto-promotes it to `isStrictEvent: true` and the engine
|
|
373
|
+
won't emit standalone occurrences. Your cohort goes empty.
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
// BAD — login is a funnel step + read by hook
|
|
377
|
+
events: [{ event: 'login', weight: 4, properties: {...} }]
|
|
378
|
+
// GOOD — explicit opt-out preserves standalone occurrences
|
|
379
|
+
events: [{ event: 'login', weight: 4, isStrictEvent: false, properties: {...} }]
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Audit: any event referenced in the `everything` hook by name AND appearing
|
|
383
|
+
as a funnel step needs `isStrictEvent: false`.
|
|
384
|
+
|
|
385
|
+
### Reentry on per-instance loops
|
|
386
|
+
|
|
387
|
+
Funnels named "X loop" / "X cycle" / "session" / repeated user behaviors
|
|
388
|
+
need `Funnel.reentry: true`. Without it, the engine produces ONE funnel
|
|
389
|
+
sequence per user — no recurring loops. Examples that need it: workout
|
|
390
|
+
loop, match flow, search-to-book, order fulfillment, engagement loop, tour
|
|
391
|
+
funnel.
|
|
392
|
+
|
|
393
|
+
### Hash-based cohorts produce textbook signals
|
|
394
|
+
|
|
395
|
+
Cleanest hidden-cohort pattern. No flag, no schema mutation, easy to verify
|
|
396
|
+
deterministically:
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
// 2% whales with 50x trade amount → long-tail Insights distribution
|
|
400
|
+
const isWhale = uid.charCodeAt(0) % 50 === 0;
|
|
401
|
+
if (isWhale && e.event === 'swap') e.trade_amount_usd *= 50;
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Use a large multiplier (≥10x, ideally 50x) so the signal beats soup noise.
|
|
405
|
+
Use `% 50` for ~2% whales, `% 25` for ~4% bots, `% 10` for ~10% cohorts.
|
|
406
|
+
|
|
407
|
+
### Hook ordering inside `everything`
|
|
408
|
+
|
|
409
|
+
If hook A injects events that hook B mutates, B must run AFTER A in the
|
|
410
|
+
same `everything` block — otherwise the injected events miss B's mutation.
|
|
411
|
+
The existing "Hook Ordering Within `everything`" section above codifies
|
|
412
|
+
this; the eval revealed it as the single most common subtle bug.
|
|
413
|
+
|
|
414
|
+
### Avoid behavioral cohorts where the gating event IS the signal
|
|
415
|
+
|
|
416
|
+
If hook says "users who did X often → reduce X count", the verifier sees
|
|
417
|
+
inverted signal because users with high X naturally have higher absolute
|
|
418
|
+
counts even after reduction. Either:
|
|
419
|
+
- Use hash cohort for the same effect, OR
|
|
420
|
+
- Verify by per-user post/pre ratio instead of raw counts
|
|
421
|
+
|
|
422
|
+
### Don't reference profile.X unless X is a defined userProp
|
|
423
|
+
|
|
424
|
+
```js
|
|
425
|
+
// BAD — profile.level isn't in userProps; resolves to undefined
|
|
426
|
+
if (meta.profile.level >= 50) e.gold_earned *= 3;
|
|
427
|
+
// GOOD — verify by SPREAD instead, OR add level to userProps with weighted distribution
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
When the hook references a missing profile field, you can still get the
|
|
431
|
+
data spread you want (gold range), but the cohort can't be analytically
|
|
432
|
+
recovered. Either add the userProp or rewrite the hook to use a hash
|
|
433
|
+
cohort.
|
|
434
|
+
|
|
435
|
+
## Workflow
|
|
436
|
+
|
|
437
|
+
1. Read the dungeon at `$ARGUMENTS[0]` and understand the existing schema.
|
|
438
|
+
2. Translate the user's story description into 3–5 engineered patterns.
|
|
439
|
+
Consult `HOOKS.md` for recipe ideas that match the user's story. Each recipe
|
|
440
|
+
includes the hook type, code snippet, and Mixpanel report format.
|
|
441
|
+
3. For each pattern:
|
|
442
|
+
- Pick a pattern from `lib/hook-patterns/` if it fits the analysis 1:1.
|
|
443
|
+
- Otherwise compose atoms from `lib/hook-helpers/`.
|
|
444
|
+
- Document the pattern in a comment block (Mixpanel report instructions).
|
|
445
|
+
4. Write the `hook` function, importing atoms/patterns at the top of the file.
|
|
446
|
+
5. Smoke-test:
|
|
447
|
+
```bash
|
|
448
|
+
node scripts/verify-runner.mjs <dungeon> verify-dungeon --small
|
|
449
|
+
```
|
|
450
|
+
Confirm the run completes without errors.
|
|
451
|
+
6. Hand off:
|
|
452
|
+
```
|
|
453
|
+
/verify-dungeon <dungeon>
|
|
454
|
+
```
|
|
455
|
+
If verify-dungeon returns WEAK, NONE, or INVERSE on any pattern, return to
|
|
456
|
+
step 4 and refine. Iterate until all patterns score STRONG or NAILED.
|
|
457
|
+
|
|
458
|
+
## Stopping condition
|
|
459
|
+
|
|
460
|
+
Stop after `/verify-dungeon` reports all engineered patterns as STRONG or NAILED,
|
|
461
|
+
OR after three iterations without convergence — at that point, document what's
|
|
462
|
+
still off in the dungeon's overview comment and report the gap to the user.
|
|
463
|
+
|
|
464
|
+
## Output
|
|
465
|
+
|
|
466
|
+
Modify the dungeon file in place. Add the `hook` function. Add the imports.
|
|
467
|
+
Add the documentation block above the config. Do NOT modify any other file.
|
|
468
|
+
Tell the user to run `/verify-dungeon <dungeon>` next.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,153 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@ak--47/dungeon-master`.
|
|
4
4
|
|
|
5
|
+
## 1.5.0 — 2026-05-08
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
### Generator changes
|
|
10
|
+
|
|
11
|
+
#### Added
|
|
12
|
+
- **`avgActiveDaysPerUser`** — Concentrates events onto fewer distinct UTC days per user. Uses weighted-without-replacement day picking from soup DOW weights. Events per active day scale naturally (`rate × remaining_days ÷ active_days`). Interacts correctly with `engagementDecay` (protects last event per picked day from being dropped).
|
|
13
|
+
- **`conversionWindowDays`** on funnels — Explicit conversion window (default 30, hard cap 180). Validator auto-bumps when `timeToConvert` exceeds the default. Funnel generator caps step-to-step time to the window. Verifier and `emulateBreakdown` apply the same window.
|
|
14
|
+
- **`maxTouchpointsPerUser`** — Per-user touchpoint cap (default 10) matching Mixpanel's `attributed_value_reader.cpp`. Engine samples eligible events across user lifetime using `chance.pickset`, stamps UTMs on the sample only. Replaces the old inline 25% UTM stamping in `events.js`.
|
|
15
|
+
- **`autoSortAfterEverything`** — Auto-sorts user events by time after the `everything` hook (default `true`). Defends greedy funnel verification from out-of-order hook-injected events. Opt out with `autoSortAfterEverything: false`.
|
|
16
|
+
- **`isStrictEvent` auto-promote** — Config validator detects events that appear in both `events[]` and `funnels[].sequence` and auto-promotes them to `isStrictEvent: true`. Prevents greedy engine corruption where standalone instances of funnel-step events confound conversion counting. Opt out per-event with `isStrictEvent: false`. Runs BEFORE catch-all funnel creation.
|
|
17
|
+
- **`Funnel.exclusionEvents: string[]`** — events that terminate the funnel for non-converters. Generator stamps 1-2 cloned events bearing one of the listed names between the last completed step and where the next step would have been. Schema-first: validator throws on undeclared entries; cloned events copy ONLY identity + super props + group keys + props declared on the exclusion event's own config (no source-event prop pollution).
|
|
18
|
+
- **`Funnel.reentry: boolean`** — verifier-only hint. Auto-applied by `verifyDungeon` to matching `funnelFrequency` / `timeToConvert` checks.
|
|
19
|
+
- **`Funnel.stepFilters: Record<number, { prop, op, value }>`** — verifier-only hint. The verifier attaches `where`-clauses at the matching step index.
|
|
20
|
+
- **Session day-boundary split** — `assignSessionIds` now ends a session at the UTC day boundary (matches Mixpanel `session_query.cpp:828-830, 911`). Three reset triggers: timeout gap > 30 min, max session > 24h, OR day-index change.
|
|
21
|
+
- **`weightedSampleNoReplacement`** — Seeded weighted sampling utility for active-day picking and touchpoint selection.
|
|
22
|
+
|
|
23
|
+
#### Changed
|
|
24
|
+
- **`bunchIntoSessions` removed.** Was a wholesale timestamp overwrite that scrambled funnel ordering and destroyed TimeSoup's time distribution. Replaced by natural TimeSoup-driven timestamps + `assignSessionIds` (which was already running but had its work overwritten by `bunchIntoSessions`). Events now arrive in correct temporal order without post-hoc rewriting.
|
|
25
|
+
- **Standalone events use `isFirstEvent: false`.** Previously all standalone events used `isFirstEvent: true`, pinning them to the same timestamp. The old `bunchIntoSessions` retimed them — now TimeSoup distributes them directly.
|
|
26
|
+
- **UTM stamping moved to per-user pass.** Inline per-event UTM stamping in `events.js` replaced by `applyTouchpointCap` in `user-loop.js`. Runs after all events are generated, samples up to `maxTouchpointsPerUser` eligible events. Matches Mixpanel's attribution counting behavior.
|
|
27
|
+
- **Funnel generator respects conversion window.** When a funnel's step-to-step span exceeds `conversionWindowDays`, the generator scales `relativeTimeMs` to fit.
|
|
28
|
+
- **`funnelFeatureCtx` preserves `latestTime`.** Bug fix: was dropping `featureCtx.latestTime`, causing funnel first events to use the full `[earliest, FIXED_NOW]` range instead of the picked day's bounds in active-day mode.
|
|
29
|
+
- **Empty event pool bail-out.** When all events are funnel steps (auto-promoted to strict) and there are no standalone events, the user loop skips standalone generation instead of crashing on `pick([])`.
|
|
30
|
+
- **Future-event filter now logs in verbose mode.** Events past `FIXED_NOW` (from catch-all funnel TTC drift) are filtered with a verbose log showing count, user, and time range.
|
|
31
|
+
- **`buildActiveDayPlan` returns `pickedDayBuckets`.** Shape changed from `number[] | null` to `{ plan, pickedDayBuckets } | null` so engagement decay can protect last events on active days.
|
|
32
|
+
- **User loop wrapped in try/finally.** SIGINT cleanup (progress interval, user count reset) runs even on error.
|
|
33
|
+
|
|
34
|
+
### Verifier changes
|
|
35
|
+
|
|
36
|
+
The "verify like Mixpanel does" half. Adds counting primitives behind `emulateBreakdown` so engineered hook patterns can be verified against the same shapes Mixpanel computes.
|
|
37
|
+
|
|
38
|
+
#### Added
|
|
39
|
+
- **Identity resolution** — `buildIdentityMap(profiles)` inverts each profile's `device_ids` / `anonymousIds` (legacy field name supported) into a flat `Map<device_id, canonical_user_id>`. `resolveUserId(event, identityMap)` resolves a single event with priority: `event.distinct_id` → identity map → `event.user_id` → `event.device_id` (Mixpanel canonical post-merge id wins). `emulateBreakdown` auto-builds the map when `profiles` are passed (any breakdown type, hoisted above `timeBucket` recursion to avoid rebuild per partition).
|
|
40
|
+
- **Funnel engine extensions** — `evaluateFunnel` accepts:
|
|
41
|
+
- `reentry: boolean` — re-runs state machine after each completion; `result.completions` reports total.
|
|
42
|
+
- `exclusionSteps: [{ event, afterStep?, beforeStep? }]` — events that terminate the current attempt. `afterStep`/`beforeStep` use the index of the step that must (have) been reached; defaults `afterStep=-Infinity`, `beforeStep=steps.length` (fires anywhere, used by simple `Funnel.exclusionEvents` shape). Cooperates with reentry.
|
|
43
|
+
- Step filters — steps may be `{ event, where: { prop, op, value } }`. Supported ops: eq, neq, gt, lt, gte, lte, contains, not_contains.
|
|
44
|
+
- `trackStepProperties: boolean | string[]` — captures matched event properties at each step into `result.stepProperties`.
|
|
45
|
+
- `countMode: 'uniques' | 'totals'` — totals mode returns `FunnelResult[]` (Mixpanel `funnel_query.cpp:2055-2100`). Includes incomplete attempts so per-step drop-off counts are preserved (`history_get_reached >= 0`, NOT "completed"). Without reentry: single-attempt array.
|
|
46
|
+
- `sessionScoped: boolean` — partition by `session_id`, run per session. Verifier-only convenience; Mixpanel's closest analog is `WINDOW_TYPE_SESSIONS` on the conversion window.
|
|
47
|
+
- **HPC** — `evaluateFunnelHPC(events, steps, holdProperty, options)` runs parallel sub-funnels per unique value of the held property on the step-0 event. Returns `Map<value, FunnelResult | FunnelResult[]>`. NOT auto-routed through `funnelFrequency` (different report shape); call directly inside `verifyDungeon` checks.
|
|
48
|
+
- **Segment modes** — `resolveFunnelSegment(result, 'first' | 'last' | { step: N })` picks property snapshot for FIRST_TOUCH / LAST_TOUCH / STEP modes.
|
|
49
|
+
- **`emulateBreakdown({ type: 'sessionMetrics' })`** — group by user→session, emit `[{ metric, avg, median, p90, total_sessions }]` for count / duration / eventsPerSession. Trusts pre-stamped `session_id`. Optional `event` filter restricts to sessions containing a target event (verifier-only convenience).
|
|
50
|
+
- **`emulateBreakdown({ type: 'retention' })`** — birth-anchored ms-delta bucketed retention (`retention_query.cpp:1227-1231`). A return 23h after birth lands in bucket 0; 25h lands in bucket 1. `birthCanRetain: false` default (`retention_query.cpp:1097-1109`). Inputs `cohortEvent`, `returnEvent`, `dayBuckets`. Optional `segmentBy` partitions cohort by birth event property (`segment_event=FIRST` mode); optional `carry_forward` marks once-retained users as retained on later buckets (CARRY_FORWARD unbounded mode).
|
|
51
|
+
- **`emulateBreakdown({ timeBucket: 'day' | 'week' | 'month' })`** — cross-cutting wrapper on every breakdown type. Partitions events by UTC bucket, tags rows with `period: string` (`YYYY-MM-DD`, `YYYY-Www`, `YYYY-MM`). Optional `timeBucketRange: { from, to }` enumerates every bucket and emits `{ period, _empty: true }` markers for empty intervals (Mixpanel `normal_query.cpp:352-356` parity).
|
|
52
|
+
- **`aggregatePerUser` cohort-level rollup** — `cohort_sum` / `cohort_min` / `cohort_max` field added when the per-user `agg` is `sum`/`count`/`min`/`max`. `avg_aggregate` always populated. Matches Mixpanel's "Aggregate per user" report column for the corresponding agg mode.
|
|
53
|
+
- **`partitionByTimeBucket(events, bucket, options?)`** — exposed helper. Accepts `{ from, to }` for empty-bucket enumeration.
|
|
54
|
+
- **`evaluateAnyOrderCompletion`** — Verifier function for `unordered`/`random` funnel modes. `emulateBreakdown` auto-dispatches based on `funnel.order`.
|
|
55
|
+
|
|
56
|
+
#### Changed
|
|
57
|
+
- **Identity resolver order** — `event.distinct_id` now wins over the merge map (Mixpanel canonical post-merge id; never demote).
|
|
58
|
+
- **Sessions split on UTC day** — `session_query.cpp:828-830` parity (added to `assignSessionIds` AND `sessionMetrics`).
|
|
59
|
+
|
|
60
|
+
### Documented divergences (intentional v1.5.0 scope gaps)
|
|
61
|
+
|
|
62
|
+
- **HPC list-property values** — scalar only; Mixpanel `aggregate_hash_get_key_cursor` explodes list values into N sub-funnels per event.
|
|
63
|
+
- **`sessionScoped` funnel** + **`sessionMetrics({ event })`** — verifier-only conveniences; not directly reproducible in Mixpanel UI.
|
|
64
|
+
- **Retention COMPOUNDED, CARRY_BACK, CONSECUTIVE_FORWARD, CALENDAR_START, segment_event=SECOND, cohort window, week/month bucket units** — out of v1.5.0 scope.
|
|
65
|
+
- **Timezone** — verifier uses UTC; Mixpanel uses query timezone (qtz).
|
|
66
|
+
- **Percentiles** — linear interpolation (d3.quantile); Mixpanel uses TDigest.
|
|
67
|
+
- **Selector grammar** — eq/neq/gt/lt/gte/lte/contains/not_contains only; no is_set/between/regex/contains_ci.
|
|
68
|
+
|
|
69
|
+
### Backward Compatibility
|
|
70
|
+
|
|
71
|
+
- **No breaking changes to the public API.** `DUNGEON_MASTER(config)` signature unchanged. All named exports unchanged.
|
|
72
|
+
- Existing dungeons run without modification. New config fields are additive and optional.
|
|
73
|
+
- `bunchIntoSessions` removal changes timestamp distribution for all dungeons. Events now follow TimeSoup's natural distribution instead of being rewritten into synthetic session clusters. This is more correct — funnels maintain temporal ordering.
|
|
74
|
+
- `isStrictEvent` auto-promote may reduce standalone event variety for dungeons where funnel-step events overlap with `events[]`. Add `isStrictEvent: false` on specific events to preserve standalone instances.
|
|
75
|
+
- Touchpoint cap (default 10) reduces UTM-stamped events from ~25% of all events to at most 10 per user. Attribution analysis produces more realistic distributions.
|
|
76
|
+
- UTC day-boundary session split may produce more sessions per user than 1.4 (sessions crossing midnight now split). Session metrics shift accordingly.
|
|
77
|
+
|
|
78
|
+
### Documentation
|
|
79
|
+
|
|
80
|
+
- **`research/1.5.0-upgrade-guide.md`** — consumer upgrade guide with behavioral changes, new verifier capabilities, identity-aware verification requirement.
|
|
81
|
+
- **CLAUDE.md** updated with `avgActiveDaysPerUser`, `conversionWindowDays`, `maxTouchpointsPerUser`, `autoSortAfterEverything`, active-day distribution section, 15-step execution order.
|
|
82
|
+
- **HOOKS.md** — §2.4 touchpoint generation contract, §2.5 active-day distribution, §2.6–2.10 (sessions, retention, reentry, HPC, segment modes), §8 v1.5.0 verification recipes (8 patterns).
|
|
83
|
+
- **Skill files** updated: `create-dungeon`, `write-hooks`, `verify-dungeon` with v1.5 considerations + new primitive table.
|
|
84
|
+
|
|
85
|
+
### Engine validation + strict clamps (post-eval ship gate)
|
|
86
|
+
|
|
87
|
+
Final 1.5.0 hardening pass — proves the engine produces clean, in-band charts across the param space and adds validator guards against the worst foot-guns. Methodology + sweep evidence: `plans/ENGINE-VALIDATION/FIX.md`.
|
|
88
|
+
|
|
89
|
+
#### Engine
|
|
90
|
+
|
|
91
|
+
- **`FUNNEL_DEAD_ZONE_CAP_SEC = 0`** (`lib/orchestrators/user-loop.js`). Earlier rounds reserved a 1-day "dead zone" before `FIXED_NOW` for funnel step-1 anchors to defend against a cursor-accumulation bug. Round 1 fixed the cursor accumulation directly, leaving the cap as defense-in-depth. The future-time guard at storage step 14 already drops any `time > FIXED_NOW`, so removing the cap is safe — funnels can now anchor right up to `FN`. Eliminates the last-day cliff for funnel-heavy dungeons. Verified across 194-combo sweep: `futureEvents == 0` everywhere.
|
|
92
|
+
|
|
93
|
+
#### Validator strict clamps (`lib/core/config-validator.js`)
|
|
94
|
+
|
|
95
|
+
Seven clamps with `console.warn` messages explaining what changed and why. All fire either unconditionally (sanity bounds) or only when the user explicitly overrides via top-level field OR `macro: { preset, ... }` object override (raw preset names exempt — preset values are designed to be safe).
|
|
96
|
+
|
|
97
|
+
| # | Clamp | Trigger | Action |
|
|
98
|
+
|---|-------|---------|--------|
|
|
99
|
+
| 1 | `percentUsersBornInDataset` ∈ [0, 100] | Always | Clamp + warn |
|
|
100
|
+
| 2 | Per-macro born cap (flat=12, steady=12, growth=30, viral=55, decline=5) | User-explicit `macro` AND user-explicit `percentUsersBornInDataset` | Clamp + warn |
|
|
101
|
+
| 3 | `bornRecentBias` ∈ [-0.5, 0.5] | User-explicit (incl. macro-object override) | Clamp + warn |
|
|
102
|
+
| 4 | Compound: `born > 60 && bias > 0.4` → bias=0.3 | User-explicit (either) | Clamp + warn |
|
|
103
|
+
| 5 | `bornRecentBias` ∈ [-1, 1] (`Math.pow` guard) | Always | Clamp |
|
|
104
|
+
| 6 | `avgEventsPerUserPerDay` > 50 → 50 | Always | Clamp + warn (recompute `numEvents`) |
|
|
105
|
+
| 7 | `avgActiveDaysPerUser` > `numDays * 0.5` → `floor(numDays * 0.5)` | Always | Clamp + warn |
|
|
106
|
+
|
|
107
|
+
Plus a warning-only check for `numDays < 14` (window may be pinned via `datasetStart`/`datasetEnd` upstream).
|
|
108
|
+
|
|
109
|
+
#### Sweep harness (`scripts/sweep-engine.mjs`)
|
|
110
|
+
|
|
111
|
+
Validates `dungeons/technical/simplest.js` (no-hook baseline) across a 194-combo cross-product matrix of macro × numDays × born × rate × activeDays. Per-macro strict bars match each preset's design intent (flat is stationary, viral is hockey-stick). Pinned to most-recent past Wednesday-EOD-UTC anchor for full calendar-day determinism — back-to-back runs produce zero metric drift. **194 / 194 PASS.**
|
|
112
|
+
|
|
113
|
+
`tests/unit/engine-shape-canary.test.js` — 10-test ~5s canary (runs every commit) with fixed-date pinning (`datasetEnd = '2026-04-30T23:59:59Z'`).
|
|
114
|
+
|
|
115
|
+
`tests/e2e/engine-shape-full-sweep.test.js` — gated by `RUN_FULL_SWEEP=1`, runs the full 194-combo matrix (~5.5 min). Pre-release acceptance gate.
|
|
116
|
+
|
|
117
|
+
#### Hook compatibility
|
|
118
|
+
|
|
119
|
+
Spot-checked 5 verticals post-fix: **56 / 56 hook checks PASS** (fitness 12, dating 13, ecommerce 10, sass 10, social 11). Hook magnitudes match prior eval within ±10%. Engine fix is hook-compatible at full fidelity. None of the 20 vertical dungeons set `percentUsersBornInDataset` / `bornRecentBias` / `avgEventsPerUserPerDay > 50` explicitly → validator clamps don't fire on existing dungeons.
|
|
120
|
+
|
|
121
|
+
#### Documentation
|
|
122
|
+
|
|
123
|
+
- **CLAUDE.md** — new "Tuning guidance — safe ranges and engine guarantees (v1.5)" section under "Trend Shape — Macro and Soup". Per-tunable safe-range table, 6 strict-bar conditions, per-macro bar values, known-engine-guarantees subsection.
|
|
124
|
+
- **types.d.ts** — JSDoc `safe range` + clamp behavior on `numDays`, `percentUsersBornInDataset`, `bornRecentBias`, `avgEventsPerUserPerDay`, `avgActiveDaysPerUser`.
|
|
125
|
+
- **`.claude/skills/create-dungeon/SKILL.md`** — macro × born% compatibility note + clamp warnings.
|
|
126
|
+
- **`.claude/skills/write-hooks/SKILL.md`** — "intentional strict-bar deviation" pattern (decline + churn cohorts and viral-with-persona-lift can intentionally exceed bars).
|
|
127
|
+
|
|
128
|
+
### Test Suite
|
|
129
|
+
|
|
130
|
+
Full suite: **46 files, 1100+ tests** (was 960 in 1.4). Engine-validation pass adds 12 (10 canary + 2 clamp + 4 macro-object form, minus updates) → **1122 passed / 2 skipped**. Highlights:
|
|
131
|
+
|
|
132
|
+
| File | Tests | Coverage |
|
|
133
|
+
|------|-------|----------|
|
|
134
|
+
| Generator: `active-days`, `conversion-window`, `order-mode-dispatch`, `touchpoint-cap`, `strict-event-autopromote`, `auto-sort`, `interrupt-funnel`, `datagen-determinism`, `decay-respects-active-days` | 41 | Engine changes |
|
|
135
|
+
| `tests/unit/identity-resolution.test.js` | 14 | Map inversion, resolver fallback chain |
|
|
136
|
+
| `tests/unit/funnel-engine.test.js` (extended) | 51 | Reentry, exclusion, HPC, step filters, step properties, segment modes, sessionScoped — 5+ ported fixtures from `test_qt_funnel.py` |
|
|
137
|
+
| `tests/unit/session-metrics.test.js` | 11 | count/duration/eventsPerSession + day-boundary + ported `test_qt_sessions.py` fixture |
|
|
138
|
+
| `tests/unit/retention.test.js` | 9 | ms-delta bucketing + birthCanRetain + carry_forward + segmentBy + ported `test_qt_retention.py` fixture |
|
|
139
|
+
| `tests/unit/time-bucketed.test.js` | 11 | day/week/month + cross-cutting + empty backfill |
|
|
140
|
+
| `tests/integration/identity-model.test.js` (extended) | +1 | `emulateBreakdown` profile-merge round-trip |
|
|
141
|
+
| `tests/integration/hook-patterns-emulator.test.js` (extended) | +7 | Funnel options + new breakdown types + cohort SUM/MAX |
|
|
142
|
+
| `tests/integration/features.test.js` (extended) | +4 | exclusionEvents validator + injection + schema-clean clone |
|
|
143
|
+
|
|
144
|
+
## 1.4.5 — 2026-05-06
|
|
145
|
+
|
|
146
|
+
### Added
|
|
147
|
+
|
|
148
|
+
- **Progress callback.** Callers can pass `onProgress: (update) => void` on the dungeon config to receive throttled updates during generation, import, and pipeline step transitions. Update frequency is configurable via `progressInterval` (default 500ms). The callback is fault-tolerant — bad functions are caught and disabled after 3 failures, never breaking the job. Return value includes a `progress` summary with update count, error count, and disabled flag.
|
|
149
|
+
- **Mixpanel import progress.** When `onProgress` is set and a Mixpanel token is provided, import progress from `mixpanel-import`'s `progressCallback` is surfaced through the same `onProgress` interface as `{ phase: "import" }` updates.
|
|
150
|
+
- **Full TypeScript typings** for `ProgressUpdate` (discriminated union), `ProgressSummary`, `ProgressGeneration`, `ProgressImport`, and `ProgressStep`.
|
|
151
|
+
|
|
5
152
|
## 1.4.4 — 2026-05-06
|
|
6
153
|
|
|
7
154
|
The "GCS imports actually work now" release.
|