@ak--47/dungeon-master 1.4.5 → 1.5.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 (78) 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 +182 -0
  9. package/HOOKS.md +1256 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +41 -49
  12. package/dungeons/technical/anonymous-users.js +38 -36
  13. package/dungeons/technical/array-of-object-lookup.js +136 -153
  14. package/dungeons/technical/datagen-v15-verify.js +87 -0
  15. package/dungeons/technical/experiments.js +42 -40
  16. package/dungeons/technical/foobar.js +114 -118
  17. package/dungeons/technical/group-analytics.js +42 -40
  18. package/dungeons/technical/hook-helpers-verify.js +69 -50
  19. package/dungeons/technical/identity-model-verify.js +22 -12
  20. package/dungeons/technical/mirror-strategies.js +37 -39
  21. package/dungeons/technical/nested-objects.js +119 -118
  22. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  23. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  24. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  25. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  26. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  27. package/dungeons/technical/retention-cadence.js +115 -112
  28. package/dungeons/technical/sanity.js +86 -80
  29. package/dungeons/technical/scale-test.js +34 -38
  30. package/dungeons/technical/scd.js +111 -128
  31. package/dungeons/technical/simple.js +134 -141
  32. package/dungeons/technical/simplest.js +111 -65
  33. package/dungeons/technical/text-generation.js +110 -146
  34. package/dungeons/vertical/ai-platform.js +300 -333
  35. package/dungeons/vertical/community.js +290 -255
  36. package/dungeons/vertical/crypto.js +400 -391
  37. package/dungeons/vertical/dating.js +421 -375
  38. package/dungeons/vertical/devtools.js +346 -298
  39. package/dungeons/vertical/ecommerce.js +322 -394
  40. package/dungeons/vertical/education.js +380 -325
  41. package/dungeons/vertical/fintech.js +371 -325
  42. package/dungeons/vertical/fitness.js +345 -291
  43. package/dungeons/vertical/food-delivery.js +352 -307
  44. package/dungeons/vertical/gaming.js +490 -444
  45. package/dungeons/vertical/healthcare.js +311 -262
  46. package/dungeons/vertical/insurance-application.js +437 -409
  47. package/dungeons/vertical/logistics.js +278 -252
  48. package/dungeons/vertical/marketplace.js +340 -323
  49. package/dungeons/vertical/media.js +390 -335
  50. package/dungeons/vertical/real-estate.js +402 -347
  51. package/dungeons/vertical/sass.js +331 -333
  52. package/dungeons/vertical/social.js +377 -316
  53. package/dungeons/vertical/travel.js +302 -295
  54. package/index.js +64 -7
  55. package/lib/core/config-validator.js +378 -17
  56. package/lib/core/dungeon-loader.js +2 -5
  57. package/lib/generators/events.js +12 -13
  58. package/lib/generators/funnels.js +76 -2
  59. package/lib/hook-helpers/index.js +1 -0
  60. package/lib/hook-helpers/inject.js +95 -0
  61. package/lib/orchestrators/mixpanel-sender.js +7 -0
  62. package/lib/orchestrators/user-loop.js +598 -48
  63. package/lib/templates/defaults.js +59 -59
  64. package/lib/templates/macro-presets.js +53 -11
  65. package/lib/utils/dataset-context.js +103 -0
  66. package/lib/utils/retention-curve.js +140 -0
  67. package/lib/utils/utils.js +157 -109
  68. package/lib/verify/counting.js +360 -0
  69. package/lib/verify/emulate-breakdown.js +531 -108
  70. package/lib/verify/funnel-engine.js +539 -0
  71. package/lib/verify/identity.js +78 -0
  72. package/lib/verify/index.js +20 -0
  73. package/lib/verify/schema-validator.js +3 -1
  74. package/lib/verify/verify-dungeon.js +58 -0
  75. package/package.json +14 -3
  76. package/scripts/run-dungeon.mjs +12 -1
  77. package/types.d.ts +353 -4
  78. package/scripts/smoke-test-all.mjs +0 -162
@@ -1,68 +1,46 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "dm4-devtools";
3
- const num_days = 120;
4
- const num_users = 10_000;
5
- const avg_events_per_user_per_day = 1.2;
6
- let token = "your-mixpanel-token";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
1
+ // ── IMPORTS ──
11
2
  import dayjs from "dayjs";
12
3
  import utc from "dayjs/plugin/utc.js";
4
+ dayjs.extend(utc);
13
5
  import "dotenv/config";
14
6
  import * as u from "../../lib/utils/utils.js";
15
7
  import * as v from "ak-tools";
8
+ /** @typedef {import("../../types").Dungeon} Config */
16
9
 
