@ak--47/dungeon-master 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/vertical/ai-platform.js +45 -52
- package/dungeons/vertical/community.js +11 -8
- package/dungeons/vertical/crypto.js +25 -24
- package/dungeons/vertical/dating.js +56 -48
- package/dungeons/vertical/devtools.js +25 -18
- package/dungeons/vertical/ecommerce.js +42 -38
- package/dungeons/vertical/education.js +24 -9
- package/dungeons/vertical/fintech.js +13 -8
- package/dungeons/vertical/fitness.js +73 -122
- package/dungeons/vertical/food-delivery.js +18 -19
- package/dungeons/vertical/gaming.js +19 -20
- package/dungeons/vertical/healthcare.js +11 -8
- package/dungeons/vertical/insurance-application.js +6 -3
- package/dungeons/vertical/logistics.js +15 -9
- package/dungeons/vertical/marketplace.js +36 -27
- package/dungeons/vertical/media.js +27 -25
- package/dungeons/vertical/real-estate.js +18 -7
- package/dungeons/vertical/sass.js +84 -68
- package/dungeons/vertical/social.js +46 -47
- package/dungeons/vertical/travel.js +8 -5
- package/lib/core/config-validator.js +136 -157
- package/lib/generators/events.js +49 -93
- package/lib/generators/funnels.js +202 -91
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +119 -269
- package/lib/utils/utils.js +29 -16
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/types.d.ts +397 -211
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixpanel breakdown emulator.
|
|
3
|
+
*
|
|
4
|
+
* Best-effort approximation of the table shapes Mixpanel produces for the five
|
|
5
|
+
* analyses the Phase 4 hook patterns target. Used by `verify-hooks` to assert
|
|
6
|
+
* that engineered patterns actually produce the expected ratios in the data, AND
|
|
7
|
+
* by consumers who want to validate dungeons against expected business shapes
|
|
8
|
+
* outside of Mixpanel.
|
|
9
|
+
*
|
|
10
|
+
* Reference: Mixpanel Insights / Funnels / Flows reports, as of 2026-05.
|
|
11
|
+
*
|
|
12
|
+
* Caveats:
|
|
13
|
+
* - Mixpanel applies its own per-account UTC offset and time-bucketing rules. This
|
|
14
|
+
* emulator uses raw event times unless the breakdown explicitly involves a window.
|
|
15
|
+
* - Mixpanel "users" are typically distinct profiles with at least one event in
|
|
16
|
+
* the date range; this emulator counts unique `user_id` (falling back to
|
|
17
|
+
* `distinct_id`) found across the events array.
|
|
18
|
+
* - This is not bit-exact — it's the *shape* check Phase 4 verification needs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} EmulateOptions
|
|
23
|
+
* @property {'frequencyByFrequency'|'funnelFrequency'|'aggregatePerUser'|'timeToConvert'|'attributedBy'} type
|
|
24
|
+
*
|
|
25
|
+
* @property {string} [metricEvent]
|
|
26
|
+
* @property {string} [breakdownByFrequencyOf]
|
|
27
|
+
* @property {boolean} [perUser]
|
|
28
|
+
*
|
|
29
|
+
* @property {string[]} [steps]
|
|
30
|
+
*
|
|
31
|
+
* @property {string} [event]
|
|
32
|
+
* @property {string} [property]
|
|
33
|
+
* @property {'avg'|'sum'|'count'|'max'|'min'} [agg]
|
|
34
|
+
*
|
|
35
|
+
* @property {string} [fromEvent]
|
|
36
|
+
* @property {string} [toEvent]
|
|
37
|
+
* @property {string} [breakdownByUserProperty]
|
|
38
|
+
* @property {Array<Object>} [profiles]
|
|
39
|
+
*
|
|
40
|
+
* @property {string} [conversionEvent]
|
|
41
|
+
* @property {string} [attributionEvent]
|
|
42
|
+
* @property {string} [attributionProperty]
|
|
43
|
+
* @property {'firstTouch'|'lastTouch'} [model]
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run a Mixpanel breakdown emulation against an events array.
|
|
48
|
+
* Routes to the type-specific implementation based on `config.type`.
|
|
49
|
+
*
|
|
50
|
+
* @param {Array<Object>} events
|
|
51
|
+
* @param {EmulateOptions} config
|
|
52
|
+
* @returns {Array<Object>} Breakdown table rows.
|
|
53
|
+
*/
|
|
54
|
+
export function emulateBreakdown(events, config) {
|
|
55
|
+
if (!Array.isArray(events)) throw new Error('emulateBreakdown: events must be an array');
|
|
56
|
+
if (!config || !config.type) throw new Error('emulateBreakdown: config.type is required');
|
|
57
|
+
switch (config.type) {
|
|
58
|
+
case 'frequencyByFrequency': return frequencyByFrequency(events, /** @type {*} */ (config));
|
|
59
|
+
case 'funnelFrequency': return funnelFrequency(events, /** @type {*} */ (config));
|
|
60
|
+
case 'aggregatePerUser': return aggregatePerUser(events, /** @type {*} */ (config));
|
|
61
|
+
case 'timeToConvert': return timeToConvert(events, /** @type {*} */ (config));
|
|
62
|
+
case 'attributedBy': return attributedBy(events, /** @type {*} */ (config));
|
|
63
|
+
default: throw new Error(`emulateBreakdown: unknown type "${config.type}"`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Frequency × Frequency (Insights, Frequency Distribution by per-user count of B) ──
|
|
68
|
+
|
|
69
|
+
function frequencyByFrequency(events, { metricEvent, breakdownByFrequencyOf }) {
|
|
70
|
+
if (!metricEvent || !breakdownByFrequencyOf) {
|
|
71
|
+
throw new Error('frequencyByFrequency requires metricEvent and breakdownByFrequencyOf');
|
|
72
|
+
}
|
|
73
|
+
const userMetric = new Map();
|
|
74
|
+
const userBreakdown = new Map();
|
|
75
|
+
const uids = new Set();
|
|
76
|
+
for (const ev of events) {
|
|
77
|
+
const uid = userIdOf(ev);
|
|
78
|
+
if (!uid) continue;
|
|
79
|
+
uids.add(uid);
|
|
80
|
+
if (ev.event === metricEvent) userMetric.set(uid, (userMetric.get(uid) || 0) + 1);
|
|
81
|
+
if (ev.event === breakdownByFrequencyOf) userBreakdown.set(uid, (userBreakdown.get(uid) || 0) + 1);
|
|
82
|
+
}
|
|
83
|
+
const cell = new Map(); // `${m}|${b}` → user_count
|
|
84
|
+
for (const uid of uids) {
|
|
85
|
+
const m = userMetric.get(uid) || 0;
|
|
86
|
+
const b = userBreakdown.get(uid) || 0;
|
|
87
|
+
const key = `${m}|${b}`;
|
|
88
|
+
cell.set(key, (cell.get(key) || 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
return [...cell.entries()].map(([k, count]) => {
|
|
91
|
+
const [m, b] = k.split('|').map(Number);
|
|
92
|
+
return { metric_freq: m, breakdown_freq: b, user_count: count };
|
|
93
|
+
}).sort((x, y) => x.breakdown_freq - y.breakdown_freq || x.metric_freq - y.metric_freq);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Funnel Frequency Breakdown (Funnel report broken down by per-user count of X) ──
|
|
97
|
+
|
|
98
|
+
function funnelFrequency(events, { steps, breakdownByFrequencyOf }) {
|
|
99
|
+
if (!Array.isArray(steps) || !steps.length) throw new Error('funnelFrequency requires steps[]');
|
|
100
|
+
if (!breakdownByFrequencyOf) throw new Error('funnelFrequency requires breakdownByFrequencyOf');
|
|
101
|
+
const userEvents = groupByUser(events);
|
|
102
|
+
const userBreakdown = new Map();
|
|
103
|
+
for (const [uid, evs] of userEvents) {
|
|
104
|
+
const c = evs.filter(e => e && e.event === breakdownByFrequencyOf).length;
|
|
105
|
+
userBreakdown.set(uid, c);
|
|
106
|
+
}
|
|
107
|
+
const result = [];
|
|
108
|
+
for (let s = 0; s < steps.length; s++) {
|
|
109
|
+
const stepName = steps[s];
|
|
110
|
+
const conversions = new Map(); // breakdown_freq → count
|
|
111
|
+
for (const [uid, evs] of userEvents) {
|
|
112
|
+
const sorted = sortByTime(evs);
|
|
113
|
+
let stepIdx = 0;
|
|
114
|
+
for (const ev of sorted) {
|
|
115
|
+
if (ev.event === steps[stepIdx]) stepIdx++;
|
|
116
|
+
if (stepIdx > s) break;
|
|
117
|
+
}
|
|
118
|
+
if (stepIdx > s) {
|
|
119
|
+
const b = userBreakdown.get(uid) || 0;
|
|
120
|
+
conversions.set(b, (conversions.get(b) || 0) + 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const [b, c] of conversions) {
|
|
124
|
+
result.push({ step: stepName, step_index: s, breakdown_freq: b, conversions: c, conversion_pct: 0 });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Conversion % at each step relative to its own breakdown_freq's step-0 baseline.
|
|
128
|
+
const baseline = new Map();
|
|
129
|
+
for (const r of result) {
|
|
130
|
+
if (r.step_index === 0) baseline.set(r.breakdown_freq, r.conversions);
|
|
131
|
+
}
|
|
132
|
+
for (const r of result) {
|
|
133
|
+
const denom = baseline.get(r.breakdown_freq) || 0;
|
|
134
|
+
r.conversion_pct = denom ? (r.conversions / denom) * 100 : 0;
|
|
135
|
+
}
|
|
136
|
+
return result.sort((a, b) => a.step_index - b.step_index || a.breakdown_freq - b.breakdown_freq);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Aggregate per user (Insights, sum/avg of property X by per-user count of B) ──
|
|
140
|
+
|
|
141
|
+
function aggregatePerUser(events, { event, property, agg = 'avg', breakdownByFrequencyOf }) {
|
|
142
|
+
if (!event) throw new Error('aggregatePerUser requires event');
|
|
143
|
+
if (!breakdownByFrequencyOf) throw new Error('aggregatePerUser requires breakdownByFrequencyOf');
|
|
144
|
+
if (agg !== 'count' && !property) throw new Error('aggregatePerUser requires property unless agg is "count"');
|
|
145
|
+
const userVals = new Map();
|
|
146
|
+
const userBreakdown = new Map();
|
|
147
|
+
for (const ev of events) {
|
|
148
|
+
const uid = userIdOf(ev);
|
|
149
|
+
if (!uid) continue;
|
|
150
|
+
if (ev.event === event) {
|
|
151
|
+
// `agg: 'count'` → count occurrences of the event regardless of property type.
|
|
152
|
+
// All other aggs only consider numeric property values.
|
|
153
|
+
if (agg === 'count') {
|
|
154
|
+
if (!userVals.has(uid)) userVals.set(uid, []);
|
|
155
|
+
userVals.get(uid).push(1);
|
|
156
|
+
} else if (property && typeof ev[property] === 'number') {
|
|
157
|
+
if (!userVals.has(uid)) userVals.set(uid, []);
|
|
158
|
+
userVals.get(uid).push(ev[property]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (ev.event === breakdownByFrequencyOf) userBreakdown.set(uid, (userBreakdown.get(uid) || 0) + 1);
|
|
162
|
+
}
|
|
163
|
+
const userAgg = new Map();
|
|
164
|
+
for (const [uid, vals] of userVals) userAgg.set(uid, applyAgg(vals, agg));
|
|
165
|
+
const buckets = new Map(); // breakdown_freq → [aggregates]
|
|
166
|
+
for (const [uid, v] of userAgg) {
|
|
167
|
+
const b = userBreakdown.get(uid) || 0;
|
|
168
|
+
if (!buckets.has(b)) buckets.set(b, []);
|
|
169
|
+
buckets.get(b).push(v);
|
|
170
|
+
}
|
|
171
|
+
return [...buckets.entries()].map(([b, vs]) => ({
|
|
172
|
+
breakdown_freq: b,
|
|
173
|
+
user_count: vs.length,
|
|
174
|
+
avg_aggregate: vs.reduce((a, x) => a + x, 0) / vs.length,
|
|
175
|
+
})).sort((x, y) => x.breakdown_freq - y.breakdown_freq);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Time to Convert (Funnel TTC, broken down by user property) ──
|
|
179
|
+
|
|
180
|
+
function timeToConvert(events, { fromEvent, toEvent, breakdownByUserProperty, profiles = [] }) {
|
|
181
|
+
if (!fromEvent || !toEvent) throw new Error('timeToConvert requires fromEvent and toEvent');
|
|
182
|
+
const userEvents = groupByUser(events);
|
|
183
|
+
const profileByUid = new Map();
|
|
184
|
+
for (const p of profiles) {
|
|
185
|
+
if (!p) continue;
|
|
186
|
+
const uid = p.distinct_id || p.user_id;
|
|
187
|
+
if (uid) profileByUid.set(uid, p);
|
|
188
|
+
}
|
|
189
|
+
const buckets = new Map(); // segValue → [ttcMs]
|
|
190
|
+
for (const [uid, evs] of userEvents) {
|
|
191
|
+
const sorted = sortByTime(evs);
|
|
192
|
+
const a = sorted.find(e => e && e.event === fromEvent);
|
|
193
|
+
if (!a) continue;
|
|
194
|
+
const aIdx = sorted.indexOf(a);
|
|
195
|
+
const b = sorted.slice(aIdx + 1).find(e => e && e.event === toEvent);
|
|
196
|
+
if (!b) continue;
|
|
197
|
+
const ttcMs = toMs(b.time) - toMs(a.time);
|
|
198
|
+
if (!Number.isFinite(ttcMs) || ttcMs < 0) continue;
|
|
199
|
+
const profile = profileByUid.get(uid);
|
|
200
|
+
const segValue = breakdownByUserProperty
|
|
201
|
+
? (profile ? (profile[breakdownByUserProperty] ?? 'unknown') : 'unknown')
|
|
202
|
+
: 'all';
|
|
203
|
+
if (!buckets.has(segValue)) buckets.set(segValue, []);
|
|
204
|
+
buckets.get(segValue).push(ttcMs);
|
|
205
|
+
}
|
|
206
|
+
return [...buckets.entries()].map(([seg, ttcs]) => ({
|
|
207
|
+
segment_value: seg,
|
|
208
|
+
user_count: ttcs.length,
|
|
209
|
+
avg_ttc_ms: ttcs.reduce((a, x) => a + x, 0) / ttcs.length,
|
|
210
|
+
median_ttc_ms: median(ttcs),
|
|
211
|
+
})).sort((x, y) => String(x.segment_value).localeCompare(String(y.segment_value)));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Attributed By (first-/last-touch attribution by event property value) ──
|
|
215
|
+
|
|
216
|
+
function attributedBy(events, { conversionEvent, attributionEvent, attributionProperty, model = 'firstTouch' }) {
|
|
217
|
+
if (!conversionEvent || !attributionEvent || !attributionProperty) {
|
|
218
|
+
throw new Error('attributedBy requires conversionEvent, attributionEvent, attributionProperty');
|
|
219
|
+
}
|
|
220
|
+
const userEvents = groupByUser(events);
|
|
221
|
+
const counts = new Map();
|
|
222
|
+
for (const [uid, evs] of userEvents) {
|
|
223
|
+
const sorted = sortByTime(evs);
|
|
224
|
+
const conversion = sorted.find(e => e && e.event === conversionEvent);
|
|
225
|
+
if (!conversion) continue;
|
|
226
|
+
const conversionTime = toMs(conversion.time);
|
|
227
|
+
const touches = sorted.filter(e =>
|
|
228
|
+
e && e.event === attributionEvent && toMs(e.time) <= conversionTime
|
|
229
|
+
);
|
|
230
|
+
if (!touches.length) continue;
|
|
231
|
+
const touch = model === 'lastTouch' ? touches[touches.length - 1] : touches[0];
|
|
232
|
+
const v = touch[attributionProperty] ?? 'unknown';
|
|
233
|
+
counts.set(v, (counts.get(v) || 0) + 1);
|
|
234
|
+
}
|
|
235
|
+
return [...counts.entries()].map(([source, count]) => ({
|
|
236
|
+
attribution_value: source,
|
|
237
|
+
conversions: count,
|
|
238
|
+
})).sort((a, b) => b.conversions - a.conversions);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── shared helpers ──
|
|
242
|
+
|
|
243
|
+
function userIdOf(ev) {
|
|
244
|
+
return ev && (ev.user_id || ev.distinct_id || ev.device_id);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function groupByUser(events) {
|
|
248
|
+
const userEvents = new Map();
|
|
249
|
+
for (const ev of events) {
|
|
250
|
+
const uid = userIdOf(ev);
|
|
251
|
+
if (!uid) continue;
|
|
252
|
+
if (!userEvents.has(uid)) userEvents.set(uid, []);
|
|
253
|
+
userEvents.get(uid).push(ev);
|
|
254
|
+
}
|
|
255
|
+
return userEvents;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function sortByTime(evs) {
|
|
259
|
+
return evs.slice().sort((a, b) => toMs(a && a.time) - toMs(b && b.time));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
import { toMs } from '../hook-helpers/_internal.js';
|
|
263
|
+
|
|
264
|
+
function applyAgg(vals, agg) {
|
|
265
|
+
if (!vals || !vals.length) return 0;
|
|
266
|
+
switch (agg) {
|
|
267
|
+
case 'sum': return vals.reduce((a, b) => a + b, 0);
|
|
268
|
+
case 'count': return vals.length;
|
|
269
|
+
case 'max': return Math.max(...vals);
|
|
270
|
+
case 'min': return Math.min(...vals);
|
|
271
|
+
case 'avg':
|
|
272
|
+
default: return vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function median(arr) {
|
|
277
|
+
if (!arr.length) return 0;
|
|
278
|
+
const sorted = arr.slice().sort((a, b) => a - b);
|
|
279
|
+
const mid = Math.floor(sorted.length / 2);
|
|
280
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
281
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ak--47/dungeon-master/verify — Phase 4 verification surface.
|
|
3
|
+
*
|
|
4
|
+
* `emulateBreakdown` produces the table shapes Mixpanel shows for the five
|
|
5
|
+
* supported analyses (frequencyByFrequency, funnelFrequency, aggregatePerUser,
|
|
6
|
+
* timeToConvert, attributedBy). `verifyDungeon` is the higher-level wrapper
|
|
7
|
+
* that runs a dungeon and asserts emulator outputs match expected ratios — wire
|
|
8
|
+
* this into your CI to catch dungeon drift.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { emulateBreakdown } from './emulate-breakdown.js';
|
|
12
|
+
export { verifyDungeon } from './verify-dungeon.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verifyDungeon — run a dungeon (in-memory) and run a series of emulator checks
|
|
3
|
+
* against its output, returning a structured report. Designed for CI use:
|
|
4
|
+
*
|
|
5
|
+
* const report = await verifyDungeon(dungeonConfig, [
|
|
6
|
+
* {
|
|
7
|
+
* name: 'engaged users do 2x purchases',
|
|
8
|
+
* breakdown: { type: 'frequencyByFrequency',
|
|
9
|
+
* metricEvent: 'Purchase', breakdownByFrequencyOf: 'Browse' },
|
|
10
|
+
* assert: (rows) => {
|
|
11
|
+
* // custom assertion against the emulator's output table
|
|
12
|
+
* return { pass: true, detail: 'looks good' };
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ]);
|
|
16
|
+
* if (!report.pass) process.exit(1);
|
|
17
|
+
*
|
|
18
|
+
* The dungeon is run via the same default-export entry point external consumers
|
|
19
|
+
* use, so this is a true end-to-end check.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import DUNGEON_MASTER from '../../index.js';
|
|
23
|
+
import { emulateBreakdown } from './emulate-breakdown.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} VerifyCheck
|
|
27
|
+
* @property {string} name - Human-readable name for the check.
|
|
28
|
+
* @property {Object} breakdown - Argument passed to `emulateBreakdown`.
|
|
29
|
+
* @property {(rows: Array<Object>, ctx: { events: Array<Object>, profiles: Array<Object> }) => { pass: boolean, detail?: string }} assert
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {Object} config - Dungeon config (or path; passed straight to DUNGEON_MASTER).
|
|
34
|
+
* @param {VerifyCheck[]} checks
|
|
35
|
+
* @returns {Promise<{ pass: boolean, results: Array<{ name: string, pass: boolean, detail?: string, rows?: Array<Object> }> }>}
|
|
36
|
+
*/
|
|
37
|
+
export async function verifyDungeon(config, checks) {
|
|
38
|
+
if (!checks || !checks.length) throw new Error('verifyDungeon: at least one check required');
|
|
39
|
+
let result = await DUNGEON_MASTER(config);
|
|
40
|
+
if (Array.isArray(result)) result = result[0];
|
|
41
|
+
const events = Array.isArray(result.eventData) ? result.eventData : Array.from(result.eventData);
|
|
42
|
+
const profiles = Array.isArray(result.userProfilesData) ? result.userProfilesData : Array.from(result.userProfilesData);
|
|
43
|
+
const ctx = { events, profiles };
|
|
44
|
+
const results = [];
|
|
45
|
+
for (const check of checks) {
|
|
46
|
+
try {
|
|
47
|
+
const breakdownArgs = { ...check.breakdown };
|
|
48
|
+
// timeToConvert + attributedBy may want profiles; auto-inject if not provided.
|
|
49
|
+
if (breakdownArgs.type === 'timeToConvert' && !breakdownArgs.profiles) {
|
|
50
|
+
breakdownArgs.profiles = profiles;
|
|
51
|
+
}
|
|
52
|
+
const rows = emulateBreakdown(events, breakdownArgs);
|
|
53
|
+
const verdict = check.assert(rows, ctx);
|
|
54
|
+
results.push({ name: check.name, pass: !!verdict.pass, detail: verdict.detail, rows });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
results.push({ name: check.name, pass: false, detail: `error: ${err.message}` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const pass = results.every(r => r.pass);
|
|
60
|
+
return { pass, results };
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ak--47/dungeon-master",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "generate fancy datasets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"types": "./types.d.ts"
|
|
12
12
|
},
|
|
13
13
|
"./utils": "./lib/utils/utils.js",
|
|
14
|
-
"./text": "./lib/generators/text.js"
|
|
14
|
+
"./text": "./lib/generators/text.js",
|
|
15
|
+
"./hook-helpers": "./lib/hook-helpers/index.js",
|
|
16
|
+
"./hook-patterns": "./lib/hook-patterns/index.js",
|
|
17
|
+
"./verify": "./lib/verify/index.js"
|
|
15
18
|
},
|
|
16
19
|
"files": [
|
|
17
20
|
"index.js",
|
|
@@ -21,7 +24,6 @@
|
|
|
21
24
|
"!dungeons/customers/",
|
|
22
25
|
"!dungeons/user/",
|
|
23
26
|
"scripts/",
|
|
24
|
-
"!scripts/experiments/",
|
|
25
27
|
"package.json",
|
|
26
28
|
"README.md",
|
|
27
29
|
"CHANGELOG.md"
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
"dotenv": "^16.4.5",
|
|
73
75
|
"hyparquet-writer": "^0.6.1",
|
|
74
76
|
"mixpanel": "^0.18.0",
|
|
75
|
-
"mixpanel-import": "^3.
|
|
77
|
+
"mixpanel-import": "^3.3.1",
|
|
76
78
|
"p-limit": "^3.1.0",
|
|
77
79
|
"pino": "^9.0.0",
|
|
78
80
|
"pino-pretty": "^11.0.0",
|