@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
|
@@ -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,
|
|
@@ -750,6 +750,33 @@ export async function userLoop(context) {
|
|
|
750
750
|
});
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
+
// v1.6.3: guarantee unique `insert_id` across the user's final stream.
|
|
754
|
+
//
|
|
755
|
+
// Hooks clone events by spreading an existing one — the documented way
|
|
756
|
+
// to inject — and a spread copies `insert_id` along with everything
|
|
757
|
+
// else. Mixpanel deduplicates on `insert_id` at ingest, so those clones
|
|
758
|
+
// are accepted, reported as successful, and then silently dropped: the
|
|
759
|
+
// engineered volume never appears in the project. Nothing downstream
|
|
760
|
+
// catches it, because local verification never inspects `insert_id`.
|
|
761
|
+
//
|
|
762
|
+
// The hook-helper clone atoms stamp fresh ids themselves, but hooks are
|
|
763
|
+
// free to hand-roll a spread (and many shipped dungeons do), so the
|
|
764
|
+
// only reliable place to enforce this is here, over the finished set.
|
|
765
|
+
// A legitimate stream never contains two events with the same id, so
|
|
766
|
+
// any collision at this point is a clone that needs its own id.
|
|
767
|
+
// `> 0`, not `> 1`: a lone event cannot collide, but it can still be
|
|
768
|
+
// missing an id if a hook replaced it with a constructed object.
|
|
769
|
+
if (usersEvents.length > 0) {
|
|
770
|
+
const seenInsertIds = new Set();
|
|
771
|
+
for (const ev of usersEvents) {
|
|
772
|
+
if (!ev) continue;
|
|
773
|
+
if (!ev.insert_id || seenInsertIds.has(ev.insert_id)) {
|
|
774
|
+
ev.insert_id = randomUUID();
|
|
775
|
+
}
|
|
776
|
+
seenInsertIds.add(ev.insert_id);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
753
780
|
// Defensive guard: drop any events whose timestamp landed past the
|
|
754
781
|
// configured dataset end. Hooks that duplicate events with time offsets
|
|
755
782
|
// (weekend surges, viral spreads) can leak a few past the boundary.
|
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.3",
|
|
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.6.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
|
};
|