@ak--47/dungeon-master 1.5.0 → 1.5.2

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 (66) hide show
  1. package/.claude/skills/create-dungeon/SKILL.md +139 -46
  2. package/.claude/skills/verify-dungeon/references/counting-semantics.md +31 -6
  3. package/.claude/skills/verify-dungeon/references/sql-recipes.md +44 -25
  4. package/.claude/skills/write-hooks/SKILL.md +31 -3
  5. package/CHANGELOG.md +85 -0
  6. package/HOOKS.md +13 -0
  7. package/dungeons/technical/ad-spend.js +41 -49
  8. package/dungeons/technical/anonymous-users.js +38 -36
  9. package/dungeons/technical/array-of-object-lookup.js +136 -153
  10. package/dungeons/technical/datagen-v15-verify.js +24 -11
  11. package/dungeons/technical/experiments.js +42 -40
  12. package/dungeons/technical/foobar.js +114 -118
  13. package/dungeons/technical/group-analytics.js +42 -40
  14. package/dungeons/technical/hook-helpers-verify.js +69 -50
  15. package/dungeons/technical/identity-model-verify.js +22 -12
  16. package/dungeons/technical/mirror-strategies.js +37 -39
  17. package/dungeons/technical/nested-objects.js +119 -118
  18. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  19. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  20. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  21. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  22. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  23. package/dungeons/technical/retention-cadence.js +115 -112
  24. package/dungeons/technical/sanity.js +86 -80
  25. package/dungeons/technical/scale-test.js +34 -38
  26. package/dungeons/technical/scd.js +111 -128
  27. package/dungeons/technical/simple.js +134 -141
  28. package/dungeons/technical/simplest.js +54 -62
  29. package/dungeons/technical/text-generation.js +110 -146
  30. package/dungeons/vertical/ai-platform.js +296 -333
  31. package/dungeons/vertical/community.js +284 -255
  32. package/dungeons/vertical/crypto.js +395 -391
  33. package/dungeons/vertical/dating.js +411 -378
  34. package/dungeons/vertical/devtools.js +336 -298
  35. package/dungeons/vertical/ecommerce.js +316 -394
  36. package/dungeons/vertical/education.js +369 -325
  37. package/dungeons/vertical/fintech.js +358 -325
  38. package/dungeons/vertical/fitness.js +335 -291
  39. package/dungeons/vertical/food-delivery.js +343 -307
  40. package/dungeons/vertical/gaming.js +480 -444
  41. package/dungeons/vertical/healthcare.js +306 -262
  42. package/dungeons/vertical/insurance-application.js +427 -409
  43. package/dungeons/vertical/logistics.js +271 -252
  44. package/dungeons/vertical/marketplace.js +333 -323
  45. package/dungeons/vertical/media.js +382 -335
  46. package/dungeons/vertical/real-estate.js +395 -346
  47. package/dungeons/vertical/sass.js +319 -333
  48. package/dungeons/vertical/social.js +368 -316
  49. package/dungeons/vertical/travel.js +297 -295
  50. package/index.js +46 -4
  51. package/lib/core/config-validator.js +126 -28
  52. package/lib/generators/funnels.js +4 -1
  53. package/lib/orchestrators/mixpanel-sender.js +7 -0
  54. package/lib/orchestrators/user-loop.js +132 -31
  55. package/lib/templates/defaults.js +59 -59
  56. package/lib/templates/macro-presets.js +14 -2
  57. package/lib/utils/dataset-context.js +103 -0
  58. package/lib/utils/retention-curve.js +140 -0
  59. package/lib/utils/utils.js +149 -38
  60. package/lib/verify/counting.js +40 -0
  61. package/lib/verify/emulate-breakdown.js +20 -1
  62. package/lib/verify/index.js +1 -0
  63. package/lib/verify/schema-validator.js +3 -1
  64. package/package.json +11 -2
  65. package/scripts/run-dungeon.mjs +12 -1
  66. package/types.d.ts +117 -1
@@ -1,70 +1,57 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "harness-sass";
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";
16
8
  import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
17
-
18
- dayjs.extend(utc);
19
- const chance = u.initChance(SEED);
20
-
21
9
  /** @typedef {import("../../types").Dungeon} Config */
22
10
 
