@ak--47/dungeon-master 1.5.3 → 1.6.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.
- package/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +96 -0
- package/.claude/skills/create-project/provision.mjs +336 -0
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +211 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +3 -2
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- package/dungeons/vertical/travel.js +0 -930
|
@@ -0,0 +1,1524 @@
|
|
|
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 "@ak--47/dungeon-master/utils";
|
|
7
|
+
import * as v from "ak-tools";
|
|
8
|
+
/** @typedef {import("../../../types").Dungeon} Config */
|
|
9
|
+
|
|
10
|
+
// ── OVERVIEW ──
|
|
11
|
+
/*
|
|
12
|
+
* NAME: CodeForge
|
|
13
|
+
* APP: Developer platform for builds, deploys, monitoring, code review,
|
|
14
|
+
* and team collaboration. Think GitHub + Vercel + PagerDuty in a
|
|
15
|
+
* unified CI/CD experience. Multi-role devs ship code through a
|
|
16
|
+
* connect-repo → configure-pipeline → build → deploy → monitor loop.
|
|
17
|
+
* SCALE: 10,000 users, ~2.2M events, 121 days (2026-01-01 → 2026-05-01)
|
|
18
|
+
* CORE LOOP: connect repo → configure pipeline → build → deploy → monitor
|
|
19
|
+
*
|
|
20
|
+
* EVENTS (18):
|
|
21
|
+
* build completed (8) > app session (8) > pull request created (6)
|
|
22
|
+
* > notification received (6) > deployment completed (5)
|
|
23
|
+
* > code review completed (5) > monitoring dashboard viewed (5)
|
|
24
|
+
* > alert triggered (4) > log searched (4) > incident created (2)
|
|
25
|
+
* > incident resolved (2) > repository connected (2) > pipeline configured (2)
|
|
26
|
+
* > collaboration invited (2) > environment created (2) > account created (1)
|
|
27
|
+
* > billing updated (1) > account deactivated (1)
|
|
28
|
+
*
|
|
29
|
+
* FUNNELS (5):
|
|
30
|
+
* - Onboarding: account created → repository connected → pipeline configured → build completed (45%)
|
|
31
|
+
* - Build-Deploy Pipeline: build completed → deployment completed → monitoring dashboard viewed (40%)
|
|
32
|
+
* - PR Review Flow: pull request created → code review completed → build completed → deployment completed (35%)
|
|
33
|
+
* - Incident Response: alert triggered → incident created → incident resolved (50%)
|
|
34
|
+
* - Upgrade Path: app session → billing updated → collaboration invited (20%)
|
|
35
|
+
*
|
|
36
|
+
* USER PROPS: dev_role, segment, team_size, repos_connected, org_name, experience_level, subscription_tier, Platform, language
|
|
37
|
+
* SUPER PROPS: subscription_tier, Platform, language
|
|
38
|
+
* SCD PROPS: subscription_tier (free/pro/enterprise, monthly fuzzy, max 6)
|
|
39
|
+
* GROUPS: none
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// ── HOOK STORIES ──
|
|
43
|
+
/*
|
|
44
|
+
* NOTE: All cohort effects are HIDDEN — discoverable only via behavioral
|
|
45
|
+
* cohorts or raw-prop breakdowns. No cohort flag is stamped on events.
|
|
46
|
+
*
|
|
47
|
+
* ---------------------------------------------------------------
|
|
48
|
+
* 1. BUILD FAILURE CASCADE (event hook)
|
|
49
|
+
* ---------------------------------------------------------------
|
|
50
|
+
*
|
|
51
|
+
* PATTERN: Builds with status "failed" get 2x build_duration_sec.
|
|
52
|
+
* Failed builds take longer because the full test suite runs before
|
|
53
|
+
* failing, and retries compound the duration.
|
|
54
|
+
*
|
|
55
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
56
|
+
*
|
|
57
|
+
* Report 1: Build Duration by Status
|
|
58
|
+
* - Report type: Insights
|
|
59
|
+
* - Event: "build completed"
|
|
60
|
+
* - Measure: Average of "build_duration_sec"
|
|
61
|
+
* - Breakdown: "build_status"
|
|
62
|
+
* - Expected: failed ~ 2x longer than success (median ~400s vs
|
|
63
|
+
* ~200s; cancelled tracks success — the hook only touches failed).
|
|
64
|
+
* Use median, not average: the extreme-value anomaly (10x
|
|
65
|
+
* build_duration_sec at 0.3% frequency) fattens the mean tail.
|
|
66
|
+
*
|
|
67
|
+
* REAL-WORLD ANALOGUE: Failed CI builds run full test suites,
|
|
68
|
+
* timeout, and trigger retry cascades that inflate build times.
|
|
69
|
+
*
|
|
70
|
+
* ---------------------------------------------------------------
|
|
71
|
+
* 2. NIGHT DEPLOY RISK (everything hook)
|
|
72
|
+
* ---------------------------------------------------------------
|
|
73
|
+
*
|
|
74
|
+
* PATTERN: Deployments between 10PM-6AM UTC get deploy_status forced
|
|
75
|
+
* to "failed" 40% of the time. No flag — analyst breaks down by
|
|
76
|
+
* hour-of-day on deployment-completed events to discover the risk window.
|
|
77
|
+
*
|
|
78
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
79
|
+
*
|
|
80
|
+
* Report 1: Deploy Failure Rate by Hour of Day
|
|
81
|
+
* - Report type: Insights
|
|
82
|
+
* - Event: "deployment completed"
|
|
83
|
+
* - Measure: Total
|
|
84
|
+
* - Filter: deploy_status = "failed"
|
|
85
|
+
* - Breakdown: hour of day
|
|
86
|
+
* - Expected: 22:00-05:59 UTC hours show ~52% failure vs ~21% day
|
|
87
|
+
* baseline. Mechanism: organic failed share is 1/5 = 20% (pool
|
|
88
|
+
* ["success","success","success","failed","rolled_back"]); the
|
|
89
|
+
* hook forces "failed" on 40% of night deploys, so night share =
|
|
90
|
+
* 0.4 + 0.6*0.2 = 0.52. Exclude days 43-49 when measuring: H6
|
|
91
|
+
* recovery clones (success/rolled_back only, never failed) dilute
|
|
92
|
+
* the failure share inside the recovery window.
|
|
93
|
+
*
|
|
94
|
+
* REAL-WORLD ANALOGUE: Night deploys fail more due to skeleton crews
|
|
95
|
+
* and delayed incident response.
|
|
96
|
+
*
|
|
97
|
+
* ---------------------------------------------------------------
|
|
98
|
+
* 3. COPILOT ADOPTION -> PR VELOCITY (everything hook)
|
|
99
|
+
* ---------------------------------------------------------------
|
|
100
|
+
*
|
|
101
|
+
* PATTERN: A hash-based cohort (~37.5% of users — GUID first char in
|
|
102
|
+
* {2,3,4,d,e,f}, i.e. charCodeAt(0) % 10 < 3) are copilot adopters:
|
|
103
|
+
* they get ai_assist="copilot" stamped on PR/review events and
|
|
104
|
+
* floor(PRs * 0.5) extra pull-request events cloned into their
|
|
105
|
+
* stream (~1.5x PR volume).
|
|
106
|
+
*
|
|
107
|
+
* MEASUREMENT CAVEAT: the copilot_integration feature (launchDay 30,
|
|
108
|
+
* fast adoption) ALSO flips ai_assist to "copilot" on PR/review
|
|
109
|
+
* events for feature adopters regardless of cohort — so a raw
|
|
110
|
+
* ai_assist breakdown mixes the two populations. The clean read is
|
|
111
|
+
* behavioral: bin users by per-user PR volume, or reproduce the hash
|
|
112
|
+
* cohort (first char of user_id) in a cohort definition.
|
|
113
|
+
*
|
|
114
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
115
|
+
*
|
|
116
|
+
* Report 1: PR Volume by AI Assist Mode
|
|
117
|
+
* - Report type: Insights
|
|
118
|
+
* - Event: "pull request created"
|
|
119
|
+
* - Measure: Total per user (average)
|
|
120
|
+
* - Breakdown: "ai_assist"
|
|
121
|
+
* - Expected: copilot-heavy users ~1.5x more PRs than manual users
|
|
122
|
+
* (directional only — see measurement caveat above)
|
|
123
|
+
*
|
|
124
|
+
* REAL-WORLD ANALOGUE: AI coding assistants measurably increase
|
|
125
|
+
* developer throughput, particularly for boilerplate and tests.
|
|
126
|
+
*
|
|
127
|
+
* ---------------------------------------------------------------
|
|
128
|
+
* 4. ON-CALL ROTATION FATIGUE (everything hook)
|
|
129
|
+
* ---------------------------------------------------------------
|
|
130
|
+
*
|
|
131
|
+
* PATTERN: Users with >20 alert_triggered events get increasing
|
|
132
|
+
* response_time_minutes on incident events. Alert fatigue causes
|
|
133
|
+
* slower response as on-call rotations grind engineers down.
|
|
134
|
+
*
|
|
135
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
136
|
+
*
|
|
137
|
+
* Report 1: Response Time vs Alert Volume
|
|
138
|
+
* - Report type: Insights
|
|
139
|
+
* - Event: "incident resolved"
|
|
140
|
+
* - Measure: Average of "response_time_minutes"
|
|
141
|
+
* - Filter: users with high alert counts
|
|
142
|
+
* - Expected: fatigued users (>20 alerts, ~31% of incident users)
|
|
143
|
+
* show ~2.6x mean response time (~138min vs ~53min). The
|
|
144
|
+
* multiplier is 1 + min(alerts/20, 3), so the fatigued-cohort
|
|
145
|
+
* mean multiplier lands ~2.67; measured ratio tracks it.
|
|
146
|
+
*
|
|
147
|
+
* REAL-WORLD ANALOGUE: On-call burnout is a top cause of attrition
|
|
148
|
+
* in SRE/DevOps. Alert fatigue degrades response quality over time.
|
|
149
|
+
*
|
|
150
|
+
* ---------------------------------------------------------------
|
|
151
|
+
* 5. OPEN SOURCE POWER USAGE (everything hook)
|
|
152
|
+
* ---------------------------------------------------------------
|
|
153
|
+
*
|
|
154
|
+
* PATTERN: OSS-segment users with >15 events get extra cloned build
|
|
155
|
+
* + deploy events in their later activity (representing power usage).
|
|
156
|
+
* Cloned events use unique offset timestamps. No flag — discover via
|
|
157
|
+
* cohort by segment + event count, observing per-user build/deploy volume.
|
|
158
|
+
*
|
|
159
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
160
|
+
*
|
|
161
|
+
* Report 1: Build Share — Active OSS Users vs Active Non-OSS
|
|
162
|
+
* - Report type: Insights (with cohort)
|
|
163
|
+
* - Cohort A: segment = "oss_user" AND events > 15
|
|
164
|
+
* - Cohort B: segment != "oss_user" AND events > 15
|
|
165
|
+
* - Event: "build completed"
|
|
166
|
+
* - Measure: share of each cohort's total events
|
|
167
|
+
* - Expected: A's build share ~1.3x B's (~0.25 vs ~0.19). Note:
|
|
168
|
+
* nearly all surviving oss users clear the >15 threshold (121
|
|
169
|
+
* days x 1.2/day x 0.5 multiplier ≈ 72 events), so the
|
|
170
|
+
* within-oss A-vs-B comparison mostly captures churn, not the
|
|
171
|
+
* hook — compare against active non-oss users instead. The
|
|
172
|
+
* deploy share also rises (~1.65x) but is coupled with H9
|
|
173
|
+
* (oss builds land in the 15-30 sweet spot; heavy non-oss
|
|
174
|
+
* builders lose deploys), so the build share is the clean read.
|
|
175
|
+
*
|
|
176
|
+
* REAL-WORLD ANALOGUE: Power OSS users hit free-tier limits via
|
|
177
|
+
* heavy build/deploy volume.
|
|
178
|
+
*
|
|
179
|
+
* ---------------------------------------------------------------
|
|
180
|
+
* 6. POST-OUTAGE RECOVERY (everything hook)
|
|
181
|
+
* ---------------------------------------------------------------
|
|
182
|
+
*
|
|
183
|
+
* PATTERN: After the major outage ends (day 42.25), deployment
|
|
184
|
+
* events get a frequency boost -- extra cloned deployment events
|
|
185
|
+
* represent the flurry of hotfixes and rollback-then-redeploy cycles.
|
|
186
|
+
*
|
|
187
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
188
|
+
*
|
|
189
|
+
* Report 1: Deployment Spike Post-Outage
|
|
190
|
+
* - Report type: Insights
|
|
191
|
+
* - Event: "deployment completed"
|
|
192
|
+
* - Measure: Total
|
|
193
|
+
* - Line chart by day
|
|
194
|
+
* - Expected: ~3.7x deploy volume on days 44-47 vs surrounding
|
|
195
|
+
* baseline, normalized against builds (ratio-of-ratios cancels
|
|
196
|
+
* the growth ramp). Mechanism: 3 clones per window deploy = 4x,
|
|
197
|
+
* minus ~5% of clones spilling past the window edge (offsets are
|
|
198
|
+
* +1-8h, so late-day-47 sources push clones into day 48). Clones
|
|
199
|
+
* carry status success/rolled_back only — never "failed" — which
|
|
200
|
+
* is why H2's failure-share read excludes this window.
|
|
201
|
+
*
|
|
202
|
+
* REAL-WORLD ANALOGUE: After a major outage, teams push a burst
|
|
203
|
+
* of hotfixes, rollbacks, and emergency deploys to stabilize.
|
|
204
|
+
*
|
|
205
|
+
* ---------------------------------------------------------------
|
|
206
|
+
* 7. DEVOPS LEAD PROFILE ENRICHMENT (user hook)
|
|
207
|
+
* ---------------------------------------------------------------
|
|
208
|
+
*
|
|
209
|
+
* PATTERN: Users in the "devops" segment get team_size boosted
|
|
210
|
+
* to 10-50 and repos_connected boosted to 5-20. DevOps leads
|
|
211
|
+
* manage larger teams and more infrastructure.
|
|
212
|
+
*
|
|
213
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
214
|
+
*
|
|
215
|
+
* Report 1: Team Size by Segment
|
|
216
|
+
* - Report type: Insights
|
|
217
|
+
* - Event: any event
|
|
218
|
+
* - Measure: Average of user property "team_size"
|
|
219
|
+
* - Breakdown: user property "segment"
|
|
220
|
+
* - Expected: devops ~30 avg (uniform 10-50), platform_eng ~15,
|
|
221
|
+
* junior ~4.5. CAVEAT: full_stack/oss keep the DEFAULT pool
|
|
222
|
+
* u.weighNumRange(1, 50, 0.4, 5), whose mean is ~24 — NOT ~10 —
|
|
223
|
+
* so the clean team_size contrast is devops vs junior.
|
|
224
|
+
* repos_connected is the crisper signal: defaults to [0], so
|
|
225
|
+
* devops ~12.5, platform_eng ~9, junior ~1.5, full_stack/oss
|
|
226
|
+
* exactly 0.
|
|
227
|
+
*
|
|
228
|
+
* REAL-WORLD ANALOGUE: DevOps leads oversee platform teams and
|
|
229
|
+
* manage organization-wide CI/CD infrastructure.
|
|
230
|
+
*
|
|
231
|
+
* ---------------------------------------------------------------
|
|
232
|
+
* 8. ENTERPRISE BUILD-DEPLOY FUNNEL LIFT (everything hook)
|
|
233
|
+
* ---------------------------------------------------------------
|
|
234
|
+
*
|
|
235
|
+
* PATTERN: Non-paid users (free AND team tier — everyone except
|
|
236
|
+
* enterprise/business) drop 35% of final funnel step events
|
|
237
|
+
* ("monitoring dashboard viewed"), creating a visible conversion
|
|
238
|
+
* gap. Enterprise/business users keep all their events.
|
|
239
|
+
*
|
|
240
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
241
|
+
*
|
|
242
|
+
* Report 1: Build-Deploy Conversion by Tier
|
|
243
|
+
* - Report type: Funnels
|
|
244
|
+
* - Steps: "build completed" -> "deployment completed" -> "monitoring dashboard viewed"
|
|
245
|
+
* - Breakdown: "subscription_tier" (superProp)
|
|
246
|
+
* - Expected: free/team step-3 conversion ~0.65x of
|
|
247
|
+
* enterprise/business. Normalized read: monitoring-views per
|
|
248
|
+
* deployment — free/team ~0.63x paid (the 0.65 keep-rate minus
|
|
249
|
+
* small drift; deploy-count hooks H6/H9 hit all tiers evenly
|
|
250
|
+
* and cancel in the ratio).
|
|
251
|
+
*
|
|
252
|
+
* REAL-WORLD ANALOGUE: Enterprise CI/CD customers get priority
|
|
253
|
+
* runners, dedicated support, and SLA-backed uptime guarantees.
|
|
254
|
+
*
|
|
255
|
+
* ---------------------------------------------------------------
|
|
256
|
+
* 9. BUILD-COUNT MAGIC NUMBER (everything hook)
|
|
257
|
+
* ---------------------------------------------------------------
|
|
258
|
+
*
|
|
259
|
+
* PATTERN: Users with 15-30 "build completed" events sit in the
|
|
260
|
+
* healthy CI sweet spot — +50% deploy events are cloned (unique
|
|
261
|
+
* timestamps, offsets 5-360min). Users with 31+ builds suffer
|
|
262
|
+
* flaky-CI burnout: 40% of their deploy events drop. No flag —
|
|
263
|
+
* discover by binning users on build-count and comparing
|
|
264
|
+
* deploys-per-build.
|
|
265
|
+
*
|
|
266
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
267
|
+
*
|
|
268
|
+
* Report 1: Deploys per Build by Build Bucket
|
|
269
|
+
* - Report type: Insights (with cohort)
|
|
270
|
+
* - Cohort A: users with 15-30 "build completed" (sweet)
|
|
271
|
+
* - Cohort B: users with 1-14 (base)
|
|
272
|
+
* - Cohort C: users with >= 31 (over)
|
|
273
|
+
* - Event: "deployment completed" / "build completed"
|
|
274
|
+
* - Measure: ratio of totals per cohort
|
|
275
|
+
* - Expected: C/A ~ 0.40 (the clean read: 0.6/1.5 — organic
|
|
276
|
+
* deploys-per-build cancels between two high-activity buckets).
|
|
277
|
+
* A/B ~ 1.35 (1.5x minus a base-bucket organic offset: low-build
|
|
278
|
+
* users run slightly deploy-richer mixes, ~0.70 vs ~0.63
|
|
279
|
+
* organic). Segment to full_stack to hold persona constant and
|
|
280
|
+
* exclude recovery-window (days 43-49) deploys to decouple H6.
|
|
281
|
+
*
|
|
282
|
+
* REAL-WORLD ANALOGUE: Healthy CI cadence drives reliable deploys;
|
|
283
|
+
* runaway builds signal a flaky pipeline that scares teams off ships.
|
|
284
|
+
*
|
|
285
|
+
* ---------------------------------------------------------------
|
|
286
|
+
* 10. BUILD-DEPLOY TIME-TO-CONVERT (funnel-post)
|
|
287
|
+
* ---------------------------------------------------------------
|
|
288
|
+
*
|
|
289
|
+
* PATTERN: Enterprise and business tier users complete the Build-Deploy
|
|
290
|
+
* Pipeline funnel (build completed -> deployment completed -> monitoring
|
|
291
|
+
* dashboard viewed) 1.5x faster (factor 0.67). Free-tier users complete
|
|
292
|
+
* it 1.33x slower (factor 1.33). The hook intercepts funnel-post arrays,
|
|
293
|
+
* computes the time gap between consecutive steps, and scales each gap
|
|
294
|
+
* by the tier-specific factor before rewriting the step timestamps.
|
|
295
|
+
*
|
|
296
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
297
|
+
*
|
|
298
|
+
* Report 1: Build-Deploy Pipeline Median TTC by Tier
|
|
299
|
+
* - Report type: Funnels
|
|
300
|
+
* - Steps: "build completed" -> "deployment completed" -> "monitoring dashboard viewed"
|
|
301
|
+
* - Measure: Median time to convert
|
|
302
|
+
* - Breakdown: "subscription_tier" (superProp)
|
|
303
|
+
* - Expected: enterprise/business ~ 0.67x baseline; free ~ 1.33x
|
|
304
|
+
* baseline; team = 1.0 control. Fully engineered enterprise/free
|
|
305
|
+
* ratio = 0.67/1.33 ≈ 0.50, but the OBSERVED funnel-report ratio
|
|
306
|
+
* lands ~0.60-0.70: greedy min-gap step picks plus clone
|
|
307
|
+
* pollution (H9 sweet-spot deploy clones at +5-360min, H6
|
|
308
|
+
* recovery clones at +1-8h) compress observed gaps for every
|
|
309
|
+
* tier, and free-tier stretched conversions censor past the
|
|
310
|
+
* window — both pull the ratio toward 1.
|
|
311
|
+
*
|
|
312
|
+
* NOTE (funnel-post measurement): visible only via funnel-instance
|
|
313
|
+
* reads (Mixpanel funnels, or emulateBreakdown timeToConvert).
|
|
314
|
+
* Cross-event MIN→MIN SQL queries on raw events do NOT show this —
|
|
315
|
+
* funnel-post adjusts gaps within funnel instances, not across the
|
|
316
|
+
* user's full event history.
|
|
317
|
+
*
|
|
318
|
+
* REAL-WORLD ANALOGUE: Enterprise CI/CD customers get priority build
|
|
319
|
+
* runners and dedicated deploy infrastructure, yielding faster
|
|
320
|
+
* end-to-end pipeline throughput.
|
|
321
|
+
*
|
|
322
|
+
* ===============================================================
|
|
323
|
+
* EXPECTED METRICS SUMMARY
|
|
324
|
+
* ===============================================================
|
|
325
|
+
*
|
|
326
|
+
* Bands were mechanism-derived and confirmed at reduced scale (2K
|
|
327
|
+
* users, same seed) BEFORE the full run; "Measured" is the
|
|
328
|
+
* full-fidelity 10K read (2,198,972 events). All 10 stories NAILED.
|
|
329
|
+
*
|
|
330
|
+
* Hook | Metric | Baseline | Effect | Measured @10K
|
|
331
|
+
* ----------------------------|-------------------------------|----------|------------|--------------
|
|
332
|
+
* Build Failure Cascade | median build_duration_sec | ~200s | 2.0x | 2.000
|
|
333
|
+
* Night Deploy Risk | deploy failure share | ~21% day | 0.52 night | 0.503 / 0.208
|
|
334
|
+
* Copilot PR Velocity | PRs/user (hash cohort) | ~23 | ~1.5x | 1.437
|
|
335
|
+
* On-Call Fatigue | mean response_time_min | ~53min | ~2.6x | 2.599
|
|
336
|
+
* OSS Power Usage | build share of events (active)| ~0.19 | ~1.28x | 1.280
|
|
337
|
+
* Post-Outage Recovery | deploys d44-47, RoR vs builds | 1x | ~3.7x | 3.774
|
|
338
|
+
* DevOps Lead Profiles | repos_connected | 0 (dflt) | ~12.5 | 12.4
|
|
339
|
+
* Enterprise Funnel Lift | monitoring views per deploy | paid 1x | keep 0.65 | 0.632
|
|
340
|
+
* Build-Count Magic Number | deploys/build sweet vs base | 1x | ~1.35x | 1.452
|
|
341
|
+
* Build-Count Magic Number | deploys/build over vs sweet | 1x | 0.40x | 0.406
|
|
342
|
+
* Build-Deploy TTC | median TTC vs free (emulator) | 1x | ~0.6-0.7x | ent 0.642 / biz 0.607 / team 0.795
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
// ── SCALE ──
|
|
346
|
+
const SEED = "dm4-devtools";
|
|
347
|
+
const NUM_USERS = 10_000;
|
|
348
|
+
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
349
|
+
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
350
|
+
const EVENTS_PER_DAY = 1.2;
|
|
351
|
+
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
352
|
+
|
|
353
|
+
const chance = u.initChance(SEED);
|
|
354
|
+
|
|
355
|
+
// ── KNOBS (tweak these to reshape stories) ──
|
|
356
|
+
const BUILD_FAILURE_DURATION_MULT = 2;
|
|
357
|
+
|
|
358
|
+
const NIGHT_DEPLOY_HOUR_START = 22;
|
|
359
|
+
const NIGHT_DEPLOY_HOUR_END = 6;
|
|
360
|
+
const NIGHT_DEPLOY_FAIL_LIKELIHOOD = 40;
|
|
361
|
+
|
|
362
|
+
const COPILOT_USER_HASH_MOD = 10;
|
|
363
|
+
const COPILOT_USER_HASH_THRESHOLD = 3;
|
|
364
|
+
const COPILOT_PR_CLONE_RATE = 0.5;
|
|
365
|
+
|
|
366
|
+
const ONCALL_ALERT_THRESHOLD = 20;
|
|
367
|
+
const ONCALL_FATIGUE_DIVISOR = 20;
|
|
368
|
+
const ONCALL_FATIGUE_CAP = 3;
|
|
369
|
+
|
|
370
|
+
const OSS_EVENT_THRESHOLD = 15;
|
|
371
|
+
const OSS_CONVERSION_POINT_PCT = 0.7;
|
|
372
|
+
const OSS_BUILD_CLONE_LIKELIHOOD = 30;
|
|
373
|
+
const OSS_DEPLOY_CLONE_LIKELIHOOD = 20;
|
|
374
|
+
|
|
375
|
+
const RECOVERY_START_DAY = 44;
|
|
376
|
+
const RECOVERY_END_DAY = 48;
|
|
377
|
+
const RECOVERY_CLONES_PER_EVENT = 3;
|
|
378
|
+
|
|
379
|
+
const ENTERPRISE_DROP_LIKELIHOOD = 35;
|
|
380
|
+
|
|
381
|
+
const BUILD_SWEET_MIN = 15;
|
|
382
|
+
const BUILD_SWEET_MAX = 30;
|
|
383
|
+
const BUILD_OVER_THRESHOLD = 31;
|
|
384
|
+
const BUILD_SWEET_CLONE_RATE = 0.5;
|
|
385
|
+
const BUILD_OVER_DROP_LIKELIHOOD = 40;
|
|
386
|
+
|
|
387
|
+
const TTC_FAST_FACTOR = 0.67;
|
|
388
|
+
const TTC_SLOW_FACTOR = 1.33;
|
|
389
|
+
|
|
390
|
+
// ── DATA ARRAYS ──
|
|
391
|
+
// Generate consistent pipeline/repo IDs at module level
|
|
392
|
+
const pipelineIds = v.range(1, 80).map(() => `PIPE_${v.uid(6)}`);
|
|
393
|
+
const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
|
|
394
|
+
|
|
395
|
+
// ── HELPER FUNCTIONS ──
|
|
396
|
+
function handleFunnelPostHooks(record, meta) {
|
|
397
|
+
// H10: BUILD-DEPLOY TIME-TO-CONVERT
|
|
398
|
+
// Enterprise tier completes Build-Deploy Pipeline funnel 1.5x faster
|
|
399
|
+
// (factor 0.67); free tier 1.33x slower (factor 1.33).
|
|
400
|
+
const segment = meta?.profile?.subscription_tier;
|
|
401
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
402
|
+
const factor = (
|
|
403
|
+
segment === "enterprise" || segment === "business" ? TTC_FAST_FACTOR :
|
|
404
|
+
segment === "free" ? TTC_SLOW_FACTOR :
|
|
405
|
+
1.0
|
|
406
|
+
);
|
|
407
|
+
if (factor !== 1.0) {
|
|
408
|
+
for (let i = 1; i < record.length; i++) {
|
|
409
|
+
const prev = dayjs(record[i - 1].time);
|
|
410
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
411
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return record;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function handleUserHooks(record) {
|
|
419
|
+
// H7: DEVOPS LEAD PROFILE ENRICHMENT
|
|
420
|
+
// DevOps leads get team_size 10-50 and repos_connected 5-20.
|
|
421
|
+
// Platform engineers get moderate boosts. Others stay at defaults.
|
|
422
|
+
if (record.segment === "devops") {
|
|
423
|
+
record.team_size = chance.integer({ min: 10, max: 50 });
|
|
424
|
+
record.repos_connected = chance.integer({ min: 5, max: 20 });
|
|
425
|
+
record.experience_level = "senior";
|
|
426
|
+
} else if (record.segment === "platform_eng") {
|
|
427
|
+
record.team_size = chance.integer({ min: 5, max: 25 });
|
|
428
|
+
record.repos_connected = chance.integer({ min: 3, max: 15 });
|
|
429
|
+
record.experience_level = chance.pickone(["mid", "senior"]);
|
|
430
|
+
} else if (record.segment === "junior") {
|
|
431
|
+
record.team_size = chance.integer({ min: 1, max: 8 });
|
|
432
|
+
record.repos_connected = chance.integer({ min: 0, max: 3 });
|
|
433
|
+
record.experience_level = "junior";
|
|
434
|
+
}
|
|
435
|
+
return record;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function handleEventHooks(record) {
|
|
439
|
+
// H1: BUILD FAILURE CASCADE
|
|
440
|
+
// Failed builds get 2x duration (retries take longer).
|
|
441
|
+
if (record.event === "build completed" && record.build_status === "failed") {
|
|
442
|
+
record.build_duration_sec = Math.floor((record.build_duration_sec || 240) * BUILD_FAILURE_DURATION_MULT);
|
|
443
|
+
}
|
|
444
|
+
// (HOOK 2: NIGHT DEPLOY RISK moved to everything hook — hour checks
|
|
445
|
+
// must run after bunchIntoSessions redistributes timestamps)
|
|
446
|
+
return record;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function handleEverythingHooks(record, meta) {
|
|
450
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
451
|
+
let events = record;
|
|
452
|
+
if (!events.length) return record;
|
|
453
|
+
const profile = meta && meta.profile ? meta.profile : {};
|
|
454
|
+
|
|
455
|
+
// SUPERPROP STAMPING
|
|
456
|
+
// Stamp superProp values from profile onto every event so they stay
|
|
457
|
+
// consistent per-user instead of randomizing per-event.
|
|
458
|
+
events.forEach(e => {
|
|
459
|
+
if (profile.subscription_tier) e.subscription_tier = profile.subscription_tier;
|
|
460
|
+
if (profile.Platform) e.Platform = profile.Platform;
|
|
461
|
+
if (profile.language) e.language = profile.language;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// H2: NIGHT DEPLOY RISK
|
|
465
|
+
// Deployments between 10PM-6AM get deploy_status forced to "failed"
|
|
466
|
+
// 40% of the time. No flag — analyst breaks down by hour-of-day.
|
|
467
|
+
events.forEach(e => {
|
|
468
|
+
if (e.event === "deployment completed") {
|
|
469
|
+
const hour = new Date(e.time).getUTCHours();
|
|
470
|
+
if ((hour >= NIGHT_DEPLOY_HOUR_START || hour < NIGHT_DEPLOY_HOUR_END) && chance.bool({ likelihood: NIGHT_DEPLOY_FAIL_LIKELIHOOD })) {
|
|
471
|
+
e.deploy_status = "failed";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// H8: ENTERPRISE BUILD-DEPLOY FUNNEL LIFT
|
|
477
|
+
// Free-tier users drop 35% of final funnel step events to create
|
|
478
|
+
// visible conversion gap vs paid subscribers.
|
|
479
|
+
if (profile.subscription_tier !== "enterprise" && profile.subscription_tier !== "business") {
|
|
480
|
+
events = events.filter(e => {
|
|
481
|
+
if (e.event === "monitoring dashboard viewed" && chance.bool({ likelihood: ENTERPRISE_DROP_LIKELIHOOD })) return false;
|
|
482
|
+
return true;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// H3: COPILOT PR VELOCITY
|
|
487
|
+
// ~30% of users are copilot adopters (hash-based cohort).
|
|
488
|
+
// Copilot users get ai_assist="copilot" stamped and 1.5x more PRs.
|
|
489
|
+
const uid = events[0]?.user_id || "";
|
|
490
|
+
const isCopilotUser = (typeof uid === "string" ? uid.charCodeAt(0) : uid) % COPILOT_USER_HASH_MOD < COPILOT_USER_HASH_THRESHOLD;
|
|
491
|
+
if (isCopilotUser) {
|
|
492
|
+
events.forEach(e => {
|
|
493
|
+
if (e.event === "pull request created" || e.event === "code review completed") {
|
|
494
|
+
e.ai_assist = "copilot";
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
const prEvents = events.filter(e => e.event === "pull request created");
|
|
498
|
+
const extraCount = Math.floor(prEvents.length * COPILOT_PR_CLONE_RATE);
|
|
499
|
+
for (let i = 0; i < extraCount; i++) {
|
|
500
|
+
const templateEvent = prEvents[i % prEvents.length];
|
|
501
|
+
if (templateEvent) {
|
|
502
|
+
events.push({
|
|
503
|
+
...templateEvent,
|
|
504
|
+
time: dayjs(templateEvent.time).add(chance.integer({ min: 1, max: 12 }), "hours").toISOString(),
|
|
505
|
+
user_id: templateEvent.user_id,
|
|
506
|
+
ai_assist: "copilot",
|
|
507
|
+
files_changed: chance.integer({ min: 1, max: 30 }),
|
|
508
|
+
lines_added: chance.integer({ min: 10, max: 800 }),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// H4: ON-CALL ROTATION FATIGUE
|
|
515
|
+
// Users with >20 alerts get increasing response_time_minutes.
|
|
516
|
+
const alertCount = events.filter(e => e.event === "alert triggered").length;
|
|
517
|
+
if (alertCount > ONCALL_ALERT_THRESHOLD) {
|
|
518
|
+
const fatigueMultiplier = 1 + Math.min(alertCount / ONCALL_FATIGUE_DIVISOR, ONCALL_FATIGUE_CAP);
|
|
519
|
+
events.forEach(e => {
|
|
520
|
+
if (e.event === "incident resolved" && e.response_time_minutes) {
|
|
521
|
+
e.response_time_minutes = Math.floor(e.response_time_minutes * fatigueMultiplier);
|
|
522
|
+
}
|
|
523
|
+
if (e.event === "incident created" && e.response_time_minutes) {
|
|
524
|
+
e.response_time_minutes = Math.floor(e.response_time_minutes * fatigueMultiplier);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// H5: OPEN SOURCE POWER USAGE
|
|
530
|
+
// OSS users with >15 events get extra cloned build + deploy events
|
|
531
|
+
// in their later activity (representing power usage that pushes them
|
|
532
|
+
// toward limits). No flag — discover via cohort by segment + event count.
|
|
533
|
+
if (profile.segment === "oss_user" && events.length > OSS_EVENT_THRESHOLD) {
|
|
534
|
+
const buildTemplate = events.find(e => e.event === "build completed");
|
|
535
|
+
const deployTemplate = events.find(e => e.event === "deployment completed");
|
|
536
|
+
if (buildTemplate || deployTemplate) {
|
|
537
|
+
const conversionPoint = Math.floor(events.length * OSS_CONVERSION_POINT_PCT);
|
|
538
|
+
const tail = events.slice(conversionPoint);
|
|
539
|
+
tail.forEach(e => {
|
|
540
|
+
const tBase = dayjs(e.time);
|
|
541
|
+
if (buildTemplate && chance.bool({ likelihood: OSS_BUILD_CLONE_LIKELIHOOD })) {
|
|
542
|
+
events.push({
|
|
543
|
+
...buildTemplate,
|
|
544
|
+
time: tBase.add(chance.integer({ min: 5, max: 240 }), "minutes").toISOString(),
|
|
545
|
+
user_id: e.user_id,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (deployTemplate && chance.bool({ likelihood: OSS_DEPLOY_CLONE_LIKELIHOOD })) {
|
|
549
|
+
events.push({
|
|
550
|
+
...deployTemplate,
|
|
551
|
+
time: tBase.add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
|
|
552
|
+
user_id: e.user_id,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// H9: BUILD-COUNT MAGIC NUMBER (no flags)
|
|
560
|
+
// Sweet 15-30 builds → +50% deploys (clone with unique offset).
|
|
561
|
+
// Over 31+ → drop 40% of deploys (flaky CI burnout).
|
|
562
|
+
const buildCount = events.filter(e => e.event === "build completed").length;
|
|
563
|
+
if (buildCount >= BUILD_SWEET_MIN && buildCount <= BUILD_SWEET_MAX) {
|
|
564
|
+
const deploys = events.filter(e => e.event === "deployment completed");
|
|
565
|
+
const extras = Math.max(Math.floor(deploys.length * BUILD_SWEET_CLONE_RATE), 1);
|
|
566
|
+
for (let k = 0; k < extras; k++) {
|
|
567
|
+
const tpl = deploys[k % deploys.length];
|
|
568
|
+
if (tpl) {
|
|
569
|
+
events.push({
|
|
570
|
+
...tpl,
|
|
571
|
+
time: dayjs(tpl.time).add(chance.integer({ min: 5, max: 360 }), "minutes").toISOString(),
|
|
572
|
+
user_id: tpl.user_id,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
} else if (buildCount >= BUILD_OVER_THRESHOLD) {
|
|
577
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
578
|
+
if (events[i].event === "deployment completed" && chance.bool({ likelihood: BUILD_OVER_DROP_LIKELIHOOD })) {
|
|
579
|
+
events.splice(i, 1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// H6: POST-OUTAGE RECOVERY
|
|
585
|
+
// After the outage volume rebound (days 44-47: window [start+44d,
|
|
586
|
+
// start+48d)), deployment events get aggressively cloned to
|
|
587
|
+
// produce a visible spike above baseline.
|
|
588
|
+
// Shifted later than outage end (d42.25) so natural volume has
|
|
589
|
+
// recovered from the 0.05x suppression before cloning kicks in.
|
|
590
|
+
const RECOVERY_START = datasetStart.add(RECOVERY_START_DAY, "days");
|
|
591
|
+
const RECOVERY_END = datasetStart.add(RECOVERY_END_DAY, "days");
|
|
592
|
+
const deployEvents = events.filter(e => {
|
|
593
|
+
if (e.event !== "deployment completed") return false;
|
|
594
|
+
const t = dayjs(e.time);
|
|
595
|
+
return t.isAfter(RECOVERY_START) && t.isBefore(RECOVERY_END);
|
|
596
|
+
});
|
|
597
|
+
deployEvents.forEach(dep => {
|
|
598
|
+
// 100% clone rate with 3 copies per event to clearly
|
|
599
|
+
// exceed baseline deploy volume (d35-41)
|
|
600
|
+
for (let c = 0; c < RECOVERY_CLONES_PER_EVENT; c++) {
|
|
601
|
+
events.push({
|
|
602
|
+
...dep,
|
|
603
|
+
time: dayjs(dep.time).add(chance.integer({ min: 1, max: 8 }), "hours").toISOString(),
|
|
604
|
+
user_id: dep.user_id,
|
|
605
|
+
deploy_status: chance.pickone(["success", "success", "rolled_back"]),
|
|
606
|
+
environment: "production",
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
return events;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ── CONFIG ──
|
|
615
|
+
/** @type {Config} */
|
|
616
|
+
const config = {
|
|
617
|
+
version: 2,
|
|
618
|
+
seed: SEED,
|
|
619
|
+
datasetStart: DATASET_START,
|
|
620
|
+
datasetEnd: DATASET_END,
|
|
621
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
622
|
+
numUsers: NUM_USERS,
|
|
623
|
+
format: "json",
|
|
624
|
+
gzip: true,
|
|
625
|
+
credentials: {
|
|
626
|
+
token,
|
|
627
|
+
},
|
|
628
|
+
switches: {
|
|
629
|
+
hasSessionIds: true,
|
|
630
|
+
alsoInferFunnels: false,
|
|
631
|
+
hasLocation: true,
|
|
632
|
+
hasAndroidDevices: false,
|
|
633
|
+
hasIOSDevices: false,
|
|
634
|
+
hasDesktopDevices: true,
|
|
635
|
+
hasBrowser: true,
|
|
636
|
+
hasCampaigns: false,
|
|
637
|
+
isAnonymous: false,
|
|
638
|
+
hasAdSpend: false,
|
|
639
|
+
hasAvatar: true,
|
|
640
|
+
},
|
|
641
|
+
identity: {
|
|
642
|
+
avgDevicePerUser: 2,
|
|
643
|
+
},
|
|
644
|
+
concurrency: 1,
|
|
645
|
+
writeToDisk: false,
|
|
646
|
+
scdProps: {
|
|
647
|
+
subscription_tier: {
|
|
648
|
+
values: ["free", "pro", "enterprise"],
|
|
649
|
+
frequency: "month",
|
|
650
|
+
timing: "fuzzy",
|
|
651
|
+
max: 6
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
mirrorProps: {},
|
|
655
|
+
lookupTables: [],
|
|
656
|
+
|
|
657
|
+
// -- Events (18) ------------------------------------------
|
|
658
|
+
events: [
|
|
659
|
+
{
|
|
660
|
+
event: "account created",
|
|
661
|
+
weight: 1,
|
|
662
|
+
isFirstEvent: true,
|
|
663
|
+
isAuthEvent: true,
|
|
664
|
+
properties: {
|
|
665
|
+
referral_source: ["organic", "github", "conference", "blog_post", "colleague", "search"],
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
event: "build completed",
|
|
670
|
+
weight: 8,
|
|
671
|
+
isStrictEvent: false,
|
|
672
|
+
properties: {
|
|
673
|
+
pipeline_id: chance.pickone.bind(chance, pipelineIds),
|
|
674
|
+
repo_id: chance.pickone.bind(chance, repoIds),
|
|
675
|
+
build_status: ["success", "success", "success", "success", "failed", "cancelled"],
|
|
676
|
+
build_duration_sec: u.weighNumRange(10, 600, 0.4, 240),
|
|
677
|
+
branch: ["main", "main", "develop", "feature", "feature", "hotfix"],
|
|
678
|
+
test_count: u.weighNumRange(10, 500, 0.5, 100),
|
|
679
|
+
test_pass_rate: u.weighNumRange(70, 100, 0.8, 95),
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
event: "deployment completed",
|
|
684
|
+
weight: 5,
|
|
685
|
+
isStrictEvent: false,
|
|
686
|
+
properties: {
|
|
687
|
+
pipeline_id: chance.pickone.bind(chance, pipelineIds),
|
|
688
|
+
repo_id: chance.pickone.bind(chance, repoIds),
|
|
689
|
+
deploy_status: ["success", "success", "success", "failed", "rolled_back"],
|
|
690
|
+
environment: ["production", "production", "staging", "staging", "dev", "preview"],
|
|
691
|
+
deploy_duration_sec: u.weighNumRange(15, 300, 0.4, 90),
|
|
692
|
+
preview_enabled: [false],
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
event: "pull request created",
|
|
697
|
+
weight: 6,
|
|
698
|
+
isStrictEvent: false,
|
|
699
|
+
properties: {
|
|
700
|
+
repo_id: chance.pickone.bind(chance, repoIds),
|
|
701
|
+
pr_size: ["small", "small", "medium", "medium", "large", "xlarge"],
|
|
702
|
+
files_changed: u.weighNumRange(1, 50, 0.4, 8),
|
|
703
|
+
lines_added: u.weighNumRange(1, 2000, 0.3, 150),
|
|
704
|
+
lines_removed: u.weighNumRange(0, 500, 0.3, 40),
|
|
705
|
+
ai_assist: ["manual"],
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
event: "code review completed",
|
|
710
|
+
weight: 5,
|
|
711
|
+
isStrictEvent: false,
|
|
712
|
+
properties: {
|
|
713
|
+
repo_id: chance.pickone.bind(chance, repoIds),
|
|
714
|
+
review_result: ["approved", "approved", "approved", "changes_requested", "commented"],
|
|
715
|
+
review_duration_hours: u.weighNumRange(0.5, 72, 0.3, 8),
|
|
716
|
+
comments_count: u.weighNumRange(0, 20, 0.5, 3),
|
|
717
|
+
ai_assist: ["manual"],
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
event: "alert triggered",
|
|
722
|
+
weight: 4,
|
|
723
|
+
isStrictEvent: false,
|
|
724
|
+
properties: {
|
|
725
|
+
alert_type: ["error_rate", "latency", "cpu", "memory", "disk", "custom_metric"],
|
|
726
|
+
severity: ["info", "warning", "warning", "critical", "critical"],
|
|
727
|
+
service: ["api", "web", "worker", "database", "cdn", "auth"],
|
|
728
|
+
acknowledged: [true, true, true, false],
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
event: "incident created",
|
|
733
|
+
weight: 2,
|
|
734
|
+
isStrictEvent: false,
|
|
735
|
+
properties: {
|
|
736
|
+
severity: ["sev1", "sev2", "sev2", "sev3", "sev3", "sev3"],
|
|
737
|
+
service: ["api", "web", "worker", "database", "cdn", "auth"],
|
|
738
|
+
response_time_minutes: u.weighNumRange(1, 120, 0.3, 30),
|
|
739
|
+
root_cause: ["deploy", "config_change", "dependency", "traffic_spike", "hardware", "unknown"],
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
event: "incident resolved",
|
|
744
|
+
weight: 2,
|
|
745
|
+
isStrictEvent: false,
|
|
746
|
+
properties: {
|
|
747
|
+
severity: ["sev1", "sev2", "sev2", "sev3", "sev3", "sev3"],
|
|
748
|
+
resolution_time_minutes: u.weighNumRange(5, 480, 0.3, 60),
|
|
749
|
+
response_time_minutes: u.weighNumRange(1, 120, 0.3, 30),
|
|
750
|
+
resolution_type: ["hotfix", "rollback", "config_change", "scaling", "restart", "manual"],
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
event: "repository connected",
|
|
755
|
+
weight: 2,
|
|
756
|
+
properties: {
|
|
757
|
+
provider: ["github", "github", "github", "gitlab", "bitbucket"],
|
|
758
|
+
repo_visibility: ["private", "private", "private", "public"],
|
|
759
|
+
language: ["javascript", "python", "go", "rust", "java", "typescript"],
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
event: "pipeline configured",
|
|
764
|
+
weight: 2,
|
|
765
|
+
properties: {
|
|
766
|
+
pipeline_id: chance.pickone.bind(chance, pipelineIds),
|
|
767
|
+
pipeline_type: ["build_test", "build_test_deploy", "deploy_only", "lint_test"],
|
|
768
|
+
trigger: ["push", "push", "pull_request", "schedule", "manual"],
|
|
769
|
+
runtime: ["docker", "docker", "node", "python", "go"],
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
event: "collaboration invited",
|
|
774
|
+
weight: 2,
|
|
775
|
+
properties: {
|
|
776
|
+
invite_role: ["developer", "developer", "admin", "viewer"],
|
|
777
|
+
invite_method: ["email", "email", "link", "github_team"],
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
event: "monitoring dashboard viewed",
|
|
782
|
+
weight: 5,
|
|
783
|
+
isStrictEvent: false,
|
|
784
|
+
properties: {
|
|
785
|
+
dashboard_type: ["overview", "performance", "errors", "deploys", "custom"],
|
|
786
|
+
time_range: ["1h", "6h", "24h", "7d", "30d"],
|
|
787
|
+
widgets_count: u.weighNumRange(1, 12, 0.5, 4),
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
event: "log searched",
|
|
792
|
+
weight: 4,
|
|
793
|
+
properties: {
|
|
794
|
+
query_type: ["full_text", "structured", "regex"],
|
|
795
|
+
time_range: ["15m", "1h", "6h", "24h", "7d"],
|
|
796
|
+
results_count: u.weighNumRange(0, 500, 0.3, 50),
|
|
797
|
+
service: ["api", "web", "worker", "database", "auth"],
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
event: "notification received",
|
|
802
|
+
weight: 6,
|
|
803
|
+
properties: {
|
|
804
|
+
notification_type: ["build_failed", "deploy_completed", "pr_review_requested", "alert_fired", "mention", "billing"],
|
|
805
|
+
channel: ["in_app", "in_app", "email", "slack", "webhook"],
|
|
806
|
+
opened: [true, true, true, false],
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
event: "billing updated",
|
|
811
|
+
weight: 1,
|
|
812
|
+
properties: {
|
|
813
|
+
change_type: ["plan_upgrade", "plan_downgrade", "payment_method", "add_seats", "remove_seats"],
|
|
814
|
+
payment_method: ["credit_card", "credit_card", "invoice", "paypal"],
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
event: "app session",
|
|
819
|
+
weight: 8,
|
|
820
|
+
properties: {
|
|
821
|
+
session_duration_sec: u.weighNumRange(10, 3600, 0.4, 180),
|
|
822
|
+
pages_viewed: u.weighNumRange(1, 20, 0.5, 4),
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
event: "environment created",
|
|
827
|
+
weight: 2,
|
|
828
|
+
properties: {
|
|
829
|
+
env_type: ["production", "staging", "dev", "preview", "test"],
|
|
830
|
+
cloud_provider: ["aws", "aws", "gcp", "azure", "self_hosted"],
|
|
831
|
+
region: ["us-east-1", "us-west-2", "eu-west-1", "ap-south-1"],
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
event: "account deactivated",
|
|
836
|
+
weight: 1,
|
|
837
|
+
isChurnEvent: true,
|
|
838
|
+
returnLikelihood: 0.15,
|
|
839
|
+
isStrictEvent: true,
|
|
840
|
+
properties: {
|
|
841
|
+
reason: ["switched_provider", "cost", "no_longer_needed", "poor_experience", "team_dissolved", "acquired"],
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
|
|
846
|
+
// -- Funnels (5) ------------------------------------------
|
|
847
|
+
funnels: [
|
|
848
|
+
{
|
|
849
|
+
name: "Onboarding",
|
|
850
|
+
sequence: ["account created", "repository connected", "pipeline configured", "build completed"],
|
|
851
|
+
conversionRate: 45,
|
|
852
|
+
order: "sequential",
|
|
853
|
+
isFirstFunnel: true,
|
|
854
|
+
timeToConvert: 72,
|
|
855
|
+
weight: 3,
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
name: "Build-Deploy Pipeline",
|
|
859
|
+
sequence: ["build completed", "deployment completed", "monitoring dashboard viewed"],
|
|
860
|
+
conversionRate: 40,
|
|
861
|
+
order: "sequential",
|
|
862
|
+
timeToConvert: 48,
|
|
863
|
+
weight: 5,
|
|
864
|
+
reentry: true,
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
name: "PR Review Flow",
|
|
868
|
+
sequence: ["pull request created", "code review completed", "build completed", "deployment completed"],
|
|
869
|
+
conversionRate: 35,
|
|
870
|
+
order: "sequential",
|
|
871
|
+
timeToConvert: 72,
|
|
872
|
+
weight: 4,
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
name: "Incident Response",
|
|
876
|
+
sequence: ["alert triggered", "incident created", "incident resolved"],
|
|
877
|
+
conversionRate: 50,
|
|
878
|
+
order: "sequential",
|
|
879
|
+
timeToConvert: 24,
|
|
880
|
+
weight: 3,
|
|
881
|
+
reentry: true,
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
name: "Upgrade Path",
|
|
885
|
+
sequence: ["app session", "billing updated", "collaboration invited"],
|
|
886
|
+
conversionRate: 20,
|
|
887
|
+
order: "sequential",
|
|
888
|
+
timeToConvert: 168,
|
|
889
|
+
weight: 2,
|
|
890
|
+
},
|
|
891
|
+
],
|
|
892
|
+
|
|
893
|
+
// -- SuperProps --------------------------------------------
|
|
894
|
+
superProps: {
|
|
895
|
+
subscription_tier: ["free", "free", "free", "team", "team", "business", "enterprise"],
|
|
896
|
+
Platform: ["web", "web", "desktop_app", "cli"],
|
|
897
|
+
language: ["javascript", "python", "go", "rust", "java", "typescript"],
|
|
898
|
+
},
|
|
899
|
+
|
|
900
|
+
// -- UserProps ---------------------------------------------
|
|
901
|
+
userProps: {
|
|
902
|
+
dev_role: ["full_stack"],
|
|
903
|
+
segment: ["full_stack"],
|
|
904
|
+
team_size: u.weighNumRange(1, 50, 0.4, 5),
|
|
905
|
+
repos_connected: [0],
|
|
906
|
+
org_name: ["personal"],
|
|
907
|
+
experience_level: ["junior", "junior", "mid", "mid", "mid", "senior", "senior"],
|
|
908
|
+
subscription_tier: ["free", "free", "free", "team", "team", "business", "enterprise"],
|
|
909
|
+
Platform: ["web", "web", "desktop_app", "cli"],
|
|
910
|
+
language: ["javascript", "python", "go", "rust", "java", "typescript"],
|
|
911
|
+
},
|
|
912
|
+
|
|
913
|
+
// -- Phase 2: Personas ------------------------------------
|
|
914
|
+
personas: [
|
|
915
|
+
{
|
|
916
|
+
name: "platform_engineer",
|
|
917
|
+
weight: 15,
|
|
918
|
+
eventMultiplier: 4.0,
|
|
919
|
+
conversionModifier: 1.5,
|
|
920
|
+
churnRate: 0.01,
|
|
921
|
+
properties: {
|
|
922
|
+
dev_role: "platform_engineer",
|
|
923
|
+
segment: "platform_eng",
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
name: "full_stack_dev",
|
|
928
|
+
weight: 35,
|
|
929
|
+
eventMultiplier: 1.5,
|
|
930
|
+
conversionModifier: 1.0,
|
|
931
|
+
churnRate: 0.05,
|
|
932
|
+
properties: {
|
|
933
|
+
dev_role: "full_stack",
|
|
934
|
+
segment: "full_stack",
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
name: "junior_dev",
|
|
939
|
+
weight: 30,
|
|
940
|
+
eventMultiplier: 0.8,
|
|
941
|
+
conversionModifier: 0.6,
|
|
942
|
+
churnRate: 0.12,
|
|
943
|
+
properties: {
|
|
944
|
+
dev_role: "junior",
|
|
945
|
+
segment: "junior",
|
|
946
|
+
},
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
name: "devops_lead",
|
|
950
|
+
weight: 10,
|
|
951
|
+
eventMultiplier: 2.0,
|
|
952
|
+
conversionModifier: 1.3,
|
|
953
|
+
churnRate: 0.02,
|
|
954
|
+
properties: {
|
|
955
|
+
dev_role: "devops_lead",
|
|
956
|
+
segment: "devops",
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
name: "open_source_user",
|
|
961
|
+
weight: 10,
|
|
962
|
+
eventMultiplier: 0.5,
|
|
963
|
+
conversionModifier: 0.3,
|
|
964
|
+
churnRate: 0.15,
|
|
965
|
+
properties: {
|
|
966
|
+
dev_role: "open_source",
|
|
967
|
+
segment: "oss_user",
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
],
|
|
971
|
+
|
|
972
|
+
// -- Phase 2: World Events --------------------------------
|
|
973
|
+
worldEvents: [
|
|
974
|
+
{
|
|
975
|
+
name: "major_outage",
|
|
976
|
+
startDay: 42,
|
|
977
|
+
duration: 0.25,
|
|
978
|
+
volumeMultiplier: 0.05,
|
|
979
|
+
affectsEvents: ["deployment completed", "build completed"],
|
|
980
|
+
injectProps: { outage_window: true },
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
name: "conference_launch",
|
|
984
|
+
startDay: 60,
|
|
985
|
+
duration: 3,
|
|
986
|
+
volumeMultiplier: 2.0,
|
|
987
|
+
affectsEvents: ["account created"],
|
|
988
|
+
injectProps: { promo: "devcon2024" },
|
|
989
|
+
},
|
|
990
|
+
],
|
|
991
|
+
|
|
992
|
+
// -- Phase 2: Data Quality --------------------------------
|
|
993
|
+
dataQuality: {
|
|
994
|
+
lateArrivingRate: 0.01,
|
|
995
|
+
duplicateRate: 0.005,
|
|
996
|
+
botUsers: 2,
|
|
997
|
+
botEventsPerUser: 300,
|
|
998
|
+
},
|
|
999
|
+
|
|
1000
|
+
hook(record, type, meta) {
|
|
1001
|
+
if (type === "funnel-post") return handleFunnelPostHooks(record, meta);
|
|
1002
|
+
if (type === "user") return handleUserHooks(record);
|
|
1003
|
+
if (type === "event") return handleEventHooks(record);
|
|
1004
|
+
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
1005
|
+
return record;
|
|
1006
|
+
},
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
export default config;
|
|
1010
|
+
|
|
1011
|
+
// ── STORIES (v1.6 machine-checkable contract) ──────────────────────
|
|
1012
|
+
// One story per numbered hook. Bands are mechanism-derived (see each
|
|
1013
|
+
// narrative) and confirmed at reduced scale (2K users, same seed)
|
|
1014
|
+
// BEFORE the full-fidelity run — never fit to full output post-hoc.
|
|
1015
|
+
// Scale guards are sized at ~50% of the expected full-fidelity (10K)
|
|
1016
|
+
// population so reduced-scale runs intentionally read WEAK.
|
|
1017
|
+
|
|
1018
|
+
const EV_CTE = `WITH ev AS (
|
|
1019
|
+
SELECT e.user_id::VARCHAR AS uid, e.time::TIMESTAMP AS t,
|
|
1020
|
+
hour(e.time::TIMESTAMP) AS hr,
|
|
1021
|
+
date_diff('day', TIMESTAMP '2026-01-01 00:00:00', e.time::TIMESTAMP) AS day_idx,
|
|
1022
|
+
e.*
|
|
1023
|
+
FROM read_json_auto('{{PREFIX}}-EVENTS*.json', sample_size=-1, union_by_name=true) e
|
|
1024
|
+
)`;
|
|
1025
|
+
|
|
1026
|
+
const US_CTE = `us AS (
|
|
1027
|
+
SELECT distinct_id::VARCHAR AS uid, segment
|
|
1028
|
+
FROM read_json_auto('{{PREFIX}}-USERS*.json', sample_size=-1, union_by_name=true)
|
|
1029
|
+
)`;
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* @param {number|null|undefined} x measured value
|
|
1033
|
+
* @param {[number, number]} nailed tight mechanism band
|
|
1034
|
+
* @param {[number, number]} strong wide band
|
|
1035
|
+
* @param {string} detail
|
|
1036
|
+
* @param {(x: number) => boolean} [inverse] effect-reversed predicate
|
|
1037
|
+
*/
|
|
1038
|
+
const bandVerdict = (x, nailed, strong, detail, inverse = () => false) => {
|
|
1039
|
+
if (x == null || Number.isNaN(Number(x))) return { verdict: "NONE", detail: `${detail} — metric missing` };
|
|
1040
|
+
const v = Number(x);
|
|
1041
|
+
if (inverse(v)) return { verdict: "INVERSE", detail };
|
|
1042
|
+
if (v >= nailed[0] && v <= nailed[1]) return { verdict: "NAILED", detail };
|
|
1043
|
+
if (v >= strong[0] && v <= strong[1]) return { verdict: "STRONG", detail };
|
|
1044
|
+
return { verdict: "WEAK", detail };
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
export const stories = [
|
|
1048
|
+
{
|
|
1049
|
+
id: "devtools-h1-failed-build-duration",
|
|
1050
|
+
hook: "H1",
|
|
1051
|
+
archetype: "cohort-prop-scale",
|
|
1052
|
+
narrative:
|
|
1053
|
+
"Failed builds run 2x longer than successful ones (BUILD_FAILURE_DURATION_MULT=2, " +
|
|
1054
|
+
"event hook multiplies build_duration_sec in place). Median is the clean read: the " +
|
|
1055
|
+
"extreme-value anomaly (10x duration at 0.3%) fattens means but not medians, and " +
|
|
1056
|
+
"cancelled builds are untouched (they track success). Mechanism ratio is exactly 2.0 " +
|
|
1057
|
+
"(both cohorts draw from the same weighNumRange(10,600,0.4,240) pool). Measured 2.000 " +
|
|
1058
|
+
"at 2K. Bot events carry null build_status and fall out of the status groups.",
|
|
1059
|
+
assertions: [
|
|
1060
|
+
{
|
|
1061
|
+
breakdown: {
|
|
1062
|
+
type: "duckdb",
|
|
1063
|
+
sql: `${EV_CTE}
|
|
1064
|
+
SELECT
|
|
1065
|
+
median(TRY_CAST(build_duration_sec AS DOUBLE)) FILTER (WHERE build_status='failed')
|
|
1066
|
+
/ median(TRY_CAST(build_duration_sec AS DOUBLE)) FILTER (WHERE build_status='success') AS med_ratio,
|
|
1067
|
+
count(*) FILTER (WHERE build_status='failed') AS n_failed
|
|
1068
|
+
FROM ev WHERE event='build completed';`,
|
|
1069
|
+
},
|
|
1070
|
+
assert: (rows) => {
|
|
1071
|
+
const r = rows?.[0] || {};
|
|
1072
|
+
const detail = `median failed/success duration ratio ${Number(r.med_ratio).toFixed(3)} (n_failed=${r.n_failed}; mechanism 2.0)`;
|
|
1073
|
+
if (Number(r.n_failed) < 35000) return { verdict: "WEAK", detail: `${detail} — scale guard: n_failed < 35000 (expect ~70K at 10K users)` };
|
|
1074
|
+
return bandVerdict(r.med_ratio, [1.9, 2.1], [1.7, 2.4], detail, (x) => x <= 1.1);
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
],
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
id: "devtools-h2-night-deploy-risk",
|
|
1081
|
+
hook: "H2",
|
|
1082
|
+
archetype: "temporal-inflection",
|
|
1083
|
+
narrative:
|
|
1084
|
+
"Deploys at 22:00-05:59 UTC are forced to failed 40% of the time. Organic failed " +
|
|
1085
|
+
"share is 1/5=0.20 (status pool), so night share = 0.4 + 0.6*0.2 = 0.52; day stays " +
|
|
1086
|
+
"~0.20 plus a small smear from H9 sweet-spot clones of night deploys landing in " +
|
|
1087
|
+
"daytime (+5-360min offsets). Days 43-49 are excluded: H6 recovery clones carry " +
|
|
1088
|
+
"success/rolled_back only and dilute the share. Measured 0.506 night / 0.213 day at 2K.",
|
|
1089
|
+
assertions: [
|
|
1090
|
+
{
|
|
1091
|
+
breakdown: {
|
|
1092
|
+
type: "duckdb",
|
|
1093
|
+
sql: `${EV_CTE}
|
|
1094
|
+
SELECT
|
|
1095
|
+
avg((deploy_status='failed')::INT) FILTER (WHERE (hr>=22 OR hr<6) AND day_idx NOT BETWEEN 43 AND 49) AS night_fail,
|
|
1096
|
+
avg((deploy_status='failed')::INT) FILTER (WHERE hr BETWEEN 6 AND 21 AND day_idx NOT BETWEEN 43 AND 49) AS day_fail,
|
|
1097
|
+
count(*) FILTER (WHERE (hr>=22 OR hr<6) AND day_idx NOT BETWEEN 43 AND 49) AS n_night
|
|
1098
|
+
FROM ev WHERE event='deployment completed';`,
|
|
1099
|
+
},
|
|
1100
|
+
assert: (rows) => {
|
|
1101
|
+
const r = rows?.[0] || {};
|
|
1102
|
+
const night = Number(r.night_fail), day = Number(r.day_fail);
|
|
1103
|
+
const detail = `night failure share ${night.toFixed(4)} vs day ${day.toFixed(4)} excl. recovery window (n_night=${r.n_night}; mechanism 0.52 vs ~0.21)`;
|
|
1104
|
+
if (Number(r.n_night) < 35000) return { verdict: "WEAK", detail: `${detail} — scale guard: n_night < 35000 (expect ~70K at 10K users)` };
|
|
1105
|
+
return bandVerdict(night, [0.46, 0.56], [0.42, 0.62], detail, (x) => x <= day + 0.05);
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
breakdown: {
|
|
1110
|
+
type: "duckdb",
|
|
1111
|
+
sql: `${EV_CTE}
|
|
1112
|
+
SELECT avg((deploy_status='failed')::INT) AS day_fail
|
|
1113
|
+
FROM ev WHERE event='deployment completed' AND hr BETWEEN 6 AND 21 AND day_idx NOT BETWEEN 43 AND 49;`,
|
|
1114
|
+
},
|
|
1115
|
+
assert: (rows) => {
|
|
1116
|
+
const day = Number(rows?.[0]?.day_fail);
|
|
1117
|
+
const detail = `daytime failure share ${day.toFixed(4)} — organic control (pool 0.20 + H9 clone smear)`;
|
|
1118
|
+
return bandVerdict(day, [0.18, 0.26], [0.15, 0.3], detail, (x) => x >= 0.4);
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
],
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
id: "devtools-h3-copilot-pr-velocity",
|
|
1125
|
+
hook: "H3",
|
|
1126
|
+
archetype: "cohort-count-scale",
|
|
1127
|
+
narrative:
|
|
1128
|
+
"Hash cohort (user_id charCodeAt(0) % 10 < 3 — GUID first char in {2,3,4,d,e,f}, " +
|
|
1129
|
+
"6/16 hex = 37.5% of users) gets floor(PRs*0.5) cloned PR events → ~1.5x PR volume. " +
|
|
1130
|
+
"The ai_assist prop is NOT the cohort key: the copilot_integration feature (launchDay " +
|
|
1131
|
+
"30) also flips it for feature adopters. Floor drag is negligible at ~23 organic " +
|
|
1132
|
+
"PRs/user. Measured ratio 1.491, cohort share 0.386 at 2K.",
|
|
1133
|
+
assertions: [
|
|
1134
|
+
{
|
|
1135
|
+
breakdown: {
|
|
1136
|
+
type: "duckdb",
|
|
1137
|
+
sql: `${EV_CTE}, pu AS (
|
|
1138
|
+
SELECT uid, (ascii(substr(uid,1,1)) % 10 < 3) AS copilot,
|
|
1139
|
+
count(*) FILTER (WHERE event='pull request created') AS prs
|
|
1140
|
+
FROM ev GROUP BY 1, 2
|
|
1141
|
+
)
|
|
1142
|
+
SELECT
|
|
1143
|
+
(sum(prs) FILTER (WHERE copilot))::DOUBLE / count(*) FILTER (WHERE copilot) AS prs_cop,
|
|
1144
|
+
(sum(prs) FILTER (WHERE NOT copilot))::DOUBLE / count(*) FILTER (WHERE NOT copilot) AS prs_manual,
|
|
1145
|
+
count(*) FILTER (WHERE copilot) AS n_cop,
|
|
1146
|
+
(count(*) FILTER (WHERE copilot))::DOUBLE / count(*) AS cohort_share
|
|
1147
|
+
FROM pu;`,
|
|
1148
|
+
},
|
|
1149
|
+
assert: (rows) => {
|
|
1150
|
+
const r = rows?.[0] || {};
|
|
1151
|
+
const ratio = Number(r.prs_cop) / Number(r.prs_manual);
|
|
1152
|
+
const detail = `PRs/user copilot ${Number(r.prs_cop).toFixed(2)} vs manual ${Number(r.prs_manual).toFixed(2)} → ratio ${ratio.toFixed(3)} (mechanism 1.5 minus floor drag)`;
|
|
1153
|
+
if (Number(r.n_cop) < 1900) return { verdict: "WEAK", detail: `${detail} — scale guard: cohort users < 1900 (expect ~3860 at 10K)` };
|
|
1154
|
+
return bandVerdict(ratio, [1.4, 1.6], [1.25, 1.75], detail, (x) => x <= 1.05);
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
breakdown: {
|
|
1159
|
+
type: "duckdb",
|
|
1160
|
+
sql: `${EV_CTE}, pu AS (SELECT uid, (ascii(substr(uid,1,1)) % 10 < 3) AS copilot FROM ev GROUP BY 1, 2)
|
|
1161
|
+
SELECT (count(*) FILTER (WHERE copilot))::DOUBLE / count(*) AS cohort_share FROM pu;`,
|
|
1162
|
+
},
|
|
1163
|
+
assert: (rows) => {
|
|
1164
|
+
const share = Number(rows?.[0]?.cohort_share);
|
|
1165
|
+
const detail = `hash cohort share ${share.toFixed(4)} (mechanism 6/16 = 0.375 of hex-GUID first chars)`;
|
|
1166
|
+
return bandVerdict(share, [0.35, 0.41], [0.32, 0.45], detail);
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
],
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
id: "devtools-h4-oncall-fatigue",
|
|
1173
|
+
hook: "H4",
|
|
1174
|
+
archetype: "cohort-prop-scale",
|
|
1175
|
+
narrative:
|
|
1176
|
+
"Users with >20 alert-triggered events get response_time_minutes scaled by " +
|
|
1177
|
+
"1 + min(alerts/20, 3) on incident created/resolved. The fatigued cohort's mean " +
|
|
1178
|
+
"multiplier measured 2.67 at 2K, and the fatigued/normal mean-response ratio tracks " +
|
|
1179
|
+
"it: 137.9 vs 53.4 min = 2.58. Cohort is behavioral (alert volume), no flag stamped.",
|
|
1180
|
+
assertions: [
|
|
1181
|
+
{
|
|
1182
|
+
breakdown: {
|
|
1183
|
+
type: "duckdb",
|
|
1184
|
+
sql: `${EV_CTE}, pu AS (
|
|
1185
|
+
SELECT uid,
|
|
1186
|
+
count(*) FILTER (WHERE event='alert triggered') AS alerts,
|
|
1187
|
+
avg(TRY_CAST(response_time_minutes AS DOUBLE)) FILTER (WHERE event IN ('incident created','incident resolved')) AS rt,
|
|
1188
|
+
count(*) FILTER (WHERE event IN ('incident created','incident resolved')) AS incidents
|
|
1189
|
+
FROM ev GROUP BY 1
|
|
1190
|
+
)
|
|
1191
|
+
SELECT
|
|
1192
|
+
avg(rt) FILTER (WHERE alerts > 20) / avg(rt) FILTER (WHERE alerts <= 20) AS rt_ratio,
|
|
1193
|
+
count(*) FILTER (WHERE alerts > 20) AS n_fatigued,
|
|
1194
|
+
(count(*) FILTER (WHERE alerts > 20))::DOUBLE / count(*) AS fatigued_share
|
|
1195
|
+
FROM pu WHERE incidents > 0;`,
|
|
1196
|
+
},
|
|
1197
|
+
assert: (rows) => {
|
|
1198
|
+
const r = rows?.[0] || {};
|
|
1199
|
+
const detail = `fatigued/normal mean response-time ratio ${Number(r.rt_ratio).toFixed(3)} (fatigued share ${Number(r.fatigued_share).toFixed(3)} of incident users; mechanism ~2.6)`;
|
|
1200
|
+
if (Number(r.n_fatigued) < 1500) return { verdict: "WEAK", detail: `${detail} — scale guard: fatigued users < 1500 (expect ~3000 at 10K)` };
|
|
1201
|
+
return bandVerdict(r.rt_ratio, [2.3, 2.9], [2.0, 3.3], detail, (x) => x <= 1.15);
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
],
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
id: "devtools-h5-oss-power-usage",
|
|
1208
|
+
hook: "H5",
|
|
1209
|
+
archetype: "cohort-count-scale",
|
|
1210
|
+
narrative:
|
|
1211
|
+
"OSS users with >15 events get tail-of-lifetime build clones (30% chance per tail " +
|
|
1212
|
+
"event) and deploy clones (20%). Nearly all surviving oss users clear the threshold " +
|
|
1213
|
+
"(~72 organic events), so the read is oss-active vs non-oss-active BUILD SHARE of " +
|
|
1214
|
+
"events: mechanism (0.19 + 0.3*0.3) / (1.15 * 0.19) ≈ 1.28, measured 1.305 at 2K. " +
|
|
1215
|
+
"Deploy share also rises (measured 1.66x) but couples with H9 (oss builds land in " +
|
|
1216
|
+
"the sweet spot → +50% deploys; heavy non-oss builders lose 40%), so it gets a wider " +
|
|
1217
|
+
"band. Per-event shares cancel the 0.5x persona event multiplier.",
|
|
1218
|
+
assertions: [
|
|
1219
|
+
{
|
|
1220
|
+
breakdown: {
|
|
1221
|
+
type: "duckdb",
|
|
1222
|
+
sql: `${EV_CTE}, ${US_CTE}, pu AS (
|
|
1223
|
+
SELECT ev.uid, count(*) AS n_ev,
|
|
1224
|
+
count(*) FILTER (WHERE event='build completed') AS builds,
|
|
1225
|
+
count(*) FILTER (WHERE event='deployment completed') AS deploys
|
|
1226
|
+
FROM ev GROUP BY 1
|
|
1227
|
+
)
|
|
1228
|
+
SELECT
|
|
1229
|
+
(sum(builds) FILTER (WHERE u.segment='oss_user' AND n_ev > 25))::DOUBLE / sum(n_ev) FILTER (WHERE u.segment='oss_user' AND n_ev > 25) AS oss_build_share,
|
|
1230
|
+
(sum(builds) FILTER (WHERE u.segment != 'oss_user' AND n_ev > 25))::DOUBLE / sum(n_ev) FILTER (WHERE u.segment != 'oss_user' AND n_ev > 25) AS ctl_build_share,
|
|
1231
|
+
(sum(deploys) FILTER (WHERE u.segment='oss_user' AND n_ev > 25))::DOUBLE / sum(n_ev) FILTER (WHERE u.segment='oss_user' AND n_ev > 25) AS oss_deploy_share,
|
|
1232
|
+
(sum(deploys) FILTER (WHERE u.segment != 'oss_user' AND n_ev > 25))::DOUBLE / sum(n_ev) FILTER (WHERE u.segment != 'oss_user' AND n_ev > 25) AS ctl_deploy_share,
|
|
1233
|
+
count(*) FILTER (WHERE u.segment='oss_user' AND n_ev > 25) AS n_oss_hi
|
|
1234
|
+
FROM pu p JOIN us u ON p.uid = u.uid;`,
|
|
1235
|
+
},
|
|
1236
|
+
assert: (rows) => {
|
|
1237
|
+
const r = rows?.[0] || {};
|
|
1238
|
+
const ratio = Number(r.oss_build_share) / Number(r.ctl_build_share);
|
|
1239
|
+
const detail = `active-oss build share ${Number(r.oss_build_share).toFixed(4)} vs active-non-oss ${Number(r.ctl_build_share).toFixed(4)} → ratio ${ratio.toFixed(3)} (mechanism ~1.28)`;
|
|
1240
|
+
if (Number(r.n_oss_hi) < 480) return { verdict: "WEAK", detail: `${detail} — scale guard: active oss users < 480 (expect ~985 at 10K)` };
|
|
1241
|
+
return bandVerdict(ratio, [1.2, 1.42], [1.1, 1.55], detail, (x) => x <= 1.02);
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
breakdown: {
|
|
1246
|
+
type: "duckdb",
|
|
1247
|
+
sql: `${EV_CTE}, ${US_CTE}, pu AS (
|
|
1248
|
+
SELECT ev.uid, count(*) AS n_ev,
|
|
1249
|
+
count(*) FILTER (WHERE event='deployment completed') AS deploys
|
|
1250
|
+
FROM ev GROUP BY 1
|
|
1251
|
+
)
|
|
1252
|
+
SELECT
|
|
1253
|
+
(sum(deploys) FILTER (WHERE u.segment='oss_user' AND n_ev > 25))::DOUBLE / sum(n_ev) FILTER (WHERE u.segment='oss_user' AND n_ev > 25) AS oss_deploy_share,
|
|
1254
|
+
(sum(deploys) FILTER (WHERE u.segment != 'oss_user' AND n_ev > 25))::DOUBLE / sum(n_ev) FILTER (WHERE u.segment != 'oss_user' AND n_ev > 25) AS ctl_deploy_share
|
|
1255
|
+
FROM pu p JOIN us u ON p.uid = u.uid;`,
|
|
1256
|
+
},
|
|
1257
|
+
assert: (rows) => {
|
|
1258
|
+
const r = rows?.[0] || {};
|
|
1259
|
+
const ratio = Number(r.oss_deploy_share) / Number(r.ctl_deploy_share);
|
|
1260
|
+
const detail = `active-oss deploy share ratio ${ratio.toFixed(3)} (H5 clones + H9 coupling: oss sweet-spot boost vs non-oss over-threshold drop)`;
|
|
1261
|
+
return bandVerdict(ratio, [1.4, 1.95], [1.2, 2.2], detail, (x) => x <= 1.0);
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
],
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
id: "devtools-h6-post-outage-recovery",
|
|
1268
|
+
hook: "H6",
|
|
1269
|
+
archetype: "temporal-inflection",
|
|
1270
|
+
narrative:
|
|
1271
|
+
"Every deploy in days 44-47 gets 3 clones (+1-8h offsets, status success/rolled_back). " +
|
|
1272
|
+
"Read is ratio-of-ratios vs builds (win/base deploys over win/base builds) to cancel " +
|
|
1273
|
+
"the growth ramp and soup DOW. Mechanism: 4x minus clone spill — offsets average +4.5h, " +
|
|
1274
|
+
"so ~4.7% of clones exit the 96h window (4x → 3.86) and land in the baseline zone " +
|
|
1275
|
+
"(day 49), deflating the ratio to ~3.71. Measured 3.705 at 2K; re-measured with a " +
|
|
1276
|
+
"spill-free baseline (excluding days 48-49) at 3.703 — the two effects nearly cancel.",
|
|
1277
|
+
assertions: [
|
|
1278
|
+
{
|
|
1279
|
+
breakdown: {
|
|
1280
|
+
type: "duckdb",
|
|
1281
|
+
sql: `${EV_CTE}, agg AS (
|
|
1282
|
+
SELECT CASE WHEN day_idx BETWEEN 44 AND 47 THEN 'win'
|
|
1283
|
+
WHEN day_idx BETWEEN 35 AND 41 OR day_idx BETWEEN 49 AND 55 THEN 'base' END AS zone,
|
|
1284
|
+
count(*) FILTER (WHERE event='deployment completed') AS deploys,
|
|
1285
|
+
count(*) FILTER (WHERE event='build completed') AS builds
|
|
1286
|
+
FROM ev WHERE event IN ('deployment completed','build completed') AND day_idx BETWEEN 35 AND 55
|
|
1287
|
+
GROUP BY 1
|
|
1288
|
+
)
|
|
1289
|
+
SELECT
|
|
1290
|
+
((max(CASE WHEN zone='win' THEN deploys END)::DOUBLE / max(CASE WHEN zone='base' THEN deploys END))
|
|
1291
|
+
/ (max(CASE WHEN zone='win' THEN builds END)::DOUBLE / max(CASE WHEN zone='base' THEN builds END))) AS ror,
|
|
1292
|
+
max(CASE WHEN zone='win' THEN deploys END) AS deploys_win
|
|
1293
|
+
FROM agg;`,
|
|
1294
|
+
},
|
|
1295
|
+
assert: (rows) => {
|
|
1296
|
+
const r = rows?.[0] || {};
|
|
1297
|
+
const detail = `recovery-window deploy ratio-of-ratios vs builds ${Number(r.ror).toFixed(3)} (deploys_win=${r.deploys_win}; mechanism 4x − spill ≈ 3.7)`;
|
|
1298
|
+
if (Number(r.deploys_win) < 12000) return { verdict: "WEAK", detail: `${detail} — scale guard: window deploys < 12000 (expect ~25K at 10K)` };
|
|
1299
|
+
return bandVerdict(r.ror, [3.45, 3.95], [3.0, 4.4], detail, (x) => x <= 1.5);
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
],
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
id: "devtools-h7-devops-profile-enrichment",
|
|
1306
|
+
hook: "H7",
|
|
1307
|
+
archetype: "cohort-prop-scale",
|
|
1308
|
+
narrative:
|
|
1309
|
+
"User hook rewrites profile props by segment: devops → team_size U(10,50) (mean 30), " +
|
|
1310
|
+
"repos_connected U(5,20) (mean 12.5), experience senior; platform_eng → U(5,25)/U(3,15); " +
|
|
1311
|
+
"junior → U(1,8)/U(0,3). full_stack and oss_user keep DEFAULTS: repos [0] exactly, and " +
|
|
1312
|
+
"team_size from weighNumRange(1,50,0.4,5) whose mean is ~24 — so the team-size contrast " +
|
|
1313
|
+
"is devops-vs-junior, and repos_connected (default 0) is the crisp cross-segment signal.",
|
|
1314
|
+
assertions: [
|
|
1315
|
+
{
|
|
1316
|
+
breakdown: {
|
|
1317
|
+
type: "duckdb",
|
|
1318
|
+
sql: `SELECT segment, count(*) AS users,
|
|
1319
|
+
avg(TRY_CAST(team_size AS DOUBLE)) AS avg_team,
|
|
1320
|
+
avg(TRY_CAST(repos_connected AS DOUBLE)) AS avg_repos,
|
|
1321
|
+
mode(experience_level) AS mode_exp
|
|
1322
|
+
FROM read_json_auto('{{PREFIX}}-USERS*.json', sample_size=-1, union_by_name=true)
|
|
1323
|
+
GROUP BY segment;`,
|
|
1324
|
+
},
|
|
1325
|
+
assert: (rows) => {
|
|
1326
|
+
const seg = Object.fromEntries((rows || []).map((r) => [r.segment, r]));
|
|
1327
|
+
const d = seg.devops || {};
|
|
1328
|
+
const detail = `devops team_size ${Number(d.avg_team).toFixed(1)} (mean-30 target), repos ${Number(d.avg_repos).toFixed(1)} (mean-12.5 target), mode exp ${d.mode_exp} (n=${d.users})`;
|
|
1329
|
+
if (Number(d.users || 0) < 480) return { verdict: "WEAK", detail: `${detail} — scale guard: devops users < 480 (expect ~1000 at 10K)` };
|
|
1330
|
+
if (d.mode_exp !== "senior") return { verdict: "INVERSE", detail };
|
|
1331
|
+
const team = bandVerdict(d.avg_team, [28.5, 31.5], [27, 33], detail, (x) => x <= 8);
|
|
1332
|
+
const repos = bandVerdict(d.avg_repos, [11.8, 13.2], [11, 14], detail, (x) => x <= 1);
|
|
1333
|
+
const worst = ["INVERSE", "NONE", "WEAK", "STRONG", "NAILED"].find((v) => v === team.verdict || v === repos.verdict);
|
|
1334
|
+
return { verdict: worst, detail };
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
breakdown: {
|
|
1339
|
+
type: "duckdb",
|
|
1340
|
+
sql: `SELECT segment, count(*) AS users,
|
|
1341
|
+
avg(TRY_CAST(team_size AS DOUBLE)) AS avg_team,
|
|
1342
|
+
avg(TRY_CAST(repos_connected AS DOUBLE)) AS avg_repos
|
|
1343
|
+
FROM read_json_auto('{{PREFIX}}-USERS*.json', sample_size=-1, union_by_name=true)
|
|
1344
|
+
GROUP BY segment;`,
|
|
1345
|
+
},
|
|
1346
|
+
assert: (rows) => {
|
|
1347
|
+
const seg = Object.fromEntries((rows || []).map((r) => [r.segment, r]));
|
|
1348
|
+
const jr = Number(seg.junior?.avg_team), pe = Number(seg.platform_eng?.avg_repos), fs = Number(seg.full_stack?.avg_repos);
|
|
1349
|
+
const detail = `junior team_size ${jr.toFixed(2)} (mean-4.5 target), platform_eng repos ${pe.toFixed(2)} (mean-9 target), full_stack repos ${fs.toFixed(3)} (default [0])`;
|
|
1350
|
+
if (fs > 0.01) return { verdict: "INVERSE", detail: `${detail} — full_stack repos nonzero: default pool violated` };
|
|
1351
|
+
const jrV = bandVerdict(jr, [4.2, 4.9], [3.8, 5.4], detail, (x) => x >= 20);
|
|
1352
|
+
const peV = bandVerdict(pe, [8.4, 9.8], [8.0, 10.5], detail);
|
|
1353
|
+
const worst = ["INVERSE", "NONE", "WEAK", "STRONG", "NAILED"].find((v) => v === jrV.verdict || v === peV.verdict);
|
|
1354
|
+
return { verdict: worst, detail };
|
|
1355
|
+
},
|
|
1356
|
+
},
|
|
1357
|
+
],
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
id: "devtools-h8-enterprise-funnel-lift",
|
|
1361
|
+
hook: "H8",
|
|
1362
|
+
archetype: "funnel-conversion-by-segment",
|
|
1363
|
+
narrative:
|
|
1364
|
+
"Free/team users (everyone except enterprise/business) drop 35% of monitoring-dashboard-" +
|
|
1365
|
+
"viewed events (keep rate 0.65). Read is monitoring views PER DEPLOYMENT by tier group — " +
|
|
1366
|
+
"the per-deploy normalization cancels H6/H9 deploy inflation (both hit all tiers evenly). " +
|
|
1367
|
+
"Mechanism 0.65; measured 0.626 at 2K (small drift from tier/activity covariance).",
|
|
1368
|
+
assertions: [
|
|
1369
|
+
{
|
|
1370
|
+
breakdown: {
|
|
1371
|
+
type: "duckdb",
|
|
1372
|
+
sql: `${EV_CTE}, pu AS (
|
|
1373
|
+
SELECT uid, any_value(subscription_tier) AS tier,
|
|
1374
|
+
count(*) FILTER (WHERE event='monitoring dashboard viewed') AS mv,
|
|
1375
|
+
count(*) FILTER (WHERE event='deployment completed') AS dep
|
|
1376
|
+
FROM ev WHERE event IN ('monitoring dashboard viewed','deployment completed')
|
|
1377
|
+
GROUP BY 1
|
|
1378
|
+
)
|
|
1379
|
+
SELECT
|
|
1380
|
+
(sum(mv) FILTER (WHERE tier IN ('free','team')))::DOUBLE / nullif(sum(dep) FILTER (WHERE tier IN ('free','team')), 0) AS ft_mv_per_dep,
|
|
1381
|
+
(sum(mv) FILTER (WHERE tier IN ('enterprise','business')))::DOUBLE / nullif(sum(dep) FILTER (WHERE tier IN ('enterprise','business')), 0) AS paid_mv_per_dep,
|
|
1382
|
+
count(*) FILTER (WHERE tier IN ('free','team')) AS n_ft,
|
|
1383
|
+
count(*) FILTER (WHERE tier IN ('enterprise','business')) AS n_paid
|
|
1384
|
+
FROM pu WHERE tier IS NOT NULL;`,
|
|
1385
|
+
},
|
|
1386
|
+
assert: (rows) => {
|
|
1387
|
+
const r = rows?.[0] || {};
|
|
1388
|
+
const keep = Number(r.ft_mv_per_dep) / Number(r.paid_mv_per_dep);
|
|
1389
|
+
const detail = `free/team mv-per-deploy ${Number(r.ft_mv_per_dep).toFixed(4)} vs paid ${Number(r.paid_mv_per_dep).toFixed(4)} → keep ratio ${keep.toFixed(3)} (knob 0.65)`;
|
|
1390
|
+
if (Number(r.n_ft) < 3400 || Number(r.n_paid) < 1400) return { verdict: "WEAK", detail: `${detail} — scale guard: free/team < 3400 or paid < 1400 users (expect ~7000/~3000 at 10K)` };
|
|
1391
|
+
return bandVerdict(keep, [0.58, 0.7], [0.52, 0.78], detail, (x) => x >= 0.92);
|
|
1392
|
+
},
|
|
1393
|
+
},
|
|
1394
|
+
],
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
id: "devtools-h9-build-count-magic-number",
|
|
1398
|
+
hook: "H9",
|
|
1399
|
+
archetype: "frequency-sweet-spot",
|
|
1400
|
+
narrative:
|
|
1401
|
+
"Users with 15-30 builds get +50% deploy clones; 31+ builds drop 40% of deploys. Read " +
|
|
1402
|
+
"is deploys-per-build within full_stack only (holds persona constant) excluding " +
|
|
1403
|
+
"recovery-window deploys (decouples H6). The clean pair is over/sweet = 0.6/1.5 = 0.40 " +
|
|
1404
|
+
"exactly — organic deploys-per-build cancels between two high-activity buckets; " +
|
|
1405
|
+
"measured 0.403 at 2K. sweet/base carries a base-bucket organic offset (low-build " +
|
|
1406
|
+
"users run deploy-richer organic mixes, ~0.70 vs ~0.63): mechanism 1.5x lands ~1.35 " +
|
|
1407
|
+
"observed; measured 1.347 at 2K.",
|
|
1408
|
+
assertions: [
|
|
1409
|
+
{
|
|
1410
|
+
breakdown: {
|
|
1411
|
+
type: "duckdb",
|
|
1412
|
+
sql: `${EV_CTE}, ${US_CTE}, pu AS (
|
|
1413
|
+
SELECT ev.uid,
|
|
1414
|
+
count(*) FILTER (WHERE event='build completed') AS builds,
|
|
1415
|
+
count(*) FILTER (WHERE event='deployment completed' AND day_idx NOT BETWEEN 43 AND 49) AS deploys
|
|
1416
|
+
FROM ev GROUP BY 1
|
|
1417
|
+
), b AS (
|
|
1418
|
+
SELECT CASE WHEN builds BETWEEN 15 AND 30 THEN 'sweet' WHEN builds >= 31 THEN 'over' ELSE 'base' END AS bucket,
|
|
1419
|
+
count(*) AS users, sum(deploys)::DOUBLE / sum(builds) AS dpb
|
|
1420
|
+
FROM pu p JOIN us u ON p.uid = u.uid
|
|
1421
|
+
WHERE u.segment = 'full_stack' AND builds >= 1
|
|
1422
|
+
GROUP BY 1
|
|
1423
|
+
)
|
|
1424
|
+
SELECT
|
|
1425
|
+
max(CASE WHEN bucket='over' THEN dpb END) / max(CASE WHEN bucket='sweet' THEN dpb END) AS over_sweet,
|
|
1426
|
+
max(CASE WHEN bucket='sweet' THEN users END) AS n_sweet,
|
|
1427
|
+
max(CASE WHEN bucket='over' THEN users END) AS n_over
|
|
1428
|
+
FROM b;`,
|
|
1429
|
+
},
|
|
1430
|
+
assert: (rows) => {
|
|
1431
|
+
const r = rows?.[0] || {};
|
|
1432
|
+
const detail = `deploys-per-build over/sweet ${Number(r.over_sweet).toFixed(3)} (mechanism 0.6/1.5 = 0.40 exact; n_sweet=${r.n_sweet}, n_over=${r.n_over})`;
|
|
1433
|
+
if (Number(r.n_sweet) < 350 || Number(r.n_over) < 1200) return { verdict: "WEAK", detail: `${detail} — scale guard: sweet < 350 or over < 1200 full_stack users (expect ~710/~2500 at 10K)` };
|
|
1434
|
+
return bandVerdict(r.over_sweet, [0.36, 0.44], [0.3, 0.5], detail, (x) => x >= 0.85);
|
|
1435
|
+
},
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
breakdown: {
|
|
1439
|
+
type: "duckdb",
|
|
1440
|
+
sql: `${EV_CTE}, ${US_CTE}, pu AS (
|
|
1441
|
+
SELECT ev.uid,
|
|
1442
|
+
count(*) FILTER (WHERE event='build completed') AS builds,
|
|
1443
|
+
count(*) FILTER (WHERE event='deployment completed' AND day_idx NOT BETWEEN 43 AND 49) AS deploys
|
|
1444
|
+
FROM ev GROUP BY 1
|
|
1445
|
+
), b AS (
|
|
1446
|
+
SELECT CASE WHEN builds BETWEEN 15 AND 30 THEN 'sweet' WHEN builds >= 31 THEN 'over' ELSE 'base' END AS bucket,
|
|
1447
|
+
count(*) AS users, sum(deploys)::DOUBLE / sum(builds) AS dpb
|
|
1448
|
+
FROM pu p JOIN us u ON p.uid = u.uid
|
|
1449
|
+
WHERE u.segment = 'full_stack' AND builds >= 1
|
|
1450
|
+
GROUP BY 1
|
|
1451
|
+
)
|
|
1452
|
+
SELECT
|
|
1453
|
+
max(CASE WHEN bucket='sweet' THEN dpb END) / max(CASE WHEN bucket='base' THEN dpb END) AS sweet_base,
|
|
1454
|
+
max(CASE WHEN bucket='base' THEN users END) AS n_base
|
|
1455
|
+
FROM b;`,
|
|
1456
|
+
},
|
|
1457
|
+
assert: (rows) => {
|
|
1458
|
+
const r = rows?.[0] || {};
|
|
1459
|
+
const detail = `deploys-per-build sweet/base ${Number(r.sweet_base).toFixed(3)} (mechanism 1.5x minus base-bucket organic offset ≈ 1.35; n_base=${r.n_base})`;
|
|
1460
|
+
if (Number(r.n_base) < 130) return { verdict: "WEAK", detail: `${detail} — scale guard: base bucket < 130 full_stack users (expect ~275 at 10K)` };
|
|
1461
|
+
return bandVerdict(r.sweet_base, [1.2, 1.55], [1.05, 1.75], detail, (x) => x <= 1.0);
|
|
1462
|
+
},
|
|
1463
|
+
},
|
|
1464
|
+
],
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
id: "devtools-h10-build-deploy-ttc-by-tier",
|
|
1468
|
+
hook: "H10",
|
|
1469
|
+
archetype: "funnel-ttc-by-segment",
|
|
1470
|
+
narrative:
|
|
1471
|
+
"funnel-post scales Build-Deploy Pipeline step gaps by tier: enterprise/business x0.67, " +
|
|
1472
|
+
"free x1.33, team 1.0 control. Engineered ent/free ratio = 0.504, but the observed " +
|
|
1473
|
+
"2-step (build→deploy) median-TTC ratio lands ~0.6-0.7: greedy min-gap picks plus H9/H6 " +
|
|
1474
|
+
"deploy-clone pollution compress gaps for every tier, and free's stretched conversions " +
|
|
1475
|
+
"censor past the window — both attenuate toward 1. Window 96h = 2x the funnel's 48h " +
|
|
1476
|
+
"timeToConvert, covering the 1.33x-stretched support. Measured at 2K (96h window): " +
|
|
1477
|
+
"ent/free 0.683, biz/free 0.604, team/free 0.798. Second assertion pins the identity " +
|
|
1478
|
+
"model: auth-on-first means every event carries user_id; device_id and tier stamps " +
|
|
1479
|
+
"miss only the 2 bot users (~0.13% of events).",
|
|
1480
|
+
assertions: [
|
|
1481
|
+
{
|
|
1482
|
+
breakdown: {
|
|
1483
|
+
type: "timeToConvert",
|
|
1484
|
+
steps: ["build completed", "deployment completed"],
|
|
1485
|
+
breakdownByUserProperty: "subscription_tier",
|
|
1486
|
+
conversionWindowMs: 96 * 3600 * 1000,
|
|
1487
|
+
},
|
|
1488
|
+
assert: (rows) => {
|
|
1489
|
+
const cells = Object.fromEntries((rows || []).map((r) => [r.segment_value, r]));
|
|
1490
|
+
const med = (t) => (cells[t] ? cells[t].median_ttc_ms : null);
|
|
1491
|
+
const free = med("free"), ent = med("enterprise"), biz = med("business"), team = med("team");
|
|
1492
|
+
if (!free || !ent || !biz || !team) return { verdict: "NONE", detail: "missing tier cell in timeToConvert breakdown" };
|
|
1493
|
+
const entR = ent / free, bizR = biz / free, teamR = team / free;
|
|
1494
|
+
const detail = `median TTC ratios vs free — enterprise ${entR.toFixed(3)}, business ${bizR.toFixed(3)}, team ${teamR.toFixed(3)} (engineered 0.504/0.504/0.752, attenuation documented; free n=${cells.free.user_count})`;
|
|
1495
|
+
if (Number(cells.free.user_count) < 1200) return { verdict: "WEAK", detail: `${detail} — scale guard: free converters < 1200 (expect ~2780 at 10K)` };
|
|
1496
|
+
if (Math.min(entR, bizR) >= 0.95) return { verdict: "INVERSE", detail };
|
|
1497
|
+
if (entR >= 0.5 && entR <= 0.8 && bizR >= 0.5 && bizR <= 0.8 && teamR >= 0.68 && teamR <= 0.9) return { verdict: "NAILED", detail };
|
|
1498
|
+
if (entR >= 0.42 && entR <= 0.9 && bizR >= 0.42 && bizR <= 0.9 && teamR >= 0.6 && teamR <= 0.98) return { verdict: "STRONG", detail };
|
|
1499
|
+
return { verdict: "WEAK", detail };
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
breakdown: {
|
|
1504
|
+
type: "duckdb",
|
|
1505
|
+
sql: `${EV_CTE}
|
|
1506
|
+
SELECT
|
|
1507
|
+
avg((uid IS NOT NULL)::INT) AS uid_share,
|
|
1508
|
+
avg((device_id IS NOT NULL)::INT) AS dev_share,
|
|
1509
|
+
avg((subscription_tier IS NOT NULL)::INT) AS tier_share
|
|
1510
|
+
FROM ev;`,
|
|
1511
|
+
},
|
|
1512
|
+
assert: (rows) => {
|
|
1513
|
+
const r = rows?.[0] || {};
|
|
1514
|
+
const uid = Number(r.uid_share), dev = Number(r.dev_share), tier = Number(r.tier_share);
|
|
1515
|
+
const detail = `identity invariants — user_id share ${uid.toFixed(4)}, device_id share ${dev.toFixed(4)}, tier stamp share ${tier.toFixed(4)} (auth-on-first: 1.0 / ~0.999 / ~0.999; bots lack device+tier)`;
|
|
1516
|
+
if (uid === 1 && dev >= 0.995 && tier >= 0.995) return { verdict: "NAILED", detail };
|
|
1517
|
+
if (uid >= 0.999 && dev >= 0.99 && tier >= 0.99) return { verdict: "STRONG", detail };
|
|
1518
|
+
if (uid < 0.9) return { verdict: "INVERSE", detail };
|
|
1519
|
+
return { verdict: "WEAK", detail };
|
|
1520
|
+
},
|
|
1521
|
+
},
|
|
1522
|
+
],
|
|
1523
|
+
},
|
|
1524
|
+
];
|