opencode-skills-collection 3.0.35 → 3.0.37

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 +2 -2
  238. package/skills_index.json +312 -0
@@ -0,0 +1,88 @@
1
+ // Primary threshold (p95>500 AND inv>=1400) WHY: 1.4k/14d is the floor where p95 stabilizes statistically
2
+ // and a 3-5x performance win still pays for engineering time. Secondary (p95>1500 AND inv>=250) catches "catastrophically
3
+ // slow at any volume" — usually a broken sync call or cold-start chain the customer wants to know about.
4
+ //
5
+ // 5xx disqualifier: when error rate >50% the route is failing, not slow — latency reflects crash time,
6
+ // not work time. route_errors covers it independently; we disqualify here so budget isn't spent on a
7
+ // sub-agent that will correctly abstain.
8
+
9
+ import { withRouteShapeWarnings } from '../route-normalize.mjs';
10
+
11
+ const ERROR_RATE_DISQUALIFY_THRESHOLD = 0.5;
12
+
13
+ export const metadata = {
14
+ id: 'slow_route',
15
+ threshold: '(p95 > 500 AND inv >= 1400) OR (p95 > 1500 AND inv >= 250); disqualified when 5xx rate > 50%; Vercel Workflow runtime endpoints are hard-gated',
16
+ billingDimension: 'function-duration',
17
+ scope: 'route',
18
+ sourceCitation: 'vercel-optimize gate threshold',
19
+ description:
20
+ 'Routes with p95 function duration above 500ms at meaningful traffic (>=1,400 invocations in window), OR catastrophically slow routes (>1500ms p95 at any volume >=250). High duration drives both function-duration cost and user-perceived latency. Investigate sequential awaits, slow external APIs, missing caching, N+1 patterns. Routes with >50% 5xx rate are disqualified — those are reliability problems, not performance tuning targets, and surface via route_errors instead. Vercel Workflow runtime endpoints (`/.well-known/workflow/v1/*`) are hard-gated before launch because long-running step/flow requests are expected orchestration, not app-route bottlenecks.',
21
+ };
22
+
23
+ export function gate(signals) {
24
+ const routes = extractFunctionRoutes(signals);
25
+ const errorRates = extractErrorRatesByRoute(signals);
26
+ return routes
27
+ .filter((r) => (r.p95Ms > 500 && r.invocations >= 1400) || (r.p95Ms > 1500 && r.invocations >= 250))
28
+ .map((r) => {
29
+ const errorRate = errorRates.get(r.route);
30
+ const candidate = {
31
+ kind: metadata.id,
32
+ scope: 'route',
33
+ route: r.route,
34
+ files: [],
35
+ priority: Math.round(r.p95Ms * Math.max(r.invocations, 1) / 1000),
36
+ confidence: 0.94,
37
+ o11ySignal: `inv=${r.invocations},p95=${r.p95Ms}ms${errorRate != null ? `,5xx=${(errorRate * 100).toFixed(0)}%` : ''}`,
38
+ reason: 'slow high-traffic route',
39
+ question: `What is the concrete bottleneck in ${r.route} (p95=${r.p95Ms}ms over ${r.invocations} invocations), and which file-level change would reduce it?`,
40
+ evidence: { metric: 'fnDurationP95ByRoute', route: r.route, p95Ms: r.p95Ms, invocations: r.invocations, errorRate },
41
+ };
42
+ if (errorRate != null && errorRate > ERROR_RATE_DISQUALIFY_THRESHOLD) {
43
+ candidate.disqualified = true;
44
+ candidate.disqualifyReason = `high error rate (${(errorRate * 100).toFixed(0)}% 5xx — reliability issue, not performance; covered by route_errors gate)`;
45
+ }
46
+ return withRouteShapeWarnings(candidate, signals);
47
+ });
48
+ }
49
+
50
+ // Routes without status data are absent from the map → gate falls back to "no disqualification".
51
+ function extractErrorRatesByRoute(signals) {
52
+ const m = signals.metrics?.fnStatusByRoute;
53
+ const out = new Map();
54
+ if (!Array.isArray(m?.rows)) return out;
55
+ const perRoute = new Map();
56
+ for (const row of m.rows) {
57
+ if (!row?.route) continue;
58
+ const v = row.value ?? 0;
59
+ const prior = perRoute.get(row.route) ?? { errors5xx: 0, total: 0 };
60
+ if (/^5/.test(String(row.http_status ?? ''))) prior.errors5xx += v;
61
+ prior.total += v;
62
+ perRoute.set(row.route, prior);
63
+ }
64
+ for (const [route, r] of perRoute) {
65
+ if (r.total > 0) out.set(route, r.errors5xx / r.total);
66
+ }
67
+ return out;
68
+ }
69
+
70
+ function extractFunctionRoutes(signals) {
71
+ const dur = signals.metrics?.fnDurationP95ByRoute;
72
+ if (!dur?.ok && !Array.isArray(dur?.rows)) return [];
73
+ const req = signals.metrics?.requestsByRouteCache;
74
+
75
+ const invByRoute = new Map();
76
+ for (const row of (req?.rows ?? [])) {
77
+ if (!row.route) continue;
78
+ invByRoute.set(row.route, (invByRoute.get(row.route) ?? 0) + (row.value ?? 0));
79
+ }
80
+
81
+ return (dur?.rows ?? [])
82
+ .filter((r) => r.route)
83
+ .map((r) => ({
84
+ route: r.route,
85
+ p95Ms: Math.round(r.value ?? 0),
86
+ invocations: invByRoute.get(r.route) ?? 0,
87
+ }));
88
+ }
@@ -0,0 +1,38 @@
1
+ export type CandidateScope = 'route' | 'file' | 'account';
2
+
3
+ export interface GateMetadata {
4
+ id: string;
5
+ threshold: string;
6
+ billingDimension: string;
7
+ scope: CandidateScope | 'mixed';
8
+ sourceCitation?: string;
9
+ description?: string;
10
+ }
11
+
12
+ export interface Candidate {
13
+ kind: string;
14
+ scope: CandidateScope;
15
+ route?: string | null;
16
+ hostname?: string | null;
17
+ files: string[];
18
+ priority: number;
19
+ confidence: number;
20
+ o11ySignal?: string;
21
+ reason: string;
22
+ question: string;
23
+ evidence?: Record<string, unknown>;
24
+ disqualified?: boolean;
25
+ disqualifyReason?: string;
26
+ warnings?: string[];
27
+ }
28
+
29
+ export interface Signals {
30
+ metrics?: Record<string, unknown>;
31
+ codebase?: {
32
+ findings?: Array<Record<string, unknown>>;
33
+ routes?: Array<Record<string, unknown>>;
34
+ };
35
+ project?: Record<string, unknown>;
36
+ usage?: Record<string, unknown>;
37
+ stack?: Record<string, unknown>;
38
+ }
@@ -0,0 +1,93 @@
1
+ // getShare>0.20 filter WHY: a route that's >80% POST/PUT/DELETE is a mutation endpoint
2
+ // where 0% cache is correct — recommending caching there is wrong.
3
+ // cache_result values STALE/REVALIDATED/BYPASS fold into "total but not HIT" — matches the "uncached" framing.
4
+
5
+ import { withRouteShapeWarnings } from '../route-normalize.mjs';
6
+
7
+ const MIN_GET_SHARE = 0.20;
8
+
9
+ /** @type {import('./types.d.ts').GateMetadata} */
10
+ export const metadata = {
11
+ id: 'uncached_route',
12
+ threshold: `requests > 500 AND hitRate < 0.5 AND getShare > ${MIN_GET_SHARE} (missing getShare is gated)`,
13
+ billingDimension: 'edge-requests',
14
+ scope: 'route',
15
+ sourceCitation: 'vercel-optimize gate threshold',
16
+ description:
17
+ 'Routes serving > 500 requests/period at < 50% cache hit AND at least 20% GET traffic. Each uncached GET request reaches the function, costing edge requests + function duration. Routes that are mostly POST/PUT/DELETE (Server Actions, mutations) are skipped — 0% cache is correct behavior there. Routes with missing method-share data are gated instead of launched. Auth-gated routes are disqualified separately.',
18
+ };
19
+
20
+ /**
21
+ * @param {import('./types.d.ts').Signals} signals
22
+ * @returns {import('./types.d.ts').Candidate[]}
23
+ */
24
+ export function gate(signals) {
25
+ const rates = extractCacheHitRates(signals);
26
+ const methods = extractMethodShares(signals);
27
+ return rates
28
+ .map((r) => ({ ...r, getShare: methods.get(r.route) ?? null }))
29
+ .filter((r) => r.requests > 500 && r.hitRate < 0.5)
30
+ .filter((r) => r.getShare === null || r.getShare > MIN_GET_SHARE)
31
+ .map((r) => {
32
+ const candidate = withRouteShapeWarnings({
33
+ kind: metadata.id,
34
+ scope: 'route',
35
+ route: r.route,
36
+ files: [],
37
+ priority: Math.round(r.requests * (1 - r.hitRate)),
38
+ confidence: r.getShare === null ? 0.5 : 0.92,
39
+ o11ySignal: `requests=${r.requests},cache=${(r.hitRate * 100).toFixed(0)}%${r.getShare !== null ? `,get=${(r.getShare * 100).toFixed(0)}%` : ''}`,
40
+ reason: 'uncached high-traffic route',
41
+ question: `Why does ${r.route} have ${(r.hitRate * 100).toFixed(0)}% cache hit rate on ${r.requests} requests in this metrics window, and is it safe to cache at the edge?`,
42
+ evidence: { metric: 'requestsByRouteCache', route: r.route, requests: r.requests, hitRate: r.hitRate, getShare: r.getShare },
43
+ }, signals);
44
+ if (r.getShare !== null) return candidate;
45
+ return {
46
+ ...candidate,
47
+ disqualified: true,
48
+ disqualifyReason: 'missing GET-share data — route method mix is required before recommending edge caching',
49
+ warnings: [...new Set([...(candidate.warnings ?? []), 'method-share:missing'])],
50
+ };
51
+ });
52
+ }
53
+
54
+ function extractCacheHitRates(signals) {
55
+ const m = signals.metrics?.requestsByRouteCache;
56
+ if (!m?.ok && !Array.isArray(m?.rows)) return [];
57
+
58
+ const perRoute = new Map();
59
+ for (const row of (m?.rows ?? [])) {
60
+ const route = row.route;
61
+ if (!route) continue;
62
+ const value = row.value ?? 0;
63
+ const prior = perRoute.get(route) ?? { route, hits: 0, total: 0 };
64
+ if (row.cache_result === 'HIT') prior.hits += value;
65
+ prior.total += value;
66
+ perRoute.set(route, prior);
67
+ }
68
+
69
+ return [...perRoute.values()].map((r) => ({
70
+ route: r.route,
71
+ requests: r.total,
72
+ hitRate: r.total > 0 ? r.hits / r.total : 0,
73
+ }));
74
+ }
75
+
76
+ function extractMethodShares(signals) {
77
+ const m = signals.metrics?.requestsByRouteMethod;
78
+ const out = new Map();
79
+ if (!Array.isArray(m?.rows)) return out;
80
+ const perRoute = new Map();
81
+ for (const row of m.rows) {
82
+ if (!row?.route) continue;
83
+ const v = row.value ?? 0;
84
+ const prior = perRoute.get(row.route) ?? { gets: 0, total: 0 };
85
+ if ((row.request_method ?? '').toUpperCase() === 'GET') prior.gets += v;
86
+ prior.total += v;
87
+ perRoute.set(row.route, prior);
88
+ }
89
+ for (const [route, r] of perRoute) {
90
+ if (r.total > 0) out.set(route, r.gets / r.total);
91
+ }
92
+ return out;
93
+ }
@@ -0,0 +1,121 @@
1
+ // Detects per-day billing spikes by inspecting usage.breakdown.data[] (daily granularity).
2
+ // Fires when any single day's total bill > 2× the window mean, OR a single SKU's day value > 3× its window mean.
3
+ // Emits one candidate per spiking SKU (or 'total' when the spike is broad).
4
+ // Degrades gracefully when daily data is unavailable — common path because the skill prefers --group-by project, which omits daily breakdown.
5
+ export const metadata = {
6
+ id: 'usage_spike_triage',
7
+ threshold: 'any-day total > 2x mean OR any-day SKU > 3x SKU mean',
8
+ billingDimension: 'mixed',
9
+ scope: 'account',
10
+ sourceCitation: 'vercel-optimize gate threshold',
11
+ description:
12
+ 'A single day in the billing window deviates sharply from the window baseline. Triage branches: bot or AI crawler spike, viral moment, pricing-model migration (legacy SKU → new), code regression. Without daily-granularity data, this gate stays dormant.',
13
+ };
14
+
15
+ const TOTAL_MULTIPLIER = 2;
16
+ const SKU_MULTIPLIER = 3;
17
+ const MIN_BILLED_FLOOR = 5; // skip spikes whose absolute value is too small to matter
18
+
19
+ export function gate(signals) {
20
+ const days = signals?.usage?.breakdown?.data;
21
+ if (!Array.isArray(days) || days.length < 3) return [];
22
+
23
+ const dayTotals = days.map(dayTotal);
24
+ const mean = dayTotals.reduce((a, b) => a + b, 0) / dayTotals.length;
25
+ if (mean <= MIN_BILLED_FLOOR) return [];
26
+
27
+ const totalSpikeDays = dayTotals
28
+ .map((value, idx) => ({ idx, value }))
29
+ .filter((d) => d.value > mean * TOTAL_MULTIPLIER && d.value > MIN_BILLED_FLOOR);
30
+
31
+ const skuStats = aggregateSkuStats(days);
32
+ const skuSpikes = [];
33
+ for (const stat of skuStats) {
34
+ if (stat.mean <= MIN_BILLED_FLOOR) continue;
35
+ for (const sample of stat.samples) {
36
+ if (sample.value > stat.mean * SKU_MULTIPLIER && sample.value > MIN_BILLED_FLOOR) {
37
+ skuSpikes.push({
38
+ name: stat.name,
39
+ dayIndex: sample.idx,
40
+ dayValue: sample.value,
41
+ skuMean: stat.mean,
42
+ multiplier: stat.mean > 0 ? sample.value / stat.mean : null,
43
+ });
44
+ }
45
+ }
46
+ }
47
+
48
+ if (totalSpikeDays.length === 0 && skuSpikes.length === 0) return [];
49
+
50
+ const candidates = [];
51
+ if (totalSpikeDays.length > 0) {
52
+ const peak = totalSpikeDays.reduce((a, b) => (a.value > b.value ? a : b));
53
+ candidates.push({
54
+ kind: metadata.id,
55
+ scope: 'account',
56
+ files: [],
57
+ priority: 60,
58
+ confidence: 0.78,
59
+ o11ySignal: `total_spike day_idx=${peak.idx} day_billed=${peak.value.toFixed(2)} window_mean=${mean.toFixed(2)} mult=${(peak.value / mean).toFixed(1)}x`,
60
+ reason: 'total billed cost on one day exceeds 2× the window mean',
61
+ question: 'Which workload generated the day-over-day spike — bot or AI-crawler traffic on a cacheable route, a viral event, a pricing-model migration, or a code regression?',
62
+ evidence: {
63
+ metric: 'usage.breakdown.data.total',
64
+ spikeDay: peak.idx,
65
+ spikeBilled: peak.value,
66
+ windowMean: mean,
67
+ multiplier: peak.value / mean,
68
+ skuName: 'total',
69
+ },
70
+ });
71
+ }
72
+ // Up to 3 SKU-specific candidates; the rest fold into 'multiple SKUs spiking' framing.
73
+ const orderedSkuSpikes = skuSpikes.sort((a, b) => b.dayValue - a.dayValue).slice(0, 3);
74
+ for (const spike of orderedSkuSpikes) {
75
+ candidates.push({
76
+ kind: metadata.id,
77
+ scope: 'account',
78
+ files: [],
79
+ priority: 55,
80
+ confidence: 0.78,
81
+ o11ySignal: `sku_spike sku="${spike.name}" day_idx=${spike.dayIndex} day_billed=${spike.dayValue.toFixed(2)} sku_mean=${spike.skuMean.toFixed(2)} mult=${spike.multiplier.toFixed(1)}x`,
82
+ reason: `${spike.name} on one day exceeds 3× its window mean`,
83
+ question: `${spike.name} spiked ${spike.multiplier.toFixed(1)}× on day ${spike.dayIndex}. Which event (bot traffic, viral content, deploy regression, integration sync) drove it, and is the spiking SKU one the skill already covers?`,
84
+ evidence: {
85
+ metric: 'usage.breakdown.data.services',
86
+ skuName: spike.name,
87
+ spikeDay: spike.dayIndex,
88
+ spikeBilled: spike.dayValue,
89
+ skuMean: spike.skuMean,
90
+ multiplier: spike.multiplier,
91
+ },
92
+ });
93
+ }
94
+ return candidates;
95
+ }
96
+
97
+ function dayTotal(day) {
98
+ if (Array.isArray(day?.services)) {
99
+ return day.services.reduce((a, s) => a + Number(s.billedCost ?? s.cost ?? 0), 0);
100
+ }
101
+ return Number(day?.billedCost ?? day?.cost ?? 0);
102
+ }
103
+
104
+ function aggregateSkuStats(days) {
105
+ const byName = new Map();
106
+ days.forEach((day, idx) => {
107
+ const services = Array.isArray(day?.services) ? day.services : [];
108
+ for (const svc of services) {
109
+ const name = String(svc?.name ?? '').trim();
110
+ if (!name) continue;
111
+ const value = Number(svc.billedCost ?? svc.cost ?? 0);
112
+ if (!byName.has(name)) byName.set(name, { name, samples: [] });
113
+ byName.get(name).samples.push({ idx, value });
114
+ }
115
+ });
116
+ for (const stat of byName.values()) {
117
+ const sum = stat.samples.reduce((a, s) => a + s.value, 0);
118
+ stat.mean = stat.samples.length > 0 ? sum / stat.samples.length : 0;
119
+ }
120
+ return [...byName.values()];
121
+ }
@@ -0,0 +1,155 @@
1
+ // 4-axis rubric (specificity, actionability, grounding, evidence) → bucket. See references/recommendations.md.
2
+ // Account-scope (platform_*) recs use a separate grounding/evidence pair — they structurally cannot produce file:line.
3
+
4
+ const HEDGE_WORDS = /\b(consider|might|may|could|perhaps|maybe|likely|probably)\b/gi;
5
+ const VERB_OPENERS = /^\s*(?:[-*]\s+|\d+[.)]\s+|[*_]+)?(?:add|set|enable|disable|replace|remove|move|wrap|cache|defer|parallelize|introduce|configure|update|change|switch|opt[-\s]?in|opt[-\s]?out|export|import|install|run|delete|rename)/im;
6
+ const COUNT_WORDS_RE = /\b(errors?|queries|invocations|requests|reads|writes|bytes|fetch(?:es)?|calls?|hits?|misses?|seconds?|images?|deployments?|cold[- ]?starts?|users?)\b/gi;
7
+ const UNIT_RE = /\b\d[\d.,]*\s*(?:%|ms|s|sec|seconds?|min|minutes?|h|hours?|GB|MB|KB|K|M|B|rps|qps|req\/s|reqs?\/min)\b/gi;
8
+ const CODE_FENCE_RE = /```[\s\S]*?```/g;
9
+ const INLINE_CODE_RE = /`[^`\n]{10,}`/g;
10
+ const FILE_LINE_RE = /[\w/.\-()\[\]]+\.\w+:\d+/g;
11
+
12
+ // Grounding + evidence are lie-detectors — weighted higher than specificity/actionability, which LLMs can game with fluff.
13
+ const W = { grounding: 0.35, evidence: 0.30, specificity: 0.20, actionability: 0.15 };
14
+
15
+ export function gradeRecommendation(rec, ctx = {}) {
16
+ const accountScope = isAccountScope(rec);
17
+ const specificity = scoreSpecificity(rec);
18
+ const actionability = scoreActionability(rec);
19
+ const grounding = accountScope ? scoreGroundingAccount(rec) : scoreGrounding(rec, ctx);
20
+ const evidence = accountScope ? scoreEvidenceAccount(rec) : scoreEvidence(rec);
21
+ const overall = roundTo(
22
+ grounding * W.grounding + evidence * W.evidence + specificity * W.specificity + actionability * W.actionability,
23
+ 4,
24
+ );
25
+ return {
26
+ specificity, actionability, grounding, evidence, overall,
27
+ grade: grade(overall),
28
+ scope: accountScope ? 'account' : 'route',
29
+ };
30
+ }
31
+
32
+ function isAccountScope(rec) {
33
+ if (rec?.scope === 'account') return true;
34
+ const ref = rec?.candidateRef;
35
+ if (typeof ref === 'string' && ref.startsWith('platform_')) return true;
36
+ return false;
37
+ }
38
+
39
+ function grade(overall) {
40
+ if (overall >= 0.85) return 'Excellent';
41
+ if (overall >= 0.70) return 'Good';
42
+ if (overall >= 0.55) return 'Fair';
43
+ return 'Poor';
44
+ }
45
+
46
+ function scoreSpecificity(rec) {
47
+ let s = 0;
48
+ const codeText = [rec.fix, rec.currentBehavior, rec.desiredBehavior].filter((x) => typeof x === 'string').join('\n');
49
+ const hasFence = CODE_FENCE_RE.test(codeText);
50
+ CODE_FENCE_RE.lastIndex = 0;
51
+ if (hasFence) s += 0.5;
52
+ if (INLINE_CODE_RE.test(codeText)) s += 0.2;
53
+ INLINE_CODE_RE.lastIndex = 0;
54
+ if (Array.isArray(rec.affectedFiles) && rec.affectedFiles.length > 0) s += 0.2;
55
+ if (Array.isArray(rec.findingRefs) && rec.findingRefs.some((r) => /:\d+/.test(r))) s += 0.3;
56
+ return Math.min(1, roundTo(s, 4));
57
+ }
58
+
59
+ function scoreActionability(rec) {
60
+ const text = typeof rec.fix === 'string' ? rec.fix : '';
61
+ if (!text) return 0;
62
+ let s = 0;
63
+ if (VERB_OPENERS.test(text)) s += 0.35;
64
+ const stepCount = (text.match(/(?:^|\n)\s*(?:\d+[.)]\s+|[-*]\s+)/g) ?? []).length;
65
+ if (stepCount >= 2) s += 0.35;
66
+ else if (stepCount === 1) s += 0.15;
67
+ const hedges = (text.match(HEDGE_WORDS) ?? []).length;
68
+ HEDGE_WORDS.lastIndex = 0;
69
+ s -= Math.min(0.3, hedges * 0.1);
70
+ // Baseline so a verb-only one-liner still scores.
71
+ s += 0.3;
72
+ return Math.max(0, Math.min(1, roundTo(s, 4)));
73
+ }
74
+
75
+ function scoreGrounding(rec, ctx) {
76
+ let s = 0;
77
+ const knownFindings = Array.isArray(ctx.knownFindings) ? ctx.knownFindings : [];
78
+ const findingKeys = new Set(knownFindings.map((f) => `${f.file}:${f.line}`));
79
+ const refs = Array.isArray(rec.findingRefs) ? rec.findingRefs : [];
80
+ const matched = refs.filter((r) => findingKeys.has(r));
81
+ if (matched.length > 0) s += 0.5;
82
+ else if (refs.length > 0) s += 0.25;
83
+ if (Array.isArray(rec.affectedFiles) && rec.affectedFiles.length > 0) s += 0.25;
84
+ const fenceText = [rec.currentBehavior, rec.desiredBehavior].filter((x) => typeof x === 'string').join('\n');
85
+ if (CODE_FENCE_RE.test(fenceText)) s += 0.25;
86
+ CODE_FENCE_RE.lastIndex = 0;
87
+ if (typeof rec.candidateRef === 'string' && rec.candidateRef.length > 0) s += 0.1;
88
+ return Math.min(1, roundTo(s, 4));
89
+ }
90
+
91
+ function scoreEvidence(rec) {
92
+ const text = [rec.what, rec.why, rec.fix, rec.verify]
93
+ .filter((x) => typeof x === 'string').join('\n');
94
+ if (!text) return 0;
95
+ const counts = (text.match(COUNT_WORDS_RE) ?? []).length;
96
+ COUNT_WORDS_RE.lastIndex = 0;
97
+ const units = (text.match(UNIT_RE) ?? []).length;
98
+ UNIT_RE.lastIndex = 0;
99
+ const filelines = (text.match(FILE_LINE_RE) ?? []).length;
100
+ FILE_LINE_RE.lastIndex = 0;
101
+ // file:line is the gold standard.
102
+ let s = Math.min(0.5, filelines * 0.2)
103
+ + Math.min(0.3, units * 0.075)
104
+ + Math.min(0.2, counts * 0.05);
105
+ return Math.min(1, roundTo(s, 4));
106
+ }
107
+
108
+ // No findingRefs/code fences possible — grade structural tie to gate + signal-quoting.
109
+ function scoreGroundingAccount(rec) {
110
+ let s = 0;
111
+ if (typeof rec.candidateRef === 'string' && rec.candidateRef.startsWith('platform_')) s += 0.4;
112
+ else if (typeof rec.candidateRef === 'string' && rec.candidateRef.length > 0) s += 0.2;
113
+ // Quoting deep-dive data in why/fix is the account-scope equivalent of citing file:line.
114
+ const text = [rec.why, rec.fix, rec.verify].filter((x) => typeof x === 'string').join('\n');
115
+ const units = (text.match(UNIT_RE) ?? []).length;
116
+ UNIT_RE.lastIndex = 0;
117
+ if (units >= 3) s += 0.4;
118
+ else if (units >= 1) s += 0.2;
119
+ const citations = Array.isArray(rec.citations) ? rec.citations.length : 0;
120
+ if (citations >= 2) s += 0.2;
121
+ else if (citations >= 1) s += 0.1;
122
+ return Math.min(1, roundTo(s, 4));
123
+ }
124
+
125
+ // Heavily weighted toward magnitude quoting — vague platform recs should score low.
126
+ function scoreEvidenceAccount(rec) {
127
+ const text = [rec.what, rec.why, rec.fix, rec.verify]
128
+ .filter((x) => typeof x === 'string').join('\n');
129
+ if (!text) return 0;
130
+ const counts = (text.match(COUNT_WORDS_RE) ?? []).length;
131
+ COUNT_WORDS_RE.lastIndex = 0;
132
+ const units = (text.match(UNIT_RE) ?? []).length;
133
+ UNIT_RE.lastIndex = 0;
134
+ // Higher weight than route-scope variant — file:line gold standard isn't available.
135
+ let s = Math.min(0.55, units * 0.15) + Math.min(0.35, counts * 0.08);
136
+ if (typeof rec.o11ySignal === 'string' && rec.o11ySignal.length > 0) s += 0.1;
137
+ return Math.min(1, roundTo(s, 4));
138
+ }
139
+
140
+ function roundTo(n, d) {
141
+ const f = 10 ** d;
142
+ return Math.round(n * f) / f;
143
+ }
144
+
145
+ // 0.55 = Poor/Fair boundary. Recommending Poor-graded items erodes trust faster than the marginal recall benefit.
146
+ export function applyQualityFloor(recs, floor = 0.55) {
147
+ const kept = [];
148
+ const dropped = [];
149
+ for (const rec of recs) {
150
+ const o = rec?.quality?.overall ?? 0;
151
+ if (o < floor) dropped.push({ rec, reason: `quality.overall=${o} < floor=${floor}` });
152
+ else kept.push(rec);
153
+ }
154
+ return { kept, dropped };
155
+ }
@@ -0,0 +1,126 @@
1
+ import { impactMagnitude } from './impact-magnitude.mjs';
2
+
3
+ const SLOW_ROUTE_P95_THRESHOLD_MS = 500;
4
+ const CACHE_HIT_THRESHOLD_PCT = 50;
5
+ const COLD_START_THRESHOLD_PCT = 40;
6
+ const RELIABILITY_TARGET_PCT = 0.1;
7
+
8
+ export function computeImpactLabel(rec, signals = {}) {
9
+ const il = rec?.impactLabel ?? {};
10
+ if (il.performance) return il.performance;
11
+ if (il.costPhrase) return il.costPhrase;
12
+
13
+ if (typeof rec?.estimatedSavingsUsd === 'number' && rec.impactTier) {
14
+ const magnitude = impactMagnitude({
15
+ currentCost: rec.estimatedSavingsUsd,
16
+ impactTier: rec.impactTier,
17
+ });
18
+ return magnitude.phrase;
19
+ }
20
+
21
+ return synthesizeImpactFromSignal(rec, signals);
22
+ }
23
+
24
+ export function synthesizeImpactFromSignal(rec, signals = {}) {
25
+ const tier = rec?.impactTier;
26
+ const sig = [
27
+ rec?.o11ySignal,
28
+ rec?.evidence?.o11ySignal,
29
+ rec?.why,
30
+ rec?.what,
31
+ ].filter((v) => typeof v === 'string' && v.trim()).join('\n') || null;
32
+ if (!sig || !tier) return null;
33
+ const m = String(sig);
34
+ const inv = parseSigNumber(m, /inv=([\d,]+)/);
35
+ const p95 = parseSigNumber(m, /p95=([\d,]+)ms/);
36
+ const cachePct = parseSigNumber(m, /cache=([\d.]+)%/);
37
+ const coldPct = parseSigNumber(m, /cold=([\d.]+)%/);
38
+ const buildSharePct = parseSigNumber(m, /build_minutes_share=([\d.]+)%/i) ??
39
+ parseSigNumber(m, /build(?: CPU)? minutes share:?\s*([\d.]+)%/i);
40
+ const errors = parseSigNumber(m, /errs=([\d,]+)/);
41
+ const errorRatePct = parseSigNumber(m, /rate=([\d.]+)%/);
42
+ const writes = parseSigNumber(m, /writes=([\d,]+)/);
43
+ const reads = parseSigNumber(m, /reads=([\d,]+)/);
44
+ const cwvIssues = [
45
+ cwvIssue('LCP', parseSigNumber(m, /LCP=([\d.]+)ms/i), 2500, 'ms'),
46
+ cwvIssue('INP', parseSigNumber(m, /INP=([\d.]+)ms/i), 200, 'ms'),
47
+ cwvIssue('CLS', parseSigNumber(m, /CLS=([\d.]+)/i), 0.1, ''),
48
+ ].filter(Boolean);
49
+
50
+ if (cwvIssues.length > 0) {
51
+ return `${tier} impact — bring ${joinEnglish(cwvIssues.map(formatCwvIssue))}.`;
52
+ }
53
+ if (errors != null && errorRatePct != null) {
54
+ const reductionPct = Math.ceil(Math.max(0, (1 - (RELIABILITY_TARGET_PCT / errorRatePct)) * 100));
55
+ return `${tier} impact — cut 5xx rate by ~${reductionPct}% to get below ${RELIABILITY_TARGET_PCT}% (current ${errorRatePct}%, ${formatInteger(errors)} errors in this window).`;
56
+ }
57
+ if (errors != null) {
58
+ return `${tier} impact — resolve ${formatInteger(errors)} billed 5xx errors in this window.`;
59
+ }
60
+ if (cachePct != null && inv != null) {
61
+ if (cachePct < CACHE_HIT_THRESHOLD_PCT) {
62
+ return `${tier} impact — current cache hit rate is ${cachePct}% across ${formatInteger(inv)} requests in this window; the gate fires below ${CACHE_HIT_THRESHOLD_PCT}%.`;
63
+ }
64
+ if (p95 == null) {
65
+ return `${tier} impact — current cache hit rate is ${cachePct}% across ${formatInteger(inv)} requests in this window; this recommendation targets the remaining uncached traffic.`;
66
+ }
67
+ }
68
+ if (writes != null && reads != null) {
69
+ const ratio = reads > 0 ? writes / reads : null;
70
+ return ratio == null
71
+ ? `${tier} impact — ${formatInteger(writes)} ISR write units with no recorded read units in this window.`
72
+ : `${tier} impact — ${formatInteger(writes)} ISR write units vs ${formatInteger(reads)} read units in this window (${round2(ratio)} writes per read).`;
73
+ }
74
+ if (p95 != null && inv != null) {
75
+ const multiple = p95 / SLOW_ROUTE_P95_THRESHOLD_MS;
76
+ return `${tier} impact — current 95th percentile duration is ${formatInteger(p95)}ms across ${formatInteger(inv)} function invocations in this window (${round1(multiple)}x the ${formatInteger(SLOW_ROUTE_P95_THRESHOLD_MS)}ms slow-route threshold).`;
77
+ }
78
+ if (coldPct != null) {
79
+ return `${tier} impact — current cold-start share is ${coldPct}%; the gate fires above ${COLD_START_THRESHOLD_PCT}%.`;
80
+ }
81
+ if (rec?.candidateRef?.startsWith('build_minutes_fanout:') || rec?.kind === 'build_minutes_fanout') {
82
+ return buildSharePct != null
83
+ ? `${tier} impact — Build CPU Minutes account for ${buildSharePct}% of observed billed cost in this window.`
84
+ : `${tier} impact — Build CPU Minutes exceeded the gate threshold in this window.`;
85
+ }
86
+ return `${tier} impact — see follow-up metrics for magnitude.`;
87
+ }
88
+
89
+ function cwvIssue(metric, value, threshold, unit) {
90
+ if (value == null || value <= threshold) return null;
91
+ return { metric, value, threshold, unit };
92
+ }
93
+
94
+ function formatCwvIssue(i) {
95
+ const current = i.metric === 'CLS' ? round2(i.value) : Math.round(i.value);
96
+ if (i.unit === 'ms') {
97
+ return `${i.metric} below ${formatInteger(i.threshold)}ms (current ${formatInteger(current)}ms)`;
98
+ }
99
+ return `${i.metric} below ${i.threshold}${i.unit} (current ${current}${i.unit})`;
100
+ }
101
+
102
+ function joinEnglish(parts) {
103
+ if (parts.length <= 1) return parts[0] ?? '';
104
+ if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
105
+ return `${parts.slice(0, -1).join(', ')}, and ${parts.at(-1)}`;
106
+ }
107
+
108
+ function parseSigNumber(s, re) {
109
+ const m = re.exec(s);
110
+ if (!m) return null;
111
+ const n = Number(String(m[1]).replace(/,/g, ''));
112
+ return Number.isFinite(n) ? n : null;
113
+ }
114
+
115
+ function round1(n) {
116
+ return Math.round(n * 10) / 10;
117
+ }
118
+
119
+ function round2(n) {
120
+ return Math.round(n * 100) / 100;
121
+ }
122
+
123
+ function formatInteger(n) {
124
+ if (!Number.isFinite(n)) return String(n);
125
+ return Math.round(n).toLocaleString('en-US');
126
+ }
@@ -0,0 +1,60 @@
1
+ // Rule: precision for performance, magnitudes for cost. Customer-facing phrases never contain $N literals.
2
+
3
+ export function impactMagnitude({ currentCost, impactTier }) {
4
+ const fraction = { high: 0.4, medium: 0.2, low: 0.1 }[impactTier] ?? 0.2;
5
+ const estUsd = (currentCost ?? 0) * fraction;
6
+
7
+ if (estUsd < 5) {
8
+ return { magnitude: 'negligible', phrase: 'small cost impact at current traffic' };
9
+ }
10
+ if (estUsd < 50) {
11
+ return { magnitude: 'small', phrase: 'low-tens of dollars per month at current traffic' };
12
+ }
13
+ if (estUsd < 500) {
14
+ return { magnitude: 'medium', phrase: 'hundreds of dollars per month at current traffic' };
15
+ }
16
+ if (estUsd < 5000) {
17
+ return { magnitude: 'large', phrase: 'low-thousands of dollars per month at current traffic' };
18
+ }
19
+ return { magnitude: 'very-large', phrase: 'thousands+ of dollars per month at current traffic' };
20
+ }
21
+
22
+ // Preserves Postgres placeholders ($1, $2, …) — digits with no comma/period/k/m suffix.
23
+ export function stripDollarLiterals(text) {
24
+ if (!text || typeof text !== 'string') return { text, stripped: 0 };
25
+
26
+ let count = 0;
27
+ const cleaned = text.replace(
28
+ /\$[\d][\d.,]*(?:[kKmMbB])?(?:\/[\dA-Za-z]+)?/g,
29
+ (m) => {
30
+ if (/^\$\d+$/.test(m)) return m;
31
+ count++;
32
+ return 'the billed cost';
33
+ }
34
+ );
35
+
36
+ return { text: cleaned, stripped: count };
37
+ }
38
+
39
+ export function applyDollarStrip(rec) {
40
+ const fields = ['what', 'why', 'fix', 'impact', 'currentBehavior', 'desiredBehavior', 'before', 'after'];
41
+ let totalStripped = 0;
42
+ for (const f of fields) {
43
+ if (typeof rec[f] !== 'string') continue;
44
+ // Preserve code-fence content so example snippets aren't mangled.
45
+ const fences = [];
46
+ rec[f] = rec[f].replace(/```[\s\S]*?```/g, (m) => {
47
+ fences.push(m);
48
+ return `__FENCE_${fences.length - 1}__`;
49
+ });
50
+ const { text, stripped } = stripDollarLiterals(rec[f]);
51
+ rec[f] = text;
52
+ totalStripped += stripped;
53
+ rec[f] = rec[f].replace(/__FENCE_(\d+)__/g, (_, i) => fences[Number(i)]);
54
+ }
55
+ if (totalStripped > 0) {
56
+ rec.sanitizerTrail = rec.sanitizerTrail ?? [];
57
+ rec.sanitizerTrail.push(`$-strip:${totalStripped}`);
58
+ }
59
+ return rec;
60
+ }