11
+ // ── OVERVIEW ──
23
12
  /*
24
- * ═══════════════════════════════════════════════════════════════════════════════
25
- * DATASET OVERVIEW
26
- * ═══════════════════════════════════════════════════════════════════════════════
27
- *
28
- * CLOUDFORGE - B2B Cloud Infrastructure Monitoring & Deployment Platform
29
- *
30
- * CloudForge is a B2B SaaS platform that combines infrastructure monitoring (like Datadog)
31
- * with deployment automation (like Terraform). It serves engineering teams across companies
32
- * of all sizes - from startups deploying their first microservice to enterprises managing
33
- * thousands of services across multi-cloud environments.
34
- *
35
- * - 5,000 users over 100 days
36
- * - 600K events across 18 event types (+ 1 hook-created event type)
37
- * - 8 funnels (onboarding, monitoring, incident response, deployment, infra, team, docs, billing)
38
- * - Group analytics (companies)
39
- * - Desktop/browser only (B2B SaaS - no mobile devices)
40
- *
41
- * CORE PLATFORM:
42
- * Teams create workspaces, deploy services across AWS/GCP/Azure, and monitor everything
43
- * from a unified dashboard. The platform tracks uptime, latency, error rates, CPU/memory
44
- * usage, and costs. When things go wrong, CloudForge triggers alerts that route through
45
- * PagerDuty/Slack integrations, and on-call engineers acknowledge and resolve incidents
46
- * using automated runbooks.
47
- *
48
- * PRICING MODEL:
49
- * Four tiers: Free, Team, Business, Enterprise. Enterprise customers get dedicated
50
- * customer success managers and annual contracts. Pricing based on seat count and
51
- * resource usage.
13
+ * NAME: CloudForge
14
+ * APP: B2B SaaS that fuses infrastructure monitoring (Datadog-style) with
15
+ * deployment automation (Terraform-style). Engineering teams create
16
+ * workspaces, deploy services across AWS/GCP/Azure, monitor uptime
17
+ * and cost, and respond to alerts via Slack/PagerDuty runbooks.
18
+ * Pricing: Free / Team / Business / Enterprise (seats + usage).
19
+ * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 2026-05-01)
20
+ * CORE LOOP: workspace created service deployed dashboard viewed alert/resolve
21
+ *
22
+ * EVENTS (19):
23
+ * dashboard viewed (20) > api call (16) > query executed (15) > alert triggered (12)
24
+ * > service deployed (10) > deployment pipeline run (9) > alert acknowledged (8)
25
+ * > alert resolved (7) > documentation viewed (7) > security scan (6)
26
+ * > infrastructure scaled (5) > cost report generated (4) > integration configured (4)
27
+ * > feature flag toggled (4) > team member invited (3) > runbook executed (3)
28
+ * > billing event (3) > workspace created (1) > incident created (1)
29
+ *
30
+ * FUNNELS (8):
31
+ * - Onboarding: workspace created service deployed dashboard viewed (70%)
32
+ * - Daily Monitoring: dashboard viewed query executed api call (80%)
33
+ * - Incident Response: alert triggered alert acknowledged alert resolved (55%)
34
+ * - Deployment: deployment pipeline run service deployed dashboard viewed (65%, Canary A/B)
35
+ * - Infrastructure Mgmt: cost report generated → infrastructure scaled → security scan (50%)
36
+ * - Team & Config: team member invited → integration configured → feature flag toggled (40%)
37
+ * - Docs & Runbooks: documentation viewed → runbook executed → service deployed (45%)
38
+ * - Billing: billing event dashboard viewed (60%)
39
+ *
40
+ * USER PROPS: company_size, primary_role, team_name, seat_count, annual_contract_value,
41
+ * customer_success_manager, customer_health_score, plan_tier, cloud_provider
42
+ * SUPER PROPS: plan_tier, cloud_provider
43
+ * SCD PROPS: primary_role (viewer/editor/admin/owner, monthly fuzzy, max 6),
44
+ * plan_tier (starter/growth/enterprise/scale, monthly fixed, max 6, company_id-scoped)
45
+ * GROUPS: company_id (300 companies)
52
46
  */
53
47
 