17
- dayjs.extend(utc);
18
- const chance = u.initChance(SEED);
19
- /** @typedef {import("../../types").Dungeon} Config */
20
-
21
- // Generate consistent pipeline/repo IDs at module level
22
- const pipelineIds = v.range(1, 80).map(() => `PIPE_${v.uid(6)}`);
23
- const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
24
-
25
- /**
26
- * ===============================================================
27
- * DATASET OVERVIEW
28
- * ===============================================================
10
+ // ── OVERVIEW ──
11
+ /*
12
+ * NAME: CodeForge
13
+ * APP: Developer platform for builds, deploys, monitoring, code review,
14
+ * and team collaboration. Think GitHub + Vercel + PagerDuty in a
15
+ * unified CI/CD experience. Multi-role devs ship code through a
16
+ * connect-repo configure-pipeline build → deploy → monitor loop.
17
+ * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
18
+ * CORE LOOP: connect repo → configure pipeline → build → deploy → monitor
29
19
  *
30
- * CodeForge -- a developer platform for builds, deploys, monitoring,
31
- * code review, and team collaboration. Think GitHub + Vercel + PagerDuty
32
- * in a unified CI/CD experience.
33
- *
34
- * - 5,000 users over 100 days, ~600K events
35
- * - Multi-role system: platform engineers (15%), full-stack devs (35%),
36
- * junior devs (30%), devops leads (10%), open source users (10%)
37
- * - Core loop: connect repo -> configure pipeline -> build -> deploy -> monitor
38
- * - Revenue: free / team ($25, 14-day trial) / business ($75) / enterprise ($200)
39
- *
40
- * Advanced Features:
41
- * - Personas: 5 archetypes with distinct activity, conversion, and churn profiles
42
- * - World Events: major outage (day 42), conference launch (day 60)
43
- * - Data Quality: late arrivals, duplicates, bot pollution
44
- * - Subscription: 4-tier revenue lifecycle with trial conversion
45
- * - Geo: US/EU/India/rest with timezone-aware activity
46
- * - Features: copilot_integration (day 30), deployment_preview (day 50)
47
- * - Anomalies: extreme build durations, alert burst during outage
48
- *
49
- * Key entities:
50
- * - pipeline_id: CI/CD pipeline for a repo
51
- * - repo_id: source code repository
52
- * - build_status / deploy_status: success/failed/cancelled
53
- * - ai_assist: manual vs copilot (driven by Feature rollout)
54
- */
55
-
56
- /**
57
- * ===============================================================
58
- * ANALYTICS HOOKS (10 hooks)
20
+ * EVENTS (18):
21
+ * build completed (8) > app session (8) > pull request created (6)
22
+ * > notification received (6) > deployment completed (5)
23
+ * > code review completed (5) > monitoring dashboard viewed (5)
24
+ * > alert triggered (4) > log searched (4) > incident created (2)
25
+ * > incident resolved (2) > repository connected (2) > pipeline configured (2)
26
+ * > collaboration invited (2) > environment created (2) > account created (1)
27
+ * > billing updated (1) > account deactivated (1)
59
28
  *
60
- * Adds 10. BUILD-DEPLOY TIME-TO-CONVERT: enterprise/business 0.67x faster, free
61
- * 1.33x slower (funnel-post). Discover via Build-Deploy Pipeline median TTC by tier.
62
- * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
63
- * Cross-event MINMIN SQL queries on raw events do NOT show this.
64
- * ===============================================================
29
+ * FUNNELS (5):
30
+ * - Onboarding: account created repository connected pipeline configured build completed (45%)
31
+ * - Build-Deploy Pipeline: build completed deployment completed monitoring dashboard viewed (40%)
32
+ * - PR Review Flow: pull request created code review completed build completed deployment completed (35%)
33
+ * - Incident Response: alert triggered → incident created → incident resolved (50%)
34
+ * - Upgrade Path: app session → billing updated → collaboration invited (20%)
65
35
  *
36
+ * USER PROPS: dev_role, segment, team_size, repos_connected, org_name, experience_level, subscription_tier, Platform, language
37
+ * SUPER PROPS: subscription_tier, Platform, language
38
+ * SCD PROPS: subscription_tier (free/pro/enterprise, monthly fuzzy, max 6)
39
+ * GROUPS: none
40
+ */
41
+
42
+ // ── HOOK STORIES ──
43
+ /*
66
44
  * NOTE: All cohort effects are HIDDEN — discoverable only via behavioral
67
45
  * cohorts or raw-prop breakdowns. No cohort flag is stamped on events.
68
46
  *
@@ -281,6 +259,11 @@ const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
281
259
  * - Breakdown: "subscription_tier" (superProp)
282
260
  * - Expected: enterprise/business ~ 0.67x baseline; free ~ 1.33x baseline
283
261
  *
262
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
263
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
264
+ * show this — funnel-post adjusts gaps within funnel instances, not
265
+ * across the user's full event history.
266
+ *
284
267
  * REAL-WORLD ANALOGUE: Enterprise CI/CD customers get priority build
285
268
  * runners and dedicated deploy infrastructure, yielding faster
286
269
  * end-to-end pipeline throughput.
@@ -304,31 +287,304 @@ const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
304
287
  * Build-Deploy TTC | median TTC by tier | 1x | 0.67/1.33x| ~ 2x range
305
288
  */
306
289
 
