@ak--47/dungeon-master 1.4.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/analyze-soup/SKILL.md +158 -0
- package/.claude/skills/create-dungeon/SKILL.md +464 -0
- package/.claude/skills/verify-dungeon/SKILL.md +157 -0
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
- package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
- package/.claude/skills/write-hooks/SKILL.md +468 -0
- package/CHANGELOG.md +147 -0
- package/HOOKS.md +1243 -597
- package/README.md +140 -5
- package/dungeons/technical/ad-spend.js +1 -1
- package/dungeons/technical/anonymous-users.js +1 -1
- package/dungeons/technical/array-of-object-lookup.js +1 -1
- package/dungeons/technical/datagen-v15-verify.js +74 -0
- package/dungeons/technical/experiments.js +1 -1
- package/dungeons/technical/foobar.js +1 -1
- package/dungeons/technical/group-analytics.js +1 -1
- package/dungeons/technical/mirror-strategies.js +1 -1
- package/dungeons/technical/nested-objects.js +1 -1
- package/dungeons/technical/retention-cadence.js +1 -1
- package/dungeons/technical/sanity.js +1 -1
- package/dungeons/technical/scale-test.js +1 -1
- package/dungeons/technical/scd.js +1 -1
- package/dungeons/technical/simple.js +1 -1
- package/dungeons/technical/simplest.js +74 -20
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +4 -0
- package/dungeons/vertical/community.js +9 -3
- package/dungeons/vertical/crypto.js +5 -0
- package/dungeons/vertical/dating.js +23 -10
- package/dungeons/vertical/devtools.js +10 -0
- package/dungeons/vertical/ecommerce.js +6 -0
- package/dungeons/vertical/education.js +11 -0
- package/dungeons/vertical/fintech.js +13 -0
- package/dungeons/vertical/fitness.js +10 -0
- package/dungeons/vertical/food-delivery.js +9 -0
- package/dungeons/vertical/gaming.js +10 -0
- package/dungeons/vertical/healthcare.js +5 -0
- package/dungeons/vertical/insurance-application.js +10 -0
- package/dungeons/vertical/logistics.js +8 -1
- package/dungeons/vertical/marketplace.js +7 -0
- package/dungeons/vertical/media.js +8 -0
- package/dungeons/vertical/real-estate.js +7 -1
- package/dungeons/vertical/sass.js +12 -0
- package/dungeons/vertical/social.js +9 -0
- package/dungeons/vertical/travel.js +5 -0
- package/index.js +45 -7
- package/lib/core/config-validator.js +270 -7
- package/lib/core/context.js +58 -0
- package/lib/core/dungeon-loader.js +2 -5
- package/lib/generators/events.js +12 -13
- package/lib/generators/funnels.js +72 -1
- package/lib/hook-helpers/index.js +1 -0
- package/lib/hook-helpers/inject.js +95 -0
- package/lib/orchestrators/mixpanel-sender.js +27 -1
- package/lib/orchestrators/user-loop.js +488 -29
- package/lib/templates/macro-presets.js +39 -9
- package/lib/utils/utils.js +16 -79
- package/lib/verify/counting.js +320 -0
- package/lib/verify/emulate-breakdown.js +512 -108
- package/lib/verify/funnel-engine.js +539 -0
- package/lib/verify/identity.js +78 -0
- package/lib/verify/index.js +19 -0
- package/lib/verify/verify-dungeon.js +58 -0
- package/package.json +4 -2
- package/types.d.ts +314 -4
- package/scripts/smoke-test-all.mjs +0 -162
package/README.md
CHANGED
|
@@ -97,6 +97,8 @@ a dungeon is a javascript file that exports a configuration object. it defines y
|
|
|
97
97
|
|
|
98
98
|
see `dungeons/vertical/` for customer-facing story dungeons (18 events, 8 hooks) and `dungeons/technical/` for feature-testing dungeons (mirrors, groups, scale, anonymous users).
|
|
99
99
|
|
|
100
|
+
every vertical dungeon ships with a verification proof at `verification/verticals/<name>.{verify.mjs,sql}` — a CI-runnable assertion that the dungeon's documented hooks actually appear in the generated data at full fidelity. 20 dungeons, 107 hooks, 107 checks. see [`verification/verticals/README.md`](verification/verticals/README.md).
|
|
101
|
+
|
|
100
102
|
```javascript
|
|
101
103
|
// dungeons/my-app.js
|
|
102
104
|
import dayjs from 'dayjs';
|
|
@@ -153,6 +155,84 @@ import { createTextGenerator, generateBatch } from '@ak--47/dungeon-master/text'
|
|
|
153
155
|
|
|
154
156
|
these are the same functions used internally. `pickAWinner` creates weighted distributions, `weighNumRange` generates realistic numeric ranges with configurable skew, and the text generators produce organic-looking strings with sentiment analysis and keyword injection.
|
|
155
157
|
|
|
158
|
+
## how it works
|
|
159
|
+
|
|
160
|
+
one call to `DUNGEON_MASTER(config)` runs through these phases in order:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
input → validate → create context → init storage → ad spend
|
|
164
|
+
(+ v1.5 clamps) (FIXED_NOW, seed) (HookedArray bins) (if hasAdSpend)
|
|
165
|
+
│
|
|
166
|
+
▼
|
|
167
|
+
┌────────────┐
|
|
168
|
+
│ userLoop │ ← per-user generation (most of the work happens here)
|
|
169
|
+
└────────────┘
|
|
170
|
+
│
|
|
171
|
+
▼
|
|
172
|
+
groups + SCDs → lookup tables → mirror datasets → flush to disk → mixpanel → return
|
|
173
|
+
(if writeToDisk) (if token)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`userLoop` per-user lifecycle (hooks marked with `►`, terminal guards with `■`):
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
[next user]
|
|
180
|
+
│
|
|
181
|
+
▼
|
|
182
|
+
assign persona + location
|
|
183
|
+
│
|
|
184
|
+
create profile + merge persona props
|
|
185
|
+
│
|
|
186
|
+
► HOOK: user — set computed segments / tiers
|
|
187
|
+
│
|
|
188
|
+
build active-day plan — if avgActiveDaysPerUser set
|
|
189
|
+
│
|
|
190
|
+
generate SCD entries
|
|
191
|
+
│
|
|
192
|
+
► HOOK: scd-pre — modify SCD mutation timeline
|
|
193
|
+
│
|
|
194
|
+
for each first funnel (attempts loop, identity stitching):
|
|
195
|
+
│
|
|
196
|
+
├─► HOOK: funnel-pre — change conversionRate, read meta.profile
|
|
197
|
+
│
|
|
198
|
+
├── generate funnel events — step1 anchored to FIXED_NOW
|
|
199
|
+
│
|
|
200
|
+
└─► HOOK: funnel-post — splice cloned events between steps
|
|
201
|
+
│
|
|
202
|
+
generate standalone events — active-day constrained
|
|
203
|
+
│
|
|
204
|
+
apply world-event props
|
|
205
|
+
apply data-quality nulls
|
|
206
|
+
│
|
|
207
|
+
► HOOK: event — per-event mutate (fires ONCE per event)
|
|
208
|
+
│
|
|
209
|
+
filter _drop events
|
|
210
|
+
apply engagementDecay
|
|
211
|
+
duplicate + late-arriving
|
|
212
|
+
│
|
|
213
|
+
sort by time
|
|
214
|
+
assign session_ids
|
|
215
|
+
per-session sticky device pick
|
|
216
|
+
│
|
|
217
|
+
touchpoint cap pass — UTM stamping, max maxTouchpointsPerUser
|
|
218
|
+
│
|
|
219
|
+
► HOOK: everything — see ALL events for user (most powerful)
|
|
220
|
+
│
|
|
221
|
+
auto-sort by time — opt out: autoSortAfterEverything: false
|
|
222
|
+
│
|
|
223
|
+
■ future-time guard — drop events past FIXED_NOW (unconditional)
|
|
224
|
+
│
|
|
225
|
+
push to storage — storage hooks fire here:
|
|
226
|
+
ad-spend / group / mirror / lookup
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
key points:
|
|
230
|
+
- **hook order matters.** `user` runs first, then per-funnel hooks, then per-event, then `everything` last. each hook can override what previous hooks did.
|
|
231
|
+
- **`event` hook fires ONCE per event.** the storage layer skips re-running it to prevent double-fire mutations (`price *= 2` won't apply twice).
|
|
232
|
+
- **`everything` is the most powerful hook.** sees the user's complete event history with `meta.profile` available. only place where you can drop events (return a filtered array).
|
|
233
|
+
- **future-time guard is unconditional.** any event with `time > FIXED_NOW` is dropped before storage. hook authors can clone events with arbitrary timestamps without polluting the dataset.
|
|
234
|
+
- **storage hooks** (`ad-spend`, `group`, `mirror`, `lookup`) fire during the storage push, not during userLoop. they're for transforming side-channel data only.
|
|
235
|
+
|
|
156
236
|
## the hook system
|
|
157
237
|
|
|
158
238
|
hooks are the most important feature. a hook is a single function on your dungeon config that receives every piece of data as it flows through the pipeline. you can mutate events, modify conversion rates, inject synthetic events, simulate churn, engineer temporal patterns, and correlate behaviors across tables.
|
|
@@ -503,13 +583,63 @@ styles: `support`, `review`, `search`, `feedback`, `chat`, `email`, `forum`, `co
|
|
|
503
583
|
## scripts
|
|
504
584
|
|
|
505
585
|
```bash
|
|
586
|
+
npm test # vitest test suite (~10s, 1122 tests)
|
|
587
|
+
npm run typecheck # typescript check
|
|
506
588
|
npm run dungeon:run # run a dungeon file locally
|
|
507
589
|
npm run dungeon:to-json # convert JS dungeon to JSON (for UI import)
|
|
508
590
|
npm run dungeon:from-json # convert JSON to JS dungeon
|
|
509
|
-
npm
|
|
510
|
-
npm run typecheck # typescript check
|
|
591
|
+
npm run dungeon:schema # extract schema from a dungeon
|
|
511
592
|
```
|
|
512
593
|
|
|
594
|
+
`./scripts/` ships with the npm package — direct-run utilities for dungeon authoring + verification:
|
|
595
|
+
|
|
596
|
+
```bash
|
|
597
|
+
node scripts/run-dungeon.mjs <path> # run a single dungeon
|
|
598
|
+
node scripts/run-many.mjs <dir> [--parallel N] # run multiple dungeons concurrently
|
|
599
|
+
node scripts/dungeon-to-json.mjs <path> # convert JS → JSON
|
|
600
|
+
node scripts/json-to-dungeon.mjs <path> # convert JSON → JS
|
|
601
|
+
node scripts/extract-dungeon-schema.mjs <path> # extract schema
|
|
602
|
+
node scripts/verify-runner.mjs <path> [prefix] # generate at full fidelity for hook verification
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## tests
|
|
606
|
+
|
|
607
|
+
vitest tests live under `tests/` in three tiers:
|
|
608
|
+
|
|
609
|
+
| dir | scope | wall time |
|
|
610
|
+
|---|---|---|
|
|
611
|
+
| `tests/unit/` | pure-function tests on helpers, validators, primitives — no `DUNGEON_MASTER()` calls | ~5s |
|
|
612
|
+
| `tests/integration/` | one generation pass per test, ≤300 users, in-memory output | ~50s |
|
|
613
|
+
| `tests/e2e/` | full pipeline — disk writes, file-path loading, multi-pass | ~50s |
|
|
614
|
+
|
|
615
|
+
run a single tier or file via `vitest` directly:
|
|
616
|
+
|
|
617
|
+
```bash
|
|
618
|
+
npx vitest run tests/unit # unit tier (~5s)
|
|
619
|
+
npx vitest run tests/integration # integration tier
|
|
620
|
+
npx vitest run tests/e2e # e2e tier
|
|
621
|
+
npx vitest run tests/unit tests/integration # fast inner loop
|
|
622
|
+
npx vitest run tests/integration/features.test.js # single file
|
|
623
|
+
npx vitest tests/unit # watch mode
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
`tests/e2e/sanity.test.js` is excluded by default (parked); run isolated with `npx vitest run tests/e2e/sanity.test.js`.
|
|
627
|
+
|
|
628
|
+
### engine tests (direct-run, NOT vitest)
|
|
629
|
+
|
|
630
|
+
`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).
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
node tests/engine/sweep-engine.mjs [--workers 4] [--tier short|normal|long|all]
|
|
634
|
+
# 194-combo strict-bar sweep on simplest.js
|
|
635
|
+
node tests/engine/sweep-bias.mjs # targeted bornRecentBias × born% exploration
|
|
636
|
+
node tests/engine/test-bunchiness.mjs <path> # chart inspector (last-14d / first-14d / spike)
|
|
637
|
+
node tests/engine/test-nosedive.mjs <path> # end-of-window nosedive check
|
|
638
|
+
node tests/engine/smoke-test-all.mjs [--dir] # tiny-scale generation across all dungeons (PASS/FAIL)
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
engine tests are NOT shipped in the npm package and NOT run as part of `npm test`. the vitest gate at `tests/e2e/engine-shape-full-sweep.test.js` wraps `sweep-engine.mjs` and runs only when `RUN_FULL_SWEEP=1` is set.
|
|
642
|
+
|
|
513
643
|
## config reference
|
|
514
644
|
|
|
515
645
|
see [types.d.ts](types.d.ts) for the complete `Dungeon` interface. here are the most commonly used properties:
|
|
@@ -519,7 +649,9 @@ see [types.d.ts](types.d.ts) for the complete `Dungeon` interface. here are the
|
|
|
519
649
|
| `numUsers` | number | 1000 | number of users to generate |
|
|
520
650
|
| `numEvents` | number | 100000 | target event count (legacy fallback; derived from `avgEventsPerUserPerDay` when set) |
|
|
521
651
|
| `avgEventsPerUserPerDay` | number | derived | per-user-per-day rate (canonical event-volume primitive) |
|
|
522
|
-
| `numDays` | number | 30 | days the dataset spans |
|
|
652
|
+
| `numDays` | number | 30 | days the dataset spans (safe range [14, 365]) |
|
|
653
|
+
| `datasetStart` | ISO/unix | undefined | pin window start (for bit-exact deterministic runs); requires `datasetEnd` too |
|
|
654
|
+
| `datasetEnd` | ISO/unix | undefined | pin window end; recomputes `numDays` from start/end span |
|
|
523
655
|
| `seed` | string | random | RNG seed for reproducibility |
|
|
524
656
|
| `format` | string | `'csv'` | output format (csv, json, parquet) |
|
|
525
657
|
| `token` | string | null | mixpanel project token (triggers import) |
|
|
@@ -532,9 +664,12 @@ see [types.d.ts](types.d.ts) for the complete `Dungeon` interface. here are the
|
|
|
532
664
|
| `concurrency` | number | 1 | parallel user generation |
|
|
533
665
|
| `macro` | string/object | `'flat'` | big-picture trend preset (flat/steady/growth/viral/decline) |
|
|
534
666
|
| `soup` | string/object | `'growth'` | intra-week / intra-day rhythm preset |
|
|
535
|
-
| `bornRecentBias` | number | 0 (from macro `flat`) | user birth date skew (-
|
|
536
|
-
| `percentUsersBornInDataset` | number |
|
|
667
|
+
| `bornRecentBias` | number | 0 (from macro `flat`) | user birth date skew (safe range [-0.5, 0.5]; user-explicit values outside the band are clamped) |
|
|
668
|
+
| `percentUsersBornInDataset` | number | 12 (from macro `flat`) | % of users born in window (clamped per-macro when both `macro` and this field are explicit) |
|
|
537
669
|
| `preExistingSpread` | string | `'uniform'` (from macro `flat`) | placement of pre-existing users' first event |
|
|
670
|
+
| `avgActiveDaysPerUser` | number | undefined | concentrate events onto N distinct UTC days per user (preserves total event count) |
|
|
671
|
+
| `maxTouchpointsPerUser` | number | 10 | UTM stamping cap per user (Mixpanel `TOUCHPOINTS_LIMIT` parity) |
|
|
672
|
+
| `autoSortAfterEverything` | boolean | true | sort events by time after `everything` hook (defends greedy funnel engine) |
|
|
538
673
|
| `hook` | function/string | passthrough | data transformation function |
|
|
539
674
|
| `hasLocation` | boolean | false | include geo properties |
|
|
540
675
|
| `hasCampaigns` | boolean | false | include UTM properties |
|
|
@@ -3,7 +3,7 @@ const SEED = "dm4-array-of-object-lookup";
|
|
|
3
3
|
const num_days = 60;
|
|
4
4
|
const num_users = 1_000;
|
|
5
5
|
const avg_events_per_user_per_day = 1.67;
|
|
6
|
-
let token = "
|
|
6
|
+
let token = "";
|
|
7
7
|
|
|
8
8
|
// ── env overrides ──
|
|
9
9
|
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* datagen-v1.5-verify.js — Canonical fixture exercising all v1.5 generation primitives.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - tests/active-days.test.js → asserts distinct-day distribution shape
|
|
6
|
+
* - tests/conversion-window.test.js → exercises Funnel.conversionWindowDays
|
|
7
|
+
* - tests/touchpoint-cap.test.js → exercises maxTouchpointsPerUser
|
|
8
|
+
* - tests/datagen-determinism.test.js → byte-equal verification across runs
|
|
9
|
+
* - tests/auto-sort.test.js → relies on sorted output
|
|
10
|
+
*
|
|
11
|
+
* Realistic small-scale config: 500 users × 30 days × 4 events/day. Includes
|
|
12
|
+
* `hasCampaigns: true` + an explicit `isAttributionEvent` flagged event so the
|
|
13
|
+
* touchpoint cap has eligible candidates beyond the default ~25% legacy fallback.
|
|
14
|
+
*
|
|
15
|
+
* **DO NOT enable `engagementDecay`** — it interacts with `avgActiveDaysPerUser`
|
|
16
|
+
* by dropping events on late picked days, eroding the effective active-day count
|
|
17
|
+
* below the configured target. See HOOKS.md §2.5.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as u from "../../lib/utils/utils.js";
|
|
21
|
+
|
|
22
|
+
const SEED = "datagen-v1.5-verify";
|
|
23
|
+
u.initChance(SEED);
|
|
24
|
+
|
|
25
|
+
/** @type {import('../../types').Dungeon} */
|
|
26
|
+
const config = {
|
|
27
|
+
seed: SEED,
|
|
28
|
+
// Pin the dataset window for full determinism (independent of run date).
|
|
29
|
+
datasetStart: "2025-09-01T00:00:00Z",
|
|
30
|
+
datasetEnd: "2025-10-01T00:00:00Z",
|
|
31
|
+
numUsers: 500,
|
|
32
|
+
avgEventsPerUserPerDay: 4,
|
|
33
|
+
// v1.5 distinct-day primitive — concentrate events onto ~6 days/user.
|
|
34
|
+
avgActiveDaysPerUser: 6,
|
|
35
|
+
// v1.5 attribution cap — explicit (default is also 10).
|
|
36
|
+
maxTouchpointsPerUser: 10,
|
|
37
|
+
hasCampaigns: true,
|
|
38
|
+
hasSessionIds: true,
|
|
39
|
+
avgDevicePerUser: 1,
|
|
40
|
+
autoSortAfterEverything: true,
|
|
41
|
+
events: [
|
|
42
|
+
{ event: "page view", weight: 5, isAttributionEvent: true },
|
|
43
|
+
{ event: "click", weight: 3 },
|
|
44
|
+
{
|
|
45
|
+
event: "sign up",
|
|
46
|
+
isFirstEvent: true,
|
|
47
|
+
isAuthEvent: true,
|
|
48
|
+
isAttributionEvent: true,
|
|
49
|
+
properties: { method: ["email", "google", "apple"] },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
event: "purchase",
|
|
53
|
+
weight: 1,
|
|
54
|
+
properties: { amount: u.weighNumRange(10, 200) },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
funnels: [
|
|
58
|
+
{
|
|
59
|
+
sequence: ["page view", "sign up", "purchase"],
|
|
60
|
+
isFirstFunnel: true,
|
|
61
|
+
conversionRate: 50,
|
|
62
|
+
timeToConvert: 4, // hours
|
|
63
|
+
// v1.5 explicit conversion window — well under the 30d default.
|
|
64
|
+
conversionWindowDays: 14,
|
|
65
|
+
order: "sequential",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
superProps: { Plan: ["Free", "Pro"] },
|
|
69
|
+
userProps: { Plan: ["Free", "Pro"] },
|
|
70
|
+
writeToDisk: false,
|
|
71
|
+
verbose: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default config;
|