48
+ // ── HOOK STORIES ──
54
49
  /*
55
- * ═══════════════════════════════════════════════════════════════════════════════
56
- * ANALYTICS HOOKS (11 hooks)
57
- * ═══════════════════════════════════════════════════════════════════════════════
58
- *
59
- * 10 deliberately architected patterns hidden in the data. NOTE: All cohort
60
- * effects are HIDDEN — no flag stamping. Discoverable via behavioral cohorts
61
- * or raw-prop breakdowns (company_size, day, doc_section). Adds:
62
- * 9. INCIDENT RESPONSE TIME-TO-CONVERT (Enterprise 0.67x gap vs Startup 1.5x)
63
- * [everything hook: scales response_time_mins and resolution_time_mins by company_size]
64
- * 10. DOCS MAGIC NUMBER (sweet 4-7 docs → +40% deploys; over 8+ → drop 25%)
50
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
51
+ * behavioral cohorts or raw-prop breakdowns (company_size, day, doc_section).
65
52
  *
66
53
  * ─────────────────────────────────────────────────────────────────────────────
67
- * 1. END-OF-QUARTER SPIKE (event)
54
+ * 1. END-OF-QUARTER SPIKE (everything)
68
55
  * ─────────────────────────────────────────────────────────────────────────────
69
56
  *
70
57
  * PATTERN: Days 100-110 billing events shift event_type toward "plan_upgraded"
@@ -217,13 +204,13 @@ const chance = u.initChance(SEED);
217
204
  * downscale; no engineer ignores a 25% month-over-month cost jump.
218
205
  *
219
206
  * ─────────────────────────────────────────────────────────────────────────────
220
- * 7. FAILED DEPLOYMENT RECOVERY (event — closure state)
207
+ * 7. FAILED DEPLOYMENT RECOVERY (everything)
221
208
  * ─────────────────────────────────────────────────────────────────────────────
222
209
  *
223
210
  * PATTERN: After a failed pipeline run, the user's next successful deploy has
224
- * duration_sec * 1.5 (recovery deploys are slower). Uses a module-level Map
225
- * for cross-call state. No flag discover by sequencing failed → next-success
226
- * pipeline events per user and comparing duration.
211
+ * duration_sec * 1.5 (recovery deploys are slower). No flag discover by
212
+ * sequencing failed next-success pipeline events per user and comparing
213
+ * duration.
227
214
  *
228
215
  * HOW TO FIND IT IN MIXPANEL:
229
216
  *
@@ -397,73 +384,292 @@ const chance = u.initChance(SEED);
397
384
  * Docs Magic Number | over (8+) deploys/user | 1x | ~0.75x | -25%
398
385
  * Deploy Experiment | Canary conversion | 65% | ~78% | 1.2x
399
386
  * Deploy Experiment | Canary TTC | 1d | ~0.85d | 0.85x
400
- *
401
- * ─────────────────────────────────────────────────────────────────────────────
402
- * ADVANCED ANALYSIS IDEAS
403
- * ─────────────────────────────────────────────────────────────────────────────
404
- *
405
- * CROSS-HOOK PATTERNS:
406
- * - Churned + Enterprise: Do churned accounts skew toward startups or are
407
- * enterprise accounts also silenced?
408
- * - Integration + Cost: Do teams with full integrations manage costs better?
409
- * - Docs + Deploys + Failures: Do docs readers have fewer failed deployments?
410
- * - Quarter Spike + Churn: Are quarter-end upgrades correlated with later churn?
411
- * - Enterprise Recovery: Do enterprise customers recover from failed deploys
412
- * differently than startups?
413
- *
414
- * COHORT ANALYSIS:
415
- * - By company_size: Compare all metrics across startup/smb/mid_market/enterprise
416
- * - By plan_tier: Free vs. Team vs. Business vs. Enterprise engagement
417
- * - By cloud_provider: AWS vs. GCP vs. Azure deployment and alert patterns
418
- * - By primary_role: Engineer vs. SRE vs. DevOps vs. Manager behaviors
419
- *
420
- * KEY METRICS:
421
- * - MTTR: alert triggered → alert resolved duration
422
- * - Deployment Frequency: service deployed per user per week
423
- * - Deployment Success Rate: pipeline success vs. failure ratio
424
- * - Cost Efficiency: total_cost trend over time per company
425
- * - Feature Adoption: integration configured events by type
426
- * - Documentation Engagement: documentation viewed by section
427
387
  */
428
388
 
429
- // Generate consistent IDs for lookup tables and event properties
389
+ // ── SCALE ──
390
+ const SEED = "harness-sass";
391
+ const NUM_USERS = 10_000;
392
+ const DATASET_START = "2026-01-01T00:00:00Z";
393
+ const DATASET_END = "2026-05-01T23:59:59Z";
394
+ const EVENTS_PER_DAY = 1.2;
395
+ const token = process.env.MP_TOKEN || "your-mixpanel-token";
396
+
397
+ const chance = u.initChance(SEED);
398
+
399
+ // ── KNOBS (tweak these to reshape stories) ──
400
+ const EOQ_START_DAY = 100;
401
+ const EOQ_END_DAY = 110;
402
+ const EOQ_UPGRADE_LIKELIHOOD = 40;
403
+ const EOQ_INVITE_CLONE_LIKELIHOOD = 50;
404
+
405
+ const CHURN_USER_HASH_MOD = 5;
406
+ const CHURN_CUTOFF_DAYS = 30;
407
+
408
+ const ALERT_ESCALATION_LIKELIHOOD = 30;
409
+
410
+ const INTEGRATION_RESPONSE_FACTOR = 0.4;
411
+ const INTEGRATION_RESOLUTION_FACTOR = 0.5;
412
+
413
+ const COST_OVERRUN_THRESHOLD = 25;
414
+
415
+ const FAILED_DEPLOY_RECOVERY_MULT = 1.5;
416
+
417
+ const DOCS_SWEET_MIN = 4;
418
+ const DOCS_SWEET_MAX = 7;
419
+ const DOCS_OVER_THRESHOLD = 8;
420
+ const DOCS_EXTRA_DEPLOYS_MIN = 2;
421
+ const DOCS_EXTRA_DEPLOYS_MAX = 3;
422
+ const DOCS_DEPLOY_DROP_LIKELIHOOD = 25;
423
+
424
+ const TTC_ENTERPRISE_FACTOR = 0.67;
425
+ const TTC_STARTUP_FACTOR = 1.5;
426
+ const INCIDENT_SEQ_WINDOW_MINS = 60 * 24 * 30;
427
+
428
+ // ── DATA ARRAYS ──
430
429
  const serviceIds = v.range(1, 201).map(() => `svc_${v.uid(8)}`);
