@ak--47/dungeon-master 1.3.0 → 1.4.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/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ak--47/dungeon-master",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "generate fancy datasets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"types": "./types.d.ts"
|
|
12
12
|
},
|
|
13
13
|
"./utils": "./lib/utils/utils.js",
|
|
14
|
-
"./text": "./lib/generators/text.js"
|
|
14
|
+
"./text": "./lib/generators/text.js",
|
|
15
|
+
"./hook-helpers": "./lib/hook-helpers/index.js",
|
|
16
|
+
"./hook-patterns": "./lib/hook-patterns/index.js",
|
|
17
|
+
"./verify": "./lib/verify/index.js"
|
|
15
18
|
},
|
|
16
19
|
"files": [
|
|
17
20
|
"index.js",
|
|
@@ -21,7 +24,6 @@
|
|
|
21
24
|
"!dungeons/customers/",
|
|
22
25
|
"!dungeons/user/",
|
|
23
26
|
"scripts/",
|
|
24
|
-
"!scripts/experiments/",
|
|
25
27
|
"package.json",
|
|
26
28
|
"README.md",
|
|
27
29
|
"CHANGELOG.md"
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
"dotenv": "^16.4.5",
|
|
73
75
|
"hyparquet-writer": "^0.6.1",
|
|
74
76
|
"mixpanel": "^0.18.0",
|
|
75
|
-
"mixpanel-import": "^3.
|
|
77
|
+
"mixpanel-import": "^3.3.1",
|
|
76
78
|
"p-limit": "^3.1.0",
|
|
77
79
|
"pino": "^9.0.0",
|
|
78
80
|
"pino-pretty": "^11.0.0",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Smoke-test runner — runs every dungeon at tiny scale in parallel to verify it
|
|
4
|
+
* loads, generates events, and writes output without crashing.
|
|
5
|
+
*
|
|
6
|
+
* Default scale: 100 users, 1000 events per dungeon. NOT for verification —
|
|
7
|
+
* use scripts/verify-runner.mjs at full fidelity for that.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/smoke-test-all.mjs
|
|
11
|
+
* node scripts/smoke-test-all.mjs --dir dungeons/vertical # default
|
|
12
|
+
* node scripts/smoke-test-all.mjs --dir dungeons/technical
|
|
13
|
+
* node scripts/smoke-test-all.mjs --dir dungeons # both subdirs
|
|
14
|
+
* node scripts/smoke-test-all.mjs --concurrency 4 # default: cpu count
|
|
15
|
+
* node scripts/smoke-test-all.mjs --users 500 --events 5000 # override scale
|
|
16
|
+
*
|
|
17
|
+
* Output: per-dungeon PASS/FAIL line + a final summary table. Spawns each
|
|
18
|
+
* dungeon as a child node process so a crash in one doesn't abort the run.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from 'child_process';
|
|
21
|
+
import { readdirSync, statSync, rmSync, existsSync, mkdirSync } from 'fs';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import os from 'os';
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
function arg(name, fallback) {
|
|
27
|
+
const i = args.indexOf(`--${name}`);
|
|
28
|
+
return i === -1 ? fallback : args[i + 1];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
32
|
+
const dirArg = arg('dir', 'dungeons/vertical');
|
|
33
|
+
const numUsers = parseInt(arg('users', '100'), 10);
|
|
34
|
+
const numEvents = parseInt(arg('events', '1000'), 10);
|
|
35
|
+
const concurrency = parseInt(arg('concurrency', String(Math.max(2, os.cpus().length))), 10);
|
|
36
|
+
const keep = args.includes('--keep');
|
|
37
|
+
|
|
38
|
+
function findDungeons(dir) {
|
|
39
|
+
const abs = path.resolve(ROOT, dir);
|
|
40
|
+
if (!existsSync(abs)) return [];
|
|
41
|
+
const stat = statSync(abs);
|
|
42
|
+
if (stat.isFile()) return [abs];
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const entry of readdirSync(abs)) {
|
|
45
|
+
const full = path.join(abs, entry);
|
|
46
|
+
const s = statSync(full);
|
|
47
|
+
if (s.isDirectory()) out.push(...findDungeons(full));
|
|
48
|
+
else if (entry.endsWith('.js')) out.push(full);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dungeons = findDungeons(dirArg).sort();
|
|
54
|
+
if (dungeons.length === 0) {
|
|
55
|
+
console.error(`No dungeons found under ${dirArg}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dataDir = path.join(ROOT, 'data');
|
|
60
|
+
mkdirSync(dataDir, { recursive: true });
|
|
61
|
+
|
|
62
|
+
const RUNNER = `
|
|
63
|
+
import generate from '${path.join(ROOT, 'index.js')}';
|
|
64
|
+
// With node -e, process.argv = [nodePath, ...userArgs] — no script slot.
|
|
65
|
+
const [dungeonPath, name, numUsers, numEvents] = process.argv.slice(1);
|
|
66
|
+
const r = await generate(dungeonPath, {
|
|
67
|
+
numUsers: parseInt(numUsers, 10),
|
|
68
|
+
numEvents: parseInt(numEvents, 10),
|
|
69
|
+
avgEventsPerUserPerDay: undefined, // force numEvents path
|
|
70
|
+
writeToDisk: true,
|
|
71
|
+
name,
|
|
72
|
+
format: 'json',
|
|
73
|
+
verbose: false,
|
|
74
|
+
concurrency: 1,
|
|
75
|
+
token: '',
|
|
76
|
+
serviceAccount: 'fake', serviceSecret: 'fake', projectId: '1',
|
|
77
|
+
});
|
|
78
|
+
console.log(JSON.stringify({ eventCount: r.eventCount, userCount: r.userCount }));
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
function runOne(dungeonPath) {
|
|
82
|
+
const base = path.basename(dungeonPath, '.js');
|
|
83
|
+
const namePrefix = `smoke-${base}`;
|
|
84
|
+
return new Promise(resolve => {
|
|
85
|
+
const start = Date.now();
|
|
86
|
+
const child = spawn(
|
|
87
|
+
process.execPath,
|
|
88
|
+
['--input-type=module', '-e', RUNNER, dungeonPath, namePrefix, String(numUsers), String(numEvents)],
|
|
89
|
+
{ cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
90
|
+
);
|
|
91
|
+
let out = '';
|
|
92
|
+
let err = '';
|
|
93
|
+
child.stdout.on('data', d => { out += d.toString(); });
|
|
94
|
+
child.stderr.on('data', d => { err += d.toString(); });
|
|
95
|
+
child.on('close', code => {
|
|
96
|
+
const ms = Date.now() - start;
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
const lastLine = out.trim().split('\n').filter(Boolean).pop() || '{}';
|
|
100
|
+
result = JSON.parse(lastLine);
|
|
101
|
+
} catch {
|
|
102
|
+
result = {};
|
|
103
|
+
}
|
|
104
|
+
const ok = code === 0 && result.eventCount > 0;
|
|
105
|
+
if (!keep) {
|
|
106
|
+
try {
|
|
107
|
+
for (const f of readdirSync(dataDir)) {
|
|
108
|
+
if (f.startsWith(`${namePrefix}-`)) rmSync(path.join(dataDir, f));
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
resolve({
|
|
113
|
+
dungeon: path.relative(ROOT, dungeonPath),
|
|
114
|
+
ok,
|
|
115
|
+
code,
|
|
116
|
+
ms,
|
|
117
|
+
eventCount: result.eventCount || 0,
|
|
118
|
+
userCount: result.userCount || 0,
|
|
119
|
+
err: ok ? '' : (err.trim().split('\n').slice(-3).join(' | ') || `exit ${code}`),
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`Smoke test: ${dungeons.length} dungeons, ${numUsers} users / ${numEvents} events each, concurrency=${concurrency}`);
|
|
126
|
+
const queue = [...dungeons];
|
|
127
|
+
const results = [];
|
|
128
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
129
|
+
while (queue.length) {
|
|
130
|
+
const d = queue.shift();
|
|
131
|
+
const res = await runOne(d);
|
|
132
|
+
const tag = res.ok ? 'PASS' : 'FAIL';
|
|
133
|
+
console.log(` [${tag}] ${path.basename(res.dungeon)} — ${res.eventCount} events, ${res.userCount} users, ${(res.ms / 1000).toFixed(2)}s${res.ok ? '' : ` — ${res.err}`}`);
|
|
134
|
+
results.push(res);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
await Promise.all(workers);
|
|
138
|
+
|
|
139
|
+
results.sort((a, b) => a.dungeon.localeCompare(b.dungeon));
|
|
140
|
+
const fails = results.filter(r => !r.ok);
|
|
141
|
+
|
|
142
|
+
console.log('\n┌─────────────────────────────────────────────────┬───────┬────────┬───────┬──────┐');
|
|
143
|
+
console.log('│ Dungeon │ State │ Events │ Users │ ms │');
|
|
144
|
+
console.log('├─────────────────────────────────────────────────┼───────┼────────┼───────┼──────┤');
|
|
145
|
+
for (const r of results) {
|
|
146
|
+
const name = r.dungeon.padEnd(47).slice(0, 47);
|
|
147
|
+
const state = (r.ok ? 'PASS' : 'FAIL').padEnd(5);
|
|
148
|
+
const ev = String(r.eventCount).padStart(6);
|
|
149
|
+
const u = String(r.userCount).padStart(5);
|
|
150
|
+
const ms = String(r.ms).padStart(4);
|
|
151
|
+
console.log(`│ ${name} │ ${state} │ ${ev} │ ${u} │ ${ms} │`);
|
|
152
|
+
}
|
|
153
|
+
console.log('└─────────────────────────────────────────────────┴───────┴────────┴───────┴──────┘');
|
|
154
|
+
|
|
155
|
+
console.log(`\n${results.length - fails.length}/${results.length} passed`);
|
|
156
|
+
if (fails.length > 0) {
|
|
157
|
+
console.log('\nFailures:');
|
|
158
|
+
for (const f of fails) {
|
|
159
|
+
console.log(` ✗ ${f.dungeon}: ${f.err}`);
|
|
160
|
+
}
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
@@ -1,42 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Verify Runner — runs a dungeon
|
|
2
|
+
* Verify Runner — runs a dungeon for hook verification.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Default mode: FULL FIDELITY — runs the dungeon with its own configured
|
|
5
|
+
* numUsers / avgEventsPerUserPerDay / numDays. This is the only way to
|
|
6
|
+
* verify the trends a benchmark consumer will actually see.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
8
|
+
* --small mode: ~1K users with avgEventsPerUserPerDay scaled proportionally
|
|
9
|
+
* so total events stay near 100K. Use only for fast smoke checks; do NOT
|
|
10
|
+
* ship verification verdicts based on --small runs.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/verify-runner.mjs <dungeon-path> [run-name] [--small]
|
|
14
|
+
*
|
|
15
|
+
* Examples:
|
|
16
|
+
* node scripts/verify-runner.mjs dungeons/vertical/gaming.js verify-gaming
|
|
17
|
+
* node scripts/verify-runner.mjs dungeons/vertical/gaming.js verify-gaming --small
|
|
7
18
|
*/
|
|
8
19
|
import generate from '../index.js';
|
|
9
20
|
import path from 'path';
|
|
10
21
|
|
|
11
|
-
const
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const flags = new Set(args.filter(a => a.startsWith('--')));
|
|
24
|
+
const positional = args.filter(a => !a.startsWith('--'));
|
|
25
|
+
|
|
26
|
+
const dungeonPath = positional[0];
|
|
12
27
|
if (!dungeonPath) {
|
|
13
|
-
console.error('Usage: node scripts/verify-runner.mjs <dungeon-path> [run-name]');
|
|
28
|
+
console.error('Usage: node scripts/verify-runner.mjs <dungeon-path> [run-name] [--small]');
|
|
14
29
|
process.exit(1);
|
|
15
30
|
}
|
|
16
31
|
|
|
17
|
-
const runName =
|
|
32
|
+
const runName = positional[1] || 'verify-hooks';
|
|
33
|
+
const isSmall = flags.has('--small');
|
|
18
34
|
const absolutePath = path.isAbsolute(dungeonPath)
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
? dungeonPath
|
|
36
|
+
: path.resolve(process.cwd(), dungeonPath);
|
|
21
37
|
|
|
22
38
|
const { default: config } = await import(absolutePath);
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
let override;
|
|
41
|
+
|
|
42
|
+
if (isSmall) {
|
|
43
|
+
// SMALL mode: 1K users, scaled per-day rate so total events ≈ 100K.
|
|
44
|
+
// Preserves the dungeon's per-user-per-day shape while shrinking the dataset.
|
|
45
|
+
// Use only for quick smoke checks — verdicts must come from full fidelity.
|
|
46
|
+
const numUsers = 1000;
|
|
47
|
+
const numDays = config.numDays || 100;
|
|
48
|
+
const targetTotal = 100_000;
|
|
49
|
+
const scaledRate = targetTotal / (numUsers * numDays);
|
|
50
|
+
override = {
|
|
51
|
+
...config,
|
|
52
|
+
token: '',
|
|
53
|
+
numUsers,
|
|
54
|
+
numDays,
|
|
55
|
+
avgEventsPerUserPerDay: scaledRate,
|
|
56
|
+
numEvents: undefined,
|
|
57
|
+
format: 'json',
|
|
58
|
+
gzip: false,
|
|
59
|
+
writeToDisk: true,
|
|
60
|
+
name: runName,
|
|
61
|
+
concurrency: 1,
|
|
62
|
+
verbose: false,
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
// FULL FIDELITY: use the dungeon's own scale settings as-shipped.
|
|
66
|
+
// Full-scale runs can take minutes for 50K-user dungeons; that's expected.
|
|
67
|
+
override = {
|
|
68
|
+
...config,
|
|
69
|
+
token: '',
|
|
70
|
+
format: 'json',
|
|
71
|
+
gzip: false,
|
|
72
|
+
writeToDisk: true,
|
|
73
|
+
name: runName,
|
|
74
|
+
concurrency: config.concurrency || 1,
|
|
75
|
+
verbose: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const t0 = Date.now();
|
|
80
|
+
const results = await generate(override);
|
|
81
|
+
const wallMs = Date.now() - t0;
|
|
36
82
|
|
|
37
83
|
console.log(JSON.stringify({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
84
|
+
mode: isSmall ? 'small' : 'full',
|
|
85
|
+
eventCount: results.eventCount,
|
|
86
|
+
userCount: results.userCount,
|
|
87
|
+
files: results.files,
|
|
88
|
+
duration: results.time?.human || `${wallMs}ms`,
|
|
89
|
+
wallMs,
|
|
42
90
|
}));
|