@oneie/claude 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/agents/abm-strategist.md +89 -0
  2. package/agents/ads-meta.md +97 -0
  3. package/agents/analyst.md +173 -0
  4. package/agents/architect.md +280 -0
  5. package/agents/brand-guardian.md +88 -0
  6. package/agents/brand-strategist.md +92 -0
  7. package/agents/campaign-content.md +90 -0
  8. package/agents/campaign-email.md +88 -0
  9. package/agents/campaign-sms.md +87 -0
  10. package/agents/campaign-social.md +87 -0
  11. package/agents/cco.md +99 -0
  12. package/agents/ceo.md +106 -0
  13. package/agents/chairman.md +120 -0
  14. package/agents/cmo.md +101 -0
  15. package/agents/community-greeter.md +89 -0
  16. package/agents/community-moderator.md +92 -0
  17. package/agents/compliance.md +92 -0
  18. package/agents/copywriter.md +100 -0
  19. package/agents/creative-strategist.md +95 -0
  20. package/agents/cro.md +98 -0
  21. package/agents/cto.md +267 -0
  22. package/agents/customer-interviewer.md +93 -0
  23. package/agents/customer-researcher.md +89 -0
  24. package/agents/customer-success-manager.md +90 -0
  25. package/agents/customer-trainer.md +90 -0
  26. package/agents/cxo.md +100 -0
  27. package/agents/demand-creator.md +89 -0
  28. package/agents/demo-mover.md +83 -0
  29. package/agents/demo-specialist.md +89 -0
  30. package/agents/demo-thai-family-law.md +87 -0
  31. package/agents/designer.md +112 -0
  32. package/agents/discovery-caller.md +98 -0
  33. package/agents/doctor.md +139 -0
  34. package/agents/educate-coach.md +100 -0
  35. package/agents/elevate-tutor.md +83 -0
  36. package/agents/email-lifecycle-marketer.md +91 -0
  37. package/agents/engage-specialist.md +99 -0
  38. package/agents/events-coordinator.md +90 -0
  39. package/agents/foundation-builder.md +94 -0
  40. package/agents/funnel-architect.md +89 -0
  41. package/agents/gift-creator.md +100 -0
  42. package/agents/google-ads.md +103 -0
  43. package/agents/guide.md +292 -0
  44. package/agents/helpdesk-dispatcher.md +88 -0
  45. package/agents/hook-specialist.md +91 -0
  46. package/agents/identify-optimizer.md +101 -0
  47. package/agents/implementer.md +305 -0
  48. package/agents/incident-commander.md +120 -0
  49. package/agents/insights-lead.md +93 -0
  50. package/agents/journey-runner.md +113 -0
  51. package/agents/linkedin-ads.md +123 -0
  52. package/agents/live-sales-chat.md +90 -0
  53. package/agents/market-researcher.md +88 -0
  54. package/agents/media-buyer.md +96 -0
  55. package/agents/movers-customer-researcher.md +95 -0
  56. package/agents/movers-foundation-builder.md +96 -0
  57. package/agents/movers-market-researcher.md +97 -0
  58. package/agents/movers-pricing-strategist.md +171 -0
  59. package/agents/nurture-architect.md +99 -0
  60. package/agents/offer-architect.md +92 -0
  61. package/agents/onboarder.md +222 -0
  62. package/agents/onboarding-specialist.md +92 -0
  63. package/agents/operations-dashboard.md +98 -0
  64. package/agents/perf-engineer.md +348 -0
  65. package/agents/playbook-writer.md +71 -0
  66. package/agents/plg-strategist.md +91 -0
  67. package/agents/positioning-architect.md +88 -0
  68. package/agents/press-officer.md +89 -0
  69. package/agents/pricing-strategist.md +94 -0
  70. package/agents/privacy-officer.md +92 -0
  71. package/agents/referral-manager.md +91 -0
  72. package/agents/refine-analyst.md +102 -0
  73. package/agents/release-manager.md +261 -0
  74. package/agents/renewals-upsell-rep.md +90 -0
  75. package/agents/review-engineer.md +327 -0
  76. package/agents/rewards-steward.md +82 -0
  77. package/agents/sales-call-coach.md +94 -0
  78. package/agents/sales-closer.md +98 -0
  79. package/agents/security-auditor.md +313 -0
  80. package/agents/sell-closer.md +101 -0
  81. package/agents/share-amplifier.md +99 -0
  82. package/agents/social-media-manager.md +92 -0
  83. package/agents/storyteller.md +250 -0
  84. package/agents/strategist.md +83 -0
  85. package/agents/strategy-aligner.md +94 -0
  86. package/agents/support-agent.md +90 -0
  87. package/agents/tagger.md +245 -0
  88. package/agents/tech-writer.md +216 -0
  89. package/agents/test-engineer.md +243 -0
  90. package/agents/tiktok-ads.md +105 -0
  91. package/agents/tracking-engineer.md +92 -0
  92. package/agents/upsell-strategist.md +101 -0
  93. package/agents/voice.md +85 -0
  94. package/agents/w4-tools.md +153 -0
  95. package/agents/w4-verify.md +20 -138
  96. package/agents/workflow-optimiser.md +84 -0
  97. package/commands/close.md +814 -160
  98. package/commands/create.md +2 -2
  99. package/commands/deploy.md +554 -175
  100. package/commands/do.md +119 -109
  101. package/commands/fast.md +67 -0
  102. package/commands/improve.md +2 -2
  103. package/commands/one.md +418 -0
  104. package/commands/see.md +1 -1
  105. package/commands/sync.md +1 -1
  106. package/commands/tasks.md +222 -0
  107. package/commands/vespio.md +110 -0
  108. package/commands/vespio.remote.md +227 -0
  109. package/hooks/hooks.json +25 -79
  110. package/hooks/lib/governor-escape-match.sh +88 -0
  111. package/hooks/lib/hook.sh +4 -13
  112. package/hooks/lib/signal.sh +9 -2
  113. package/hooks/scripts/branch-pin.sh +6 -0
  114. package/hooks/scripts/config-protect.sh +6 -0
  115. package/hooks/scripts/do-outcome-gate.sh +19 -2
  116. package/hooks/scripts/git-add-guard.sh +43 -2
  117. package/hooks/scripts/governor-escape.sh +96 -0
  118. package/hooks/scripts/load-guard.sh +127 -0
  119. package/hooks/scripts/post-edit-check.sh +36 -29
  120. package/hooks/scripts/session-start.sh +34 -80
  121. package/hooks/scripts/task-complete-verify.sh +46 -40
  122. package/package.json +1 -1
  123. package/rules/documentation.md +9 -0
  124. package/scripts/ad-copy-lint.sh +656 -0
  125. package/scripts/agentverse-audit.sh +256 -0
  126. package/scripts/asi-walk.sh +435 -0
  127. package/scripts/astro-build-cached.sh +143 -0
  128. package/scripts/blocks-manifest-cached.sh +100 -0
  129. package/scripts/blocks-manifest.mjs +497 -0
  130. package/scripts/blocks-ratchet.sh +116 -0
  131. package/scripts/blocks-render-probe.mjs +529 -0
  132. package/scripts/blocks-usage.mjs +334 -0
  133. package/scripts/chat-context-check.sh +89 -0
  134. package/scripts/close-metrics.sh +558 -0
  135. package/scripts/close-owner.sh +268 -0
  136. package/scripts/db-sync-lock-check.sh +116 -0
  137. package/scripts/deploy-dev.sh +35 -0
  138. package/scripts/deploy-emit.sh +298 -0
  139. package/scripts/deploy-gate-check.sh +128 -0
  140. package/scripts/deploy-ready.sh +78 -0
  141. package/scripts/deploy-record.sh +605 -0
  142. package/scripts/deploy.sh +1273 -0
  143. package/scripts/do-auto.sh +598 -48
  144. package/scripts/do-brief.sh +113 -0
  145. package/scripts/do-close.sh +1137 -0
  146. package/scripts/do-commit.sh +75 -0
  147. package/scripts/do-consumer-sweep.sh +18 -1
  148. package/scripts/do-cycle-shape-check.sh +160 -0
  149. package/scripts/do-decide.sh +476 -0
  150. package/scripts/do-derive-check.sh +436 -0
  151. package/scripts/do-fleet.sh +106 -28
  152. package/scripts/do-folder.sh +10 -1
  153. package/scripts/do-next.sh +106 -0
  154. package/scripts/do-orchestrate.sh +17 -5
  155. package/scripts/do-plan-json.mjs +201 -0
  156. package/scripts/do-plan-json.sh +8 -0
  157. package/scripts/do-preflight.sh +117 -0
  158. package/scripts/do-project.sh +157 -0
  159. package/scripts/do-prove-selftest.sh +108 -0
  160. package/scripts/do-prove.sh +295 -23
  161. package/scripts/do-rank.py +31 -1
  162. package/scripts/do-recon-cache.sh +7 -1
  163. package/scripts/do-recon-pack.sh +196 -0
  164. package/scripts/do-reconcile.sh +121 -4
  165. package/scripts/do-signal.sh +280 -23
  166. package/scripts/do-smoke.sh +18 -1
  167. package/scripts/do-test-gate.sh +80 -0
  168. package/scripts/do-tick.sh +102 -0
  169. package/scripts/do-tier.sh +6 -0
  170. package/scripts/do-triage.sh +182 -0
  171. package/scripts/do-ui-gate.sh +1 -1
  172. package/scripts/do-w4-gates.sh +451 -0
  173. package/scripts/do-walk.sh +12 -1
  174. package/scripts/env-sync.sh +173 -0
  175. package/scripts/factory-brief-check.sh +330 -0
  176. package/scripts/factory-check.sh +68 -14
  177. package/scripts/factory-close-check.sh +257 -0
  178. package/scripts/factory-emit.sh +211 -0
  179. package/scripts/factory-executor-check.mjs +353 -0
  180. package/scripts/factory-peak.sh +301 -0
  181. package/scripts/factory-repo.sh +118 -3
  182. package/scripts/factory-review-check.mjs +61 -0
  183. package/scripts/factory-ship.sh +61 -0
  184. package/scripts/factory-tasks-check.sh +18 -1
  185. package/scripts/factory-turn.sh +326 -0
  186. package/scripts/factory-walk.sh +396 -0
  187. package/scripts/factory-width.sh +57 -0
  188. package/scripts/fade-toxic.sh +4 -3
  189. package/scripts/fixtures/factory-brief-real.md +44 -0
  190. package/scripts/fixtures/triage-dupe.md +5 -0
  191. package/scripts/fleet-manifest.mjs +108 -0
  192. package/scripts/fleet-status.sh +110 -0
  193. package/scripts/full-suite-paths-check.sh +144 -0
  194. package/scripts/gate-reaper-check.sh +98 -0
  195. package/scripts/gate-reaper.sh +125 -0
  196. package/scripts/gate-run.sh +73 -0
  197. package/scripts/gc-content-check.sh +140 -0
  198. package/scripts/gen-dev-config.py +20 -0
  199. package/scripts/govern-bound-check.sh +60 -0
  200. package/scripts/govern-claims-check.sh +233 -0
  201. package/scripts/govern-mem-check.sh +290 -0
  202. package/scripts/governor-doors-check.sh +362 -0
  203. package/scripts/governor-escape-check.sh +171 -0
  204. package/scripts/health.sh +413 -0
  205. package/scripts/id-inventory.mjs +418 -0
  206. package/scripts/land.sh +551 -0
  207. package/scripts/lib/gc-finished.sh +77 -0
  208. package/scripts/lib/govern.sh +361 -0
  209. package/scripts/lib/govern.ts +756 -0
  210. package/scripts/lighthouse-run.sh +187 -0
  211. package/scripts/livekit-live-check.sh +61 -0
  212. package/scripts/livekit-ratchet.sh +46 -0
  213. package/scripts/load-guard-check.sh +49 -0
  214. package/scripts/machine-check.sh +102 -0
  215. package/scripts/machine-watch.sh +177 -0
  216. package/scripts/one-agents.mjs +415 -0
  217. package/scripts/one-resume.sh +78 -0
  218. package/scripts/orphan-baseline.json +182 -0
  219. package/scripts/orphan-modules.mjs +179 -0
  220. package/scripts/pr-body.sh +335 -0
  221. package/scripts/preview-fd-check.sh +289 -0
  222. package/scripts/promise-manifest.mjs +24 -1
  223. package/scripts/release.sh +322 -0
  224. package/scripts/roles-check.sh +946 -0
  225. package/scripts/sdk-build-cached.sh +64 -0
  226. package/scripts/signal-watch.sh +241 -0
  227. package/scripts/skills-publish.sh +94 -0
  228. package/scripts/speed-cache-check.sh +173 -0
  229. package/scripts/speed-check.mjs +907 -0
  230. package/scripts/speed-parity-check.sh +648 -0
  231. package/scripts/speed-waterfall-check.sh +355 -0
  232. package/scripts/substrate-env-parity.mjs +156 -0
  233. package/scripts/tasks-claim-race.mjs +108 -0
  234. package/scripts/tasks-loop.sh +185 -0
  235. package/scripts/test-cached.sh +255 -0
  236. package/scripts/test-full.sh +87 -0
  237. package/scripts/test-honesty.mjs +137 -0
  238. package/scripts/test-lanes.sh +166 -0
  239. package/scripts/test-speed.sh +94 -0
  240. package/scripts/triage-shape-check.sh +149 -0
  241. package/scripts/tsc-cached.sh +179 -0
  242. package/scripts/typedb-flake-check.sh +197 -0
  243. package/scripts/urls-lint.sh +15 -0
  244. package/scripts/verify-fast.sh +445 -0
  245. package/scripts/vespio-sync.sh +149 -0
  246. package/scripts/wf-check.mjs +104 -1
  247. package/scripts/worktree-preview.sh +879 -0
  248. package/scripts/worktree-up.sh +208 -0
  249. package/skills/livekit-agents/SKILL.md +285 -0
  250. package/skills/livekit-agents/references/freshness-rules.md +168 -0
  251. package/skills/shadcn/SKILL.md +1 -1
  252. package/skills/signal/SKILL.md +0 -1
  253. package/skills/voice/SKILL.md +94 -6
  254. package/skills/voice/corpus-check.sh +87 -0
  255. package/hooks/scripts/compact-hint.sh +0 -35
  256. package/hooks/scripts/gate-guard.sh +0 -83
  257. package/hooks/scripts/read-tracker.sh +0 -26
  258. package/hooks/scripts/session-end-verify.sh +0 -51
  259. package/hooks/scripts/stop-reflect.sh +0 -140
  260. package/hooks/scripts/sync-priority-todo.sh +0 -57
  261. package/hooks/scripts/sync-todo-docs.sh +0 -46
  262. package/hooks/scripts/tool-signal.sh +0 -48
  263. package/scripts/do-tasks-bridge.py +0 -366