431
430
  const alertIds = v.range(1, 501).map(() => `alert_${v.uid(6)}`);
432
431
  const pipelineIds = v.range(1, 101).map(() => `pipe_${v.uid(6)}`);
433
432
  const runbookIds = v.range(1, 51).map(() => `rb_${v.uid(6)}`);
434
- const companyIds = v.range(1, 301).map(() => `comp_${v.uid(8)}`);
435
433
 
436
- // Module-level Maps for closure-based state tracking across hook calls
434
+ // ── HOOK STATE ──
435
+ // Module-level Map for closure-based state tracking across event-hook calls
437
436
  const costOverrunUsers = new Map();
438
- const failedDeployUsers = new Map();
439
437
 
438
+ // ── HELPER FUNCTIONS ──
439
+ function handleEventHooks(record) {
440
+ // H3: ALERT ESCALATION REPLACEMENT — critical/emergency alerts sometimes
441
+ // become incident-created events.
442
+ if (record.event === "alert triggered") {
443
+ const severity = record.severity;
444
+ if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: ALERT_ESCALATION_LIKELIHOOD })) {
445
+ return {
446
+ ...record,
447
+ event: "incident created",
448
+ escalation_level: chance.pickone(["P1", "P2"]),
449
+ teams_paged: chance.integer({ min: 1, max: 5 }),
450
+ incident_id: `inc_${v.uid(8)}`,
451
+ original_severity: severity,
452
+ original_alert_type: record.alert_type,
453
+ auto_escalated: true,
454
+ };
455
+ }
456
+ }
457
+
458
+ // H6: COST OVERRUN PATTERN — cost reports with cost_change > 25% record
459
+ // the user, then the next infrastructure-scaled event from that user is
460
+ // forced to scale_direction = "down".
461
+ if (record.event === "cost report generated" && record.cost_change_percent > COST_OVERRUN_THRESHOLD) {
462
+ costOverrunUsers.set(record.user_id, true);
463
+ }
464
+ if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
465
+ record.scale_direction = "down";
466
+ costOverrunUsers.delete(record.user_id);
467
+ }
468
+
469
+ return record;
470
+ }
471
+
472
+ function handleUserHooks(record) {
473
+ // H8: ENTERPRISE VS STARTUP — company size determines seat count, ACV,
474
+ // and CSM. Real profile attrs.
475
+ const companySize = record.company_size;
476
+ if (companySize === "enterprise") {
477
+ record.seat_count = chance.integer({ min: 50, max: 500 });
478
+ record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
479
+ record.customer_success_manager = true;
480
+ } else if (companySize === "mid_market") {
481
+ record.seat_count = chance.integer({ min: 10, max: 50 });
482
+ record.annual_contract_value = chance.integer({ min: 12000, max: 50000 });
483
+ record.customer_success_manager = false;
484
+ } else if (companySize === "smb") {
485
+ record.seat_count = chance.integer({ min: 3, max: 10 });
486
+ record.annual_contract_value = chance.integer({ min: 3600, max: 12000 });
487
+ record.customer_success_manager = false;
488
+ } else if (companySize === "startup") {
489
+ record.seat_count = chance.integer({ min: 1, max: 5 });
490
+ record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
491
+ record.customer_success_manager = false;
492
+ }
493
+ record.customer_health_score = chance.integer({ min: 1, max: 100 });
494
+ return record;
495
+ }
496
+
497
+ function handleEverythingHooks(record, meta) {
498
+ const datasetStart = dayjs.unix(meta.datasetStart);
499
+ const userEvents = record;
500
+ const profile = meta.profile;
501
+
502
+ userEvents.forEach(e => {
503
+ e.plan_tier = profile.plan_tier;
504
+ e.cloud_provider = profile.cloud_provider;
505
+ });
506
+
507
+ // H1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
508
+ // event_type to plan_upgraded 40% of the time.
509
+ userEvents.forEach(e => {
510
+ if (e.event !== "billing event") return;
511
+ const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
512
+ if (dayInDataset >= EOQ_START_DAY && dayInDataset <= EOQ_END_DAY && chance.bool({ likelihood: EOQ_UPGRADE_LIKELIHOOD })) {
513
+ e.event_type = "plan_upgraded";
514
+ }
515
+ });
516
+
517
+ // H1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone 50% of
518
+ // team-member-invited events (push, not return).
519
+ for (let i = userEvents.length - 1; i >= 0; i--) {
520
+ const e = userEvents[i];
521
+ if (e.event !== "team member invited") continue;
522
+ const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
523
+ if (dayInDataset >= EOQ_START_DAY && dayInDataset <= EOQ_END_DAY && chance.bool({ likelihood: EOQ_INVITE_CLONE_LIKELIHOOD })) {
524
+ userEvents.push({
525
+ ...e,
526
+ time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
527
+ user_id: e.user_id,
528
+ role: chance.pickone(["editor", "viewer"]),
529
+ invitation_method: chance.pickone(["email", "sso", "slack"]),
530
+ });
531
+ }
532
+ }
533
+
534
+ // H2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5) have post-day-30
535
+ // events removed.
536
+ if (userEvents && userEvents.length > 0) {
537
+ const firstEvent = userEvents[0];
538
+ const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
539
+ if ((idHash % CHURN_USER_HASH_MOD) === 0) {
540
+ for (let i = userEvents.length - 1; i >= 0; i--) {
541
+ const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
542
+ if (dayInDataset > CHURN_CUTOFF_DAYS) {
543
+ userEvents.splice(i, 1);
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ // H4: INTEGRATION USERS SUCCEED — Slack+PagerDuty users get alert
550
+ // response_time_mins 0.4x and resolution_time_mins 0.5x.
551
+ let hasSlack = false;
552
+ let hasPagerduty = false;
553
+ userEvents.forEach((event) => {
554
+ if (event.event === "integration configured") {
555
+ if (event.integration_type === "slack") hasSlack = true;
556
+ if (event.integration_type === "pagerduty") hasPagerduty = true;
557
+ }
558
+ });
559
+ if (hasSlack && hasPagerduty) {
560
+ userEvents.forEach((event) => {
561
+ if (event.event === "alert acknowledged" && event.response_time_mins) {
562
+ event.response_time_mins = Math.floor(event.response_time_mins * INTEGRATION_RESPONSE_FACTOR);
563
+ }
564
+ if (event.event === "alert resolved" && event.resolution_time_mins) {
565
+ event.resolution_time_mins = Math.floor(event.resolution_time_mins * INTEGRATION_RESOLUTION_FACTOR);
566
+ }
567
+ });
568
+ }
569
+
570
+ // H5 + H10: DOCS MAGIC NUMBER — sweet 4-7 docs → +40% extra cloned
571
+ // service-deployed events; over 8+ → drop 25% of service-deployed events.
572
+ const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
573
+ const deployTemplate = userEvents.find(e => e.event === "service deployed");
574
+ if (docsCount >= DOCS_SWEET_MIN && docsCount <= DOCS_SWEET_MAX && deployTemplate) {
575
+ const lastEvent = userEvents[userEvents.length - 1];
576
+ const extraDeploys = chance.integer({ min: DOCS_EXTRA_DEPLOYS_MIN, max: DOCS_EXTRA_DEPLOYS_MAX });
577
+ for (let i = 0; i < extraDeploys; i++) {
578
+ userEvents.push({
579
+ ...deployTemplate,
580
+ time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
581
+ user_id: lastEvent.user_id,
582
+ service_id: chance.pickone(serviceIds),
583
+ service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
584
+ environment: "production",
585
+ cloud_provider: profile.cloud_provider,
586
+ });
587
+ }
588
+ } else if (docsCount >= DOCS_OVER_THRESHOLD) {
589
+ for (let i = userEvents.length - 1; i >= 0; i--) {
590
+ if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: DOCS_DEPLOY_DROP_LIKELIHOOD })) {
591
+ userEvents.splice(i, 1);
592
+ }
593
+ }
594
+ }
595
+
596
+ // H7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs in this
597
+ // user's pipeline events, multiply duration_sec by 1.5 on the recovery
598
+ // deploy.
599
+ const pipelineEvents = userEvents
600
+ .filter(e => e.event === "deployment pipeline run")
601
+ .sort((a, b) => a.time.localeCompare(b.time));
602
+ for (let i = 1; i < pipelineEvents.length; i++) {
603
+ if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
604
+ pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * FAILED_DEPLOY_RECOVERY_MULT);
605
+ }
606
+ }
607
+
608
+ // H9: INCIDENT RESPONSE TTC — enterprise resolves faster, startup
609
+ // resolves slower. Scale response_time_mins on acknowledged events and
610
+ // resolution_time_mins on resolved events by company_size, and shift the
611
+ // first incident-response funnel sequence timestamps for funnel TTC
612
+ // reports. Compounds with H4 (integration users).
613
+ const companySegment = profile?.company_size;
614
+ const ttcFactor = (
615
+ companySegment === "enterprise" ? TTC_ENTERPRISE_FACTOR :
616
+ companySegment === "startup" ? TTC_STARTUP_FACTOR :
617
+ 1.0
618
+ );
619
+ if (ttcFactor !== 1.0) {
620
+ // Timestamp shift: affects Mixpanel funnel TTC
621
+ const incidentSeq = findFirstSequence(
622
+ userEvents,
623
+ ["alert triggered", "alert acknowledged", "alert resolved"],
624
+ INCIDENT_SEQ_WINDOW_MINS
625
+ );
626
+ if (incidentSeq) scaleFunnelTTC(incidentSeq, ttcFactor);
627
+ // Property scale: affects Insights AVG reports
628
+ userEvents.forEach(e => {
629
+ if (e.event === "alert acknowledged" && e.response_time_mins) {
630
+ e.response_time_mins = Math.max(1, Math.round(e.response_time_mins * ttcFactor));
631
+ }
632
+ if (e.event === "alert resolved" && e.resolution_time_mins) {
633
+ e.resolution_time_mins = Math.max(1, Math.round(e.resolution_time_mins * ttcFactor));
634
+ }
635
+ });
636
+ }
637
+
638
+ return record;
639
+ }
640
+
641
+ // ── CONFIG ──
440
642
  /** @type {Config} */
