@ak--47/dungeon-master 1.3.0 → 1.4.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 (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "harness-sass";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -52,153 +52,217 @@ const chance = u.initChance(SEED);
52
52
 
53
53
  /*
54
54
  * ═══════════════════════════════════════════════════════════════════════════════
55
- * ANALYTICS HOOKS
55
+ * ANALYTICS HOOKS (10 hooks)
56
56
  * ═══════════════════════════════════════════════════════════════════════════════
57
57
  *
58
- * 8 deliberately architected patterns hidden in the data, simulating real-world
59
- * B2B SaaS behavior. Several hooks use event removal (splice), event replacement,
60
- * and module-level closure state tracking via Map objects.
58
+ * 10 deliberately architected patterns hidden in the data. NOTE: All cohort
59
+ * effects are HIDDEN no flag stamping. Discoverable via behavioral cohorts
60
+ * or raw-prop breakdowns (company_size, day, doc_section). Adds:
61
+ * 9. INCIDENT RESPONSE TIME-TO-CONVERT (Enterprise 1.4x faster vs Startup)
62
+ * [funnel-post: visible only in Mixpanel funnel median TTC; cross-event
63
+ * MIN→MIN SQL queries do NOT show this]
64
+ * 10. DOCS MAGIC NUMBER (sweet 4-7 docs → +40% deploys; over 8+ → drop 25%)
61
65
  *
62
66
  * ─────────────────────────────────────────────────────────────────────────────
63
- * 1. END-OF-QUARTER SPIKE (event hook)
67
+ * 1. END-OF-QUARTER SPIKE (event)
64
68
  * ─────────────────────────────────────────────────────────────────────────────
65
69
  *
66
- * Days 80-90: billing events shift toward plan upgrades 40% of the time, and team
67
- * member invitations are duplicated 50% of the time. Tagged: quarter_end_push: true.
70
+ * PATTERN: Days 100-110 billing events shift event_type toward "plan_upgraded"
71
+ * 40% of the time and team member invitations are duplicated 50% of the time.
72
+ * No flag — discover via line chart by day.
68
73
  *
69
- * Mixpanel Report Plan Upgrades Over Time:
70
- * • Insights line chart
71
- * • Event: "billing event", filter "event_type" = "plan_upgraded"
72
- * • Daily trend
73
- * • Expected: Spike in plan upgrades during days 80-90 (4x normal volume)
74
+ * HOW TO FIND IT IN MIXPANEL:
74
75
  *
75
- * Mixpanel Report Team Expansion Surge:
76
- * Insights line chart
77
- * Event: "team member invited", filter "quarter_end_push" = true
78
- * Daily trend
79
- * Expected: Clear volume spike in last 10 days with duplicate invites
76
+ * Report 1: Plan Upgrades Over Time
77
+ * - Report type: Insights
78
+ * - Event: "billing event"
79
+ * - Measure: Total
80
+ * - Filter: event_type = "plan_upgraded"
81
+ * - Line chart by day
82
+ * - Expected: ~4x normal upgrade volume during days 100-110
83
+ *
84
+ * Report 2: Team Expansion Surge
85
+ * - Report type: Insights
86
+ * - Event: "team member invited"
87
+ * - Measure: Total
88
+ * - Line chart by day
89
+ * - Expected: clear volume spike in the final 10 days from duplicated invites
90
+ *
91
+ * REAL-WORLD ANALOGUE: B2B SaaS revenue clusters at quarter-close as sales
92
+ * teams pull deals forward and customers expand seats to lock in pricing.
80
93
  *
81
94
  * ─────────────────────────────────────────────────────────────────────────────
82
- * 2. CHURNED ACCOUNT SILENCING (everything hook)
95
+ * 2. CHURNED ACCOUNT SILENCING (everything)
83
96
  * ─────────────────────────────────────────────────────────────────────────────
84
97
  *
85
- * ~10% of users (hash of distinct_id, idHash % 5 === 0) go completely silent
86
- * after day 30. ALL events after month 1 are removed via splice(). User profiles
87
- * are tagged churned_account: true for discoverability.
98
+ * PATTERN: ~10-20% of users (deterministic via distinct_id char hash) go
99
+ * completely silent after day 30. All post-d30 events are removed via splice().
100
+ * No flag derive cohort via behavioral retention bucket (users with zero
101
+ * activity past d30 vs the rest).
102
+ *
103
+ * HOW TO FIND IT IN MIXPANEL:
88
104
  *
89
- * Mixpanel Report Churned Account Retention:
90
- * Retention report
91
- * Event A/B: Any event
92
- * Breakdown: User profile "churned_account"
93
- * • Expected: churned_account=true shows 0% retention after day 30
105
+ * Report 1: Retention Cliff
106
+ * - Cohort A: users with at least 1 event AFTER day 30
107
+ * - Cohort B: users with events ONLY in days 1-30
108
+ * - Compare cohort sizes — B should be ~10-20% of total
94
109
  *
95
- * Mixpanel Report Churned Account Activity:
96
- * Insights line chart
97
- * Event: Any event, measure total per user
98
- * Breakdown: User profile "churned_account"
99
- * Weekly trend
100
- * Expected: churned_account=true flatlines after week 4
110
+ * Report 2: Activity Volume Pre/Post Day 30
111
+ * - Report type: Insights
112
+ * - Event: any event
113
+ * - Measure: Total per user
114
+ * - Line chart by day
115
+ * - Expected: a visible drop after d30 driven by the silent cohort
116
+ *
117
+ * REAL-WORLD ANALOGUE: Most SaaS churn happens silently — accounts simply
118
+ * stop logging in long before the formal cancellation lands.
101
119
  *
102
120
  * ─────────────────────────────────────────────────────────────────────────────
103
- * 3. ALERT ESCALATION REPLACEMENT (event hook)
121
+ * 3. ALERT ESCALATION REPLACEMENT (event)
104
122
  * ─────────────────────────────────────────────────────────────────────────────
105
123
  *
106
- * 30% of critical/emergency "alert triggered" events are REPLACED with a new
107
- * event type "incident created" (not in the events array — hook-only). Includes
108
- * escalation_level (P1/P2), teams_paged, incident_id.
124
+ * PATTERN: 30% of critical/emergency "alert triggered" events are REPLACED
125
+ * with a hook-only "incident created" event (not in the events array). The
126
+ * new event carries escalation_level (P1/P2), teams_paged, and incident_id.
127
+ *
128
+ * HOW TO FIND IT IN MIXPANEL:
109
129
  *
110
- * Mixpanel Report Incident Created Discovery:
111
- * Insights report
112
- * Event: "incident created"
113
- * Breakdown: "escalation_level"
114
- * Expected: P1 and P2 incidents, ~30% of critical/emergency alert volume
130
+ * Report 1: Incident Created Discovery
131
+ * - Report type: Insights
132
+ * - Event: "incident created"
133
+ * - Measure: Total
134
+ * - Breakdown: "escalation_level"
135
+ * - Expected: P1 and P2 incidents, ~30% of critical/emergency alert volume
115
136
  *
116
- * Mixpanel Report Alert vs Incident Ratio:
117
- * Insights report
118
- * Events: "alert triggered" AND "incident created"
119
- * Expected: incident created count ~ 30% of critical+emergency alerts
137
+ * Report 2: Alert vs Incident Ratio
138
+ * - Report type: Insights
139
+ * - Events: "alert triggered" AND "incident created"
140
+ * - Measure: Total
141
+ * - Expected: incident count ~ 30% of critical+emergency alert count
142
+ *
143
+ * REAL-WORLD ANALOGUE: Severe alerts get auto-promoted into incident
144
+ * tickets that page on-call engineers and trigger customer comms.
120
145
  *
121
146
  * ─────────────────────────────────────────────────────────────────────────────
122
- * 4. INTEGRATION USERS SUCCEED (everything hook)
147
+ * 4. INTEGRATION USERS SUCCEED (everything)
123
148
  * ─────────────────────────────────────────────────────────────────────────────
124
149
  *
125
- * Users with BOTH Slack AND PagerDuty integrations resolve alerts faster:
126
- * response_time_mins reduced 60%, resolution_time_mins reduced 50%.
127
- * Tagged: integrated_team: true.
150
+ * PATTERN: Users with BOTH "slack" AND "pagerduty" "integration configured"
151
+ * events resolve alerts faster: response_time_mins reduced 60%, resolution_time_mins
152
+ * reduced 50%. No flag — derive cohort behaviorally.
153
+ *
154
+ * HOW TO FIND IT IN MIXPANEL:
155
+ *
156
+ * Report 1: Response Time by Integration Cohort
157
+ * - Cohort A: users who configured BOTH slack AND pagerduty integrations
158
+ * - Cohort B: rest
159
+ * - Event: "alert acknowledged"
160
+ * - Measure: Average of "response_time_mins"
161
+ * - Expected: A ~ 60% lower response time
128
162
  *
129
- * Mixpanel Report Integration Impact on Response Time:
130
- * Insights report
131
- * Event: "alert acknowledged", measure avg "response_time_mins"
132
- * Breakdown: "integrated_team"
133
- * Expected: integrated_team=true ~ 60% lower response time
163
+ * Report 2: Resolution Time by Integration Cohort
164
+ * - Cohort A vs B (as above)
165
+ * - Event: "alert resolved"
166
+ * - Measure: Average of "resolution_time_mins"
167
+ * - Expected: A ~ 50% faster resolution
134
168
  *
135
- * Mixpanel Report Integration Impact on Resolution:
136
- * Insights report
137
- * • Event: "alert resolved", measure avg "resolution_time_mins"
138
- * • Breakdown: "integrated_team"
139
- * • Expected: integrated_team=true ~ 50% faster resolution
169
+ * REAL-WORLD ANALOGUE: Teams that wire alerting into their existing comms
170
+ * stack respond minutes faster — the alert literally finds the human.
140
171
  *
141
172
  * ─────────────────────────────────────────────────────────────────────────────
142
- * 5. DOCS READERS DEPLOY MORE (everything hook)
173
+ * 5. DOCS READERS DEPLOY MORE (everything)
143
174
  * ─────────────────────────────────────────────────────────────────────────────
144
175
  *
145
- * Users with 3+ "best_practices" documentation views get 2-3 extra production
146
- * deploys spliced into their event stream. Tagged: docs_informed: true.
176
+ * PATTERN: Users with 3+ "documentation viewed" events where doc_section
177
+ * (or equivalent prop) indicates best-practices reading get 2-3 extra
178
+ * production deploys spliced in. No flag — derive cohort by counting
179
+ * doc views per user.
147
180
  *
148
- * Mixpanel Report Docs-Informed Deployments:
149
- * • Insights report
150
- * • Event: "service deployed", filter "environment" = "production"
151
- * • Breakdown: "docs_informed"
152
- * • Expected: docs_informed=true shows extra production deployments
181
+ * HOW TO FIND IT IN MIXPANEL:
153
182
  *
154
- * Mixpanel Report Docs Readers vs Non-Readers:
155
- * Insights report
156
- * Event: "service deployed", measure total per user
157
- * Segment: Users with 3+ "documentation viewed" (doc_section = "best_practices")
158
- * Expected: ~1.8x more production deploys per user for docs readers
183
+ * Report 1: Per-User Deploy Volume by Docs Cohort
184
+ * - Cohort A: users with >= 3 "documentation viewed" events
185
+ * - Cohort B: users with < 3
186
+ * - Event: "service deployed"
187
+ * - Measure: Total per user
188
+ * - Expected: A ~ 1.8x B
189
+ *
190
+ * REAL-WORLD ANALOGUE: Engineers who read the docs ship more confidently
191
+ * and more often than those who guess at the platform.
159
192
  *
160
193
  * ─────────────────────────────────────────────────────────────────────────────
161
- * 6. COST OVERRUN PATTERN (event hook — closure state)
194
+ * 6. COST OVERRUN PATTERN (event — closure state)
162
195
  * ─────────────────────────────────────────────────────────────────────────────
163
196
  *
164
- * When cost_change_percent > 25 on a "cost report generated" event, the user
165
- * is stored in a module-level Map. Their next "infrastructure scaled" event
166
- * is forced to scale_direction: "down". Tagged: budget_exceeded, cost_reaction.
197
+ * PATTERN: When cost_change_percent > 25 on a "cost report generated" event,
198
+ * the user is stored in a module-level Map. Their next "infrastructure scaled"
199
+ * event is forced to scale_direction = "down". No flag — discover by
200
+ * sequencing cost-report → infrastructure-scaled per user.
201
+ *
202
+ * HOW TO FIND IT IN MIXPANEL:
167
203
  *
168
- * Mixpanel Report Cost Overrun to Scale Down:
169
- * Insights report
170
- * Event: "infrastructure scaled"
171
- * Breakdown: "cost_reaction"
172
- * Expected: cost_reaction=true events are 100% scale_direction="down"
204
+ * Report 1: Scale Direction Distribution
205
+ * - Report type: Insights
206
+ * - Event: "infrastructure scaled"
207
+ * - Measure: Total
208
+ * - Breakdown: "scale_direction"
209
+ * - Expected: "down" share is elevated above the configured baseline
210
+ *
211
+ * Report 2: Sequencing Check
212
+ * - Inspect users with cost_change_percent > 25 on cost report;
213
+ * their next "infrastructure scaled" should be scale_direction="down"
214
+ * - Expected: ~100% match for the next-scale event after a cost spike
215
+ *
216
+ * REAL-WORLD ANALOGUE: A surprise cloud bill triggers an immediate
217
+ * downscale; no engineer ignores a 25% month-over-month cost jump.
173
218
  *
174
219
  * ─────────────────────────────────────────────────────────────────────────────
175
- * 7. FAILED DEPLOYMENT RECOVERY (event hook — closure state)
220
+ * 7. FAILED DEPLOYMENT RECOVERY (event — closure state)
176
221
  * ─────────────────────────────────────────────────────────────────────────────
177
222
  *
178
- * After a failed pipeline run, the user's next successful deploy has
179
- * duration_sec * 1.5 (recovery deploys are slower). Tagged: recovery_deployment.
180
- * Uses module-level Map for cross-call state.
223
+ * 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.
227
+ *
228
+ * HOW TO FIND IT IN MIXPANEL:
181
229
  *
182
- * Mixpanel Report Recovery Deploy Duration:
183
- * Insights report
184
- * Event: "deployment pipeline run", measure avg "duration_sec"
185
- * Breakdown: "recovery_deployment"
186
- * • Expected: recovery_deployment=true ~ 1.5x longer duration
230
+ * Report 1: Pipeline Duration After Failure (sequencing query)
231
+ * - For each user, find runs where prior run was status="failed"
232
+ * - Compare avg duration_sec of those "next" runs vs all other successful runs
233
+ * - Expected: post-failure runs ~ 1.5x longer duration
234
+ *
235
+ * REAL-WORLD ANALOGUE: After a bad deploy, teams add manual gates and
236
+ * extra verification steps that slow the very next release.
187
237
  *
188
238
  * ─────────────────────────────────────────────────────────────────────────────
189
- * 8. ENTERPRISE VS STARTUP (user hook)
239
+ * 8. ENTERPRISE VS STARTUP (user)
190
240
  * ─────────────────────────────────────────────────────────────────────────────
191
241
  *
192
- * Company size determines seat_count, annual_contract_value, and
193
- * customer_success_manager (enterprise only). All users get customer_health_score.
242
+ * PATTERN: Company size determines seat_count, annual_contract_value, and
243
+ * customer_success_manager (enterprise only). All users get a
244
+ * customer_health_score on the profile.
245
+ *
246
+ * HOW TO FIND IT IN MIXPANEL:
194
247
  *
195
- * Mixpanel Report ACV by Company Size:
196
- * Insights report
197
- * Event: Any, measure unique users
198
- * Breakdown: User profile "company_size"
199
- * Expected: startup ($0-3.6K), smb ($3.6K-12K), mid_market ($12K-50K),
248
+ * Report 1: ACV by Company Size
249
+ * - Report type: Insights
250
+ * - Event: any event
251
+ * - Measure: Unique users
252
+ * - Breakdown: "company_size" (user property)
253
+ * - Expected: startup ($0-3.6K), smb ($3.6K-12K), mid_market ($12K-50K),
200
254
  * enterprise ($50K-500K)
201
255
  *
256
+ * Report 2: Seat Count by Company Size
257
+ * - Report type: Insights
258
+ * - Event: any event
259
+ * - Measure: Average of "seat_count" (user property)
260
+ * - Breakdown: "company_size"
261
+ * - Expected: monotonic ramp from startup to enterprise
262
+ *
263
+ * REAL-WORLD ANALOGUE: B2B SaaS pricing scales orders of magnitude across
264
+ * customer segments — from a $99/mo startup to a $500K Fortune 500 contract.
265
+ *
202
266
  * ─────────────────────────────────────────────────────────────────────────────
203
267
  * EXPECTED METRICS SUMMARY
204
268
  * ─────────────────────────────────────────────────────────────────────────────
@@ -257,10 +321,16 @@ const failedDeployUsers = new Map();
257
321
  const config = {
258
322
  token,
259
323
  seed: SEED,
260
- numDays: num_days,
324
+ datasetStart: "2026-01-01T00:00:00Z",
325
+ datasetEnd: "2026-05-01T23:59:59Z",
326
+ // numDays: num_days,
261
327
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
262
328
  numUsers: num_users,
263
- hasAnonIds: false,
329
+ // Phase 2 identity model — B2B SaaS reference. Engineers commonly use 1-2
330
+ // devices (desktop + work laptop). avgDevicePerUser:2 puts a meaningful
331
+ // per-session sticky-device pattern in Mixpanel device dashboards.
332
+ hasAnonIds: true,
333
+ avgDevicePerUser: 2,
264
334
  hasSessionIds: true,
265
335
  format: "json",
266
336
  gzip: true,
@@ -294,10 +364,13 @@ const config = {
294
364
 
295
365
  funnels: [
296
366
  {
367
+ // First funnel — `workspace created` is the auth event for B2B users.
368
+ // Models real B2B onboarding: most teams take ≤1 retry before sticking.
297
369
  sequence: ["workspace created", "service deployed", "dashboard viewed"],
298
370
  isFirstFunnel: true,
299
371
  conversionRate: 70,
300
372
  timeToConvert: 2,
373
+ attempts: { min: 0, max: 1 },
301
374
  },
302
375
  {
303
376
  // Daily monitoring: dashboards, queries, API calls (most common)
@@ -355,6 +428,10 @@ const config = {
355
428
  event: "workspace created",
356
429
  weight: 1,
357
430
  isFirstEvent: true,
431
+ // Phase 2 identity: workspace creation is the B2B equivalent of Sign Up
432
+ // — engine stamps user_id+device_id on this event when it fires inside
433
+ // the user's first funnel.
434
+ isAuthEvent: true,
358
435
  properties: {
359
436
  company_size: ["startup", "smb", "mid_market", "enterprise"],
360
437
  industry: ["tech", "finance", "healthcare", "retail", "media"],
@@ -368,7 +445,6 @@ const config = {
368
445
  service_type: ["web_app", "api", "database", "cache", "queue", "ml_model"],
369
446
  environment: ["production", "staging", "dev"],
370
447
  cloud_provider: ["aws", "gcp", "azure"],
371
- docs_informed: [false],
372
448
  }
373
449
  },
374
450
  {
@@ -409,7 +485,6 @@ const config = {
409
485
  alert_id: alertIds,
410
486
  response_time_mins: u.weighNumRange(1, 120),
411
487
  acknowledged_by_role: ["engineer", "sre", "manager", "oncall"],
412
- integrated_team: [false],
413
488
  }
414
489
  },
415
490
  {
@@ -419,7 +494,6 @@ const config = {
419
494
  alert_id: alertIds,
420
495
  resolution_time_mins: u.weighNumRange(5, 1440),
421
496
  root_cause: ["config_change", "capacity", "bug", "dependency", "network"],
422
- integrated_team: [false],
423
497
  }
424
498
  },
425
499
  {
@@ -430,7 +504,6 @@ const config = {
430
504
  status: ["success", "failed", "cancelled"],
431
505
  duration_sec: u.weighNumRange(30, 1800),
432
506
  commit_count: u.weighNumRange(1, 20),
433
- recovery_deployment: [false],
434
507
  }
435
508
  },
436
509
  {
@@ -438,11 +511,10 @@ const config = {
438
511
  weight: 5,
439
512
  properties: {
440
513
  service_id: serviceIds,
441
- scale_direction: ["down", "down", "down", "down", "down", "down", "up"],
514
+ scale_direction: ["up", "up", "up", "down"],
442
515
  previous_capacity: u.weighNumRange(1, 100),
443
516
  new_capacity: u.weighNumRange(1, 100),
444
517
  auto_scaled: [false, false, false, false, false, false, true],
445
- cost_reaction: [false],
446
518
  }
447
519
  },
448
520
  {
@@ -452,8 +524,6 @@ const config = {
452
524
  report_period: ["daily", "weekly", "monthly"],
453
525
  total_cost: u.weighNumRange(100, 50000),
454
526
  cost_change_percent: u.weighNumRange(-30, 50),
455
- cost_alert: [false],
456
- budget_exceeded: [false],
457
527
  }
458
528
  },
459
529
  {
@@ -462,8 +532,6 @@ const config = {
462
532
  properties: {
463
533
  role: ["admin", "editor", "viewer", "billing"],
464
534
  invitation_method: ["email", "sso", "slack"],
465
- quarter_end_push: [false],
466
- duplicate_invite: [false],
467
535
  }
468
536
  },
469
537
  {
@@ -496,9 +564,8 @@ const config = {
496
564
  event: "billing event",
497
565
  weight: 3,
498
566
  properties: {
499
- event_type: ["invoice_generated", "payment_received", "payment_failed", "plan_upgraded", "plan_downgraded"],
567
+ event_type: ["invoice_generated", "invoice_generated", "payment_received", "payment_received", "payment_received", "payment_failed", "plan_upgraded", "plan_downgraded"],
500
568
  amount: u.weighNumRange(99, 25000),
501
- quarter_end_push: [false],
502
569
  }
503
570
  },
504
571
  {
@@ -548,7 +615,6 @@ const config = {
548
615
  company_size: ["startup", "startup", "smb", "mid_market", "enterprise"],
549
616
  primary_role: ["engineer", "sre", "devops", "manager", "executive"],
550
617
  team_name: ["Platform", "Backend", "Frontend", "Data", "Security", "Infrastructure"],
551
- churned_account: [false],
552
618
  seat_count: [1],
553
619
  annual_contract_value: [0],
554
620
  customer_success_manager: [false],
@@ -577,7 +643,7 @@ const config = {
577
643
  *
578
644
  * This hook function creates 8 deliberate patterns in the data:
579
645
  *
580
- * 1. END-OF-QUARTER SPIKE: Days 80-90 drive plan upgrades and team expansion
646
+ * 1. END-OF-QUARTER SPIKE: Days 100-110 drive plan upgrades and team expansion
581
647
  * 2. CHURNED ACCOUNT SILENCING: ~10% of users go completely silent after month 1
582
648
  * 3. ALERT ESCALATION REPLACEMENT: Critical alerts become "incident created" events
583
649
  * 4. INTEGRATION USERS SUCCEED: Slack+PagerDuty users resolve incidents 50-60% faster
@@ -587,55 +653,14 @@ const config = {
587
653
  * 8. ENTERPRISE VS STARTUP: Company size determines seat count, ACV, and health score
588
654
  */
589
655
  hook: function (record, type, meta) {
590
- const NOW = dayjs();
591
- const DATASET_START = NOW.subtract(num_days, "days");
656
+ // (Hook 1a moved to everything hook for reliable datasetStart access)
592
657
 
593
- // ─────────────────────────────────────────────────────────────
594
- // Hook #1: END-OF-QUARTER SPIKE (event)
595
- // Days 80-90: billing upgrades and team expansion surge
596
- // ─────────────────────────────────────────────────────────────
597
- if (type === "event") {
598
- const EVENT_TIME = dayjs(record.time);
599
- const dayInDataset = EVENT_TIME.diff(DATASET_START, "days", true);
600
-
601
- if (record.event === "billing event") {
602
- if (dayInDataset >= 80 && dayInDataset <= 90 && chance.bool({ likelihood: 40 })) {
603
- record.event_type = "plan_upgraded";
604
- record.quarter_end_push = true;
605
- } else {
606
- record.quarter_end_push = false;
607
- }
608
- }
609
-
610
- if (record.event === "team member invited") {
611
- if (dayInDataset >= 80 && dayInDataset <= 90) {
612
- record.quarter_end_push = true;
613
- // 50% of the time duplicate the invite event (hiring push)
614
- if (chance.bool({ likelihood: 50 })) {
615
- return {
616
- ...record,
617
- time: EVENT_TIME.add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
618
- role: chance.pickone(["editor", "viewer"]),
619
- invitation_method: chance.pickone(["email", "sso", "slack"]),
620
- quarter_end_push: true,
621
- duplicate_invite: true,
622
- };
623
- }
624
- } else {
625
- record.quarter_end_push = false;
626
- }
627
- }
628
- }
629
-
630
- // ─────────────────────────────────────────────────────────────
631
- // Hook #3: ALERT ESCALATION REPLACEMENT (event)
632
- // Critical/emergency alerts sometimes become formal incidents
633
- // ─────────────────────────────────────────────────────────────
658
+ // HOOK 3: ALERT ESCALATION REPLACEMENT (event) — critical/emergency
659
+ // alerts sometimes become incident-created events. Real product flow.
634
660
  if (type === "event") {
635
661
  if (record.event === "alert triggered") {
636
662
  const severity = record.severity;
637
663
  if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: 30 })) {
638
- // REPLACE the event entirely with an "incident created" event
639
664
  return {
640
665
  ...record,
641
666
  event: "incident created",
@@ -650,178 +675,160 @@ const config = {
650
675
  }
651
676
  }
652
677
 
653
- // ─────────────────────────────────────────────────────────────
654
- // Hook #6: COST OVERRUN PATTERN (event)
655
- // Budget-exceeded users react by scaling down infrastructure
656
- // Uses module-level costOverrunUsers Map for cross-call state
657
- // ─────────────────────────────────────────────────────────────
678
+ // HOOK 6: COST OVERRUN PATTERN (event) — cost reports with cost_change
679
+ // > 25% record user, then next infrastructure-scaled event from that
680
+ // user gets scale_direction = "down". No flag.
658
681
  if (type === "event") {
659
- if (record.event === "cost report generated") {
660
- const costChange = record.cost_change_percent;
661
- if (costChange > 25) {
662
- record.cost_alert = true;
663
- record.budget_exceeded = true;
664
- costOverrunUsers.set(record.user_id, true);
665
- } else {
666
- record.cost_alert = false;
667
- record.budget_exceeded = false;
668
- }
682
+ if (record.event === "cost report generated" && record.cost_change_percent > 25) {
683
+ costOverrunUsers.set(record.user_id, true);
669
684
  }
670
-
671
- if (record.event === "infrastructure scaled") {
672
- if (costOverrunUsers.has(record.user_id)) {
673
- record.scale_direction = "down";
674
- record.cost_reaction = true;
675
- costOverrunUsers.delete(record.user_id);
676
- } else {
677
- record.cost_reaction = false;
678
- }
685
+ if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
686
+ record.scale_direction = "down";
687
+ costOverrunUsers.delete(record.user_id);
679
688
  }
680
689
  }
681
690
 
682
- // ─────────────────────────────────────────────────────────────
683
- // Hook #7: FAILED DEPLOYMENT RECOVERY (event)
684
- // Recovery deploys take 1.5x longer after a failure
685
- // Uses module-level failedDeployUsers Map for cross-call state
686
- // ─────────────────────────────────────────────────────────────
687
- if (type === "event") {
688
- if (record.event === "deployment pipeline run") {
689
- const status = record.status;
690
- if (status === "failed") {
691
- failedDeployUsers.set(record.user_id, true);
692
- record.recovery_deployment = false;
693
- } else if (status === "success" && failedDeployUsers.has(record.user_id)) {
694
- record.duration_sec = Math.floor((record.duration_sec || 300) * 1.5);
695
- record.recovery_deployment = true;
696
- failedDeployUsers.delete(record.user_id);
697
- } else {
698
- record.recovery_deployment = false;
699
- }
700
- }
701
- }
702
-
703
- // ─────────────────────────────────────────────────────────────
704
- // Hook #2: CHURNED ACCOUNT SILENCING (everything)
705
- // ~20% targeted (hash % 5), yielding ~10% visible after accounting for invisible churned users
706
- // ─────────────────────────────────────────────────────────────
707
691
  if (type === "everything") {
692
+ const datasetStart = dayjs.unix(meta.datasetStart);
708
693
  const userEvents = record;
709
694
  const profile = meta.profile;
710
695
 
711
- // Stamp superProps from profile for consistency
712
696
  userEvents.forEach(e => {
713
697
  e.plan_tier = profile.plan_tier;
714
698
  e.cloud_provider = profile.cloud_provider;
715
699
  });
716
700
 
701
+ // HOOK 1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
702
+ // event_type to plan_upgraded 40% of the time. No flag.
703
+ userEvents.forEach(e => {
704
+ if (e.event !== "billing event") return;
705
+ const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
706
+ if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 40 })) {
707
+ e.event_type = "plan_upgraded";
708
+ }
709
+ });
710
+
711
+ // HOOK 1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone
712
+ // 50% of team-member-invited events (push, not return). No flag.
713
+ for (let i = userEvents.length - 1; i >= 0; i--) {
714
+ const e = userEvents[i];
715
+ if (e.event !== "team member invited") continue;
716
+ const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
717
+ if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 50 })) {
718
+ userEvents.push({
719
+ ...e,
720
+ time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
721
+ user_id: e.user_id,
722
+ role: chance.pickone(["editor", "viewer"]),
723
+ invitation_method: chance.pickone(["email", "sso", "slack"]),
724
+ });
725
+ }
726
+ }
727
+
728
+ // HOOK 2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5)
729
+ // have post-day-30 events removed. No flag.
717
730
  if (userEvents && userEvents.length > 0) {
718
731
  const firstEvent = userEvents[0];
719
732
  const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
720
- const isChurnedAccount = (idHash % 5) === 0;
721
-
722
- if (isChurnedAccount) {
733
+ if ((idHash % 5) === 0) {
723
734
  for (let i = userEvents.length - 1; i >= 0; i--) {
724
- const evt = userEvents[i];
725
- const dayInDataset = dayjs(evt.time).diff(DATASET_START, "days", true);
735
+ const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
726
736
  if (dayInDataset > 30) {
727
737
  userEvents.splice(i, 1);
728
738
  }
729
739
  }
730
740
  }
731
741
  }
732
- }
733
-
734
- // ─────────────────────────────────────────────────────────────
735
- // Hook #4: INTEGRATION USERS SUCCEED (everything)
736
- // Users with both Slack AND PagerDuty integrations resolve faster
737
- // ─────────────────────────────────────────────────────────────
738
- if (type === "everything") {
739
- const userEvents = record;
740
742
 
741
- // First pass: check if user has both slack and pagerduty integrations
743
+ // HOOK 4: INTEGRATION USERS SUCCEED Slack+PagerDuty users get
744
+ // alert response_time_mins 0.4x and resolution_time_mins 0.5x.
745
+ // Mutates raw props. No flag.
742
746
  let hasSlack = false;
743
747
  let hasPagerduty = false;
744
-
745
748
  userEvents.forEach((event) => {
746
749
  if (event.event === "integration configured") {
747
- const integrationType = event.integration_type;
748
- if (integrationType === "slack") hasSlack = true;
749
- if (integrationType === "pagerduty") hasPagerduty = true;
750
+ if (event.integration_type === "slack") hasSlack = true;
751
+ if (event.integration_type === "pagerduty") hasPagerduty = true;
750
752
  }
751
753
  });
752
-
753
- const hasFullIntegration = hasSlack && hasPagerduty;
754
-
755
- // Second pass: set integrated_team on all alert events, then boost for integrated users
756
- userEvents.forEach((event) => {
757
- if (event.event === "alert acknowledged") {
758
- if (hasFullIntegration && event.response_time_mins) {
754
+ if (hasSlack && hasPagerduty) {
755
+ userEvents.forEach((event) => {
756
+ if (event.event === "alert acknowledged" && event.response_time_mins) {
759
757
  event.response_time_mins = Math.floor(event.response_time_mins * 0.4);
760
- event.integrated_team = true;
761
- } else {
762
- event.integrated_team = false;
763
758
  }
764
- }
765
- if (event.event === "alert resolved") {
766
- if (hasFullIntegration && event.resolution_time_mins) {
759
+ if (event.event === "alert resolved" && event.resolution_time_mins) {
767
760
  event.resolution_time_mins = Math.floor(event.resolution_time_mins * 0.5);
768
- event.integrated_team = true;
769
- } else {
770
- event.integrated_team = false;
771
761
  }
772
- }
773
- });
774
- }
762
+ });
763
+ }
775
764
 
776
- // ─────────────────────────────────────────────────────────────
777
- // Hook #5: DOCS READERS DEPLOY MORE (everything)
778
- // Users who read best_practices 3+ times get extra production deploys
779
- // ─────────────────────────────────────────────────────────────
780
- if (type === "everything") {
781
- const userEvents = record;
765
+ // HOOK 5 + HOOK 10: DOCS MAGIC NUMBER (no flags)
766
+ // Sweet 4-7 documentation-viewed events +40% extra cloned
767
+ // service-deployed events. Over 8+ drop 25% of service-deployed
768
+ // events. No flag.
769
+ const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
770
+ const deployTemplate = userEvents.find(e => e.event === "service deployed");
771
+ if (docsCount >= 4 && docsCount <= 7 && deployTemplate) {
772
+ const lastEvent = userEvents[userEvents.length - 1];
773
+ const extraDeploys = chance.integer({ min: 2, max: 3 });
774
+ for (let i = 0; i < extraDeploys; i++) {
775
+ userEvents.push({
776
+ ...deployTemplate,
777
+ time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
778
+ user_id: lastEvent.user_id,
779
+ service_id: chance.pickone(serviceIds),
780
+ service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
781
+ environment: "production",
782
+ cloud_provider: profile.cloud_provider,
783
+ });
784
+ }
785
+ } else if (docsCount >= 8) {
786
+ for (let i = userEvents.length - 1; i >= 0; i--) {
787
+ if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: 25 })) {
788
+ userEvents.splice(i, 1);
789
+ }
790
+ }
791
+ }
782
792
 
