@ak--47/dungeon-master 1.4.5 → 1.5.1

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 (78) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +182 -0
  9. package/HOOKS.md +1256 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +41 -49
  12. package/dungeons/technical/anonymous-users.js +38 -36
  13. package/dungeons/technical/array-of-object-lookup.js +136 -153
  14. package/dungeons/technical/datagen-v15-verify.js +87 -0
  15. package/dungeons/technical/experiments.js +42 -40
  16. package/dungeons/technical/foobar.js +114 -118
  17. package/dungeons/technical/group-analytics.js +42 -40
  18. package/dungeons/technical/hook-helpers-verify.js +69 -50
  19. package/dungeons/technical/identity-model-verify.js +22 -12
  20. package/dungeons/technical/mirror-strategies.js +37 -39
  21. package/dungeons/technical/nested-objects.js +119 -118
  22. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  23. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  24. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  25. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  26. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  27. package/dungeons/technical/retention-cadence.js +115 -112
  28. package/dungeons/technical/sanity.js +86 -80
  29. package/dungeons/technical/scale-test.js +34 -38
  30. package/dungeons/technical/scd.js +111 -128
  31. package/dungeons/technical/simple.js +134 -141
  32. package/dungeons/technical/simplest.js +111 -65
  33. package/dungeons/technical/text-generation.js +110 -146
  34. package/dungeons/vertical/ai-platform.js +300 -333
  35. package/dungeons/vertical/community.js +290 -255
  36. package/dungeons/vertical/crypto.js +400 -391
  37. package/dungeons/vertical/dating.js +421 -375
  38. package/dungeons/vertical/devtools.js +346 -298
  39. package/dungeons/vertical/ecommerce.js +322 -394
  40. package/dungeons/vertical/education.js +380 -325
  41. package/dungeons/vertical/fintech.js +371 -325
  42. package/dungeons/vertical/fitness.js +345 -291
  43. package/dungeons/vertical/food-delivery.js +352 -307
  44. package/dungeons/vertical/gaming.js +490 -444
  45. package/dungeons/vertical/healthcare.js +311 -262
  46. package/dungeons/vertical/insurance-application.js +437 -409
  47. package/dungeons/vertical/logistics.js +278 -252
  48. package/dungeons/vertical/marketplace.js +340 -323
  49. package/dungeons/vertical/media.js +390 -335
  50. package/dungeons/vertical/real-estate.js +402 -347
  51. package/dungeons/vertical/sass.js +331 -333
  52. package/dungeons/vertical/social.js +377 -316
  53. package/dungeons/vertical/travel.js +302 -295
  54. package/index.js +64 -7
  55. package/lib/core/config-validator.js +378 -17
  56. package/lib/core/dungeon-loader.js +2 -5
  57. package/lib/generators/events.js +12 -13
  58. package/lib/generators/funnels.js +76 -2
  59. package/lib/hook-helpers/index.js +1 -0
  60. package/lib/hook-helpers/inject.js +95 -0
  61. package/lib/orchestrators/mixpanel-sender.js +7 -0
  62. package/lib/orchestrators/user-loop.js +598 -48
  63. package/lib/templates/defaults.js +59 -59
  64. package/lib/templates/macro-presets.js +53 -11
  65. package/lib/utils/dataset-context.js +103 -0
  66. package/lib/utils/retention-curve.js +140 -0
  67. package/lib/utils/utils.js +157 -109
  68. package/lib/verify/counting.js +360 -0
  69. package/lib/verify/emulate-breakdown.js +531 -108
  70. package/lib/verify/funnel-engine.js +539 -0
  71. package/lib/verify/identity.js +78 -0
  72. package/lib/verify/index.js +20 -0
  73. package/lib/verify/schema-validator.js +3 -1
  74. package/lib/verify/verify-dungeon.js +58 -0
  75. package/package.json +14 -3
  76. package/scripts/run-dungeon.mjs +12 -1
  77. package/types.d.ts +353 -4
  78. 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 test # vitest test suite
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 (-1..1) |
536
- | `percentUsersBornInDataset` | number | 15 (from macro `flat`) | % of users born in time window |
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 |
@@ -1,59 +1,51 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "ad spend test";
3
- const num_days = 90;
4
- const num_users = 1_000;
5
- const avg_events_per_user_per_day = 0.56;
6
- let token = "your-mixpanel-token";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
11
- import Chance from 'chance';
12
- let chance = new Chance();
13
- import dayjs from "dayjs";
14
- import utc from "dayjs/plugin/utc.js";
15
- dayjs.extend(utc);
16
- import { uid, comma } from 'ak-tools';
17
- import { weighNumRange, date, integer, weighChoices } from "../../lib/utils/utils.js";
18
-
1
+ // ── IMPORTS ──
2
+ import { weighNumRange } from "../../lib/utils/utils.js";
19
3
  /** @typedef {import("../../types").Dungeon} Config */
20
- /**
21
- * ═══════════════════════════════════════════════════════════════
22
- * TECHNICAL TEST: Ad Spend & Campaign Attribution
23
- * ═══════════════════════════════════════════════════════════════
24
- *
25
- * Tests Mixpanel ad spend data generation and campaign attribution.
26
- * - 1,000 users, 50K events, 90 days
27
- * - hasAdSpend: true — generates cost, CPC, CTR, impressions, clicks
28
- * - hasCampaigns: true — adds UTM parameters to events
29
- * - hasBrowser: true — required for campaign tracking
30
- * - 6 events: signup, page view, purchase, ad click, search, share
31
- *
32
- * No hooks. Focus is on verifying ad spend event structure
33
- * and campaign attribution properties flow through correctly.
4
+
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: ad-spend
8
+ * PURPOSE: exercises hasAdSpend + hasCampaigns — generates cost/CPC/CTR/impressions/clicks + UTM attribution
9
+ * SCALE: 1,000 users, ~50K events, 90 days
10
+ * EVENTS (6): sign up, page view, purchase, ad click, search, share
11
+ * FUNNELS (0): none
34
12
  */