290
+ // ── SCALE ──
291
+ const SEED = "dm4-devtools";
292
+ const NUM_USERS = 10_000;
293
+ const DATASET_START = "2026-01-01T00:00:00Z";
294
+ const DATASET_END = "2026-05-01T23:59:59Z";
295
+ const EVENTS_PER_DAY = 1.2;
296
+ const token = process.env.MP_TOKEN || "your-mixpanel-token";
297
+
298
+ const chance = u.initChance(SEED);
299
+
300
+ // ── KNOBS (tweak these to reshape stories) ──
301
+ const BUILD_FAILURE_DURATION_MULT = 2;
302
+
303
+ const NIGHT_DEPLOY_HOUR_START = 22;
304
+ const NIGHT_DEPLOY_HOUR_END = 6;
305
+ const NIGHT_DEPLOY_FAIL_LIKELIHOOD = 40;
306
+
307
+ const COPILOT_USER_HASH_MOD = 10;
308
+ const COPILOT_USER_HASH_THRESHOLD = 3;
309
+ const COPILOT_PR_CLONE_RATE = 0.5;
310
+
311
+ const ONCALL_ALERT_THRESHOLD = 20;
312
+ const ONCALL_FATIGUE_DIVISOR = 20;
313
+ const ONCALL_FATIGUE_CAP = 3;
314
+
315
+ const OSS_EVENT_THRESHOLD = 15;
316
+ const OSS_CONVERSION_POINT_PCT = 0.7;
317
+ const OSS_BUILD_CLONE_LIKELIHOOD = 30;
318
+ const OSS_DEPLOY_CLONE_LIKELIHOOD = 20;
319
+
320
+ const RECOVERY_START_DAY = 44;
321
+ const RECOVERY_END_DAY = 48;
322
+ const RECOVERY_CLONES_PER_EVENT = 3;
323
+
324
+ const ENTERPRISE_DROP_LIKELIHOOD = 35;
325
+
326
+ const BUILD_SWEET_MIN = 15;
327
+ const BUILD_SWEET_MAX = 30;
328
+ const BUILD_OVER_THRESHOLD = 31;
329
+ const BUILD_SWEET_CLONE_RATE = 0.5;
330
+ const BUILD_OVER_DROP_LIKELIHOOD = 40;
331
+
332
+ const TTC_FAST_FACTOR = 0.67;
333
+ const TTC_SLOW_FACTOR = 1.33;
334
+
335
+ // ── DATA ARRAYS ──
336
+ // Generate consistent pipeline/repo IDs at module level
337
+ const pipelineIds = v.range(1, 80).map(() => `PIPE_${v.uid(6)}`);
338
+ const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
339
+
340
+ // ── HELPER FUNCTIONS ──
341
+ function handleFunnelPostHooks(record, meta) {
342
+ // H10: BUILD-DEPLOY TIME-TO-CONVERT
343
+ // Enterprise tier completes Build-Deploy Pipeline funnel 1.5x faster
344
+ // (factor 0.67); free tier 1.33x slower (factor 1.33).
345
+ const segment = meta?.profile?.subscription_tier;
346
+ if (Array.isArray(record) && record.length > 1) {
347
+ const factor = (
348
+ segment === "enterprise" || segment === "business" ? TTC_FAST_FACTOR :
349
+ segment === "free" ? TTC_SLOW_FACTOR :
350
+ 1.0
351
+ );
352
+ if (factor !== 1.0) {
353
+ for (let i = 1; i < record.length; i++) {
354
+ const prev = dayjs(record[i - 1].time);
355
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
356
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
357
+ }
358
+ }
359
+ }
360
+ return record;
361
+ }
362
+
363
+ function handleUserHooks(record) {
364
+ // H7: DEVOPS LEAD PROFILE ENRICHMENT
365
+ // DevOps leads get team_size 10-50 and repos_connected 5-20.
366
+ // Platform engineers get moderate boosts. Others stay at defaults.
367
+ if (record.segment === "devops") {
368
+ record.team_size = chance.integer({ min: 10, max: 50 });
369
+ record.repos_connected = chance.integer({ min: 5, max: 20 });
370
+ record.experience_level = "senior";
371
+ } else if (record.segment === "platform_eng") {
372
+ record.team_size = chance.integer({ min: 5, max: 25 });
373
+ record.repos_connected = chance.integer({ min: 3, max: 15 });
374
+ record.experience_level = chance.pickone(["mid", "senior"]);
375
+ } else if (record.segment === "junior") {
376
+ record.team_size = chance.integer({ min: 1, max: 8 });
377
+ record.repos_connected = chance.integer({ min: 0, max: 3 });
378
+ record.experience_level = "junior";
379
+ }
380
+ return record;
381
+ }
382
+
383
+ function handleEventHooks(record) {
384
+ // H1: BUILD FAILURE CASCADE
385
+ // Failed builds get 2x duration (retries take longer).
386
+ if (record.event === "build completed" && record.build_status === "failed") {
387
+ record.build_duration_sec = Math.floor((record.build_duration_sec || 240) * BUILD_FAILURE_DURATION_MULT);
388
+ }
389
+ // (HOOK 2: NIGHT DEPLOY RISK moved to everything hook — hour checks
390
+ // must run after bunchIntoSessions redistributes timestamps)
391
+ return record;
392
+ }
393
+
394
+ function handleEverythingHooks(record, meta) {
395
+ const datasetStart = dayjs.unix(meta.datasetStart);
396
+ let events = record;
397
+ if (!events.length) return record;
398
+ const profile = meta && meta.profile ? meta.profile : {};
399
+
400
+ // SUPERPROP STAMPING
401
+ // Stamp superProp values from profile onto every event so they stay
402
+ // consistent per-user instead of randomizing per-event.
403
+ events.forEach(e => {
404
+ if (profile.subscription_tier) e.subscription_tier = profile.subscription_tier;
405
+ if (profile.Platform) e.Platform = profile.Platform;
406
+ if (profile.language) e.language = profile.language;
407
+ });
408
+
409
+ // H2: NIGHT DEPLOY RISK
410
+ // Deployments between 10PM-6AM get deploy_status forced to "failed"
411
+ // 40% of the time. No flag — analyst breaks down by hour-of-day.
412
+ events.forEach(e => {
413
+ if (e.event === "deployment completed") {
414
+ const hour = new Date(e.time).getUTCHours();
415
+ if ((hour >= NIGHT_DEPLOY_HOUR_START || hour < NIGHT_DEPLOY_HOUR_END) && chance.bool({ likelihood: NIGHT_DEPLOY_FAIL_LIKELIHOOD })) {
416
+ e.deploy_status = "failed";
417
+ }
418
+ }
419
+ });
420
+
421
+ // H8: ENTERPRISE BUILD-DEPLOY FUNNEL LIFT
422
+ // Free-tier users drop 35% of final funnel step events to create
423
+ // visible conversion gap vs paid subscribers.
424
+ if (profile.subscription_tier !== "enterprise" && profile.subscription_tier !== "business") {
425
+ events = events.filter(e => {
426
+ if (e.event === "monitoring dashboard viewed" && chance.bool({ likelihood: ENTERPRISE_DROP_LIKELIHOOD })) return false;
427
+ return true;
428
+ });
429
+ }
430
+
431
+ // H3: COPILOT PR VELOCITY
432
+ // ~30% of users are copilot adopters (hash-based cohort).
433
+ // Copilot users get ai_assist="copilot" stamped and 1.5x more PRs.
434
+ const uid = events[0]?.user_id || "";
435
+ const isCopilotUser = (typeof uid === "string" ? uid.charCodeAt(0) : uid) % COPILOT_USER_HASH_MOD < COPILOT_USER_HASH_THRESHOLD;
436
+ if (isCopilotUser) {
437
+ events.forEach(e => {
438
+ if (e.event === "pull request created" || e.event === "code review completed") {
439
+ e.ai_assist = "copilot";
440
+ }
441
+ });
442
+ const prEvents = events.filter(e => e.event === "pull request created");
443
+ const extraCount = Math.floor(prEvents.length * COPILOT_PR_CLONE_RATE);
444
+ for (let i = 0; i < extraCount; i++) {
445
+ const templateEvent = prEvents[i % prEvents.length];
446
+ if (templateEvent) {
447
+ events.push({
448
+ ...templateEvent,
449
+ time: dayjs(templateEvent.time).add(chance.integer({ min: 1, max: 12 }), "hours").toISOString(),
450
+ user_id: templateEvent.user_id,
451
+ ai_assist: "copilot",
452
+ files_changed: chance.integer({ min: 1, max: 30 }),
453
+ lines_added: chance.integer({ min: 10, max: 800 }),
454
+ });
455
+ }
456
+ }
457
+ }
458
+
459
+ // H4: ON-CALL ROTATION FATIGUE
460
+ // Users with >20 alerts get increasing response_time_minutes.
461
+ const alertCount = events.filter(e => e.event === "alert triggered").length;
462
+ if (alertCount > ONCALL_ALERT_THRESHOLD) {
463
+ const fatigueMultiplier = 1 + Math.min(alertCount / ONCALL_FATIGUE_DIVISOR, ONCALL_FATIGUE_CAP);
464
+ events.forEach(e => {
465
+ if (e.event === "incident resolved" && e.response_time_minutes) {
466
+ e.response_time_minutes = Math.floor(e.response_time_minutes * fatigueMultiplier);
467
+ }
468
+ if (e.event === "incident created" && e.response_time_minutes) {
469
+ e.response_time_minutes = Math.floor(e.response_time_minutes * fatigueMultiplier);
470
+ }
471
+ });
472
+ }
473
+
474
+ // H5: OPEN SOURCE POWER USAGE
475
+ // OSS users with >15 events get extra cloned build + deploy events
476
+ // in their later activity (representing power usage that pushes them
477
+ // toward limits). No flag — discover via cohort by segment + event count.
478
+ if (profile.segment === "oss_user" && events.length > OSS_EVENT_THRESHOLD) {
479
+ const buildTemplate = events.find(e => e.event === "build completed");
480
+ const deployTemplate = events.find(e => e.event === "deployment completed");
481
+ if (buildTemplate || deployTemplate) {
482
+ const conversionPoint = Math.floor(events.length * OSS_CONVERSION_POINT_PCT);
483
+ const tail = events.slice(conversionPoint);
484
+ tail.forEach(e => {
485
+ const tBase = dayjs(e.time);
486
+ if (buildTemplate && chance.bool({ likelihood: OSS_BUILD_CLONE_LIKELIHOOD })) {
487
+ events.push({
488
+ ...buildTemplate,
489
+ time: tBase.add(chance.integer({ min: 5, max: 240 }), "minutes").toISOString(),
490
+ user_id: e.user_id,
491
+ });
492
+ }
493
+ if (deployTemplate && chance.bool({ likelihood: OSS_DEPLOY_CLONE_LIKELIHOOD })) {
494
+ events.push({
495
+ ...deployTemplate,
496
+ time: tBase.add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
497
+ user_id: e.user_id,
498
+ });
499
+ }
500
+ });
501
+ }
502
+ }
503
+
504
+ // H9: BUILD-COUNT MAGIC NUMBER (no flags)
505
+ // Sweet 15-30 builds → +50% deploys (clone with unique offset).
506
+ // Over 31+ → drop 40% of deploys (flaky CI burnout).
507
+ const buildCount = events.filter(e => e.event === "build completed").length;
508
+ if (buildCount >= BUILD_SWEET_MIN && buildCount <= BUILD_SWEET_MAX) {
509
+ const deploys = events.filter(e => e.event === "deployment completed");
510
+ const extras = Math.max(Math.floor(deploys.length * BUILD_SWEET_CLONE_RATE), 1);
511
+ for (let k = 0; k < extras; k++) {
512
+ const tpl = deploys[k % deploys.length];
513
+ if (tpl) {
514
+ events.push({
515
+ ...tpl,
516
+ time: dayjs(tpl.time).add(chance.integer({ min: 5, max: 360 }), "minutes").toISOString(),
517
+ user_id: tpl.user_id,
518
+ });
519
+ }
520
+ }
521
+ } else if (buildCount >= BUILD_OVER_THRESHOLD) {
522
+ for (let i = events.length - 1; i >= 0; i--) {
523
+ if (events[i].event === "deployment completed" && chance.bool({ likelihood: BUILD_OVER_DROP_LIKELIHOOD })) {
524
+ events.splice(i, 1);
525
+ }
526
+ }
527
+ }
528
+
529
+ // H6: POST-OUTAGE RECOVERY
530
+ // After the outage volume rebound (d44-48), deployment events get
531
+ // aggressively cloned to produce a visible spike above baseline.
532
+ // Shifted later than outage end (d42.25) so natural volume has
533
+ // recovered from the 0.05x suppression before cloning kicks in.
534
+ const RECOVERY_START = datasetStart.add(RECOVERY_START_DAY, "days");
535
+ const RECOVERY_END = datasetStart.add(RECOVERY_END_DAY, "days");
536
+ const deployEvents = events.filter(e => {
537
+ if (e.event !== "deployment completed") return false;
538
+ const t = dayjs(e.time);
539
+ return t.isAfter(RECOVERY_START) && t.isBefore(RECOVERY_END);
540
+ });
541
+ deployEvents.forEach(dep => {
542
+ // 100% clone rate with 3 copies per event to clearly
543
+ // exceed baseline deploy volume (d35-41)
544
+ for (let c = 0; c < RECOVERY_CLONES_PER_EVENT; c++) {
545
+ events.push({
546
+ ...dep,
547
+ time: dayjs(dep.time).add(chance.integer({ min: 1, max: 8 }), "hours").toISOString(),
548
+ user_id: dep.user_id,
549
+ deploy_status: chance.pickone(["success", "success", "rolled_back"]),
550
+ environment: "production",
551
+ });
552
+ }
553
+ });
554
+
555
+ return events;
556
+ }
557
+
558
+ // ── CONFIG ──
307
559
  /** @type {Config} */
