@ak--47/dungeon-master 1.6.1 → 1.6.3
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/create-project/provision.mjs +16 -3
- package/.claude/skills/headless-build/SKILL.md +254 -0
- package/.claude/skills/powertools/SKILL.md +21 -1
- package/CHANGELOG.md +152 -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/hook-helpers/_internal.js +45 -0
- package/lib/hook-helpers/inject.js +7 -7
- package/lib/hook-helpers/mutate.js +8 -6
- package/lib/hook-helpers/shape.js +4 -4
- package/lib/hook-patterns/frequency-by-frequency.js +2 -2
- package/lib/orchestrators/mixpanel-sender.js +52 -2
- package/lib/orchestrators/user-loop.js +27 -0
- 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
|
@@ -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
|
|
@@ -1,3 +1,48 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Give a cloned event its own `insert_id`.
|
|
5
|
+
*
|
|
6
|
+
* Clones MUST NOT inherit their template's id, and stripping the id is not
|
|
7
|
+
* enough either: mixpanel-import runs with `fixData: true`, which synthesizes a
|
|
8
|
+
* missing id by content-hashing the record — so two clones that differ only in
|
|
9
|
+
* ways the hash ignores collapse into one on ingest. Mixpanel then dedupes them
|
|
10
|
+
* server-side and the engineered volume silently disappears from the project
|
|
11
|
+
* while local verification (which never looks at `insert_id`) still passes.
|
|
12
|
+
*
|
|
13
|
+
* `insert_id` is already assigned via `randomUUID()` in the event generator
|
|
14
|
+
* (`lib/generators/events.js`), so it sits outside the seeded
|
|
15
|
+
* byte-identical-output guarantee; using it here keeps clone ids consistent with
|
|
16
|
+
* engine-generated ones.
|
|
17
|
+
*
|
|
18
|
+
* @template {{insert_id?: string}} T
|
|
19
|
+
* @param {T} clone
|
|
20
|
+
* @returns {T} the same object, with a fresh `insert_id`
|
|
21
|
+
*/
|
|
22
|
+
export function stampFreshInsertId(clone) {
|
|
23
|
+
clone.insert_id = randomUUID();
|
|
24
|
+
return clone;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Spread a template into a new event, merge `overrides`, and give it a fresh
|
|
29
|
+
* `insert_id` — unless the caller pinned one explicitly.
|
|
30
|
+
*
|
|
31
|
+
* Every clone site should go through this so the override contract is uniform:
|
|
32
|
+
* a caller that passes `insert_id` in `overrides` keeps it, and everyone else
|
|
33
|
+
* gets a unique id rather than a duplicate of the template's.
|
|
34
|
+
*
|
|
35
|
+
* @template {Record<string, any>} T
|
|
36
|
+
* @param {T} template - Event to clone from.
|
|
37
|
+
* @param {Record<string, any>} [overrides] - Fields to merge on top.
|
|
38
|
+
* @returns {T} the new event
|
|
39
|
+
*/
|
|
40
|
+
export function cloneWithFreshId(template, overrides = {}) {
|
|
41
|
+
const clone = /** @type {T} */ ({ ...template, ...overrides });
|
|
42
|
+
if (!(overrides && 'insert_id' in overrides)) stampFreshInsertId(clone);
|
|
43
|
+
return clone;
|
|
44
|
+
}
|
|
45
|
+
|
|
1
46
|
export function toMs(t) {
|
|
2
47
|
if (typeof t === 'number') return t > 1e12 ? t : t > 1e9 ? t * 1000 : t;
|
|
3
48
|
return Date.parse(t);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { getChance } from '../utils/utils.js';
|
|
13
|
-
import { toMs, writeTime } from './_internal.js';
|
|
13
|
+
import { toMs, writeTime, stampFreshInsertId, cloneWithFreshId } from './_internal.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Splice a cloned event into `events` immediately after `sourceEvent`. Time is
|
|
@@ -29,7 +29,7 @@ export function injectAfterEvent(events, sourceEvent, templateEvent, gapMs, over
|
|
|
29
29
|
if (!events || !sourceEvent || !templateEvent) return null;
|
|
30
30
|
const baseT = toMs(sourceEvent.time);
|
|
31
31
|
if (!Number.isFinite(baseT)) return null;
|
|
32
|
-
const newEv =
|
|
32
|
+
const newEv = cloneWithFreshId(templateEvent, overrides);
|
|
33
33
|
writeTime(newEv, baseT + gapMs);
|
|
34
34
|
const idx = events.indexOf(sourceEvent);
|
|
35
35
|
if (idx >= 0) events.splice(idx + 1, 0, newEv);
|
|
@@ -60,7 +60,7 @@ export function injectBetween(events, eventA, eventB, templateEvent, overrides =
|
|
|
60
60
|
const aT = toMs(a.time);
|
|
61
61
|
const bT = toMs(b.time);
|
|
62
62
|
if (!Number.isFinite(aT) || !Number.isFinite(bT)) return null;
|
|
63
|
-
const newEv =
|
|
63
|
+
const newEv = cloneWithFreshId(templateEvent, overrides);
|
|
64
64
|
writeTime(newEv, (aT + bT) / 2);
|
|
65
65
|
const bIdxOrig = events.indexOf(b);
|
|
66
66
|
if (bIdxOrig >= 0) events.splice(bIdxOrig, 0, newEv);
|
|
@@ -89,7 +89,7 @@ export function injectBurst(events, templateEvent, count, anchorTime, spreadMs,
|
|
|
89
89
|
const created = [];
|
|
90
90
|
for (let i = 0; i < count; i++) {
|
|
91
91
|
const offset = chance.floating({ min: -spreadMs, max: spreadMs });
|
|
92
|
-
const newEv =
|
|
92
|
+
const newEv = cloneWithFreshId(templateEvent, overrides);
|
|
93
93
|
writeTime(newEv, anchorMs + offset);
|
|
94
94
|
events.push(newEv);
|
|
95
95
|
created.push(newEv);
|
|
@@ -119,7 +119,8 @@ const DAY_MS = 86400000;
|
|
|
119
119
|
* 5. Find a template event of `eventName`; if none exist for this user,
|
|
120
120
|
* return unchanged (we honor schema-first: don't fabricate events).
|
|
121
121
|
* 6. Clone the template onto each picked day at a random hour within the
|
|
122
|
-
* day. `insert_id`
|
|
122
|
+
* day. Each clone gets a FRESH `insert_id` (inheriting or omitting it lets
|
|
123
|
+
* Mixpanel dedupe the clones away on ingest).
|
|
123
124
|
* 7. Append clones to the array. Caller's downstream sort handles ordering.
|
|
124
125
|
*
|
|
125
126
|
* @param {Object[]} events - Full user event array (from `everything` hook).
|
|
@@ -183,9 +184,8 @@ export function injectOnNewDays(events, eventName, targetDays, options = {}) {
|
|
|
183
184
|
const lo = Math.max(dayStart, minMs);
|
|
184
185
|
const hi = Math.min(dayEnd, maxMs);
|
|
185
186
|
const newMs = lo >= hi ? lo : chance.integer({ min: lo, max: hi });
|
|
186
|
-
const clone =
|
|
187
|
+
const clone = cloneWithFreshId(template, overrides);
|
|
187
188
|
writeTime(clone, newMs);
|
|
188
|
-
delete clone.insert_id;
|
|
189
189
|
events.push(clone);
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `Math.random()` — so dungeon runs stay reproducible.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { toMs } from './_internal.js';
|
|
11
|
+
import { toMs, stampFreshInsertId, cloneWithFreshId } from './_internal.js';
|
|
12
12
|
|
|
13
13
|
import { getChance } from '../utils/utils.js';
|
|
14
14
|
|
|
@@ -25,7 +25,9 @@ import { getChance } from '../utils/utils.js';
|
|
|
25
25
|
*/
|
|
26
26
|
export function cloneEvent(template, overrides = {}) {
|
|
27
27
|
if (!template) throw new Error('cloneEvent: template is required');
|
|
28
|
-
|
|
28
|
+
// A clone that keeps its template's insert_id is deduped away by Mixpanel on
|
|
29
|
+
// ingest. Callers may still pin one explicitly via overrides.
|
|
30
|
+
return /** @type {T} */ (cloneWithFreshId(template, overrides));
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -57,8 +59,9 @@ export function dropEventsWhere(events, predicate) {
|
|
|
57
59
|
* integer = -dropped.
|
|
58
60
|
* - factor === 1 or no matches: no-op, returns 0.
|
|
59
61
|
*
|
|
60
|
-
* Note:
|
|
61
|
-
*
|
|
62
|
+
* Note: cloned events get a FRESH `insert_id` — they must not inherit the
|
|
63
|
+
* source's, and leaving it blank is not safe either (the importer content-hashes
|
|
64
|
+
* missing ids, so identical clones collide and Mixpanel dedupes them away).
|
|
62
65
|
*
|
|
63
66
|
* @param {Array<{event:string,time:string|number,insert_id?:string}>} events
|
|
64
67
|
* @param {string} eventName
|
|
@@ -78,8 +81,7 @@ export function scaleEventCount(events, eventName, factor) {
|
|
|
78
81
|
const newTime = Number.isFinite(baseMs)
|
|
79
82
|
? new Date(baseMs + (i + 1) * 1000).toISOString()
|
|
80
83
|
: src.time;
|
|
81
|
-
const clone = { ...src, time: newTime };
|
|
82
|
-
delete clone.insert_id;
|
|
84
|
+
const clone = stampFreshInsertId({ ...src, time: newTime });
|
|
83
85
|
events.push(clone);
|
|
84
86
|
added++;
|
|
85
87
|
}
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* (HOOKS.md §2.16), a biased Flows path branch (§2.17), and a deterministic
|
|
7
7
|
* session cadence (§2.13). All three are `everything`-hook-only — they need
|
|
8
8
|
* the whole stream — and obey the schema-first rules: clones only (spread
|
|
9
|
-
* from the user's own events, `insert_id`
|
|
9
|
+
* from the user's own events, each given a fresh `insert_id`), no fabricated events,
|
|
10
10
|
* seeded `chance` for all randomness. Timestamp rewrites are safe because
|
|
11
11
|
* the engine re-derives `session_id` on the final event set (v1.6 P2.1).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getChance } from '../utils/utils.js';
|
|
15
|
-
import { toMs, writeTime } from './_internal.js';
|
|
15
|
+
import { toMs, writeTime, stampFreshInsertId } from './_internal.js';
|
|
16
16
|
import { hashFloat } from './cohort.js';
|
|
17
17
|
|
|
18
18
|
const DAY_MS = 86400000;
|
|
@@ -100,7 +100,7 @@ export function applyLifecycleWave(events, uid, opts) {
|
|
|
100
100
|
for (let i = 0; i < resurrectBurst; i++) {
|
|
101
101
|
const clone = { ...template };
|
|
102
102
|
writeTime(clone, t);
|
|
103
|
-
|
|
103
|
+
stampFreshInsertId(clone);
|
|
104
104
|
if (!clone.user_id && uid) clone.user_id = uid;
|
|
105
105
|
kept.push(clone);
|
|
106
106
|
t += chance.integer({ min: MIN_MS, max: 10 * MIN_MS });
|
|
@@ -168,7 +168,7 @@ export function applyPathBias(events, uid, opts) {
|
|
|
168
168
|
t += chance.integer({ min: lo, max: hi }) * 1000;
|
|
169
169
|
const clone = { ...tpl };
|
|
170
170
|
writeTime(clone, t);
|
|
171
|
-
|
|
171
|
+
stampFreshInsertId(clone);
|
|
172
172
|
if (!clone.user_id && uid) clone.user_id = uid;
|
|
173
173
|
events.push(clone);
|
|
174
174
|
}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* - Operates on the user's full event stream — call from the `everything` hook.
|
|
17
17
|
* - Does NOT add new properties; uses existing event names defined in the dungeon
|
|
18
18
|
* schema.
|
|
19
|
-
* - Cloned events
|
|
20
|
-
*
|
|
19
|
+
* - Cloned events get a fresh `insert_id` (mutate.scaleEventCount handles that),
|
|
20
|
+
* so Mixpanel does not dedupe them away on ingest.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { binUsersByEventCount } from '../hook-helpers/cohort.js';
|