opencode-skills-collection 3.0.35 → 3.0.36

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 (238) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +15 -1
  2. package/bundled-skills/accesslint-audit/SKILL.md +115 -0
  3. package/bundled-skills/accesslint-diff/SKILL.md +81 -0
  4. package/bundled-skills/accesslint-scan/SKILL.md +47 -0
  5. package/bundled-skills/composition-patterns/SKILL.md +87 -0
  6. package/bundled-skills/composition-patterns/rules/_sections.md +29 -0
  7. package/bundled-skills/composition-patterns/rules/_template.md +24 -0
  8. package/bundled-skills/composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
  9. package/bundled-skills/composition-patterns/rules/architecture-compound-components.md +112 -0
  10. package/bundled-skills/composition-patterns/rules/patterns-children-over-render-props.md +87 -0
  11. package/bundled-skills/composition-patterns/rules/patterns-explicit-variants.md +100 -0
  12. package/bundled-skills/composition-patterns/rules/react19-no-forwardref.md +42 -0
  13. package/bundled-skills/composition-patterns/rules/state-context-interface.md +191 -0
  14. package/bundled-skills/composition-patterns/rules/state-decouple-implementation.md +113 -0
  15. package/bundled-skills/composition-patterns/rules/state-lift-state.md +125 -0
  16. package/bundled-skills/debugging-toolkit/SKILL.md +35 -0
  17. package/bundled-skills/deploy-to-vercel/SKILL.md +304 -0
  18. package/bundled-skills/deploy-to-vercel/resources/deploy-codex.sh +301 -0
  19. package/bundled-skills/deploy-to-vercel/resources/deploy.sh +301 -0
  20. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  21. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  22. package/bundled-skills/docs/maintainers/backups/README-2026-06-02.md +687 -0
  23. package/bundled-skills/docs/maintainers/repo-growth-seo.md +4 -4
  24. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  25. package/bundled-skills/docs/users/bundles.md +245 -1
  26. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  27. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  28. package/bundled-skills/docs/users/getting-started.md +1 -1
  29. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  30. package/bundled-skills/docs/users/plugins.md +21 -13
  31. package/bundled-skills/docs/users/specialized-plugin-roadmap.md +95 -0
  32. package/bundled-skills/docs/users/usage.md +4 -4
  33. package/bundled-skills/docs/users/visual-guide.md +4 -4
  34. package/bundled-skills/polis-protocol/SKILL.md +93 -0
  35. package/bundled-skills/python-development/SKILL.md +35 -0
  36. package/bundled-skills/radix-ui-design-system/SKILL.md +2 -2
  37. package/bundled-skills/react-native-skills/SKILL.md +120 -0
  38. package/bundled-skills/react-native-skills/rules/_sections.md +86 -0
  39. package/bundled-skills/react-native-skills/rules/_template.md +28 -0
  40. package/bundled-skills/react-native-skills/rules/animation-derived-value.md +53 -0
  41. package/bundled-skills/react-native-skills/rules/animation-gesture-detector-press.md +95 -0
  42. package/bundled-skills/react-native-skills/rules/animation-gpu-properties.md +65 -0
  43. package/bundled-skills/react-native-skills/rules/design-system-compound-components.md +66 -0
  44. package/bundled-skills/react-native-skills/rules/fonts-config-plugin.md +71 -0
  45. package/bundled-skills/react-native-skills/rules/imports-design-system-folder.md +68 -0
  46. package/bundled-skills/react-native-skills/rules/js-hoist-intl.md +61 -0
  47. package/bundled-skills/react-native-skills/rules/list-performance-callbacks.md +44 -0
  48. package/bundled-skills/react-native-skills/rules/list-performance-function-references.md +132 -0
  49. package/bundled-skills/react-native-skills/rules/list-performance-images.md +53 -0
  50. package/bundled-skills/react-native-skills/rules/list-performance-inline-objects.md +97 -0
  51. package/bundled-skills/react-native-skills/rules/list-performance-item-expensive.md +94 -0
  52. package/bundled-skills/react-native-skills/rules/list-performance-item-memo.md +82 -0
  53. package/bundled-skills/react-native-skills/rules/list-performance-item-types.md +104 -0
  54. package/bundled-skills/react-native-skills/rules/list-performance-virtualize.md +67 -0
  55. package/bundled-skills/react-native-skills/rules/monorepo-native-deps-in-app.md +46 -0
  56. package/bundled-skills/react-native-skills/rules/monorepo-single-dependency-versions.md +63 -0
  57. package/bundled-skills/react-native-skills/rules/navigation-native-navigators.md +188 -0
  58. package/bundled-skills/react-native-skills/rules/react-compiler-destructure-functions.md +50 -0
  59. package/bundled-skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md +48 -0
  60. package/bundled-skills/react-native-skills/rules/react-state-dispatcher.md +91 -0
  61. package/bundled-skills/react-native-skills/rules/react-state-fallback.md +56 -0
  62. package/bundled-skills/react-native-skills/rules/react-state-minimize.md +65 -0
  63. package/bundled-skills/react-native-skills/rules/rendering-no-falsy-and.md +74 -0
  64. package/bundled-skills/react-native-skills/rules/rendering-text-in-text-component.md +36 -0
  65. package/bundled-skills/react-native-skills/rules/scroll-position-no-state.md +82 -0
  66. package/bundled-skills/react-native-skills/rules/state-ground-truth.md +80 -0
  67. package/bundled-skills/react-native-skills/rules/ui-expo-image.md +66 -0
  68. package/bundled-skills/react-native-skills/rules/ui-image-gallery.md +104 -0
  69. package/bundled-skills/react-native-skills/rules/ui-measure-views.md +78 -0
  70. package/bundled-skills/react-native-skills/rules/ui-menus.md +174 -0
  71. package/bundled-skills/react-native-skills/rules/ui-native-modals.md +77 -0
  72. package/bundled-skills/react-native-skills/rules/ui-pressable.md +61 -0
  73. package/bundled-skills/react-native-skills/rules/ui-safe-area-scroll.md +65 -0
  74. package/bundled-skills/react-native-skills/rules/ui-scrollview-content-inset.md +45 -0
  75. package/bundled-skills/react-native-skills/rules/ui-styling.md +87 -0
  76. package/bundled-skills/skill-issue/SKILL.md +73 -0
  77. package/bundled-skills/tdd-workflows/SKILL.md +35 -0
  78. package/bundled-skills/vercel-cli-with-tokens/SKILL.md +361 -0
  79. package/bundled-skills/vercel-optimize/CONTRIBUTING.md +41 -0
  80. package/bundled-skills/vercel-optimize/SKILL.md +331 -0
  81. package/bundled-skills/vercel-optimize/lib/auth-route.mjs +23 -0
  82. package/bundled-skills/vercel-optimize/lib/budget-summary.mjs +182 -0
  83. package/bundled-skills/vercel-optimize/lib/citations.mjs +139 -0
  84. package/bundled-skills/vercel-optimize/lib/cost-coverage.mjs +143 -0
  85. package/bundled-skills/vercel-optimize/lib/dedup-recs.mjs +325 -0
  86. package/bundled-skills/vercel-optimize/lib/deep-dive.mjs +350 -0
  87. package/bundled-skills/vercel-optimize/lib/display-labels.mjs +185 -0
  88. package/bundled-skills/vercel-optimize/lib/extract-claims.mjs +550 -0
  89. package/bundled-skills/vercel-optimize/lib/framework-support.mjs +67 -0
  90. package/bundled-skills/vercel-optimize/lib/gates/build-minutes-fanout.mjs +69 -0
  91. package/bundled-skills/vercel-optimize/lib/gates/cold-start.mjs +66 -0
  92. package/bundled-skills/vercel-optimize/lib/gates/contract.mjs +79 -0
  93. package/bundled-skills/vercel-optimize/lib/gates/cwv-poor.mjs +87 -0
  94. package/bundled-skills/vercel-optimize/lib/gates/external-api-slow.mjs +55 -0
  95. package/bundled-skills/vercel-optimize/lib/gates/hard-gates.mjs +73 -0
  96. package/bundled-skills/vercel-optimize/lib/gates/index.mjs +45 -0
  97. package/bundled-skills/vercel-optimize/lib/gates/isr-overrevalidation.mjs +62 -0
  98. package/bundled-skills/vercel-optimize/lib/gates/middleware-heavy.mjs +51 -0
  99. package/bundled-skills/vercel-optimize/lib/gates/observability-events-attribution.mjs +56 -0
  100. package/bundled-skills/vercel-optimize/lib/gates/platform-bot-protection.mjs +115 -0
  101. package/bundled-skills/vercel-optimize/lib/gates/platform-fluid-compute.mjs +83 -0
  102. package/bundled-skills/vercel-optimize/lib/gates/region-misconfig.mjs +64 -0
  103. package/bundled-skills/vercel-optimize/lib/gates/route-errors.mjs +80 -0
  104. package/bundled-skills/vercel-optimize/lib/gates/scanner-driven.mjs +122 -0
  105. package/bundled-skills/vercel-optimize/lib/gates/select-candidates.mjs +134 -0
  106. package/bundled-skills/vercel-optimize/lib/gates/slow-route.mjs +88 -0
  107. package/bundled-skills/vercel-optimize/lib/gates/types.d.ts +38 -0
  108. package/bundled-skills/vercel-optimize/lib/gates/uncached-route.mjs +93 -0
  109. package/bundled-skills/vercel-optimize/lib/gates/usage-spike-triage.mjs +121 -0
  110. package/bundled-skills/vercel-optimize/lib/grade-recommendation.mjs +155 -0
  111. package/bundled-skills/vercel-optimize/lib/impact-label.mjs +126 -0
  112. package/bundled-skills/vercel-optimize/lib/impact-magnitude.mjs +60 -0
  113. package/bundled-skills/vercel-optimize/lib/investigation-brief.mjs +610 -0
  114. package/bundled-skills/vercel-optimize/lib/observation-safety.mjs +174 -0
  115. package/bundled-skills/vercel-optimize/lib/project-facts.mjs +99 -0
  116. package/bundled-skills/vercel-optimize/lib/queries.mjs +315 -0
  117. package/bundled-skills/vercel-optimize/lib/reconcile-candidates.mjs +372 -0
  118. package/bundled-skills/vercel-optimize/lib/render-report.mjs +955 -0
  119. package/bundled-skills/vercel-optimize/lib/repo-root.mjs +86 -0
  120. package/bundled-skills/vercel-optimize/lib/route-normalize.mjs +220 -0
  121. package/bundled-skills/vercel-optimize/lib/sanitizers/bot-protection-certainty.mjs +38 -0
  122. package/bundled-skills/vercel-optimize/lib/sanitizers/cache-tag-invalidation-certainty.mjs +30 -0
  123. package/bundled-skills/vercel-optimize/lib/sanitizers/count-correct.mjs +52 -0
  124. package/bundled-skills/vercel-optimize/lib/sanitizers/function-duration-invocations.mjs +38 -0
  125. package/bundled-skills/vercel-optimize/lib/sanitizers/index.mjs +79 -0
  126. package/bundled-skills/vercel-optimize/lib/sanitizers/middleware-conflict.mjs +36 -0
  127. package/bundled-skills/vercel-optimize/lib/sanitizers/missing-citation.mjs +16 -0
  128. package/bundled-skills/vercel-optimize/lib/sanitizers/pre-release.mjs +74 -0
  129. package/bundled-skills/vercel-optimize/lib/sanitizers/rate-limit.mjs +67 -0
  130. package/bundled-skills/vercel-optimize/lib/sanitizers/rendering-mode-mislabel.mjs +38 -0
  131. package/bundled-skills/vercel-optimize/lib/sanitizers/undeclared-dep.mjs +78 -0
  132. package/bundled-skills/vercel-optimize/lib/sanitizers/vercel-directive-strip.mjs +37 -0
  133. package/bundled-skills/vercel-optimize/lib/sanitizers/window-units.mjs +32 -0
  134. package/bundled-skills/vercel-optimize/lib/scanners/cache-components-suspense-dedupe.mjs +109 -0
  135. package/bundled-skills/vercel-optimize/lib/scanners/edge-heavy-import.mjs +94 -0
  136. package/bundled-skills/vercel-optimize/lib/scanners/force-dynamic.mjs +42 -0
  137. package/bundled-skills/vercel-optimize/lib/scanners/headers-in-page.mjs +44 -0
  138. package/bundled-skills/vercel-optimize/lib/scanners/index.mjs +35 -0
  139. package/bundled-skills/vercel-optimize/lib/scanners/large-static-asset.mjs +92 -0
  140. package/bundled-skills/vercel-optimize/lib/scanners/max-age-without-s-maxage.mjs +42 -0
  141. package/bundled-skills/vercel-optimize/lib/scanners/middleware-broad-matcher.mjs +55 -0
  142. package/bundled-skills/vercel-optimize/lib/scanners/missing-cache-headers.mjs +90 -0
  143. package/bundled-skills/vercel-optimize/lib/scanners/prisma-include-tree.mjs +42 -0
  144. package/bundled-skills/vercel-optimize/lib/scanners/region-pin-in-config.mjs +88 -0
  145. package/bundled-skills/vercel-optimize/lib/scanners/source-maps-production.mjs +36 -0
  146. package/bundled-skills/vercel-optimize/lib/scanners/sveltekit-prerender-missing.mjs +43 -0
  147. package/bundled-skills/vercel-optimize/lib/scanners/turbo-force-bypass.mjs +129 -0
  148. package/bundled-skills/vercel-optimize/lib/scanners/unoptimized-image.mjs +113 -0
  149. package/bundled-skills/vercel-optimize/lib/scanners/use-cache-date-stamp.mjs +106 -0
  150. package/bundled-skills/vercel-optimize/lib/support-topics.mjs +355 -0
  151. package/bundled-skills/vercel-optimize/lib/throttle.mjs +273 -0
  152. package/bundled-skills/vercel-optimize/lib/util.mjs +17 -0
  153. package/bundled-skills/vercel-optimize/lib/vercel.mjs +784 -0
  154. package/bundled-skills/vercel-optimize/lib/verify-claim.mjs +1296 -0
  155. package/bundled-skills/vercel-optimize/lib/workspace-resolver.mjs +521 -0
  156. package/bundled-skills/vercel-optimize/references/candidates.md +176 -0
  157. package/bundled-skills/vercel-optimize/references/data-collection.md +218 -0
  158. package/bundled-skills/vercel-optimize/references/docs-library.json +683 -0
  159. package/bundled-skills/vercel-optimize/references/doctrine.md +105 -0
  160. package/bundled-skills/vercel-optimize/references/observability-plus.md +108 -0
  161. package/bundled-skills/vercel-optimize/references/playbooks/README.md +53 -0
  162. package/bundled-skills/vercel-optimize/references/playbooks/ai-application.md +32 -0
  163. package/bundled-skills/vercel-optimize/references/playbooks/api-service.md +30 -0
  164. package/bundled-skills/vercel-optimize/references/playbooks/content-site.md +30 -0
  165. package/bundled-skills/vercel-optimize/references/playbooks/ecommerce.md +30 -0
  166. package/bundled-skills/vercel-optimize/references/playbooks/marketing.md +30 -0
  167. package/bundled-skills/vercel-optimize/references/playbooks/saas.md +31 -0
  168. package/bundled-skills/vercel-optimize/references/playbooks/sveltekit.md +75 -0
  169. package/bundled-skills/vercel-optimize/references/recommendations.md +203 -0
  170. package/bundled-skills/vercel-optimize/references/scanner-patterns.md +251 -0
  171. package/bundled-skills/vercel-optimize/references/scoring.md +205 -0
  172. package/bundled-skills/vercel-optimize/references/support-topics/README.md +46 -0
  173. package/bundled-skills/vercel-optimize/references/support-topics/astro-edge-middleware-scope.md +22 -0
  174. package/bundled-skills/vercel-optimize/references/support-topics/astro-output-mode-and-isr.md +22 -0
  175. package/bundled-skills/vercel-optimize/references/support-topics/auth-preserving-parallelization.md +22 -0
  176. package/bundled-skills/vercel-optimize/references/support-topics/bot-protection-product-guardrails.md +22 -0
  177. package/bundled-skills/vercel-optimize/references/support-topics/build-minutes-monorepo-fanout.md +23 -0
  178. package/bundled-skills/vercel-optimize/references/support-topics/cache-components-static-shell-boundaries.md +22 -0
  179. package/bundled-skills/vercel-optimize/references/support-topics/cache-components-suspense-dedupe-pitfall.md +23 -0
  180. package/bundled-skills/vercel-optimize/references/support-topics/cdn-cache-auth-safety.md +22 -0
  181. package/bundled-skills/vercel-optimize/references/support-topics/cold-start-initialization-bundle.md +22 -0
  182. package/bundled-skills/vercel-optimize/references/support-topics/core-web-vitals-client-bottlenecks.md +22 -0
  183. package/bundled-skills/vercel-optimize/references/support-topics/database-egress-pooling-region.md +22 -0
  184. package/bundled-skills/vercel-optimize/references/support-topics/dynamic-rendering-traps.md +22 -0
  185. package/bundled-skills/vercel-optimize/references/support-topics/external-api-critical-path-platform.md +22 -0
  186. package/bundled-skills/vercel-optimize/references/support-topics/external-api-critical-path.md +22 -0
  187. package/bundled-skills/vercel-optimize/references/support-topics/fast-data-transfer-payloads.md +22 -0
  188. package/bundled-skills/vercel-optimize/references/support-topics/fluid-compute-caveats.md +22 -0
  189. package/bundled-skills/vercel-optimize/references/support-topics/function-duration-io-and-after.md +22 -0
  190. package/bundled-skills/vercel-optimize/references/support-topics/function-invocation-reduction.md +22 -0
  191. package/bundled-skills/vercel-optimize/references/support-topics/function-region-misconfiguration-ttfb.md +23 -0
  192. package/bundled-skills/vercel-optimize/references/support-topics/image-optimization-cost-control.md +22 -0
  193. package/bundled-skills/vercel-optimize/references/support-topics/isr-revalidation-static-generation.md +22 -0
  194. package/bundled-skills/vercel-optimize/references/support-topics/middleware-proxy-edge-cost.md +22 -0
  195. package/bundled-skills/vercel-optimize/references/support-topics/next-fetch-revalidate-floor.md +22 -0
  196. package/bundled-skills/vercel-optimize/references/support-topics/next-font-cls-self-hosting.md +23 -0
  197. package/bundled-skills/vercel-optimize/references/support-topics/next-heavy-ui-lazy-load-boundaries.md +23 -0
  198. package/bundled-skills/vercel-optimize/references/support-topics/next-image-lcp-preload-sizes.md +23 -0
  199. package/bundled-skills/vercel-optimize/references/support-topics/next-route-handler-get-cache-defaults.md +22 -0
  200. package/bundled-skills/vercel-optimize/references/support-topics/next-script-third-party-strategy.md +23 -0
  201. package/bundled-skills/vercel-optimize/references/support-topics/nextjs-version-cache-semantics.md +22 -0
  202. package/bundled-skills/vercel-optimize/references/support-topics/not-found-catchall-request-waste.md +23 -0
  203. package/bundled-skills/vercel-optimize/references/support-topics/nuxt-route-rules-cache-isr.md +22 -0
  204. package/bundled-skills/vercel-optimize/references/support-topics/observability-events-cost-attribution.md +22 -0
  205. package/bundled-skills/vercel-optimize/references/support-topics/post-response-work-waituntil.md +22 -0
  206. package/bundled-skills/vercel-optimize/references/support-topics/route-error-durable-offload.md +22 -0
  207. package/bundled-skills/vercel-optimize/references/support-topics/route-error-runtime-limits.md +22 -0
  208. package/bundled-skills/vercel-optimize/references/support-topics/runtime-cache-reusable-data.md +22 -0
  209. package/bundled-skills/vercel-optimize/references/support-topics/sveltekit-isr-prerender-safety.md +22 -0
  210. package/bundled-skills/vercel-optimize/references/support-topics/sveltekit-split-cold-start-tradeoff.md +22 -0
  211. package/bundled-skills/vercel-optimize/references/support-topics/usage-spike-triage.md +22 -0
  212. package/bundled-skills/vercel-optimize/references/support-topics/use-cache-date-stamp-isr-write-amplifier.md +23 -0
  213. package/bundled-skills/vercel-optimize/references/support-topics/use-cache-remote-shared-origin-data.md +22 -0
  214. package/bundled-skills/vercel-optimize/references/support-topics/workflow-resumable-stream-routes.md +23 -0
  215. package/bundled-skills/vercel-optimize/references/verification.md +102 -0
  216. package/bundled-skills/vercel-optimize/references/voice.md +76 -0
  217. package/bundled-skills/vercel-optimize/scripts/budget-summary.mjs +56 -0
  218. package/bundled-skills/vercel-optimize/scripts/build-docs.mjs +74 -0
  219. package/bundled-skills/vercel-optimize/scripts/check-citations.mjs +81 -0
  220. package/bundled-skills/vercel-optimize/scripts/check-docs-fresh.mjs +93 -0
  221. package/bundled-skills/vercel-optimize/scripts/collect-signals.mjs +576 -0
  222. package/bundled-skills/vercel-optimize/scripts/collect-sub-agent-outputs.mjs +296 -0
  223. package/bundled-skills/vercel-optimize/scripts/deep-dive.mjs +319 -0
  224. package/bundled-skills/vercel-optimize/scripts/gate-investigations.mjs +166 -0
  225. package/bundled-skills/vercel-optimize/scripts/merge-signals.mjs +192 -0
  226. package/bundled-skills/vercel-optimize/scripts/prepare-investigation-brief.mjs +231 -0
  227. package/bundled-skills/vercel-optimize/scripts/reconcile-candidates.mjs +62 -0
  228. package/bundled-skills/vercel-optimize/scripts/render-report.mjs +437 -0
  229. package/bundled-skills/vercel-optimize/scripts/scan-codebase.mjs +313 -0
  230. package/bundled-skills/vercel-optimize/scripts/verify-and-regen.mjs +346 -0
  231. package/bundled-skills/vercel-optimize/scripts/verify-finding.mjs +19 -0
  232. package/bundled-skills/vercel-react-view-transitions/SKILL.md +327 -0
  233. package/bundled-skills/vercel-react-view-transitions/references/css-recipes.md +242 -0
  234. package/bundled-skills/vercel-react-view-transitions/references/implementation.md +182 -0
  235. package/bundled-skills/vercel-react-view-transitions/references/nextjs.md +176 -0
  236. package/bundled-skills/vercel-react-view-transitions/references/patterns.md +262 -0
  237. package/package.json +1 -1
  238. package/skills_index.json +312 -0
