@ak--47/dungeon-master 1.3.1 → 1.4.1

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 (54) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/hook-helpers-verify.js +89 -0
  3. package/dungeons/technical/identity-model-verify.js +47 -0
  4. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  5. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  6. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  7. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  8. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  9. package/dungeons/vertical/ai-platform.js +45 -52
  10. package/dungeons/vertical/community.js +11 -8
  11. package/dungeons/vertical/crypto.js +25 -24
  12. package/dungeons/vertical/dating.js +56 -48
  13. package/dungeons/vertical/devtools.js +25 -18
  14. package/dungeons/vertical/ecommerce.js +42 -38
  15. package/dungeons/vertical/education.js +24 -9
  16. package/dungeons/vertical/fintech.js +13 -8
  17. package/dungeons/vertical/fitness.js +73 -122
  18. package/dungeons/vertical/food-delivery.js +18 -19
  19. package/dungeons/vertical/gaming.js +19 -20
  20. package/dungeons/vertical/healthcare.js +11 -8
  21. package/dungeons/vertical/insurance-application.js +6 -3
  22. package/dungeons/vertical/logistics.js +15 -9
  23. package/dungeons/vertical/marketplace.js +36 -27
  24. package/dungeons/vertical/media.js +27 -25
  25. package/dungeons/vertical/real-estate.js +18 -7
  26. package/dungeons/vertical/sass.js +84 -68
  27. package/dungeons/vertical/social.js +46 -47
  28. package/dungeons/vertical/travel.js +8 -5
  29. package/index.js +17 -71
  30. package/lib/core/config-validator.js +143 -164
  31. package/lib/core/storage.js +5 -1
  32. package/lib/generators/events.js +49 -93
  33. package/lib/generators/funnels.js +202 -91
  34. package/lib/hook-helpers/_internal.js +23 -0
  35. package/lib/hook-helpers/cohort.js +124 -0
  36. package/lib/hook-helpers/identity.js +56 -0
  37. package/lib/hook-helpers/index.js +44 -0
  38. package/lib/hook-helpers/inject.js +99 -0
  39. package/lib/hook-helpers/mutate.js +151 -0
  40. package/lib/hook-helpers/timing.js +99 -0
  41. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  42. package/lib/hook-patterns/attributed-by-source.js +72 -0
  43. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  44. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  45. package/lib/hook-patterns/index.js +14 -0
  46. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  47. package/lib/orchestrators/mixpanel-sender.js +46 -51
  48. package/lib/orchestrators/user-loop.js +119 -269
  49. package/lib/utils/utils.js +39 -16
  50. package/lib/verify/emulate-breakdown.js +281 -0
  51. package/lib/verify/index.js +12 -0
  52. package/lib/verify/verify-dungeon.js +61 -0
  53. package/package.json +6 -4
  54. package/types.d.ts +404 -212
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "harness-social";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -107,7 +107,7 @@ const chance = u.initChance(SEED);
107
107
  * receive follow-backs and post more frequently.
108
108
  *
109
109
  * -------------------------------------------------------------------------------------
110
- * 3. ALGORITHM CHANGE (event)
110
+ * 3. ALGORITHM CHANGE (everything)
111
111
  * -------------------------------------------------------------------------------------
112
112
  *
113
113
  * PATTERN: On day 45, the dominant `source` for "post viewed" flips from
@@ -146,7 +146,7 @@ const chance = u.initChance(SEED);
146
146
  * quality is awful, dragging down avg watch time.
147
147
  *
148
148
  * -------------------------------------------------------------------------------------
149
- * 5. NOTIFICATION RE-ENGAGEMENT (event)
149
+ * 5. NOTIFICATION RE-ENGAGEMENT (everything)
150
150
  * -------------------------------------------------------------------------------------
151
151
  *
152
152
  * PATTERN: After day 30, 30% of "post viewed" events have source flipped
@@ -190,14 +190,14 @@ const chance = u.initChance(SEED);
190
190
  * 7. TOXICITY CHURN (everything)
191
191
  * -------------------------------------------------------------------------------------
192
192
  *
193
- * PATTERN: Users with 3+ "report submitted" events lose 60% of activity
193
+ * PATTERN: Users with 2+ "report submitted" events lose 60% of activity
194
194
  * after day 30. No flag — discover via retention or per-user activity drop.
195
195
  *
196
196
  * HOW TO FIND IT IN MIXPANEL:
197
197
  *
198
198
  * Report 1: Retention by Toxicity
199
199
  * - Report type: Retention
