@ak--47/dungeon-master 1.8.0 → 1.8.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/analyze-soup/SKILL.md +9 -0
- package/.claude/skills/create-dungeon/SKILL.md +35 -34
- package/.claude/skills/create-project/SKILL.md +6 -0
- package/.claude/skills/headless-build/SKILL.md +21 -11
- package/.claude/skills/powertools/SKILL.md +6 -2
- package/.claude/skills/release-check/SKILL.md +27 -2
- package/.claude/skills/verify-dungeon/SKILL.md +32 -13
- package/.claude/skills/verify-dungeon/references/alignment-contract.md +110 -0
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +29 -16
- package/.claude/skills/verify-dungeon/references/report-format.md +23 -9
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +135 -225
- package/.claude/skills/warehouse-metrics/SKILL.md +6 -0
- package/.claude/skills/write-hooks/SKILL.md +61 -48
- package/CHANGELOG.md +82 -0
- package/HOOKS.md +105 -47
- package/README.md +41 -1
- package/docs/guides/1.8.1-upgrade-guide.md +153 -0
- package/docs/guides/1.8.2-upgrade-guide.md +110 -0
- package/lib/generators/events.js +6 -0
- package/lib/generators/funnels.js +16 -0
- package/lib/hook-helpers/shape.js +73 -17
- package/lib/hook-patterns/attributed-by-source.js +4 -3
- package/lib/hook-patterns/funnel-frequency-breakdown.js +4 -7
- package/lib/orchestrators/user-loop.js +82 -15
- package/lib/verify/counting.js +7 -10
- package/lib/verify/emulate-breakdown.js +48 -29
- package/lib/verify/funnel-engine.js +93 -40
- package/lib/verify/identity.js +32 -9
- package/lib/verify/story-runner.js +93 -30
- package/lib/verify/verify-dungeon.js +4 -1
- package/package.json +1 -1
- package/scripts/verify-stories.mjs +3 -3
- package/types.d.ts +9 -5
package/HOOKS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# HOOKS.md -- Hook Encyclopedia
|
|
2
2
|
|
|
3
|
-
Hook reference and recipe catalog for dungeon-master.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[
|
|
3
|
+
Hook reference and recipe catalog for dungeon-master. Recipes have different
|
|
4
|
+
counting and evidence requirements. Read [Section 2](#2-how-mixpanel-counts-things)
|
|
5
|
+
before adapting a pattern, then verify the exact report on the emitted dataset.
|
|
6
|
+
The [1.8.2 guide](docs/guides/1.8.2-upgrade-guide.md) records live-tested scope and limits.
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -172,20 +172,28 @@ verification emulator (`@ak--47/dungeon-master/verify`) now matches these
|
|
|
172
172
|
rules; old recipes that ignored them will look correct on the dataset but
|
|
173
173
|
fail when verified or queried in Mixpanel.
|
|
174
174
|
|
|
175
|
-
### 2.1 Frequency
|
|
175
|
+
### 2.1 Frequency depends on the selected report
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
event
|
|
177
|
+
Three measurements require different checks:
|
|
178
|
+
|
|
179
|
+
- **Raw per-user event count:** two purchases on one day count as two. Insights
|
|
180
|
+
per-user count histograms and frequency breakdowns using total event count use
|
|
181
|
+
this axis. `applyFrequencyByFrequency` scales this target count.
|
|
182
|
+
- **Distinct calendar-day activity:** those purchases count as one active day.
|
|
183
|
+
`countDistinctPeriods` defaults to UTC calendar buckets (`ui-bucket`), and
|
|
184
|
+
`binBy: 'distinctDays'` uses the same rule. This default is retained for
|
|
185
|
+
compatibility; it is not a universal Mixpanel frequency-report rule.
|
|
186
|
+
- **Rolling Frequency/Addiction:** an event counts when it is at least one
|
|
187
|
+
selected unit after the last counted event. Histories reset per report interval.
|
|
188
|
+
Two events around midnight can occupy two calendar days but one rolling period.
|
|
180
189
|
|
|
181
190
|
Two related rules exist (v1.6 names them for what they are; the old
|
|
182
191
|
`'calendar'` / `'rolling'` names remain as silent aliases, unknown names
|
|
183
192
|
now throw):
|
|
184
193
|
|
|
185
194
|
- **`algorithm: 'ui-bucket'`** (default in our verifier):
|
|
186
|
-
`COUNT(DISTINCT date_trunc(unit, time))` in UTC.
|
|
187
|
-
|
|
188
|
-
internally.
|
|
195
|
+
`COUNT(DISTINCT date_trunc(unit, time))` in UTC. This is also what
|
|
196
|
+
[`injectOnNewDays`](lib/hook-helpers/inject.js) uses internally.
|
|
189
197
|
- **`algorithm: 'mixpanel-rolling'`**: the C++
|
|
190
198
|
`addiction_query.cpp` rule `qtz_time >= last_counted + seconds_for_unit`
|
|
191
199
|
(`addiction_query_update_history`, `addiction_query.cpp:363-374`) — what
|
|
@@ -193,8 +201,8 @@ now throw):
|
|
|
193
201
|
boundaries (events at 23:59 + 00:01 next day = 1 rolling period, 2
|
|
194
202
|
calendar periods).
|
|
195
203
|
|
|
196
|
-
|
|
197
|
-
|
|
204
|
+
Choose the algorithm from the requested report. Do not replace a raw-count
|
|
205
|
+
histogram with active-day counts, or use calendar days to claim rolling parity.
|
|
198
206
|
|
|
199
207
|
**The actual Frequency report shape** is `frequencyHistogram(events,
|
|
200
208
|
{ event, unit, intervalDays, profiles })` (v1.6): per report interval, a
|
|
@@ -205,19 +213,19 @@ zero bucket (`addiction_query.cpp:546-573`). Array length is
|
|
|
205
213
|
`ceil(interval / unit)` (`unit.c:108-113`). Use it when a dungeon targets
|
|
206
214
|
the Frequency report itself rather than a frequency-derived cohort.
|
|
207
215
|
|
|
208
|
-
**Implication for hooks:** `scaleEventCount(record, "Buy", 3)`
|
|
209
|
-
many Buy events at
|
|
210
|
-
|
|
216
|
+
**Implication for hooks:** `scaleEventCount(record, "Buy", 3)` targets 3x as
|
|
217
|
+
many Buy events at one-second offsets. This changes raw-count histogram bins.
|
|
218
|
+
It generally does not add active days or rolling periods. Use
|
|
211
219
|
[`injectOnNewDays`](lib/hook-helpers/inject.js) when the goal is to move
|
|
212
|
-
users between
|
|
220
|
+
users between calendar-day activity bins. Both `injectOnNewDays` and the default
|
|
213
221
|
`countDistinctPeriods` algorithm use calendar-bucket math, so they agree
|
|
214
222
|
at boundaries.
|
|
215
223
|
|
|
216
224
|
One second is also below Mixpanel's 30-minute session gap, so the default
|
|
217
225
|
spread cannot create a new session either. The full list of metrics a default
|
|
218
|
-
`scaleEventCount` call
|
|
219
|
-
(
|
|
220
|
-
|
|
226
|
+
`scaleEventCount` call does not reliably move: active days per user, DAU,
|
|
227
|
+
stickiness (DAU/MAU), sessions per user, rolling frequency, and retention.
|
|
228
|
+
At dataset edges, clones can be clipped. Verify surviving counts separately.
|
|
221
229
|
|
|
222
230
|
As of v1.6.4 you can pass `{ spreadDays: N }` to scatter the clones across the
|
|
223
231
|
next N days instead:
|
|
@@ -363,7 +371,7 @@ sampling.
|
|
|
363
371
|
|
|
364
372
|
### 2.5 Active-day distribution is config-first
|
|
365
373
|
|
|
366
|
-
|
|
374
|
+
Distinct-day activity is a separate measurement from raw counts (§2.1). The engine
|
|
367
375
|
exposes `Dungeon.avgActiveDaysPerUser` as the canonical primitive for this
|
|
368
376
|
shape. Set it at the config level and the engine concentrates each user's
|
|
369
377
|
events onto a sampled subset of days drawn from `normal(mean=N, sd=N/3)`,
|
|
@@ -537,12 +545,22 @@ enter on birth, or drop next-day spill in an `everything` hook.
|
|
|
537
545
|
|
|
538
546
|
### 2.8 Funnel reentry: state machine resets after completion
|
|
539
547
|
|
|
540
|
-
Reference: `history.cpp` (`
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
548
|
+
Reference: `history.cpp` (`history_is_mutable`) and `funnel_query.cpp`
|
|
549
|
+
(shared first/last step handling). With `reentry: true`, completion absorbs
|
|
550
|
+
events through the inclusive 2-second grace period. The next event beyond
|
|
551
|
+
grace starts the next scan. Conversion-window expiry can restart earlier.
|
|
552
|
+
`graceperiod: false` disables the completion wait.
|
|
553
|
+
|
|
554
|
+
When an event records the ordered last step and also matches the ordered
|
|
555
|
+
first step, it closes one attempt and anchors the next immediately. Both
|
|
556
|
+
selectors must match. Any-order edges do not use this exception.
|
|
557
|
+
`woRepeat` still restarts only at window expiry.
|
|
558
|
+
|
|
559
|
+
`result.completions` reports repeat completions in uniques mode. In
|
|
560
|
+
`countMode: 'totals'`, the engine returns one `FunnelResult` per attempt,
|
|
561
|
+
including partial attempts. **Compatibility unchanged:** `reentry` defaults
|
|
562
|
+
to `false`, even for totals. Totals alone does not enable analytics general
|
|
563
|
+
counting's repeat-history behavior.
|
|
546
564
|
|
|
547
565
|
### 2.9 HPC (Hold Property Constant) — parallel sub-funnels
|
|
548
566
|
|
|
@@ -555,13 +573,24 @@ directly, or (v1.6) pass `holdPropertyConstant: '<prop>'` to the
|
|
|
555
573
|
`funnelFrequency` emulator — it routes through the HPC engine and reports
|
|
556
574
|
per-held-value sub-funnel counts.
|
|
557
575
|
|
|
576
|
+
Session windows derive ordinals from the full user stream before HPC
|
|
577
|
+
partitioning. Events with another held value can bridge a session but cannot
|
|
578
|
+
fill steps in the current bucket. This applies to explicit session windows
|
|
579
|
+
and `countMode: 'sessions'`. The local session defaults remain a 30-minute
|
|
580
|
+
timeout, 24-hour maximum, and UTC day boundaries.
|
|
581
|
+
|
|
558
582
|
### 2.10 Funnel segment modes (FIRST_TOUCH / LAST_TOUCH / STEP)
|
|
559
583
|
|
|
560
584
|
Reference: `options.hpp` `funnel_segment_mode`; `history.cpp`
|
|
561
585
|
`property_set_buffer`. The engine snapshots the matched event's properties
|
|
562
|
-
at every
|
|
563
|
-
|
|
564
|
-
|
|
586
|
+
at every reached position. FIRST_TOUCH and LAST_TOUCH merge those snapshots
|
|
587
|
+
in recorded path order, including partial and any-order paths. The first
|
|
588
|
+
or last defined non-null value wins, respectively. Undefined never replaces
|
|
589
|
+
a defined value; null never replaces a defined non-null value. If only null
|
|
590
|
+
and undefined are present, null wins. Snapshots remain unchanged.
|
|
591
|
+
|
|
592
|
+
STEP N selects one reached position without merging fallback values.
|
|
593
|
+
Enable with `evaluateFunnel(events, steps, { trackStepProperties: true })`, then pick with
|
|
565
594
|
`resolveFunnelSegment(result, 'first' | 'last' | { step: N })`.
|
|
566
595
|
|
|
567
596
|
### 2.11 Engine-validation guarantees (v1.5+)
|
|
@@ -855,15 +884,12 @@ Reference: `flows_query.cpp:988-994` (next-anchor-only), `flows.cpp:680-717`
|
|
|
855
884
|
|
|
856
885
|
### New principles from the emulator alignment
|
|
857
886
|
|
|
858
|
-
21. **
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
instead of `binUsersByEventCount`. For hooks targeting raw event counts
|
|
862
|
-
(Insights `total events`, `events per user`), `binUsersByEventCount` is
|
|
863
|
-
still correct.
|
|
887
|
+
21. **Match the bin to the report.** Use `binUsersByEventCount` for raw event
|
|
888
|
+
counts and `binByDistinctPeriods` for calendar-day activity. Use rolling
|
|
889
|
+
counting for the Frequency/Addiction report. Section 2.1 distinguishes them.
|
|
864
890
|
|
|
865
|
-
22. **`scaleEventCount`
|
|
866
|
-
|
|
891
|
+
22. **`scaleEventCount` changes raw-count bins.** Cloning
|
|
892
|
+
Buy events at one-second offsets usually places them on the same calendar day and
|
|
867
893
|
inside the same 30-minute session window, so distinct days, sessions, DAU,
|
|
868
894
|
stickiness, and retention are all unchanged. It moves event volume only. To
|
|
869
895
|
shift any of the others, pass `{ spreadDays: N }` (v1.6.4) or use
|
|
@@ -877,11 +903,10 @@ Reference: `flows_query.cpp:988-994` (next-anchor-only), `flows.cpp:680-717`
|
|
|
877
903
|
after the prior step's timestamp (with margin > 2 seconds for the grace
|
|
878
904
|
window).
|
|
879
905
|
|
|
880
|
-
24. **
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
touches with deterministic weight ratios.
|
|
906
|
+
24. **Separate stamping caps from attribution reads.** The generator defaults
|
|
907
|
+
to ten stamped touches per user. Mixpanel first/last reads are uncapped
|
|
908
|
+
within their conversion-bounded lookback. Distinct touch timestamps avoid
|
|
909
|
+
ambiguous attribution when different values tie at the first or last time.
|
|
885
910
|
|
|
886
911
|
25. **Null-aware aggregation removes the need to "fill" defaults.** Don't
|
|
887
912
|
coalesce missing numeric properties to 0 to keep AVG sane — Mixpanel
|
|
@@ -1720,6 +1745,13 @@ touch with a seeded weighted pick (models: `firstTouch`, `lastTouch`,
|
|
|
1720
1745
|
`both`). It never stamps unstamped events, so total touch count is
|
|
1721
1746
|
unchanged.
|
|
1722
1747
|
|
|
1748
|
+
The helper selects lifetime endpoints. A report reads eligible touches before
|
|
1749
|
+
each conversion and inside its lookback. The lifetime-last touch can occur after
|
|
1750
|
+
the conversion; the lifetime-first can expire. Neither endpoint is a general
|
|
1751
|
+
conversion-aware guarantee. Pass a deliberately eligible stream to the existing
|
|
1752
|
+
helper or implement that selection in the hook, then verify the requested report.
|
|
1753
|
+
Conflicting sources at an equal timestamp have no stable cross-ingestion order.
|
|
1754
|
+
|
|
1723
1755
|
---
|
|
1724
1756
|
|
|
1725
1757
|
#### 4.27 Active-Day Cohort Engineering (v1.5)
|
|
@@ -1843,8 +1875,9 @@ event set rather than one value moment.
|
|
|
1843
1875
|
**Hook:** `everything`
|
|
1844
1876
|
**Mixpanel report:** Flows — top paths after the anchor event show the engineered branch (Section 2.17)
|
|
1845
1877
|
|
|
1846
|
-
**In Mixpanel:**
|
|
1847
|
-
|
|
1878
|
+
**In Mixpanel:** Bias the first branch toward `add to cart → begin checkout`.
|
|
1879
|
+
The helper selects ~30% of users for append-only injection. Existing traffic
|
|
1880
|
+
can interrupt the branch; the final branch share is not guaranteed to be 30%.
|
|
1848
1881
|
|
|
1849
1882
|
```js
|
|
1850
1883
|
import { applyPathBias } from "@ak--47/dungeon-master/hook-helpers";
|
|
@@ -1889,6 +1922,8 @@ if (type === "everything") {
|
|
|
1889
1922
|
sessionsPerWeek: 3,
|
|
1890
1923
|
eventsPerSession: 5,
|
|
1891
1924
|
sessionMinutes: 25,
|
|
1925
|
+
datasetStart: meta.datasetStart,
|
|
1926
|
+
datasetEnd: meta.datasetEnd,
|
|
1892
1927
|
});
|
|
1893
1928
|
}
|
|
1894
1929
|
```
|
|
@@ -1897,12 +1932,35 @@ if (type === "everything") {
|
|
|
1897
1932
|
event set (after the `everything` hook), so wholesale timestamp rewrites no
|
|
1898
1933
|
longer leave stale session labels. The atom keeps intra-session gaps well
|
|
1899
1934
|
under the 30-min timeout (spacing capped at 20min + bounded jitter), keeps
|
|
1900
|
-
inter-session gaps
|
|
1935
|
+
inter-session gaps over it when explicit bounds are supplied, and never crosses UTC midnight inside one
|
|
1901
1936
|
engineered session (the day-boundary split would cut it). Retiming only — no
|
|
1902
1937
|
events are added or dropped, so total counts and event mixes are untouched.
|
|
1903
1938
|
Session count follows `min(sessionsPerWeek × weeks, ceil(N /
|
|
1904
1939
|
eventsPerSession))`: scarce users get fewer sessions, not fabricated events.
|
|
1905
1940
|
|
|
1941
|
+
`datasetStart` and `datasetEnd` are additive, optional arguments. Existing
|
|
1942
|
+
calls with neither bound keep the original full-UTC-day placement between
|
|
1943
|
+
the user's first and last active days. A two-event stream at 12:00/12:20 can
|
|
1944
|
+
still request two sessions with `eventsPerSession: 1` and `sessionMinutes: 5`.
|
|
1945
|
+
Legacy overfull requests also keep their old behavior: they do not throw,
|
|
1946
|
+
but their clusters can merge under the 30-minute timeout.
|
|
1947
|
+
|
|
1948
|
+
The bounds accept ISO strings, unix seconds, or unix milliseconds. Hook
|
|
1949
|
+
metadata uses unix seconds and can be passed directly, as in the example.
|
|
1950
|
+
Either bound enables constrained placement; an omitted side uses the start
|
|
1951
|
+
of the first active UTC day or the end of the last active UTC day. The helper
|
|
1952
|
+
cannot infer `datasetEnd` from the last event. Pass known bounds when the
|
|
1953
|
+
dataset ends partway through an active day, including an inclusive midnight
|
|
1954
|
+
endpoint, to prevent later engine clipping.
|
|
1955
|
+
|
|
1956
|
+
With explicit bounds, partial days compress clusters, including zero-duration
|
|
1957
|
+
clusters at midnight. If the requested sessions cannot fit inside a week's
|
|
1958
|
+
available day slices, the helper throws `RangeError` before changing any
|
|
1959
|
+
events. Invalid bounds also throw. It never silently reduces the target or
|
|
1960
|
+
drops records. Widen the allowed window or reduce the session target. Exact
|
|
1961
|
+
session separation assumes UTC and the default 30-minute timeout, without
|
|
1962
|
+
a maximum session duration.
|
|
1963
|
+
|
|
1906
1964
|
---
|
|
1907
1965
|
|
|
1908
1966
|
## 5. Phase 3 Atom Reference
|
|
@@ -1933,7 +1991,7 @@ Import from `@ak--47/dungeon-master/hook-helpers`:
|
|
|
1933
1991
|
| `splitByAuth` | identity | `(events, authTime) -> { preAuth, postAuth, stitch }` | Partition by auth boundary |
|
|
1934
1992
|
| **`applyLifecycleWave`** | shape | `(events, uid, { dormantFromDay, dormantDays, resurrectBurst?, valueMomentEvent, dropAll? }) -> events[]` | Clean dormancy gap + resurrection burst; sweeps the ENTIRE window by timestamp (v1.6, recipe 4.29). Returns a NEW array |
|
|
1935
1993
|
| **`applyPathBias`** | shape | `(events, uid, { anchor, path, share, gapSeconds? }) -> events[]` | Inject a Flows path after the user's first anchor for ~`share` (fraction) of users; skips users missing any step template (v1.6, recipe 4.30) |
|
|
1936
|
-
| **`applySessionShape`** | shape | `(events, uid, { sessionsPerWeek, eventsPerSession, sessionMinutes }) -> events[]` |
|
|
1994
|
+
| **`applySessionShape`** | shape | `(events, uid, { sessionsPerWeek, eventsPerSession, sessionMinutes, datasetStart?, datasetEnd? }) -> events[]` | Preserve records; default legacy full-UTC-day placement. Optional bounds constrain placement and throw atomically on insufficient capacity (recipe 4.31) |
|
|
1937
1995
|
|
|
1938
1996
|
**Inject atoms + v1.5:** the engine auto-sorts events by time after the
|
|
1939
1997
|
`everything` hook (`autoSortAfterEverything: true` default — see Principle
|
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ i built this because i needed it. and after using it across hundreds of customer
|
|
|
18
18
|
npm install @ak--47/dungeon-master
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
1.8.2 adds live counting verification and fixes identity, session-funnel,
|
|
22
|
+
attribution, and experiment-filter defects. See the
|
|
23
|
+
[upgrade guide](docs/guides/1.8.2-upgrade-guide.md) for compatibility and tested limits.
|
|
24
|
+
|
|
21
25
|
## quick start
|
|
22
26
|
|
|
23
27
|
```javascript
|
|
@@ -957,7 +961,7 @@ styles: `support`, `review`, `search`, `feedback`, `chat`, `email`, `forum`, `co
|
|
|
957
961
|
## scripts
|
|
958
962
|
|
|
959
963
|
```bash
|
|
960
|
-
npm test #
|
|
964
|
+
npm test # default unit/integration/e2e suite; prunes data/tmp
|
|
961
965
|
npm run typecheck # typescript check
|
|
962
966
|
npm run dungeon:run # run a dungeon file locally
|
|
963
967
|
npm run dungeon:to-json # convert JS dungeon to JSON (for UI import)
|
|
@@ -978,6 +982,8 @@ node scripts/verify-runner.mjs <path> [prefix] # generate at full fidelity for
|
|
|
978
982
|
|
|
979
983
|
## tests
|
|
980
984
|
|
|
985
|
+
1.8.1 compatibility and output changes: [upgrade guide](docs/guides/1.8.1-upgrade-guide.md).
|
|
986
|
+
|
|
981
987
|
vitest tests live under `tests/` in three tiers:
|
|
982
988
|
|
|
983
989
|
| dir | scope | wall time |
|
|
@@ -999,6 +1005,40 @@ npx vitest tests/unit # watch mode
|
|
|
999
1005
|
|
|
1000
1006
|
`tests/e2e/engine-shape-full-sweep.test.js` skips itself unless `RUN_FULL_SWEEP=1` is set (it wraps the long-running 194-combo engine sweep).
|
|
1001
1007
|
|
|
1008
|
+
### editor and offline alignment tests
|
|
1009
|
+
|
|
1010
|
+
VS Code discovers unit, integration, E2E, and alignment tests through one serial
|
|
1011
|
+
`vitest.editor.config.js`. The workspace disables Go test
|
|
1012
|
+
discovery and ignores the overlapping diagnostic Vitest configs. After changing
|
|
1013
|
+
these settings, run **Developer: Reload Window** if stale providers or test runs
|
|
1014
|
+
remain in the Testing panel. Editor runs omit the pruning setup, but they are not
|
|
1015
|
+
OS-sandboxed.
|
|
1016
|
+
|
|
1017
|
+
Use **Tasks: Run Test Task** for `test: regression (offline, macOS)`, or choose
|
|
1018
|
+
the named alignment and sweep tasks from **Tasks: Run Task**. These test tasks
|
|
1019
|
+
never invoke the prune or dungeon-run tasks. Existing dungeon-run cleanup is
|
|
1020
|
+
unchanged and remains separate from testing.
|
|
1021
|
+
|
|
1022
|
+
The `test: engine matrix`, `test: engine short sweep`, and `test: engine full sweep`
|
|
1023
|
+
tasks expose the direct-run engine checks. They are opt-in and use OS network denial.
|
|
1024
|
+
The full engine wrapper is visible under E2E but skipped until `RUN_FULL_SWEEP=1`.
|
|
1025
|
+
Direct-run `.mjs` scripts are not Vitest tests, so they do not get a separate
|
|
1026
|
+
`engine` folder in the Testing tree. The legacy engine sweeps do not use the
|
|
1027
|
+
alignment runner's ten-minute deadline. E2E tests may write files or perform
|
|
1028
|
+
network operations when run directly in the editor; editor execution is not an
|
|
1029
|
+
offline guarantee.
|
|
1030
|
+
|
|
1031
|
+
```sh
|
|
1032
|
+
node tests/alignment/run.mjs # offline alignment gate
|
|
1033
|
+
node tests/alignment/run.mjs --sweep --timeout-ms=600000 # opt-in bounded sweep
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
Alignment is a separate test family, excluded from `npm test` but visible in the editor.
|
|
1037
|
+
Its runner enforces OS network denial on macOS, fails closed elsewhere, and kills
|
|
1038
|
+
workers at the ten-minute deadline. Tests and reports live in the source checkout;
|
|
1039
|
+
they are not included in the npm package. The default `npm test` and direct root
|
|
1040
|
+
Vitest commands still prune `data` and `tmp` through their global setup.
|
|
1041
|
+
|
|
1002
1042
|
### engine tests (direct-run, NOT vitest)
|
|
1003
1043
|
|
|
1004
1044
|
`tests/engine/` houses direct-run regression tests at scale. these are NOT vitest-compatible — invoke with `node` directly. used to catch engine regressions across a wide variety of dungeon configurations and for ad-hoc chart inspection. outputs land in `./tmp/` (gitignored).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# 1.8.1 Upgrade Guide
|
|
2
|
+
|
|
3
|
+
1.8.1 repairs funnel counting, lifecycle timing, and emitted identity. Existing
|
|
4
|
+
configurations and call forms remain supported. No property renames or required
|
|
5
|
+
configuration changes are introduced. `applySessionShape` adds optional dataset
|
|
6
|
+
bounds; its existing unbounded calls retain their behavior.
|
|
7
|
+
|
|
8
|
+
Generated timestamps, identities, and report counts can change from 1.8.0 because
|
|
9
|
+
the previous output contained defects. Recheck saved story expectations rather
|
|
10
|
+
than expecting byte-identical output across versions.
|
|
11
|
+
|
|
12
|
+
## Update the dependency
|
|
13
|
+
|
|
14
|
+
After 1.8.1 is published:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install @ak--47/dungeon-master@1.8.1
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No additional runtime dependency is required. The package still requires Node.js
|
|
21
|
+
20.20.0 or newer. This guide does not indicate that npm publication has occurred.
|
|
22
|
+
|
|
23
|
+
## Funnel counts can change
|
|
24
|
+
|
|
25
|
+
- An ordered funnel whose first and last steps match now finalizes its conversion
|
|
26
|
+
correctly. With reentry enabled, the closing event can also start the next
|
|
27
|
+
attempt. Without reentry, it still completes and does not create another attempt.
|
|
28
|
+
- Ordinary completed histories consume events through the inclusive two-second
|
|
29
|
+
completion grace period. Those events do not become a second attempt. The
|
|
30
|
+
existing `graceperiod: false` option disables that wait.
|
|
31
|
+
- Hold-property-constant session windows use session boundaries from the full
|
|
32
|
+
stream before property filtering. Unrelated activity can keep a session open.
|
|
33
|
+
- First/last-touch segmentation merges properties across reached steps. A missing
|
|
34
|
+
property on the preferred step can fall back to another reached step. Explicit
|
|
35
|
+
step selection remains separate.
|
|
36
|
+
|
|
37
|
+
`countMode: 'totals'` still defaults to `reentry: false`. Specify `reentry: true`
|
|
38
|
+
when repeated histories are intended. This compatibility default differs from
|
|
39
|
+
Mixpanel general totals. Do not change existing report options implicitly when
|
|
40
|
+
comparing old and new results.
|
|
41
|
+
|
|
42
|
+
## Lifecycle and identity output is corrected
|
|
43
|
+
|
|
44
|
+
Retention entry follows adjusted creation, retries precede the final onboarding
|
|
45
|
+
attempt, and usage follows onboarding completion. The engine reconciles its own
|
|
46
|
+
identity fields against surviving auth events after filtering. Engine-created
|
|
47
|
+
data-quality duplicates and world-event clones preserve identity provenance.
|
|
48
|
+
|
|
49
|
+
Explicit hook identity overrides and the synthetic experiment identity exception
|
|
50
|
+
remain supported. A later valid event carrying both IDs can provide identity
|
|
51
|
+
mapping evidence; this is not restricted to the first funnel.
|
|
52
|
+
|
|
53
|
+
Short windows retain partial output instead of introducing a required exception.
|
|
54
|
+
Inspect the existing `result.warnings` collection for:
|
|
55
|
+
|
|
56
|
+
| warning key | meaning |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `lifecycle.firstFunnelClipped` | Configured timing cannot fit before dataset end; future steps are omitted without compressing TTC. |
|
|
59
|
+
| `lifecycle.emptyPreAuthAttempt` | Auth is the first step, so a failed pre-auth attempt emits no rows. |
|
|
60
|
+
| `lifecycle.strictAttemptBudget` | The strict event budget cannot retain every surviving attempt entry. |
|
|
61
|
+
|
|
62
|
+
Configured suppression and hook filtering remain authoritative. The engine does
|
|
63
|
+
not recreate intentionally removed events to satisfy an attempt count.
|
|
64
|
+
|
|
65
|
+
## Bound session reshaping when the dataset end matters
|
|
66
|
+
|
|
67
|
+
Calls without bounds retain legacy full-UTC-day placement. They cannot infer the
|
|
68
|
+
dataset end from the last observed event and may still move events beyond it.
|
|
69
|
+
Pass known bounds to prevent that clipping:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { applySessionShape } from '@ak--47/dungeon-master/hook-helpers';
|
|
73
|
+
|
|
74
|
+
const hook = (records, type, meta) => {
|
|
75
|
+
if (type === 'everything') {
|
|
76
|
+
applySessionShape(records, meta.profile.distinct_id, {
|
|
77
|
+
sessionsPerWeek: 3,
|
|
78
|
+
eventsPerSession: 5,
|
|
79
|
+
sessionMinutes: 10,
|
|
80
|
+
datasetStart: meta.datasetStart,
|
|
81
|
+
datasetEnd: meta.datasetEnd,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return records;
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Bounds accept ISO strings, Unix seconds, or Unix milliseconds. Either explicit
|
|
89
|
+
bound enables constrained placement; an omitted side uses the original UTC day
|
|
90
|
+
edge. Invalid bounds or insufficient per-week session capacity throw `RangeError`
|
|
91
|
+
before records are mutated. Two same-day sessions separated by more than thirty
|
|
92
|
+
minutes cannot fit in a twenty-minute interval. Adjust the requested cadence or
|
|
93
|
+
available window rather than swallowing that error.
|
|
94
|
+
|
|
95
|
+
The helper retimes the same records. It does not add or drop events. Unbounded
|
|
96
|
+
legacy requests remain nonthrowing, including overfull layouts where derived
|
|
97
|
+
sessions can merge.
|
|
98
|
+
|
|
99
|
+
## Path injection remains append-only
|
|
100
|
+
|
|
101
|
+
`applyPathBias` selects the earliest chronological anchor and leaves original
|
|
102
|
+
traffic intact. `share: 1` selects all eligible users for injection. Competing
|
|
103
|
+
events can still interrupt the immediate branch, so it does not promise a 100%
|
|
104
|
+
Flows branch share. Recheck the observed branch, not only the injected count.
|
|
105
|
+
|
|
106
|
+
## Revalidate the story, not just the schema
|
|
107
|
+
|
|
108
|
+
Run a pinned, seeded representative dungeon with ordinary standalone traffic and
|
|
109
|
+
competing funnels. Compare counts using explicit report settings. For deterministic
|
|
110
|
+
same-version comparisons, use `concurrency: 1` and strip only `insert_id`.
|
|
111
|
+
|
|
112
|
+
The new alignment checks establish selected source-derived Mixpanel contracts.
|
|
113
|
+
They do not execute Mixpanel's engine. Timezone/DST variants, list-valued HPC,
|
|
114
|
+
project-specific session exclusions, all parameter combinations, and arbitrary
|
|
115
|
+
hooks are not covered by a universal parity claim.
|
|
116
|
+
|
|
117
|
+
The recorded sweep produced 17.08 million events across 594 dungeons within ten
|
|
118
|
+
minutes. Its largest single dungeon had 281,751 events. Of 297 cells, 125 met their
|
|
119
|
+
evidence criteria and 172 had insufficient eligible populations. Increasing total
|
|
120
|
+
events is not a substitute for enough eligible users or converters.
|
|
121
|
+
|
|
122
|
+
## Repository test workflows
|
|
123
|
+
|
|
124
|
+
These commands require a repository checkout with development dependencies.
|
|
125
|
+
The npm package ships this guide, but does not ship the test suites.
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
node tests/alignment/run.mjs
|
|
129
|
+
node tests/alignment/run.mjs --sweep --timeout-ms=600000
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Both commands use macOS OS-level network denial and a hard maximum of ten minutes.
|
|
133
|
+
They fail closed on unsupported platforms. The editor's Testing panel is a separate
|
|
134
|
+
workflow: one serial editor config discovers unit, integration, E2E, and alignment
|
|
135
|
+
tests without global pruning. Direct-run engine scripts have opt-in tasks; their
|
|
136
|
+
full-sweep E2E wrapper is visible but skipped by default. Editor runs do not provide
|
|
137
|
+
an OS network sandbox. Use the named offline test tasks when network denial is
|
|
138
|
+
required, especially for E2E tests that can perform external operations.
|
|
139
|
+
|
|
140
|
+
`npm test` retains the default unit/integration/E2E suite and excludes alignment.
|
|
141
|
+
Its global setup prunes local `data` and `tmp`; preserve artifacts before using it.
|
|
142
|
+
The explicit `prune` task and default dungeon-run task retain their existing cleanup
|
|
143
|
+
behavior. None of the new offline test tasks depend on them.
|
|
144
|
+
|
|
145
|
+
## Before publishing
|
|
146
|
+
|
|
147
|
+
Confirm package version and changelog are 1.8.1. Inspect `npm pack --dry-run --json`
|
|
148
|
+
and rerun the relevant release checks. Publishing to npm is a separate operator
|
|
149
|
+
action; neither test execution nor a Git commit publishes the package.
|
|
150
|
+
|
|
151
|
+
See the [changelog](../../CHANGELOG.md), [hook reference](../../HOOKS.md), and
|
|
152
|
+
[1.8.0 guide](1.8.0-upgrade-guide.md) for the unchanged standalone and warehouse
|
|
153
|
+
metric APIs.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# 1.8.2 upgrade guide
|
|
2
|
+
|
|
3
|
+
1.8.2 fixes verifier counting and experiment-filter defects found while importing
|
|
4
|
+
synthetic datasets and querying them in Mixpanel. Public exports, signatures,
|
|
5
|
+
options, and defaults remain unchanged. No new runtime dependency is required.
|
|
6
|
+
|
|
7
|
+
After publication:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @ak--47/dungeon-master@1.8.2
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This guide describes a release candidate. A branch, PR, or passing test does not
|
|
14
|
+
publish a package.
|
|
15
|
+
|
|
16
|
+
## identity requires emitted evidence
|
|
17
|
+
|
|
18
|
+
Automatic verification links a device only when an emitted event contains both
|
|
19
|
+
`device_id` and `user_id`. A profile device pool alone cannot create a link.
|
|
20
|
+
Ordinary event names work; linking is not restricted to an auth-named event.
|
|
21
|
+
|
|
22
|
+
`buildIdentityMap(profiles)` remains unchanged for explicit caller overrides.
|
|
23
|
+
Pass that map only when it represents trusted identity knowledge. Automatic
|
|
24
|
+
mapping preserves an explicit authenticated user on a conflicting device event.
|
|
25
|
+
Original-merge projects, B2B identity rules, external mapping history, and all
|
|
26
|
+
ingestion validation policies are not fully emulated.
|
|
27
|
+
|
|
28
|
+
Historical anonymous events may join their user after the import receipt returns.
|
|
29
|
+
The live verification run observed that delay and later verified the same fixture
|
|
30
|
+
without sending it again. Check event totals and identity readiness before accepting
|
|
31
|
+
a funnel comparison. No sender ordering change or fixed sleep claims to solve this.
|
|
32
|
+
|
|
33
|
+
## report wrappers use the supported counting code
|
|
34
|
+
|
|
35
|
+
Held-property funnels now support session counting through `funnelFrequency`.
|
|
36
|
+
Matching dungeon defaults no longer inject a millisecond conversion window into
|
|
37
|
+
an implicit one-session report. List-valued held properties expand into per-value
|
|
38
|
+
histories while preserving full-stream session boundaries.
|
|
39
|
+
|
|
40
|
+
Scalar-only held-property keys retain their existing behavior. Mixed list/scalar
|
|
41
|
+
keys use the source-derived string representation. Explicit list-only mode,
|
|
42
|
+
extreme value formatting, and engine cardinality caps remain outside this API.
|
|
43
|
+
|
|
44
|
+
Totals still defaults to `reentry: false`. Set `reentry: true` when comparing
|
|
45
|
+
repeated histories with Mixpanel general totals. Session tests use UTC and the
|
|
46
|
+
default thirty-minute inactivity timeout. Project exclusions and non-UTC/DST
|
|
47
|
+
variants require separate proof.
|
|
48
|
+
|
|
49
|
+
## passing stories need enough independent users
|
|
50
|
+
|
|
51
|
+
`minCohort` no longer sums overlapping period populations or treats arbitrary rows
|
|
52
|
+
as independent users. It uses a conservative lower bound and applies to custom
|
|
53
|
+
callbacks too. Missing denominator evidence caps an otherwise passing result at
|
|
54
|
+
`WEAK`, with an explanation. Assertions without `minCohort` retain their behavior.
|
|
55
|
+
|
|
56
|
+
This can expose previously optimistic story verdicts. Inspect the selected users,
|
|
57
|
+
entrants, converters, or mature retention cohorts. Do not lower the threshold just
|
|
58
|
+
to restore a green verdict.
|
|
59
|
+
|
|
60
|
+
## exposure events retain global filters
|
|
61
|
+
|
|
62
|
+
Synthetic `$experiment_started` events now include declared `superProps`, including
|
|
63
|
+
context-aware values. Experiment name and variant fields remain authoritative.
|
|
64
|
+
Run-isolated experiment queries therefore retain the exposure and outcome rows.
|
|
65
|
+
This can change seeded output when global property generation consumes randomness.
|
|
66
|
+
Same-version deterministic runs still exclude only fresh `insert_id` values.
|
|
67
|
+
|
|
68
|
+
Funnel-frequency keep/drop decisions now use stable event fields instead of random
|
|
69
|
+
insertion IDs. Their outcome can change from 1.8.1 but is reproducible on rerun.
|
|
70
|
+
|
|
71
|
+
## frequency and attribution have distinct contracts
|
|
72
|
+
|
|
73
|
+
Raw event-count histograms, calendar-day activity, and rolling Frequency/Addiction
|
|
74
|
+
reports are separate measurements. `applyFrequencyByFrequency` scales raw target
|
|
75
|
+
event counts. Its existing `binBy: 'distinctDays'` default only selects the cohort
|
|
76
|
+
axis; it does not make clones add new active days. Use explicit report settings.
|
|
77
|
+
|
|
78
|
+
`attributedBy` now ignores null or absent touch values, retains touchless conversions
|
|
79
|
+
as `unknown`, and preserves earlier touches across conversion time buckets.
|
|
80
|
+
The public default still selects the first conversion per user; `perConversion:
|
|
81
|
+
'all'` evaluates every conversion. The API has no finite-lookback argument.
|
|
82
|
+
|
|
83
|
+
`applyAttributedBySource` retains lifetime endpoint selection. Verify that the
|
|
84
|
+
selected endpoint is inside each intended conversion's lookback. Distinct touch
|
|
85
|
+
timestamps are required for reproducible conflicting source order. Same-time
|
|
86
|
+
ties and backend transition compression can differ from raw event-array order.
|
|
87
|
+
|
|
88
|
+
## verified scope and remaining work
|
|
89
|
+
|
|
90
|
+
The repository's `tests/alignment/live/REPORT.md` records source revision, run IDs,
|
|
91
|
+
queries, counts, practical effects, neutral controls, and historical failures.
|
|
92
|
+
The local analytics checkout supplied counting contracts; live Mixpanel queries
|
|
93
|
+
tested their behavior on imported data. The C++ engine was not built locally.
|
|
94
|
+
|
|
95
|
+
Evidence covers selected generated conversion, TTC, retention, volume, weights,
|
|
96
|
+
incidents, raw-count frequency, numeric aggregates, attribution, sessions, and
|
|
97
|
+
Flows. It does not establish every configuration cross-product, literal retention
|
|
98
|
+
probability calibration, arbitrary hook behavior, warehouse deployment, or every
|
|
99
|
+
project setting. Capacity and ambiguous-order limits remain explicit.
|
|
100
|
+
|
|
101
|
+
Repository gates preserve data and deny network access:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
node tests/alignment/run.mjs
|
|
105
|
+
node tests/alignment/run.mjs --sweep --timeout-ms=600000
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Live scripts are opt-in and require project-specific authorization and credentials.
|
|
109
|
+
They are excluded from the npm package. Do not use the default pruning test setup
|
|
110
|
+
while retained live datasets are still needed.
|
package/lib/generators/events.js
CHANGED
|
@@ -17,6 +17,8 @@ import { dataLogger as logger } from "../utils/logger.js";
|
|
|
17
17
|
// Keys that must never be nulled by data quality gremlins
|
|
18
18
|
const NULL_EXEMPT_KEYS = new Set(['event', 'time', 'insert_id', 'user_id', 'device_id', 'distinct_id', '_drop', '_anomaly', '_persona']);
|
|
19
19
|
|
|
20
|
+
export const engineIdentity = Symbol('engineIdentity');
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Creates a Mixpanel event with a flat shape
|
|
@@ -293,6 +295,9 @@ export async function makeEvent(
|
|
|
293
295
|
|
|
294
296
|
eventTemplate.insert_id = randomUUID();
|
|
295
297
|
|
|
298
|
+
const originalIdentity = { user_id: eventTemplate.user_id, device_id: eventTemplate.device_id };
|
|
299
|
+
Object.defineProperty(eventTemplate, engineIdentity, { value: originalIdentity, configurable: true });
|
|
300
|
+
|
|
296
301
|
// Call hook if configured (hooks override everything — they are the final authority)
|
|
297
302
|
const { hook } = config;
|
|
298
303
|
if (hook) {
|
|
@@ -305,6 +310,7 @@ export async function makeEvent(
|
|
|
305
310
|
});
|
|
306
311
|
// If hook returns a modified event, use it; otherwise use original
|
|
307
312
|
if (hookedEvent && typeof hookedEvent === 'object') {
|
|
313
|
+
Object.defineProperty(hookedEvent, engineIdentity, { value: originalIdentity, configurable: true });
|
|
308
314
|
return hookedEvent;
|
|
309
315
|
}
|
|
310
316
|
}
|