@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.
Files changed (67) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +147 -0
  9. package/HOOKS.md +1243 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +1 -1
  12. package/dungeons/technical/anonymous-users.js +1 -1
  13. package/dungeons/technical/array-of-object-lookup.js +1 -1
  14. package/dungeons/technical/datagen-v15-verify.js +74 -0
  15. package/dungeons/technical/experiments.js +1 -1
  16. package/dungeons/technical/foobar.js +1 -1
  17. package/dungeons/technical/group-analytics.js +1 -1
  18. package/dungeons/technical/mirror-strategies.js +1 -1
  19. package/dungeons/technical/nested-objects.js +1 -1
  20. package/dungeons/technical/retention-cadence.js +1 -1
  21. package/dungeons/technical/sanity.js +1 -1
  22. package/dungeons/technical/scale-test.js +1 -1
  23. package/dungeons/technical/scd.js +1 -1
  24. package/dungeons/technical/simple.js +1 -1
  25. package/dungeons/technical/simplest.js +74 -20
  26. package/dungeons/technical/text-generation.js +1 -1
  27. package/dungeons/vertical/ai-platform.js +4 -0
  28. package/dungeons/vertical/community.js +9 -3
  29. package/dungeons/vertical/crypto.js +5 -0
  30. package/dungeons/vertical/dating.js +23 -10
  31. package/dungeons/vertical/devtools.js +10 -0
  32. package/dungeons/vertical/ecommerce.js +6 -0
  33. package/dungeons/vertical/education.js +11 -0
  34. package/dungeons/vertical/fintech.js +13 -0
  35. package/dungeons/vertical/fitness.js +10 -0
  36. package/dungeons/vertical/food-delivery.js +9 -0
  37. package/dungeons/vertical/gaming.js +10 -0
  38. package/dungeons/vertical/healthcare.js +5 -0
  39. package/dungeons/vertical/insurance-application.js +10 -0
  40. package/dungeons/vertical/logistics.js +8 -1
  41. package/dungeons/vertical/marketplace.js +7 -0
  42. package/dungeons/vertical/media.js +8 -0
  43. package/dungeons/vertical/real-estate.js +7 -1
  44. package/dungeons/vertical/sass.js +12 -0
  45. package/dungeons/vertical/social.js +9 -0
  46. package/dungeons/vertical/travel.js +5 -0
  47. package/index.js +45 -7
  48. package/lib/core/config-validator.js +270 -7
  49. package/lib/core/context.js +58 -0
  50. package/lib/core/dungeon-loader.js +2 -5
  51. package/lib/generators/events.js +12 -13
  52. package/lib/generators/funnels.js +72 -1
  53. package/lib/hook-helpers/index.js +1 -0
  54. package/lib/hook-helpers/inject.js +95 -0
  55. package/lib/orchestrators/mixpanel-sender.js +27 -1
  56. package/lib/orchestrators/user-loop.js +488 -29
  57. package/lib/templates/macro-presets.js +39 -9
  58. package/lib/utils/utils.js +16 -79
  59. package/lib/verify/counting.js +320 -0
  60. package/lib/verify/emulate-breakdown.js +512 -108
  61. package/lib/verify/funnel-engine.js +539 -0
  62. package/lib/verify/identity.js +78 -0
  63. package/lib/verify/index.js +19 -0
  64. package/lib/verify/verify-dungeon.js +58 -0
  65. package/package.json +4 -2
  66. package/types.d.ts +314 -4
  67. 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;