441
643
  const config = {
442
- token,
443
644
  seed: SEED,
444
- datasetStart: "2026-01-01T00:00:00Z",
445
- datasetEnd: "2026-05-01T23:59:59Z",
446
- // numDays: num_days,
447
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
448
- numUsers: num_users,
645
+ datasetStart: DATASET_START,
646
+ datasetEnd: DATASET_END,
647
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
648
+ numUsers: NUM_USERS,
649
+ format: "json",
650
+ gzip: true,
651
+ credentials: {
652
+ token,
653
+ },
654
+ switches: {
655
+ hasSessionIds: true,
656
+ alsoInferFunnels: false,
657
+ hasLocation: true,
658
+ hasAndroidDevices: false,
659
+ hasIOSDevices: false,
660
+ hasDesktopDevices: true,
661
+ hasBrowser: true,
662
+ hasCampaigns: false,
663
+ isAnonymous: false,
664
+ hasAdSpend: false,
665
+ hasAvatar: true,
666
+ },
449
667
  // Phase 2 identity model — B2B SaaS reference. Engineers commonly use 1-2
450
668
  // devices (desktop + work laptop). avgDevicePerUser:2 puts a meaningful
451
669
  // per-session sticky-device pattern in Mixpanel device dashboards.
452
- hasAnonIds: true,
453
- avgDevicePerUser: 2,
454
- hasSessionIds: true,
455
- format: "json",
456
- gzip: true,
457
- alsoInferFunnels: false,
458
- hasLocation: true,
459
- hasAndroidDevices: false,
460
- hasIOSDevices: false,
461
- hasDesktopDevices: true,
462
- hasBrowser: true,
463
- hasCampaigns: false,
464
- isAnonymous: false,
465
- hasAdSpend: false,
466
- hasAvatar: true,
670
+ identity: {
671
+ avgDevicePerUser: 2,
672
+ },
467
673
  concurrency: 1,
468
674
  writeToDisk: false,
469
675
  scdProps: {
@@ -778,230 +984,10 @@ const config = {
778
984
 
779
985
  lookupTables: [],
780
986
 
781
- /**
782
- * ARCHITECTED ANALYTICS HOOKS
783
- *
784
- * This hook function creates 10 deliberate patterns in the data.
785
- * Hook 11 (Deploy Pipeline Experiment) is engine-managed via funnel
786
- * experiment config — no hook code needed.
787
- *
788
- * 1. END-OF-QUARTER SPIKE: Days 100-110 drive plan upgrades and team expansion
789
- * 2. CHURNED ACCOUNT SILENCING: ~10% of users go completely silent after month 1
790
- * 3. ALERT ESCALATION REPLACEMENT: Critical alerts become "incident created" events
791
- * 4. INTEGRATION USERS SUCCEED: Slack+PagerDuty users resolve incidents 50-60% faster
792
- * 5. DOCS READERS DEPLOY MORE: Best practices readers get extra production deploys
793
- * 6. COST OVERRUN PATTERN: Budget-exceeded users react by scaling down infrastructure
794
- * 7. FAILED DEPLOYMENT RECOVERY: Recovery deploys take 1.5x longer, tracked across calls
795
- * 8. ENTERPRISE VS STARTUP: Company size determines seat count, ACV, and health score
796
- * 9. INCIDENT RESPONSE TTC: Enterprise 0.67x faster, startup 1.5x slower incident resolution
797
- * 10. DOCS MAGIC NUMBER: Sweet 4-7 docs → extra deploys; over 8+ → drop 25% of deploys
798
- * 11. DEPLOY PIPELINE EXPERIMENT: Canary Deploys A/B test on deployment funnel (engine-managed)
799
- */
800
- hook: function (record, type, meta) {
801
- // (Hook 1a moved to everything hook for reliable datasetStart access)
802
-
803
- // HOOK 3: ALERT ESCALATION REPLACEMENT (event) — critical/emergency
804
- // alerts sometimes become incident-created events. Real product flow.
805
- if (type === "event") {
806
- if (record.event === "alert triggered") {
807
- const severity = record.severity;
808
- if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: 30 })) {
809
- return {
810
- ...record,
811
- event: "incident created",
812
- escalation_level: chance.pickone(["P1", "P2"]),
813
- teams_paged: chance.integer({ min: 1, max: 5 }),
814
- incident_id: `inc_${v.uid(8)}`,
815
- original_severity: severity,
816
- original_alert_type: record.alert_type,
817
- auto_escalated: true,
818
- };
819
- }
820
- }
821
- }
822
-
823
- // HOOK 6: COST OVERRUN PATTERN (event) — cost reports with cost_change
824
- // > 25% record user, then next infrastructure-scaled event from that
825
- // user gets scale_direction = "down". No flag.
826
- if (type === "event") {
827
- if (record.event === "cost report generated" && record.cost_change_percent > 25) {
828
- costOverrunUsers.set(record.user_id, true);
829
- }
830
- if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
831
- record.scale_direction = "down";
832
- costOverrunUsers.delete(record.user_id);
833
- }
834
- }
835
-
836
- if (type === "everything") {
837
- const datasetStart = dayjs.unix(meta.datasetStart);
838
- const userEvents = record;
839
- const profile = meta.profile;
840
-
841
- userEvents.forEach(e => {
842
- e.plan_tier = profile.plan_tier;
843
- e.cloud_provider = profile.cloud_provider;
844
- });
845
-
846
- // HOOK 1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
847
- // event_type to plan_upgraded 40% of the time. No flag.
848
- userEvents.forEach(e => {
849
- if (e.event !== "billing event") return;
850
- const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
851
- if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 40 })) {
852
- e.event_type = "plan_upgraded";
853
- }
854
- });
855
-
856
- // HOOK 1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone
857
- // 50% of team-member-invited events (push, not return). No flag.
858
- for (let i = userEvents.length - 1; i >= 0; i--) {
859
- const e = userEvents[i];
860
- if (e.event !== "team member invited") continue;
861
- const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
862
- if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 50 })) {
863
- userEvents.push({
864
- ...e,
865
- time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
866
- user_id: e.user_id,
867
- role: chance.pickone(["editor", "viewer"]),
868
- invitation_method: chance.pickone(["email", "sso", "slack"]),
869
- });
870
- }
871
- }
872
-
873
- // HOOK 2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5)
874
- // have post-day-30 events removed. No flag.
875
- if (userEvents && userEvents.length > 0) {
876
- const firstEvent = userEvents[0];
877
- const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
878
- if ((idHash % 5) === 0) {
879
- for (let i = userEvents.length - 1; i >= 0; i--) {
880
- const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
881
- if (dayInDataset > 30) {
882
- userEvents.splice(i, 1);
883
- }
884
- }
885
- }
886
- }
887
-
888
- // HOOK 4: INTEGRATION USERS SUCCEED — Slack+PagerDuty users get
889
- // alert response_time_mins 0.4x and resolution_time_mins 0.5x.
890
- // Mutates raw props. No flag.
891
- let hasSlack = false;
892
- let hasPagerduty = false;
893
- userEvents.forEach((event) => {
894
- if (event.event === "integration configured") {
895
- if (event.integration_type === "slack") hasSlack = true;
896
- if (event.integration_type === "pagerduty") hasPagerduty = true;
897
- }
898
- });
899
- if (hasSlack && hasPagerduty) {
900
- userEvents.forEach((event) => {
901
- if (event.event === "alert acknowledged" && event.response_time_mins) {
902
- event.response_time_mins = Math.floor(event.response_time_mins * 0.4);
903
- }
904
- if (event.event === "alert resolved" && event.resolution_time_mins) {
905
- event.resolution_time_mins = Math.floor(event.resolution_time_mins * 0.5);
906
- }
907
- });
908
- }
909
-
910
- // HOOK 5 + HOOK 10: DOCS MAGIC NUMBER (no flags)
911
- // Sweet 4-7 documentation-viewed events → +40% extra cloned
912
- // service-deployed events. Over 8+ → drop 25% of service-deployed
913
- // events. No flag.
914
- const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
915
- const deployTemplate = userEvents.find(e => e.event === "service deployed");
916
- if (docsCount >= 4 && docsCount <= 7 && deployTemplate) {
917
- const lastEvent = userEvents[userEvents.length - 1];
918
- const extraDeploys = chance.integer({ min: 2, max: 3 });
919
- for (let i = 0; i < extraDeploys; i++) {
920
- userEvents.push({
921
- ...deployTemplate,
922
- time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
923
- user_id: lastEvent.user_id,
924
- service_id: chance.pickone(serviceIds),
925
- service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
926
- environment: "production",
927
- cloud_provider: profile.cloud_provider,
928
- });
929
- }
930
- } else if (docsCount >= 8) {
931
- for (let i = userEvents.length - 1; i >= 0; i--) {
932
- if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: 25 })) {
933
- userEvents.splice(i, 1);
934
- }
935
- }
936
- }
937
-
938
- // HOOK 7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs
939
- // in this user's pipeline events, multiply duration_sec by 1.5 on
940
- // the recovery deploy. Full control via everything hook.
941
- const pipelineEvents = userEvents
942
- .filter(e => e.event === "deployment pipeline run")
943
- .sort((a, b) => a.time.localeCompare(b.time));
944
- for (let i = 1; i < pipelineEvents.length; i++) {
945
- if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
946
- pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * 1.5);
947
- }
948
- }
949
-
950
- // HOOK 9: INCIDENT RESPONSE TTC — enterprise resolves faster,
951
- // startup resolves slower. Scale response_time_mins on
952
- // acknowledged events and resolution_time_mins on resolved
953
- // events by company_size. This compounds with H4 (integration
954
- // users) — realistic: enterprise + good tooling = fastest.
955
- const companySegment = profile?.company_size;
956
- const ttcFactor = (
957
- companySegment === "enterprise" ? 0.67 :
958
- companySegment === "startup" ? 1.5 :
959
- 1.0
960
- );
961
- if (ttcFactor !== 1.0) {
962
- // Timestamp shift: affects Mixpanel funnel TTC
963
- const incidentSeq = findFirstSequence(
964
- userEvents,
965
- ["alert triggered", "alert acknowledged", "alert resolved"],
966
- 60 * 24 * 30
967
- );
968
- if (incidentSeq) scaleFunnelTTC(incidentSeq, ttcFactor);
969
- // Property scale: affects Insights AVG reports
970
- userEvents.forEach(e => {
971
- if (e.event === "alert acknowledged" && e.response_time_mins) {
972
- e.response_time_mins = Math.max(1, Math.round(e.response_time_mins * ttcFactor));
973
- }
974
- if (e.event === "alert resolved" && e.resolution_time_mins) {
975
- e.resolution_time_mins = Math.max(1, Math.round(e.resolution_time_mins * ttcFactor));
976
- }
977
- });
978
- }
979
- }
980
-
981
- // HOOK 8: ENTERPRISE VS STARTUP (user) — company size determines
982
- // seat count, ACV, and CSM. Real profile attrs.
983
- if (type === "user") {
984
- const companySize = record.company_size;
985
- if (companySize === "enterprise") {
986
- record.seat_count = chance.integer({ min: 50, max: 500 });
987
- record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
988
- record.customer_success_manager = true;
989
- } else if (companySize === "mid_market") {
990
- record.seat_count = chance.integer({ min: 10, max: 50 });
991
- record.annual_contract_value = chance.integer({ min: 12000, max: 50000 });
992
- record.customer_success_manager = false;
993
- } else if (companySize === "smb") {
994
- record.seat_count = chance.integer({ min: 3, max: 10 });
995
- record.annual_contract_value = chance.integer({ min: 3600, max: 12000 });
996
- record.customer_success_manager = false;
997
- } else if (companySize === "startup") {
998
- record.seat_count = chance.integer({ min: 1, max: 5 });
999
- record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
1000
- record.customer_success_manager = false;
1001
- }
1002
- record.customer_health_score = chance.integer({ min: 1, max: 100 });
1003
- }
1004
-
987
+ hook(record, type, meta) {
988
+ if (type === "event") return handleEventHooks(record);
989
+ if (type === "user") return handleUserHooks(record);
990
+ if (type === "everything") return handleEverythingHooks(record, meta);
1005
991
  return record;
1006
992
  }
1007
993
  };