200
- * - Cohort A: users with >= 3 "report submitted"
200
+ * - Cohort A: users with >= 2 "report submitted"
201
201
  * - Cohort B: rest
202
202
  * - Expected: A ~ 40% retention vs B ~ 80%
203
203
  *
@@ -276,15 +276,17 @@ const postIds = v.range(1, 1001).map(n => `post_${v.uid(8)}`);
276
276
 
277
277
  /** @type {Config} */
278
278
  const config = {
279
+ version: 2,
279
280
  token,
280
281
  seed: SEED,
281
282
  datasetStart: "2026-01-01T00:00:00Z",
282
- datasetEnd: "2026-04-28T23:59:59Z",
283
+ datasetEnd: "2026-05-01T23:59:59Z",
283
284
  soup: { dayOfWeekWeights: [1.0, 1.0, 1.0, 1.0, 1.0, 1.2, 1.2] },
284
285
  // numDays: num_days,
285
286
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
286
287
  numUsers: num_users,
287
- hasAnonIds: false,
288
+ hasAnonIds: true,
289
+ avgDevicePerUser: 2,
288
290
  hasSessionIds: true,
289
291
  format: "json",
290
292
  gzip: true,
@@ -379,6 +381,7 @@ const config = {
379
381
  event: "account created",
380
382
  weight: 1,
381
383
  isFirstEvent: true,
384
+ isAuthEvent: true,
382
385
  properties: {
383
386
  "signup_method": ["email", "google", "apple", "sso"],
384
387
  "referred_by": ["organic", "friend", "ad", "influencer"],
@@ -576,43 +579,6 @@ const config = {
576
579
  }
577
580
 
578
581
 
579
- // ─── EVENT-LEVEL HOOKS ───────────────────────────────────────────
580
-
581
- if (type === "event") {
582
- const datasetStart = dayjs.unix(meta.datasetStart);
583
- const ALGORITHM_CHANGE_DAY = datasetStart.add(45, 'days');
584
- const REENGAGEMENT_START = datasetStart.add(30, 'days');
585
- const EVENT_TIME = dayjs(record.time);
586
-
587
- // Hook #3: ALGORITHM CHANGE - Day 45 flips feed -> explore.
588
- // Mutates the existing config-defined `source` prop.
589
- if (record.event === "post viewed") {
590
- if (EVENT_TIME.isAfter(ALGORITHM_CHANGE_DAY)) {
591
- if (chance.bool({ likelihood: 70 })) {
592
- record.source = "explore";
593
- }
594
- } else {
595
- if (chance.bool({ likelihood: 70 })) {
596
- record.source = "feed";
597
- }
598
- }
599
- }
600
-
601
- // Hook #4: ENGAGEMENT BAIT - 20% of post views get crushed view duration.
602
- // No flag — analyst sees bimodal duration distribution + low-tail share.
603
- if (record.event === "post viewed") {
604
- if (chance.bool({ likelihood: 20 })) {
605
- record.view_duration_sec = chance.integer({ min: 1, max: 5 });
606
- }
607
-
608
- // Hook #5: NOTIFICATION RE-ENGAGEMENT — after day 30, 30% of views
609
- // flip source to "notification". Mutates existing source prop.
610
- if (EVENT_TIME.isAfter(REENGAGEMENT_START) && chance.bool({ likelihood: 30 })) {
611
- record.source = "notification";
612
- }
613
- }
614
- }
615
-
616
582
  // ─── EVERYTHING-LEVEL HOOKS ──────────────────────────────────────
617
583
 
618
584
  if (type === "everything") {
@@ -782,9 +748,42 @@ const config = {
782
748
  }
783
749
  }
784
750
 
751
+ // Hook #3: ALGORITHM CHANGE — day 45 flips feed → explore on post viewed.
752
+ // Hook #4: ENGAGEMENT BAIT — 20% of post-viewed events get crushed duration.
753
+ // Hook #5: NOTIFICATION RE-ENGAGEMENT — after day 30, 30% of views → notification.
754
+ // All three run AFTER injection passes so they apply to cloned events too.
755
+ const ALGORITHM_CHANGE_DAY = datasetStart.add(45, 'days');
756
+ const REENGAGEMENT_START = datasetStart.add(30, 'days');
757
+ userEvents.forEach(e => {
758
+ if (e.event === "post viewed") {
759
+ const eventTime = dayjs(e.time);
760
+
761
+ // Hook #3: Algorithm Change
762
+ if (eventTime.isAfter(ALGORITHM_CHANGE_DAY)) {
763
+ if (chance.bool({ likelihood: 70 })) {
764
+ e.source = "explore";
765
+ }
766
+ } else {
767
+ if (chance.bool({ likelihood: 70 })) {
768
+ e.source = "feed";
769
+ }
770
+ }
771
+
772
+ // Hook #4: Engagement Bait — 20% crushed view duration
773
+ if (chance.bool({ likelihood: 20 })) {
774
+ e.view_duration_sec = chance.integer({ min: 1, max: 5 });
775
+ }
776
+
777
+ // Hook #5: Notification Re-engagement (runs after #3 so can override)
778
+ if (eventTime.isAfter(REENGAGEMENT_START) && chance.bool({ likelihood: 30 })) {
779
+ e.source = "notification";
780
+ }
781
+ }
782
+ });
783
+
785
784
  // Hook #7: TOXICITY CHURN — drop 60% of activity after day 30 for high reporters.
786
- // Discovery: cohort users with >=3 report-submitted events, observe retention drop.
787
- if (reportSubmittedCount >= 3) {
785
+ // Discovery: cohort users with >=2 report-submitted events, observe retention drop.
786
+ if (reportSubmittedCount >= 2) {
788
787
  const churnCutoff = datasetStart.add(30, 'days');
789
788
  for (let i = userEvents.length - 1; i >= 0; i--) {
790
789
  const evt = userEvents[i];
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-travel";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -29,7 +29,7 @@ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona",
29
29
  * StayQuest — a hotel booking platform for business and leisure travelers.
30
30
  * Users search destinations, compare hotels, book rooms, and leave reviews.
31
31
  *
32
- * - 5,000 users over 100 days, ~600K events
32
+ * - 5,000 users over 120 days, ~600K events
33
33
  * - Segments: business travelers (weekday), leisure families, luxury, budget
34
34
  * - Core loop: search → view hotel → compare → book → stay → review
35
35
  * - Revenue: commission per booking + premium loyalty membership
@@ -210,14 +210,16 @@ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona",
210
210
 
211
211
  /** @type {Config} */
212
212
  const config = {
213
+ version: 2,
213
214
  token,
214
215
  seed: SEED,
215
216
  datasetStart: "2026-01-01T00:00:00Z",
216
- datasetEnd: "2026-04-28T23:59:59Z",
217
+ datasetEnd: "2026-05-01T23:59:59Z",
217
218
  // numDays: num_days,
218
219
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
219
220
  numUsers: num_users,
220
- hasAnonIds: false,
221
+ hasAnonIds: true,
222
+ avgDevicePerUser: 2,
221
223
  hasSessionIds: true,
222
224
  format: "json",
223
225
  gzip: true,
@@ -250,6 +252,7 @@ const config = {
250
252
  event: "account created",
251
253
  weight: 1,
252
254
  isFirstEvent: true,
255
+ isAuthEvent: true,
253
256
  properties: {
254
257
  signup_source: ["organic", "google", "instagram", "tripadvisor", "referral", "email_campaign"],
255
258
  },
package/index.js CHANGED
@@ -18,14 +18,14 @@ import { detectInputType, loadFromFile, loadFromText, parseJSONDungeon, validate
18
18
 
19
19
  // Orchestrators
20
20
  import { userLoop } from './lib/orchestrators/user-loop.js';
21
- import { sendToMixpanel } from './lib/orchestrators/mixpanel-sender.js';
21
+ import { sendToMixpanel, collectWrittenFiles } from './lib/orchestrators/mixpanel-sender.js';
22
22
  // Generators
23
23
  import { makeAdSpend } from './lib/generators/adspend.js';
24
24
  import { makeMirror } from './lib/generators/mirror.js';
25
25
  import { makeGroupProfile, makeProfile } from './lib/generators/profiles.js';
26
26
 
27
27
  // Utilities
28
- import { initChance, setDatasetNow } from './lib/utils/utils.js';
28
+ import { initChance, setDatasetNow, deleteFile } from './lib/utils/utils.js';
29
29
 
30
30
  // External dependencies
31
31
  import dayjs from "dayjs";
@@ -122,6 +122,7 @@ async function runDungeon(config) {
122
122
 
123
123
  if (config.verbose) logger.info({ seed: config.seed }, 'Configuring dungeon');
124
124
  let validatedConfig;
125
+ let storage;
125
126
  try {
126
127
  // Initialize seeded RNG BEFORE validation — config-validator captures a
127
128
  // chance reference for default userProps (spiritAnimal). If we init after,
@@ -148,7 +149,7 @@ async function runDungeon(config) {
148
149
 
149
150
  // Step 3: Initialize storage containers
150
151
  const storageManager = new StorageManager(context);
151
- const storage = await storageManager.initializeContainers();
152
+ storage = await storageManager.initializeContainers();
152
153
  updateContextWithStorage(context, storage);
153
154
 
154
155
  // ! DATA GENERATION STARTS HERE
@@ -215,7 +216,7 @@ async function runDungeon(config) {
215
216
  return {
216
217
  ...extractedData,
217
218
  importResults,
218
- files: await extractFileInfo(storage, validatedConfig),
219
+ files: extractFileInfo(storage),
219
220
  time: { start, end, delta, human },
220
221
  operations: context.getOperations(),
221
222
  eventCount: context.getStoredEventCount(),
@@ -225,6 +226,15 @@ async function runDungeon(config) {
225
226
  } catch (error) {
226
227
  logger.error({ err: error }, `Error: ${error.message}`);
227
228
  throw error;
229
+ } finally {
230
+ if (validatedConfig?.cleanup && storage) {
231
+ const allFiles = collectWrittenFiles(storage);
232
+ if (allFiles.length > 0) {
233
+ if (validatedConfig.verbose) console.log(`\nCleaning up ${allFiles.length} written files...`);
234
+ await Promise.allSettled(allFiles.map(f => deleteFile(f)));
235
+ if (validatedConfig.verbose) console.log(`Cleanup complete`);
236
+ }
237
+ }
228
238
  }
229
239
  }
230
240
 
@@ -480,74 +490,10 @@ async function flushStorageToDisk(storage, config) {
480
490
  /**
481
491
  * Extract file information from storage containers
482
492
  * @param {import('./types').Storage} storage - Storage object
483
- * @param {import('./types').Dungeon} config - Configuration object
484
- * @returns {Promise<string[]>} Array of file paths
493
+ * @returns {string[]} Array of file paths
485
494
  */
486
- async function extractFileInfo(storage, config) {
487
- const files = [];
488
-
489
- // Try to get paths from containers first
490
- Object.values(storage).forEach(container => {
491
- if (Array.isArray(container)) {
492
- container.forEach(subContainer => {
493
- if (subContainer?.getWritePath) {
494
- files.push(subContainer.getWritePath());
495
- }
496
- });
497
- } else if (container?.getWritePath) {
498
- files.push(container.getWritePath());
499
- }
500
- });
501
-
502
- // If no files found from containers and writeToDisk is enabled, scan the data directory
503
- if (files.length === 0 && config.writeToDisk) {
504
- try {
505
- const fs = await import('fs');
506
- const path = await import('path');
507
-
508
- let dataDir = path.resolve("./data");
509
- if (!fs.existsSync(dataDir)) {
510
- dataDir = path.resolve("./");
511
- }
512
-
513
- if (fs.existsSync(dataDir)) {
514
- const allFiles = fs.readdirSync(dataDir);
515
- const simulationName = config.name;
516
-
517
- // Filter files that match our patterns and were likely created by this run
518
- const relevantFiles = allFiles.filter(file => {
519
- // Skip system files
520
- if (file.startsWith('.')) return false;
521
-
522
- // If we have a simulation name, only include files with that prefix
523
- if (simulationName && !file.startsWith(simulationName)) {
524
- return false;
525
- }
526
-
527
- // Check for common patterns
528
- const hasEventPattern = file.includes('-EVENTS.');
529
- const hasUserPattern = file.includes('-USERS.');
530
- const hasScdPattern = file.includes('-SCD.');
531
- const hasGroupPattern = file.includes('-GROUPS.');
532
- const hasLookupPattern = file.includes('-LOOKUP.');
533
- const hasAdspendPattern = file.includes('-ADSPEND.');
534
- const hasMirrorPattern = file.includes('-MIRROR.');
535
-
536
- return hasEventPattern || hasUserPattern || hasScdPattern ||
537
- hasGroupPattern || hasLookupPattern || hasAdspendPattern || hasMirrorPattern;
538
- });
539
-
540
- // Convert to full paths
541
- relevantFiles.forEach(file => {
542
- files.push(path.join(dataDir, file));
543
- });
544
- }
545
- } catch (error) {
546
- // If scanning fails, just return empty array
547
- }
548
- }
549
-
550
- return files;
495
+ function extractFileInfo(storage) {
496
+ return collectWrittenFiles(storage);
551
497
  }
552
498
 
553
499
  /**