@ak--47/dungeon-master 1.6.0 → 1.6.2
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/powertools/SKILL.md +75 -0
- package/.claude/skills/powertools/pt.mjs +91 -0
- package/.claude/skills/powertools/snapshot-project.mjs +124 -0
- package/CHANGELOG.md +138 -0
- package/README.md +2 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +4 -5
- package/dungeons/vertical/community/community.verify.mjs +4 -6
- package/dungeons/vertical/crypto/crypto.verify.mjs +4 -2
- package/dungeons/vertical/dating/dating.verify.mjs +4 -6
- package/dungeons/vertical/devtools/devtools.verify.mjs +4 -2
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +4 -4
- package/dungeons/vertical/education/education.verify.mjs +4 -9
- package/dungeons/vertical/fintech/fintech.verify.mjs +4 -4
- package/dungeons/vertical/fitness/fitness.verify.mjs +4 -5
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +4 -2
- package/dungeons/vertical/gaming/gaming.verify.mjs +4 -4
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +4 -6
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +4 -2
- package/dungeons/vertical/logistics/logistics.verify.mjs +4 -7
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +4 -2
- package/dungeons/vertical/media/media.verify.mjs +4 -2
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +4 -9
- package/dungeons/vertical/sass/sass.verify.mjs +4 -2
- package/dungeons/vertical/social/social.verify.mjs +4 -2
- package/dungeons/vertical/streaming/streaming.verify.mjs +4 -2
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +4 -2
- package/dungeons/vertical/travel/travel.verify.mjs +4 -6
- package/index.js +35 -2
- package/lib/core/config-validator.js +19 -1
- package/lib/orchestrators/mixpanel-sender.js +52 -2
- package/lib/utils/utils.js +146 -55
- package/lib/verify/index.js +6 -0
- package/lib/verify/verify-dungeon.js +39 -12
- package/package.json +4 -4
- package/scripts/verify-stories.mjs +2 -1
- package/types.d.ts +17 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: powertools
|
|
3
|
+
description: Use when any task needs the Mixpanel Power Tools API ("use powertools") — schema export (get-schema), event volumes, project CRUD, query methods, macros, or snapshotting a prod project's schema to copy it into a dungeon. Companion to create-project (which handles provisioning specifically).
|
|
4
|
+
argument-hint: [what to do, e.g. "get schema for project 12345" or "copy project 12345 into a dungeon"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Power Tools API
|
|
8
|
+
|
|
9
|
+
Base URL: `https://mixpanel-power-tools-api-lmozz6xkha-uc.a.run.app`
|
|
10
|
+
|
|
11
|
+
## Auth model — read this first
|
|
12
|
+
|
|
13
|
+
- **GET any endpoint path = documentation, no auth.** Always `curl -s GET <base><path>` before first use of an unfamiliar endpoint — docs include exact body params and response shapes.
|
|
14
|
+
- **POST = execute.** `Authorization: Bearer <oauth-token>` (employee OAuth from repo `.env` `BEARER_TOKEN`, or a customer's OAuth token) or `Basic base64(service_acct:secret)`.
|
|
15
|
+
- **Customer OAuth tokens are accepted** (verified 2026-07-06): `/auth` and `/macro/get-schema` work with a customer token on projects that token can access. `ai_endpoints_allowed: false` for non-employees — the `ai-*` family stays employee-only.
|
|
16
|
+
- **Every POST body** should include `client_id: "dungeon-master"` and `region` (`US` default).
|
|
17
|
+
- **`/auth` accessibility ≠ data access.** `/auth {project_id}` can report `accessible: true` while `get-schema`/`query/*` on the same project return `HTTP 403: Forbidden` (token lacks data-level access, e.g. an employee token on a customer project). Diagnose with `/auth`, but don't trust it for data endpoints. Prefer the token of an actual project member.
|
|
18
|
+
|
|
19
|
+
## Tools in this skill
|
|
20
|
+
|
|
21
|
+
### `pt.mjs` — ad-hoc client
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node .claude/skills/powertools/pt.mjs <path> ['<json-body>'] [--bearer <token>] [--get] [--region US]
|
|
25
|
+
# examples
|
|
26
|
+
node .claude/skills/powertools/pt.mjs /auth '{}'
|
|
27
|
+
node .claude/skills/powertools/pt.mjs /macro/get-schema '{"project_id":"123","include_metadata":true,"verbose":true}'
|
|
28
|
+
node .claude/skills/powertools/pt.mjs /query/getTopEvents --get # docs, no auth
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Bearer defaults to `.env` `BEARER_TOKEN`. Merges `client_id`/`region` into the body. Prints pretty JSON to stdout.
|
|
32
|
+
|
|
33
|
+
### `snapshot-project.mjs` — schema + relative-volume snapshot
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node .claude/skills/powertools/snapshot-project.mjs <project_id> --bearer <token> \
|
|
37
|
+
[--region US] [--out snapshot.json]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Uses `/macro/get-schema` (`include_metadata` + `verbose`) + `/query/getTopEvents`. Token (customer or employee) needs data access to the project.
|
|
41
|
+
- Output shape: `{ projectId, projectName, fetchedAt, totalCount, events: [{ name, count, pct, properties: [{name, type, description}] }], userProps: [{name, type, description}], groups: {} }`, events sorted by count desc.
|
|
42
|
+
- **Never captures property values** — schema + volumes only. Snapshots stay privacy-safe by construction.
|
|
43
|
+
|
|
44
|
+
## Endpoint catalog (the useful subset)
|
|
45
|
+
|
|
46
|
+
GET the path for full docs. Full list: GET `/` and GET `/macro`.
|
|
47
|
+
|
|
48
|
+
**crud** — `/crud/createProject`, `/crud/deleteProject`, `/crud/getProjects`, `/crud/mintServiceAccount`, `/crud/addGroupKey`, `/crud/setBusinessContext` (all used by the create-project skill's `provision.mjs`).
|
|
49
|
+
|
|
50
|
+
**query** — `/query/getTopEvents` (per-event counts, limit≤100 default), `/query/getEventNames`, `/query/getPropertyValues`, `/query/getTopProperties`, `/query/getSegmentation`, `/query/getFunnel`, `/query/listFunnels`, `/query/listCohorts`, `/query/runJQL`. Rate limits: 5 concurrent / 60 per hour; 1h response cache.
|
|
51
|
+
|
|
52
|
+
**macros** — `/macro/get-schema` (always pass `include_metadata: true, verbose: true`; also `include_density: true` for per-(event,property) coverage % and `include_sdk_defaults: true` to keep `$browser`/`$os`-style SDK props — needed for carbon copies), `/macro/analyze-project` (volumes + cardinality + activity), `/macro/enumerate-project`, `/macro/clone-project`, `/macro/clone-boards`, `/macro/delete-entities`, `/macro/dungeon-master` (runs a dungeon config server-side and ingests), `/macro/ai-e2e-dm4` (demo build from a supplied schema, designed for dungeon-master), plus the `ai-*` family (dashboards, cohorts, metrics, schema naming).
|
|
53
|
+
|
|
54
|
+
### get-schema field notes (verified on a 2,410-event project, 2026-07-06)
|
|
55
|
+
|
|
56
|
+
- Response nests under `json`: `{ json: { events, properties, users, groups, dependencies }, duration_ms }`. Large projects are slow — 2,410 events took ~13 min; run it in the background.
|
|
57
|
+
- Event counts include custom events (`customEventId > 0`) and merged events. Filter `!merged && !(customEventId > 0)` to match the Lexicon UI event count.
|
|
58
|
+
- `properties` includes `mp_*` internals and `$custom_property:<id>` computed-prop references — exclude both when authoring a dungeon (ingestion stamps `mp_*` itself; computed props can't be tracked).
|
|
59
|
+
- `exampleValue` fields contain REAL customer values — never copy them into a dungeon.
|
|
60
|
+
- **Known issue**: `dependencies` can come back empty (`{events:{},properties:{}}`) on large projects even with `verbose: true` + `include_density: true` — the bulk dependency call silently fails. Retry the call; per-event property mapping is unavailable until it succeeds.
|
|
61
|
+
|
|
62
|
+
## Recipe: copy a prod project into a dungeon
|
|
63
|
+
|
|
64
|
+
Goal: a purely synthetic dungeon with the same events/props/user-props and matching **relative** event volumes. Safe to share — no customer data.
|
|
65
|
+
|
|
66
|
+
1. **Snapshot** the source project (`snapshot-project.mjs`, customer token unless the employee token has data access). Sanity-check event count + that counts are non-zero.
|
|
67
|
+
2. **Author the dungeon** from the snapshot (schema-first, per repo hook rules):
|
|
68
|
+
- Take the top-N events covering ≥95% of total volume (`pct` cumsum); note dropped tail in the OVERVIEW comment.
|
|
69
|
+
- `weight` per event ∝ snapshot `count` (normalize so max ≈ 100, min ≥ 1).
|
|
70
|
+
- Properties per event from snapshot; **invent all values** from name/type/description — never copy real values.
|
|
71
|
+
- `userProps` from snapshot; shared high-frequency props → `superProps`.
|
|
72
|
+
- Funnels: best-effort from event-name semantics (or `/query/listFunnels` + `/query/getFunnel` on the source if accessible).
|
|
73
|
+
3. **Provision** into our org via the create-project skill: `node .claude/skills/create-project/provision.mjs <dungeon> --dry-run` → confirm → live. Uses `.env` `BEARER_TOKEN` + `ORG_ID`, writes `credentials` back into the dungeon.
|
|
74
|
+
4. **Run**: `node scripts/run-dungeon.mjs <dungeon>`.
|
|
75
|
+
5. **Verify** relative volumes: top-10 generated events should rank in the same order as the snapshot's top-10.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pt.mjs — thin ad-hoc client for the Mixpanel Power Tools API.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node .claude/skills/powertools/pt.mjs <path> ['<json-body>'] [--bearer <token>] [--get] [--region US]
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* node .claude/skills/powertools/pt.mjs /auth '{}'
|
|
11
|
+
* node .claude/skills/powertools/pt.mjs /macro/get-schema '{"project_id":"123","include_metadata":true,"verbose":true}'
|
|
12
|
+
* node .claude/skills/powertools/pt.mjs /query/getTopEvents --get # endpoint docs, no auth
|
|
13
|
+
*
|
|
14
|
+
* POST bodies are merged with { client_id: "dungeon-master", region }.
|
|
15
|
+
* Bearer defaults to BEARER_TOKEN in the repo .env (employee OAuth). Customer
|
|
16
|
+
* OAuth tokens are accepted on non-ai endpoints; the ai-* family is
|
|
17
|
+
* employee-only (see SKILL.md).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { dirname, resolve } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import dotenv from 'dotenv';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
dotenv.config({ path: resolve(__dirname, '../../../.env') });
|
|
26
|
+
|
|
27
|
+
const BASE = 'https://mixpanel-power-tools-api-lmozz6xkha-uc.a.run.app';
|
|
28
|
+
const CLIENT_ID = 'dungeon-master';
|
|
29
|
+
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const getMode = popFlag('--get');
|
|
32
|
+
const bearer = popOpt('--bearer') ?? process.env.BEARER_TOKEN;
|
|
33
|
+
const region = popOpt('--region') ?? 'US';
|
|
34
|
+
const [pathArg, bodyArg] = args;
|
|
35
|
+
|
|
36
|
+
if (!pathArg || !pathArg.startsWith('/')) {
|
|
37
|
+
console.error('Usage: node pt.mjs <path starting with /> [\'<json-body>\'] [--bearer <token>] [--get] [--region US]');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (getMode) {
|
|
42
|
+
const res = await fetch(BASE + pathArg);
|
|
43
|
+
console.log(JSON.stringify(await res.json(), null, 2));
|
|
44
|
+
process.exit(res.ok ? 0 : 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!bearer) {
|
|
48
|
+
console.error('No bearer token: pass --bearer or set BEARER_TOKEN in .env');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let body = {};
|
|
53
|
+
if (bodyArg) {
|
|
54
|
+
try {
|
|
55
|
+
body = JSON.parse(bodyArg);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(`Body is not valid JSON: ${err.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const res = await fetch(BASE + pathArg, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${bearer}` },
|
|
65
|
+
body: JSON.stringify({ client_id: CLIENT_ID, region, ...body }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
try {
|
|
70
|
+
console.log(JSON.stringify(JSON.parse(text), null, 2));
|
|
71
|
+
} catch {
|
|
72
|
+
console.log(text);
|
|
73
|
+
}
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
console.error(`\nHTTP ${res.status} ${res.statusText}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function popFlag(name) {
|
|
80
|
+
const i = args.indexOf(name);
|
|
81
|
+
if (i === -1) return false;
|
|
82
|
+
args.splice(i, 1);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function popOpt(name) {
|
|
87
|
+
const i = args.indexOf(name);
|
|
88
|
+
if (i === -1) return undefined;
|
|
89
|
+
const [, value] = args.splice(i, 2);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* snapshot-project.mjs — export a Mixpanel project's schema + relative event
|
|
5
|
+
* volumes into one normalized JSON snapshot, for authoring a synthetic
|
|
6
|
+
* dungeon-master "copy" of the project.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node .claude/skills/powertools/snapshot-project.mjs <project_id> --bearer <token> \
|
|
10
|
+
* [--region US|EU|IN] [--out <file>]
|
|
11
|
+
*
|
|
12
|
+
* Uses the Power Tools API: /macro/get-schema (include_metadata + verbose) +
|
|
13
|
+
* /query/getTopEvents. The bearer token (customer or employee OAuth) needs
|
|
14
|
+
* data access to the project — /auth reporting accessible: true is not enough.
|
|
15
|
+
*
|
|
16
|
+
* Output shape (events sorted by count desc):
|
|
17
|
+
* {
|
|
18
|
+
* projectId, projectName, fetchedAt, region, totalCount,
|
|
19
|
+
* events: [{ name, count, pct, properties: [{ name, type, description }] }],
|
|
20
|
+
* userProps: [{ name, type, description }],
|
|
21
|
+
* groups: {}
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Privacy: captures schema + volumes ONLY — never property values.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { writeFileSync } from 'fs';
|
|
28
|
+
import { dirname, resolve } from 'path';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import dotenv from 'dotenv';
|
|
31
|
+
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
dotenv.config({ path: resolve(__dirname, '../../../.env') });
|
|
34
|
+
|
|
35
|
+
const PT_BASE = 'https://mixpanel-power-tools-api-lmozz6xkha-uc.a.run.app';
|
|
36
|
+
const CLIENT_ID = 'dungeon-master';
|
|
37
|
+
const REGIONS = ['US', 'EU', 'IN'];
|
|
38
|
+
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
const bearer = popOpt('--bearer') ?? process.env.BEARER_TOKEN;
|
|
41
|
+
const region = (popOpt('--region') ?? 'US').toUpperCase();
|
|
42
|
+
const outArg = popOpt('--out');
|
|
43
|
+
const projectId = args.find((a) => !a.startsWith('--'));
|
|
44
|
+
|
|
45
|
+
if (!projectId || !/^\d+$/.test(projectId)) fail('Usage: snapshot-project.mjs <project_id> --bearer <token> [--region US] [--out file]');
|
|
46
|
+
if (!bearer) fail('No bearer token: pass --bearer or set BEARER_TOKEN in .env');
|
|
47
|
+
if (!REGIONS.includes(region)) fail(`Unknown --region "${region}"`);
|
|
48
|
+
|
|
49
|
+
const outPath = resolve(process.cwd(), outArg ?? `snapshot-${projectId}.json`);
|
|
50
|
+
|
|
51
|
+
const snapshot = await snapshotViaPowertools();
|
|
52
|
+
|
|
53
|
+
snapshot.events.sort((a, b) => b.count - a.count);
|
|
54
|
+
snapshot.totalCount = snapshot.events.reduce((s, e) => s + e.count, 0);
|
|
55
|
+
for (const e of snapshot.events) e.pct = snapshot.totalCount ? +(e.count / snapshot.totalCount * 100).toFixed(4) : 0;
|
|
56
|
+
|
|
57
|
+
writeFileSync(outPath, JSON.stringify(snapshot, null, 2));
|
|
58
|
+
|
|
59
|
+
const zeros = snapshot.events.filter((e) => e.count === 0).length;
|
|
60
|
+
console.log(`✓ snapshot → ${outPath}`);
|
|
61
|
+
console.log(` project: ${snapshot.projectName ?? '(name unknown)'} (${snapshot.projectId})`);
|
|
62
|
+
console.log(` events: ${snapshot.events.length} (${zeros} with zero volume)`);
|
|
63
|
+
console.log(` userProps: ${snapshot.userProps.length}`);
|
|
64
|
+
console.log(` total vol: ${snapshot.totalCount.toLocaleString()} events`);
|
|
65
|
+
console.log(' top 10:');
|
|
66
|
+
for (const e of snapshot.events.slice(0, 10)) console.log(` ${e.pct.toFixed(2).padStart(6)}% ${e.count.toLocaleString().padStart(12)} ${e.name}`);
|
|
67
|
+
|
|
68
|
+
async function snapshotViaPowertools() {
|
|
69
|
+
const schema = await ptPost('/macro/get-schema', { project_id: projectId, include_metadata: true, verbose: true });
|
|
70
|
+
const top = await ptPost('/query/getTopEvents', { project_id: projectId, limit: 500 });
|
|
71
|
+
|
|
72
|
+
const counts = {};
|
|
73
|
+
for (const r of top.results ?? []) counts[r.event] = r.count;
|
|
74
|
+
|
|
75
|
+
// dependencies.events maps eventName → [propertyNames]; property defs live in schema.properties
|
|
76
|
+
const propDefs = new Map();
|
|
77
|
+
for (const p of schema.properties ?? []) propDefs.set(p.name, p);
|
|
78
|
+
const deps = schema.dependencies?.events ?? {};
|
|
79
|
+
|
|
80
|
+
const events = (schema.events ?? []).map((ev) => {
|
|
81
|
+
const name = ev.name ?? ev;
|
|
82
|
+
const propNames = deps[name] ?? [];
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
count: counts[name] ?? 0,
|
|
86
|
+
pct: 0,
|
|
87
|
+
properties: propNames.map((pn) => {
|
|
88
|
+
const def = propDefs.get(pn) ?? {};
|
|
89
|
+
return { name: pn, type: def.type ?? 'string', description: def.description ?? '' };
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const userProps = (schema.users ?? []).map((p) => ({
|
|
95
|
+
name: p.name ?? p,
|
|
96
|
+
type: p.type ?? 'string',
|
|
97
|
+
description: p.description ?? '',
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
return { projectId, projectName: null, fetchedAt: new Date().toISOString(), region, totalCount: 0, events, userProps, groups: schema.groups ?? {} };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function ptPost(pathname, body) {
|
|
104
|
+
const res = await fetch(PT_BASE + pathname, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${bearer}` },
|
|
107
|
+
body: JSON.stringify({ client_id: CLIENT_ID, region, ...body }),
|
|
108
|
+
});
|
|
109
|
+
const json = await res.json().catch(() => ({}));
|
|
110
|
+
if (!res.ok) throw new Error(`POST ${pathname} → HTTP ${res.status}: ${json.error ?? JSON.stringify(json).slice(0, 300)}`);
|
|
111
|
+
return json;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function popOpt(name) {
|
|
115
|
+
const i = args.indexOf(name);
|
|
116
|
+
if (i === -1) return undefined;
|
|
117
|
+
const [, value] = args.splice(i, 2);
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function fail(msg) {
|
|
122
|
+
console.error(`✖ ${msg}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,144 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@ak--47/dungeon-master`.
|
|
4
4
|
|
|
5
|
+
## 1.6.2 — 2026-07-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Running the same dungeon twice in one process no longer collapses the second
|
|
10
|
+
run.** `validateDungeonConfig` enriched in place — stamping `isStrictEvent` on
|
|
11
|
+
funnel-step events and `conversionWindowDays` / `_experiment` on funnels — and
|
|
12
|
+
`DUNGEON_MASTER` handed the pipeline a shallow spread, so those stamps landed on
|
|
13
|
+
the caller's own `events` / `funnels` arrays. For a file input that array belongs
|
|
14
|
+
to the ESM module cache, so run 2 got a config already enriched by run 1: every
|
|
15
|
+
event pre-promoted to strict, the catch-all funnel swept nothing, and event volume
|
|
16
|
+
collapsed (measured 317 → 0 on `dungeons/technical/simple.js`). The validator now
|
|
17
|
+
clones its input and enriches only what it returns; functions (`hook`,
|
|
18
|
+
`onProgress`, chance-bound prop thunks) are preserved by reference. Affects an
|
|
19
|
+
object config passed by a caller who reuses it, a file path run more than once in
|
|
20
|
+
a process, and an array of paths. Not affected: raw-text input (each call writes a
|
|
21
|
+
fresh temp module) and `scripts/run-many.mjs` (forks a child per dungeon). Pinned
|
|
22
|
+
by `tests/integration/config-isolation.test.js`.
|
|
23
|
+
- **`verifyDungeon` now applies funnel config to path and JSON inputs.** It read
|
|
24
|
+
`conversionWindowDays` / `order` back off the caller's own `config`, which for a
|
|
25
|
+
string input has no `.funnels` at all — so every funnel check silently ran with
|
|
26
|
+
the default order and an unbounded window. It now reads
|
|
27
|
+
`result.validatedConfig.funnels`.
|
|
28
|
+
- **`verifyDungeon`'s schema report is no longer spurious.** `validateSchema` also
|
|
29
|
+
ran against the raw input: a path input has no fields to derive an expected
|
|
30
|
+
schema from, and a v1.5.1 dungeon keeps `hasAndroidDevices` / `hasBrowser` under
|
|
31
|
+
`switches`, which `deriveExpectedSchema` only sees once flattened. Both produced
|
|
32
|
+
a wall of phantom `flagStamping` findings and a permanently false `report.pass`
|
|
33
|
+
(16 phantom findings on `dungeons/technical/experiments.js`; now 0). It now
|
|
34
|
+
validates against the config the run actually used.
|
|
35
|
+
- **The 22 shipped `dungeons/vertical/*/*.verify.mjs` wrappers now thread VALIDATED
|
|
36
|
+
funnels** into `evaluateStories`. They passed `config.funnels` raw, so no funnel
|
|
37
|
+
story in any vertical had a conversion window — every vertical funnel resolves to
|
|
38
|
+
a 30- or 45-day `conversionWindowDays` that was being dropped. Pre-existing (these
|
|
39
|
+
scripts read shards off disk, so nothing ever enriched their config).
|
|
40
|
+
`validateDungeonConfig` is now exported from `@ak--47/dungeon-master/verify` for
|
|
41
|
+
exactly this.
|
|
42
|
+
- **`verifyDungeon` throws on a multi-dungeon input** instead of silently verifying
|
|
43
|
+
`result[0]` and discarding the rest — which returned a green report for dungeons
|
|
44
|
+
nobody looked at. Call it once per dungeon.
|
|
45
|
+
- **`tests/unit/dungeon-shapes.test.js` no longer asserts the lowercase-hyphen naming
|
|
46
|
+
convention against `dungeons/user/`.** That directory is gitignored per-machine
|
|
47
|
+
scratch space, so the check failed on whatever a given developer had checked out
|
|
48
|
+
locally and was unreproducible in CI. The convention still applies to the tracked
|
|
49
|
+
`technical/` and `vertical/` dungeons.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **`result.validatedConfig`** — the enriched config the run actually used. Read
|
|
54
|
+
resolved values (`funnels[].conversionWindowDays`, `events[].isStrictEvent`, the
|
|
55
|
+
resolved dataset window) here now that the validator no longer writes them back to
|
|
56
|
+
the object you passed in. Two caveats, both documented on the type: credentials
|
|
57
|
+
are stripped (a Result gets logged), and it is **read-only** — validation is not
|
|
58
|
+
idempotent, so feeding it back into `DUNGEON_MASTER` grows the funnel set and
|
|
59
|
+
eventually yields an empty `sequence`. Re-run the original config instead.
|
|
60
|
+
- **`verifyDungeon(config, checks, overrides)`** — an optional third argument,
|
|
61
|
+
merged into the dungeon before it runs exactly like `DUNGEON_MASTER`'s second
|
|
62
|
+
argument. Lets CI verify a production-scale dungeon at a small `numUsers` /
|
|
63
|
+
`numEvents` without editing it. The report also carries `validatedConfig`.
|
|
64
|
+
- **`validateDungeonConfig`** re-exported from `@ak--47/dungeon-master/verify`, so a
|
|
65
|
+
standalone verify script that reads shards off disk can resolve funnel defaults
|
|
66
|
+
before calling `evaluateStories` / `applyFunnelDefaults`.
|
|
67
|
+
- **`releaseConnections()`** exported from `lib/orchestrators/mixpanel-sender.js`,
|
|
68
|
+
for hosts that drive `mixpanel-import` directly and want the same pool teardown.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- **`validateDungeonConfig` no longer mutates its input.** Enrichment lands only on
|
|
73
|
+
the returned object. If you called it directly and then read `isStrictEvent` /
|
|
74
|
+
`conversionWindowDays` back off the config you passed in, read the return value
|
|
75
|
+
instead (or `result.validatedConfig` after a run). Running a dungeon is
|
|
76
|
+
unaffected. This is the fix for the collapse bug above, so it ships on a patch.
|
|
77
|
+
- **`mixpanel-import` bumped `^3.3.2` → `^3.5.1`.** Notable for dungeon-master:
|
|
78
|
+
- **Flat events past `epochEnd` no longer kill the entire import job.** The sender
|
|
79
|
+
sets `epochEnd: dayjs().unix()` and dungeon-master events are flat, which on
|
|
80
|
+
3.3.2 threw `Record has no properties object, cannot fix time` and failed the
|
|
81
|
+
whole batch; 3.5.1 counts the record as `outOfBounds` and carries on. Verified
|
|
82
|
+
directly against both versions.
|
|
83
|
+
- **India-region SCD imports go to `api-in`.** 3.3.2 routed `region: 'IN'` SCD
|
|
84
|
+
batches to `api-eu`.
|
|
85
|
+
- **The library no longer installs five process-global handlers**
|
|
86
|
+
(`unhandledRejection`, `uncaughtException`, `exit`, `SIGINT`, `SIGTERM`) as an
|
|
87
|
+
import side effect. **Hosts embedding dungeon-master will now crash on uncaught
|
|
88
|
+
exceptions and unhandled rejections instead of logging and continuing** — those
|
|
89
|
+
errors were always happening, only the reporting changes. Register your own
|
|
90
|
+
handlers to restore the old behavior. Upside: `user-loop`'s own SIGINT handler is
|
|
91
|
+
no longer preempted, so Ctrl+C cancellation works as designed.
|
|
92
|
+
- Prod-only `npm audit` for the dependency tree: 45 findings → 20 (critical 3 → 1).
|
|
93
|
+
- **`sendToMixpanel` releases mixpanel-import's shared undici connection pools**
|
|
94
|
+
when it settles, so a host that runs occasional imports doesn't hold ingest
|
|
95
|
+
sockets open in between. Runs in a `finally`, guarded, and non-fatal; pools are
|
|
96
|
+
recreated on demand. The pools are process-global, so teardown is refcounted —
|
|
97
|
+
concurrent `DUNGEON_MASTER()` calls in one process won't close sockets out from
|
|
98
|
+
under each other.
|
|
99
|
+
- **`engines.node` raised `>=18.0.0` → `>=20.20.0`**, matching what mixpanel-import
|
|
100
|
+
requires. The old floor had been wrong since 3.3.2 (which already wanted 20.18.1).
|
|
101
|
+
|
|
102
|
+
## 1.6.1 — 2026-07-08
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- **Unweighted string arrays now produce stable power-law distributions**
|
|
107
|
+
(the long-documented intent finally works). Previously `choose()` rolled a
|
|
108
|
+
fresh random winner on every event — two compounding bugs (`!mostChosenIndex`
|
|
109
|
+
discarding an explicit `0`, plus a fresh `pickAWinner` closure per event) —
|
|
110
|
+
so aggregate breakdowns came out uniform (±edge noise). Now any plain array
|
|
111
|
+
of 3–19 unique strings gets ONE seed-deterministic winner per array per run,
|
|
112
|
+
drawn as ~45% winner / ~25% second / ~15% third / geometric-decay tail
|
|
113
|
+
(`winnerWeights` in `lib/utils/utils.js` is the single tuning point).
|
|
114
|
+
- Existing opt-outs unchanged: arrays containing
|
|
115
|
+
`variant`/`group`/`experiment`/`population`, arrays with explicit
|
|
116
|
+
duplicate entries (dupes honored exactly), arrays of length ≤2 or ≥20,
|
|
117
|
+
and non-string values.
|
|
118
|
+
- Direct `u.pickAWinner([...])` use in dungeon files gets the same stable
|
|
119
|
+
memoized winner for the run; explicit `pickAWinner(arr, 0)` is honored now.
|
|
120
|
+
(Note: `pickAWinner` calls at module-import time in dungeon files execute
|
|
121
|
+
before the run's seed is applied — as before — so those winners are stable
|
|
122
|
+
within a run but not pinned across processes.) Each `pickAWinner` resolver
|
|
123
|
+
now also carries its own expansion — previously `choose()`'s resolver cache
|
|
124
|
+
keyed by function source could serve one property's value list to a
|
|
125
|
+
different property.
|
|
126
|
+
- UTM properties (`utm_campaign` etc.) draw through the same path, so
|
|
127
|
+
campaign values now skew realistically per network instead of splitting
|
|
128
|
+
uniformly.
|
|
129
|
+
- New `resetValueCaches()` clears the per-run winner memo (and the
|
|
130
|
+
weighted-array resolver cache, which previously leaked across in-process
|
|
131
|
+
runs); called automatically by `initChance()` and at every run start.
|
|
132
|
+
- Side effect: ~1 RNG draw per string-array property per event instead of
|
|
133
|
+
~30, and the seeded RNG stream shifts vs 1.6.0 — same seed no longer
|
|
134
|
+
reproduces 1.6.0 output byte-for-byte (within-version determinism is
|
|
135
|
+
unchanged).
|
|
136
|
+
- **Engine-shape canary and hook-pattern integration tests made truly
|
|
137
|
+
deterministic** — their generating tests now run `describe.sequential`,
|
|
138
|
+
since concurrent in-process `generate()` calls interleave draws (and
|
|
139
|
+
re-seeds) on the shared seeded chance. The hook-pattern negative control
|
|
140
|
+
was recalibrated to 600 users; at 150 it exceeded its own threshold on
|
|
141
|
+
1.6.0 too and only passed under one lucky interleaving.
|
|
142
|
+
|
|
5
143
|
## 1.6.0 — 2026-07-04
|
|
6
144
|
|
|
7
145
|
### Added
|
package/README.md
CHANGED
|
@@ -155,6 +155,8 @@ import { createTextGenerator, generateBatch } from '@ak--47/dungeon-master/text'
|
|
|
155
155
|
|
|
156
156
|
these are the same functions used internally. `pickAWinner` creates weighted distributions, `weighNumRange` generates realistic numeric ranges with configurable skew, and the text generators produce organic-looking strings with sentiment analysis and keyword injection.
|
|
157
157
|
|
|
158
|
+
**you usually don't need `pickAWinner`** — as of 1.6.1, any property value that is a plain array of 3–19 unique strings automatically gets a stable power-law distribution: one seed-deterministic winner per array per run (~45% winner / ~25% second / ~15% third / decaying tail). to opt out and get uniform draws, use exactly 2 values, 20+, or include one of the keywords `variant` / `group` / `experiment` / `population` in a value (experiment arms stay balanced). arrays with explicit duplicate entries (`["card", "card", "apple_pay"]`) skip the auto-weighting and honor the duplicates exactly.
|
|
159
|
+
|
|
158
160
|
### named exports
|
|
159
161
|
|
|
160
162
|
alongside the default `DUNGEON_MASTER` export, the package root exports loader + interop helpers:
|
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import readline from 'node:readline';
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
|
-
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
20
|
+
import { buildIdentityMap, evaluateStories, validateDungeonConfig, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
21
|
import config, { stories } from './ai-platform.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-ai-platform';
|
|
@@ -51,12 +51,11 @@ const runSql = async (sql) => {
|
|
|
51
51
|
return stdout.trim() ? JSON.parse(stdout) : [];
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
// funnels passed raw (unvalidated) — the H9 story carries its own explicit
|
|
55
|
-
// 336-hour conversion window (2x the generative window, covering the
|
|
56
|
-
// stretched support), so no funnel-default threading is needed
|
|
57
54
|
const results = await evaluateStories(stories, events, {
|
|
58
55
|
profiles,
|
|
59
|
-
|
|
56
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
57
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
58
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
60
59
|
identityMap: buildIdentityMap(profiles),
|
|
61
60
|
runSql,
|
|
62
61
|
});
|
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import readline from 'node:readline';
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
|
-
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
20
|
+
import { buildIdentityMap, evaluateStories, validateDungeonConfig, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
21
|
import config, { stories } from './community.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-community';
|
|
@@ -51,13 +51,11 @@ const runSql = async (sql) => {
|
|
|
51
51
|
return stdout.trim() ? JSON.parse(stdout) : [];
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
// funnels passed raw (unvalidated) — the H8/H9 emulator stories carry their
|
|
55
|
-
// own explicit conversion window (48h Content Creation generative window ×
|
|
56
|
-
// the 1.25 free-tier stretch = 60h, covering the stretched support), so no
|
|
57
|
-
// funnel-default threading is needed
|
|
58
54
|
const results = await evaluateStories(stories, events, {
|
|
59
55
|
profiles,
|
|
60
|
-
|
|
56
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
57
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
58
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
61
59
|
identityMap: buildIdentityMap(profiles),
|
|
62
60
|
runSql,
|
|
63
61
|
});
|
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import readline from 'node:readline';
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
|
-
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
20
|
+
import { buildIdentityMap, evaluateStories, validateDungeonConfig, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
21
|
import config, { stories } from './crypto.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-crypto';
|
|
@@ -63,7 +63,9 @@ const runSql = async (sql) => {
|
|
|
63
63
|
// assemble chains across unscaled clones and collapse the read.
|
|
64
64
|
const results = await evaluateStories(stories, events, {
|
|
65
65
|
profiles,
|
|
66
|
-
|
|
66
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
67
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
68
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
67
69
|
identityMap: buildIdentityMap(profiles),
|
|
68
70
|
runSql,
|
|
69
71
|
});
|
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import readline from 'node:readline';
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
|
-
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
20
|
+
import { buildIdentityMap, evaluateStories, validateDungeonConfig, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
21
|
import config, { stories } from './dating.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-dating';
|
|
@@ -51,13 +51,11 @@ const runSql = async (sql) => {
|
|
|
51
51
|
return stdout.trim() ? JSON.parse(stdout) : [];
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
// funnels passed raw (unvalidated) — the H9 stories carry their own explicit
|
|
55
|
-
// conversion window (24h Match Flow generative window × the 1.4 max stretch
|
|
56
|
-
// factor = 33.6h, covering the stretched support), so no funnel-default
|
|
57
|
-
// threading is needed
|
|
58
54
|
const results = await evaluateStories(stories, events, {
|
|
59
55
|
profiles,
|
|
60
|
-
|
|
56
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
57
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
58
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
61
59
|
identityMap: buildIdentityMap(profiles),
|
|
62
60
|
runSql,
|
|
63
61
|
});
|
|
@@ -18,7 +18,7 @@ import path from 'node:path';
|
|
|
18
18
|
import readline from 'node:readline';
|
|
19
19
|
import { execFile } from 'node:child_process';
|
|
20
20
|
import { promisify } from 'node:util';
|
|
21
|
-
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
|
+
import { buildIdentityMap, evaluateStories, validateDungeonConfig, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
22
22
|
import config, { stories } from './devtools.js';
|
|
23
23
|
|
|
24
24
|
const PREFIX = process.argv[2] || 'verify-devtools';
|
|
@@ -58,7 +58,9 @@ const runSql = async (sql) => {
|
|
|
58
58
|
// semantics, not cross-event SQL (greedy MIN→MIN picks flatten it).
|
|
59
59
|
const results = await evaluateStories(stories, events, {
|
|
60
60
|
profiles,
|
|
61
|
-
|
|
61
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
62
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
63
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
62
64
|
identityMap: buildIdentityMap(profiles),
|
|
63
65
|
runSql,
|
|
64
66
|
});
|
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import readline from 'node:readline';
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
|
-
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
20
|
+
import { buildIdentityMap, evaluateStories, validateDungeonConfig, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
21
|
import config, { stories } from './ecommerce.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-ecommerce';
|
|
@@ -51,11 +51,11 @@ const runSql = async (sql) => {
|
|
|
51
51
|
return stdout.trim() ? JSON.parse(stdout) : [];
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
// funnels passed raw (unvalidated) — the H8 story carries its own explicit
|
|
55
|
-
// 75-minute pairing window in SQL, so no funnel-default threading is needed
|
|
56
54
|
const results = await evaluateStories(stories, events, {
|
|
57
55
|
profiles,
|
|
58
|
-
|
|
56
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
57
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
58
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
59
59
|
identityMap: buildIdentityMap(profiles),
|
|
60
60
|
runSql,
|
|
61
61
|
});
|
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import readline from 'node:readline';
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
|
-
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
20
|
+
import { buildIdentityMap, evaluateStories, validateDungeonConfig, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
21
|
import config, { stories } from './education.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-education';
|
|
@@ -51,16 +51,11 @@ const runSql = async (sql) => {
|
|
|
51
51
|
return stdout.trim() ? JSON.parse(stdout) : [];
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
// funnels passed raw (unvalidated) — the emulator stories carry their own
|
|
55
|
-
// explicit conversion windows (H7/H9: 86.4h = 48h generative × the 1.8 free
|
|
56
|
-
// TTC stretch, on the 2-step enrolled→cert read; the 4-step doc funnel would
|
|
57
|
-
// break because H9's annual ×0.5 compression can move a cert before the
|
|
58
|
-
// interior quiz step). H10 pairs in SQL anchored at funnel ENTRY (first
|
|
59
|
-
// 'discussion posted' at/after $experiment_started, conversion within 12h of
|
|
60
|
-
// entry) because the exp→entry lag is arm-dependent.
|
|
61
54
|
const results = await evaluateStories(stories, events, {
|
|
62
55
|
profiles,
|
|
63
|
-
|
|
56
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
57
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
58
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
64
59
|
identityMap: buildIdentityMap(profiles),
|
|
65
60
|
runSql,
|
|
66
61
|
});
|
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import readline from 'node:readline';
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
|
-
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
20
|
+
import { buildIdentityMap, evaluateStories, validateDungeonConfig, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
21
21
|
import config, { stories } from './fintech.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-fintech';
|
|
@@ -51,11 +51,11 @@ const runSql = async (sql) => {
|
|
|
51
51
|
return stdout.trim() ? JSON.parse(stdout) : [];
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
// funnels passed raw (unvalidated) — every timeToConvert story carries an
|
|
55
|
-
// explicit conversionWindowMs, so no funnel-default threading is needed
|
|
56
54
|
const results = await evaluateStories(stories, events, {
|
|
57
55
|
profiles,
|
|
58
|
-
|
|
56
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
57
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
58
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
59
59
|
identityMap: buildIdentityMap(profiles),
|
|
60
60
|
runSql,
|
|
61
61
|
});
|