@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. 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.0",
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.2.9",
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 at constrained params for hook verification.
2
+ * Verify Runner — runs a dungeon for hook verification.
3
3
  *
4
- * Usage: node scripts/verify-runner.mjs <dungeon-path> [run-name]
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
- * Example: node scripts/verify-runner.mjs dungeons/streaming.js verify-streaming
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 dungeonPath = process.argv[2];
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 = process.argv[3] || 'verify-hooks';
32
+ const runName = positional[1] || 'verify-hooks';
33
+ const isSmall = flags.has('--small');
18
34
  const absolutePath = path.isAbsolute(dungeonPath)
19
- ? dungeonPath
20
- : path.resolve(process.cwd(), dungeonPath);
35
+ ? dungeonPath
36
+ : path.resolve(process.cwd(), dungeonPath);
21
37
 
22
38
  const { default: config } = await import(absolutePath);
23
39
 
24
- const results = await generate({
25
- ...config,
26
- token: "",
27
- numUsers: 1000,
28
- numEvents: 100_000,
29
- format: "json",
30
- gzip: false,
31
- writeToDisk: true,
32
- name: runName,
33
- concurrency: 1,
34
- verbose: false,
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
- eventCount: results.eventCount,
39
- userCount: results.userCount,
40
- files: results.files,
41
- duration: results.time?.human || results.time?.delta + 'ms'
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
  }));