@ak--47/dungeon-master 1.5.4 → 1.6.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 (149) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -12
  2. package/.claude/skills/create-dungeon/SKILL.md +65 -5
  3. package/.claude/skills/create-project/SKILL.md +1 -1
  4. package/.claude/skills/create-project/provision.mjs +57 -10
  5. package/.claude/skills/powertools/SKILL.md +75 -0
  6. package/.claude/skills/powertools/pt.mjs +91 -0
  7. package/.claude/skills/powertools/snapshot-project.mjs +124 -0
  8. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  9. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  10. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  11. package/.claude/skills/write-hooks/SKILL.md +156 -18
  12. package/CHANGELOG.md +224 -0
  13. package/HOOKS.md +566 -112
  14. package/README.md +30 -9
  15. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  16. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  17. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  18. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  19. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  20. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  21. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  22. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  23. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  24. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  25. package/dungeons/technical/stories-verify.js +152 -0
  26. package/dungeons/vertical/README.md +154 -0
  27. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  28. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  29. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  30. package/dungeons/vertical/community/community.js +1589 -0
  31. package/dungeons/vertical/community/community.sql +216 -0
  32. package/dungeons/vertical/community/community.verify.mjs +72 -0
  33. package/dungeons/vertical/crypto/crypto.js +1483 -0
  34. package/dungeons/vertical/crypto/crypto.sql +194 -0
  35. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  36. package/dungeons/vertical/dating/dating.js +1540 -0
  37. package/dungeons/vertical/dating/dating.sql +288 -0
  38. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  39. package/dungeons/vertical/devtools/devtools.js +1524 -0
  40. package/dungeons/vertical/devtools/devtools.sql +161 -0
  41. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  42. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  43. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  44. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  45. package/dungeons/vertical/education/education.js +1763 -0
  46. package/dungeons/vertical/education/education.sql +243 -0
  47. package/dungeons/vertical/education/education.verify.mjs +75 -0
  48. package/dungeons/vertical/fintech/fintech.js +1410 -0
  49. package/dungeons/vertical/fintech/fintech.sql +175 -0
  50. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  51. package/dungeons/vertical/fitness/fitness.js +1495 -0
  52. package/dungeons/vertical/fitness/fitness.sql +206 -0
  53. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  54. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  55. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  56. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  57. package/dungeons/vertical/gaming/gaming.js +1741 -0
  58. package/dungeons/vertical/gaming/gaming.sql +249 -0
  59. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  60. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  61. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  62. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  63. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  64. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  65. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  66. package/dungeons/vertical/logistics/logistics.js +1541 -0
  67. package/dungeons/vertical/logistics/logistics.sql +205 -0
  68. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  69. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  70. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  71. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  72. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  73. package/dungeons/vertical/media/media.sql +173 -0
  74. package/dungeons/vertical/media/media.verify.mjs +74 -0
  75. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  76. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  77. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  78. package/dungeons/vertical/sass/sass.js +1733 -0
  79. package/dungeons/vertical/sass/sass.sql +173 -0
  80. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  81. package/dungeons/vertical/social/social.js +1456 -0
  82. package/dungeons/vertical/social/social.sql +160 -0
  83. package/dungeons/vertical/social/social.verify.mjs +76 -0
  84. package/dungeons/vertical/streaming/streaming.js +677 -0
  85. package/dungeons/vertical/streaming/streaming.sql +101 -0
  86. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  87. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  88. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  89. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  90. package/dungeons/vertical/travel/travel.js +1603 -0
  91. package/dungeons/vertical/travel/travel.sql +236 -0
  92. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  93. package/index.js +7 -3
  94. package/lib/core/config-validator.js +37 -2
  95. package/lib/core/context.js +4 -2
  96. package/lib/core/dungeon-loader.js +20 -15
  97. package/lib/core/dungeon-to-json.js +22 -4
  98. package/lib/core/storage.js +3 -3
  99. package/lib/generators/funnels.js +36 -9
  100. package/lib/hook-helpers/cohort.js +53 -0
  101. package/lib/hook-helpers/index.js +8 -0
  102. package/lib/hook-helpers/mutate.js +1 -1
  103. package/lib/hook-helpers/shape.js +319 -0
  104. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  105. package/lib/hook-patterns/attributed-by-source.js +60 -58
  106. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  107. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  108. package/lib/hook-patterns/index.js +1 -1
  109. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  110. package/lib/orchestrators/user-loop.js +41 -0
  111. package/lib/templates/story-spec.schema.json +175 -0
  112. package/lib/utils/json-evaluator.js +1 -1
  113. package/lib/utils/utils.js +151 -59
  114. package/lib/verify/coerce.js +186 -0
  115. package/lib/verify/counting.js +270 -32
  116. package/lib/verify/emulate-breakdown.js +1285 -203
  117. package/lib/verify/first-time.js +89 -0
  118. package/lib/verify/flows.js +679 -0
  119. package/lib/verify/formula.js +259 -0
  120. package/lib/verify/funnel-engine.js +697 -113
  121. package/lib/verify/index.js +32 -4
  122. package/lib/verify/sessionize.js +275 -0
  123. package/lib/verify/story-runner.js +483 -0
  124. package/lib/verify/verify-dungeon.js +59 -37
  125. package/package.json +2 -1
  126. package/scripts/extract-dungeon-schema.mjs +2 -2
  127. package/scripts/run-many.mjs +2 -2
  128. package/scripts/verify-runner.mjs +2 -2
  129. package/scripts/verify-stories.mjs +244 -0
  130. package/types.d.ts +298 -30
  131. package/dungeons/vertical/ai-platform.js +0 -818
  132. package/dungeons/vertical/community.js +0 -966
  133. package/dungeons/vertical/crypto.js +0 -895
  134. package/dungeons/vertical/dating.js +0 -830
  135. package/dungeons/vertical/devtools.js +0 -1050
  136. package/dungeons/vertical/ecommerce.js +0 -811
  137. package/dungeons/vertical/education.js +0 -964
  138. package/dungeons/vertical/fintech.js +0 -946
  139. package/dungeons/vertical/fitness.js +0 -935
  140. package/dungeons/vertical/food-delivery.js +0 -934
  141. package/dungeons/vertical/gaming.js +0 -1120
  142. package/dungeons/vertical/healthcare.js +0 -945
  143. package/dungeons/vertical/insurance-application.js +0 -1039
  144. package/dungeons/vertical/logistics.js +0 -986
  145. package/dungeons/vertical/marketplace.js +0 -1019
  146. package/dungeons/vertical/real-estate.js +0 -872
  147. package/dungeons/vertical/sass.js +0 -995
  148. package/dungeons/vertical/social.js +0 -904
  149. package/dungeons/vertical/travel.js +0 -930
