@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
@@ -416,6 +416,7 @@ const config = {
416
416
  {
417
417
  event: "content selected",
418
418
  weight: 15,
419
+ isStrictEvent: false,
419
420
  properties: {
420
421
  "content_type": ["movie", "series", "documentary", "special"],
421
422
  "genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
@@ -425,6 +426,7 @@ const config = {
425
426
  {
426
427
  event: "playback started",
427
428
  weight: 18,
429
+ isStrictEvent: false,
428
430
  properties: {
429
431
  "content_id": contentIds,
430
432
  "content_type": ["movie", "series", "documentary", "special"],
@@ -436,6 +438,7 @@ const config = {
436
438
  {
437
439
  event: "playback completed",
438
440
  weight: 12,
441
+ isStrictEvent: false,
439
442
  properties: {
440
443
  "content_id": contentIds,
441
444
  "content_type": ["movie", "series", "documentary", "special"],
@@ -446,6 +449,7 @@ const config = {
446
449
  {
447
450
  event: "playback paused",
448
451
  weight: 10,
452
+ isStrictEvent: false,
449
453
  properties: {
450
454
  "content_id": contentIds,
451
455
  "pause_reason": ["manual", "ad_break", "buffering", "notification"],
@@ -454,6 +458,7 @@ const config = {
454
458
  {
455
459
  event: "content rated",
456
460
  weight: 6,
461
+ isStrictEvent: false,
457
462
  properties: {
458
463
  "content_id": contentIds,
459
464
  "rating": u.weighNumRange(1, 5, 2, 4),
@@ -489,6 +494,7 @@ const config = {
489
494
  {
490
495
  event: "recommendation clicked",
491
496
  weight: 9,
497
+ isStrictEvent: false,
492
498
  properties: {
493
499
  "algorithm": ["collaborative_filtering", "content_based", "trending", "editorial"],
494
500
  "position": u.weighNumRange(1, 20),
@@ -504,6 +510,7 @@ const config = {
504
510
  {
505
511
  event: "ad impression",
506
512
  weight: 8,
513
+ isStrictEvent: false,
507
514
  properties: {
508
515
  "ad_type": ["pre_roll", "mid_roll", "banner", "interstitial"],
509
516
  "ad_duration_sec": u.weighNumRange(5, 30),
@@ -539,6 +546,7 @@ const config = {
539
546
  {
540
547
  event: "subtitle toggled",
541
548
  weight: 4,
549
+ isStrictEvent: false,
542
550
  properties: {
543
551
  "subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
544
552
  "action": ["enabled", "disabled", "changed"],
@@ -344,7 +344,8 @@ const config = {
344
344
  conversionRate: 40,
345
345
  timeToConvert: 24,
346
346
  weight: 5,
347
- name: "Tour Funnel"
347
+ name: "Tour Funnel",
348
+ reentry: true,
348
349
  },
349
350
  {
350
351
  sequence: ["property listed", "open house attended", "property sold"],
@@ -379,6 +380,7 @@ const config = {
379
380
  {
380
381
  event: "property viewed",
381
382
  weight: 12,
383
+ isStrictEvent: false,
382
384
  properties: {
383
385
  listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
384
386
  listing_price: u.weighNumRange(150000, 1500000, 0.4, 60),
@@ -422,6 +424,7 @@ const config = {
422
424
  {
423
425
  event: "tour scheduled",
424
426
  weight: 4,
427
+ isStrictEvent: false,
425
428
  properties: {
426
429
  listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
427
430
  agent_id: () => `AGT-${chance.integer({ min: 1000, max: 9999 })}`,
@@ -440,6 +443,7 @@ const config = {
440
443
  {
441
444
  event: "offer submitted",
442
445
  weight: 2,
446
+ isStrictEvent: false,
443
447
  properties: {
444
448
  listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
445
449
  offer_price: u.weighNumRange(200000, 1500000, 0.4, 40),
@@ -465,6 +469,7 @@ const config = {
465
469
  {
466
470
  event: "property listed",
467
471
  weight: 3,
472
+ isStrictEvent: false,
468
473
  properties: {
469
474
  listing_price: u.weighNumRange(200000, 1500000, 0.4, 50),
470
475
  property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
@@ -475,6 +480,7 @@ const config = {
475
480
  {
476
481
  event: "property sold",
477
482
  weight: 1,
483
+ isStrictEvent: false,
478
484
  properties: {
479
485
  sale_price: u.weighNumRange(200000, 1500000, 0.4, 40),
480
486
  days_on_market: u.weighNumRange(5, 180, 0.4, 30),
@@ -568,6 +568,7 @@ const config = {
568
568
  {
569
569
  event: "service deployed",
570
570
  weight: 10,
571
+ isStrictEvent: false,
571
572
  properties: {
572
573
  service_id: serviceIds,
573
574
  service_type: ["web_app", "api", "database", "cache", "queue", "ml_model"],
@@ -578,6 +579,7 @@ const config = {
578
579
  {
579
580
  event: "dashboard viewed",
580
581
  weight: 20,
582
+ isStrictEvent: false,
581
583
  properties: {
582
584
  dashboard_type: ["overview", "cost", "performance", "security", "custom"],
583
585
  time_range: ["1h", "6h", "24h", "7d", "30d"],
@@ -586,6 +588,7 @@ const config = {
586
588
  {
587
589
  event: "alert triggered",
588
590
  weight: 12,
591
+ isStrictEvent: false,
589
592
  properties: {
590
593
  alert_id: alertIds,
591
594
  severity: ["info", "warning", "critical", "emergency"],
@@ -609,6 +612,7 @@ const config = {
609
612
  {
610
613
  event: "alert acknowledged",
611
614
  weight: 8,
615
+ isStrictEvent: false,
612
616
  properties: {
613
617
  alert_id: alertIds,
614
618
  response_time_mins: u.weighNumRange(1, 120),
@@ -618,6 +622,7 @@ const config = {
618
622
  {
619
623
  event: "alert resolved",
620
624
  weight: 7,
625
+ isStrictEvent: false,
621
626
  properties: {
622
627
  alert_id: alertIds,
623
628
  resolution_time_mins: u.weighNumRange(5, 1440),
@@ -627,6 +632,7 @@ const config = {
627
632
  {
628
633
  event: "deployment pipeline run",
629
634
  weight: 9,
635
+ isStrictEvent: false,
630
636
  properties: {
631
637
  pipeline_id: pipelineIds,
632
638
  status: ["success", "failed", "cancelled"],
@@ -637,6 +643,7 @@ const config = {
637
643
  {
638
644
  event: "infrastructure scaled",
639
645
  weight: 5,
646
+ isStrictEvent: false,
640
647
  properties: {
641
648
  service_id: serviceIds,
642
649
  scale_direction: ["up", "up", "up", "down"],
@@ -648,6 +655,7 @@ const config = {
648
655
  {
649
656
  event: "cost report generated",
650
657
  weight: 4,
658
+ isStrictEvent: false,
651
659
  properties: {
652
660
  report_period: ["daily", "weekly", "monthly"],
653
661
  total_cost: u.weighNumRange(100, 50000),
@@ -657,6 +665,7 @@ const config = {
657
665
  {
658
666
  event: "team member invited",
659
667
  weight: 3,
668
+ isStrictEvent: false,
660
669
  properties: {
661
670
  role: ["admin", "editor", "viewer", "billing"],
662
671
  invitation_method: ["email", "sso", "slack"],
@@ -665,6 +674,7 @@ const config = {
665
674
  {
666
675
  event: "integration configured",
667
676
  weight: 4,
677
+ isStrictEvent: false,
668
678
  properties: {
669
679
  integration_type: ["slack", "pagerduty", "jira", "github", "datadog", "terraform"],
670
680
  status: ["active", "paused", "error"],
@@ -691,6 +701,7 @@ const config = {
691
701
  {
692
702
  event: "billing event",
693
703
  weight: 3,
704
+ isStrictEvent: false,
694
705
  properties: {
695
706
  event_type: ["invoice_generated", "invoice_generated", "payment_received", "payment_received", "payment_received", "payment_failed", "plan_upgraded", "plan_downgraded"],
696
707
  amount: u.weighNumRange(99, 25000),
@@ -718,6 +729,7 @@ const config = {
718
729
  {
719
730
  event: "documentation viewed",
720
731
  weight: 7,
732
+ isStrictEvent: false,
721
733
  properties: {
722
734
  doc_section: ["getting_started", "api_reference", "best_practices", "troubleshooting", "changelog"],
723
735
  time_on_page_sec: u.weighNumRange(5, 600),
@@ -415,6 +415,7 @@ const config = {
415
415
  {
416
416
  event: "post created",
417
417
  weight: 12,
418
+ isStrictEvent: false,
418
419
  properties: {
419
420
  "post_type": ["text", "image", "video", "poll", "link"],
420
421
  "character_count": u.weighNumRange(1, 280),
@@ -425,6 +426,7 @@ const config = {
425
426
  {
426
427
  event: "post viewed",
427
428
  weight: 30,
429
+ isStrictEvent: false,
428
430
  properties: {
429
431
  "post_type": ["text", "image", "video", "poll", "link"],
430
432
  "view_duration_sec": u.weighNumRange(1, 120, 0.3, 5),
@@ -434,6 +436,7 @@ const config = {
434
436
  {
435
437
  event: "post liked",
436
438
  weight: 18,
439
+ isStrictEvent: false,
437
440
  properties: {
438
441
  "post_type": ["text", "image", "video", "poll", "link"],
439
442
  }
@@ -441,6 +444,7 @@ const config = {
441
444
  {
442
445
  event: "post shared",
443
446
  weight: 6,
447
+ isStrictEvent: false,
444
448
  properties: {
445
449
  "share_destination": ["repost", "dm", "external", "copy_link"],
446
450
  }
@@ -448,6 +452,7 @@ const config = {
448
452
  {
449
453
  event: "comment posted",
450
454
  weight: 10,
455
+ isStrictEvent: false,
451
456
  properties: {
452
457
  "comment_length": u.weighNumRange(1, 500, 0.3, 20),
453
458
  "has_mention": [true, false, false],
@@ -456,6 +461,7 @@ const config = {
456
461
  {
457
462
  event: "user followed",
458
463
  weight: 8,
464
+ isStrictEvent: false,
459
465
  properties: {
460
466
  "discovery_source": ["suggested", "search", "post", "profile", "mutual"],
461
467
  }
@@ -470,6 +476,7 @@ const config = {
470
476
  {
471
477
  event: "story viewed",
472
478
  weight: 15,
479
+ isStrictEvent: false,
473
480
  properties: {
474
481
  "story_type": ["photo", "video", "text"],
475
482
  "view_duration_sec": u.weighNumRange(1, 30, 0.5, 5),
@@ -479,6 +486,7 @@ const config = {
479
486
  {
480
487
  event: "story created",
481
488
  weight: 5,
489
+ isStrictEvent: false,
482
490
  properties: {
483
491
  "story_type": ["photo", "video", "text"],
484
492
  "has_filter": [true, false],
@@ -504,6 +512,7 @@ const config = {
504
512
  {
505
513
  event: "dm sent",
506
514
  weight: 8,
515
+ isStrictEvent: false,
507
516
  properties: {
508
517
  "message_type": ["text", "image", "voice", "link"],
509
518
  "conversation_length": u.weighNumRange(1, 100),
@@ -342,6 +342,7 @@ const config = {
342
342
  {
343
343
  event: "hotel viewed",
344
344
  weight: 7,
345
+ isStrictEvent: false,
345
346
  properties: {
346
347
  hotel_id: chance.pickone.bind(chance, hotelIds),
347
348
  destination: chance.pickone.bind(chance, destinationCities),
@@ -364,6 +365,7 @@ const config = {
364
365
  {
365
366
  event: "booking completed",
366
367
  weight: 3,
368
+ isStrictEvent: false,
367
369
  properties: {
368
370
  hotel_id: chance.pickone.bind(chance, hotelIds),
369
371
  destination: chance.pickone.bind(chance, destinationCities),
@@ -379,6 +381,7 @@ const config = {
379
381
  {
380
382
  event: "room upgrade selected",
381
383
  weight: 2,
384
+ isStrictEvent: false,
382
385
  properties: {
383
386
  upgrade_type: ["suite", "ocean_view", "executive_floor", "club_lounge", "premium_king"],
384
387
  upgrade_cost: u.weighNumRange(25, 200, 0.4, 75),
@@ -415,6 +418,7 @@ const config = {
415
418
  {
416
419
  event: "review submitted",
417
420
  weight: 2,
421
+ isStrictEvent: false,
418
422
  properties: {
419
423
  hotel_id: chance.pickone.bind(chance, hotelIds),
420
424
  stay_rating: u.weighNumRange(1, 5, 0.7, 4),
@@ -504,6 +508,7 @@ const config = {
504
508
  order: "sequential",
505
509
  timeToConvert: 48,
506
510
  weight: 5,
511
+ reentry: true,
507
512
  },
508
513
  {
509
514
  name: "Full Stay Journey",
package/index.js CHANGED
@@ -157,31 +157,49 @@ async function runDungeon(config) {
157
157
 
158
158
  // Step 4: Generate ad spend data (if enabled)
159
159
  if (validatedConfig.hasAdSpend) {
160
+ context.reportProgress({ phase: "step", step: "adspend", status: "start" });
161
+ const _t4 = Date.now();
160
162
  await generateAdSpendData(context);
163
+ context.reportProgress({ phase: "step", step: "adspend", status: "complete", duration: Date.now() - _t4 });
161
164
  }
162
165
 
163
166
  if (context.config.verbose) logger.info('Starting user and event generation...');
164
167
  // Step 5: Main user and event generation
168
+ context.reportProgress({ phase: "step", step: "users", status: "start" });
169
+ const _t5 = Date.now();
165
170
  await userLoop(context);
171
+ context.reportProgress({ phase: "step", step: "users", status: "complete", duration: Date.now() - _t5 });
166
172
 
167
173
  // Step 6: Generate group profiles (if configured)
168
174
  if (validatedConfig.groupKeys && validatedConfig.groupKeys.length > 0) {
175
+ context.reportProgress({ phase: "step", step: "group-profiles", status: "start" });
176
+ const _t6 = Date.now();
169
177
  await generateGroupProfiles(context);
178
+ context.reportProgress({ phase: "step", step: "group-profiles", status: "complete", duration: Date.now() - _t6 });
170
179
  }
171
180
 
172
181
  // Step 7: Generate group SCDs (if configured)
173
182
  if (validatedConfig.scdProps && validatedConfig.groupKeys && validatedConfig.groupKeys.length > 0) {
183
+ context.reportProgress({ phase: "step", step: "group-scds", status: "start" });
184
+ const _t7 = Date.now();
174
185
  await generateGroupSCDs(context);
186
+ context.reportProgress({ phase: "step", step: "group-scds", status: "complete", duration: Date.now() - _t7 });
175
187
  }
176
188
 
177
189
  // Step 8: Generate lookup tables (if configured)
178
190
  if (validatedConfig.lookupTables && validatedConfig.lookupTables.length > 0) {
191
+ context.reportProgress({ phase: "step", step: "lookups", status: "start" });
192
+ const _t8 = Date.now();
179
193
  await generateLookupTables(context);
194
+ context.reportProgress({ phase: "step", step: "lookups", status: "complete", duration: Date.now() - _t8 });
180
195
  }
181
196
 
182
197
  // Step 9: Generate mirror datasets (if configured)
183
198
  if (validatedConfig.mirrorProps && Object.keys(validatedConfig.mirrorProps).length > 0) {
199
+ context.reportProgress({ phase: "step", step: "mirrors", status: "start" });
200
+ const _t9 = Date.now();
184
201
  await makeMirror(context);
202
+ context.reportProgress({ phase: "step", step: "mirrors", status: "complete", duration: Date.now() - _t9 });
185
203
  }
186
204
 
187
205
  if (context.config.verbose) logger.info('Data generation completed successfully');
@@ -191,21 +209,38 @@ async function runDungeon(config) {
191
209
  // Flush when writeToDisk is enabled OR batch mode activated (to capture tail data)
192
210
  const shouldFlush = validatedConfig.writeToDisk || context.isBatchMode();
193
211
 
194
- // Step 10: Flush lookup tables to disk (always as CSVs)
212
+ // Step 10-11: Flush to disk
195
213
  if (shouldFlush) {
214
+ context.reportProgress({ phase: "step", step: "flush", status: "start" });
215
+ const _tFlush = Date.now();
196
216
  await flushLookupTablesToDisk(storage, validatedConfig);
197
- }
198
-
199
- // Step 11: Flush other storage containers to disk
200
- if (shouldFlush) {
201
217
  await flushStorageToDisk(storage, validatedConfig);
218
+ context.reportProgress({ phase: "step", step: "flush", status: "complete", duration: Date.now() - _tFlush });
202
219
  }
203
220
 
204
221
  // Step 12: Send to Mixpanel (if token provided)
205
222
  // Now happens AFTER disk flush so batch files are available for import
206
223
  let importResults;
207
224
  if (validatedConfig.token) {
208
- importResults = await sendToMixpanel(context);
225
+ // Defensive guard: real Mixpanel project tokens are 32-char hex.
226
+ // Placeholder strings ("your-mixpanel-token", "test-token", "hello token!", etc.)
227
+ // trigger mixpanel-import's infinite retry loop and hang the entire
228
+ // process — historically a brutal source of silent test timeouts.
229
+ // If the token doesn't look like a real project token, warn loudly and skip the send.
230
+ // Set MP_BYPASS_TOKEN_CHECK=1 to override (e.g., legitimate non-standard tokens).
231
+ const looksReal = /^[0-9a-f]{32}$/i.test(String(validatedConfig.token).trim());
232
+ if (looksReal || process.env.MP_BYPASS_TOKEN_CHECK === '1') {
233
+ context.reportProgress({ phase: "step", step: "import", status: "start" });
234
+ const _t12 = Date.now();
235
+ importResults = await sendToMixpanel(context);
236
+ context.reportProgress({ phase: "step", step: "import", status: "complete", duration: Date.now() - _t12 });
237
+ } else {
238
+ console.warn(
239
+ `⚠️ Skipping Mixpanel import: token "${validatedConfig.token}" does not look like a real Mixpanel project token ` +
240
+ `(expected 32-char hex). Set a real token or pass empty string to skip. ` +
241
+ `Override with MP_BYPASS_TOKEN_CHECK=1 if your token is legitimately non-standard.`
242
+ );
243
+ }
209
244
  }
210
245
 
211
246
  // Step 13: Compile results
@@ -214,6 +249,8 @@ async function runDungeon(config) {
214
249
 
215
250
  const extractedData = extractStorageData(storage);
216
251
 
252
+ const progressSummary = context.getProgressSummary();
253
+
217
254
  return {
218
255
  ...extractedData,
219
256
  importResults,
@@ -221,7 +258,8 @@ async function runDungeon(config) {
221
258
  time: { start, end, delta, human },
222
259
  operations: context.getOperations(),
223
260
  eventCount: context.getStoredEventCount(),
224
- userCount: context.getUserCount()
261
+ userCount: context.getUserCount(),
262
+ ...(progressSummary.updates > 0 || progressSummary.errors > 0 ? { progress: progressSummary } : {})
225
263
  };
226
264
 
227
265
  } catch (error) {