@ak--47/dungeon-master 1.5.3 → 1.6.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 +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +96 -0
- package/.claude/skills/create-project/provision.mjs +336 -0
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +211 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +3 -2
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- package/dungeons/vertical/travel.js +0 -930
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# 1.3.1 Upgrade Guide
|
|
2
|
+
|
|
3
|
+
This release is a behavioral and API-surface change for anyone writing or maintaining dungeon configs. The biggest item is the **removal of the time-shift mechanism** in favor of an explicit, deterministic dataset window.
|
|
4
|
+
|
|
5
|
+
If you only consume the npm module to generate data and never write hooks: the only thing you need to know is that you should pin `datasetStart` and `datasetEnd` on your config to get bit-exact reproducibility.
|
|
6
|
+
|
|
7
|
+
If you write or maintain dungeons: read all of this. The hook authoring rules changed.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## TL;DR
|
|
12
|
+
|
|
13
|
+
- **New config keys**: `datasetStart` and `datasetEnd` (ISO string, unix seconds, or anything `dayjs()` parses). Pin both for bit-exact deterministic runs.
|
|
14
|
+
- **Time-shift is gone.** No more `FIXED_NOW = 2024-02-02 + shift to present`. Events are generated directly inside the window. `context.TIME_SHIFT_SECONDS`, `context.MAX_TIME`, `context.getTimeShift()`, `context.getDaysShift()` no longer exist.
|
|
15
|
+
- **All 20 vertical dungeons** are now pinned to `2026-01-01T00:00:00Z` → `2026-04-28T23:59:59Z`. Technical dungeons still use `numDays` (relative window).
|
|
16
|
+
- **Hook authoring rule (NEW + ENFORCED)**: never use wall-clock `dayjs()` inside a hook. Read `meta.datasetStart` / `meta.datasetEnd` (unix seconds) instead.
|
|
17
|
+
- **`meta.datasetStart` / `meta.datasetEnd`** are now on every hook's `meta` object — typed via the new `HookMetaTimeAnchors` interface that all `HookMeta*` types extend.
|
|
18
|
+
- **Two new helper scripts**: `scripts/smoke-test-all.mjs` (parallel small-scale smoke test for every dungeon).
|
|
19
|
+
- **Text generator default `max` lowered from 500 → 254 chars** (matches Mixpanel string property limit). If you depended on the 500-char default in `createTextGenerator()`, override per-instance: `createTextGenerator({ max: 500, ... })`. Same change applied to the `productGen` template (300 → 254) and `dungeons/technical/text-generation.js` `twitterGen` (280 → 254).
|
|
20
|
+
- **Determinism is "near bit-exact" not "strictly bit-exact"**: 17/20 vertical dungeons produce identical event counts across runs; 3 (ecommerce, gaming, travel) show <0.5% variance from a non-investigated RNG-state interaction. Hook signal direction and magnitude remain stable across runs. Treat <1% drift as acceptable.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What changed and why
|
|
25
|
+
|
|
26
|
+
### 1. Time model: explicit window, no shift
|
|
27
|
+
|
|
28
|
+
**Before (1.3.0):** Events were generated in a fixed historical window anchored at `FIXED_NOW = 2024-02-02`, then every timestamp was shifted forward to present at context-creation time using `TIME_SHIFT_SECONDS = wall_clock_now - FIXED_NOW`. This meant:
|
|
29
|
+
|
|
30
|
+
- Same seed + same config produced *different* absolute calendar dates each run (the shift offset depended on wall-clock now).
|
|
31
|
+
- DOW / DOM hooks that targeted "Friday" or "the 1st of the month" silently aligned to *different* calendar days across runs.
|
|
32
|
+
- The "deterministic" guarantee in CLAUDE.md was technically false for any time-related output.
|
|
33
|
+
|
|
34
|
+
**After (1.3.1):**
|
|
35
|
+
|
|
36
|
+
- Two new optional config keys: `datasetStart` and `datasetEnd`. Both accept ISO strings, unix seconds, unix milliseconds, or anything `dayjs()` parses.
|
|
37
|
+
- **If both are set**: window is pinned. Events are generated directly inside that window, no shift. **Bit-exact deterministic across runs and machines.** `numDays` is recomputed from the window and any user-supplied `numDays` is overridden (with a warning).
|
|
38
|
+
- **If neither is set**: window falls back to `(today_start - numDays, today_start)`. Deterministic within a single calendar day; slides by 1 day across days. A warning is logged.
|
|
39
|
+
- **If exactly one is set**: throws. Both or neither.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// Pin for bit-exact determinism (what every shipped dungeon now does):
|
|
43
|
+
{
|
|
44
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
45
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
46
|
+
// numDays: 100, // ignored when datasetStart/datasetEnd are pinned
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Relative fallback (existing behavior, with a warning):
|
|
50
|
+
{
|
|
51
|
+
numDays: 100,
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Removed surface area
|
|
56
|
+
|
|
57
|
+
The following no longer exist on `context` (you'll get type errors in `tsc --noEmit` if you reference them):
|
|
58
|
+
|
|
59
|
+
- `context.TIME_SHIFT_SECONDS`
|
|
60
|
+
- `context.MAX_TIME`
|
|
61
|
+
- `context.getTimeShift()`
|
|
62
|
+
- `context.getDaysShift()`
|
|
63
|
+
|
|
64
|
+
If you wrote a hook or extension that touched any of these, replace with:
|
|
65
|
+
|
|
66
|
+
| Old | New |
|
|
67
|
+
|-----|-----|
|
|
68
|
+
| `context.TIME_SHIFT_SECONDS` | gone — events live in the window directly, no shift |
|
|
69
|
+
| `context.MAX_TIME` | `context.FIXED_NOW` (or `meta.datasetEnd` from inside a hook) |
|
|
70
|
+
| `context.getTimeShift()` | gone |
|
|
71
|
+
| `context.getDaysShift()` | gone |
|
|
72
|
+
|
|
73
|
+
`context.FIXED_NOW` and `context.FIXED_BEGIN` are still around — they're now equal to the resolved `datasetEnd` / `datasetStart` directly (no shift).
|
|
74
|
+
|
|
75
|
+
### 3. Hook authoring rule: no wall-clock reads
|
|
76
|
+
|
|
77
|
+
**This is now ENFORCED by convention and audited by the validator.** Inside any hook, never read wall-clock time:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
// ❌ Forbidden — non-deterministic across runs
|
|
81
|
+
hook(record, type, meta) {
|
|
82
|
+
const NOW = dayjs();
|
|
83
|
+
const LAUNCH_DATE = NOW.subtract(25, 'days');
|
|
84
|
+
// ...
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ✅ Required — anchored to the resolved dataset window
|
|
88
|
+
hook(record, type, meta) {
|
|
89
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
90
|
+
const datasetEnd = dayjs.unix(meta.datasetEnd);
|
|
91
|
+
const LAUNCH_DATE = datasetEnd.subtract(25, 'days');
|
|
92
|
+
// ...
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`meta.datasetStart` and `meta.datasetEnd` are unix seconds, present on every hook invocation (`event`, `user`, `everything`, `funnel-pre`, `funnel-post`, `scd-pre`, plus storage hooks). The `HookMetaTimeAnchors` interface in `types.d.ts` documents this — every `HookMeta*` type extends it.
|
|
97
|
+
|
|
98
|
+
**Why this matters**: any wall-clock read inside a hook leaks the run's wall-clock time into the output, breaking determinism. The previous time-shift would mask this somewhat (the shift compensated), but with the shift gone, any `dayjs()` call in a hook directly produces non-deterministic data.
|
|
99
|
+
|
|
100
|
+
### 4. Audited and reworked
|
|
101
|
+
|
|
102
|
+
All 15 internal source files that touched the time-shift were updated:
|
|
103
|
+
|
|
104
|
+
- `lib/core/context.js` — removed shift fields, simplified `DATASET_*_SECONDS` to direct getters
|
|
105
|
+
- `lib/core/config-validator.js` — added `resolveDatasetWindow()` helper and `parseToUnix()`. Module-level `FIXED_NOW = dayjs('2024-02-02').unix()` is gone. `resolveWorldEvents` and `resolveAnomalies` now take resolved unix seconds instead of `numDays`.
|
|
106
|
+
- `lib/utils/utils.js` — `TimeSoup()` and `bunchIntoSessions()` no longer take `timeShiftSeconds`. `person()` now takes `datasetEndUnix` to anchor `created` dates inside the window.
|
|
107
|
+
- `lib/generators/events.js` — events written with raw soup timestamp; no shift
|
|
108
|
+
- `lib/generators/funnels.js` — `MAX_TIME` clamp replaced with `FIXED_NOW` clamp
|
|
109
|
+
- `lib/generators/scd.js` — replaced `const now = dayjs()` (wall-clock leak) with `dayjs.unix(context.FIXED_NOW)`
|
|
110
|
+
- `lib/generators/mirror.js` — same fix as scd.js
|
|
111
|
+
- `lib/orchestrators/user-loop.js` — every `+ TIME_SHIFT_SECONDS` arithmetic dropped; subscription, bot, and anomaly burst generators no longer add the shift
|
|
112
|
+
- `index.js` — module-level `FIXED_NOW = dayjs('2024-02-02').unix()` is gone; window flows from validated config
|
|
113
|
+
- `dungeons/vertical/*.js` (×20) — pinned to the new window
|
|
114
|
+
- `dungeons/technical/simple.js` and `dungeons/technical/array-of-object-lookup.js` — removed wall-clock leaks in hooks
|
|
115
|
+
|
|
116
|
+
Two pre-existing wall-clock leaks were fixed in the process (`lib/generators/scd.js`, `lib/generators/mirror.js`). These were silently breaking determinism even in 1.3.0; they're now plugged.
|
|
117
|
+
|
|
118
|
+
### 5. Type changes
|
|
119
|
+
|
|
120
|
+
`types.d.ts` was updated:
|
|
121
|
+
|
|
122
|
+
- `Dungeon.datasetStart?: string | number` and `Dungeon.datasetEnd?: string | number` added.
|
|
123
|
+
- `Dungeon.epochStart` / `Dungeon.epochEnd` are now `@deprecated` aliases — internally aliased to `datasetStart` / `datasetEnd`.
|
|
124
|
+
- `Context.TIME_SHIFT_SECONDS`, `Context.MAX_TIME`, `Context.getTimeShift`, `Context.getDaysShift` are removed.
|
|
125
|
+
- `Context.DATASET_START_SECONDS` and `Context.DATASET_END_SECONDS` added (aliases of `FIXED_BEGIN` / `FIXED_NOW`).
|
|
126
|
+
- New `HookMetaTimeAnchors` interface; every `HookMeta*` type now extends it.
|
|
127
|
+
|
|
128
|
+
`tsc --noEmit` is clean (`npm run typecheck`).
|
|
129
|
+
|
|
130
|
+
### 6. New scripts
|
|
131
|
+
|
|
132
|
+
**`scripts/smoke-test-all.mjs`** — runs every dungeon in a directory at tiny scale (default: 100 users, 1000 events) in parallel, one child node process per dungeon. Used to verify dungeons load and generate data without crashing before committing changes.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
node scripts/smoke-test-all.mjs # vertical dungeons (default)
|
|
136
|
+
node scripts/smoke-test-all.mjs --dir dungeons/technical # technical dungeons
|
|
137
|
+
node scripts/smoke-test-all.mjs --dir dungeons # both
|
|
138
|
+
node scripts/smoke-test-all.mjs --concurrency 4 # default: cpu count
|
|
139
|
+
node scripts/smoke-test-all.mjs --users 500 --events 5000 # override scale
|
|
140
|
+
node scripts/smoke-test-all.mjs --keep # keep output files
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Output is a per-dungeon PASS/FAIL line + a summary table + a non-zero exit on any failure.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Rules for new hooks (the canonical reference)
|
|
148
|
+
|
|
149
|
+
The `.claude/skills/create-dungeon/SKILL.md` file has been updated with these rules. Summarized here:
|
|
150
|
+
|
|
151
|
+
### Time anchors
|
|
152
|
+
|
|
153
|
+
- **Always** read time anchors from `meta.datasetStart` / `meta.datasetEnd` (unix seconds).
|
|
154
|
+
- **Never** call `dayjs()` (wall-clock) inside a hook.
|
|
155
|
+
- **Never** declare `const NOW = dayjs(); const DATASET_START = NOW.subtract(num_days, 'days')` at module scope. This was the legacy pattern; it is now forbidden.
|
|
156
|
+
- For pre-existing wall-clock anchors that referred to "today", use `dayjs.unix(meta.datasetEnd)` instead.
|
|
157
|
+
|
|
158
|
+
### Schema-first hook design (unchanged from 1.3.0)
|
|
159
|
+
|
|
160
|
+
- Every property in the output must be defined in `events`, `userProps`, or `superProps` with a default. Hooks modify existing values; they do not invent new properties.
|
|
161
|
+
- Boolean flags must be defined as `[false]` defaults in the event config; the hook flips them to `true`.
|
|
162
|
+
|
|
163
|
+
### NO flag-stamping for behavioral cohorts (REV 2 from prior session)
|
|
164
|
+
|
|
165
|
+
- Don't stamp cohort flags like `is_whale=true`, `sweet_spot=true`, `over_engaged=true` onto events.
|
|
166
|
+
- Cohort effects must be discoverable behaviorally (`COUNT(swap) per user → bucket`), never via a tell-tale boolean breakdown.
|
|
167
|
+
|
|
168
|
+
### Where DOW / DOM checks belong
|
|
169
|
+
|
|
170
|
+
- DOW / DOM / hour-of-day checks belong in the **everything hook**, not the event hook. When `hasSessionIds: true`, `bunchIntoSessions()` redistributes timestamps after the event hook fires — any time-of-day tagging done in the event hook becomes decorrelated from the final output.
|
|
171
|
+
- Use `new Date(e.time).getUTCDay()` / `.getUTCDate()` / `.getUTCHours()` (NOT `dayjs(e.time).day()` etc., which defaults to local time).
|
|
172
|
+
|
|
173
|
+
### Window pinning for new dungeons
|
|
174
|
+
|
|
175
|
+
The base config template in `.claude/skills/create-dungeon/SKILL.md` now includes:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
{
|
|
179
|
+
token,
|
|
180
|
+
seed: SEED,
|
|
181
|
+
// Pin the dataset window — gives bit-exact determinism.
|
|
182
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
183
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
184
|
+
// numDays: num_days, // commented for reference
|
|
185
|
+
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
186
|
+
numUsers: num_users,
|
|
187
|
+
// ...
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Pin every new vertical dungeon. Technical dungeons that exist to test relative-window behavior should keep `numDays`.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Migration checklist (for existing dungeons not in this repo)
|
|
196
|
+
|
|
197
|
+
1. **Audit your hook for wall-clock reads**:
|
|
198
|
+
```bash
|
|
199
|
+
grep -nE "const NOW = dayjs|NOW\.subtract|dayjs\(\)\.subtract" path/to/your/dungeon.js
|
|
200
|
+
```
|
|
201
|
+
Replace each hit with `meta.datasetStart` / `meta.datasetEnd` based anchors.
|
|
202
|
+
|
|
203
|
+
2. **Pin the window** in your config:
|
|
204
|
+
```js
|
|
205
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
206
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
3. **Remove uses of removed Context fields**: `TIME_SHIFT_SECONDS`, `MAX_TIME`, `getTimeShift`, `getDaysShift`. If a hook needs the upper window bound, use `context.FIXED_NOW` (orchestrator code) or `meta.datasetEnd` (hook code).
|
|
210
|
+
|
|
211
|
+
4. **Run typecheck**: `npm run typecheck` (must be clean).
|
|
212
|
+
|
|
213
|
+
5. **Run smoke test**: `node scripts/smoke-test-all.mjs --dir path/to/your/dungeons`. Every dungeon must report PASS.
|
|
214
|
+
|
|
215
|
+
6. **Run vitest**: `npm test`. 759 tests should pass.
|
|
216
|
+
|
|
217
|
+
7. **Verify determinism** (if reproducibility matters to you):
|
|
218
|
+
```bash
|
|
219
|
+
node scripts/verify-runner.mjs path/to/dungeon.js verify-1
|
|
220
|
+
node scripts/verify-runner.mjs path/to/dungeon.js verify-2
|
|
221
|
+
diff <(jq -r '.eventCount' /tmp/verify-1.json) <(jq -r '.eventCount' /tmp/verify-2.json)
|
|
222
|
+
# Event counts should match within ~0.5%. Hash equality is the strict goal
|
|
223
|
+
# but Round 3 verification found 3/20 shipped dungeons have <0.5% drift
|
|
224
|
+
# from RNG-state interaction (not a regression). >1% drift indicates a
|
|
225
|
+
# genuine non-determinism source (wall-clock leak, missed seed, etc.).
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Things that are NOT changed
|
|
231
|
+
|
|
232
|
+
- The hook system signature (`hook(record, type, meta)`) is identical.
|
|
233
|
+
- Storage, batching, output formats — unchanged.
|
|
234
|
+
- All non-hook config keys (events, funnels, userProps, superProps, scdProps, mirrorProps, lookupTables, groupKeys, etc.) — unchanged.
|
|
235
|
+
- The seeded RNG — unchanged. (`chance` + `seedrandom` still drives every random decision.)
|
|
236
|
+
- TimeSoup's DOW/HOD weighting logic — unchanged behaviorally; internally simpler (no `+ timeShiftSeconds` arithmetic).
|
|
237
|
+
- The 20 dungeon stories themselves — same hooks, same intents, same expected metrics. Only the time anchors moved.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Known issues observed during the upgrade
|
|
242
|
+
|
|
243
|
+
- `dungeons/technical/text-generation.js` has a pre-existing bug in `lib/generators/text.js:506` (`pick(...) is not a function`) that surfaces under certain seeds. Not related to the time-model change. Tracked separately.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## References
|
|
248
|
+
|
|
249
|
+
- New config validator: `lib/core/config-validator.js` (`resolveDatasetWindow`, `parseToUnix`)
|
|
250
|
+
- New `Context` definition: `lib/core/context.js`
|
|
251
|
+
- New `HookMetaTimeAnchors`: `types.d.ts`
|
|
252
|
+
- Updated skill rules: `.claude/skills/create-dungeon/SKILL.md` (now includes "Common bugs to avoid" #1-10) and `.claude/skills/verify-hooks/SKILL.md` (now includes Funnel-Post Cross-Event MIN-to-MIN Limitation, Magic-Number Cohort Sizing, and Re-run Required After Hook Edits sections)
|
|
253
|
+
- Validator hand-off: `research/hand-off-prod-validator.md` (REV 6 + REV 7 ADDENDUM)
|
|
254
|
+
- Phase-8 production-validator outcome: `research/verticals/SUMMARY.md`
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Post-1.3.1 hook-pattern audit (Phase 8 fix sweep)
|
|
259
|
+
|
|
260
|
+
After the 1.3.1 time-model migration, the production-validator was re-run end-to-end across all 20 vertical dungeons. The migration itself was clean (0 wall-clock leaks remained), but the audit revealed cohort-size and measurement-design issues that pre-dated 1.3.1 and were now exposed by the deterministic window. Phase 8 addressed all of them — outcome: ~190 PASS / 0 WEAK / 0 FAIL.
|
|
261
|
+
|
|
262
|
+
### What Phase 8 codified
|
|
263
|
+
|
|
264
|
+
11 dungeon files were modified (no `lib/` changes). 18 hook fixes plus 3 follow-up tweaks. The patterns are now documented in the skill files and should not need re-discovery for future dungeons:
|
|
265
|
+
|
|
266
|
+
| Pattern | Skill location | Phase-8 dungeons that hit it |
|
|
267
|
+
|---------|----------------|------------------------------|
|
|
268
|
+
| Drop-event funnel dilution (per-event filter doesn't move funnel %) | create-dungeon "Common bugs" #1; verify-hooks "Drop-Event Funnel Dilution Diagnosis" | food-delivery, healthcare, insurance, travel, fitness |
|
|
269
|
+
| Inverted-U cohort confound on drop side | create-dungeon "Common bugs" #3 | education, fintech, food-delivery, healthcare, social |
|
|
270
|
+
| Subscription cohort sizing (lifecycle rates default too low) | create-dungeon "Common bugs" #2; verify-hooks "Subscription Tier Cohort Sizing Check" | fitness, healthcare |
|
|
271
|
+
| Funnel-post can't influence cross-event MIN-to-MIN T2C | create-dungeon "Common bugs" #8; verify-hooks "Funnel-Post Cross-Event MIN-to-MIN Limitation" | gaming H12 |
|
|
272
|
+
| Subscription lifecycle rates ≤ 0.65 to avoid likelihood-out-of-range | create-dungeon "Common bugs" #9 | fitness initial overshoot at trialToPayRate=0.85 |
|
|
273
|
+
| Magic-number ranges must match actual event distribution | create-dungeon "Common bugs" #10; verify-hooks "Magic-Number Cohort Sizing" | dating photo magic |
|
|
274
|
+
| Re-run required after hook edit (stale data trap) | verify-hooks "Re-run Required After Hook Edits" | every dungeon during fix loop |
|
|
275
|
+
|
|
276
|
+
### Two dungeons now scaled above defaults
|
|
277
|
+
|
|
278
|
+
REV 6 of the validator hand-off granted permission to bump `numUsers` / `avgEventsPerUserPerDay` for cohort statistical adequacy. Two dungeons used this:
|
|
279
|
+
|
|
280
|
+
- `insurance-application.js`: numUsers 5K → 15K (cohort dilution across app_version values)
|
|
281
|
+
- `dating.js`: numUsers 8K → 30K, avgEventsPerUserPerDay 0.75 → 1.5 (photo cohort distribution heavily skewed to 0-1 photos)
|
|
282
|
+
|
|
283
|
+
These bumps are checked in. Other dungeons remain at original scale.
|
|
284
|
+
|
|
285
|
+
### Updated dungeon hook-pattern recipes (now in create-dungeon skill)
|
|
286
|
+
|
|
287
|
+
The create-dungeon skill's "Common bugs to avoid" section now has 10 entries (was 7 pre-Phase-8). Bugs #8, #9, #10 are net-new from Phase 8 and cover the funnel-post limitation, the lifecycle-rate range constraint, and the magic-number distribution check. Future dungeon authors should consult these BEFORE writing T2C / subscription-cohort / magic-number hooks.
|
|
288
|
+
|
|
289
|
+
### What Round 2 of the validator should look for
|
|
290
|
+
|
|
291
|
+
The validator hand-off REV 7 (in `research/hand-off-prod-validator.md`) describes a Round 2 re-verification pass that confirms Phase 8 fixes hold up under fresh verification, with no regressions. Round 2 is observe-only — no fix loop unless a hook regresses.
|
|
292
|
+
|
|
293
|
+
### Round 2 outcome (post-Phase-8 re-verification)
|
|
294
|
+
|
|
295
|
+
Round 2 was completed end-to-end on all 20 dungeons. **Outcome: 187 hooks, all PASS, zero regressions.** Every Phase 8 fix held under fresh re-verification. See `research/verticals/ROUND2-SUMMARY.md` for the full per-dungeon breakdown.
|
|
296
|
+
|
|
297
|
+
### Round 3 outcome (post-refinement re-verification)
|
|
298
|
+
|
|
299
|
+
After the JSDoc-only refinement pass below, all 20 dungeons were re-verified at full fidelity a third time. **Outcome: 187 hooks, all PASS, zero regressions.** 17/20 dungeons produced bit-exact identical event counts to Round 2; 3 (ecommerce, gaming, travel) showed <0.5% event-count variance with hook signals stable. JSDoc-only edits do not affect runtime, confirmed by the comparison. See `research/verticals/ROUND3-SUMMARY.md`.
|
|
300
|
+
|
|
301
|
+
### Refinement pass learnings (added after Round 2)
|
|
302
|
+
|
|
303
|
+
Three small JSDoc-only improvements were applied across all 20 dungeons:
|
|
304
|
+
|
|
305
|
+
1. **Funnel-post measurement caveat added to every funnel-post hook JSDoc.** Without it, validators and analysts running cross-event MIN→MIN SQL queries see no segment effect and waste cycles trying to "fix" a working hook. Caveat text is in `.claude/skills/create-dungeon/SKILL.md` under "Required cohort-discovery hook patterns → 1. Time-to-convert".
|
|
306
|
+
|
|
307
|
+
2. **Stale PATTERN prose after fix-loop refactors.** Two dungeons had JSDoc PATTERNs that described the OLD code behavior even though Phase 8 had reworked the implementation. healthcare H10's PATTERN said "drop 30% of follow up scheduled events" but Phase 8 C4 changed the code to `days_until_followup *= 1.5`. insurance H3's EXPECTED METRICS table row showed "1.7x" but PATTERN + Phase 8 A3 reflect 1.3x. Both fixed; codified as bug #11 in create-dungeon SKILL.md.
|
|
308
|
+
|
|
309
|
+
3. **Cross-event T2C verdict rule.** verify-hooks SKILL.md now documents that funnel-post hooks with the standard measurement caveat in their JSDoc should be marked PASS by code inspection alone — running the cross-event SQL is wasted effort because the caveat already says it won't show.
|
|
310
|
+
|
|
311
|
+
### Hook-author / verifier checklist updates
|
|
312
|
+
|
|
313
|
+
After this refinement pass, the canonical lists are:
|
|
314
|
+
|
|
315
|
+
- **create-dungeon SKILL.md "Common bugs to avoid"**: 11 entries (was 10). Bug #11 codifies the JSDoc-PATTERN-drift rule.
|
|
316
|
+
- **create-dungeon SKILL.md "Time-to-convert" pattern**: now includes the required JSDoc caveat text. Every new T2C hook MUST include the caveat in its HOW TO FIND IT block.
|
|
317
|
+
- **verify-hooks SKILL.md "Funnel-Post Cross-Event MIN-to-MIN Limitation"**: now includes a verdict rule — PASS by code inspection if the dungeon's JSDoc has the standard caveat.
|
|
318
|
+
|
|
319
|
+
### Files modified during refinement
|
|
320
|
+
|
|
321
|
+
20 dungeon files received the funnel-post caveat. healthcare.js + insurance-application.js additionally received PATTERN/Report/metrics row corrections. gaming.js had a stranded `EXPECTED METRICS SUMMARY` placeholder between hooks 11 and 12 removed. All dungeon edits are JSDoc-only — no runtime behavior changed.
|
|
322
|
+
|
|
323
|
+
Three `lib/` changes were made for the text-length cap:
|
|
324
|
+
- `lib/generators/text.js` — default `max` lowered from 500 → 254 (matches Mixpanel string property limit)
|
|
325
|
+
- `lib/generators/product-names.js` — `productGen` `max` lowered from 300 → 254
|
|
326
|
+
- `dungeons/technical/text-generation.js` — `twitterGen` `max` lowered from 280 → 254
|
|
327
|
+
|
|
328
|
+
Vertical dungeons do not use `createTextGenerator`, so these changes do not affect any shipped vertical. Only consumers who used the text generator with the default `max` (or relied on the larger cap) need to override `max` explicitly to restore the prior behavior.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# 1.4.0 Upgrade Guide
|
|
2
|
+
|
|
3
|
+
**TL;DR: No breaking changes.** Existing dungeons run without modification. New features are opt-in.
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
### Removed from engine (silently ignored)
|
|
8
|
+
- `subscription`, `attribution`, `geo`, `features`, `anomalies` config keys
|
|
9
|
+
- Engine strips them with one deprecation warning per dungeon and continues
|
|
10
|
+
- Recreate these patterns via hooks (see `HOOKS.md` recipe 3.22 and the `write-hooks` skill)
|
|
11
|
+
|
|
12
|
+
### Removed behavior
|
|
13
|
+
- **42% per-event user_id dice** — every event now gets `user_id` by default. Dungeons that relied on the probabilistic stamping will see more consistent identity. This is more correct for Mixpanel.
|
|
14
|
+
|
|
15
|
+
### New (additive, opt-in)
|
|
16
|
+
- **Identity model**: `avgDevicePerUser`, `isAuthEvent`, `isAttributionEvent` event flags, `Funnel.attempts`
|
|
17
|
+
- **Hook helpers**: `@ak--47/dungeon-master/hook-helpers` (14 atoms)
|
|
18
|
+
- **Hook patterns**: `@ak--47/dungeon-master/hook-patterns` (5 Mixpanel-aligned recipes)
|
|
19
|
+
- **Verifier**: `@ak--47/dungeon-master/verify` (`emulateBreakdown`, `verifyDungeon`)
|
|
20
|
+
- **Experiment API**: `experiment: boolean | ExperimentConfig` on funnels
|
|
21
|
+
- **Funnel-pre cursor**: usage funnels advance a temporal cursor (enables temporal conversion trends)
|
|
22
|
+
- **`Dungeon.version`**: optional metadata field (`string | number`)
|
|
23
|
+
|
|
24
|
+
### Behavioral changes (non-breaking)
|
|
25
|
+
- `insert_id` now uses `crypto.randomUUID()` instead of `quickHash(tuple)`. Eliminates collisions. Non-deterministic but irrelevant for analytics.
|
|
26
|
+
- `isStrictEvent` events no longer leak into standalone event generation. Funnels produce cleaner data.
|
|
27
|
+
- Funnel-pre hooks now have final authority over `conversionRate` (persona/world-event modifiers apply before the hook).
|
|
28
|
+
- Experiment variant assignment is deterministic per user (hash-based), not random per funnel run.
|
|
29
|
+
|
|
30
|
+
## Migration Checklist
|
|
31
|
+
|
|
32
|
+
For most dungeons: **nothing to do**. Just upgrade.
|
|
33
|
+
|
|
34
|
+
If you want to adopt new features:
|
|
35
|
+
1. Set `avgDevicePerUser: 2` + `isAuthEvent: true` on your sign-up event for realistic identity
|
|
36
|
+
2. Use `experiment: { ... }` on a funnel instead of manual variant injection in hooks
|
|
37
|
+
3. Replace hand-rolled cohort analysis in hooks with Phase 3 atoms (`binUsersByEventCount`, etc.)
|
|
38
|
+
4. Verify patterns with `/verify-hooks` after changes
|
|
39
|
+
|
|
40
|
+
## Hook Writing Guide (from 20-dungeon eval)
|
|
41
|
+
|
|
42
|
+
These patterns were validated across all 20 vertical dungeons (191 hooks total).
|
|
43
|
+
|
|
44
|
+
### Event hook vs everything hook
|
|
45
|
+
|
|
46
|
+
The `event` hook fires during generation when timestamps are in a pre-shift
|
|
47
|
+
fixed window. The `everything` hook fires after time-shifting, so timestamps
|
|
48
|
+
match `meta.datasetStart`/`meta.datasetEnd`.
|
|
49
|
+
|
|
50
|
+
| Use case | Hook type | Why |
|
|
51
|
+
|----------|-----------|-----|
|
|
52
|
+
| Temporal checks (day-in-dataset) | `everything` | Event hook timestamps don't match meta anchors |
|
|
53
|
+
| Event cloning/injection | `everything` | Event hook return replaces; everything can push() |
|
|
54
|
+
| Closure-state Maps | `event` | Track state across users (cost overrun → scale down) |
|
|
55
|
+
| Event replacement | `event` | Return a different object (alert → incident) |
|
|
56
|
+
| Simple property mutation | `event` | No temporal context needed |
|
|
57
|
+
| Profile-based effects | `everything` | `meta.profile` available |
|
|
58
|
+
|
|
59
|
+
### Ordering within everything
|
|
60
|
+
|
|
61
|
+
When multiple effects coexist in the everything hook, order matters:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
1. SuperProp stamping (profile values → events)
|
|
65
|
+
2. Non-temporal mutations and event cloning/injection
|
|
66
|
+
3. Event filtering (churn, retention, rate-limit drops)
|
|
67
|
+
4. Temporal value mutations (run LAST — catches cloned events in window)
|
|
68
|
+
5. Sort by time
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If a temporal mutation runs before cloning, cloned events that land in the
|
|
72
|
+
window miss the mutation. This caused 3 dungeons to score WEAK during the eval.
|
|
73
|
+
|
|
74
|
+
### Deprecated feature replacement
|
|
75
|
+
|
|
76
|
+
When hooks depend on properties from deprecated features (`subscription`,
|
|
77
|
+
`attribution`, `features`, `geo`, `anomalies`), those properties are missing
|
|
78
|
+
because the engine strips the config blocks. Fix:
|
|
79
|
+
|
|
80
|
+
1. Add the property to `superProps` and `userProps` with default values
|
|
81
|
+
2. Assign meaningful values in the `user` hook (hash-based or persona-based)
|
|
82
|
+
3. Use the assigned values in `everything` for downstream effects
|
|
83
|
+
|
|
84
|
+
Example: `subscription_tier` → add to superProps/userProps, assign by hash in
|
|
85
|
+
user hook, gate conversion effects on tier in everything.
|
|
86
|
+
|
|
87
|
+
### Cohort detection sizing
|
|
88
|
+
|
|
89
|
+
| Detection condition | Population | Signal quality |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `events.some(e => e.event === X)` with common X | 90%+ | No control group |
|
|
92
|
+
| `events.filter(...).length >= 3` | 10-30% | Clean signal |
|
|
93
|
+
| `charCodeAt(0) % 50 === 0` | 2% | Too small |
|
|
94
|
+
| `charCodeAt(0) % 10 === 0` | 10% | Usable |
|
|
95
|
+
|
|
96
|
+
Target 10-30% of users in the affected cohort. If detection catches 90%+,
|
|
97
|
+
require more evidence (3+ events instead of 1+). If < 5%, increase the hash
|
|
98
|
+
modulus or relax the threshold.
|
|
99
|
+
|
|
100
|
+
### Cohort detection must survive filtering
|
|
101
|
+
|
|
102
|
+
If Hook A classifies users by event presence and Hook B later removes some
|
|
103
|
+
events (churn/retention filter), verification queries can't reconstruct the
|
|
104
|
+
cohort from output data. The "non-cohort" group gets contaminated.
|
|
105
|
+
|
|
106
|
+
Fix: require 3+ marker events instead of 1+ so surviving events still
|
|
107
|
+
identify the cohort, or accept the verification limitation.
|
|
108
|
+
|
|
109
|
+
### numDays vs datasetStart/datasetEnd
|
|
110
|
+
|
|
111
|
+
Three resolution modes:
|
|
112
|
+
|
|
113
|
+
| Config | Window | Deterministic? |
|
|
114
|
+
|--------|--------|----------------|
|
|
115
|
+
| `numDays: 120` only | `[today - 120d, today]` | No (today changes) |
|
|
116
|
+
| `datasetStart` + `datasetEnd` | Pinned exactly | Yes |
|
|
117
|
+
| All three | start/end win, numDays ignored | Yes |
|
|
118
|
+
|
|
119
|
+
Ad-hoc dungeons: use `numDays` alone (simplest API).
|
|
120
|
+
Production/vertical dungeons: pin `datasetStart` + `datasetEnd` for bit-exact runs.
|
|
121
|
+
|
|
122
|
+
## External API Surface
|
|
123
|
+
|
|
124
|
+
No changes to:
|
|
125
|
+
- Default export function signature
|
|
126
|
+
- Named exports (`parseJSONDungeon`, `validateDungeonShape`, `loadFromFile`, `loadFromText`)
|
|
127
|
+
- `./utils` and `./text` subpath exports
|
|
128
|
+
|
|
129
|
+
New subpath exports (additive):
|
|
130
|
+
- `./hook-helpers`, `./hook-patterns`, `./verify`
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# 1.4.1 Upgrade Guide
|
|
2
|
+
|
|
3
|
+
**TL;DR: No breaking changes.** Two new features for Cloud Run / serverless deployments, plus bug fixes.
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
### New: File path tracking (`getWrittenFiles()`)
|
|
8
|
+
|
|
9
|
+
Every HookedArray storage container now tracks the exact file paths it writes during a run. This replaces the old `ls()` + string-filter directory scan, which:
|
|
10
|
+
|
|
11
|
+
- **Failed on GCS paths** — `ak-tools.ls()` wraps `fs.readdir`, so `writeToDisk: 'gs://bucket/...'` broke the Mixpanel sender's file discovery
|
|
12
|
+
- **Could pick up stale files** from prior runs in the same directory
|
|
13
|
+
- **Missed multi-batch SCD files** — the sender used `.pop()`, only importing the last batch file (bug fix)
|
|
14
|
+
|
|
15
|
+
The new `getWrittenFiles()` method returns an array of every path written by that container (local or `gs://`). The Mixpanel sender now uses this exclusively.
|
|
16
|
+
|
|
17
|
+
#### API surface
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const results = await DUNGEON_MASTER(config);
|
|
21
|
+
|
|
22
|
+
// results.files is now populated from getWrittenFiles() across all containers
|
|
23
|
+
// Works for both local paths and gs:// URIs
|
|
24
|
+
console.log(results.files);
|
|
25
|
+
// ['/abs/path/myDungeon-EVENTS-part-1.json', '/abs/path/myDungeon-USERS-part-1.json', ...]
|
|
26
|
+
// or ['gs://bucket/prefix/myDungeon-EVENTS-part-1.json', ...]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If you use HookedArrays directly (uncommon):
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
storage.eventData.getWrittenFiles(); // string[] — all paths written by this container
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### New: `cleanup: true` config option
|
|
36
|
+
|
|
37
|
+
Deletes all written files at end of run. Works for both local files and GCS objects. Runs in a `finally` block — files are cleaned up even if the Mixpanel import fails.
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
await DUNGEON_MASTER({
|
|
41
|
+
numUsers: 1000,
|
|
42
|
+
numEvents: 50000,
|
|
43
|
+
writeToDisk: 'gs://my-bucket/output',
|
|
44
|
+
token: 'my-token',
|
|
45
|
+
cleanup: true, // delete all files after import
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The `files` array in the result is populated *before* cleanup runs, so you can still inspect which files were written.
|
|
50
|
+
|
|
51
|
+
| Property | Type | Default | Description |
|
|
52
|
+
|----------|------|---------|-------------|
|
|
53
|
+
| `cleanup` | `boolean` | `false` | Delete all written files at end of run |
|
|
54
|
+
|
|
55
|
+
### Cloud Run / Serverless OOM Guide
|
|
56
|
+
|
|
57
|
+
The primary motivation for 1.4.1. Here's how to run dungeons larger than your container's RAM on Cloud Run (or similar serverless with tmpfs-backed `/tmp`):
|
|
58
|
+
|
|
59
|
+
**Problem:** Cloud Run's `/tmp` is backed by RAM. Writing batch files to `/tmp` doesn't reduce memory pressure — it moves bytes from one RAM region to another. A 10M-event dungeon can easily exceed 8GB.
|
|
60
|
+
|
|
61
|
+
**Solution:** Write batches directly to GCS, import from there, then clean up.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
await DUNGEON_MASTER({
|
|
65
|
+
numUsers: 100_000,
|
|
66
|
+
numEvents: 10_000_000,
|
|
67
|
+
batchSize: 100_000, // small batches = frequent flushes = low peak memory
|
|
68
|
+
concurrency: 1, // one user at a time
|
|
69
|
+
writeToDisk: 'gs://your-bucket/run-1', // bypass tmpfs entirely
|
|
70
|
+
token: 'your-mixpanel-token',
|
|
71
|
+
format: 'json', // or 'csv' — sender can read both back
|
|
72
|
+
cleanup: true, // delete GCS files after import
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Why this works:**
|
|
77
|
+
1. `batchSize: 100_000` — HookedArray flushes to GCS every 100K records, then clears memory (`arr.length = 0`)
|
|
78
|
+
2. `writeToDisk: 'gs://...'` — streams directly to GCS, never touches `/tmp`
|
|
79
|
+
3. The sender passes the `gs://` file paths directly to `mixpanel-import`, which streams from GCS natively (no download)
|
|
80
|
+
4. `cleanup: true` — deletes GCS objects after import completes
|
|
81
|
+
|
|
82
|
+
**Memory profile:** Peak memory ≈ `batchSize × ~1KB/event` + one user's events (typically <2MB). A 100K `batchSize` uses ~100MB peak, well within 8GB.
|
|
83
|
+
|
|
84
|
+
**Performance note:** Each batch flush does a network write to GCS (~50-200ms). For 10M events with `batchSize: 100_000`, that's 100 GCS writes. Total overhead is typically 10-20 seconds — negligible compared to generation time.
|
|
85
|
+
|
|
86
|
+
### Warnings gated behind `verbose: true`
|
|
87
|
+
|
|
88
|
+
All `console.warn()` calls in the config validator and storage layer now only fire when `verbose: true` (previously some fired unconditionally). Affected warnings:
|
|
89
|
+
|
|
90
|
+
- Dataset window fallback (no `datasetStart`/`datasetEnd`)
|
|
91
|
+
- `numDays` override by `datasetStart`/`datasetEnd`
|
|
92
|
+
- Auto-batch mode activation
|
|
93
|
+
- Hook string conversion failures
|
|
94
|
+
- `isAuthEvent` without `avgDevicePerUser`
|
|
95
|
+
- `writeToDisk: false` with low `batchSize`
|
|
96
|
+
- Deprecated 1.4 config key removal
|
|
97
|
+
|
|
98
|
+
### Bug fix: SCD multi-batch import
|
|
99
|
+
|
|
100
|
+
The Mixpanel sender previously used `.pop()` when discovering SCD batch files, which meant only the last batch file was imported. If a dungeon generated enough SCD records to span multiple batch files, earlier batches were silently dropped. Fixed — all batch files are now imported.
|
|
101
|
+
|
|
102
|
+
## Migration Checklist
|
|
103
|
+
|
|
104
|
+
For most dungeons: **nothing to do**. Just upgrade.
|
|
105
|
+
|
|
106
|
+
If you're running on Cloud Run / serverless:
|
|
107
|
+
1. Set `writeToDisk: 'gs://your-bucket/path'` to bypass tmpfs
|
|
108
|
+
2. Set `batchSize: 100_000` (or smaller) for low peak memory
|
|
109
|
+
3. Set `concurrency: 1`
|
|
110
|
+
4. Set `cleanup: true` if you don't need the files after import
|
|
111
|
+
5. Use `format: 'json'` or `'csv'` (not `'parquet'` — the sender can't read Parquet back yet)
|
|
112
|
+
|
|
113
|
+
## Type Changes
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
interface Dungeon {
|
|
117
|
+
// ... existing ...
|
|
118
|
+
cleanup?: boolean; // NEW — default false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface HookedArray<T> extends Array<T> {
|
|
122
|
+
// ... existing ...
|
|
123
|
+
getWrittenFiles: () => string[]; // NEW
|
|
124
|
+
}
|
|
125
|
+
```
|