@@ -1,935 +0,0 @@
1
- // ── IMPORTS ──
2
- import dayjs from "dayjs";
3
- import utc from "dayjs/plugin/utc.js";
4
- dayjs.extend(utc);
5
- import "dotenv/config";
6
- import * as u from "../../lib/utils/utils.js";
7
- /** @typedef {import("../../types").Dungeon} Config */
8
-
9
- // ── OVERVIEW ──
10
- /*
11
- * NAME: FitQuest
12
- * APP: Fitness & wellness app for workout tracking, meal planning,
13
- * social fitness challenges, and AI coaching. Core loop: sign
14
- * up → plan workout → complete workout → track progress.
15
- * Revenue: free / monthly ($12.99, 7-day trial) / annual
16
- * ($99.99) / family ($149.99).
17
- * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
18
- * CORE LOOP: account created → workout planned → workout completed → progress checked
19
- *
20
- * EVENTS (18):
21
- * workout completed (8) > app session (8) > meal logged (7) > workout planned (6)
22
- * > progress checked (5) > notification received (5) > leaderboard viewed (4)
23
- * > nutrition plan viewed (4) > challenge joined (3) > coach session (3)
24
- * > heart rate recorded (3) > achievement unlocked (2) > friend added (2)
25
- * > challenge completed (2) > subscription managed (2) > profile updated (2)
26
- * > account created (1) > account deactivated (1)
27
- *
28
- * FUNNELS (5):
29
- * - Onboarding: account created → profile updated → workout planned → workout completed (45%)
30
- * - Workout Loop: workout planned → workout completed → progress checked (45%, reentry)
31
- * - Social Engagement: friend added → leaderboard viewed → challenge joined (35%)
32
- * - Challenge Completion: challenge joined → workout completed → challenge completed → achievement unlocked (30%)
33
- * - Coaching Path: coach session → workout planned → workout completed → progress checked (50%)
34
- *
35
- * USER PROPS: fitness_level, segment, streak_days, total_workouts, preferred_workout, goal, Platform, workout_type, subscription_tier
36
- * SUPER PROPS: Platform, workout_type, subscription_tier
37
- * SCD PROPS: fitness_level (beginner/intermediate/advanced/elite, monthly fuzzy, max 8)
38
- * GROUPS: none
39
- */
40
-
41
- // ── HOOK STORIES ──
42
- /*
43
- * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
44
- * via raw-prop breakdowns (HOD, day, segment) or behavioral cohorts.
45
- *
46
- * ───────────────────────────────────────────────────────────────
47
- * 1. MORNING WORKOUT BOOST (everything)
48
- * ───────────────────────────────────────────────────────────────
49
- *
50
- * PATTERN: Workouts in 5-9 UTC get calories_burned 1.3x. Mutates
51
- * raw prop. No flag.
52
- *
53
- * HOW TO FIND IT IN MIXPANEL:
54
- *
55
- * Report 1: Avg Calories Burned by Hour of Day
56
- * - Event: "workout completed"
57
- * - Measure: Average of "calories_burned"
58
- * - Breakdown: hour of day
59
- * - Expected: 5-9 hours show ~ 1.3x baseline
60
- *
61
- * REAL-WORLD ANALOGUE: Morning workouts get a metabolic boost.
62
- *
63
- * ───────────────────────────────────────────────────────────────
64
- * 2. POST-LAUNCH AI COACHING LIFT (everything hook)
65
- * ───────────────────────────────────────────────────────────────
66
- *
67
- * PATTERN: After day 35 (ai_coach feature launch), workouts
68
- * with coaching_mode="ai_assisted" get 1.2x duration_minutes.
69
- * AI coaching helps users push through longer sessions.
70
- *
71
- * HOW TO FIND IT IN MIXPANEL:
72
- *
73
- * Report 1: AI Coaching Duration Lift
74
- * • Report type: Insights
75
- * • Event: "workout completed"
76
- * • Measure: Average of "duration_minutes"
77
- * • Breakdown: "coaching_mode"
78
- * • Filter: time after day 35
79
- * • Expected: ai_assisted ≈ 48 min vs self_guided ≈ 40 min
80
- *
81
- * Report 2: AI Coaching Adoption Over Time
82
- * • Report type: Insights
83
- * • Event: "workout completed"
84
- * • Measure: Total
85
- * • Breakdown: "coaching_mode"
86
- * • Line chart by week
87
- * • Expected: ai_assisted grows from 0 after day 35
88
- *
89
- * REAL-WORLD ANALOGUE: AI-powered coaching features increase
90
- * session duration as users get real-time form and pacing guidance.
91
- *
92
- * ───────────────────────────────────────────────────────────────
93
- * 3. STREAK RETENTION (everything hook)
94
- * ───────────────────────────────────────────────────────────────
95
- *
96
- * PATTERN: Users with >=2 workout events get streak_days set
97
- * to their actual workout count on their profile, and receive
98
- * achievement clones with super-linear scaling: 1 per workout
99
- * for workouts 2-4, then 4 per workout beyond that. This
100
- * amplifies the gap so athlete/casual ratio reaches 2x+.
101
- *
102
- * HOW TO FIND IT IN MIXPANEL:
103
- *
104
- * Report 1: Achievement Events by Workout Volume
105
- * • Report type: Insights
106
- * • Event: "achievement unlocked"
107
- * • Measure: Total per user
108
- * • Breakdown: user property "segment"
109
- * • Expected: athlete and coach segments show 2-3x more
110
- * achievements than casual and resolver segments
111
- *
112
- * Report 2: Streak Days Distribution
113
- * • Report type: Insights
114
- * • Event: "profile updated"
115
- * • Measure: Average of user property "streak_days"
116
- * • Breakdown: user property "segment"
117
- * • Expected: athlete/coach ≈ 30-50 streaks, casual ≈ 10-15
118
- *
119
- * REAL-WORLD ANALOGUE: Gamification streaks are the #1 retention
120
- * driver in fitness apps — users who hit milestones stay longer.
121
- *
122
- * ───────────────────────────────────────────────────────────────
123
- * 4. SOCIAL CHALLENGE COMPLETION (everything hook)
124
- * ───────────────────────────────────────────────────────────────
125
- *
126
- * PATTERN: Users with "friend added" events complete 1.5x more
127
- * "challenge completed" events (cloned from existing ones).
128
- * Social users are more accountable.
129
- *
130
- * HOW TO FIND IT IN MIXPANEL:
131
- *
132
- * Report 1: Challenge Completion Rate by Social Activity
133
- * • Report type: Insights
134
- * • Event: "challenge completed"
135
- * • Measure: Total per user
136
- * • Filter: users who did "friend added" at least once
137
- * • Compare to: users who never did "friend added"
138
- * • Expected: social users ≈ 1.5x more challenge completions
139
- *
140
- * REAL-WORLD ANALOGUE: Social accountability is a proven
141
- * motivator — users with friends complete more challenges.
142
- *
143
- * ───────────────────────────────────────────────────────────────
144
- * 5. RESOLVER CHURN CLIFF (everything hook)
145
- * ───────────────────────────────────────────────────────────────
146
- *
147
- * PATTERN: Users in the "resolver" segment with <8 events
148
- * lose 70% of their events after day 14. Simulates the classic
149
- * New Year's resolution drop-off.
150
- *
151
- * HOW TO FIND IT IN MIXPANEL:
152
- *
153
- * Report 1: Resolver Retention Drop
154
- * • Report type: Insights
155
- * • Event: All events
156
- * • Measure: Total
157
- * • Filter: segment = "resolver"
158
- * • Line chart by week
159
- * • Expected: Sharp cliff after week 2, ~70% drop in volume
160
- *
161
- * Report 2: Segment Retention Comparison
162
- * • Report type: Retention
163
- * • Starting event: "account created"
164
- * • Return event: Any active event
165
- * • Breakdown: user property "segment"
166
- * • Expected: resolver retention drops to <30% by week 3
167
- *
168
- * REAL-WORLD ANALOGUE: 80% of New Year's gym memberships are
169
- * abandoned by mid-February — the "resolution cliff."
170
- *
171
- * ───────────────────────────────────────────────────────────────
172
- * 6. COACH SESSION QUALITY (everything hook)
173
- * ───────────────────────────────────────────────────────────────
174
- *
175
- * PATTERN: Users with "coach session" events get higher
176
- * satisfaction_score (4.0-5.0) on those events. Coached users
177
- * rate their experience higher.
178
- *
179
- * HOW TO FIND IT IN MIXPANEL:
180
- *
181
- * Report 1: Satisfaction by Session Type
182
- * • Report type: Insights
183
- * • Event: "coach session"
184
- * • Measure: Average of "satisfaction_score"
185
- * • Expected: ≈ 4.3 avg (vs baseline 3.0 for non-coached)
186
- *
187
- * REAL-WORLD ANALOGUE: Personal coaching sessions have higher
188
- * satisfaction scores because of personalized attention.
189
- *
190
- * ───────────────────────────────────────────────────────────────
191
- * 7. COACH PROFILE ENRICHMENT (user hook)
192
- * ───────────────────────────────────────────────────────────────
193
- *
194
- * PATTERN: Users in the "coach" segment get total_workouts
195
- * boosted to 200-500 and streak_days to 60-365. Coaches are
196
- * power users who lead by example.
197
- *
198
- * HOW TO FIND IT IN MIXPANEL:
199
- *
200
- * Report 1: Workout Volume by Segment
201
- * • Report type: Insights
202
- * • Event: "workout completed"
203
- * • Measure: Average of user property "total_workouts"
204
- * • Breakdown: user property "segment"
205
- * • Expected: coach ≈ 350 vs athlete ≈ 0 (default) vs casual ≈ 0
206
- *
207
- * Report 2: Streak Distribution by Segment
208
- * • Report type: Insights
209
- * • Measure: Average of user property "streak_days"
210
- * • Breakdown: user property "segment"
211
- * • Expected: coach ≈ 200 vs others ≈ 0 (pre-hook default)
212
- *
213
- * REAL-WORLD ANALOGUE: Fitness coaches maintain extreme workout
214
- * consistency to build credibility with their clients.
215
- *
216
- * ───────────────────────────────────────────────────────────────
217
- * 8. ANNUAL SUBSCRIBER WORKOUT FUNNEL LIFT (everything hook)
218
- * ───────────────────────────────────────────────────────────────
219
- *
220
- * PATTERN: Free/monthly-tier users lose ~30% of "progress checked"
221
- * events (last step of the Workout Loop funnel), simulating lower
222
- * follow-through. Annual/family subscribers retain all events.
223
- *
224
- * HOW TO FIND IT IN MIXPANEL:
225
- *
226
- * Report 1: Workout Funnel Conversion by Tier
227
- * • Report type: Funnels
228
- * • Steps: "workout planned" → "workout completed" → "progress checked"
229
- * • Breakdown: "subscription_tier" (superProp)
230
- * • Expected: annual ≈ 63% vs free ≈ 45% conversion
231
- *
232
- * REAL-WORLD ANALOGUE: Annual gym memberships have higher
233
- * utilization — sunk cost + commitment drives consistency.
234
- *
235
- * ───────────────────────────────────────────────────────────────
236
- * 9. WORKOUT LOOP TIME-TO-CONVERT (funnel-post)
237
- * ───────────────────────────────────────────────────────────────
238
- *
239
- * PATTERN: Annual + family subscribers complete the Workout Loop
240
- * funnel 1.3x faster (factor 0.77); Free users 1.25x slower (factor 1.25).
241
- *
242
- * HOW TO FIND IT IN MIXPANEL:
243
- *
244
- * Report 1: Workout Loop Median Time-to-Convert by Subscription
245
- * - Funnels > "workout planned" -> "workout completed" -> "progress checked"
246
- * - Measure: Median time to convert
247
- * - Breakdown: subscription_plan
248
- * - Expected: annual ~ 0.77x; free ~ 1.25x
249
- *
250
- * NOTE (funnel-post measurement): visible only via Mixpanel funnel
251
- * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
252
- * show this — funnel-post adjusts gaps within funnel instances, not
253
- * across the user's full event history.
254
- *
255
- * ───────────────────────────────────────────────────────────────
256
- * 10. WORKOUT-COUNT MAGIC NUMBER (everything)
257
- * ───────────────────────────────────────────────────────────────
258
- *
259
- * PATTERN: Sweet 12-14 workouts/user → +35% on workout
260
- * duration_minutes (peak progression). Over 15+ → drop 65% of
261
- * post-day-30 non-workout, non-progress events (overtraining
262
- * churn). Preserves workout + progress events so H8 funnel
263
- * lift isn't diluted. No flag.
264
- *
265
- * HOW TO FIND IT IN MIXPANEL:
266
- *
267
- * Report 1: Avg Workout Duration by Workout-Count Bucket
268
- * - Cohort A: users with 12-14 "workout completed"
269
- * - Cohort B: users with 0-11
270
- * - Event: "workout completed"
271
- * - Measure: Average of "duration_minutes"
272
- * - Expected: A ~ 1.35x B
273
- *
274
- * Report 2: D30+ Activity on Heavy Workout Cohort
275
- * - Cohort C: users with >= 15 "workout completed"
276
- * - Cohort A: users with 12-14
277
- * - Event: any event
278
- * - Measure: post-d30/pre-d30 ratio per user
279
- * - Expected: C ~ 70% lower post/pre ratio than A (overtraining churn)
280
- *
281
- * REAL-WORLD ANALOGUE: Sweet-spot training drives progression;
282
- * over-training causes injury and burnout.
283
- *
284
- * ═══════════════════════════════════════════════════════════════
285
- * EXPECTED METRICS SUMMARY
286
- * ═══════════════════════════════════════════════════════════════
287
- *
288
- * Hook | Metric | Baseline | Effect | Ratio
289
- * ----------------------------|---------------------|----------|-----------|------
290
- * Morning Workout Boost | calories_burned 5-9 | 1x | 1.3x | 1.3x
291
- * AI Coaching Lift | duration_minutes | 1x | 1.2x | 1.2x
292
- * Streak Retention | achievements/user | 1x | 2-3x | 2-3x
293
- * Social Challenge Completion | challenges/user | 1x | 1.5x | 1.5x
294
- * Resolver Churn Cliff | events after day 14 | 1x | 0.3x | -70%
295
- * Coach Session Quality | satisfaction_score | 3.0 | 4.3 | 1.4x
296
- * Coach Profile Enrichment | total_workouts | 0 | 350 | n/a
297
- * Annual Funnel Lift | funnel conversion | 45% | 63% | 1.4x
298
- * Workout Loop T2C | median min by tier | 1x | 0.77/1.25x| ~ 1.6x range
299
- * Workout Magic Number | sweet duration_min | 1x | 1.35x | 1.35x
300
- * Workout Magic Number | over D30+ post/pre | 1x | 0.29x | -71%
301
- */
302
-
303
- // ── SCALE ──
304
- const SEED = "dm4-fitness";
305
- const NUM_USERS = 10_000;
306
- const DATASET_START = "2026-01-01T00:00:00Z";
307
- const DATASET_END = "2026-05-01T23:59:59Z";
308
- const EVENTS_PER_DAY = 1.2;
309
- const token = process.env.MP_TOKEN || "your-mixpanel-token";
310
-
311
- const chance = u.initChance(SEED);
312
-
313
- // ── KNOBS (tweak these to reshape stories) ──
314
- const MORNING_HOUR_START = 5;
315
- const MORNING_HOUR_END = 9;
316
- const MORNING_CALORIE_MULT = 1.3;
317
-
318
- const AI_LAUNCH_DAY = 35;
319
- const AI_ADOPTION_LIKELIHOOD = 40;
320
- const AI_DURATION_MULT = 1.2;
321
-
322
- const GROUP_LAUNCH_DAY = 55;
323
- const GROUP_ADOPTION_LIKELIHOOD = 30;
324
-
325
- const STREAK_MIN_WORKOUTS = 2;
326
- const STREAK_LINEAR_CAP = 3; // workouts 2-4 (count - 1 capped to 3) → 1 achievement each
327
- const STREAK_SUPER_LINEAR_MULT = 4; // workouts 5+ → 4 achievements each
328
-
329
- const SOCIAL_FRIEND_THRESHOLD = 3;
330
- const SOCIAL_CHALLENGE_CLONE_FACTOR = 0.5;
331
-
332
- const RESOLVER_EVENT_THRESHOLD = 30;
333
- const RESOLVER_CLIFF_DAYS = 14;
334
- const RESOLVER_DROP_LIKELIHOOD = 70;
335
-
336
- const COACH_SESSION_SATISFACTION_MIN = 4.0;
337
- const COACH_SESSION_SATISFACTION_MAX = 5.0;
338
-
339
- const COACH_TOTAL_WORKOUTS_MIN = 200;
340
- const COACH_TOTAL_WORKOUTS_MAX = 500;
341
- const COACH_STREAK_DAYS_MIN = 60;
342
- const COACH_STREAK_DAYS_MAX = 365;
343
-
344
- const ANNUAL_FUNNEL_FREE_DROP_LIKELIHOOD = 30;
345
-
346
- const TTC_ANNUAL_FACTOR = 0.77;
347
- const TTC_FREE_FACTOR = 1.25;
348
-
349
- const WORKOUT_SWEET_MIN = 12;
350
- const WORKOUT_SWEET_MAX = 14;
351
- const WORKOUT_OVER_THRESHOLD = 15;
352
- const WORKOUT_DURATION_BOOST = 1.35;
353
- const WORKOUT_OVER_CUTOFF_DAYS = 30;
354
- const WORKOUT_OVER_DROP_LIKELIHOOD = 65;
355
-
356
- // ── HELPER FUNCTIONS ──
357
- function handleUserHooks(record) {
358
- // H7: COACH PROFILE ENRICHMENT — coach segment users get high
359
- // total_workouts + streak_days. Also assign subscription_tier by segment.
360
- if (record.segment === "coach") {
361
- record.total_workouts = chance.integer({ min: COACH_TOTAL_WORKOUTS_MIN, max: COACH_TOTAL_WORKOUTS_MAX });
362
- record.streak_days = chance.integer({ min: COACH_STREAK_DAYS_MIN, max: COACH_STREAK_DAYS_MAX });
363
- }
364
- // Subscription tier: athletes/coaches → annual/family; social → monthly; casual/resolver → mostly free
365
- if (record.segment === "athlete") {
366
- record.subscription_tier = chance.pickone(["annual", "annual", "family", "monthly"]);
367
- } else if (record.segment === "coach") {
368
- record.subscription_tier = chance.pickone(["annual", "family", "family"]);
369
- } else if (record.segment === "social") {
370
- record.subscription_tier = chance.pickone(["monthly", "monthly", "annual", "free"]);
371
- } else if (record.segment === "resolver") {
372
- record.subscription_tier = chance.pickone(["free", "free", "free", "monthly"]);
373
- } else {
374
- record.subscription_tier = chance.pickone(["free", "free", "monthly"]);
375
- }
376
- return record;
377
- }
378
-
379
- function handleFunnelPostHooks(record, meta) {
380
- // H9: WORKOUT LOOP TIME-TO-CONVERT — Annual/family complete 1.3x faster
381
- // (factor 0.77); Free 1.25x slower (factor 1.25).
382
- const tier = meta?.profile?.subscription_tier;
383
- if (Array.isArray(record) && record.length > 1) {
384
- const factor = (
385
- tier === "annual" || tier === "family" ? TTC_ANNUAL_FACTOR :
386
- tier === "free" ? TTC_FREE_FACTOR :
387
- 1.0
388
- );
389
- if (factor !== 1.0) {
390
- for (let i = 1; i < record.length; i++) {
391
- const prev = dayjs(record[i - 1].time);
392
- const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
393
- record[i].time = prev.add(newGap, "milliseconds").toISOString();
394
- }
395
- }
396
- }
397
- return record;
398
- }
399
-
400
- function handleEverythingHooks(record, meta) {
401
- const datasetStart = dayjs.unix(meta.datasetStart);
402
- let events = record;
403
- if (!events.length) return record;
404
-
405
- // ── SUPERPROP STAMPING ──────────────────────────
406
- // Stamp superProps from profile so they are consistent per user.
407
- if (meta && meta.profile) {
408
- const p = meta.profile;
409
- events.forEach(e => {
410
- if (p.Platform) e.Platform = p.Platform;
411
- if (p.workout_type) e.workout_type = p.workout_type;
412
- if (p.subscription_tier) e.subscription_tier = p.subscription_tier;
413
- });
414
- }
415
-
416
- // HOOK 1: MORNING WORKOUT BOOST — 5AM-9AM UTC workouts get
417
- // calories_burned 1.3x. No flag — analyst breaks down by HOD.
418
- events.forEach(e => {
419
- if (e.event === "workout completed") {
420
- const hour = new Date(e.time).getUTCHours();
421
- if (hour >= MORNING_HOUR_START && hour < MORNING_HOUR_END && e.calories_burned) {
422
- e.calories_burned = Math.floor(e.calories_burned * MORNING_CALORIE_MULT);
423
- }
424
- }
425
- });
426
-
427
- // ── HOOK 2: POST-LAUNCH AI COACHING LIFT ────────────
428
- // After day 35, ~40% of workouts switch to ai_assisted coaching,
429
- // then ai_assisted workouts get 1.2x duration.
430
- const AI_LAUNCH = datasetStart.add(AI_LAUNCH_DAY, "days");
431
- events.forEach(e => {
432
- if ((e.event === "workout completed" || e.event === "workout planned") &&
433
- dayjs(e.time).isAfter(AI_LAUNCH)) {
434
- // Adopt ai_assisted for ~40% of post-launch workouts
435
- if (chance.bool({ likelihood: AI_ADOPTION_LIKELIHOOD })) {
436
- e.coaching_mode = "ai_assisted";
437
- }
438
- // AI-assisted workouts get 1.2x duration
439
- if (e.coaching_mode === "ai_assisted") {
440
- if (e.duration_minutes) {
441
- e.duration_minutes = Math.floor(e.duration_minutes * AI_DURATION_MULT);
442
- }
443
- if (e.planned_duration_minutes) {
444
- e.planned_duration_minutes = Math.floor(e.planned_duration_minutes * AI_DURATION_MULT);
445
- }
446
- }
447
- }
448
- });
449
-
450
- // ── GROUP CHALLENGES ADOPTION ────────────────────
451
- // After day 55, ~30% of challenge events switch to group mode.
452
- const GROUP_LAUNCH = datasetStart.add(GROUP_LAUNCH_DAY, "days");
453
- events.forEach(e => {
454
- if ((e.event === "challenge joined" || e.event === "workout completed") &&
455
- dayjs(e.time).isAfter(GROUP_LAUNCH) &&
456
- chance.bool({ likelihood: GROUP_ADOPTION_LIKELIHOOD })) {
457
- e.challenge_mode = "group";
458
- }
459
- });
460
-
461
- // ── HOOK 8: ANNUAL SUBSCRIBER CONVERSION FILTER ─
462
- // Free/monthly-tier users drop ~30% of "progress checked"
463
- // (last step of Workout Loop funnel) to simulate lower conversion.
464
- if (meta && meta.profile) {
465
- const tier = meta.profile.subscription_tier;
466
- if (tier !== "annual" && tier !== "family" && chance.bool({ likelihood: ANNUAL_FUNNEL_FREE_DROP_LIKELIHOOD })) {
467
- record = record.filter(e => e.event !== "progress checked");
468
- events = record;
469
- }
470
- }
471
-
472
- // ── HOOK 3: STREAK RETENTION ─────────────────────
473
- // Users with >=2 workouts get streak_days updated and
474
- // cloned achievement events. Achievements scale super-
475
- // linearly: 1 per workout for workouts 2-4, then 4 per
476
- // workout beyond that.
477
- const workoutEvents = events.filter(e => e.event === "workout completed");
478
- if (workoutEvents.length >= STREAK_MIN_WORKOUTS) {
479
- // Update profile streak_days via a profile update event
480
- if (meta && meta.profile) {
481
- meta.profile.streak_days = workoutEvents.length;
482
- }
483
-
484
- // Super-linear achievement scaling:
485
- // workouts 2-4: 1 achievement each
486
- // workouts 5+: 4 achievements each
487
- const templateAchievement = events.find(e => e.event === "achievement unlocked");
488
- if (templateAchievement) {
489
- let achievementCount = Math.min(workoutEvents.length - 1, STREAK_LINEAR_CAP); // 1 each for workouts 2-4
490
- if (workoutEvents.length > 4) {
491
- achievementCount += (workoutEvents.length - 4) * STREAK_SUPER_LINEAR_MULT; // 4 each for workouts 5+
492
- }
493
- for (let a = 0; a < achievementCount; a++) {
494
- const srcIdx = Math.min(a, workoutEvents.length - 1);
495
- const sourceEvent = workoutEvents[srcIdx];
496
- events.push({
497
- ...templateAchievement,
498
- time: dayjs(sourceEvent.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
499
- user_id: sourceEvent.user_id,
500
- achievement_type: "streak_milestone",
501
- streak_days_at_unlock: a + 2,
502
- });
503
- }
504
- }
505
- }
506
-
507
- // ── HOOK 4: SOCIAL CHALLENGE COMPLETION ──────────
508
- // Users with >=3 friend_added events get 1.5x challenge completions.
509
- const friendCount = events.filter(e => e.event === "friend added").length;
510
- if (friendCount >= SOCIAL_FRIEND_THRESHOLD) {
511
- const templateChallenge = events.find(e => e.event === "challenge completed");
512
- if (templateChallenge) {
513
- const challengeCompletions = events.filter(e => e.event === "challenge completed");
514
- const extraCount = Math.max(1, Math.floor(challengeCompletions.length * SOCIAL_CHALLENGE_CLONE_FACTOR));
515
- for (let i = 0; i < extraCount; i++) {
516
- const source = challengeCompletions[i % challengeCompletions.length];
517
- events.push({
518
- ...templateChallenge,
519
- time: dayjs(source.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
520
- user_id: source.user_id,
521
- challenge_type: source.challenge_type,
522
- completion_pct: chance.integer({ min: 80, max: 100 }),
523
- });
524
- }
525
- }
526
- }
527
-
528
- // ── HOOK 5: RESOLVER CHURN CLIFF ─────────────────
529
- // Resolver segment users with <30 events lose 70% after day 14.
530
- if (meta && meta.profile && meta.profile.segment === "resolver" && events.length < RESOLVER_EVENT_THRESHOLD) {
531
- const CHURN_CLIFF = datasetStart.add(RESOLVER_CLIFF_DAYS, "days");
532
- for (let i = events.length - 1; i >= 0; i--) {
533
- const eventTime = dayjs(events[i].time);
534
- if (eventTime.isAfter(CHURN_CLIFF) && chance.bool({ likelihood: RESOLVER_DROP_LIKELIHOOD })) {
535
- events.splice(i, 1);
536
- }
537
- }
538
- }
539
-
540
- // HOOK 6: COACH SESSION QUALITY — coach-session satisfaction 4-5.
541
- const hasCoachSessions = events.some(e => e.event === "coach session");
542
- if (hasCoachSessions) {
543
- events.forEach(e => {
544
- if (e.event === "coach session") {
545
- e.satisfaction_score = chance.floating({ min: COACH_SESSION_SATISFACTION_MIN, max: COACH_SESSION_SATISFACTION_MAX, fixed: 1 });
546
- }
547
- });
548
- }
549
-
550
- // HOOK 10: WORKOUT-COUNT MAGIC NUMBER (no flags)
551
- // Sweet 12-14 workouts → +35% on workout duration_minutes (peak
552
- // progression). Over 15+ → drop 65% of post-day-30 non-workout
553
- // events (overtraining → churn). Workout events are preserved
554
- // so the bucket categorization stays consistent.
555
- const workoutCount = events.filter(e => e.event === "workout completed").length;
556
- if (workoutCount >= WORKOUT_SWEET_MIN && workoutCount <= WORKOUT_SWEET_MAX) {
557
- events.forEach(e => {
558
- if (e.event === "workout completed" && typeof e.duration_minutes === "number") {
559
- e.duration_minutes = Math.round(e.duration_minutes * WORKOUT_DURATION_BOOST);
560
- }
561
- });
562
- } else if (workoutCount >= WORKOUT_OVER_THRESHOLD) {
563
- const day30 = datasetStart.add(WORKOUT_OVER_CUTOFF_DAYS, "days");
564
- const preserveEvents = new Set(["workout completed", "progress checked"]);
565
- for (let i = events.length - 1; i >= 0; i--) {
566
- if (!preserveEvents.has(events[i].event) &&
567
- dayjs(events[i].time).isAfter(day30) && chance.bool({ likelihood: WORKOUT_OVER_DROP_LIKELIHOOD })) {
568
- events.splice(i, 1);
569
- }
570
- }
571
- }
572
-
573
- return record;
574
- }
575
-
576
- // ── CONFIG ──
577
- /** @type {Config} */
578
- const config = {
579
- version: 2,
580
- seed: SEED,
581
- datasetStart: DATASET_START,
582
- datasetEnd: DATASET_END,
583
- avgEventsPerUserPerDay: EVENTS_PER_DAY,
584
- numUsers: NUM_USERS,
585
- format: "json",
586
- gzip: true,
587
- credentials: {
588
- token,
589
- },
590
- switches: {
591
- hasSessionIds: true,
592
- alsoInferFunnels: false,
593
- hasLocation: true,
594
- hasAndroidDevices: true,
595
- hasIOSDevices: true,
596
- hasDesktopDevices: false,
597
- hasBrowser: false,
598
- hasCampaigns: false,
599
- isAnonymous: false,
600
- hasAdSpend: false,
601
- hasAvatar: true,
602
- },
603
- identity: {
604
- avgDevicePerUser: 3,
605
- },
606
- concurrency: 1,
607
- writeToDisk: false,
608
- scdProps: {
609
- fitness_level: {
610
- values: ["beginner", "intermediate", "advanced", "elite"],
611
- frequency: "month",
612
- timing: "fuzzy",
613
- max: 8
614
- }
615
- },
616
- mirrorProps: {},
617
- lookupTables: [],
618
-
619
- // ── Events (18) ──────────────────────────────────────────
620
- events: [
621
- {
622
- event: "account created",
623
- weight: 1,
624
- isFirstEvent: true,
625
- isAuthEvent: true,
626
- properties: {
627
- referral_source: ["organic", "friend_invite", "app_store", "social_media", "search"],
628
- },
629
- },
630
- {
631
- event: "workout completed",
632
- weight: 8,
633
- isStrictEvent: false,
634
- properties: {
635
- duration_minutes: u.weighNumRange(10, 90, 0.5, 40),
636
- calories_burned: u.weighNumRange(50, 800, 0.4, 300),
637
- heart_rate_avg: u.weighNumRange(80, 185, 0.5, 130),
638
- satisfaction_score: u.weighNumRange(1, 5, 0.7, 3),
639
- coaching_mode: ["self_guided"],
640
- challenge_mode: ["solo"],
641
- },
642
- },
643
- {
644
- event: "workout planned",
645
- weight: 6,
646
- isStrictEvent: false,
647
- properties: {
648
- planned_duration_minutes: u.weighNumRange(15, 90, 0.5, 45),
649
- day_of_week: ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"],
650
- coaching_mode: ["self_guided"],
651
- },
652
- },
653
- {
654
- event: "meal logged",
655
- weight: 7,
656
- properties: {
657
- meal_type: ["breakfast", "lunch", "dinner", "snack"],
658
- calories: u.weighNumRange(50, 1200, 0.5, 400),
659
- protein_g: u.weighNumRange(0, 60, 0.5, 20),
660
- meal_quality: ["healthy", "healthy", "balanced", "balanced", "indulgent"],
661
- },
662
- },
663
- {
664
- event: "challenge joined",
665
- weight: 3,
666
- isStrictEvent: false,
667
- properties: {
668
- challenge_type: ["steps", "calories", "streak", "strength", "team_relay"],
669
- duration_days: [7, 14, 21, 30],
670
- participants: u.weighNumRange(2, 50, 0.3, 10),
671
- challenge_mode: ["solo"],
672
- },
673
- },
674
- {
675
- event: "challenge completed",
676
- weight: 2,
677
- isStrictEvent: false,
678
- properties: {
679
- challenge_type: ["steps", "calories", "streak", "strength", "team_relay"],
680
- final_rank: u.weighNumRange(1, 50, 0.3, 10),
681
- completion_pct: u.weighNumRange(50, 100, 0.7, 85),
682
- challenge_mode: ["solo"],
683
- },
684
- },
685
- {
686
- event: "achievement unlocked",
687
- weight: 2,
688
- isStrictEvent: false,
689
- properties: {
690
- achievement_type: ["streak_milestone", "weight_goal", "distance_record", "calories_target", "social_champion", "first_workout"],
691
- streak_days_at_unlock: u.weighNumRange(1, 100, 0.3, 10),
692
- },
693
- },
694
- {
695
- event: "friend added",
696
- weight: 2,
697
- isStrictEvent: false,
698
- properties: {
699
- source: ["search", "contacts", "challenge", "suggestion", "qr_code"],
700
- },
701
- },
702
- {
703
- event: "leaderboard viewed",
704
- weight: 4,
705
- properties: {
706
- leaderboard_type: ["friends", "global", "challenge", "local"],
707
- user_rank: u.weighNumRange(1, 500, 0.3, 50),
708
- },
709
- },
710
- {
711
- event: "progress checked",
712
- weight: 5,
713
- isStrictEvent: false,
714
- properties: {
715
- metric_viewed: ["weight", "body_fat", "steps", "calories_burned", "workout_count", "streaks"],
716
- trend_direction: ["improving", "improving", "stable", "declining"],
717
- time_range: ["week", "month", "3_months", "year"],
718
- },
719
- },
720
- {
721
- event: "coach session",
722
- weight: 3,
723
- isStrictEvent: false,
724
- properties: {
725
- session_type: ["live_video", "chat", "plan_review", "form_check"],
726
- duration_minutes: u.weighNumRange(10, 60, 0.5, 30),
727
- satisfaction_score: u.weighNumRange(1, 5, 0.6, 3),
728
- coach_speciality: ["strength", "cardio", "nutrition", "yoga", "general"],
729
- },
730
- },
731
- {
732
- event: "nutrition plan viewed",
733
- weight: 4,
734
- properties: {
735
- plan_type: ["weight_loss", "muscle_gain", "maintenance", "custom"],
736
- adherence_pct: u.weighNumRange(0, 100, 0.5, 60),
737
- },
738
- },
739
- {
740
- event: "heart rate recorded",
741
- weight: 3,
742
- properties: {
743
- bpm: u.weighNumRange(50, 200, 0.5, 110),
744
- activity_state: ["resting", "warmup", "active", "peak", "cooldown"],
745
- device: ["watch", "chest_strap", "phone_sensor"],
746
- },
747
- },
748
- {
749
- event: "app session",
750
- weight: 8,
751
- properties: {
752
- session_duration_sec: u.weighNumRange(10, 1800, 0.4, 120),
753
- pages_viewed: u.weighNumRange(1, 15, 0.5, 3),
754
- },
755
- },
756
- {
757
- event: "notification received",
758
- weight: 5,
759
- properties: {
760
- notification_type: ["workout_reminder", "workout_reminder", "challenge_update", "friend_activity", "streak_warning", "coaching_tip"],
761
- channel: ["push", "push", "email", "sms"],
762
- opened: [true, true, true, false],
763
- },
764
- },
765
- {
766
- event: "subscription managed",
767
- weight: 2,
768
- properties: {
769
- action: ["viewed_plans", "started_trial", "upgraded", "downgraded", "cancelled", "renewed"],
770
- plan_viewed: ["free", "monthly", "annual", "family"],
771
- },
772
- },
773
- {
774
- event: "profile updated",
775
- weight: 2,
776
- isStrictEvent: false,
777
- properties: {
778
- field_updated: ["weight", "height", "goal", "avatar", "workout_preferences", "notification_settings"],
779
- },
780
- },
781
- {
782
- event: "account deactivated",
783
- weight: 1,
784
- isChurnEvent: true,
785
- returnLikelihood: 0.15,
786
- isStrictEvent: true,
787
- properties: {
788
- reason: ["lost_motivation", "cost", "switched_app", "injury", "achieved_goal"],
789
- },
790
- },
791
- ],
792
-
793
- // ── Funnels (5) ──────────────────────────────────────────
794
- funnels: [
795
- {
796
- name: "Onboarding",
797
- sequence: ["account created", "profile updated", "workout planned", "workout completed"],
798
- conversionRate: 45,
799
- order: "sequential",
800
- isFirstFunnel: true,
801
- timeToConvert: 72,
802
- weight: 3,
803
- },
804
- {
805
- name: "Workout Loop",
806
- sequence: ["workout planned", "workout completed", "progress checked"],
807
- conversionRate: 45,
808
- order: "sequential",
809
- timeToConvert: 48,
810
- weight: 5,
811
- reentry: true,
812
- },
813
- {
814
- name: "Social Engagement",
815
- sequence: ["friend added", "leaderboard viewed", "challenge joined"],
816
- conversionRate: 35,
817
- order: "sequential",
818
- timeToConvert: 96,
819
- weight: 3,
820
- },
821
- {
822
- name: "Challenge Completion",
823
- sequence: ["challenge joined", "workout completed", "challenge completed", "achievement unlocked"],
824
- conversionRate: 30,
825
- order: "sequential",
826
- timeToConvert: 336,
827
- weight: 2,
828
- },
829
- {
830
- name: "Coaching Path",
831
- sequence: ["coach session", "workout planned", "workout completed", "progress checked"],
832
- conversionRate: 50,
833
- order: "sequential",
834
- timeToConvert: 72,
835
- weight: 2,
836
- },
837
- ],
838
-
839
- // ── SuperProps ──────────────────────────────────────────
840
- superProps: {
841
- Platform: ["ios", "ios", "android"],
842
- workout_type: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
843
- subscription_tier: ["free"],
844
- },
845
-
846
- // ── UserProps ──────────────────────────────────────────
847
- userProps: {
848
- fitness_level: ["beginner"],
849
- segment: ["casual"],
850
- streak_days: [0],
851
- total_workouts: u.weighNumRange(0, 0, 0.5),
852
- preferred_workout: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
853
- goal: ["weight_loss", "muscle_gain", "endurance", "flexibility", "general_health"],
854
- Platform: ["ios", "ios", "android"],
855
- workout_type: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
856
- subscription_tier: ["free"],
857
- },
858
-
859
- // ── Personas ──────────────────────────────────
860
- personas: [
861
- {
862
- name: "athlete",
863
- weight: 10,
864
- eventMultiplier: 4.0,
865
- conversionModifier: 1.5,
866
- churnRate: 0.02,
867
- properties: {
868
- fitness_level: "advanced",
869
- segment: "athlete",
870
- },
871
- },
872
- {
873
- name: "casual_exerciser",
874
- weight: 40,
875
- eventMultiplier: 1.0,
876
- conversionModifier: 0.8,
877
- churnRate: 0.08,
878
- properties: {
879
- fitness_level: "intermediate",
880
- segment: "casual",
881
- },
882
- },
883
- {
884
- name: "new_year_resolver",
885
- weight: 25,
886
- eventMultiplier: 0.6,
887
- conversionModifier: 0.5,
888
- churnRate: 0.4,
889
- properties: {
890
- fitness_level: "beginner",
891
- segment: "resolver",
892
- },
893
- activeWindow: { maxDays: 30 },
894
- },
895
- {
896
- name: "social_motivator",
897
- weight: 15,
898
- eventMultiplier: 2.0,
899
- conversionModifier: 1.2,
900
- churnRate: 0.05,
901
- properties: {
902
- fitness_level: "intermediate",
903
- segment: "social",
904
- },
905
- },
906
- {
907
- name: "coach",
908
- weight: 10,
909
- eventMultiplier: 3.0,
910
- conversionModifier: 1.5,
911
- churnRate: 0.01,
912
- properties: {
913
- fitness_level: "expert",
914
- segment: "coach",
915
- },
916
- },
917
- ],
918
-
919
- // ── Engagement Decay ──────────────────────────
920
- engagementDecay: {
921
- model: "step",
922
- halfLife: 30,
923
- floor: 0.1,
924
- reactivationChance: 0.02,
925
- },
926
-
927
- hook(record, type, meta) {
928
- if (type === "user") return handleUserHooks(record);
929
- if (type === "funnel-post") return handleFunnelPostHooks(record, meta);
930
- if (type === "everything") return handleEverythingHooks(record, meta);
931
- return record;
932
- },
933
- };
934
-
935
- export default config;