@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.
- 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 +182 -0
- package/HOOKS.md +1256 -597
- package/README.md +140 -5
- package/dungeons/technical/ad-spend.js +41 -49
- package/dungeons/technical/anonymous-users.js +38 -36
- package/dungeons/technical/array-of-object-lookup.js +136 -153
- package/dungeons/technical/datagen-v15-verify.js +87 -0
- package/dungeons/technical/experiments.js +42 -40
- package/dungeons/technical/foobar.js +114 -118
- package/dungeons/technical/group-analytics.js +42 -40
- package/dungeons/technical/hook-helpers-verify.js +69 -50
- package/dungeons/technical/identity-model-verify.js +22 -12
- package/dungeons/technical/mirror-strategies.js +37 -39
- package/dungeons/technical/nested-objects.js +119 -118
- package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
- package/dungeons/technical/pattern-attributed-by-source.js +23 -9
- package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
- package/dungeons/technical/pattern-funnel-frequency.js +30 -15
- package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
- package/dungeons/technical/retention-cadence.js +115 -112
- package/dungeons/technical/sanity.js +86 -80
- package/dungeons/technical/scale-test.js +34 -38
- package/dungeons/technical/scd.js +111 -128
- package/dungeons/technical/simple.js +134 -141
- package/dungeons/technical/simplest.js +111 -65
- package/dungeons/technical/text-generation.js +110 -146
- package/dungeons/vertical/ai-platform.js +300 -333
- package/dungeons/vertical/community.js +290 -255
- package/dungeons/vertical/crypto.js +400 -391
- package/dungeons/vertical/dating.js +421 -375
- package/dungeons/vertical/devtools.js +346 -298
- package/dungeons/vertical/ecommerce.js +322 -394
- package/dungeons/vertical/education.js +380 -325
- package/dungeons/vertical/fintech.js +371 -325
- package/dungeons/vertical/fitness.js +345 -291
- package/dungeons/vertical/food-delivery.js +352 -307
- package/dungeons/vertical/gaming.js +490 -444
- package/dungeons/vertical/healthcare.js +311 -262
- package/dungeons/vertical/insurance-application.js +437 -409
- package/dungeons/vertical/logistics.js +278 -252
- package/dungeons/vertical/marketplace.js +340 -323
- package/dungeons/vertical/media.js +390 -335
- package/dungeons/vertical/real-estate.js +402 -347
- package/dungeons/vertical/sass.js +331 -333
- package/dungeons/vertical/social.js +377 -316
- package/dungeons/vertical/travel.js +302 -295
- package/index.js +64 -7
- package/lib/core/config-validator.js +378 -17
- package/lib/core/dungeon-loader.js +2 -5
- package/lib/generators/events.js +12 -13
- package/lib/generators/funnels.js +76 -2
- package/lib/hook-helpers/index.js +1 -0
- package/lib/hook-helpers/inject.js +95 -0
- package/lib/orchestrators/mixpanel-sender.js +7 -0
- package/lib/orchestrators/user-loop.js +598 -48
- package/lib/templates/defaults.js +59 -59
- package/lib/templates/macro-presets.js +53 -11
- package/lib/utils/dataset-context.js +103 -0
- package/lib/utils/retention-curve.js +140 -0
- package/lib/utils/utils.js +157 -109
- package/lib/verify/counting.js +360 -0
- package/lib/verify/emulate-breakdown.js +531 -108
- package/lib/verify/funnel-engine.js +539 -0
- package/lib/verify/identity.js +78 -0
- package/lib/verify/index.js +20 -0
- package/lib/verify/schema-validator.js +3 -1
- package/lib/verify/verify-dungeon.js +58 -0
- package/package.json +14 -3
- package/scripts/run-dungeon.mjs +12 -1
- package/types.d.ts +353 -4
- package/scripts/smoke-test-all.mjs +0 -162
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: analyze-soup
|
|
3
|
+
description: Use when investigating TimeSoup parameters, diagnosing event-distribution shape, or comparing soup configs — runs a dungeon locally and analyzes time distribution at week/day/hour/minute granularities, producing a soup-analysis.md diagnostic report.
|
|
4
|
+
argument-hint: [dungeon path, e.g. dungeons/soup-test.js]
|
|
5
|
+
model: claude-opus-4-6
|
|
6
|
+
effort: max
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Analyze TimeSoup Distribution
|
|
10
|
+
|
|
11
|
+
Run a dungeon and analyze the time distribution of generated events to evaluate TimeSoup parameters.
|
|
12
|
+
|
|
13
|
+
**Dungeon file:** `$ARGUMENTS` (default: `dungeons/soup-test.js`)
|
|
14
|
+
|
|
15
|
+
## Step 1: Run the Dungeon
|
|
16
|
+
|
|
17
|
+
Run the dungeon with forced local-only settings:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run prune
|
|
21
|
+
node -e "
|
|
22
|
+
import generate from './index.js';
|
|
23
|
+
import config from './$ARGUMENTS';
|
|
24
|
+
const result = await generate({ ...config, writeToDisk: true, format: 'json', token: '', verbose: true, name: 'soup-analysis' });
|
|
25
|
+
console.log('Events:', result.eventCount, 'Users:', result.userCount);
|
|
26
|
+
"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Wait for generation to complete. Note the event count and EPS.
|
|
30
|
+
|
|
31
|
+
## Step 2: Query with DuckDB
|
|
32
|
+
|
|
33
|
+
Run these DuckDB queries against the generated JSONL file. Use `duckdb` CLI.
|
|
34
|
+
|
|
35
|
+
### 2a. Week over Week
|
|
36
|
+
```bash
|
|
37
|
+
duckdb -c "
|
|
38
|
+
SELECT date_trunc('week', (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) as week,
|
|
39
|
+
count(*) as events
|
|
40
|
+
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
41
|
+
GROUP BY 1 ORDER BY 1;
|
|
42
|
+
"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2b. Day over Day
|
|
46
|
+
```bash
|
|
47
|
+
duckdb -c "
|
|
48
|
+
SELECT date_trunc('day', (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) as day,
|
|
49
|
+
count(*) as events
|
|
50
|
+
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
51
|
+
GROUP BY 1 ORDER BY 1;
|
|
52
|
+
"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2c. Hour over Hour (last 7 days only)
|
|
56
|
+
```bash
|
|
57
|
+
duckdb -c "
|
|
58
|
+
SELECT date_trunc('hour', (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) as hour,
|
|
59
|
+
count(*) as events
|
|
60
|
+
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
61
|
+
WHERE (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles') > (SELECT max((time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) - interval '7 days' FROM read_json_auto('./data/soup-analysis-EVENTS.json'))
|
|
62
|
+
GROUP BY 1 ORDER BY 1;
|
|
63
|
+
"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2d. Minute over Minute (last 24 hours only)
|
|
67
|
+
```bash
|
|
68
|
+
duckdb -c "
|
|
69
|
+
SELECT date_trunc('minute', (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) as minute,
|
|
70
|
+
count(*) as events
|
|
71
|
+
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
72
|
+
WHERE (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles') > (SELECT max((time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) - interval '1 day' FROM read_json_auto('./data/soup-analysis-EVENTS.json'))
|
|
73
|
+
GROUP BY 1 ORDER BY 1;
|
|
74
|
+
"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2e. Distribution Statistics
|
|
78
|
+
```bash
|
|
79
|
+
duckdb -c "
|
|
80
|
+
WITH daily AS (
|
|
81
|
+
SELECT date_trunc('day', (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) as day, count(*) as events
|
|
82
|
+
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
83
|
+
GROUP BY 1
|
|
84
|
+
),
|
|
85
|
+
hourly AS (
|
|
86
|
+
SELECT date_trunc('hour', (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) as hour, count(*) as events
|
|
87
|
+
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
88
|
+
WHERE (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles') > (SELECT max((time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) - interval '7 days' FROM read_json_auto('./data/soup-analysis-EVENTS.json'))
|
|
89
|
+
GROUP BY 1
|
|
90
|
+
)
|
|
91
|
+
SELECT 'daily' as granularity,
|
|
92
|
+
count(*) as buckets,
|
|
93
|
+
round(avg(events), 1) as avg_events,
|
|
94
|
+
min(events) as min_events,
|
|
95
|
+
max(events) as max_events,
|
|
96
|
+
round(max(events)::float / nullif(avg(events), 0), 2) as max_to_avg_ratio,
|
|
97
|
+
round(stddev(events) / nullif(avg(events), 0), 3) as cv
|
|
98
|
+
FROM daily
|
|
99
|
+
UNION ALL
|
|
100
|
+
SELECT 'hourly',
|
|
101
|
+
count(*),
|
|
102
|
+
round(avg(events), 1),
|
|
103
|
+
min(events),
|
|
104
|
+
max(events),
|
|
105
|
+
round(max(events)::float / nullif(avg(events), 0), 2),
|
|
106
|
+
round(stddev(events) / nullif(avg(events), 0), 3)
|
|
107
|
+
FROM hourly;
|
|
108
|
+
"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2f. Spike Detection
|
|
112
|
+
```bash
|
|
113
|
+
duckdb -c "
|
|
114
|
+
WITH daily AS (
|
|
115
|
+
SELECT date_trunc('day', (time::timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles')) as day, count(*) as events
|
|
116
|
+
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
117
|
+
GROUP BY 1
|
|
118
|
+
)
|
|
119
|
+
SELECT
|
|
120
|
+
'last_day_vs_avg' as check,
|
|
121
|
+
CASE WHEN last_events > avg_events * 2.0 THEN '❌ FAIL (>2x avg)'
|
|
122
|
+
WHEN last_events > avg_events * 1.5 THEN '⚠️ WARN (>1.5x avg)'
|
|
123
|
+
ELSE '✅ PASS' END as result,
|
|
124
|
+
last_events,
|
|
125
|
+
round(avg_events, 0) as avg_events,
|
|
126
|
+
round(last_events::float / avg_events, 2) as ratio
|
|
127
|
+
FROM (
|
|
128
|
+
SELECT
|
|
129
|
+
(SELECT events FROM daily ORDER BY day DESC LIMIT 1) as last_events,
|
|
130
|
+
(SELECT avg(events) FROM daily) as avg_events
|
|
131
|
+
);
|
|
132
|
+
"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Step 3: Write Report
|
|
136
|
+
|
|
137
|
+
Create `soup-analysis.md` in the project root with:
|
|
138
|
+
|
|
139
|
+
1. **Config**: The soup parameters used (peaks, deviation, mean, numDays)
|
|
140
|
+
2. **Summary stats**: Total events, event count, avg EPS
|
|
141
|
+
3. **Distribution tables**: Week/Day/Hour/Minute tables from Step 2
|
|
142
|
+
4. **Statistics**: CV, max-to-avg ratio at each granularity
|
|
143
|
+
5. **Spike detection**: Pass/fail for last day and last hour
|
|
144
|
+
6. **Assessment**: Overall quality judgment and recommendations
|
|
145
|
+
|
|
146
|
+
### Quality Criteria
|
|
147
|
+
- **Daily CV**: 0.2-0.6 is ideal (some variation, not flat or spiky)
|
|
148
|
+
- **Max-to-avg ratio**: < 2.0 at daily level, < 3.0 at hourly level
|
|
149
|
+
- **Last day spike**: < 1.5x average = PASS, < 2x = WARN, > 2x = FAIL
|
|
150
|
+
- **Hourly pattern**: Should show visible peaks but no single hour > 5x average
|
|
151
|
+
|
|
152
|
+
## Step 4: Cleanup
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm run prune
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Remove `soup-analysis.md` only if the user asks. It's meant to persist for comparison across runs.
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-dungeon
|
|
3
|
+
description: Use when authoring a new dungeon-master config from an app description — designs events, funnels, properties, identity model, and macro/soup. SCHEMA ONLY; engineered story trends + hooks are added separately by write-hooks.
|
|
4
|
+
argument-hint: [free-text app description, e.g. "AI meeting assistant" or "B2B logistics platform"]
|
|
5
|
+
model: claude-opus-4-6
|
|
6
|
+
effort: max
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Create a Dungeon (schema only)
|
|
10
|
+
|
|
11
|
+
Design and write a complete dungeon-master dungeon for: **$ARGUMENTS**
|
|
12
|
+
|
|
13
|
+
## Scope
|
|
14
|
+
|
|
15
|
+
This skill produces a **realistic baseline schema** that runs cleanly out of the
|
|
16
|
+
box. It does **NOT** engineer story trends or magic numbers — those are the
|
|
17
|
+
`write-hooks` skill's job. After this skill produces a dungeon, the next call
|
|
18
|
+
should be:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
/write-hooks dungeons/user/<your-dungeon>.js "describe the trends to engineer"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
In scope here:
|
|
25
|
+
- Event names, weights, properties (with realistic value distributions)
|
|
26
|
+
- `superProps` and `userProps` (consistent across users)
|
|
27
|
+
- Funnels with `sequence`, `conversionRate`, `timeToConvert`, `weight`,
|
|
28
|
+
`isFirstFunnel`, `attempts`
|
|
29
|
+
- Event flags: `isAuthEvent`, `isAttributionEvent`, `isFirstEvent`,
|
|
30
|
+
`isStrictEvent`, `isChurnEvent`, `isSessionStartEvent`
|
|
31
|
+
- Top-level: `datasetStart`, `datasetEnd`, `numUsers`, `avgEventsPerUserPerDay`,
|
|
32
|
+
`seed`, `format`, device flags, `avgDevicePerUser`, `hasLocation`,
|
|
33
|
+
`hasCampaigns`, `hasSessionIds`, `hasAvatar`, `macro`, `soup`
|
|
34
|
+
- Surviving advanced entities: `personas`, `worldEvents`, `engagementDecay`,
|
|
35
|
+
`dataQuality` — use sparingly
|
|
36
|
+
|
|
37
|
+
Out of scope (hand off to `write-hooks`):
|
|
38
|
+
- The `hook` function (default to no hook OR a tiny stub that stamps superProps
|
|
39
|
+
on injected events)
|
|
40
|
+
- Engineered patterns (magic numbers, A/B effects, time-bomb regressions, etc.)
|
|
41
|
+
|
|
42
|
+
For an encyclopedia of hook patterns, recipes, and real-world examples:
|
|
43
|
+
see `HOOKS.md` at the project root.
|
|
44
|
+
|
|
45
|
+
These config keys are silently ignored — DO NOT use them: `subscription`, `attribution`, `geo`, `features`, `anomalies`. Recreate with hooks via `write-hooks`.
|
|
46
|
+
|
|
47
|
+
## Reference reading
|
|
48
|
+
|
|
49
|
+
Before writing any code, scan:
|
|
50
|
+
|
|
51
|
+
- `types.d.ts` — the complete API reference. Every Dungeon field, EventConfig
|
|
52
|
+
flag, Funnel option, AttemptsConfig, and Hook meta interface is documented
|
|
53
|
+
with full JSDoc. **Treat this as the source of truth.**
|
|
54
|
+
- `lib/utils/utils.js` — `pickAWinner`, `weighNumRange`, `initChance`, `exhaust`,
|
|
55
|
+
`takeSome` for property value distributions
|
|
56
|
+
- `dungeons/vertical/sass.js` — B2B reference dungeon with full identity model
|
|
57
|
+
- `dungeons/user/my-buddy.js` — consumer-app reference (gitignored)
|
|
58
|
+
- `dungeons/technical/identity-model-verify.js` — minimal identity-model fixture
|
|
59
|
+
|
|
60
|
+
## File structure
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// ── TWEAK THESE ──
|
|
64
|
+
const SEED = "dm4-VERTICAL";
|
|
65
|
+
const num_days = 120;
|
|
66
|
+
const num_users = 5_000;
|
|
67
|
+
const avg_events_per_user_per_day = 1.2;
|
|
68
|
+
let token = "your-mixpanel-token";
|
|
69
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
70
|
+
|
|
71
|
+
import dayjs from "dayjs";
|
|
72
|
+
import "dotenv/config";
|
|
73
|
+
import * as u from "../../lib/utils/utils.js";
|
|
74
|
+
import * as v from "ak-tools";
|
|
75
|
+
|
|
76
|
+
const chance = u.initChance(SEED);
|
|
77
|
+
|
|
78
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
79
|
+
|
|
80
|
+
// Generate consistent IDs at module level
|
|
81
|
+
const productIds = v.range(1, 200).map(n => `prod_${v.uid(8)}`);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* ═══════════════════════════════════════════════════════════════
|
|
85
|
+
* DATASET OVERVIEW
|
|
86
|
+
* ═══════════════════════════════════════════════════════════════
|
|
87
|
+
*
|
|
88
|
+
* App Name — what it models, the core user loop, monetization.
|
|
89
|
+
* - N users over M days, ~X events
|
|
90
|
+
* - Key entities and relationships
|
|
91
|
+
* - Why these events/properties were chosen
|
|
92
|
+
*
|
|
93
|
+
* NO STORY TRENDS YET — schema only. Pass to /write-hooks for engineering.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/** @type {Config} */
|
|
97
|
+
const config = {
|
|
98
|
+
token, seed: SEED,
|
|
99
|
+
numDays: num_days,
|
|
100
|
+
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
101
|
+
numUsers: num_users,
|
|
102
|
+
|
|
103
|
+
// Identity model — see "Identity guidelines" below
|
|
104
|
+
hasAnonIds: true,
|
|
105
|
+
avgDevicePerUser: 2,
|
|
106
|
+
hasSessionIds: true,
|
|
107
|
+
|
|
108
|
+
// I/O
|
|
109
|
+
format: "json", gzip: true, writeToDisk: false, concurrency: 1,
|
|
110
|
+
|
|
111
|
+
// Realistic platform
|
|
112
|
+
hasLocation: true, hasAndroidDevices: false, hasIOSDevices: false,
|
|
113
|
+
hasDesktopDevices: true, hasBrowser: true, hasAvatar: true,
|
|
114
|
+
|
|
115
|
+
funnels: [ /* see "Funnels" below */ ],
|
|
116
|
+
events: [ /* see "Events" below */ ],
|
|
117
|
+
superProps: { /* see "SuperProps" below */ },
|
|
118
|
+
userProps: { /* see "UserProps" below */ },
|
|
119
|
+
scdProps: { /* see "SCDs" below */ },
|
|
120
|
+
groupKeys: [ /* see "Groups" below */ ],
|
|
121
|
+
|
|
122
|
+
// No hook — schema only.
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default config;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Required components
|
|
129
|
+
|
|
130
|
+
### 1. Events (~15–20)
|
|
131
|
+
|
|
132
|
+
15–20 distinct event types covering the app's core loop. Each event:
|
|
133
|
+
|
|
134
|
+
- `event` — name in **lowercase with spaces** (Mixpanel convention)
|
|
135
|
+
- `weight` — relative frequency 1–10 (clamped)
|
|
136
|
+
- `properties` — flat property map. Values can be arrays (random pick) or
|
|
137
|
+
utility calls like `u.weighNumRange(1, 100, 0.5, 20)`
|
|
138
|
+
|
|
139
|
+
**Event flags** (see `types.d.ts` `EventConfig`):
|
|
140
|
+
|
|
141
|
+
- `isFirstEvent: true` — the user's first-ever event (e.g., "sign up")
|
|
142
|
+
- `isAuthEvent: true` — marks the identity stitch moment. See "Identity
|
|
143
|
+
guidelines" below. Multiple events may carry it; the engine looks at the
|
|
144
|
+
first occurrence in the user's stream when stamping inside an `isFirstFunnel`.
|
|
145
|
+
- `isAttributionEvent: true` — when `hasCampaigns: true`, only flagged events
|
|
146
|
+
get UTMs (~25% of them). Without flags, ~25% of all events get UTMs (legacy).
|
|
147
|
+
- `isStrictEvent: true` — exclude from auto-generated catch-all funnels. Use
|
|
148
|
+
for funnel-only events (Sign Up, Onboarding Question) so they don't bleed
|
|
149
|
+
into the standalone weighted picker.
|
|
150
|
+
- `isChurnEvent: true` + `returnLikelihood` — fire-and-stop semantics
|
|
151
|
+
- `isSessionStartEvent: true` — auto-prepended 15s before each funnel sequence
|
|
152
|
+
|
|
153
|
+
When using experiments, include `$experiment_started` in the events array with
|
|
154
|
+
`isStrictEvent: true` so the engine schema includes its properties:
|
|
155
|
+
```js
|
|
156
|
+
{ event: "$experiment_started", weight: 1, isStrictEvent: true, properties: {
|
|
157
|
+
"Experiment name": ["My Experiment"],
|
|
158
|
+
"Variant name": ["Control", "Variant A", "Variant B"],
|
|
159
|
+
}}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 2. Funnels (3–6)
|
|
163
|
+
|
|
164
|
+
- First funnel: includes `isFirstEvent` AND has `isAuthEvent: true` on the
|
|
165
|
+
identity-transition step.
|
|
166
|
+
- Usage funnels: ordinary sequences without `isFirstFunnel`. Optionally use
|
|
167
|
+
`attempts` for repeat-usage modeling (abandon-cart pattern).
|
|
168
|
+
- Pick `conversionRate` between 30 and 80; `timeToConvert` in hours.
|
|
169
|
+
|
|
170
|
+
Funnel `props` stamp constant properties on all events in that funnel run.
|
|
171
|
+
Use for funnel-level context (checkout flow variant, onboarding version):
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
{
|
|
175
|
+
sequence: ["View Item", "Add to Cart", "Checkout"],
|
|
176
|
+
conversionRate: 40,
|
|
177
|
+
timeToConvert: 2,
|
|
178
|
+
conversionWindowDays: 7, // cap how late "Checkout" can fire
|
|
179
|
+
props: {
|
|
180
|
+
checkout_version: ["v1", "v2"], // random per funnel run
|
|
181
|
+
payment_method: ["card", "paypal"],
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**`conversionWindowDays`:** explicit Mixpanel-style conversion window
|
|
187
|
+
cap, in days. Default 30 (Mixpanel UI default). Hard cap 180 (Mixpanel max).
|
|
188
|
+
The validator auto-bumps to `min(180, ceil(timeToConvert/24 * 1.5))` if your
|
|
189
|
+
`timeToConvert` exceeds 30 days. Set explicitly to silence the warning. The
|
|
190
|
+
verifier (`verifyDungeon`) reads this field automatically.
|
|
191
|
+
|
|
192
|
+
**`isStrictEvent` auto-promote:** if you list a funnel-step event in
|
|
193
|
+
`events[]`, the validator auto-sets `isStrictEvent: true` for you and warns.
|
|
194
|
+
This heals the silent-corruption footgun where the greedy funnel engine
|
|
195
|
+
consumed standalone instances as funnel matches. Set `isStrictEvent: false`
|
|
196
|
+
explicitly only when you intend mixed funnel/standalone semantics for that
|
|
197
|
+
event.
|
|
198
|
+
|
|
199
|
+
**Mark hook-readable funnel-step events with `isStrictEvent: false`.** When a hook reads `event === 'X'` and `X` is also a funnel step, the validator's auto-promote turns it into `isStrictEvent: true` and the engine stops emitting standalone occurrences — cohort goes empty. Identify these candidates at schema time so `write-hooks` doesn't have to re-thread the schema. Common candidates: login, page view, search, add to cart, swap, deposit — anything that's both a funnel step AND a recurring user behavior hooks will likely cohort on.
|
|
200
|
+
|
|
201
|
+
**Funnels representing loops need `reentry: true`.** Any funnel named "X loop" / "X cycle" / "session" / per-instance recurring behavior must declare `reentry: true`. Without it, the engine emits one sequence per user and downstream "power user" / "daily active" cohorts have no behavioral signal to bin on.
|
|
202
|
+
|
|
203
|
+
### 3. SuperProps (2–3)
|
|
204
|
+
|
|
205
|
+
Properties present on EVERY event. Common picks: `Plan`, `Region`, `Platform`,
|
|
206
|
+
`App Version`. Values must come from same enumerations used in `userProps` for
|
|
207
|
+
consistency.
|
|
208
|
+
|
|
209
|
+
### 4. UserProps (4–8)
|
|
210
|
+
|
|
211
|
+
User profile properties. Set once per user. Use enumerations whose values
|
|
212
|
+
match `superProps` for any overlapping keys (so per-event Region matches per-user
|
|
213
|
+
Region).
|
|
214
|
+
|
|
215
|
+
### 5. Groups (0–2)
|
|
216
|
+
|
|
217
|
+
Use `groupKeys: [["company_id", 250]]` for B2B SaaS. Add `groupProps` for
|
|
218
|
+
group attributes. Skip for B2C apps.
|
|
219
|
+
|
|
220
|
+
### 6. SCDs (0–2)
|
|
221
|
+
|
|
222
|
+
Slowly-changing dimensions for plan tier, role, etc. JSDoc on `SCDProp` covers
|
|
223
|
+
type/frequency/timing/values/max.
|
|
224
|
+
|
|
225
|
+
### 7. Hook function — DO NOT WRITE
|
|
226
|
+
|
|
227
|
+
Skip the `hook:` field entirely (engine defaults to pass-through). The
|
|
228
|
+
`write-hooks` skill picks this up and engineers the trends.
|
|
229
|
+
|
|
230
|
+
If you absolutely need a stub for downstream stamping consistency, leave a
|
|
231
|
+
1-liner that returns the record unchanged.
|
|
232
|
+
|
|
233
|
+
## Identity guidelines
|
|
234
|
+
|
|
235
|
+
The identity model has three knobs:
|
|
236
|
+
|
|
237
|
+
### `avgDevicePerUser` (whole number, default 0)
|
|
238
|
+
|
|
239
|
+
| App type | Recommended | Why |
|
|
240
|
+
|----------|-------------|-----|
|
|
241
|
+
| B2C consumer app, web/mobile single device | 1 | Sticky single device; ratio of user_id to device_id is 1:1 |
|
|
242
|
+
| B2B SaaS engineer / knowledge worker | 2 | Laptop + work-from-home laptop; per-session sticky pick |
|
|
243
|
+
| Multi-device-heavy product (streaming, fitness) | 2–3 | TV + phone + tablet sessions distinguishable |
|
|
244
|
+
| Server / API-only product | 0 | No client device concept |
|
|
245
|
+
|
|
246
|
+
`hasAnonIds: true` aliases to `avgDevicePerUser: 1`. Set both for clarity.
|
|
247
|
+
|
|
248
|
+
### `isAuthEvent` placement
|
|
249
|
+
|
|
250
|
+
Flag the event that represents "user becomes identified". Put it in the
|
|
251
|
+
`isFirstFunnel` sequence. Common picks:
|
|
252
|
+
|
|
253
|
+
- Consumer apps: `Sign Up`, `Login`
|
|
254
|
+
- B2B SaaS: `workspace created`, `account created`
|
|
255
|
+
- Marketplaces: `Account Activated`
|
|
256
|
+
|
|
257
|
+
The engine stamps user_id+device_id on this event; pre-auth funnel steps get
|
|
258
|
+
device_id only; post-auth funnel steps get user_id only.
|
|
259
|
+
|
|
260
|
+
### `attempts` (per-funnel, optional)
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
{
|
|
264
|
+
sequence: ["Land", "Onboarding Question", "Sign Up"],
|
|
265
|
+
isFirstFunnel: true,
|
|
266
|
+
conversionRate: 70,
|
|
267
|
+
attempts: { min: 0, max: 2 }, // 0–2 failed priors → 1–3 total passes
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Typical ranges:
|
|
272
|
+
|
|
273
|
+
- Direct-acquisition first funnels: `{ min: 0, max: 0 }` (single attempt) or omit
|
|
274
|
+
- Shared-link / friction-heavy onboarding: `{ min: 0, max: 2 }` (some retry)
|
|
275
|
+
- Re-engagement / abandon-cart usage funnels: `{ min: 0, max: 3 }`
|
|
276
|
+
|
|
277
|
+
When set, `attempts.conversionRate` (optional) overrides `funnel.conversionRate`
|
|
278
|
+
on the FINAL attempt. Failed prior attempts truncate before the first
|
|
279
|
+
`isAuthEvent` step (no stitch fires for those attempts).
|
|
280
|
+
|
|
281
|
+
### Experiments (per-funnel, optional)
|
|
282
|
+
|
|
283
|
+
Funnels can run A/B/C experiments with `experiment: true` (3 default variants) or
|
|
284
|
+
a rich `ExperimentConfig`:
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
{
|
|
288
|
+
sequence: ["Create Agenda", "Agenda Generated"],
|
|
289
|
+
conversionRate: 60,
|
|
290
|
+
timeToConvert: 0.5,
|
|
291
|
+
name: "Collaborative Agenda",
|
|
292
|
+
experiment: {
|
|
293
|
+
name: "Collaborative Agenda",
|
|
294
|
+
variants: [
|
|
295
|
+
{ name: "Control (No Collab)" },
|
|
296
|
+
{ name: "Variant A (Ask User)", conversionMultiplier: 1.15, ttcMultiplier: 0.9 },
|
|
297
|
+
{ name: "Variant B (Assume + Confirm)", conversionMultiplier: 1.35, ttcMultiplier: 0.7 },
|
|
298
|
+
],
|
|
299
|
+
startDaysBeforeEnd: 30,
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Key fields (see `ExperimentConfig` in `types.d.ts`):
|
|
305
|
+
- `variants[]` — custom names, conversion/TTC multipliers, distribution weights
|
|
306
|
+
- `startDaysBeforeEnd` — temporal gating (experiment activates N days before dataset end)
|
|
307
|
+
- Variant assignment is **deterministic per user** (hash-based, not random per run)
|
|
308
|
+
- Engine injects `$experiment_started` with "Experiment name" and "Variant name" properties
|
|
309
|
+
- Add `$experiment_started` to the events array with `isStrictEvent: true` so the schema includes it
|
|
310
|
+
|
|
311
|
+
Variant-specific downstream effects (e.g., "Variant B boosts downstream event X") go in the `write-hooks` skill via `funnel-post` hooks that check `meta.experiment.variantName`.
|
|
312
|
+
|
|
313
|
+
### World Events (optional)
|
|
314
|
+
|
|
315
|
+
Shared temporal events affecting all users. Good for modeling outages, campaigns,
|
|
316
|
+
or launches that create visible inflection points:
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
worldEvents: [
|
|
320
|
+
{
|
|
321
|
+
name: "Black Friday Sale",
|
|
322
|
+
startDay: 55,
|
|
323
|
+
duration: 3,
|
|
324
|
+
affectsEvents: ["Purchase", "Add to Cart"],
|
|
325
|
+
volumeMultiplier: 2.5,
|
|
326
|
+
conversionModifier: 1.3,
|
|
327
|
+
injectProps: { promo_active: true },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "API Outage",
|
|
331
|
+
startDay: 30,
|
|
332
|
+
duration: 1,
|
|
333
|
+
affectsEvents: "*",
|
|
334
|
+
volumeMultiplier: 0.3,
|
|
335
|
+
},
|
|
336
|
+
]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
World events stamp `injectProps` on matching events and modulate volume via
|
|
340
|
+
accept/reject sampling. `conversionModifier` affects funnel conversion rates.
|
|
341
|
+
See `types.d.ts` `ResolvedWorldEvent` for the full interface.
|
|
342
|
+
|
|
343
|
+
## Trend shape — `macro` and `soup`
|
|
344
|
+
|
|
345
|
+
Default to NOT setting either. Defaults: `macro: "flat"` (preset born=12,
|
|
346
|
+
bias=0, uniform pre-existing spread) + `soup: "growth"` (standard intra-week /
|
|
347
|
+
intra-day rhythm). Only override when you have a specific reason:
|
|
348
|
+
|
|
349
|
+
- Use `macro: "growth"` if you want a mild acquisition trend (visible births
|
|
350
|
+
over the window, 30% born-in-dataset).
|
|
351
|
+
- Use `macro: "viral"` only if the app has a hockey-stick acquisition story.
|
|
352
|
+
Pair with `personas` so the late entrants behave differently.
|
|
353
|
+
- Use `macro: "decline"` for sunsetting products. Pair with `engagementDecay`
|
|
354
|
+
or a churn cohort to get a real downtrend (the macro alone produces a flat
|
|
355
|
+
shape — see `lib/templates/macro-presets.js` decline JSDoc).
|
|
356
|
+
- Use `soup: "spiky"` for products with dramatic peaks / valleys (gaming
|
|
357
|
+
weekends, financial market hours).
|
|
358
|
+
- Use `soup: "global"` to flatten all DOW/HOD weights (24/7 server-side products).
|
|
359
|
+
|
|
360
|
+
### Macro × born% / bias compatibility (strict clamps)
|
|
361
|
+
|
|
362
|
+
When you set `macro` AND `percentUsersBornInDataset` explicitly, the validator
|
|
363
|
+
clamps born% to the macro's preset value: flat=12, steady=12, growth=30,
|
|
364
|
+
viral=55, decline=5. Same for `bornRecentBias` outside `[-0.5, 0.5]`. If you
|
|
365
|
+
need higher born% (e.g., "this app launched mid-window — every user is in the
|
|
366
|
+
dataset"), switch macros first (flat → growth → viral) instead of pushing the
|
|
367
|
+
preset's value. Setting born% without a macro keeps legacy behavior (no clamp).
|
|
368
|
+
|
|
369
|
+
The clamp warning explains why and points to safe alternatives — read it.
|
|
370
|
+
|
|
371
|
+
⚠️ **Don't set `percentUsersBornInDataset > 60`** with macro=flat / steady /
|
|
372
|
+
decline. Cumulative-acquisition right-edge explosion is the inevitable result
|
|
373
|
+
on a no-hook dungeon. The strict clamps will rescue the run, but the chart
|
|
374
|
+
won't look like what you asked for.
|
|
375
|
+
|
|
376
|
+
See [CLAUDE.md "Engine guarantees"](../../../CLAUDE.md#engine-guarantees) for the full safe-range table.
|
|
377
|
+
|
|
378
|
+
### `avgActiveDaysPerUser` (concentrator)
|
|
379
|
+
|
|
380
|
+
Top-level optional knob. Sets the mean number of distinct UTC days each user
|
|
381
|
+
fires events on. Total event count is preserved (still `rate × numDays`),
|
|
382
|
+
but events cluster onto fewer days. Per-user count drawn from
|
|
383
|
+
`normal(mean=N, sd=N/3)`, clamped to `[1, userActiveDays]`.
|
|
384
|
+
|
|
385
|
+
Default: undefined (legacy — events spread across the whole window via TimeSoup).
|
|
386
|
+
|
|
387
|
+
**Concentrator semantic — per-active-day rate inflates.** Example:
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
avgEventsPerUserPerDay: 4
|
|
391
|
+
avgActiveDaysPerUser: 2
|
|
392
|
+
numDays: 30
|
|
393
|
+
→ 120 events per user, concentrated onto 2 days = 60 events/active day
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The validator warns when the implied per-active-day rate > 50. To reduce
|
|
397
|
+
total events, lower `avgEventsPerUserPerDay`, NOT this knob.
|
|
398
|
+
|
|
399
|
+
**Incompatibility with `engagementDecay`:** these two erosive primitives
|
|
400
|
+
combine destructively — decay drops events from late picked days, eroding
|
|
401
|
+
the effective active-day count below the configured target. Use one or the
|
|
402
|
+
other in a dungeon. If you need both, set `avgActiveDaysPerUser` and write
|
|
403
|
+
the decay as an `everything` hook scoped to specific cohorts (the `write-hooks`
|
|
404
|
+
skill handles this).
|
|
405
|
+
|
|
406
|
+
### `maxTouchpointsPerUser` (attribution cap)
|
|
407
|
+
|
|
408
|
+
Top-level optional knob. Caps UTM stamping at this many events per user
|
|
409
|
+
(default 10, matching Mixpanel `TOUCHPOINTS_LIMIT`). When `hasCampaigns: true`
|
|
410
|
+
and a user has more eligible events than the cap, the engine takes a
|
|
411
|
+
uniform-random sample across the user's lifetime and stamps UTMs on the
|
|
412
|
+
sampled events only. Sampling across lifetime (NOT first-N) preserves
|
|
413
|
+
realistic touch shape — Mixpanel's last-10-window then gives meaningful
|
|
414
|
+
first/last-touch attribution. Set to `Infinity` to disable the cap.
|
|
415
|
+
|
|
416
|
+
## SuperProp consistency rule
|
|
417
|
+
|
|
418
|
+
If `superProps` and `userProps` both define a property like `Plan`, the
|
|
419
|
+
enumeration must match exactly. Otherwise users with `userProps.Plan = 'pro'`
|
|
420
|
+
will fire events with `superProps.Plan = 'free'` — Mixpanel will see broken
|
|
421
|
+
breakdowns.
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
const PLANS = ["Free", "Free", "Free", "Pro", "Pro", "Enterprise"];
|
|
425
|
+
// ...
|
|
426
|
+
superProps: { Plan: PLANS, Region: REGIONS },
|
|
427
|
+
userProps: { Plan: PLANS, Region: REGIONS, Role: ROLES, ... },
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Verification
|
|
431
|
+
|
|
432
|
+
After writing the file:
|
|
433
|
+
|
|
434
|
+
1. Smoke-test: `node scripts/verify-runner.mjs dungeons/user/<file>.js verify-<file> --small`. Confirm zero errors.
|
|
435
|
+
2. Hand to the next skill: `/write-hooks dungeons/user/<file>.js "describe trends"`.
|
|
436
|
+
3. After hooks land: `/verify-dungeon dungeons/user/<file>.js`.
|
|
437
|
+
|
|
438
|
+
## Property Type Reference
|
|
439
|
+
|
|
440
|
+
Use the correct helper for each Mixpanel property data type. All helpers are imported from `@ak--47/dungeon-master/utils` (already available as `u` in dungeon files).
|
|
441
|
+
|
|
442
|
+
| Mixpanel Type | Helper | Example |
|
|
443
|
+
|---|---|---|
|
|
444
|
+
| **String** | Array of options | `["Basic", "Pro", "Enterprise"]` |
|
|
445
|
+
| **Numeric** | `weighNumRange()` or array | `u.weighNumRange(1, 100)` or `[10, 20, 50, 100]` |
|
|
446
|
+
| **Boolean** | Boolean array | `[true, false, false]` (weighted 33/67) |
|
|
447
|
+
| **Date** | `dateRange()` | `dateRange()` (dataset window) or `dateRange('2023-01-01', '2024-01-01')` |
|
|
448
|
+
| **List** | `listOf()` | `listOf(["tag1", "tag2", "tag3"], {min: 1, max: 3})` |
|
|
449
|
+
| **Object** | Plain object | `{tier: "premium", seats: 5}` |
|
|
450
|
+
| **List of Objects** | `objectList()` | `objectList({sku: u.weighNumRange(1000,9999), qty: [1,2,3]}, {min:1, max:4})` |
|
|
451
|
+
|
|
452
|
+
When designing event properties, always consider which Mixpanel type best represents the data:
|
|
453
|
+
- Tags, genres, interests → `listOf()`
|
|
454
|
+
- Cart items, line items, participants → `objectList()`
|
|
455
|
+
- Subscription start, trial end, next billing → `dateRange()`
|
|
456
|
+
- Status, tier, category → string array
|
|
457
|
+
|
|
458
|
+
## Output
|
|
459
|
+
|
|
460
|
+
Write the file to `dungeons/user/<descriptive-name>.js`. Do NOT inject hooks.
|
|
461
|
+
Do NOT use `subscription`, `attribution`, `geo`, `features`, or `anomalies`
|
|
462
|
+
(the engine will silently strip them and warn).
|
|
463
|
+
|
|
464
|
+
When done, tell the user the next skill to run.
|