@@ -0,0 +1,115 @@
1
+ // Recommend BotID only when there's EVIDENCE of bot traffic or scale large enough that the rec is defensible.
2
+ // Without an evidence gate the rec fires on quiet hobby sites and erodes trust.
3
+ const MIN_BOT_PCT = 0.05;
4
+ const MIN_EDGE_COST = 25; // halved for 14d window
5
+ const MIN_TOTAL_REQUESTS = 14_000; // ~14k/14d matches the prior 30k/30d rate
6
+ const MIN_TOTAL_FDT_BYTES = 1_000_000;
7
+
8
+ export const metadata = {
9
+ id: 'platform_bot_protection',
10
+ threshold: 'botIdEnabled=false AND (botPct >= 0.05 OR edge_cost >= $25/window OR requests >= 14k/14d)',
11
+ billingDimension: 'edge-requests',
12
+ scope: 'account',
13
+ sourceCitation: 'vercel-optimize gate threshold',
14
+ description:
15
+ 'When BotID is disabled AND there is evidence (observed bot bandwidth share, edge cost, or substantial request volume) that bot traffic is non-trivial. Bot traffic inflates edge request counts without delivering user value; staged bot protection can reduce waste on bot-heavy projects. Skipped on quiet projects with no bot evidence — the recommendation would be noise.',
16
+ };
17
+
18
+ export function gate(signals) {
19
+ // BotID surfaces under several legacy fields; check all.
20
+ const botEnabled =
21
+ signals.project?.security?.botIdEnabled === true
22
+ || signals.project?.security?.botProtection === true
23
+ || signals.project?.botProtection?.enabled === true
24
+ || signals.project?.delegatedProtection?.bot === true;
25
+ if (botEnabled) return [];
26
+
27
+ // Project config failed — we can't tell if BotID is on, so stay silent.
28
+ if (signals.project?.error) return [];
29
+
30
+ const totalRequests = totalRequestsFromSignals(signals);
31
+ const botShare = computeBotShare(signals);
32
+ const edgeService = (signals.usage?.services ?? []).find(
33
+ (s) => /edge.request/i.test(s.name ?? '')
34
+ );
35
+ const edgeCost = edgeService?.billedCost ?? null;
36
+
37
+ // Require observable bot share, edge cost, OR substantial traffic — otherwise rec is just config nagging.
38
+ const hasObservedBots = botShare?.botPct != null && botShare.botPct >= MIN_BOT_PCT;
39
+ const hasMaterialEdgeCost = edgeCost != null && edgeCost >= MIN_EDGE_COST;
40
+ const hasSubstantialTraffic = totalRequests >= MIN_TOTAL_REQUESTS;
41
+ if (!hasObservedBots && !hasMaterialEdgeCost && !hasSubstantialTraffic) return [];
42
+
43
+ const challengeRule = signals.project?.security?.managedRules?.bot_filter;
44
+ const ruleNote = challengeRule?.active
45
+ ? `firewall bot_filter rule active (action=${challengeRule.action ?? '?'})`
46
+ : 'no firewall bot_filter rule';
47
+
48
+ // Kicker on high observed bot share — harder evidence than config alone.
49
+ let priority = edgeCost != null ? Math.max(20, Math.round(edgeCost)) : 30;
50
+ if (botShare?.botPct != null && botShare.botPct > 0.2) priority += 20;
51
+
52
+ // Confidence bumps when we can SEE bot traffic, not just infer from config.
53
+ let confidence = edgeCost != null ? 0.85 : 0.6;
54
+ if (botShare?.botPct != null && botShare.botPct > 0.2) confidence = Math.min(0.95, confidence + 0.05);
55
+
56
+ const botShareNote = botShare?.botPct != null
57
+ ? `bot_fdt_pct=${(botShare.botPct * 100).toFixed(0)}%`
58
+ : 'bot_fdt_pct=unknown';
59
+
60
+ return [{
61
+ kind: metadata.id,
62
+ scope: 'account',
63
+ files: [],
64
+ priority,
65
+ confidence,
66
+ o11ySignal: edgeCost != null
67
+ ? `edge_cost=${edgeCost.toFixed(0)},bot_protection=disabled,${botShareNote},${ruleNote}`
68
+ : `requests=${totalRequests},bot_protection=disabled,${botShareNote},${ruleNote}`,
69
+ reason: botShare?.botPct != null && botShare.botPct > 0.2
70
+ ? 'BotID disabled with observable bot bandwidth share'
71
+ : 'BotID disabled with observable traffic',
72
+ question: botShare?.botPct != null && botShare.botPct > 0.2
73
+ ? `Bot traffic accounts for ${(botShare.botPct * 100).toFixed(0)}% of FDT bytes (top category: ${botShare.topCategory ?? 'unknown'}). Would enabling BotID + a challenge rule reduce that share?`
74
+ : 'Would enabling BotID (Bot Protection) reduce edge request volume from automated traffic?',
75
+ evidence: {
76
+ botEnabled: false,
77
+ edgeCost,
78
+ totalRequests,
79
+ managedRules: challengeRule ?? null,
80
+ botShare: botShare ?? null,
81
+ },
82
+ }];
83
+ }
84
+
85
+ function totalRequestsFromSignals(signals) {
86
+ const rows = signals.metrics?.requestsByRouteCache?.rows;
87
+ if (!Array.isArray(rows)) return 0;
88
+ return rows.reduce((s, r) => s + (r.value ?? 0), 0);
89
+ }
90
+
91
+ // CLI convention: bot_category="" means "not classified as a bot" (human + unclassified); any non-empty = bot.
92
+ function computeBotShare(signals) {
93
+ const rows = signals.metrics?.fdtByBot?.rows;
94
+ if (!Array.isArray(rows) || rows.length === 0) return null;
95
+ let humanBytes = 0;
96
+ let botBytes = 0;
97
+ let topCategory = null;
98
+ let topBytes = 0;
99
+ for (const r of rows) {
100
+ const v = r.value ?? 0;
101
+ const cat = r.bot_category ?? '';
102
+ if (cat === '') {
103
+ humanBytes += v;
104
+ } else {
105
+ botBytes += v;
106
+ if (v > topBytes) {
107
+ topBytes = v;
108
+ topCategory = cat;
109
+ }
110
+ }
111
+ }
112
+ const total = humanBytes + botBytes;
113
+ if (total < MIN_TOTAL_FDT_BYTES) return null;
114
+ return { humanBytes, botBytes, botPct: botBytes / total, topCategory };
115
+ }
@@ -0,0 +1,83 @@
1
+ // Second branch (slow p95 + traffic floor) keeps the gate useful on teams where
2
+ // cold-start isn't directly observable — common on CLI v53 — trading specificity for coverage.
3
+ export const metadata = {
4
+ id: 'platform_fluid_compute',
5
+ threshold: 'fluid=false AND (any cold_start signal OR any route with p95>1000ms AND inv>1000)',
6
+ billingDimension: 'function-duration',
7
+ scope: 'account',
8
+ sourceCitation: 'vercel-optimize gate threshold',
9
+ description:
10
+ 'When Fluid Compute is disabled on a project that shows cold-start pressure (high cold-start rate) or sustained slow function p95 on hot routes. Fluid Compute reduces cold starts via instance reuse — recommend turning it on at the project level rather than per-route.',
11
+ };
12
+
13
+ export function gate(signals) {
14
+ // If project config failed to load we can't tell if Fluid is on; recommending it when already-on
15
+ // erodes trust badly, so stay silent and let Strengths note the gap.
16
+ if (signals.project?.error) return [];
17
+
18
+ const fluidEnabled =
19
+ signals.project?.resourceConfig?.fluid === true
20
+ || signals.project?.defaultResourceConfig?.fluid === true;
21
+ if (fluidEnabled) return [];
22
+
23
+ const cold = extractHighColdRoutes(signals);
24
+ const slow = extractSlowHotRoutes(signals);
25
+ if (cold.length === 0 && slow.length === 0) return [];
26
+
27
+ return [{
28
+ kind: metadata.id,
29
+ scope: 'account',
30
+ files: [],
31
+ priority: 50,
32
+ confidence: cold.length > 0 ? 0.85 : 0.65,
33
+ o11ySignal: cold.length > 0
34
+ ? `${cold.length} route(s) with high cold-start rate`
35
+ : `${slow.length} hot route(s) with p95>1s; cold-start not directly observable`,
36
+ reason: cold.length > 0
37
+ ? 'cold starts observed and Fluid Compute is disabled'
38
+ : 'slow hot routes and Fluid Compute is disabled',
39
+ question: 'Would enabling Fluid Compute reduce cold-start and warm-instance reuse overhead for the observed hot routes?',
40
+ evidence: { fluidEnabled, highColdRoutes: cold.slice(0, 5), slowHotRoutes: slow.slice(0, 5) },
41
+ }];
42
+ }
43
+
44
+ function extractHighColdRoutes(signals) {
45
+ const live = signals.metrics?.fnStartTypeByRoute?.rows;
46
+ if (Array.isArray(live) && live.some((r) => 'coldCount' in r || 'coldPct' in r)) {
47
+ return live.filter((r) => r.route && (r.coldPct ?? 0) > 0.3 && (r.total ?? 0) > 100);
48
+ }
49
+ // Legacy pre-derived fixture shape.
50
+ const direct = signals.metrics?.coldStartByRoute?.rows;
51
+ if (Array.isArray(direct)) {
52
+ return direct.filter((r) => r.route && (r.coldPct ?? 0) > 0.3 && (r.total ?? 0) > 100);
53
+ }
54
+ const legacy = signals.metrics?.coldStarts?.series;
55
+ if (Array.isArray(legacy)) {
56
+ return legacy
57
+ .map((s) => {
58
+ const total = s.summary?.count ?? 0;
59
+ const coldCount = s.summary?.coldCount ?? s.summary?.sum ?? 0;
60
+ return { route: s.groupValues?.route, total, coldPct: total > 0 ? coldCount / total : 0 };
61
+ })
62
+ .filter((r) => r.route && r.coldPct > 0.3 && r.total > 100);
63
+ }
64
+ return [];
65
+ }
66
+
67
+ function extractSlowHotRoutes(signals) {
68
+ const dur = signals.metrics?.fnDurationP95ByRoute?.rows;
69
+ const cache = signals.metrics?.requestsByRouteCache?.rows;
70
+ if (!Array.isArray(dur)) return [];
71
+
72
+ // Sum requests per route across cache_result.
73
+ const inv = new Map();
74
+ for (const r of (cache ?? [])) {
75
+ if (!r.route) continue;
76
+ inv.set(r.route, (inv.get(r.route) ?? 0) + (r.value ?? 0));
77
+ }
78
+ return dur
79
+ .filter((r) => r.route)
80
+ .map((r) => ({ route: r.route, p95Ms: Math.round(r.value ?? 0), invocations: inv.get(r.route) ?? 0 }))
81
+ // inv>500 floor is the 14d-window equivalent of the old 1000/30d.
82
+ .filter((r) => r.p95Ms > 1000 && r.invocations > 500);
83
+ }
@@ -0,0 +1,64 @@
1
+ // Region-misconfig gate. Branch 2 (scanner-only) — per-region TTFB data gap.
2
+ //
3
+ // The intended Branch 1 (region-grouped TTFB metric) was preflight-tested but the
4
+ // CLI returned INTERNAL_ERROR for the `--group-by route --group-by function_region`
5
+ // combination, and SAML re-auth blocked single-dim verification (see Phase 0 in
6
+ // plans/wild-splashing-flamingo.md). Ship scanner-only with `evidence.dataGap` and
7
+ // add the query later when verifiable.
8
+ //
9
+ // Fires when a single-region pin is found AND the project has meaningful surface area
10
+ // (routes.length > 20). Skips multi-region configs (informational only).
11
+ export const metadata = {
12
+ id: 'region_misconfig',
13
+ threshold: 'single-region pin found AND routes.length > 20 (scanner-only branch)',
14
+ billingDimension: 'function-duration',
15
+ scope: 'account',
16
+ sourceCitation: 'vercel-optimize gate threshold',
17
+ description:
18
+ "A single function region is pinned in `vercel.json` or per-route `preferredRegion`. Without per-region TTFB data (data gap), the gate can't quantify the geographic latency cost — but a single-region pin on a project with 20+ routes is worth auditing against Speed Insights traffic geo.",
19
+ };
20
+
21
+ const ROUTE_FLOOR = 20;
22
+ const SCANNER_PATTERN = 'region-pin-in-config';
23
+
24
+ export function gate(signals) {
25
+ const findings = (signals?.codebase?.findings ?? []).filter((f) => f.pattern === SCANNER_PATTERN);
26
+ if (findings.length === 0) return [];
27
+
28
+ const routes = signals?.codebase?.routes ?? [];
29
+ if (routes.length < ROUTE_FLOOR) return [];
30
+
31
+ const singleRegionFindings = findings.filter((f) => Array.isArray(f.regions) && f.regions.length === 1);
32
+ if (singleRegionFindings.length === 0) return [];
33
+
34
+ const allPinned = new Set();
35
+ for (const f of singleRegionFindings) {
36
+ for (const r of f.regions ?? []) allPinned.add(r);
37
+ }
38
+ const regionList = [...allPinned];
39
+
40
+ // If multiple distinct single-region pins exist across files, the surface is partly
41
+ // multi-region by accident; that's noteworthy but lower priority.
42
+ const homogeneous = regionList.length === 1;
43
+
44
+ return [{
45
+ kind: metadata.id,
46
+ scope: 'account',
47
+ files: singleRegionFindings.map((f) => f.file).slice(0, 6),
48
+ priority: homogeneous ? 42 : 38,
49
+ confidence: 0.6, // low — no per-region TTFB data
50
+ o11ySignal: `pinned_regions=${regionList.join(',')} routes=${routes.length}`,
51
+ reason: homogeneous
52
+ ? `all functions pinned to a single region (${regionList[0]}) on a project with ${routes.length} routes`
53
+ : `${regionList.length} different single-region pins across files`,
54
+ question: 'Are the pinned function regions aligned with the dominant user geography and the data source location? Speed Insights TTFB-by-country can ground the comparison.',
55
+ evidence: {
56
+ metric: 'codebase.findings',
57
+ pinnedRegions: regionList,
58
+ findingsCount: singleRegionFindings.length,
59
+ routeCount: routes.length,
60
+ sampleFiles: singleRegionFindings.slice(0, 3).map((f) => ({ file: f.file, regions: f.regions, subtype: f.subtype })),
61
+ dataGap: 'region-grouped-TTFB-unavailable',
62
+ },
63
+ }];
64
+ }
@@ -0,0 +1,80 @@
1
+ // Errored function invocations still bill at full duration, so high-volume 5xx is a cost issue, not just reliability.
2
+ import { withRouteShapeWarnings } from '../route-normalize.mjs';
3
+
4
+ const MIN_VOLUME_FOR_RATE_EMISSION = 1000;
5
+
6
+ export const metadata = {
7
+ id: 'route_errors',
8
+ threshold: `count > 250 OR (totalRequests >= ${MIN_VOLUME_FOR_RATE_EMISSION} AND errorRate > 0.01)`,
9
+ billingDimension: 'function-duration',
10
+ scope: 'route',
11
+ sourceCitation: 'vercel-optimize gate threshold',
12
+ description:
13
+ 'Routes producing > 250 5xx errors over the window, or with > 1% error rate on at least 1,000 total requests. Errored function invocations still bill at full duration; high error rates also poison user experience.',
14
+ };
15
+
16
+ export function gate(signals) {
17
+ const errors = extractErrors(signals);
18
+ return errors
19
+ .filter((e) => e.count > 250 || (e.total >= MIN_VOLUME_FOR_RATE_EMISSION && (e.errorRate ?? 0) > 0.01))
20
+ .map((e) => withRouteShapeWarnings({
21
+ kind: metadata.id,
22
+ scope: 'route',
23
+ route: e.route,
24
+ files: [],
25
+ priority: e.count,
26
+ confidence: 0.93,
27
+ o11ySignal: e.errorRate != null
28
+ ? `errs=${e.count},rate=${(e.errorRate * 100).toFixed(1)}%`
29
+ : `errs=${e.count}`,
30
+ reason: 'concentrated 5xx errors',
31
+ question: `Why does ${e.route} produce ${e.count} 5xx errors over the window, and what code path is failing?`,
32
+ evidence: { metric: e.metric, route: e.route, count: e.count, totalRequests: e.total, errorRate: e.errorRate },
33
+ }, signals));
34
+ }
35
+
36
+ function extractErrors(signals) {
37
+ const fnStatus = signals.metrics?.fnStatusByRoute;
38
+ if (Array.isArray(fnStatus?.rows)) return extractFromStatusRows(fnStatus.rows, 'fnStatusByRoute');
39
+
40
+ const m = signals.metrics?.requestsByRouteStatus;
41
+ const cache = signals.metrics?.requestsByRouteCache;
42
+ if (!m?.ok && !Array.isArray(m?.rows)) return [];
43
+
44
+ const errors = extractFromStatusRows(m?.rows ?? [], 'requestsByRouteStatus');
45
+
46
+ // cache rollup is summed across cache_result, giving per-route total request count.
47
+ const totalByRoute = new Map();
48
+ for (const row of (cache?.rows ?? [])) {
49
+ if (!row.route) continue;
50
+ totalByRoute.set(row.route, (totalByRoute.get(row.route) ?? 0) + (row.value ?? 0));
51
+ }
52
+
53
+ return errors.map((e) => {
54
+ const total = totalByRoute.get(e.route) ?? 0;
55
+ return {
56
+ ...e,
57
+ total,
58
+ errorRate: total > 0 ? e.count / total : null,
59
+ };
60
+ });
61
+ }
62
+
63
+ function extractFromStatusRows(rows, metric) {
64
+ const errByRoute = new Map();
65
+ const totalByRoute = new Map();
66
+ for (const row of rows) {
67
+ const route = row.route;
68
+ if (!route) continue;
69
+ const v = row.value ?? 0;
70
+ const status = String(row.http_status ?? '');
71
+ if (/^5\d\d$/.test(status)) errByRoute.set(route, (errByRoute.get(route) ?? 0) + v);
72
+ totalByRoute.set(route, (totalByRoute.get(route) ?? 0) + v);
73
+ }
74
+
75
+ return [...errByRoute.entries()].map(([route, count]) => {
76
+ const total = totalByRoute.get(route) ?? 0;
77
+ const errorRate = total > 0 ? count / total : null;
78
+ return { route, count, total, errorRate, metric };
79
+ });
80
+ }
@@ -0,0 +1,122 @@
1
+ // Signal source is the codebase itself, not traffic. COLD-PATH and NO-ROUTE-MAPPING findings
2
+ // are dropped unless the scanner sets trafficIndependent (build configs, middleware matchers, etc.).
3
+ // Annotation happens in scan-codebase.mjs; gates here just read scanner.o11ySignal.
4
+
5
+ export const SCANNER_GATES = [
6
+ { id: 'image_optimization', patterns: ['unoptimized-image'], threshold: 2,
7
+ billingDimension: 'image-optimization', priority: 30 },
8
+ { id: 'cache_header_gap', patterns: ['max-age-without-s-maxage', 'missing-cache-headers'], threshold: 1,
9
+ billingDimension: 'edge-requests', priority: 40 },
10
+ { id: 'rendering_candidate', patterns: ['force-dynamic', 'headers-in-page'], threshold: 3,
11
+ billingDimension: 'function-duration', priority: 35 },
12
+ { id: 'use_cache_date_stamp', patterns: ['use-cache-date-stamp'], threshold: 1,
13
+ billingDimension: 'isr', priority: 45 },
14
+ { id: 'cache_components_suspense_dedupe', patterns: ['cache-components-suspense-dedupe'], threshold: 1,
15
+ billingDimension: 'function-duration', priority: 38 },
16
+ ];
17
+
18
+ export const metadata = {
19
+ id: 'scanner-driven',
20
+ threshold: 'per-kind: scanner matches.length >= threshold',
21
+ billingDimension: 'mixed',
22
+ scope: 'mixed',
23
+ sourceCitation: 'vercel-optimize gate threshold',
24
+ description:
25
+ 'Configured kinds emitted from scanner output. Each requires a minimum match count to avoid noise. Findings on cold-path or unmappable files are dropped unless the underlying scanner is trafficIndependent.',
26
+ };
27
+
28
+ export function gate(signals) {
29
+ const findings = signals.codebase?.findings ?? [];
30
+ if (findings.length === 0) return [];
31
+
32
+ const candidates = [];
33
+
34
+ for (const cfg of SCANNER_GATES) {
35
+ const matched = findings.filter((f) => {
36
+ if (!cfg.patterns.includes(f.pattern)) return false;
37
+ if (!f.trafficIndependent) {
38
+ if (!f.o11ySignal || f.o11ySignal === 'scanner-only') return false;
39
+ if (f.o11ySignal === 'COLD-PATH') return false;
40
+ if (f.o11ySignal === 'NO-ROUTE-MAPPING') return false;
41
+ }
42
+ if (cfg.id === 'cache_header_gap' && observedCacheHitRate(f.o11ySignal) >= 90) return false;
43
+ return true;
44
+ });
45
+
46
+ for (const group of groupFindings(cfg, matched)) {
47
+ if (group.findings.length < cfg.threshold) continue;
48
+ candidates.push(candidateForGroup(cfg, group));
49
+ }
50
+ }
51
+
52
+ return candidates;
53
+ }
54
+
55
+ function groupFindings(cfg, findings) {
56
+ const groups = new Map();
57
+ for (const finding of findings) {
58
+ const scope = finding.route ? 'route' : 'file';
59
+ const target = scope === 'route' ? finding.route : finding.file;
60
+ if (!target) continue;
61
+ const key = `${cfg.id}:${scope}:${target}`;
62
+ if (!groups.has(key)) groups.set(key, { scope, target, findings: [] });
63
+ groups.get(key).findings.push(finding);
64
+ }
65
+ return [...groups.values()];
66
+ }
67
+
68
+ function candidateForGroup(cfg, group) {
69
+ const matched = group.findings;
70
+ const route = group.scope === 'route' ? group.target : null;
71
+ return {
72
+ kind: cfg.id,
73
+ scope: group.scope,
74
+ route,
75
+ files: uniqueStrings(matched.map((m) => m.file)).slice(0, 6),
76
+ priority: cfg.priority + Math.min(matched.length, 10),
77
+ confidence: 0.88,
78
+ o11ySignal: matched
79
+ .map((m) => m.o11ySignal)
80
+ .find((s) => s && s !== 'COLD-PATH' && s !== 'NO-ROUTE-MAPPING')
81
+ ?? 'scanner-only',
82
+ reason: `${matched.length} ${cfg.patterns.join('+')} finding(s)`,
83
+ question: questionFor(cfg.id, matched),
84
+ evidence: {
85
+ scannerMatches: matched.length,
86
+ patterns: cfg.patterns,
87
+ scope: group.scope,
88
+ route,
89
+ sampleFiles: matched.slice(0, 3).map((m) => ({ file: m.file, line: m.line })),
90
+ },
91
+ };
92
+ }
93
+
94
+ function questionFor(kindId, matched) {
95
+ const sample = matched.slice(0, 3).map((m) => m.file).join(', ');
96
+ switch (kindId) {
97
+ case 'image_optimization':
98
+ return `Which raw <img> tags in ${sample} should move to next/image (or the framework's image component)?`;
99
+ case 'cache_header_gap':
100
+ return `Should the route handlers in ${sample} set Cache-Control with s-maxage to serve from the CDN?`;
101
+ case 'rendering_candidate':
102
+ return `Why are the routes in ${sample} forced to dynamic rendering, and can any of them tolerate ISR or static generation?`;
103
+ case 'use_cache_date_stamp':
104
+ return `Which 'use cache' boundaries in ${sample} embed new Date()/Date.now()/Math.random() that destabilizes cache keys, and can the timestamps be hoisted to a build constant or moved into a client useEffect?`;
105
+ case 'cache_components_suspense_dedupe':
106
+ return `In ${sample}, which repeated fetch or helper is being re-invoked across separate <Suspense> boundaries, and can the promise be hoisted to the page level or moved to 'use cache: remote' for cross-boundary dedupe?`;
107
+ default:
108
+ return `Investigate ${matched.length} ${kindId} finding(s).`;
109
+ }
110
+ }
111
+
112
+ function uniqueStrings(values) {
113
+ return [...new Set(values.filter((v) => typeof v === 'string' && v.length > 0))];
114
+ }
115
+
116
+ function observedCacheHitRate(signal) {
117
+ if (typeof signal !== 'string') return null;
118
+ const m = /\bcache=([\d.]+)%/.exec(signal);
119
+ if (!m) return null;
120
+ const n = Number(m[1]);
121
+ return Number.isFinite(n) ? n : null;
122
+ }
@@ -0,0 +1,134 @@
1
+ // Deterministic launch selection for the code-scope investigation budget.
2
+ //
3
+ // Raw priority still orders candidates inside each pass. The default budget is
4
+ // impact-first, with failure-mode diversity when a kind's top signal is large
5
+ // enough to justify taking a first-pass slot.
6
+
7
+ const DEFAULT_KIND_CAPS = new Map([
8
+ ['slow_route', 2],
9
+ ['uncached_route', 2],
10
+ ['route_errors', 2],
11
+ ]);
12
+
13
+ const DIVERSITY_ELIGIBILITY = new Map([
14
+ // A handful of 5xx errors can pass the route_errors gate because the rate is
15
+ // high, but that should not displace much larger cost/performance signals in
16
+ // the default six-candidate pass.
17
+ ['route_errors', (candidate) => numberFromEvidence(candidate, 'count') >= 1000],
18
+
19
+ // Scanner-driven cache findings are valuable, but the default pass should
20
+ // spend a slot only when observability shows meaningful route traffic or a
21
+ // very slow route handler.
22
+ ['cache_header_gap', (candidate) => {
23
+ const invocations = numberFromSignal(candidate?.o11ySignal, 'inv');
24
+ const p95Ms = durationMsFromSignal(candidate?.o11ySignal, 'p95');
25
+ return invocations >= 50_000 || p95Ms >= 2000;
26
+ }],
27
+ ['rendering_candidate', (candidate) => numberFromSignal(candidate?.o11ySignal, 'inv') >= 50_000],
28
+ ]);
29
+
30
+ export function selectLaunchCandidates(candidates, budget, { diversify = false } = {}) {
31
+ const pool = Array.isArray(candidates) ? candidates : [];
32
+ if (budget === Infinity) {
33
+ return { selected: pool, skipped: [], selectionMode: 'all' };
34
+ }
35
+ if (!Number.isInteger(budget) || budget < 1) {
36
+ throw new TypeError('selectLaunchCandidates budget must be a positive integer or Infinity');
37
+ }
38
+ if (!diversify) {
39
+ return {
40
+ selected: pool.slice(0, budget),
41
+ skipped: pool.slice(budget),
42
+ selectionMode: 'priority',
43
+ };
44
+ }
45
+
46
+ const selected = [];
47
+ const selectedKeys = new Set();
48
+ const countsByKind = new Map();
49
+
50
+ const add = (candidate) => {
51
+ const key = candidateIdentity(candidate);
52
+ if (selectedKeys.has(key)) return false;
53
+ selectedKeys.add(key);
54
+ selected.push(candidate);
55
+ const kind = candidate.kind ?? '<unknown>';
56
+ countsByKind.set(kind, (countsByKind.get(kind) ?? 0) + 1);
57
+ return true;
58
+ };
59
+
60
+ // First pass: one candidate per failure mode, preserving the existing sorted
61
+ // order. This is where the default run gets broad coverage, but only for
62
+ // kinds whose signal is strong enough for a default slot.
63
+ for (const candidate of pool) {
64
+ if (selected.length >= budget) break;
65
+ const kind = candidate.kind ?? '<unknown>';
66
+ if ((countsByKind.get(kind) ?? 0) > 0) continue;
67
+ if (!isDiversityEligible(candidate)) continue;
68
+ add(candidate);
69
+ }
70
+
71
+ // Second pass: allow a second entry for high-frequency families, but avoid
72
+ // letting slow_route consume the entire default budget when other kinds exist.
73
+ for (const candidate of pool) {
74
+ if (selected.length >= budget) break;
75
+ const kind = candidate.kind ?? '<unknown>';
76
+ const cap = DEFAULT_KIND_CAPS.get(kind) ?? 1;
77
+ if ((countsByKind.get(kind) ?? 0) >= cap) continue;
78
+ if (!isDiversityEligible(candidate)) continue;
79
+ add(candidate);
80
+ }
81
+
82
+ // Final fill: if the project only has one or two candidate kinds, use the
83
+ // whole requested budget rather than leaving slots empty.
84
+ for (const candidate of pool) {
85
+ if (selected.length >= budget) break;
86
+ add(candidate);
87
+ }
88
+
89
+ return {
90
+ selected,
91
+ skipped: pool.filter((candidate) => !selectedKeys.has(candidateIdentity(candidate))),
92
+ selectionMode: 'diverse-default',
93
+ };
94
+ }
95
+
96
+ function candidateIdentity(candidate) {
97
+ return [
98
+ candidate?.kind ?? '',
99
+ candidate?.route ?? '',
100
+ candidate?.hostname ?? '',
101
+ candidate?.scope ?? '',
102
+ candidate?.o11ySignal ?? '',
103
+ ].join('\u0000');
104
+ }
105
+
106
+ function isDiversityEligible(candidate) {
107
+ const fn = DIVERSITY_ELIGIBILITY.get(candidate?.kind);
108
+ return fn ? fn(candidate) : true;
109
+ }
110
+
111
+ function numberFromEvidence(candidate, key) {
112
+ const value = candidate?.evidence?.[key];
113
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
114
+ }
115
+
116
+ function numberFromSignal(signal, key) {
117
+ if (typeof signal !== 'string') return 0;
118
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
119
+ const re = new RegExp(`(?:^|,)${escaped}=([\\d.]+)`);
120
+ const m = re.exec(signal);
121
+ if (!m) return 0;
122
+ const n = Number(m[1]);
123
+ return Number.isFinite(n) ? n : 0;
124
+ }
125
+
126
+ function durationMsFromSignal(signal, key) {
127
+ if (typeof signal !== 'string') return 0;
128
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
129
+ const re = new RegExp(`(?:^|,)${escaped}=([\\d.]+)ms`);
130
+ const m = re.exec(signal);
131
+ if (!m) return 0;
132
+ const n = Number(m[1]);
133
+ return Number.isFinite(n) ? n : 0;
134
+ }