@ak--47/dungeon-master 1.6.1 → 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/CHANGELOG.md +97 -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 +30 -1
- package/lib/core/config-validator.js +19 -1
- package/lib/orchestrators/mixpanel-sender.js +52 -2
- 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 +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,103 @@
|
|
|
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
|
+
|
|
5
102
|
## 1.6.1 — 2026-07-08
|
|
6
103
|
|
|
7
104
|
### Fixed
|
|
@@ -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
|
});
|
|
@@ -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 './fitness.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-fitness';
|
|
@@ -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 stories carry their own explicit
|
|
55
|
-
// conversion window (48h generative window × the 1.25 max stretch factor,
|
|
56
|
-
// covering the 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
|
});
|
|
@@ -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 './food-delivery.js';
|
|
23
23
|
|
|
24
24
|
const PREFIX = process.argv[2] || 'verify-food-delivery';
|
|
@@ -60,7 +60,9 @@ const runSql = async (sql) => {
|
|
|
60
60
|
// confounds a raw emulator read would inherit.
|
|
61
61
|
const results = await evaluateStories(stories, events, {
|
|
62
62
|
profiles,
|
|
63
|
-
|
|
63
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
64
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
65
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
64
66
|
identityMap: buildIdentityMap(profiles),
|
|
65
67
|
runSql,
|
|
66
68
|
});
|
|
@@ -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 './gaming.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-gaming';
|
|
@@ -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
|
});
|
|
@@ -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 './healthcare.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-healthcare';
|
|
@@ -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 stories carry their own
|
|
55
|
-
// explicit conversion windows (derived from the funnel's generative window
|
|
56
|
-
// × the max H9 stretch factor, 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
|
});
|
|
@@ -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 './insurance-application.js';
|
|
23
23
|
|
|
24
24
|
const PREFIX = process.argv[2] || 'verify-insurance-application';
|
|
@@ -59,7 +59,9 @@ const runSql = async (sql) => {
|
|
|
59
59
|
// because H5 pins approvals to firstStart + target + [0,4h) by construction.
|
|
60
60
|
const results = await evaluateStories(stories, events, {
|
|
61
61
|
profiles,
|
|
62
|
-
|
|
62
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
63
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
64
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
63
65
|
identityMap: buildIdentityMap(profiles),
|
|
64
66
|
runSql,
|
|
65
67
|
});
|
|
@@ -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 './logistics.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-logistics';
|
|
@@ -51,14 +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 (H8: 48h Integration Setup treated arm, 336h
|
|
56
|
-
// Supplier Management placebo; H10: 93.6h = 72h Onboarding generative
|
|
57
|
-
// window × the 1.3 small-business stretch, covering the stretched support),
|
|
58
|
-
// so no funnel-default threading is needed
|
|
59
54
|
const results = await evaluateStories(stories, events, {
|
|
60
55
|
profiles,
|
|
61
|
-
|
|
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,
|
|
62
59
|
identityMap: buildIdentityMap(profiles),
|
|
63
60
|
runSql,
|
|
64
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 './marketplace.js';
|
|
23
23
|
|
|
24
24
|
const PREFIX = process.argv[2] || 'verify-marketplace';
|
|
@@ -59,7 +59,9 @@ const runSql = async (sql) => {
|
|
|
59
59
|
// windows admit cross-instance chains that contaminate the fast tail.
|
|
60
60
|
const results = await evaluateStories(stories, events, {
|
|
61
61
|
profiles,
|
|
62
|
-
|
|
62
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
63
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
64
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
63
65
|
identityMap: buildIdentityMap(profiles),
|
|
64
66
|
runSql,
|
|
65
67
|
});
|
|
@@ -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 './media.js';
|
|
23
23
|
|
|
24
24
|
const PREFIX = process.argv[2] || 'verify-media';
|
|
@@ -59,7 +59,9 @@ const runSql = async (sql) => {
|
|
|
59
59
|
// vs cross-session) and flips on sampling noise.
|
|
60
60
|
const results = await evaluateStories(stories, events, {
|
|
61
61
|
profiles,
|
|
62
|
-
|
|
62
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
63
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
64
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
63
65
|
identityMap: buildIdentityMap(profiles),
|
|
64
66
|
runSql,
|
|
65
67
|
});
|
|
@@ -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 './real-estate.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-real-estate';
|
|
@@ -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) — H10's emulator stories carry their own
|
|
55
|
-
// conversion window: 31.2h = 24h generative Tour Funnel TTC × the 1.3
|
|
56
|
-
// Standard-tier stretch, so the stretched support is fully covered. The
|
|
57
|
-
// PRIMARY read is the 2-step view→tour-scheduled pair; the doc's 3-step read
|
|
58
|
-
// (…→offer submitted) stays as a directional secondary because H4/H6 offer
|
|
59
|
-
// clones at random timestamps collide with the greedy third-step pick and
|
|
60
|
-
// attenuate the ratio toward 1 (see the H10 story narrative).
|
|
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 './sass.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-sass';
|
|
@@ -59,7 +59,9 @@ const runSql = async (sql) => {
|
|
|
59
59
|
// a slow organic tail that dilutes the segment medians.
|
|
60
60
|
const results = await evaluateStories(stories, events, {
|
|
61
61
|
profiles,
|
|
62
|
-
|
|
62
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
63
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
64
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
63
65
|
identityMap: buildIdentityMap(profiles),
|
|
64
66
|
runSql,
|
|
65
67
|
});
|
|
@@ -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 './social.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-social';
|
|
@@ -61,7 +61,9 @@ const runSql = async (sql) => {
|
|
|
61
61
|
// across 1h-24h windows.
|
|
62
62
|
const results = await evaluateStories(stories, events, {
|
|
63
63
|
profiles,
|
|
64
|
-
|
|
64
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
65
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
66
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
65
67
|
identityMap: buildIdentityMap(profiles),
|
|
66
68
|
runSql,
|
|
67
69
|
});
|
|
@@ -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 './streaming.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-streaming';
|
|
@@ -60,7 +60,9 @@ const runSql = async (sql) => {
|
|
|
60
60
|
// campaign cohort without needing hashFloat in SQL.
|
|
61
61
|
const results = await evaluateStories(stories, events, {
|
|
62
62
|
profiles,
|
|
63
|
-
|
|
63
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
64
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
65
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
64
66
|
identityMap: buildIdentityMap(profiles),
|
|
65
67
|
runSql,
|
|
66
68
|
});
|
|
@@ -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 './support-desk.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-support-desk';
|
|
@@ -60,7 +60,9 @@ const runSql = async (sql) => {
|
|
|
60
60
|
// > 30 min) and split by role without needing hashFloat.
|
|
61
61
|
const results = await evaluateStories(stories, events, {
|
|
62
62
|
profiles,
|
|
63
|
-
|
|
63
|
+
// funnel defaults (conversionWindowDays, order) resolve on the VALIDATED
|
|
64
|
+
// config — the dungeon was not run in this process, so validate here.
|
|
65
|
+
funnels: validateDungeonConfig({ ...config, token: '' }).funnels,
|
|
64
66
|
identityMap: buildIdentityMap(profiles),
|
|
65
67
|
runSql,
|
|
66
68
|
});
|
|
@@ -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 './travel.js';
|
|
22
22
|
|
|
23
23
|
const PREFIX = process.argv[2] || 'verify-travel';
|
|
@@ -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 windows (72h Onboarding; 48h Search to Book
|
|
56
|
-
// generative window × the 1.25 budget/leisure stretch = 60h, covering the
|
|
57
|
-
// stretched support), so no 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
|
});
|
package/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { extractComments } from './lib/core/extract-comments.js';
|
|
|
20
20
|
|
|
21
21
|
// Orchestrators
|
|
22
22
|
import { userLoop } from './lib/orchestrators/user-loop.js';
|
|
23
|
-
import { sendToMixpanel, collectWrittenFiles } from './lib/orchestrators/mixpanel-sender.js';
|
|
23
|
+
import { sendToMixpanel, collectWrittenFiles, releaseConnections } from './lib/orchestrators/mixpanel-sender.js';
|
|
24
24
|
// Generators
|
|
25
25
|
import { makeAdSpend } from './lib/generators/adspend.js';
|
|
26
26
|
import { makeMirror } from './lib/generators/mirror.js';
|
|
@@ -291,6 +291,12 @@ async function runDungeon(config) {
|
|
|
291
291
|
eventCount: context.getStoredEventCount(),
|
|
292
292
|
userCount: context.getUserCount(),
|
|
293
293
|
profilesPushed,
|
|
294
|
+
// v1.6.2: the enriched config this run actually used. `validateDungeonConfig`
|
|
295
|
+
// no longer writes back to the caller's object, so consumers that need
|
|
296
|
+
// resolved values (`funnels[].conversionWindowDays`, `events[].isStrictEvent`,
|
|
297
|
+
// the resolved dataset window) read them here. `verifyDungeon` depends on this.
|
|
298
|
+
// Credentials are stripped — a Result is a thing hosts log.
|
|
299
|
+
validatedConfig: redactCredentials(validatedConfig),
|
|
294
300
|
...(progressSummary.updates > 0 || progressSummary.errors > 0 ? { progress: progressSummary } : {})
|
|
295
301
|
};
|
|
296
302
|
|
|
@@ -560,6 +566,26 @@ async function flushStorageToDisk(storage, config) {
|
|
|
560
566
|
}
|
|
561
567
|
}
|
|
562
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Shallow copy of the validated config with every credential removed, for the
|
|
571
|
+
* `validatedConfig` field on a Result.
|
|
572
|
+
*
|
|
573
|
+
* A Result is a thing hosts log, serialize, and attach to CI artifacts. Before
|
|
574
|
+
* v1.6.2 it carried no credentials at all, and surfacing the resolved config
|
|
575
|
+
* shouldn't quietly change that: the run object holds `token`, `serviceAccount`,
|
|
576
|
+
* `serviceSecret` and `projectId` both flattened and under `credentials`.
|
|
577
|
+
* @param {import('./types').Dungeon} validated
|
|
578
|
+
* @returns {import('./types').Dungeon}
|
|
579
|
+
*/
|
|
580
|
+
function redactCredentials(validated) {
|
|
581
|
+
if (!validated || typeof validated !== 'object') return validated;
|
|
582
|
+
const safe = { ...validated };
|
|
583
|
+
for (const key of ['token', 'serviceAccount', 'serviceSecret', 'projectId', 'credentials']) {
|
|
584
|
+
delete safe[key];
|
|
585
|
+
}
|
|
586
|
+
return safe;
|
|
587
|
+
}
|
|
588
|
+
|
|
563
589
|
/**
|
|
564
590
|
* Extract file information from storage containers
|
|
565
591
|
* @param {import('./types').Storage} storage - Storage object
|
|
@@ -604,4 +630,7 @@ function extractStorageData(storage) {
|
|
|
604
630
|
// ES Module exports
|
|
605
631
|
export default DUNGEON_MASTER;
|
|
606
632
|
export { parseJSONDungeon, validateDungeonShape, loadFromFile, loadFromText, dungeonToJSON, extractComments };
|
|
633
|
+
// v1.6.2: pool teardown is automatic when the last import settles; exported for hosts
|
|
634
|
+
// that drive `mixpanel-import` themselves and want the same cleanup.
|
|
635
|
+
export { releaseConnections };
|
|
607
636
|
|
|
@@ -407,13 +407,31 @@ function resolveDevicesPerUser(config) {
|
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
/**
|
|
410
|
-
* Validates and enriches a dungeon configuration object
|
|
410
|
+
* Validates and enriches a dungeon configuration object.
|
|
411
|
+
*
|
|
412
|
+
* **Does not mutate its input** (as of v1.6.2). Enrichment lands only on the
|
|
413
|
+
* returned object. Callers that need the enriched `events` / `funnels` must read
|
|
414
|
+
* the return value — reading them back off the object they passed in worked
|
|
415
|
+
* before v1.6.2 and no longer does.
|
|
416
|
+
*
|
|
411
417
|
* @param {Partial<Dungeon>} config - Raw configuration object
|
|
412
418
|
* @returns {Dungeon} Validated and enriched configuration
|
|
413
419
|
*/
|
|
414
420
|
export function validateDungeonConfig(config) {
|
|
415
421
|
const chance = u.getChance();
|
|
416
422
|
|
|
423
|
+
// v1.6.2: own our input. Enrichment below writes through to the caller's
|
|
424
|
+
// nested objects otherwise — `isStrictEvent` onto `events[i]`,
|
|
425
|
+
// `conversionWindowDays` / `_experiment` onto `funnels[j]`. For a dungeon
|
|
426
|
+
// loaded from a file those arrays belong to the ESM module cache, so a second
|
|
427
|
+
// run in the same process was handed a config already enriched by the first:
|
|
428
|
+
// every event pre-promoted to strict, the catch-all funnel swept nothing, and
|
|
429
|
+
// event volume collapsed. Enrichment is also not idempotent — re-validating an
|
|
430
|
+
// already-validated config rebuilds the funnel set incorrectly.
|
|
431
|
+
// deepClone keeps functions by reference, so `hook`, `onProgress` and
|
|
432
|
+
// chance-bound prop thunks survive.
|
|
433
|
+
config = u.deepClone(config);
|
|
434
|
+
|
|
417
435
|
// v1.5.1 (TODO #8): merge `switches` / `identity` / `credentials`
|
|
418
436
|
// sub-objects into top-level keys. New shape is preferred; old top-level
|
|
419
437
|
// keys still honored for back-compat with a verbose-gated warning. The
|
|
@@ -10,14 +10,64 @@ import { comma, rm } from "ak-tools";
|
|
|
10
10
|
import * as u from "../utils/utils.js";
|
|
11
11
|
import mp from "mixpanel-import";
|
|
12
12
|
|
|
13
|
+
export { collectWrittenFiles };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Count of `sendToMixpanel` calls currently in flight in this process.
|
|
17
|
+
*
|
|
18
|
+
* `mp.destroy()` tears down mixpanel-import's PROCESS-GLOBAL undici pools, so a
|
|
19
|
+
* run that finishes while another is still importing would close the sockets out
|
|
20
|
+
* from under it — surfacing as `UND_ERR_CLOSED`, which mixpanel-import does not
|
|
21
|
+
* retry. Only the last run out turns off the lights.
|
|
22
|
+
*/
|
|
23
|
+
let _inFlightImports = 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Release mixpanel-import's shared undici connection pools, if no import is still
|
|
27
|
+
* running. dungeon-master is a library — a host process that runs occasional
|
|
28
|
+
* imports shouldn't hold ingest sockets open in between. Pools are lazily
|
|
29
|
+
* re-created, so a later run is unaffected.
|
|
30
|
+
*
|
|
31
|
+
* Called automatically when the last in-flight import settles; exported so a host
|
|
32
|
+
* that drives `mixpanel-import` directly can force the same cleanup. Note the
|
|
33
|
+
* refcount only sees imports that go through `sendToMixpanel` — a host running its
|
|
34
|
+
* own `mixpanel-import` job concurrently with a dungeon run can still have its
|
|
35
|
+
* pools closed underneath it.
|
|
36
|
+
* @returns {Promise<boolean>} true if pools were torn down.
|
|
37
|
+
*/
|
|
38
|
+
export async function releaseConnections() {
|
|
39
|
+
// `destroy` only exists on mixpanel-import >= 3.5.1.
|
|
40
|
+
if (_inFlightImports > 0 || typeof mp.destroy !== 'function') return false;
|
|
41
|
+
try {
|
|
42
|
+
await mp.destroy();
|
|
43
|
+
return true;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Never fail a completed import over socket cleanup.
|
|
46
|
+
log(` !! connection pool cleanup failed: ${err.message}\n`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
13
51
|
/**
|
|
14
52
|
* Sends the data to Mixpanel
|
|
15
53
|
* @param {Context} context - Context object containing config, storage, etc.
|
|
16
54
|
* @returns {Promise<Object>} Import results for all data types
|
|
17
55
|
*/
|
|
18
|
-
export { collectWrittenFiles };
|
|
19
|
-
|
|
20
56
|
export async function sendToMixpanel(context) {
|
|
57
|
+
_inFlightImports++;
|
|
58
|
+
try {
|
|
59
|
+
return await _sendToMixpanel(context);
|
|
60
|
+
} finally {
|
|
61
|
+
_inFlightImports--;
|
|
62
|
+
await releaseConnections();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {Context} context
|
|
68
|
+
* @returns {Promise<Object>}
|
|
69
|
+
*/
|
|
70
|
+
async function _sendToMixpanel(context) {
|
|
21
71
|
const { config, storage } = context;
|
|
22
72
|
const {
|
|
23
73
|
adSpendData,
|
package/lib/verify/index.js
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
export { emulateBreakdown } from './emulate-breakdown.js';
|
|
13
13
|
export { verifyDungeon } from './verify-dungeon.js';
|
|
14
|
+
// v1.6.2: exposed on the verify surface so a standalone verify script — one that
|
|
15
|
+
// reads shards off disk instead of running the dungeon — can resolve funnel
|
|
16
|
+
// defaults (`conversionWindowDays`, `order`) before handing funnels to
|
|
17
|
+
// `evaluateStories` / `applyFunnelDefaults`. Use the RETURN value; it does not
|
|
18
|
+
// enrich the config you pass in.
|
|
19
|
+
export { validateDungeonConfig } from '../core/config-validator.js';
|
|
14
20
|
export { deriveExpectedSchema, validateSchema } from './schema-validator.js';
|
|
15
21
|
export {
|
|
16
22
|
evaluateFunnel,
|
|
@@ -59,9 +59,10 @@ function findMatchingFunnel(funnels, steps) {
|
|
|
59
59
|
* args object — the input is not mutated. Extracted from `verifyDungeon` in
|
|
60
60
|
* v1.6 so the story runner (P3.3) reuses the exact same threading.
|
|
61
61
|
* @param {Object} breakdownArgs - Args destined for `emulateBreakdown`.
|
|
62
|
-
* @param {Array<Object>} funnels - VALIDATED dungeon funnels
|
|
63
|
-
*
|
|
64
|
-
* `conversionWindowDays` / `order
|
|
62
|
+
* @param {Array<Object>} funnels - VALIDATED dungeon funnels — i.e. the funnels off
|
|
63
|
+
* `validateDungeonConfig`'s RETURN value (post-1.6.2 it no longer enriches the
|
|
64
|
+
* caller's object), which carry resolved `conversionWindowDays` / `order`. A run
|
|
65
|
+
* result exposes them as `result.validatedConfig.funnels`.
|
|
65
66
|
* @param {Array<Object>} [profiles] - User profiles, threaded into `timeToConvert`.
|
|
66
67
|
* @returns {Object}
|
|
67
68
|
*/
|
|
@@ -113,20 +114,44 @@ export function applyFunnelDefaults(breakdownArgs, funnels, profiles) {
|
|
|
113
114
|
/**
|
|
114
115
|
* @param {Object} config - Dungeon config (or path; passed straight to DUNGEON_MASTER).
|
|
115
116
|
* @param {VerifyCheck[]} checks
|
|
116
|
-
* @
|
|
117
|
+
* @param {Object} [overrides] - v1.6.2: merged into the dungeon before it runs, same as
|
|
118
|
+
* `DUNGEON_MASTER`'s second argument. Lets CI verify a production-scale dungeon at a
|
|
119
|
+
* small `numUsers` / `numEvents` without editing it.
|
|
120
|
+
* @returns {Promise<{ pass: boolean, results: Array<{ name: string, pass: boolean, detail?: string, rows?: Array<Object> }>, schemaReport: Object, validatedConfig: Object }>}
|
|
117
121
|
*/
|
|
118
|
-
export async function verifyDungeon(config, checks) {
|
|
122
|
+
export async function verifyDungeon(config, checks, overrides) {
|
|
119
123
|
if (!checks || !checks.length) throw new Error('verifyDungeon: at least one check required');
|
|
120
|
-
let result = await DUNGEON_MASTER(config);
|
|
121
|
-
if (Array.isArray(result))
|
|
124
|
+
let result = await DUNGEON_MASTER(config, overrides);
|
|
125
|
+
if (Array.isArray(result)) {
|
|
126
|
+
// v1.6.2: an array input runs N dungeons but checks are written against ONE
|
|
127
|
+
// schema, and we used to silently verify `result[0]` and throw the rest away —
|
|
128
|
+
// a green report for dungeons that were never looked at. Call verifyDungeon
|
|
129
|
+
// per dungeon instead.
|
|
130
|
+
if (result.length !== 1) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`verifyDungeon: got ${result.length} dungeons, but checks apply to one. ` +
|
|
133
|
+
`Call verifyDungeon once per dungeon.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
result = result[0];
|
|
137
|
+
}
|
|
122
138
|
const events = Array.isArray(result.eventData) ? result.eventData : Array.from(result.eventData);
|
|
123
139
|
const profiles = Array.isArray(result.userProfilesData) ? result.userProfilesData : Array.from(result.userProfilesData);
|
|
124
|
-
|
|
140
|
+
// v1.6.2: schema-check against the config the run actually used. `config` may be a
|
|
141
|
+
// PATH STRING (no fields at all → every property reads as unexpected), and even for
|
|
142
|
+
// an object input a v1.5.1 dungeon keeps `hasAndroidDevices` / `hasBrowser` under
|
|
143
|
+
// `switches`, which `deriveExpectedSchema` only reads once flattened.
|
|
144
|
+
const schemaReport = validateSchema(events, result.validatedConfig || config);
|
|
125
145
|
const ctx = { events, profiles, schemaReport };
|
|
126
146
|
const results = [];
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
147
|
+
// v1.6.2: read the funnels off the run's `validatedConfig` — `conversionWindowDays`
|
|
148
|
+
// and the resolved `order` live there. Before 1.6.2 we read them back off the
|
|
149
|
+
// caller's own `config`, relying on `validateDungeonConfig` enriching in place;
|
|
150
|
+
// that also meant a path/JSON input (no `.funnels` on the string) silently got an
|
|
151
|
+
// empty funnel list and thus an unbounded conversion window. Both are fixed here.
|
|
152
|
+
const validatedFunnels = Array.isArray(result?.validatedConfig?.funnels)
|
|
153
|
+
? result.validatedConfig.funnels
|
|
154
|
+
: (config && Array.isArray(config.funnels)) ? config.funnels : [];
|
|
130
155
|
for (const check of checks) {
|
|
131
156
|
try {
|
|
132
157
|
const breakdownArgs = applyFunnelDefaults(check.breakdown, validatedFunnels, profiles);
|
|
@@ -138,5 +163,7 @@ export async function verifyDungeon(config, checks) {
|
|
|
138
163
|
}
|
|
139
164
|
}
|
|
140
165
|
const pass = results.every(r => r.pass) && schemaReport.pass;
|
|
141
|
-
|
|
166
|
+
// v1.6.2: hand back the enriched config the run used, so callers can read
|
|
167
|
+
// resolved funnel/event values without re-validating.
|
|
168
|
+
return { pass, results, schemaReport, validatedConfig: result.validatedConfig };
|
|
142
169
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ak--47/dungeon-master",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "generate fancy datasets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"HOOKS.md"
|
|
34
34
|
],
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
36
|
+
"node": ">=20.20.0"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"dotenv": "^16.4.5",
|
|
88
88
|
"hyparquet-writer": "^0.6.1",
|
|
89
89
|
"mixpanel": "^0.18.0",
|
|
90
|
-
"mixpanel-import": "^3.
|
|
90
|
+
"mixpanel-import": "^3.5.1",
|
|
91
91
|
"p-limit": "^3.1.0",
|
|
92
92
|
"pino": "^9.0.0",
|
|
93
93
|
"pino-pretty": "^11.0.0",
|
|
@@ -108,4 +108,4 @@
|
|
|
108
108
|
"tmp/"
|
|
109
109
|
]
|
|
110
110
|
}
|
|
111
|
-
}
|
|
111
|
+
}
|
|
@@ -176,7 +176,8 @@ if (inMemory) {
|
|
|
176
176
|
|
|
177
177
|
// Funnel auto-threading reads VALIDATED funnel fields (conversionWindowDays,
|
|
178
178
|
// order). The dungeon was not run in this process, so validate the config
|
|
179
|
-
// here —
|
|
179
|
+
// here and use the RETURN value — as of v1.6.2 validateDungeonConfig does not
|
|
180
|
+
// enrich the object you hand it.
|
|
180
181
|
const validated = validateDungeonConfig({ ...config, token: '' });
|
|
181
182
|
const identityMap = buildIdentityMap(profiles);
|
|
182
183
|
|
package/types.d.ts
CHANGED
|
@@ -1324,6 +1324,21 @@ export type Result = {
|
|
|
1324
1324
|
* so `userProfilesData.length - profilesPushed` = dropped profile count.
|
|
1325
1325
|
*/
|
|
1326
1326
|
profilesPushed?: number;
|
|
1327
|
+
/**
|
|
1328
|
+
* v1.6.2: the enriched config this run actually used. `validateDungeonConfig`
|
|
1329
|
+
* no longer writes back to the object you passed in, so resolved values —
|
|
1330
|
+
* `funnels[].conversionWindowDays`, `events[].isStrictEvent`, the resolved
|
|
1331
|
+
* dataset window — must be read here rather than off your own config.
|
|
1332
|
+
*
|
|
1333
|
+
* READ-ONLY. Do NOT feed this back into `DUNGEON_MASTER` — validation is not
|
|
1334
|
+
* idempotent (each pass appends to the funnel set, eventually producing an
|
|
1335
|
+
* empty `sequence`), and the object carries engine scratch fields. Re-run the
|
|
1336
|
+
* ORIGINAL config instead.
|
|
1337
|
+
*
|
|
1338
|
+
* Credentials (`token`, `serviceAccount`, `serviceSecret`, `projectId`,
|
|
1339
|
+
* `credentials`) are stripped — a Result is a thing hosts log.
|
|
1340
|
+
*/
|
|
1341
|
+
validatedConfig?: Dungeon;
|
|
1327
1342
|
/** Progress callback summary. Only present when `onProgress` was provided. */
|
|
1328
1343
|
progress?: ProgressSummary;
|
|
1329
1344
|
};
|