@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.
Files changed (146) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -12
  2. package/.claude/skills/create-dungeon/SKILL.md +65 -5
  3. package/.claude/skills/create-project/SKILL.md +96 -0
  4. package/.claude/skills/create-project/provision.mjs +336 -0
  5. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  6. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  7. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  8. package/.claude/skills/write-hooks/SKILL.md +156 -18
  9. package/CHANGELOG.md +211 -0
  10. package/HOOKS.md +566 -112
  11. package/README.md +28 -9
  12. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  13. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  14. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  15. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  16. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  17. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  18. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  19. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  20. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  21. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  22. package/dungeons/technical/stories-verify.js +152 -0
  23. package/dungeons/vertical/README.md +154 -0
  24. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  25. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  26. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  27. package/dungeons/vertical/community/community.js +1589 -0
  28. package/dungeons/vertical/community/community.sql +216 -0
  29. package/dungeons/vertical/community/community.verify.mjs +72 -0
  30. package/dungeons/vertical/crypto/crypto.js +1483 -0
  31. package/dungeons/vertical/crypto/crypto.sql +194 -0
  32. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  33. package/dungeons/vertical/dating/dating.js +1540 -0
  34. package/dungeons/vertical/dating/dating.sql +288 -0
  35. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  36. package/dungeons/vertical/devtools/devtools.js +1524 -0
  37. package/dungeons/vertical/devtools/devtools.sql +161 -0
  38. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  39. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  40. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  41. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  42. package/dungeons/vertical/education/education.js +1763 -0
  43. package/dungeons/vertical/education/education.sql +243 -0
  44. package/dungeons/vertical/education/education.verify.mjs +75 -0
  45. package/dungeons/vertical/fintech/fintech.js +1410 -0
  46. package/dungeons/vertical/fintech/fintech.sql +175 -0
  47. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  48. package/dungeons/vertical/fitness/fitness.js +1495 -0
  49. package/dungeons/vertical/fitness/fitness.sql +206 -0
  50. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  51. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  52. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  53. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  54. package/dungeons/vertical/gaming/gaming.js +1741 -0
  55. package/dungeons/vertical/gaming/gaming.sql +249 -0
  56. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  57. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  58. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  59. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  60. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  61. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  62. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  63. package/dungeons/vertical/logistics/logistics.js +1541 -0
  64. package/dungeons/vertical/logistics/logistics.sql +205 -0
  65. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  66. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  67. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  68. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  69. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  70. package/dungeons/vertical/media/media.sql +173 -0
  71. package/dungeons/vertical/media/media.verify.mjs +74 -0
  72. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  73. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  74. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  75. package/dungeons/vertical/sass/sass.js +1733 -0
  76. package/dungeons/vertical/sass/sass.sql +173 -0
  77. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  78. package/dungeons/vertical/social/social.js +1456 -0
  79. package/dungeons/vertical/social/social.sql +160 -0
  80. package/dungeons/vertical/social/social.verify.mjs +76 -0
  81. package/dungeons/vertical/streaming/streaming.js +677 -0
  82. package/dungeons/vertical/streaming/streaming.sql +101 -0
  83. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  84. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  85. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  86. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  87. package/dungeons/vertical/travel/travel.js +1603 -0
  88. package/dungeons/vertical/travel/travel.sql +236 -0
  89. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  90. package/index.js +2 -2
  91. package/lib/core/config-validator.js +37 -2
  92. package/lib/core/context.js +4 -2
  93. package/lib/core/dungeon-loader.js +20 -15
  94. package/lib/core/dungeon-to-json.js +22 -4
  95. package/lib/core/storage.js +3 -3
  96. package/lib/generators/funnels.js +36 -9
  97. package/lib/hook-helpers/cohort.js +53 -0
  98. package/lib/hook-helpers/index.js +8 -0
  99. package/lib/hook-helpers/mutate.js +1 -1
  100. package/lib/hook-helpers/shape.js +319 -0
  101. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  102. package/lib/hook-patterns/attributed-by-source.js +60 -58
  103. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  104. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  105. package/lib/hook-patterns/index.js +1 -1
  106. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  107. package/lib/orchestrators/user-loop.js +41 -0
  108. package/lib/templates/story-spec.schema.json +175 -0
  109. package/lib/utils/json-evaluator.js +1 -1
  110. package/lib/utils/utils.js +5 -4
  111. package/lib/verify/coerce.js +186 -0
  112. package/lib/verify/counting.js +270 -32
  113. package/lib/verify/emulate-breakdown.js +1285 -203
  114. package/lib/verify/first-time.js +89 -0
  115. package/lib/verify/flows.js +679 -0
  116. package/lib/verify/formula.js +259 -0
  117. package/lib/verify/funnel-engine.js +697 -113
  118. package/lib/verify/index.js +32 -4
  119. package/lib/verify/sessionize.js +275 -0
  120. package/lib/verify/story-runner.js +483 -0
  121. package/lib/verify/verify-dungeon.js +59 -37
  122. package/package.json +3 -2
  123. package/scripts/extract-dungeon-schema.mjs +2 -2
  124. package/scripts/run-many.mjs +2 -2
  125. package/scripts/verify-runner.mjs +2 -2
  126. package/scripts/verify-stories.mjs +244 -0
  127. package/types.d.ts +296 -30
  128. package/dungeons/vertical/ai-platform.js +0 -818
  129. package/dungeons/vertical/community.js +0 -966
  130. package/dungeons/vertical/crypto.js +0 -895
  131. package/dungeons/vertical/dating.js +0 -830
  132. package/dungeons/vertical/devtools.js +0 -1050
  133. package/dungeons/vertical/ecommerce.js +0 -811
  134. package/dungeons/vertical/education.js +0 -964
  135. package/dungeons/vertical/fintech.js +0 -946
  136. package/dungeons/vertical/fitness.js +0 -935
  137. package/dungeons/vertical/food-delivery.js +0 -934
  138. package/dungeons/vertical/gaming.js +0 -1120
  139. package/dungeons/vertical/healthcare.js +0 -945
  140. package/dungeons/vertical/insurance-application.js +0 -1039
  141. package/dungeons/vertical/logistics.js +0 -986
  142. package/dungeons/vertical/marketplace.js +0 -1019
  143. package/dungeons/vertical/real-estate.js +0 -872
  144. package/dungeons/vertical/sass.js +0 -995
  145. package/dungeons/vertical/social.js +0 -904
  146. 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
+ ```