@ak--47/dungeon-master 1.4.4 → 1.5.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/.claude/skills/analyze-soup/SKILL.md +158 -0
- package/.claude/skills/create-dungeon/SKILL.md +464 -0
- package/.claude/skills/verify-dungeon/SKILL.md +157 -0
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
- package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
- package/.claude/skills/write-hooks/SKILL.md +468 -0
- package/CHANGELOG.md +147 -0
- package/HOOKS.md +1243 -597
- package/README.md +140 -5
- package/dungeons/technical/ad-spend.js +1 -1
- package/dungeons/technical/anonymous-users.js +1 -1
- package/dungeons/technical/array-of-object-lookup.js +1 -1
- package/dungeons/technical/datagen-v15-verify.js +74 -0
- package/dungeons/technical/experiments.js +1 -1
- package/dungeons/technical/foobar.js +1 -1
- package/dungeons/technical/group-analytics.js +1 -1
- package/dungeons/technical/mirror-strategies.js +1 -1
- package/dungeons/technical/nested-objects.js +1 -1
- package/dungeons/technical/retention-cadence.js +1 -1
- package/dungeons/technical/sanity.js +1 -1
- package/dungeons/technical/scale-test.js +1 -1
- package/dungeons/technical/scd.js +1 -1
- package/dungeons/technical/simple.js +1 -1
- package/dungeons/technical/simplest.js +74 -20
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +4 -0
- package/dungeons/vertical/community.js +9 -3
- package/dungeons/vertical/crypto.js +5 -0
- package/dungeons/vertical/dating.js +23 -10
- package/dungeons/vertical/devtools.js +10 -0
- package/dungeons/vertical/ecommerce.js +6 -0
- package/dungeons/vertical/education.js +11 -0
- package/dungeons/vertical/fintech.js +13 -0
- package/dungeons/vertical/fitness.js +10 -0
- package/dungeons/vertical/food-delivery.js +9 -0
- package/dungeons/vertical/gaming.js +10 -0
- package/dungeons/vertical/healthcare.js +5 -0
- package/dungeons/vertical/insurance-application.js +10 -0
- package/dungeons/vertical/logistics.js +8 -1
- package/dungeons/vertical/marketplace.js +7 -0
- package/dungeons/vertical/media.js +8 -0
- package/dungeons/vertical/real-estate.js +7 -1
- package/dungeons/vertical/sass.js +12 -0
- package/dungeons/vertical/social.js +9 -0
- package/dungeons/vertical/travel.js +5 -0
- package/index.js +45 -7
- package/lib/core/config-validator.js +270 -7
- package/lib/core/context.js +58 -0
- package/lib/core/dungeon-loader.js +2 -5
- package/lib/generators/events.js +12 -13
- package/lib/generators/funnels.js +72 -1
- package/lib/hook-helpers/index.js +1 -0
- package/lib/hook-helpers/inject.js +95 -0
- package/lib/orchestrators/mixpanel-sender.js +27 -1
- package/lib/orchestrators/user-loop.js +488 -29
- package/lib/templates/macro-presets.js +39 -9
- package/lib/utils/utils.js +16 -79
- package/lib/verify/counting.js +320 -0
- package/lib/verify/emulate-breakdown.js +512 -108
- package/lib/verify/funnel-engine.js +539 -0
- package/lib/verify/identity.js +78 -0
- package/lib/verify/index.js +19 -0
- package/lib/verify/verify-dungeon.js +58 -0
- package/package.json +4 -2
- package/types.d.ts +314 -4
- package/scripts/smoke-test-all.mjs +0 -162
|
@@ -97,3 +97,98 @@ export function injectBurst(events, templateEvent, count, anchorTime, spreadMs,
|
|
|
97
97
|
return created;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
const DAY_MS = 86400000;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Inject cloned events on days where the user had no activity for this event
|
|
104
|
+
* type. Increases distinct-day frequency without disturbing existing event
|
|
105
|
+
* ordering or inflating same-day counts.
|
|
106
|
+
*
|
|
107
|
+
* Designed for use in `everything` hooks to move users between frequency bins
|
|
108
|
+
* in Mixpanel's frequency distribution reports — which count DISTINCT DAYS
|
|
109
|
+
* (not total events). See `lib/verify/counting.js#countDistinctPeriods` and
|
|
110
|
+
* `mixpanel/analytics` `addiction_query.cpp` for the counting rule.
|
|
111
|
+
*
|
|
112
|
+
* Behavior:
|
|
113
|
+
* 1. Collect all existing distinct UTC days for `eventName`.
|
|
114
|
+
* 2. If the user already has >= `targetDays` distinct days, return unchanged.
|
|
115
|
+
* 3. Restrict candidate days to the user's active window (first event time
|
|
116
|
+
* to last event time, by default).
|
|
117
|
+
* 4. Pick `targetDays - existing` random days that have no `eventName`
|
|
118
|
+
* activity (uses seeded RNG).
|
|
119
|
+
* 5. Find a template event of `eventName`; if none exist for this user,
|
|
120
|
+
* return unchanged (we honor schema-first: don't fabricate events).
|
|
121
|
+
* 6. Clone the template onto each picked day at a random hour within the
|
|
122
|
+
* day. `insert_id` is stripped (Mixpanel re-deduplicates on import).
|
|
123
|
+
* 7. Append clones to the array. Caller's downstream sort handles ordering.
|
|
124
|
+
*
|
|
125
|
+
* @param {Object[]} events - Full user event array (from `everything` hook).
|
|
126
|
+
* @param {string} eventName - Event name to inject.
|
|
127
|
+
* @param {number} targetDays - Desired absolute count of distinct active days.
|
|
128
|
+
* @param {Object} [options]
|
|
129
|
+
* @param {('active')} [options.timeRange='active'] - 'active' = user's
|
|
130
|
+
* first-to-last event window. Reserved for future range modes.
|
|
131
|
+
* @param {Object} [options.overrides] - Property overrides applied via spread
|
|
132
|
+
* on top of the cloned template.
|
|
133
|
+
* @returns {Object[]} The (possibly mutated) events array. Same reference as
|
|
134
|
+
* the input — convenient to return from an `everything` hook.
|
|
135
|
+
*/
|
|
136
|
+
export function injectOnNewDays(events, eventName, targetDays, options = {}) {
|
|
137
|
+
if (!Array.isArray(events) || !eventName || typeof targetDays !== 'number' || targetDays <= 0) return events;
|
|
138
|
+
const overrides = options.overrides || {};
|
|
139
|
+
|
|
140
|
+
// Collect timestamps for the named event + global window for the user.
|
|
141
|
+
const matches = [];
|
|
142
|
+
const allTimes = [];
|
|
143
|
+
let template = null;
|
|
144
|
+
for (const ev of events) {
|
|
145
|
+
if (!ev) continue;
|
|
146
|
+
const t = toMs(ev.time);
|
|
147
|
+
if (Number.isFinite(t)) allTimes.push(t);
|
|
148
|
+
if (ev.event === eventName && Number.isFinite(t)) {
|
|
149
|
+
matches.push(t);
|
|
150
|
+
template = template || ev;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!template) return events;
|
|
154
|
+
if (!allTimes.length) return events;
|
|
155
|
+
|
|
156
|
+
// Distinct UTC days that already have an event of `eventName`.
|
|
157
|
+
const existingDays = new Set();
|
|
158
|
+
for (const t of matches) existingDays.add(Math.floor(t / DAY_MS));
|
|
159
|
+
if (existingDays.size >= targetDays) return events;
|
|
160
|
+
|
|
161
|
+
const minMs = Math.min(...allTimes);
|
|
162
|
+
const maxMs = Math.max(...allTimes);
|
|
163
|
+
const firstDay = Math.floor(minMs / DAY_MS);
|
|
164
|
+
const lastDay = Math.floor(maxMs / DAY_MS);
|
|
165
|
+
|
|
166
|
+
const candidateDays = [];
|
|
167
|
+
for (let d = firstDay; d <= lastDay; d++) {
|
|
168
|
+
if (!existingDays.has(d)) candidateDays.push(d);
|
|
169
|
+
}
|
|
170
|
+
const need = targetDays - existingDays.size;
|
|
171
|
+
if (candidateDays.length === 0 || need <= 0) return events;
|
|
172
|
+
|
|
173
|
+
const chance = getChance();
|
|
174
|
+
// Use chance.pickset for a unique-without-replacement sample from the
|
|
175
|
+
// candidate day pool. If `need` exceeds candidates, fall back to all.
|
|
176
|
+
const pickCount = Math.min(need, candidateDays.length);
|
|
177
|
+
const picked = chance.pickset(candidateDays, pickCount);
|
|
178
|
+
|
|
179
|
+
for (const day of picked) {
|
|
180
|
+
// Pick a random ms within this UTC day. Clamp to active window.
|
|
181
|
+
const dayStart = day * DAY_MS;
|
|
182
|
+
const dayEnd = dayStart + DAY_MS - 1;
|
|
183
|
+
const lo = Math.max(dayStart, minMs);
|
|
184
|
+
const hi = Math.min(dayEnd, maxMs);
|
|
185
|
+
const newMs = lo >= hi ? lo : chance.integer({ min: lo, max: hi });
|
|
186
|
+
const clone = { ...template, ...overrides };
|
|
187
|
+
writeTime(clone, newMs);
|
|
188
|
+
delete clone.insert_id;
|
|
189
|
+
events.push(clone);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return events;
|
|
193
|
+
}
|
|
194
|
+
|
|
@@ -63,6 +63,21 @@ export async function sendToMixpanel(context) {
|
|
|
63
63
|
workers: 35
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
const hasProgressCb = typeof config.onProgress === 'function';
|
|
67
|
+
function makeProgressCallback(total) {
|
|
68
|
+
if (!hasProgressCb) return undefined;
|
|
69
|
+
return (recordType, processed, requests, eps, bytesProcessed) => {
|
|
70
|
+
context.reportProgress({
|
|
71
|
+
phase: "import",
|
|
72
|
+
recordType,
|
|
73
|
+
processed,
|
|
74
|
+
total,
|
|
75
|
+
eps,
|
|
76
|
+
bytesProcessed
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
log(`\n${'─'.repeat(50)}`);
|
|
67
82
|
log(` Importing data to Mixpanel (${region})`);
|
|
68
83
|
log(`${'─'.repeat(50)}\n`);
|
|
@@ -76,9 +91,11 @@ export async function sendToMixpanel(context) {
|
|
|
76
91
|
const files = eventData.getWrittenFiles();
|
|
77
92
|
if (files.length > 0) eventDataToImport = files;
|
|
78
93
|
}
|
|
94
|
+
const eventTotal = Array.isArray(eventDataToImport) ? eventDataToImport.length : 0;
|
|
79
95
|
const imported = await mp(creds, eventDataToImport, {
|
|
80
96
|
recordType: "event",
|
|
81
97
|
...commonOpts,
|
|
98
|
+
progressCallback: makeProgressCallback(eventTotal),
|
|
82
99
|
});
|
|
83
100
|
log(` -> ${comma(imported.success)} events sent\n`);
|
|
84
101
|
importResults.events = imported;
|
|
@@ -93,9 +110,11 @@ export async function sendToMixpanel(context) {
|
|
|
93
110
|
const files = userProfilesData.getWrittenFiles();
|
|
94
111
|
if (files.length > 0) userProfilesToImport = files;
|
|
95
112
|
}
|
|
113
|
+
const userTotal = Array.isArray(userProfilesToImport) ? userProfilesToImport.length : 0;
|
|
96
114
|
const imported = await mp(creds, userProfilesToImport, {
|
|
97
115
|
recordType: "user",
|
|
98
116
|
...commonOpts,
|
|
117
|
+
progressCallback: makeProgressCallback(userTotal),
|
|
99
118
|
});
|
|
100
119
|
log(` -> ${comma(imported.success)} user profiles sent\n`);
|
|
101
120
|
importResults.users = imported;
|
|
@@ -110,9 +129,11 @@ export async function sendToMixpanel(context) {
|
|
|
110
129
|
const files = adSpendData.getWrittenFiles();
|
|
111
130
|
if (files.length > 0) adSpendDataToImport = files;
|
|
112
131
|
}
|
|
132
|
+
const adTotal = Array.isArray(adSpendDataToImport) ? adSpendDataToImport.length : 0;
|
|
113
133
|
const imported = await mp(creds, adSpendDataToImport, {
|
|
114
134
|
recordType: "event",
|
|
115
135
|
...commonOpts,
|
|
136
|
+
progressCallback: makeProgressCallback(adTotal),
|
|
116
137
|
});
|
|
117
138
|
log(` -> ${comma(imported.success)} ad spend events sent\n`);
|
|
118
139
|
importResults.adSpend = imported;
|
|
@@ -131,10 +152,12 @@ export async function sendToMixpanel(context) {
|
|
|
131
152
|
const files = groupEntity.getWrittenFiles();
|
|
132
153
|
if (files.length > 0) groupProfilesToImport = files;
|
|
133
154
|
}
|
|
155
|
+
const groupTotal = Array.isArray(groupProfilesToImport) ? groupProfilesToImport.length : 0;
|
|
134
156
|
const imported = await mp({ token, groupKey }, groupProfilesToImport, {
|
|
135
157
|
recordType: "group",
|
|
136
158
|
...commonOpts,
|
|
137
159
|
groupKey,
|
|
160
|
+
progressCallback: makeProgressCallback(groupTotal),
|
|
138
161
|
});
|
|
139
162
|
log(` -> ${comma(imported.success)} ${groupKey} profiles sent\n`);
|
|
140
163
|
importResults.groups.push(imported);
|
|
@@ -150,9 +173,11 @@ export async function sendToMixpanel(context) {
|
|
|
150
173
|
const files = groupEventData.getWrittenFiles();
|
|
151
174
|
if (files.length > 0) groupEventDataToImport = files;
|
|
152
175
|
}
|
|
176
|
+
const groupEvTotal = Array.isArray(groupEventDataToImport) ? groupEventDataToImport.length : 0;
|
|
153
177
|
const imported = await mp(creds, groupEventDataToImport, {
|
|
154
178
|
recordType: "event",
|
|
155
179
|
...commonOpts,
|
|
180
|
+
progressCallback: makeProgressCallback(groupEvTotal),
|
|
156
181
|
});
|
|
157
182
|
log(` -> ${comma(imported.success)} group events sent\n`);
|
|
158
183
|
importResults.groupEvents = imported;
|
|
@@ -197,6 +222,7 @@ export async function sendToMixpanel(context) {
|
|
|
197
222
|
}
|
|
198
223
|
|
|
199
224
|
try {
|
|
225
|
+
const scdTotal = Array.isArray(scdDataToImport) ? scdDataToImport.length : 0;
|
|
200
226
|
const imported = await mp(
|
|
201
227
|
{
|
|
202
228
|
token,
|
|
@@ -205,7 +231,7 @@ export async function sendToMixpanel(context) {
|
|
|
205
231
|
project: projectId
|
|
206
232
|
},
|
|
207
233
|
scdDataToImport,
|
|
208
|
-
options
|
|
234
|
+
{ ...options, progressCallback: makeProgressCallback(scdTotal) }
|
|
209
235
|
);
|
|
210
236
|
log(` -> ${comma(imported.success)} ${scdKey} SCD entries sent\n`);
|
|
211
237
|
importResults[`${scdKey}_scd`] = imported;
|