@@ -0,0 +1,907 @@
1
+ #!/usr/bin/env node
2
+ // speed-check.mjs — the Lighthouse ratchet. "We should always be getting faster."
3
+ //
4
+ // WHY IT EXISTS: this repo HAD Lighthouse gates and they went dark. The runners
5
+ // (one.ie/web/scripts/lighthouse-{dashboard,in}.sh), the thresholds, the vitest
6
+ // tests and the npm dep all still exist — but both tests are
7
+ // `describe.skipIf(process.env.LIGHTHOUSE !== '1')` and LIGHTHOUSE=1 is set
8
+ // NOWHERE: not in package.json, not in either .github workflow, not in
9
+ // deploy.sh. So they skipped silently on every run for months and reported
10
+ // green — nobody could have told you whether the site got slower.
11
+ //
12
+ // That is the same failure as a cron whose expression is absent from
13
+ // wrangler.toml — registered in code, dark in practice. The fix is not "run
14
+ // Lighthouse", it is "make an unrun check impossible to mistake for a pass".
15
+ // Hence exit 2, everywhere, for anything that is not a real measurement.
16
+ //
17
+ // RATCHET DIRECTION (the actual ask): scores may not FALL, timings may not
18
+ // RISE. `--update` only ever moves a baseline in the improving direction; it
19
+ // will refuse to record a regression. A baseline you can rewrite on demand is
20
+ // fail-open, which is how the last one died.
21
+ //
22
+ // WHAT WE GATE ON, AND WHY (measured 2026-08-18, do not undo without redoing
23
+ // the measurement): the enforced ratchet runs against the PRODUCTION BUILD
24
+ // SERVED LOCALLY, not against https://one.ie.
25
+ //
26
+ // same page, 4-5 consecutive runs, identical code:
27
+ // prod over the internet ....... perf 62 · 64 · 91 · 94 (spread 32)
28
+ // localhost, prod build ........ perf 90 · 93 · 97 · 99 · 99 (spread 9)
29
+ //
30
+ // Remote measurement from a laptop is dominated by network and CDN jitter, in
31
+ // BOTH throttling modes — `simulate` still measures the real server response
32
+ // and only synthesises the transfer on top. A 32-point spread cannot ratchet
33
+ // anything: it is red on noise every other run, which is how the previous gate
34
+ // earned its reputation and got switched off. Localhost isolates the variable
35
+ // we can actually control and act on: our own code and bundle.
36
+ //
37
+ // (default) localhost prod build — ENFORCED. Committed baseline
38
+ // text/perf-baseline.json. Same code -> comparable everywhere.
39
+ // --prod https://one.ie — ADVISORY ONLY, never gates. Answers a
40
+ // different question (is the live site fast for users right now),
41
+ // which needs RUM or many samples over time, not one CI run.
42
+ //
43
+ // TOLERANCE: Lighthouse varies run to run on identical code even locally
44
+ // (90-99 above). Without a band this goes red on noise inside a week and
45
+ // someone disables it — exactly how it died before. Each route takes the BEST
46
+ // of N runs (default 3) and then still allows a small band. Best-of-N is the
47
+ // honest estimator: noise only ever makes a page look slower, never faster, so
48
+ // the best sample is the closest reading of what the code actually does.
49
+ //
50
+ // USAGE
51
+ // speed-check.mjs ENFORCED gate (localhost prod build)
52
+ // speed-check.mjs --prod advisory reading of the live site
53
+ // speed-check.mjs --update record improvements (refuses regressions)
54
+ // speed-check.mjs --update --force overwrite baseline regardless (rare;
55
+ // e.g. a deliberate feature that costs perf)
56
+ // speed-check.mjs --route /chat just one route
57
+ // speed-check.mjs --host mover.chat just the tenant row(s) for that host
58
+ // speed-check.mjs --runs 5 more samples per route
59
+ // speed-check.mjs --check-gate SELF-TEST: prove the lighthouse gate can go red
60
+ // speed-check.mjs --check-tenant-gate SELF-TEST: prove the tenant preamble gate
61
+ // (auth/det/ttfb via Server-Timing) can go red
62
+ //
63
+ // EXIT 0 pass (nothing regressed) · 1 REGRESSION · 2 INCONCLUSIVE (could not
64
+ // measure — never treat as green; callers must not map 2 to "skip")
65
+ //
66
+ // TWO MEASUREMENT KINDS, ONE GATE (added for the tenant-preamble gap — see
67
+ // text/speed-plan.md): a route's `kind` decides HOW it is measured, but the
68
+ // same METRICS table, regressed()/improved() logic, baseline shape and
69
+ // --update/--check flow apply to both, so this stays one mechanism, not two.
70
+ //
71
+ // kind: 'lighthouse' (default) a full-page Lighthouse audit (perf/a11y/…).
72
+ // kind: 'timing' a direct HTTP request (curl) reading the
73
+ // `Server-Timing` response header perf.ts/perf-middleware.ts already
74
+ // emit on every request. WHY NOT LIGHTHOUSE HERE: Lighthouse drives
75
+ // real Chrome navigation, and Chrome does not let a page override the
76
+ // `Host` header the way `mainMiddleware` needs to pick a tenant branch
77
+ // (host.startsWith('localhost') short-circuits to the PRIMARY branch —
78
+ // `src/middleware.ts` reads `ctx.request.headers.get('host')`, and every
79
+ // localhost/127.0.0.1 request IS that branch by definition, so Lighthouse
80
+ // against localhost:4331 can never reach the tenant preamble at all).
81
+ // A raw HTTP request has no such restriction — `curl -H "Host:
82
+ // mover.chat" http://localhost:4331/` makes middleware take the
83
+ // custom-domain branch while the socket still points at the local
84
+ // build, exactly like the curl-based measurements already cited in
85
+ // perf-middleware.ts's header comment. `--prod` measures the same kind
86
+ // by hitting the tenant's real host directly (no Host override needed).
87
+
88
+ import { execFileSync, spawn } from 'node:child_process';
89
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
90
+ import { tmpdir } from 'node:os';
91
+ import { dirname, join } from 'node:path';
92
+ import { fileURLToPath } from 'node:url';
93
+
94
+ const REPO = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
95
+ const RUNNER = join(REPO, '.claude', 'scripts', 'lighthouse-run.sh');
96
+
97
+ const argv = process.argv.slice(2);
98
+ const has = (f) => argv.includes(f);
99
+ const val = (f, d) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
100
+
101
+ const PROD = has('--prod'); // advisory reading of the live site
102
+ const LOCAL = !PROD; // default: the enforced localhost ratchet
103
+ const UPDATE = has('--update');
104
+ const FORCE = has('--force');
105
+ const CHECK_GATE = has('--check-gate');
106
+ const CHECK_TENANT_GATE = has('--check-tenant-gate');
107
+ const CHECK_TTFT_GATE = has('--check-ttft-gate');
108
+ const RUNS = Math.max(1, Number(val('--runs', '3')) || 3);
109
+ const ONLY = val('--route', null);
110
+ const ONLY_HOST = val('--host', null);
111
+
112
+ // The committed baseline is the LOCAL one — it is the one that means the same
113
+ // thing on every machine, because every machine builds the same code.
114
+ // SPEED_BASELINE_FILE lets a self-test point at a throwaway file instead of
115
+ // the committed one — --check-tenant-gate uses it so proving the gate can go
116
+ // red never touches text/perf-baseline.json.
117
+ const BASELINE = process.env.SPEED_BASELINE_FILE || (LOCAL
118
+ ? join(REPO, 'text', 'perf-baseline.json')
119
+ : join(REPO, 'text', 'perf-baseline.prod.json'));
120
+
121
+ // --- the route matrix -------------------------------------------------------
122
+ // Deliberately small. Every route here must render WITHOUT a session: a
123
+ // signed-out /u/<slug>/* 302s to /signin and Lighthouse will happily score the
124
+ // LOGIN PAGE at 99 and call it green (this repo's own do-prove landing rule
125
+ // exists because that exact bug scored routes as passing for months). Authed
126
+ // routes need the dev-session work do-prove.sh already does — until that is
127
+ // wired here, they stay out rather than ratchet a lie.
128
+ //
129
+ // `where` scopes a route to a mode. MOBILE IS PROD-ONLY, and that is a
130
+ // measured limitation, not laziness: mobile emulation (4x CPU throttle) against
131
+ // the local wrangler server returns runtimeError NO_FCP on every attempt, in
132
+ // both throttling methods, while the SAME mobile run against https://one.ie
133
+ // scores 65 and the SAME localhost server scores 98 on desktop. The server
134
+ // serves byte-identical HTML to both user agents, so this is a Lighthouse
135
+ // emulation/localhost interaction, not a bug in the page. Rather than ratchet a
136
+ // number we cannot reproduce, mobile lives in the advisory prod run — where it
137
+ // is also the weakest surface we have and most worth watching.
138
+ // Tenant `kind: 'timing'` rows (below) measure the preamble Server-Timing
139
+ // already emits — no `mode` (no Chrome involved), and `host` names the Host
140
+ // header to send (LOCAL) or the real host to hit directly (--prod). Measured
141
+ // 2026-08-19 prod, the reason these rows exist:
142
+ // one.ie/ auth 0.0ms det 0.0ms TTFB 0.32-0.43s
143
+ // app.one.ie/ auth 243-492ms det 1.9-2.5s TTFB 2.9-5.3s
144
+ // mover.chat/ auth 224-655ms det 1.9-3.0s TTFB 3.2-4.2s
145
+ const ROUTES = [
146
+ { path: '/', mode: 'desktop', where: 'both', kind: 'lighthouse', why: 'homepage — the public speed claim' },
147
+ { path: '/chat', mode: 'desktop', where: 'both', kind: 'lighthouse', why: 'the TTFT showcase surface' },
148
+ { path: '/', mode: 'mobile', where: 'prod', kind: 'lighthouse', why: 'homepage on mobile — the weakest surface' },
149
+ { path: '/', where: 'both', kind: 'timing', host: 'mover.chat',
150
+ why: 'tenant preamble (custom-domain rewrite) — a request must not re-derive what a publish already knew' },
151
+ { path: '/', where: 'both', kind: 'timing', host: 'app.one.ie',
152
+ why: 'tenant preamble (app-kind domain, subdomain branch) — same gate for the other tenant shape' },
153
+ // C2 (speed round 2): a real chat TURN, not the /chat page paint. `dd` (the
154
+ // start frame's data+dispatch phases — channels' own trace, all `det`, all
155
+ // BEFORE the model is called) is the gated metric; `ttfb` (total wall time
156
+ // to the SSE start) is recorded but `advisory` because it is hostage to
157
+ // model latency and flaps 3.1-7.4s on identical code — gating it would
158
+ // repeat the exact Lighthouse-spread failure this file's band table exists
159
+ // to prevent. See measureTurn() below for why `dd` alone is trustworthy.
160
+ { path: '/api/chat', where: 'both', kind: 'turn', advisory: ['ttfb'],
161
+ why: 'a chat TURN\'s deterministic prefix (data+dispatch) — the real TTFT showcase, replacing the page-paint proxy' },
162
+ ];
163
+
164
+ // Port 4331, not 4321: 4321 is `astro dev` (unoptimised, and often already
165
+ // running someone else's worktree). The ratchet must measure the BUILT output.
166
+ // cd one.ie/web && bun run build \
167
+ // && npx wrangler dev --config dist/server/wrangler.json --port 4331 --local
168
+ const PROD_ORIGIN = process.env.SPEED_ORIGIN || 'https://one.ie';
169
+ const LOCAL_ORIGIN = process.env.SPEED_ORIGIN || 'http://localhost:4331';
170
+ const ORIGIN = LOCAL ? LOCAL_ORIGIN : PROD_ORIGIN;
171
+
172
+ // Higher-is-better vs lower-is-better, and the noise band each gets.
173
+ // Bands are generous on timing (network jitter dominates) and tight on scores.
174
+ const METRICS = {
175
+ perf: { dir: 'up', band: 3, label: 'performance' },
176
+ a11y: { dir: 'up', band: 0, label: 'accessibility' },
177
+ bp: { dir: 'up', band: 3, label: 'best-practices' },
178
+ seo: { dir: 'up', band: 0, label: 'seo' },
179
+ lcp: { dir: 'down', band: 0.40, label: 'LCP', pct: true },
180
+ tbt: { dir: 'down', band: 0.50, label: 'TBT', pct: true, floor: 50 },
181
+ cls: { dir: 'down', band: 0.02, label: 'CLS' },
182
+ bytes: { dir: 'down', band: 0.10, label: 'page bytes', pct: true },
183
+ // `timing`-kind rows only. Bands are wide (50%) because Server-Timing on a
184
+ // cold isolate swings hard on D1/KV cold-start — the same reason the
185
+ // lighthouse bands above are generous on ms and tight on scores. `floor`
186
+ // absorbs a near-zero baseline the same way `tbt`'s does (a homepage row
187
+ // with auth=0 would otherwise regress on any nonzero reading at all).
188
+ auth: { dir: 'down', band: 0.50, label: 'auth phase', pct: true, floor: 20 },
189
+ det: { dir: 'down', band: 0.50, label: 'det total', pct: true, floor: 50 },
190
+ ttfb: { dir: 'down', band: 0.40, label: 'TTFB', pct: true, floor: 100 },
191
+ // `turn`-kind rows only (C2). Same 50%+floor shape as `det` above — a real
192
+ // chat turn's pre-model window swings on cold KV/Composio/D1 reads the same
193
+ // way the tenant preamble does (measured 567-890ms same code, same host).
194
+ dd: { dir: 'down', band: 0.50, label: 'chat det (data+dispatch)', pct: true, floor: 50 },
195
+ };
196
+
197
+ const key = (r) => `${r.path}::${r.mode ?? 'timing'}${r.host ? '::' + r.host : ''}`;
198
+ const routeLabel = (r) => `${r.path} (${r.host ?? r.mode ?? r.kind})`;
199
+ const c = { red: '\x1b[31m', grn: '\x1b[32m', yel: '\x1b[33m', dim: '\x1b[2m', off: '\x1b[0m' };
200
+
201
+ function parseLine(line) {
202
+ const out = {};
203
+ for (const tok of line.trim().split(/\s+/)) {
204
+ const [k, v] = tok.split('=');
205
+ if (k && v !== undefined) out[k] = Number(v);
206
+ }
207
+ return Number.isFinite(out.perf) ? out : null;
208
+ }
209
+
210
+ // One measurement = best-of-RUNS. Noise makes a page look slower, never faster,
211
+ // so the max score / min timing is the closest thing to the true value.
212
+ function measure(url, mode) {
213
+ const samples = [];
214
+ for (let i = 0; i < RUNS; i++) {
215
+ let stdout;
216
+ try {
217
+ stdout = execFileSync('bash', [RUNNER, url, mode], {
218
+ encoding: 'utf8', timeout: 180_000, stdio: ['ignore', 'pipe', 'pipe'],
219
+ });
220
+ } catch (e) {
221
+ // exit 2 from the runner = inconclusive; a single bad sample is ok if
222
+ // others land, so keep going and decide after the loop.
223
+ if (e.stderr) process.stderr.write(c.dim + String(e.stderr).trim() + c.off + '\n');
224
+ continue;
225
+ }
226
+ const p = parseLine(stdout);
227
+ if (p) samples.push(p);
228
+ }
229
+ if (!samples.length) return null;
230
+ const best = {};
231
+ for (const [m, spec] of Object.entries(METRICS)) {
232
+ const vals = samples.map((s) => s[m]).filter((v) => Number.isFinite(v) && v >= 0);
233
+ if (!vals.length) continue;
234
+ best[m] = spec.dir === 'up' ? Math.max(...vals) : Math.min(...vals);
235
+ }
236
+ best._samples = samples.length;
237
+ return best;
238
+ }
239
+
240
+ // Where a `timing`-kind route's request goes: LOCAL spoofs the Host header
241
+ // against the built server on :4331 (the only way to reach a non-primary
242
+ // middleware branch without owning DNS for the tenant host); --prod hits the
243
+ // tenant's real host directly, no override needed.
244
+ function timingTarget(r) {
245
+ if (LOCAL) return { url: LOCAL_ORIGIN.replace(/\/$/, '') + r.path, hostHeader: r.host };
246
+ return { url: `https://${r.host}${r.path}`, hostHeader: null };
247
+ }
248
+
249
+ // `Server-Timing: auth;dur=4.0;desc="det", det;dur=120.3;desc="deterministic total", …`
250
+ // -> { auth: 4, det: 120.3, … }. One regex per comma-separated token; anything
251
+ // that doesn't match `name;dur=N` is ignored rather than throwing — a header
252
+ // this parser can't fully read should degrade, not crash the whole route.
253
+ function parseServerTiming(headerLine) {
254
+ const out = {};
255
+ for (const part of headerLine.split(',')) {
256
+ const m = part.trim().match(/^([A-Za-z0-9_-]+);dur=([\d.]+)/);
257
+ if (m) out[m[1]] = Number(m[2]);
258
+ }
259
+ return out;
260
+ }
261
+
262
+ // One measurement = best-of-RUNS over a raw HTTP request, reading the
263
+ // `Server-Timing` header perf-middleware.ts already attaches to every
264
+ // response (see perf.ts). No Chrome, no Lighthouse — this is the mechanism
265
+ // that CAN see a spoofed Host header, which is the whole reason it exists.
266
+ function measureTiming(r) {
267
+ const { url, hostHeader } = timingTarget(r);
268
+ const samples = [];
269
+ for (let i = 0; i < RUNS; i++) {
270
+ const args = ['-s', '-D', '-', '-o', '/dev/null', '--max-time', '15', '-L'];
271
+ if (hostHeader) args.push('-H', `Host: ${hostHeader}`);
272
+ args.push('-w', '\nTTFB_S=%{time_starttransfer}\n', url);
273
+ let stdout;
274
+ try {
275
+ stdout = execFileSync('curl', args, { encoding: 'utf8', timeout: 20_000 });
276
+ } catch (e) {
277
+ if (e.stderr) process.stderr.write(c.dim + String(e.stderr).trim() + c.off + '\n');
278
+ continue;
279
+ }
280
+ const lines = stdout.split('\n');
281
+ const stLine = lines.find((l) => /^server-timing:/i.test(l));
282
+ const ttfbLine = lines.find((l) => l.startsWith('TTFB_S='));
283
+ if (!stLine) continue;
284
+ const parsed = parseServerTiming(stLine.replace(/^server-timing:\s*/i, ''));
285
+ if (ttfbLine) {
286
+ const s = Number(ttfbLine.slice('TTFB_S='.length));
287
+ if (Number.isFinite(s)) parsed.ttfb = s * 1000;
288
+ }
289
+ if (Number.isFinite(parsed.det)) samples.push(parsed);
290
+ }
291
+ if (!samples.length) return null;
292
+ const best = {};
293
+ for (const m of ['auth', 'det', 'ttfb']) {
294
+ const vals = samples.map((s) => s[m]).filter((v) => Number.isFinite(v) && v >= 0);
295
+ if (vals.length) best[m] = Math.min(...vals); // lower-is-better: noise only ever adds latency
296
+ }
297
+ best._samples = samples.length;
298
+ return best;
299
+ }
300
+
301
+ // A unique, non-cached probe text — never one of chat.ts's STARTER_PROMPTS.
302
+ // A starter prompt hits chat.ts's KV edge-cache short-circuit (T3-P11) and
303
+ // replays a PAST turn's cached SSE body verbatim, including its stale `start`
304
+ // frame — that would measure whatever the cache held, not a live turn.
305
+ const TURN_PROBE_TEXT = 'speed-check ttft probe — do not cache this turn';
306
+
307
+ // One measurement = best-of-RUNS over a REAL POST to /api/chat, reading the
308
+ // `start` frame's `messageMetadata.phases` (channels' own trace — see
309
+ // channels/src/lifecycle.ts PHASE_OF/tracePhases/visibleTrace). `dd` sums the
310
+ // `data`+`dispatch` phases, all `kind:'det'`, all emitted BEFORE the model is
311
+ // ever called — that is our half. `ttfb` is the total wall time to the first
312
+ // SSE byte, recorded for visibility but never compared (see `advisory` on the
313
+ // ROUTES entry) because it also carries the model's own latency.
314
+ function measureTurn(r) {
315
+ const url = ORIGIN.replace(/\/$/, '') + r.path;
316
+ const samples = [];
317
+ for (let i = 0; i < RUNS; i++) {
318
+ const body = JSON.stringify({
319
+ messages: [{ id: `speed-check-${Date.now()}-${i}`, role: 'user', parts: [{ type: 'text', text: TURN_PROBE_TEXT }] }],
320
+ });
321
+ const args = [
322
+ '-s', '-o', '-', '--max-time', '30', '-X', 'POST',
323
+ '-H', 'Content-Type: application/json',
324
+ '-H', `Referer: ${ORIGIN}/chat`,
325
+ '-d', body,
326
+ '-w', '\n__TTFB_S__=%{time_starttransfer}\n',
327
+ url,
328
+ ];
329
+ let stdout;
330
+ try {
331
+ stdout = execFileSync('curl', args, { encoding: 'utf8', timeout: 35_000 });
332
+ } catch (e) {
333
+ if (e.stderr) process.stderr.write(c.dim + String(e.stderr).trim() + c.off + '\n');
334
+ continue;
335
+ }
336
+ const lines = stdout.split('\n');
337
+ const ttfbLine = lines.find((l) => l.startsWith('__TTFB_S__='));
338
+ const startLine = lines.find((l) => l.startsWith('data: ') && l.includes('"type":"start"'));
339
+ if (!startLine) continue;
340
+ let phases;
341
+ try {
342
+ phases = JSON.parse(startLine.slice('data: '.length)).messageMetadata?.phases;
343
+ } catch {
344
+ continue;
345
+ }
346
+ if (!Array.isArray(phases)) continue;
347
+ const dataMs = phases.find((p) => p.name === 'data')?.ms ?? 0;
348
+ const dispatchMs = phases.find((p) => p.name === 'dispatch')?.ms ?? 0;
349
+ const sample = { dd: dataMs + dispatchMs };
350
+ if (ttfbLine) {
351
+ const s = Number(ttfbLine.slice('__TTFB_S__='.length));
352
+ if (Number.isFinite(s)) sample.ttfb = s * 1000;
353
+ }
354
+ samples.push(sample);
355
+ }
356
+ if (!samples.length) return null;
357
+ const best = {};
358
+ for (const m of ['dd', 'ttfb']) {
359
+ const vals = samples.map((s) => s[m]).filter((v) => Number.isFinite(v) && v >= 0);
360
+ if (vals.length) best[m] = Math.min(...vals); // lower-is-better: noise only ever adds latency
361
+ }
362
+ best._samples = samples.length;
363
+ return best;
364
+ }
365
+
366
+ // Is `now` worse than `base` beyond the noise band?
367
+ function regressed(metric, base, now) {
368
+ const s = METRICS[metric];
369
+ if (!s || !Number.isFinite(base) || !Number.isFinite(now)) return false;
370
+ if (s.dir === 'up') return now < base - s.band;
371
+ // lower-is-better
372
+ const allowed = s.pct ? base * (1 + s.band) + (s.floor ?? 0) : base + s.band;
373
+ return now > allowed;
374
+ }
375
+ function improved(metric, base, now) {
376
+ const s = METRICS[metric];
377
+ if (!s || !Number.isFinite(base) || !Number.isFinite(now)) return false;
378
+ return s.dir === 'up' ? now > base : now < base;
379
+ }
380
+
381
+ // --- self-test: prove the gate can go red -----------------------------------
382
+ // A checker that has never been observed failing is not evidence of anything.
383
+ if (CHECK_GATE) {
384
+ let bad = 0;
385
+ const t = (name, got, want) => {
386
+ const ok = got === want;
387
+ console.log(` ${ok ? c.grn + 'ok ' : c.red + 'FAIL'}${c.off} ${name} ${c.dim}(got ${got}, want ${want})${c.off}`);
388
+ if (!ok) bad++;
389
+ };
390
+ console.log('speed-check --check-gate — proving the ratchet bites\n');
391
+ t('perf 92 -> 80 is a regression', regressed('perf', 92, 80), true);
392
+ t('perf 92 -> 90 is inside the band', regressed('perf', 92, 90), false);
393
+ t('perf 92 -> 88 breaks the band', regressed('perf', 92, 88), true);
394
+ t('a11y 100 -> 99 regresses (band 0)', regressed('a11y', 100, 99), true);
395
+ t('LCP 1000 -> 2000 is a regression', regressed('lcp', 1000, 2000), true);
396
+ t('LCP 1000 -> 1200 inside 40% band', regressed('lcp', 1000, 1200), false);
397
+ t('TBT 0 -> 40 inside floor', regressed('tbt', 0, 40), false);
398
+ t('TBT 0 -> 500 regresses', regressed('tbt', 0, 500), true);
399
+ t('bytes 100k -> 130k regresses', regressed('bytes', 100_000, 130_000), true);
400
+ t('CLS 0 -> 0.2 regresses', regressed('cls', 0, 0.2), true);
401
+ t('perf 56 -> 61 is an improvement', improved('perf', 56, 61), true);
402
+ t('LCP 6600 -> 3900 is an improvement', improved('lcp', 6600, 3900), true);
403
+ // THE ROT ITSELF, tested for real: a run that can measure nothing must exit
404
+ // 2, never 0. This spawns the actual script against a dead origin — not a
405
+ // tautology about the function's type.
406
+ let unmeasurableExit = null;
407
+ try {
408
+ execFileSync(process.execPath, [fileURLToPath(import.meta.url), '--runs', '1'], {
409
+ env: { ...process.env, SPEED_ORIGIN: 'http://127.0.0.1:1' },
410
+ encoding: 'utf8', timeout: 120_000, stdio: ['ignore', 'pipe', 'pipe'],
411
+ });
412
+ unmeasurableExit = 0;
413
+ } catch (e) {
414
+ unmeasurableExit = e.status ?? -1;
415
+ }
416
+ t('unmeasurable run exits 2, not 0', unmeasurableExit, 2);
417
+
418
+ // THE --prod EXIT-CODE ROT ITSELF, tested for real: deploy.sh's Step 9 was
419
+ // observed printing a red "SLOWER" block immediately followed by a green
420
+ // "speed: nothing got slower (live site)" tick, because a --prod regression
421
+ // and a --prod clean run both exited 0. Fixed by exit 3 = "regression
422
+ // detected, advisory, not gated"; exit 0 stays reserved for "measured
423
+ // clean". Proved against a real spawned child hitting a real local Chrome
424
+ // via lighthouse-run.sh — not a stub of the exit-code function — with a
425
+ // synthetic baseline so the outcome is deterministic either way.
426
+ {
427
+ const tmp = mkdtempSync(join(tmpdir(), 'speed-prod-exit-'));
428
+ // The fake page server runs as a SEPARATE CHILD PROCESS, not an in-process
429
+ // http.createServer() — same reason the other self-tests' fake servers do
430
+ // below: this self-test's own measurement calls are execFileSync, which
431
+ // blocks this process's event loop for the whole subprocess run, so an
432
+ // in-process server cannot accept a connection while this thread waits.
433
+ //
434
+ // lighthouse-run.sh also treats a page under 100000 total-byte-weight as a
435
+ // partial load ("INCONCLUSIVE: partial load") rather than a real
436
+ // measurement — pad the body well past that floor so the runner scores a
437
+ // real page instead of bailing before it ever reaches the exit-code
438
+ // branch this test exists to prove.
439
+ const serverScript = join(tmp, 'fake-page-server.mjs');
440
+ writeFileSync(serverScript, [
441
+ "import { createServer } from 'node:http';",
442
+ "const pad = '<!-- pad -->'.repeat(10000);",
443
+ 'const server = createServer((_req, res) => {',
444
+ " res.setHeader('content-type', 'text/html');",
445
+ " res.end('<!doctype html><html><head><title>t</title>' +",
446
+ " '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head>' +",
447
+ " `<body><h1>speed-check self-test page</h1>${pad}</body></html>`);",
448
+ '});',
449
+ "server.listen(0, '127.0.0.1', () => console.log(`PORT=${server.address().port}`));",
450
+ ].join('\n'));
451
+
452
+ const startServer = () => new Promise((resolve, reject) => {
453
+ const proc = spawn(process.execPath, [serverScript], { stdio: ['ignore', 'pipe', 'pipe'] });
454
+ let buf = '';
455
+ const timer = setTimeout(
456
+ () => reject(new Error('fake page server did not report a port in time')), 5000,
457
+ );
458
+ proc.stdout.on('data', (d) => {
459
+ buf += d.toString();
460
+ const m = buf.match(/PORT=(\d+)/);
461
+ if (m) { clearTimeout(timer); resolve({ proc, port: Number(m[1]) }); }
462
+ });
463
+ proc.once('error', (e) => { clearTimeout(timer); reject(e); });
464
+ });
465
+
466
+ let server = null;
467
+ try {
468
+ server = await startServer();
469
+ const origin = `http://127.0.0.1:${server.port}`;
470
+
471
+ // Baseline the /chat::desktop route cannot possibly meet: lcp=1ms is
472
+ // guaranteed to regress (dir:'down', 40% band) no matter how fast the
473
+ // synthetic page loads, so the regression branch is deterministic.
474
+ const regressedBaseline = join(tmp, 'regressed-baseline.json');
475
+ writeFileSync(regressedBaseline, JSON.stringify({
476
+ routes: { '/chat::desktop': { perf: 100, a11y: 100, bp: 100, seo: 100, lcp: 1, tbt: 1, cls: 0, bytes: 1 } },
477
+ }));
478
+ let prodRegressExit = null;
479
+ try {
480
+ execFileSync(process.execPath, [
481
+ fileURLToPath(import.meta.url), '--prod', '--runs', '1', '--route', '/chat',
482
+ ], {
483
+ env: { ...process.env, SPEED_ORIGIN: origin, SPEED_BASELINE_FILE: regressedBaseline },
484
+ encoding: 'utf8', timeout: 180_000, stdio: ['ignore', 'pipe', 'pipe'],
485
+ });
486
+ prodRegressExit = 0;
487
+ } catch (e) {
488
+ prodRegressExit = e.status ?? -1;
489
+ }
490
+ t('--prod run with a forced regression exits 3, not 0', prodRegressExit, 3);
491
+
492
+ // Baseline no real measurement can beat downward (huge timings) and
493
+ // cannot regress upward from (zero scores) — the clean-run control.
494
+ const cleanBaseline = join(tmp, 'clean-baseline.json');
495
+ writeFileSync(cleanBaseline, JSON.stringify({
496
+ routes: { '/chat::desktop': { perf: 0, a11y: 0, bp: 0, seo: 0, lcp: 1e9, tbt: 1e9, cls: 1e9, bytes: 1e9 } },
497
+ }));
498
+ let prodCleanExit = null;
499
+ try {
500
+ execFileSync(process.execPath, [
501
+ fileURLToPath(import.meta.url), '--prod', '--runs', '1', '--route', '/chat',
502
+ ], {
503
+ env: { ...process.env, SPEED_ORIGIN: origin, SPEED_BASELINE_FILE: cleanBaseline },
504
+ encoding: 'utf8', timeout: 180_000, stdio: ['ignore', 'pipe', 'pipe'],
505
+ });
506
+ prodCleanExit = 0;
507
+ } catch (e) {
508
+ prodCleanExit = e.status ?? -1;
509
+ }
510
+ t('--prod run with nothing to regress against exits 0', prodCleanExit, 0);
511
+ } finally {
512
+ if (server?.proc) server.proc.kill();
513
+ rmSync(tmp, { recursive: true, force: true });
514
+ }
515
+ }
516
+
517
+ console.log(bad === 0
518
+ ? `\n${c.grn}gate proven: it fails on regressions and passes on improvements${c.off}`
519
+ : `\n${c.red}${bad} self-test(s) FAILED — the gate is not trustworthy${c.off}`);
520
+ process.exit(bad === 0 ? 0 : 1);
521
+ }
522
+
523
+ // --- self-test: prove the TENANT preamble gate can go red -------------------
524
+ // Same idea as --check-gate, but for `kind: 'timing'` rows: a synthetic HTTP
525
+ // server stands in for the tenant host so this proves the real round trip
526
+ // (curl -> Server-Timing header -> parseServerTiming -> regressed() -> exit
527
+ // code) without needing a live wrangler build on :4331.
528
+ if (CHECK_TENANT_GATE) {
529
+ let bad = 0;
530
+ const t = (name, got, want) => {
531
+ const ok = got === want;
532
+ console.log(` ${ok ? c.grn + 'ok ' : c.red + 'FAIL'}${c.off} ${name} ${c.dim}(got ${got}, want ${want})${c.off}`);
533
+ if (!ok) bad++;
534
+ };
535
+ console.log('speed-check --check-tenant-gate — proving the tenant preamble gate bites\n');
536
+
537
+ // Pure regressed()/improved() coverage for the three new metrics.
538
+ t('auth 50 -> 500 is a regression (10x, past 50% band+floor)', regressed('auth', 50, 500), true);
539
+ t('auth 50 -> 60 is inside the band', regressed('auth', 50, 60), false);
540
+ t('det 200 -> 2000 is a regression', regressed('det', 200, 2000), true);
541
+ t('det 200 -> 280 is inside the band', regressed('det', 200, 280), false);
542
+ t('ttfb 400 -> 4000 is a regression', regressed('ttfb', 400, 4000), true);
543
+ t('auth 0 -> 15 stays inside the floor', regressed('auth', 0, 15), false);
544
+ t('det 2000 -> 200 is an improvement', improved('det', 2000, 200), true);
545
+
546
+ // parseServerTiming reads the real header shape perf.ts (Timer.header())
547
+ // emits: raw phase marks plus the `det`/`llm`/`chain` roll-ups.
548
+ const parsed = parseServerTiming(
549
+ 'auth;dur=243.5;desc="det", data;dur=1900.2;desc="det", ' +
550
+ 'det;dur=2143.7;desc="deterministic total", render;dur=12.0;desc="det"',
551
+ );
552
+ t('parseServerTiming reads the auth phase', parsed.auth, 243.5);
553
+ t('parseServerTiming reads the det roll-up', parsed.det, 2143.7);
554
+
555
+ // THE ROT ITSELF, tested for real: a synthetic tenant server, measured
556
+ // through the actual CLI (spawned as a child, exactly like --check-gate's
557
+ // unmeasurable-run test), must PASS when timings match the baseline and
558
+ // REGRESS when they blow past it. Asserted on the full curl -> parse ->
559
+ // compare -> exit-code path, never on a function in isolation.
560
+ let passExit = null;
561
+ let regressExit = null;
562
+ // The fake tenant server runs as a SEPARATE CHILD PROCESS, not an in-process
563
+ // http.createServer(). This self-test's own measurement calls are
564
+ // execFileSync — synchronous, so they block this process's event loop for
565
+ // their whole duration. An in-process server can't accept a connection
566
+ // while its own thread is blocked waiting on that subprocess (verified:
567
+ // curl against an in-process server hung to its --max-time ceiling on every
568
+ // attempt, 0 bytes received, until it was moved out of process).
569
+ const tmp = mkdtempSync(join(tmpdir(), 'speed-tenant-gate-'));
570
+ const baselineFile = join(tmp, 'baseline.json');
571
+ const serverScript = join(tmp, 'fake-tenant-server.mjs');
572
+ let child = null;
573
+ try {
574
+ writeFileSync(serverScript, [
575
+ "import { createServer } from 'node:http';",
576
+ 'const [authMs, detMs] = process.argv.slice(2).map(Number);',
577
+ 'const server = createServer((_req, res) => {',
578
+ ' res.setHeader(',
579
+ " 'Server-Timing',",
580
+ " `auth;dur=${authMs};desc=\"det\", det;dur=${detMs};desc=\"deterministic total\"`,",
581
+ ' );',
582
+ " res.end('ok');",
583
+ '});',
584
+ "server.listen(0, '127.0.0.1', () => console.log(`PORT=${server.address().port}`));",
585
+ ].join('\n'));
586
+
587
+ const startServer = (authMs, detMs) => new Promise((resolve, reject) => {
588
+ const proc = spawn(process.execPath, [serverScript, String(authMs), String(detMs)], {
589
+ stdio: ['ignore', 'pipe', 'pipe'],
590
+ });
591
+ let buf = '';
592
+ const timer = setTimeout(
593
+ () => reject(new Error('fake tenant server did not report a port in time')), 5000,
594
+ );
595
+ const onData = (d) => {
596
+ buf += d.toString();
597
+ const m = buf.match(/PORT=(\d+)/);
598
+ if (m) { clearTimeout(timer); proc.stdout.off('data', onData); resolve({ proc, port: Number(m[1]) }); }
599
+ };
600
+ proc.stdout.on('data', onData);
601
+ proc.once('error', reject);
602
+ });
603
+
604
+ const runOnce = (port) => {
605
+ try {
606
+ execFileSync(process.execPath, [
607
+ fileURLToPath(import.meta.url), '--route', '/', '--host', 'mover.chat', '--runs', '1',
608
+ ], {
609
+ env: { ...process.env, SPEED_ORIGIN: `http://127.0.0.1:${port}`, SPEED_BASELINE_FILE: baselineFile },
610
+ encoding: 'utf8', timeout: 60_000, stdio: ['ignore', 'pipe', 'pipe'],
611
+ });
612
+ return 0;
613
+ } catch (e) {
614
+ return e.status ?? -1;
615
+ }
616
+ };
617
+
618
+ // Baseline written ONCE, at 50/200, and never rewritten — the whole test
619
+ // is whether a measurement against that fixed baseline passes or fails.
620
+ writeFileSync(baselineFile, JSON.stringify({
621
+ _mode: 'local-build', _origin: 'http://127.0.0.1:0',
622
+ routes: {
623
+ '/::timing::mover.chat': { auth: 50, det: 200, _measured: 'test', _why: 'check-tenant-gate self-test' },
624
+ },
625
+ }));
626
+
627
+ const passServer = await startServer(50, 200);
628
+ child = passServer.proc;
629
+ passExit = runOnce(passServer.port);
630
+ child.kill();
631
+
632
+ // 10x — well past the 50% band + floor on both auth and det.
633
+ const regressServer = await startServer(500, 2000);
634
+ child = regressServer.proc;
635
+ regressExit = runOnce(regressServer.port); // same baseline file, server now answers 10x slower
636
+ child.kill();
637
+ child = null;
638
+ } finally {
639
+ if (child) child.kill();
640
+ rmSync(tmp, { recursive: true, force: true });
641
+ }
642
+ t('matching timings pass (exit 0)', passExit, 0);
643
+ t('10x-inflated timings regress (exit 1)', regressExit, 1);
644
+
645
+ console.log(bad === 0
646
+ ? `\n${c.grn}tenant gate proven: it fails on a preamble regression and passes on matching timings${c.off}`
647
+ : `\n${c.red}${bad} self-test(s) FAILED — the tenant gate is not trustworthy${c.off}`);
648
+ process.exit(bad === 0 ? 0 : 1);
649
+ }
650
+
651
+ // --- self-test: prove the chat TURN (TTFT) gate can go red ------------------
652
+ // Same idea as --check-tenant-gate, but for the `turn`-kind row: a synthetic
653
+ // HTTP server stands in for /api/chat and answers a POST with a real SSE
654
+ // `start` frame carrying a chosen `data`+`dispatch` value, so this proves the
655
+ // real path (curl POST -> parse SSE -> measureTurn -> regressed() -> exit
656
+ // code) without ever hitting the live model or spending a real credit.
657
+ if (CHECK_TTFT_GATE) {
658
+ let bad = 0;
659
+ const t = (name, got, want) => {
660
+ const ok = got === want;
661
+ console.log(` ${ok ? c.grn + 'ok ' : c.red + 'FAIL'}${c.off} ${name} ${c.dim}(got ${got}, want ${want})${c.off}`);
662
+ if (!ok) bad++;
663
+ };
664
+ console.log('speed-check --check-ttft-gate — proving the chat-turn gate bites\n');
665
+
666
+ // Pure regressed()/improved() coverage for the new `dd` metric.
667
+ t('dd 100 -> 1000 is a regression (10x, past 50% band+floor)', regressed('dd', 100, 1000), true);
668
+ t('dd 100 -> 130 is inside the band', regressed('dd', 100, 130), false);
669
+ t('dd 1000 -> 100 is an improvement', improved('dd', 1000, 100), true);
670
+
671
+ let child = null;
672
+ let passExit = null;
673
+ let regressExit = null;
674
+ const tmp = mkdtempSync(join(tmpdir(), 'speed-ttft-gate-'));
675
+ const baselineFile = join(tmp, 'baseline.json');
676
+ const serverScript = join(tmp, 'fake-chat-server.mjs');
677
+ try {
678
+ writeFileSync(serverScript, [
679
+ "import { createServer } from 'node:http';",
680
+ 'const [ddMs] = process.argv.slice(2).map(Number);',
681
+ 'const server = createServer((_req, res) => {',
682
+ " res.setHeader('Content-Type', 'text/event-stream');",
683
+ ' const frame = { type: "start", messageMetadata: { phases: [',
684
+ ' { name: "data", kind: "det", ms: ddMs }, { name: "dispatch", kind: "det", ms: 0 },',
685
+ ' ] } };',
686
+ " res.end('data: ' + JSON.stringify(frame) + '\\n\\n');",
687
+ '});',
688
+ "server.listen(0, '127.0.0.1', () => console.log(`PORT=${server.address().port}`));",
689
+ ].join('\n'));
690
+
691
+ const startServer = (ddMs) => new Promise((resolve, reject) => {
692
+ const proc = spawn(process.execPath, [serverScript, String(ddMs)], {
693
+ stdio: ['ignore', 'pipe', 'pipe'],
694
+ });
695
+ let buf = '';
696
+ const timer = setTimeout(
697
+ () => reject(new Error('fake chat server did not report a port in time')), 5000,
698
+ );
699
+ const onData = (d) => {
700
+ buf += d.toString();
701
+ const m = buf.match(/PORT=(\d+)/);
702
+ if (m) { clearTimeout(timer); proc.stdout.off('data', onData); resolve({ proc, port: Number(m[1]) }); }
703
+ };
704
+ proc.stdout.on('data', onData);
705
+ proc.once('error', reject);
706
+ });
707
+
708
+ const runOnce = (port) => {
709
+ try {
710
+ execFileSync(process.execPath, [
711
+ fileURLToPath(import.meta.url), '--route', '/api/chat', '--runs', '1',
712
+ ], {
713
+ env: { ...process.env, SPEED_ORIGIN: `http://127.0.0.1:${port}`, SPEED_BASELINE_FILE: baselineFile },
714
+ encoding: 'utf8', timeout: 60_000, stdio: ['ignore', 'pipe', 'pipe'],
715
+ });
716
+ return 0;
717
+ } catch (e) {
718
+ return e.status ?? -1;
719
+ }
720
+ };
721
+
722
+ // Baseline written ONCE, at dd=100, ttfb omitted (advisory-only — it must
723
+ // never gate even if a base value existed).
724
+ writeFileSync(baselineFile, JSON.stringify({
725
+ _mode: 'local-build', _origin: 'http://127.0.0.1:0',
726
+ routes: {
727
+ '/api/chat::timing': { dd: 100, _measured: 'test', _why: 'check-ttft-gate self-test' },
728
+ },
729
+ }));
730
+
731
+ const passServer = await startServer(100);
732
+ child = passServer.proc;
733
+ passExit = runOnce(passServer.port);
734
+ child.kill();
735
+
736
+ // 10x on `dd` — well past the 50% band + floor.
737
+ const regressServer = await startServer(1000);
738
+ child = regressServer.proc;
739
+ regressExit = runOnce(regressServer.port); // same baseline file, server now answers 10x slower
740
+ child.kill();
741
+ child = null;
742
+ } finally {
743
+ if (child) child.kill();
744
+ rmSync(tmp, { recursive: true, force: true });
745
+ }
746
+ t('matching dd passes (exit 0)', passExit, 0);
747
+ t('10x-inflated dd regresses (exit 1)', regressExit, 1);
748
+
749
+ console.log(bad === 0
750
+ ? `\n${c.grn}ttft gate proven: it fails on a dd regression and passes on matching timings; ttfb never gates${c.off}`
751
+ : `\n${c.red}${bad} self-test(s) FAILED — the ttft gate is not trustworthy${c.off}`);
752
+ process.exit(bad === 0 ? 0 : 1);
753
+ }
754
+
755
+ // --- main -------------------------------------------------------------------
756
+ const baseline = existsSync(BASELINE)
757
+ ? JSON.parse(readFileSync(BASELINE, 'utf8'))
758
+ : { _mode: LOCAL ? 'local-build' : 'live-site', _origin: ORIGIN, routes: {} };
759
+ baseline.routes ??= {};
760
+
761
+ const applicable = ROUTES.filter((r) => r.where === 'both' || r.where === (LOCAL ? 'local' : 'prod'));
762
+ const byPath = ONLY ? applicable.filter((r) => r.path === ONLY) : applicable;
763
+ const targets = ONLY_HOST ? byPath.filter((r) => r.host === ONLY_HOST) : byPath;
764
+ if (!targets.length) {
765
+ console.error(`no route matching path=${ONLY ?? '*'} host=${ONLY_HOST ?? '*'}. Known: ${ROUTES.map((r) => r.host ? `${r.path}@${r.host}` : r.path).join(', ')}`);
766
+ process.exit(2);
767
+ }
768
+
769
+ console.log(`speed-check ${LOCAL ? '(local build, ENFORCED)' : '(live site, ADVISORY)'} — ${ORIGIN}`);
770
+ console.log(`${c.dim}best of ${RUNS} runs per route · baseline ${BASELINE.replace(REPO + '/', '')}${c.off}\n`);
771
+
772
+ const results = [];
773
+ let measuredNone = true;
774
+
775
+ for (const r of targets) {
776
+ const isTiming = r.kind === 'timing';
777
+ const isTurn = r.kind === 'turn';
778
+ const label = isTiming ? `${r.path} ${c.dim}(${r.host})${c.off}`
779
+ : isTurn ? `${r.path} ${c.dim}(turn)${c.off}`
780
+ : `${r.path} ${c.dim}(${r.mode})${c.off}`;
781
+ process.stdout.write(`${label} … `);
782
+ const now = isTiming ? measureTiming(r) : isTurn ? measureTurn(r) : measure(ORIGIN.replace(/\/$/, '') + r.path, r.mode);
783
+ if (!now) { console.log(`${c.yel}INCONCLUSIVE${c.off}`); results.push({ r, now: null }); continue; }
784
+ measuredNone = false;
785
+ const base = baseline.routes[key(r)];
786
+ console.log(
787
+ isTiming
788
+ ? `auth=${now.auth ?? '-'}ms det=${now.det ?? '-'}ms ttfb=${Number.isFinite(now.ttfb) ? Math.round(now.ttfb) : '-'}ms ${c.dim}(${now._samples}/${RUNS} samples)${c.off}`
789
+ : isTurn
790
+ ? `dd=${now.dd ?? '-'}ms ttfb=${Number.isFinite(now.ttfb) ? Math.round(now.ttfb) : '-'}ms (advisory) ${c.dim}(${now._samples}/${RUNS} samples)${c.off}`
791
+ : `perf=${now.perf} a11y=${now.a11y} lcp=${now.lcp}ms tbt=${now.tbt}ms ` +
792
+ `cls=${now.cls} ${Math.round(now.bytes / 1024)}KB ${c.dim}(${now._samples}/${RUNS} samples)${c.off}`
793
+ );
794
+ results.push({ r, now, base });
795
+ }
796
+
797
+ if (measuredNone) {
798
+ console.error(`\n${c.yel}INCONCLUSIVE — nothing could be measured. This is NOT a pass.${c.off}`);
799
+ console.error(LOCAL
800
+ ? 'Local build server not up. Start it with:\n' +
801
+ ' cd one.ie/web && bun run build && \\\n' +
802
+ ' npx wrangler dev --config dist/server/wrangler.json --port 4331 --local'
803
+ : `Is ${ORIGIN} reachable?`);
804
+ process.exit(2);
805
+ }
806
+
807
+ // --- compare ----------------------------------------------------------------
808
+ const regressions = [];
809
+ const improvements = [];
810
+ for (const { r, now, base } of results) {
811
+ if (!now) continue;
812
+ if (!base) { console.log(`${c.dim} ${routeLabel(r)}: no baseline yet${c.off}`); continue; }
813
+ for (const m of Object.keys(METRICS)) {
814
+ // C2: a route's `advisory` list names metrics that are recorded but never
815
+ // gated — the chat turn's total `ttfb` is hostage to model latency, and
816
+ // gating it would repeat the exact Lighthouse-spread failure this file's
817
+ // band table exists to prevent (see the ROUTES comment on the turn row).
818
+ if (r.advisory?.includes(m)) continue;
819
+ if (regressed(m, base[m], now[m])) {
820
+ regressions.push({ r, m, from: base[m], to: now[m] });
821
+ } else if (improved(m, base[m], now[m])) {
822
+ improvements.push({ r, m, from: base[m], to: now[m] });
823
+ }
824
+ }
825
+ }
826
+
827
+ if (improvements.length) {
828
+ console.log(`\n${c.grn}faster${c.off}`);
829
+ for (const i of improvements) {
830
+ console.log(` ${routeLabel(i.r)} ${METRICS[i.m].label}: ${i.from} → ${c.grn}${i.to}${c.off}`);
831
+ }
832
+ }
833
+ if (regressions.length) {
834
+ console.log(`\n${c.red}SLOWER — beyond the noise band${c.off}`);
835
+ for (const g of regressions) {
836
+ console.log(` ${routeLabel(g.r)} ${METRICS[g.m].label}: ${g.from} → ${c.red}${g.to}${c.off}`);
837
+ }
838
+ }
839
+
840
+ // --- update -----------------------------------------------------------------
841
+ if (UPDATE) {
842
+ if (regressions.length && !FORCE) {
843
+ console.error(
844
+ `\n${c.red}refusing to record a regression.${c.off} The ratchet only moves one way.\n` +
845
+ `If this slowdown is deliberate, re-run with --force and say why in the commit.`
846
+ );
847
+ process.exit(1);
848
+ }
849
+ let written = 0;
850
+ for (const { r, now, base } of results) {
851
+ if (!now) continue;
852
+ const next = { ...(base ?? {}) };
853
+ for (const m of Object.keys(METRICS)) {
854
+ if (!Number.isFinite(now[m])) continue;
855
+ // improve-only, unless forced
856
+ if (base === undefined || FORCE || improved(m, base[m], now[m]) || !Number.isFinite(base[m])) {
857
+ next[m] = now[m];
858
+ }
859
+ }
860
+ next._measured = new Date().toISOString().slice(0, 10);
861
+ next._why = r.why;
862
+ baseline.routes[key(r)] = next;
863
+ written++;
864
+ }
865
+ baseline._mode = LOCAL ? 'local-build' : 'live-site';
866
+ baseline._origin = ORIGIN;
867
+ baseline._runs = RUNS;
868
+ baseline._bands = Object.fromEntries(
869
+ Object.entries(METRICS).map(([m, s]) => [m, s.pct ? `${s.band * 100}%` : s.band])
870
+ );
871
+ writeFileSync(BASELINE, JSON.stringify(baseline, null, 2) + '\n');
872
+ console.log(`\n${c.grn}baseline updated${c.off} — ${written} route(s) → ${BASELINE.replace(REPO + '/', '')}`);
873
+ process.exit(0);
874
+ }
875
+
876
+ // A missing baseline must not read as "pass" — that is fail-open, the same
877
+ // class of bug as a skipped test reporting green. Seed it explicitly.
878
+ const unbaselined = results.filter((r) => r.now && !r.base);
879
+ if (!UPDATE && unbaselined.length && LOCAL) {
880
+ console.error(
881
+ `\n${c.yel}INCONCLUSIVE — ${unbaselined.length} route(s) have no baseline.${c.off}\n` +
882
+ `Nothing to ratchet against, so this is not a pass. Seed it with:\n` +
883
+ ` cd one.ie/web && bun run speed:update`
884
+ );
885
+ process.exit(2);
886
+ }
887
+
888
+ if (regressions.length) {
889
+ if (PROD) {
890
+ console.log(
891
+ `\n${c.yel}advisory only (--prod) — not gating.${c.off} Live-site numbers swing ` +
892
+ `~30 points on network jitter; treat this as a signal to investigate, not a failure.`
893
+ );
894
+ // Exit 3, NOT 0. "I found a regression and am not gating on it" and "nothing
895
+ // got slower" are different statements, and exiting 0 let deploy.sh collapse
896
+ // them into `ok speed: nothing got slower (live site)` — printed directly
897
+ // beneath this script's own SLOWER block, in the same log, twice on
898
+ // 2026-08-26 while / (mobile) went 86 -> 82. A caller cannot be trusted to
899
+ // read prose; the exit code has to carry the distinction.
900
+ process.exit(3);
901
+ }
902
+ console.error(`\n${c.red}FAIL: ${regressions.length} regression(s). We are meant to be getting faster.${c.off}`);
903
+ process.exit(1);
904
+ }
905
+
906
+ console.log(`\n${c.grn}pass — nothing got slower${c.off}`);
907
+ process.exit(0);