35
13
 
36
- /** @type {import('../../types').Dungeon} */
14
+ // ── SCALE ──
15
+ const SEED = "ad spend test";
16
+ const NUM_DAYS = 90;
17
+ const NUM_USERS = 1_000;
18
+ const EVENTS_PER_DAY = 0.56;
19
+ const token = process.env.MP_TOKEN || "";
20
+
21
+ // ── CONFIG ──
22
+ /** @type {Config} */
37
23
  const config = {
38
- token,
39
24
  seed: SEED,
40
25
  name: "ad-spend",
41
- numDays: num_days,
42
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
43
- numUsers: num_users,
26
+ numDays: NUM_DAYS,
27
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
28
+ numUsers: NUM_USERS,
44
29
  format: 'json',
45
- region: "US",
46
- hasAnonIds: true,
47
- hasSessionIds: true,
48
- hasAdSpend: true,
49
- hasLocation: false,
50
- hasAndroidDevices: false,
51
- hasIOSDevices: false,
52
- hasDesktopDevices: true,
53
- hasBrowser: true,
54
- hasCampaigns: true,
55
- isAnonymous: false,
56
- alsoInferFunnels: false,
30
+ credentials: {
31
+ token,
32
+ region: "US",
33
+ },
34
+ switches: {
35
+ hasSessionIds: true,
36
+ hasAdSpend: true,
37
+ hasLocation: false,
38
+ hasAndroidDevices: false,
39
+ hasIOSDevices: false,
40
+ hasDesktopDevices: true,
41
+ hasBrowser: true,
42
+ hasCampaigns: true,
43
+ isAnonymous: false,
44
+ alsoInferFunnels: false,
45
+ },
46
+ identity: {
47
+ avgDevicePerUser: 1,
48
+ },
57
49
  concurrency: 1,
58
50
  writeToDisk: false,
59
51
 
@@ -1,50 +1,52 @@
1
- // ── TWEAK THESE ──
1
+ // ── IMPORTS ──
2
+ import Chance from 'chance';
3
+ let chance = new Chance();
4
+ import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
5
+ /** @typedef {import("../../types").Dungeon} Config */
6
+
7
+ // ── OVERVIEW ──
8
+ /*
9
+ * NAME: anonymous-users
10
+ * PURPOSE: Anonymous-user mode fixture — exercises isAnonymous + hasAnonIds + anon-to-identified flow
11
+ * SCALE: 1,000 users, ~50K events, 180 days
12
+ * EVENTS (5): page view, signup, feature used, purchase, button click
13
+ * FUNNELS (0): none
14
+ */
15
+
16
+ // ── SCALE ──
2
17
  const SEED = "anonymous-users";
3
18
  const num_days = 180;
4
19
  const num_users = 1_000;
5
20
  const avg_events_per_user_per_day = 0.28;
6
- let token = "your-mixpanel-token";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
11
- /**
12
- * Anonymous Users — tests anonymous user mode, anonIds, and identity merge.
13
- *
14
- * Exercises: isAnonymous mode, hasAnonIds for anonymous-to-identified
15
- * user flows, long 180-day timespan for tail behavior.
16
- *
17
- * - 1000 users, 50K events, 180 days
18
- * - isAnonymous: true, hasAnonIds: true
19
- * - Simple events with signup as isFirstEvent (identity resolution point)
20
- * - No hooks, minimal config
21
- */
22
-
23
- import Chance from 'chance';
24
- let chance = new Chance();
25
- import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
21
+ const token = process.env.MP_TOKEN || "";
26
22
 
27
- /** @typedef {import("../../types").Dungeon} Config */
28
- /** @type {import('../../types').Dungeon} */
23
+ // ── CONFIG ──
24
+ /** @type {Config} */
29
25
  const config = {
30
- token,
31
26
  seed: SEED,
32
27
  numDays: num_days,
33
28
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
34
29
  numUsers: num_users,
35
30
  format: "json",
36
- region: "US",
37
- isAnonymous: true,
38
- hasAnonIds: true,
39
- hasSessionIds: true,
40
- hasAdSpend: false,
41
- hasLocation: false,
42
- hasAndroidDevices: true,
43
- hasIOSDevices: true,
44
- hasDesktopDevices: true,
45
- hasBrowser: true,
46
- hasCampaigns: false,
47
- alsoInferFunnels: false,
31
+ credentials: {
32
+ token,
33
+ region: "US",
34
+ },
35
+ switches: {
36
+ isAnonymous: true,
37
+ hasSessionIds: true,
38
+ hasAdSpend: false,
39
+ hasLocation: false,
40
+ hasAndroidDevices: true,
41
+ hasIOSDevices: true,
42
+ hasDesktopDevices: true,
43
+ hasBrowser: true,
44
+ hasCampaigns: false,
45
+ alsoInferFunnels: false,
46
+ },
47
+ identity: {
48
+ avgDevicePerUser: 1,
49
+ },
48
50
  concurrency: 1,
49
51
  writeToDisk: false,
50
52