308
560
  const config = {
309
561
  version: 2,
310
- token,
311
562
  seed: SEED,
312
- datasetStart: "2026-01-01T00:00:00Z",
313
- datasetEnd: "2026-05-01T23:59:59Z",
314
- // numDays: num_days,
315
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
316
- numUsers: num_users,
317
- hasAnonIds: true,
318
- avgDevicePerUser: 2,
319
- hasSessionIds: true,
563
+ datasetStart: DATASET_START,
564
+ datasetEnd: DATASET_END,
565
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
566
+ numUsers: NUM_USERS,
320
567
  format: "json",
321
568
  gzip: true,
322
- alsoInferFunnels: false,
323
- hasLocation: true,
324
- hasAndroidDevices: false,
325
- hasIOSDevices: false,
326
- hasDesktopDevices: true,
327
- hasBrowser: true,
328
- hasCampaigns: false,
329
- isAnonymous: false,
330
- hasAdSpend: false,
331
- hasAvatar: true,
569
+ credentials: {
570
+ token,
571
+ },
572
+ switches: {
573
+ hasSessionIds: true,
574
+ alsoInferFunnels: false,
575
+ hasLocation: true,
576
+ hasAndroidDevices: false,
577
+ hasIOSDevices: false,
578
+ hasDesktopDevices: true,
579
+ hasBrowser: true,
580
+ hasCampaigns: false,
581
+ isAnonymous: false,
582
+ hasAdSpend: false,
583
+ hasAvatar: true,
584
+ },
585
+ identity: {
586
+ avgDevicePerUser: 2,
587
+ },
332
588
  concurrency: 1,
333
589
  writeToDisk: false,
334
590
  scdProps: {
@@ -356,6 +612,7 @@ const config = {
356
612
  {
357
613
  event: "build completed",
358
614
  weight: 8,
615
+ isStrictEvent: false,
359
616
  properties: {
360
617
  pipeline_id: chance.pickone.bind(chance, pipelineIds),
361
618
  repo_id: chance.pickone.bind(chance, repoIds),
@@ -369,6 +626,7 @@ const config = {
369
626
  {
370
627
  event: "deployment completed",
371
628
  weight: 5,
629
+ isStrictEvent: false,
372
630
  properties: {
373
631
  pipeline_id: chance.pickone.bind(chance, pipelineIds),
374
632
  repo_id: chance.pickone.bind(chance, repoIds),
@@ -381,6 +639,7 @@ const config = {
381
639
  {
382
640
  event: "pull request created",
383
641
  weight: 6,
642
+ isStrictEvent: false,
384
643
  properties: {
385
644
  repo_id: chance.pickone.bind(chance, repoIds),
386
645
  pr_size: ["small", "small", "medium", "medium", "large", "xlarge"],
@@ -393,6 +652,7 @@ const config = {
393
652
  {
394
653
  event: "code review completed",
395
654
  weight: 5,
655
+ isStrictEvent: false,
396
656
  properties: {
397
657
  repo_id: chance.pickone.bind(chance, repoIds),
398
658
  review_result: ["approved", "approved", "approved", "changes_requested", "commented"],
@@ -404,6 +664,7 @@ const config = {
404
664
  {
405
665
  event: "alert triggered",
406
666
  weight: 4,
667
+ isStrictEvent: false,
407
668
  properties: {
408
669
  alert_type: ["error_rate", "latency", "cpu", "memory", "disk", "custom_metric"],
409
670
  severity: ["info", "warning", "warning", "critical", "critical"],
@@ -414,6 +675,7 @@ const config = {
414
675
  {
415
676
  event: "incident created",
416
677
  weight: 2,
678
+ isStrictEvent: false,
417
679
  properties: {
418
680
  severity: ["sev1", "sev2", "sev2", "sev3", "sev3", "sev3"],
419
681
  service: ["api", "web", "worker", "database", "cdn", "auth"],
@@ -424,6 +686,7 @@ const config = {
424
686
  {
425
687
  event: "incident resolved",
426
688
  weight: 2,
689
+ isStrictEvent: false,
427
690
  properties: {
428
691
  severity: ["sev1", "sev2", "sev2", "sev3", "sev3", "sev3"],
429
692
  resolution_time_minutes: u.weighNumRange(5, 480, 0.3, 60),
@@ -461,6 +724,7 @@ const config = {
461
724
  {
462
725
  event: "monitoring dashboard viewed",
463
726
  weight: 5,
727
+ isStrictEvent: false,
464
728
  properties: {
465
729
  dashboard_type: ["overview", "performance", "errors", "deploys", "custom"],
466
730
  time_range: ["1h", "6h", "24h", "7d", "30d"],
@@ -541,6 +805,7 @@ const config = {
541
805
  order: "sequential",
542
806
  timeToConvert: 48,
543
807
  weight: 5,
808
+ reentry: true,
544
809
  },
545
810
  {
546
811
  name: "PR Review Flow",
@@ -557,6 +822,7 @@ const config = {
557
822
  order: "sequential",
558
823
  timeToConvert: 24,
559
824
  weight: 3,
825
+ reentry: true,
560
826
  },
561
827
  {
562
828
  name: "Upgrade Path",
@@ -772,229 +1038,11 @@ const config = {
772
1038
  ],
773
1039
  },
774
1040
 
775
- // -- Hook Function ----------------------------------------
776
- hook: function (record, type, meta) {
777
- // HOOK 10 (T2C): BUILD-DEPLOY TIME-TO-CONVERT (funnel-post)
778
- // Enterprise tier completes Build-Deploy Pipeline funnel 1.5x faster
779
- // (factor 0.67); free tier 1.33x slower (factor 1.33).
780
- if (type === "funnel-post") {
781
- const segment = meta?.profile?.subscription_tier;
782
- if (Array.isArray(record) && record.length > 1) {
783
- const factor = (
784
- segment === "enterprise" || segment === "business" ? 0.67 :
785
- segment === "free" ? 1.33 :
786
- 1.0
787
- );
788
- if (factor !== 1.0) {
789
- for (let i = 1; i < record.length; i++) {
790
- const prev = dayjs(record[i - 1].time);
791
- const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
792
- record[i].time = prev.add(newGap, "milliseconds").toISOString();
793
- }
794
- }
795
- }
796
- }
797
-
798
- // -- HOOK 7: DEVOPS LEAD PROFILE ENRICHMENT (user) --------
799
- // DevOps leads get team_size 10-50 and repos_connected 5-20.
800
- // Platform engineers get moderate boosts. Others stay at defaults.
801
- if (type === "user") {
802
- if (record.segment === "devops") {
803
- record.team_size = chance.integer({ min: 10, max: 50 });
804
- record.repos_connected = chance.integer({ min: 5, max: 20 });
805
- record.experience_level = "senior";
806
- } else if (record.segment === "platform_eng") {
807
- record.team_size = chance.integer({ min: 5, max: 25 });
808
- record.repos_connected = chance.integer({ min: 3, max: 15 });
809
- record.experience_level = chance.pickone(["mid", "senior"]);
810
- } else if (record.segment === "junior") {
811
- record.team_size = chance.integer({ min: 1, max: 8 });
812
- record.repos_connected = chance.integer({ min: 0, max: 3 });
813
- record.experience_level = "junior";
814
- }
815
- }
816
-
817
- // -- HOOK 8: ENTERPRISE BUILD-DEPLOY FUNNEL LIFT
818
- // Conversion differences handled in everything hook via event filtering.
819
- // (funnel-pre conversionRate modifications are diluted by organic events)
820
-
821
- // -- HOOK 1: BUILD FAILURE CASCADE (event) ----------------
822
- // Failed builds get 2x duration (retries take longer).
823
- if (type === "event") {
824
- if (record.event === "build completed" && record.build_status === "failed") {
825
- record.build_duration_sec = Math.floor((record.build_duration_sec || 240) * 2);
826
- }
827
-
828
- // (HOOK 2: NIGHT DEPLOY RISK moved to everything hook — hour checks
829
- // must run after bunchIntoSessions redistributes timestamps)
830
- }
831
-
832
- // -- EVERYTHING HOOKS -------------------------------------
833
- if (type === "everything") {
834
- const datasetStart = dayjs.unix(meta.datasetStart);
835
- let events = record;
836
- if (!events.length) return record;
837
- const profile = meta && meta.profile ? meta.profile : {};
838
-
839
- // -- SUPERPROP STAMPING -------------------------------
840
- // Stamp superProp values from profile onto every event so
841
- // they stay consistent per-user instead of randomizing per-event.
842
- events.forEach(e => {
843
- if (profile.subscription_tier) e.subscription_tier = profile.subscription_tier;
844
- if (profile.Platform) e.Platform = profile.Platform;
845
- if (profile.language) e.language = profile.language;
846
- });
847
-
848
- // -- HOOK 2: NIGHT DEPLOY RISK -------------------------
849
- // Deployments between 10PM-6AM get deploy_status forced to
850
- // "failed" 40% of the time. No flag — analyst breaks down by
851
- // hour-of-day on deployment completed events.
852
- events.forEach(e => {
853
- if (e.event === "deployment completed") {
854
- const hour = new Date(e.time).getUTCHours();
855
- if ((hour >= 22 || hour < 6) && chance.bool({ likelihood: 40 })) {
856
- e.deploy_status = "failed";
857
- }
858
- }
859
- });
860
-
861
- // -- HOOK 8: ENTERPRISE BUILD-DEPLOY FUNNEL LIFT ------
862
- // Free-tier users drop 35% of final funnel step events to
863
- // create visible conversion gap vs paid subscribers.
864
- if (profile.subscription_tier !== "enterprise" && profile.subscription_tier !== "business") {
865
- events = events.filter(e => {
866
- if (e.event === "monitoring dashboard viewed" && chance.bool({ likelihood: 35 })) return false;
867
- return true;
868
- });
869
- }
870
-
871
- // -- HOOK 3: COPILOT PR VELOCITY ----------------------
872
- // ~30% of users are copilot adopters (hash-based cohort).
873
- // Copilot users get ai_assist="copilot" stamped and 1.5x more PRs.
874
- const uid = events[0]?.user_id || "";
875
- const isCopilotUser = (typeof uid === "string" ? uid.charCodeAt(0) : uid) % 10 < 3;
876
- if (isCopilotUser) {
877
- events.forEach(e => {
878
- if (e.event === "pull request created" || e.event === "code review completed") {
879
- e.ai_assist = "copilot";
880
- }
881
- });
882
- const prEvents = events.filter(e => e.event === "pull request created");
883
- const extraCount = Math.floor(prEvents.length * 0.5);
884
- for (let i = 0; i < extraCount; i++) {
885
- const templateEvent = prEvents[i % prEvents.length];
886
- if (templateEvent) {
887
- events.push({
888
- ...templateEvent,
889
- time: dayjs(templateEvent.time).add(chance.integer({ min: 1, max: 12 }), "hours").toISOString(),
890
- user_id: templateEvent.user_id,
891
- ai_assist: "copilot",
892
- files_changed: chance.integer({ min: 1, max: 30 }),
893
- lines_added: chance.integer({ min: 10, max: 800 }),
894
- });
895
- }
896
- }
897
- }
898
-
899
- // -- HOOK 4: ON-CALL ROTATION FATIGUE -----------------
900
- // Users with >20 alerts get increasing response_time_minutes.
901
- const alertCount = events.filter(e => e.event === "alert triggered").length;
902
- if (alertCount > 20) {
903
- const fatigueMultiplier = 1 + Math.min(alertCount / 20, 3);
904
- events.forEach(e => {
905
- if (e.event === "incident resolved" && e.response_time_minutes) {
906
- e.response_time_minutes = Math.floor(e.response_time_minutes * fatigueMultiplier);
907
- }
908
- if (e.event === "incident created" && e.response_time_minutes) {
909
- e.response_time_minutes = Math.floor(e.response_time_minutes * fatigueMultiplier);
910
- }
911
- });
912
- }
913
-
914
- // -- HOOK 5: OPEN SOURCE POWER USAGE ------------------
915
- // OSS users with >15 events get extra cloned build + deploy events
916
- // in their later activity (representing power usage that pushes them
917
- // toward limits). No flag — discover via cohort by segment + event count.
918
- if (profile.segment === "oss_user" && events.length > 15) {
919
- const buildTemplate = events.find(e => e.event === "build completed");
920
- const deployTemplate = events.find(e => e.event === "deployment completed");
921
- if (buildTemplate || deployTemplate) {
922
- const conversionPoint = Math.floor(events.length * 0.7);
923
- const tail = events.slice(conversionPoint);
924
- tail.forEach(e => {
925
- const tBase = dayjs(e.time);
926
- if (buildTemplate && chance.bool({ likelihood: 30 })) {
927
- events.push({
928
- ...buildTemplate,
929
- time: tBase.add(chance.integer({ min: 5, max: 240 }), "minutes").toISOString(),
930
- user_id: e.user_id,
931
- });
932
- }
933
- if (deployTemplate && chance.bool({ likelihood: 20 })) {
934
- events.push({
935
- ...deployTemplate,
936
- time: tBase.add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
937
- user_id: e.user_id,
938
- });
939
- }
940
- });
941
- }
942
- }
943
-
944
- // -- HOOK 9: BUILD-COUNT MAGIC NUMBER (no flags) ------
945
- // Sweet 15-30 builds → +50% deploys (clone with unique offset).
946
- // Over 31+ → drop 40% of deploys (flaky CI burnout).
947
- const buildCount = events.filter(e => e.event === "build completed").length;
948
- if (buildCount >= 15 && buildCount <= 30) {
949
- const deploys = events.filter(e => e.event === "deployment completed");
950
- const extras = Math.max(Math.floor(deploys.length * 0.5), 1);
951
- for (let k = 0; k < extras; k++) {
952
- const tpl = deploys[k % deploys.length];
953
- if (tpl) {
954
- events.push({
955
- ...tpl,
956
- time: dayjs(tpl.time).add(chance.integer({ min: 5, max: 360 }), "minutes").toISOString(),
957
- user_id: tpl.user_id,
958
- });
959
- }
960
- }
961
- } else if (buildCount >= 31) {
962
- for (let i = events.length - 1; i >= 0; i--) {
963
- if (events[i].event === "deployment completed" && chance.bool({ likelihood: 40 })) {
964
- events.splice(i, 1);
965
- }
966
- }
967
- }
968
-
969
- // -- HOOK 6: POST-OUTAGE RECOVERY ---------------------
970
- // After the outage volume rebound (d44-48), deployment events get
971
- // aggressively cloned to produce a visible spike above baseline.
972
- // Shifted later than outage end (d42.25) so natural volume has
973
- // recovered from the 0.05x suppression before cloning kicks in.
974
- const RECOVERY_START = datasetStart.add(44, "days");
975
- const RECOVERY_END = datasetStart.add(48, "days");
976
- const deployEvents = events.filter(e => {
977
- if (e.event !== "deployment completed") return false;
978
- const t = dayjs(e.time);
979
- return t.isAfter(RECOVERY_START) && t.isBefore(RECOVERY_END);
980
- });
981
- deployEvents.forEach(dep => {
982
- // 100% clone rate with 3 copies per event to clearly
983
- // exceed baseline deploy volume (d35-41)
984
- for (let c = 0; c < 3; c++) {
985
- events.push({
986
- ...dep,
987
- time: dayjs(dep.time).add(chance.integer({ min: 1, max: 8 }), "hours").toISOString(),
988
- user_id: dep.user_id,
989
- deploy_status: chance.pickone(["success", "success", "rolled_back"]),
990
- environment: "production",
991
- });
992
- }
993
- });
994
-
995
- return events;
996
- }
997
-
1041
+ hook(record, type, meta) {
1042
+ if (type === "funnel-post") return handleFunnelPostHooks(record, meta);
1043
+ if (type === "user") return handleUserHooks(record);
1044
+ if (type === "event") return handleEventHooks(record);
1045
+ if (type === "everything") return handleEverythingHooks(record, meta);
998
1046
  return record;
999
1047
  },
1000
1048
  };