@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.
- package/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +1 -1
- package/.claude/skills/create-project/provision.mjs +57 -10
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +183 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +2 -1
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- 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);
|