@ak--47/dungeon-master 1.6.5 → 1.8.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 -11
- package/.claude/skills/create-dungeon/SKILL.md +49 -10
- package/.claude/skills/create-project/SKILL.md +22 -3
- package/.claude/skills/create-project/context.mjs +89 -0
- package/.claude/skills/create-project/provision.mjs +1 -60
- package/.claude/skills/headless-build/SKILL.md +18 -1
- package/.claude/skills/powertools/SKILL.md +20 -1
- package/.claude/skills/release-check/SKILL.md +99 -0
- package/.claude/skills/verify-dungeon/SKILL.md +71 -16
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +14 -0
- package/.claude/skills/verify-dungeon/references/report-format.md +18 -1
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +36 -1
- package/.claude/skills/warehouse-metrics/GAPS-template.md +34 -0
- package/.claude/skills/warehouse-metrics/SKILL.md +105 -0
- package/.claude/skills/warehouse-metrics/deploy.mjs +651 -0
- package/.claude/skills/write-hooks/SKILL.md +33 -3
- package/CHANGELOG.md +331 -0
- package/HOOKS.md +154 -5
- package/README.md +357 -8
- package/docs/guides/1.7.0-upgrade-guide.md +154 -0
- package/docs/guides/1.8.0-upgrade-guide.md +151 -0
- package/dungeons/technical/warehouse.js +187 -0
- package/index.js +131 -2
- package/lib/core/config-validator.js +264 -13
- package/lib/core/context.js +39 -0
- package/lib/core/dungeon-loader.js +5 -2
- package/lib/core/storage.js +51 -3
- package/lib/generators/events.js +53 -7
- package/lib/generators/funnels.js +85 -11
- package/lib/generators/profiles.js +9 -4
- package/lib/generators/standalone.js +248 -0
- package/lib/generators/warehouse.js +828 -0
- package/lib/orchestrators/mixpanel-sender.js +39 -3
- package/lib/orchestrators/user-loop.js +240 -9
- package/lib/templates/story-spec.schema.json +41 -16
- package/lib/utils/conditions.js +62 -0
- package/lib/utils/json-evaluator.js +12 -2
- package/lib/utils/utils.js +115 -19
- package/lib/verify/index.js +1 -0
- package/lib/verify/schema-validator.js +8 -0
- package/lib/verify/story-runner.js +71 -8
- package/lib/verify/warehouse.js +683 -0
- package/package.json +5 -11
- package/scripts/verify-stories.mjs +150 -44
- package/types.d.ts +606 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ak--47/dungeon-master",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "generate fancy datasets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -48,15 +48,7 @@
|
|
|
48
48
|
"dungeon:run": "node ./scripts/run-dungeon.mjs",
|
|
49
49
|
"dungeon:to-json": "node ./scripts/dungeon-to-json.mjs",
|
|
50
50
|
"dungeon:from-json": "node ./scripts/json-to-dungeon.mjs",
|
|
51
|
-
"dungeon:schema": "node ./scripts/extract-dungeon-schema.mjs"
|
|
52
|
-
"kodiak:deploy": "gcloud builds submit --config dungeons/user/kodiak/cloudbuild.yaml",
|
|
53
|
-
"kodiak:plan": "node dungeons/user/kodiak/kickoff.mjs --plan-only",
|
|
54
|
-
"kodiak:smoke:local": "bash dungeons/user/kodiak/smoke-local.sh",
|
|
55
|
-
"kodiak:smoke": "node dungeons/user/kodiak/kickoff.mjs --total-events 1000000000 --chunk-cap 10",
|
|
56
|
-
"kodiak:generate": "node dungeons/user/kodiak/kickoff.mjs --no-wait",
|
|
57
|
-
"kodiak:status": "node dungeons/user/kodiak/status.mjs",
|
|
58
|
-
"kodiak:run": "bash dungeons/user/kodiak/run-all.sh",
|
|
59
|
-
"kodiak:load": "node dungeons/user/kodiak/load-bq.mjs --replace --no-wait"
|
|
51
|
+
"dungeon:schema": "node ./scripts/extract-dungeon-schema.mjs"
|
|
60
52
|
},
|
|
61
53
|
"repository": {
|
|
62
54
|
"type": "git",
|
|
@@ -83,6 +75,7 @@
|
|
|
83
75
|
"@google-cloud/storage": "^7.14.0",
|
|
84
76
|
"ak-tools": "^1.1.12",
|
|
85
77
|
"chance": "^1.1.11",
|
|
78
|
+
"csv-parse": "^7.0.2",
|
|
86
79
|
"dayjs": "^1.11.11",
|
|
87
80
|
"dotenv": "^16.4.5",
|
|
88
81
|
"hyparquet-writer": "^0.6.1",
|
|
@@ -100,7 +93,8 @@
|
|
|
100
93
|
"@vitest/ui": "^2.1.9",
|
|
101
94
|
"nodemon": "^3.1.3",
|
|
102
95
|
"typescript": "^5.6.0",
|
|
103
|
-
"vitest": "^2.1.9"
|
|
96
|
+
"vitest": "^2.1.9",
|
|
97
|
+
"yaml": "^2.9.0"
|
|
104
98
|
},
|
|
105
99
|
"nodemonConfig": {
|
|
106
100
|
"ignore": [
|
|
@@ -34,15 +34,18 @@ import readline from 'readline';
|
|
|
34
34
|
import { execFile } from 'child_process';
|
|
35
35
|
import { promisify } from 'util';
|
|
36
36
|
import { pathToFileURL } from 'url';
|
|
37
|
+
import { parse as parseCsv } from 'csv-parse';
|
|
38
|
+
import generate from '../index.js';
|
|
37
39
|
import { extractComments } from '../lib/core/extract-comments.js';
|
|
38
40
|
import { validateDungeonConfig } from '../lib/core/config-validator.js';
|
|
39
41
|
import {
|
|
40
42
|
buildIdentityMap,
|
|
41
|
-
verifyDungeon,
|
|
42
43
|
VERDICT_RANK,
|
|
43
44
|
validateStories,
|
|
44
|
-
|
|
45
|
+
validateSchema,
|
|
45
46
|
evaluateStories,
|
|
47
|
+
auditWarehouseRows,
|
|
48
|
+
computeWarehouseSourceRows,
|
|
46
49
|
} from '../lib/verify/index.js';
|
|
47
50
|
|
|
48
51
|
const USAGE = `Usage: node scripts/verify-stories.mjs <dungeon-path> [--data-prefix <prefix>] [--in-memory] [--json]
|
|
@@ -89,13 +92,16 @@ if (!fs.existsSync(abs)) die(`verify-stories: dungeon not found at ${abs}`);
|
|
|
89
92
|
|
|
90
93
|
const mod = await import(pathToFileURL(abs).href);
|
|
91
94
|
const config = mod.default;
|
|
92
|
-
const stories = mod.stories;
|
|
95
|
+
const stories = Array.isArray(mod.stories) ? mod.stories : [];
|
|
93
96
|
if (!config || typeof config !== 'object') die(`verify-stories: ${dungeonPath} has no default-exported config object`);
|
|
94
|
-
|
|
97
|
+
const hasWarehouseMetrics = Array.isArray(config.warehouseMetrics) && config.warehouseMetrics.length > 0;
|
|
98
|
+
if (!stories.length && !hasWarehouseMetrics) {
|
|
95
99
|
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
100
|
}
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
if (stories.length) {
|
|
102
|
+
const sv = validateStories(stories);
|
|
103
|
+
if (!sv.valid) die(`verify-stories: invalid stories:\n ${sv.errors.join('\n ')}`);
|
|
104
|
+
}
|
|
99
105
|
|
|
100
106
|
// ── coverage discipline ─────────────────────────────────────────────────────
|
|
101
107
|
// Every numbered hook in the HOOK STORIES comment block needs >=1 story. One
|
|
@@ -129,28 +135,30 @@ const coverage = {
|
|
|
129
135
|
|
|
130
136
|
let storyResults; // Array<{ id, hook, archetype, verdict, assertions }>
|
|
131
137
|
let schemaPass = true;
|
|
138
|
+
let warehouseAudits = [];
|
|
132
139
|
|
|
133
140
|
if (inMemory) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
schemaPass = !!
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
:
|
|
152
|
-
|
|
153
|
-
|
|
141
|
+
const result = await generate({ ...config, token: '', writeToDisk: false });
|
|
142
|
+
const events = Array.isArray(result.eventData) ? result.eventData : Array.from(result.eventData || []);
|
|
143
|
+
const profiles = Array.isArray(result.userProfilesData) ? result.userProfilesData : Array.from(result.userProfilesData || []);
|
|
144
|
+
const validated = result.validatedConfig || validateDungeonConfig({ ...config, token: '' });
|
|
145
|
+
schemaPass = !!validateSchema(events, validated)?.pass;
|
|
146
|
+
const warehouseSpecs = Object.fromEntries((validated.warehouseMetrics || []).map((spec) => [spec.name, spec]));
|
|
147
|
+
const warehouseRows = result.warehouseMetricData || {};
|
|
148
|
+
storyResults = stories.length
|
|
149
|
+
? await evaluateStories(stories, events, {
|
|
150
|
+
profiles,
|
|
151
|
+
funnels: Array.isArray(validated.funnels) ? validated.funnels : [],
|
|
152
|
+
identityMap: buildIdentityMap(profiles),
|
|
153
|
+
warehouseRows,
|
|
154
|
+
warehouseSpecs,
|
|
155
|
+
datasetStart: validated.datasetStart,
|
|
156
|
+
datasetEnd: validated.datasetEnd,
|
|
157
|
+
skipDiskOnlyDuckdb: true,
|
|
158
|
+
onWarning: (message) => console.error(message),
|
|
159
|
+
})
|
|
160
|
+
: [];
|
|
161
|
+
warehouseAudits = buildWarehouseAudits(warehouseSpecs, warehouseRows, validated, events);
|
|
154
162
|
} else {
|
|
155
163
|
const prefix = dataPrefix || `verify-${path.basename(abs).replace(/\.(js|mjs)$/, '')}`;
|
|
156
164
|
const prefixPath = prefix.includes('/') ? prefix : path.join('data', prefix);
|
|
@@ -180,6 +188,12 @@ if (inMemory) {
|
|
|
180
188
|
// enrich the object you hand it.
|
|
181
189
|
const validated = validateDungeonConfig({ ...config, token: '' });
|
|
182
190
|
const identityMap = buildIdentityMap(profiles);
|
|
191
|
+
schemaPass = !!validateSchema(events, validated)?.pass;
|
|
192
|
+
const warehouseSpecs = Object.fromEntries((validated.warehouseMetrics || []).map((spec) => [spec.name, spec]));
|
|
193
|
+
const warehouseManifest = loadWarehouseManifest(prefixPath);
|
|
194
|
+
const warehouseRows = Object.fromEntries(await Promise.all(
|
|
195
|
+
Object.values(warehouseSpecs).map(async (spec) => [spec.name, await loadWarehouseRows(prefixPath, spec, warehouseManifest)])
|
|
196
|
+
));
|
|
183
197
|
|
|
184
198
|
const execFileP = promisify(execFile);
|
|
185
199
|
const runSql = async (sql) => {
|
|
@@ -195,19 +209,27 @@ if (inMemory) {
|
|
|
195
209
|
return trimmed ? JSON.parse(trimmed) : [];
|
|
196
210
|
};
|
|
197
211
|
|
|
198
|
-
storyResults =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
212
|
+
storyResults = stories.length
|
|
213
|
+
? await evaluateStories(stories, events, {
|
|
214
|
+
profiles,
|
|
215
|
+
funnels: Array.isArray(validated.funnels) ? validated.funnels : [],
|
|
216
|
+
identityMap,
|
|
217
|
+
runSql,
|
|
218
|
+
warehouseRows,
|
|
219
|
+
warehouseSpecs,
|
|
220
|
+
datasetStart: validated.datasetStart,
|
|
221
|
+
datasetEnd: validated.datasetEnd,
|
|
222
|
+
})
|
|
223
|
+
: [];
|
|
224
|
+
warehouseAudits = buildWarehouseAudits(warehouseSpecs, warehouseRows, validated, events, warehouseManifest);
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
// ── report ──────────────────────────────────────────────────────────────────
|
|
207
228
|
|
|
208
229
|
const counted = storyResults.filter(s => s.verdict !== 'SKIPPED');
|
|
209
230
|
const failing = counted.filter(s => VERDICT_RANK[s.verdict] < VERDICT_RANK.STRONG);
|
|
210
|
-
const
|
|
231
|
+
const failingAudits = warehouseAudits.filter((audit) => !audit.pass);
|
|
232
|
+
const pass = failing.length === 0 && failingAudits.length === 0 && missing.length === 0 && schemaPass;
|
|
211
233
|
|
|
212
234
|
if (asJson) {
|
|
213
235
|
console.log(JSON.stringify({
|
|
@@ -215,26 +237,36 @@ if (asJson) {
|
|
|
215
237
|
mode: inMemory ? 'in-memory' : 'disk',
|
|
216
238
|
coverage,
|
|
217
239
|
stories: storyResults,
|
|
240
|
+
warehouseAudits,
|
|
218
241
|
schemaPass,
|
|
219
242
|
pass,
|
|
220
243
|
}, null, 2));
|
|
221
244
|
} else {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
245
|
+
if (storyResults.length) {
|
|
246
|
+
const wId = Math.max(5, ...storyResults.map(s => s.id.length));
|
|
247
|
+
const wHook = Math.max(4, ...storyResults.map(s => String(s.hook).length));
|
|
248
|
+
const wArch = Math.max(9, ...storyResults.map(s => s.archetype.length));
|
|
249
|
+
console.log('');
|
|
250
|
+
console.log(`${'STORY'.padEnd(wId)} ${'HOOK'.padEnd(wHook)} ${'ARCHETYPE'.padEnd(wArch)} VERDICT`);
|
|
251
|
+
for (const s of storyResults) {
|
|
252
|
+
console.log(`${s.id.padEnd(wId)} ${String(s.hook).padEnd(wHook)} ${s.archetype.padEnd(wArch)} ${s.verdict}`);
|
|
253
|
+
for (const a of s.assertions) {
|
|
254
|
+
console.log(` ${a.name.padEnd(wId)} ${a.verdict} — ${a.detail.replace(/^[A-Z]+ — /, '')}`);
|
|
255
|
+
}
|
|
231
256
|
}
|
|
257
|
+
console.log('');
|
|
232
258
|
}
|
|
233
|
-
console.log('');
|
|
234
259
|
const tally = {};
|
|
235
260
|
for (const s of counted) tally[s.verdict] = (tally[s.verdict] || 0) + 1;
|
|
236
261
|
const skipped = storyResults.length - counted.length;
|
|
237
|
-
console.log(`${counted.length} stories: ${Object.entries(tally).map(([v, n]) => `${n} ${v}`).join(', ') || 'none'}${skipped ? ` (${skipped} skipped —
|
|
262
|
+
console.log(`${counted.length} stories: ${Object.entries(tally).map(([v, n]) => `${n} ${v}`).join(', ') || 'none'}${skipped ? ` (${skipped} skipped — disk mode required)` : ''}`);
|
|
263
|
+
if (warehouseAudits.length) {
|
|
264
|
+
console.log('warehouse audit:');
|
|
265
|
+
for (const audit of warehouseAudits) {
|
|
266
|
+
const summary = `stats corr=${formatNumber(audit.stats.corr)} buckets=${audit.stats.buckets} gaps=${audit.stats.gaps} empty=${audit.stats.emptyNumericCells}`;
|
|
267
|
+
console.log(` ${audit.table}: ${audit.pass ? 'PASS' : 'FAIL'} — ${audit.failures.length ? audit.failures.join('; ') : summary}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
238
270
|
if (coverage.note) console.log(`coverage: ${coverage.note}`);
|
|
239
271
|
else if (missing.length) console.log(`coverage: FAIL — hooks with no story: ${missing.map(n => `H${n}`).join(', ')} (declared H${coverage.declared.join(', H')})`);
|
|
240
272
|
else console.log(`coverage: ${coverage.declared.length} hooks declared in HOOK STORIES, all covered`);
|
|
@@ -243,3 +275,77 @@ if (asJson) {
|
|
|
243
275
|
}
|
|
244
276
|
|
|
245
277
|
process.exit(pass ? 0 : 1);
|
|
278
|
+
|
|
279
|
+
function buildWarehouseAudits(warehouseSpecs, warehouseRows, validated, events = [], warehouseManifest = null) {
|
|
280
|
+
return Object.values(warehouseSpecs || {}).map((spec) => {
|
|
281
|
+
const rows = warehouseRows?.[spec.name] || [];
|
|
282
|
+
const sourceRows = events.length ? computeWarehouseSourceRows(events, spec) : [];
|
|
283
|
+
const manifestColumns = warehouseManifest?.tables?.find((table) => table.table === spec.name)?.columns || [];
|
|
284
|
+
return {
|
|
285
|
+
table: spec.name,
|
|
286
|
+
...auditWarehouseRows(rows, spec, {
|
|
287
|
+
datasetStart: validated.datasetStart,
|
|
288
|
+
datasetEnd: validated.datasetEnd,
|
|
289
|
+
sourceRows,
|
|
290
|
+
manifestColumns,
|
|
291
|
+
}),
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function loadWarehouseManifest(prefixPath) {
|
|
297
|
+
const manifestPath = `${prefixPath}-WAREHOUSE-MANIFEST.json`;
|
|
298
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
299
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function loadWarehouseRows(prefixPath, spec, manifest) {
|
|
303
|
+
const manifestEntry = manifest?.tables?.find((table) => table.table === spec.name) || null;
|
|
304
|
+
const fileBase = manifestEntry?.file || `${path.basename(prefixPath)}-WAREHOUSE-${spec.name}`;
|
|
305
|
+
const format = manifestEntry?.format || spec.format || 'csv';
|
|
306
|
+
const filePath = path.join(path.dirname(prefixPath), `${fileBase}.${format}`);
|
|
307
|
+
if (!fs.existsSync(filePath)) return [];
|
|
308
|
+
if (format === 'json') return loadNdjson(filePath);
|
|
309
|
+
return loadCsv(filePath, manifestEntry?.columns || []);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function loadNdjson(filePath) {
|
|
313
|
+
const out = [];
|
|
314
|
+
const rl = readline.createInterface({ input: fs.createReadStream(filePath), crlfDelay: Infinity });
|
|
315
|
+
for await (const line of rl) {
|
|
316
|
+
if (line.trim()) out.push(JSON.parse(line));
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function loadCsv(filePath, columns) {
|
|
322
|
+
const out = [];
|
|
323
|
+
const parser = fs.createReadStream(filePath).pipe(parseCsv({
|
|
324
|
+
bom: true,
|
|
325
|
+
columns: true,
|
|
326
|
+
skip_empty_lines: true,
|
|
327
|
+
}));
|
|
328
|
+
for await (const record of parser) {
|
|
329
|
+
const row = {};
|
|
330
|
+
for (const [header, rawValue] of Object.entries(record)) {
|
|
331
|
+
const type = columns.find((column) => column.name === header)?.bqType;
|
|
332
|
+
row[header] = coerceWarehouseCell(rawValue ?? '', type);
|
|
333
|
+
}
|
|
334
|
+
out.push(row);
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function coerceWarehouseCell(value, bqType) {
|
|
340
|
+
if (bqType === 'FLOAT64') {
|
|
341
|
+
if (value === '') return '';
|
|
342
|
+
const numeric = Number(value);
|
|
343
|
+
return Number.isFinite(numeric) ? numeric : value;
|
|
344
|
+
}
|
|
345
|
+
if (bqType === 'BOOL') return value === 'true';
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function formatNumber(value) {
|
|
350
|
+
return Number.isFinite(value) ? value.toFixed(3) : String(value);
|
|
351
|
+
}
|