@ak--47/dungeon-master 1.5.4 → 1.6.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 (146) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -12
  2. package/.claude/skills/create-dungeon/SKILL.md +65 -5
  3. package/.claude/skills/create-project/SKILL.md +1 -1
  4. package/.claude/skills/create-project/provision.mjs +57 -10
  5. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  6. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  7. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  8. package/.claude/skills/write-hooks/SKILL.md +156 -18
  9. package/CHANGELOG.md +183 -0
  10. package/HOOKS.md +566 -112
  11. package/README.md +28 -9
  12. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  13. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  14. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  15. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  16. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  17. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  18. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  19. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  20. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  21. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  22. package/dungeons/technical/stories-verify.js +152 -0
  23. package/dungeons/vertical/README.md +154 -0
  24. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  25. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  26. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  27. package/dungeons/vertical/community/community.js +1589 -0
  28. package/dungeons/vertical/community/community.sql +216 -0
  29. package/dungeons/vertical/community/community.verify.mjs +72 -0
  30. package/dungeons/vertical/crypto/crypto.js +1483 -0
  31. package/dungeons/vertical/crypto/crypto.sql +194 -0
  32. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  33. package/dungeons/vertical/dating/dating.js +1540 -0
  34. package/dungeons/vertical/dating/dating.sql +288 -0
  35. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  36. package/dungeons/vertical/devtools/devtools.js +1524 -0
  37. package/dungeons/vertical/devtools/devtools.sql +161 -0
  38. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  39. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  40. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  41. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  42. package/dungeons/vertical/education/education.js +1763 -0
  43. package/dungeons/vertical/education/education.sql +243 -0
  44. package/dungeons/vertical/education/education.verify.mjs +75 -0
  45. package/dungeons/vertical/fintech/fintech.js +1410 -0
  46. package/dungeons/vertical/fintech/fintech.sql +175 -0
  47. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  48. package/dungeons/vertical/fitness/fitness.js +1495 -0
  49. package/dungeons/vertical/fitness/fitness.sql +206 -0
  50. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  51. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  52. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  53. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  54. package/dungeons/vertical/gaming/gaming.js +1741 -0
  55. package/dungeons/vertical/gaming/gaming.sql +249 -0
  56. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  57. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  58. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  59. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  60. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  61. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  62. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  63. package/dungeons/vertical/logistics/logistics.js +1541 -0
  64. package/dungeons/vertical/logistics/logistics.sql +205 -0
  65. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  66. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  67. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  68. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  69. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  70. package/dungeons/vertical/media/media.sql +173 -0
  71. package/dungeons/vertical/media/media.verify.mjs +74 -0
  72. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  73. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  74. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  75. package/dungeons/vertical/sass/sass.js +1733 -0
  76. package/dungeons/vertical/sass/sass.sql +173 -0
  77. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  78. package/dungeons/vertical/social/social.js +1456 -0
  79. package/dungeons/vertical/social/social.sql +160 -0
  80. package/dungeons/vertical/social/social.verify.mjs +76 -0
  81. package/dungeons/vertical/streaming/streaming.js +677 -0
  82. package/dungeons/vertical/streaming/streaming.sql +101 -0
  83. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  84. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  85. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  86. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  87. package/dungeons/vertical/travel/travel.js +1603 -0
  88. package/dungeons/vertical/travel/travel.sql +236 -0
  89. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  90. package/index.js +2 -2
  91. package/lib/core/config-validator.js +37 -2
  92. package/lib/core/context.js +4 -2
  93. package/lib/core/dungeon-loader.js +20 -15
  94. package/lib/core/dungeon-to-json.js +22 -4
  95. package/lib/core/storage.js +3 -3
  96. package/lib/generators/funnels.js +36 -9
  97. package/lib/hook-helpers/cohort.js +53 -0
  98. package/lib/hook-helpers/index.js +8 -0
  99. package/lib/hook-helpers/mutate.js +1 -1
  100. package/lib/hook-helpers/shape.js +319 -0
  101. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  102. package/lib/hook-patterns/attributed-by-source.js +60 -58
  103. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  104. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  105. package/lib/hook-patterns/index.js +1 -1
  106. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  107. package/lib/orchestrators/user-loop.js +41 -0
  108. package/lib/templates/story-spec.schema.json +175 -0
  109. package/lib/utils/json-evaluator.js +1 -1
  110. package/lib/utils/utils.js +5 -4
  111. package/lib/verify/coerce.js +186 -0
  112. package/lib/verify/counting.js +270 -32
  113. package/lib/verify/emulate-breakdown.js +1285 -203
  114. package/lib/verify/first-time.js +89 -0
  115. package/lib/verify/flows.js +679 -0
  116. package/lib/verify/formula.js +259 -0
  117. package/lib/verify/funnel-engine.js +697 -113
  118. package/lib/verify/index.js +32 -4
  119. package/lib/verify/sessionize.js +275 -0
  120. package/lib/verify/story-runner.js +483 -0
  121. package/lib/verify/verify-dungeon.js +59 -37
  122. package/package.json +2 -1
  123. package/scripts/extract-dungeon-schema.mjs +2 -2
  124. package/scripts/run-many.mjs +2 -2
  125. package/scripts/verify-runner.mjs +2 -2
  126. package/scripts/verify-stories.mjs +244 -0
  127. package/types.d.ts +296 -30
  128. package/dungeons/vertical/ai-platform.js +0 -818
  129. package/dungeons/vertical/community.js +0 -966
  130. package/dungeons/vertical/crypto.js +0 -895
  131. package/dungeons/vertical/dating.js +0 -830
  132. package/dungeons/vertical/devtools.js +0 -1050
  133. package/dungeons/vertical/ecommerce.js +0 -811
  134. package/dungeons/vertical/education.js +0 -964
  135. package/dungeons/vertical/fintech.js +0 -946
  136. package/dungeons/vertical/fitness.js +0 -935
  137. package/dungeons/vertical/food-delivery.js +0 -934
  138. package/dungeons/vertical/gaming.js +0 -1120
  139. package/dungeons/vertical/healthcare.js +0 -945
  140. package/dungeons/vertical/insurance-application.js +0 -1039
  141. package/dungeons/vertical/logistics.js +0 -986
  142. package/dungeons/vertical/marketplace.js +0 -1019
  143. package/dungeons/vertical/real-estate.js +0 -872
  144. package/dungeons/vertical/sass.js +0 -995
  145. package/dungeons/vertical/social.js +0 -904
  146. package/dungeons/vertical/travel.js +0 -930
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verify-stories — evaluate a dungeon's `stories` named export against its
4
+ * generated data and print a five-tier verdict table (v1.6, P3.3).
5
+ *
6
+ * Stories are JS-dungeon-only: `stories` must be a NAMED export of a .js/.mjs
7
+ * dungeon file (JSON dungeons cannot carry `assert` functions or comment-block
8
+ * hook stories). The dungeon is loaded with dynamic import(), not the
9
+ * dungeon-loader.
10
+ *
11
+ * Modes:
12
+ * disk (default) — streams already-generated shards from
13
+ * ./data/<prefix>-EVENTS*.json and ./data/<prefix>-USERS*.json
14
+ * (generate first: node scripts/verify-runner.mjs <dungeon-path> <prefix>).
15
+ * duckdb assertions shell out to the `duckdb` CLI with {{PREFIX}}
16
+ * substituted by the data prefix path.
17
+ * --in-memory — runs the dungeon fresh at its configured scale via
18
+ * verifyDungeon(config, storiesToChecks(stories)). duckdb assertions are
19
+ * disk-mode-only and are skipped with a warning.
20
+ *
21
+ * Coverage discipline: every numbered hook in the dungeon's HOOK STORIES
22
+ * comment block (lines mentioning `H<n>` / `Hook <n>`) must be targeted by at
23
+ * least one story. Missing hooks fail the run with a coverage report.
24
+ *
25
+ * Exit code: non-zero when any story lands WEAK / NONE / INVERSE, when
26
+ * coverage is incomplete, or (in-memory) when the schema report fails.
27
+ *
28
+ * Usage:
29
+ * node scripts/verify-stories.mjs <dungeon-path> [--data-prefix <prefix>] [--in-memory] [--json]
30
+ */
31
+ import fs from 'fs';
32
+ import path from 'path';
33
+ import readline from 'readline';
34
+ import { execFile } from 'child_process';
35
+ import { promisify } from 'util';
36
+ import { pathToFileURL } from 'url';
37
+ import { extractComments } from '../lib/core/extract-comments.js';
38
+ import { validateDungeonConfig } from '../lib/core/config-validator.js';
39
+ import {
40
+ buildIdentityMap,
41
+ verifyDungeon,
42
+ VERDICT_RANK,
43
+ validateStories,
44
+ storiesToChecks,
45
+ evaluateStories,
46
+ } from '../lib/verify/index.js';
47
+
48
+ const USAGE = `Usage: node scripts/verify-stories.mjs <dungeon-path> [--data-prefix <prefix>] [--in-memory] [--json]
49
+
50
+ <dungeon-path> .js/.mjs dungeon with a \`stories\` named export (JS-only —
51
+ JSON dungeons cannot carry stories).
52
+ --data-prefix <prefix> shard prefix under ./data (default: verify-<dungeon-name>).
53
+ Disk mode reads ./data/<prefix>-EVENTS*.json + -USERS*.json.
54
+ --in-memory run the dungeon fresh via verifyDungeon instead of reading
55
+ shards. duckdb assertions are skipped (disk-mode-only).
56
+ --json print machine-readable JSON instead of the verdict table.`;
57
+
58
+ // ── args ────────────────────────────────────────────────────────────────────
59
+
60
+ const argv = process.argv.slice(2);
61
+ let dungeonPath = null, dataPrefix = null, inMemory = false, asJson = false;
62
+ for (let i = 0; i < argv.length; i++) {
63
+ const a = argv[i];
64
+ if (a === '--help' || a === '-h') { console.log(USAGE); process.exit(0); }
65
+ else if (a === '--in-memory') inMemory = true;
66
+ else if (a === '--json') asJson = true;
67
+ else if (a === '--data-prefix') {
68
+ dataPrefix = argv[++i];
69
+ if (!dataPrefix || dataPrefix.startsWith('--')) die('--data-prefix requires a value');
70
+ }
71
+ else if (a.startsWith('--')) die(`unknown flag ${a}`);
72
+ else if (!dungeonPath) dungeonPath = a;
73
+ else die(`unexpected argument ${a}`);
74
+ }
75
+ if (!dungeonPath) die(USAGE);
76
+
77
+ function die(msg) {
78
+ console.error(msg);
79
+ process.exit(1);
80
+ }
81
+
82
+ // ── load dungeon + stories ──────────────────────────────────────────────────
83
+
84
+ const abs = path.isAbsolute(dungeonPath) ? dungeonPath : path.resolve(process.cwd(), dungeonPath);
85
+ if (!/\.(js|mjs)$/.test(abs)) {
86
+ die(`verify-stories: "${dungeonPath}" is not a .js/.mjs dungeon — stories are JS-dungeon-only (a JSON dungeon cannot carry a \`stories\` export).`);
87
+ }
88
+ if (!fs.existsSync(abs)) die(`verify-stories: dungeon not found at ${abs}`);
89
+
90
+ const mod = await import(pathToFileURL(abs).href);
91
+ const config = mod.default;
92
+ const stories = mod.stories;
93
+ if (!config || typeof config !== 'object') die(`verify-stories: ${dungeonPath} has no default-exported config object`);
94
+ if (!Array.isArray(stories) || !stories.length) {
95
+ die(`verify-stories: ${dungeonPath} has no \`stories\` named export — add one (see lib/templates/story-spec.schema.json) or use scripts/verify-runner.mjs for ad-hoc checks.`);
96
+ }
97
+ const sv = validateStories(stories);
98
+ if (!sv.valid) die(`verify-stories: invalid stories:\n ${sv.errors.join('\n ')}`);
99
+
100
+ // ── coverage discipline ─────────────────────────────────────────────────────
101
+ // Every numbered hook in the HOOK STORIES comment block needs >=1 story. One
102
+ // id per line (first match): hook-story blocks lead each entry with its label.
103
+
104
+ const HOOK_LINE_RE = /\b(?:H|Hook\s*)(\d+)\b/i;
105
+ const STORY_HOOK_RE = /^(?:H|Hook\s*)?(\d+)$/i;
106
+ const declared = new Set();
107
+ const comments = extractComments(abs);
108
+ const hookStoriesText = Array.isArray(comments) ? null : comments.hookStories;
109
+ if (hookStoriesText) {
110
+ for (const line of hookStoriesText.split('\n')) {
111
+ const m = line.match(HOOK_LINE_RE);
112
+ if (m) declared.add(Number(m[1]));
113
+ }
114
+ }
115
+ const covered = new Set();
116
+ for (const s of stories) {
117
+ const m = String(s.hook || '').trim().match(STORY_HOOK_RE);
118
+ if (m) covered.add(Number(m[1]));
119
+ }
120
+ const missing = [...declared].filter(n => !covered.has(n)).sort((a, b) => a - b);
121
+ const coverage = {
122
+ declared: [...declared].sort((a, b) => a - b),
123
+ covered: [...covered].sort((a, b) => a - b),
124
+ missing,
125
+ note: hookStoriesText ? undefined : 'no HOOK STORIES comment block found — coverage check skipped',
126
+ };
127
+
128
+ // ── evaluate ────────────────────────────────────────────────────────────────
129
+
130
+ let storyResults; // Array<{ id, hook, archetype, verdict, assertions }>
131
+ let schemaPass = true;
132
+
133
+ if (inMemory) {
134
+ const checks = storiesToChecks(stories); // warns + skips duckdb assertions
135
+ if (!checks.length) die('verify-stories: every assertion is duckdb (disk-mode-only) — nothing to run in-memory. Drop --in-memory.');
136
+ // token: '' prevents any Mixpanel send; in-memory verification never writes.
137
+ const report = await verifyDungeon({ ...config, token: '', writeToDisk: false }, checks);
138
+ schemaPass = !!report.schemaReport?.pass;
139
+ const byStory = new Map(stories.map(s => [s.id, { id: s.id, hook: s.hook, archetype: s.archetype, verdict: null, assertions: [] }]));
140
+ for (const r of report.results) {
141
+ const m = /^(.+)\[(\d+)\]$/.exec(r.name);
142
+ const st = m && byStory.get(m[1]);
143
+ if (!st) continue;
144
+ const verdict = (/^([A-Z]+) — /.exec(r.detail || '') || [])[1] || (r.pass ? 'STRONG' : 'NONE');
145
+ st.assertions.push({ name: r.name, verdict, observed: null, detail: r.detail || '' });
146
+ }
147
+ for (const st of byStory.values()) {
148
+ // duckdb-only stories have zero in-memory assertions: informational SKIPPED.
149
+ st.verdict = st.assertions.length
150
+ ? st.assertions.reduce((w, a) => VERDICT_RANK[a.verdict] < VERDICT_RANK[w] ? a.verdict : w, 'NAILED')
151
+ : 'SKIPPED';
152
+ }
153
+ storyResults = [...byStory.values()];
154
+ } else {
155
+ const prefix = dataPrefix || `verify-${path.basename(abs).replace(/\.(js|mjs)$/, '')}`;
156
+ const prefixPath = prefix.includes('/') ? prefix : path.join('data', prefix);
157
+ const dir = path.dirname(prefixPath), base = path.basename(prefixPath);
158
+ async function loadShards(suffix) {
159
+ // streaming load: full-fidelity event shards can exceed the readFileSync cap
160
+ if (!fs.existsSync(dir)) return [];
161
+ const out = [];
162
+ for (const f of fs.readdirSync(dir).filter(f => f.startsWith(`${base}-${suffix}`) && f.endsWith('.json')).sort()) {
163
+ const rl = readline.createInterface({ input: fs.createReadStream(path.join(dir, f)), crlfDelay: Infinity });
164
+ for await (const line of rl) {
165
+ if (line.trim()) out.push(JSON.parse(line));
166
+ }
167
+ }
168
+ return out;
169
+ }
170
+ const events = await loadShards('EVENTS');
171
+ const profiles = await loadShards('USERS');
172
+ if (!events.length) {
173
+ die(`verify-stories: no shards at ${prefixPath}-EVENTS*.json — generate first:\n node scripts/verify-runner.mjs ${dungeonPath} ${base}`);
174
+ }
175
+ if (!asJson) console.log(`${path.basename(abs)} — events=${events.length} users=${profiles.length} (${prefixPath})`);
176
+
177
+ // Funnel auto-threading reads VALIDATED funnel fields (conversionWindowDays,
178
+ // order). The dungeon was not run in this process, so validate the config
179
+ // here — validateDungeonConfig resolves those defaults in place.
180
+ const validated = validateDungeonConfig({ ...config, token: '' });
181
+ const identityMap = buildIdentityMap(profiles);
182
+
183
+ const execFileP = promisify(execFile);
184
+ const runSql = async (sql) => {
185
+ const substituted = sql.replaceAll('{{PREFIX}}', prefixPath);
186
+ let stdout;
187
+ try {
188
+ ({ stdout } = await execFileP('duckdb', ['-json', '-c', substituted], { maxBuffer: 512 * 1024 * 1024 }));
189
+ } catch (err) {
190
+ if (err.code === 'ENOENT') throw new Error('duckdb CLI not found on PATH (required for duckdb assertions)');
191
+ throw new Error(`duckdb failed: ${(err.stderr || err.message || '').trim().slice(0, 500)}`);
192
+ }
193
+ const trimmed = (stdout || '').trim();
194
+ return trimmed ? JSON.parse(trimmed) : [];
195
+ };
196
+
197
+ storyResults = await evaluateStories(stories, events, {
198
+ profiles,
199
+ funnels: Array.isArray(validated.funnels) ? validated.funnels : [],
200
+ identityMap,
201
+ runSql,
202
+ });
203
+ }
204
+
205
+ // ── report ──────────────────────────────────────────────────────────────────
206
+
207
+ const counted = storyResults.filter(s => s.verdict !== 'SKIPPED');
208
+ const failing = counted.filter(s => VERDICT_RANK[s.verdict] < VERDICT_RANK.STRONG);
209
+ const pass = failing.length === 0 && missing.length === 0 && schemaPass;
210
+
211
+ if (asJson) {
212
+ console.log(JSON.stringify({
213
+ dungeon: path.relative(process.cwd(), abs),
214
+ mode: inMemory ? 'in-memory' : 'disk',
215
+ coverage,
216
+ stories: storyResults,
217
+ schemaPass,
218
+ pass,
219
+ }, null, 2));
220
+ } else {
221
+ const wId = Math.max(5, ...storyResults.map(s => s.id.length));
222
+ const wHook = Math.max(4, ...storyResults.map(s => String(s.hook).length));
223
+ const wArch = Math.max(9, ...storyResults.map(s => s.archetype.length));
224
+ console.log('');
225
+ console.log(`${'STORY'.padEnd(wId)} ${'HOOK'.padEnd(wHook)} ${'ARCHETYPE'.padEnd(wArch)} VERDICT`);
226
+ for (const s of storyResults) {
227
+ console.log(`${s.id.padEnd(wId)} ${String(s.hook).padEnd(wHook)} ${s.archetype.padEnd(wArch)} ${s.verdict}`);
228
+ for (const a of s.assertions) {
229
+ console.log(` ${a.name.padEnd(wId)} ${a.verdict} — ${a.detail.replace(/^[A-Z]+ — /, '')}`);
230
+ }
231
+ }
232
+ console.log('');
233
+ const tally = {};
234
+ for (const s of counted) tally[s.verdict] = (tally[s.verdict] || 0) + 1;
235
+ const skipped = storyResults.length - counted.length;
236
+ console.log(`${counted.length} stories: ${Object.entries(tally).map(([v, n]) => `${n} ${v}`).join(', ') || 'none'}${skipped ? ` (${skipped} skipped — duckdb-only, disk mode required)` : ''}`);
237
+ if (coverage.note) console.log(`coverage: ${coverage.note}`);
238
+ else if (missing.length) console.log(`coverage: FAIL — hooks with no story: ${missing.map(n => `H${n}`).join(', ')} (declared H${coverage.declared.join(', H')})`);
239
+ else console.log(`coverage: ${coverage.declared.length} hooks declared in HOOK STORIES, all covered`);
240
+ if (!schemaPass) console.log('schema report: FAIL (see verifyDungeon output)');
241
+ console.log(pass ? 'PASS' : 'FAIL');
242
+ }
243
+
244
+ process.exit(pass ? 0 : 1);