783
- // First pass: count best_practices documentation views
784
- let bestPracticesCount = 0;
785
- userEvents.forEach((event) => {
786
- if (event.event === "documentation viewed" && event.doc_section === "best_practices") {
787
- bestPracticesCount++;
793
+ // HOOK 7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs
794
+ // in this user's pipeline events, multiply duration_sec by 1.5 on
795
+ // the recovery deploy. Full control via everything hook.
796
+ const pipelineEvents = userEvents
797
+ .filter(e => e.event === "deployment pipeline run")
798
+ .sort((a, b) => a.time.localeCompare(b.time));
799
+ for (let i = 1; i < pipelineEvents.length; i++) {
800
+ if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
801
+ pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * 1.5);
788
802
  }
789
- });
803
+ }
790
804
 
791
- // Second pass: if 3+ best practices views, add extra production deploys
792
- if (bestPracticesCount >= 3) {
793
- const extraDeploys = chance.integer({ min: 2, max: 3 });
794
- const lastEvent = userEvents[userEvents.length - 1];
795
- const deployTemplate = userEvents.find(e => e.event === "service deployed");
796
- if (lastEvent && deployTemplate) {
797
- for (let i = 0; i < extraDeploys; i++) {
798
- const deployEvent = {
799
- ...deployTemplate,
800
- time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
801
- user_id: lastEvent.user_id,
802
- service_id: chance.pickone(serviceIds),
803
- service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
804
- environment: "production",
805
- cloud_provider: chance.pickone(["aws", "gcp", "azure"]),
806
- docs_informed: true,
807
- };
808
- userEvents.splice(userEvents.length, 0, deployEvent);
809
- }
805
+ // HOOK 9: INCIDENT RESPONSE TTC enterprise resolves faster,
806
+ // startup resolves slower. Scale the time gap between alert
807
+ // triggered acknowledged resolved for this user.
808
+ const segment = profile?.company_size;
809
+ const ttcFactor = (
810
+ segment === "enterprise" ? 0.5 :
811
+ segment === "startup" ? 1.8 :
812
+ 1.0
813
+ );
814
+ if (ttcFactor !== 1.0) {
815
+ const alertEvents = userEvents
816
+ .filter(e => e.event === "alert triggered" || e.event === "alert acknowledged" || e.event === "alert resolved")
817
+ .sort((a, b) => a.time.localeCompare(b.time));
818
+ for (let i = 1; i < alertEvents.length; i++) {
819
+ const prev = dayjs(alertEvents[i - 1].time);
820
+ const curr = dayjs(alertEvents[i].time);
821
+ const gap = curr.diff(prev);
822
+ const newGap = Math.round(gap * ttcFactor);
823
+ alertEvents[i].time = prev.add(newGap, "milliseconds").toISOString();
810
824
  }
811
825
  }
812
826
  }
813
827
 
814
- // ─────────────────────────────────────────────────────────────
815
- // Hook #8: ENTERPRISE VS STARTUP (user)
816
- // Company size determines seat count, ACV, and health score
817
- // ─────────────────────────────────────────────────────────────
828
+ // HOOK 8: ENTERPRISE VS STARTUP (user) — company size determines
829
+ // seat count, ACV, and CSM. Real profile attrs.
818
830
  if (type === "user") {
819
- // Hook #2 support: tag churned accounts on user profile for discoverability
820
- const idHash = String(record.distinct_id || "").split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
821
- record.churned_account = (idHash % 5) === 0;
822
-
823
831
  const companySize = record.company_size;
824
-
825
832
  if (companySize === "enterprise") {
826
833
  record.seat_count = chance.integer({ min: 50, max: 500 });
827
834
  record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
@@ -839,7 +846,6 @@ const config = {
839
846
  record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
840
847
  record.customer_success_manager = false;
841
848
  }
842
-
843
849
  record.customer_health_score = chance.integer({ min: 1, max: 100 });
844
850
  }
845
851