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,67 @@
1
+ // Prepend a caveat (don't drop — customer may be on a higher tier) when a
2
+ // rec prescribes concurrency above a known provider rate limit.
3
+
4
+ const PROVIDER_LIMITS = {
5
+ notion: { rps: 3, label: 'Notion', doc: 'https://developers.notion.com/reference/request-limits' },
6
+ openai: { rps: 30, label: 'OpenAI', doc: 'https://platform.openai.com/docs/guides/rate-limits' },
7
+ stripe: { rps: 100, label: 'Stripe', doc: 'https://docs.stripe.com/rate-limits' },
8
+ anthropic: { rps: 10, label: 'Anthropic', doc: 'https://docs.anthropic.com/en/api/rate-limits' },
9
+ };
10
+
11
+ export const metadata = {
12
+ id: 'rate-limit',
13
+ description: 'Prepend caveat when a rec prescribes concurrency above a known provider rate limit.',
14
+ };
15
+
16
+ const PROVIDER_RE = new RegExp(`\\b(${Object.keys(PROVIDER_LIMITS).join('|')})\\b`, 'gi');
17
+ const CONCURRENCY_RE = /\b(?:concurrency|parallel|in\s+parallel|simultaneous|simultaneously|fan[- ]?out|Promise\.all)\b[^\d]{0,40}(\d{1,4})\b/gi;
18
+ const CONCURRENCY_RE_REVERSE = /\b(\d{1,4})\s*(?:concurrent|parallel|simultaneous|in flight)\b/gi;
19
+
20
+ export function apply(rec, _ctx = {}) {
21
+ const text = collectText(rec);
22
+ const providers = matchProviders(text);
23
+ if (providers.length === 0) return {};
24
+ const concurrency = matchConcurrency(text);
25
+ if (concurrency === null) return {};
26
+
27
+ const tags = [];
28
+ let prepend = '';
29
+ for (const key of providers) {
30
+ const limit = PROVIDER_LIMITS[key];
31
+ if (!limit) continue;
32
+ if (concurrency > limit.rps) {
33
+ const tag = `rate-limit:${limit.label}:${concurrency}/${limit.rps}`;
34
+ tags.push(tag);
35
+ prepend += `⚠ ${limit.label} rate-limits to ~${limit.rps} requests/second on first-tier plans; the prescribed concurrency of ${concurrency} may saturate the limit. Verify your tier before applying.\n\n`;
36
+ }
37
+ }
38
+ if (tags.length === 0) return {};
39
+ if (typeof rec.fix === 'string') rec.fix = prepend + rec.fix;
40
+ else rec.fix = prepend.trim();
41
+ return { tags, needsReview: true };
42
+ }
43
+
44
+ function collectText(rec) {
45
+ return [rec.what, rec.why, rec.fix, rec.currentBehavior, rec.desiredBehavior]
46
+ .filter((s) => typeof s === 'string')
47
+ .join('\n');
48
+ }
49
+
50
+ function matchProviders(text) {
51
+ const out = new Set();
52
+ for (const m of text.matchAll(PROVIDER_RE)) out.add(m[1].toLowerCase());
53
+ return [...out];
54
+ }
55
+
56
+ function matchConcurrency(text) {
57
+ let max = null;
58
+ for (const m of text.matchAll(CONCURRENCY_RE)) {
59
+ const n = Number(m[1]);
60
+ if (Number.isFinite(n) && (max === null || n > max)) max = n;
61
+ }
62
+ for (const m of text.matchAll(CONCURRENCY_RE_REVERSE)) {
63
+ const n = Number(m[1]);
64
+ if (Number.isFinite(n) && (max === null || n > max)) max = n;
65
+ }
66
+ return max;
67
+ }
@@ -0,0 +1,38 @@
1
+ // Warn when a rec's claimed rendering mode (static/ISR/SSR) contradicts
2
+ // the scanner-tagged mode for that route. No-op when the scanner didn't
3
+ // tag a renderingMode — full AST inference isn't implemented yet.
4
+
5
+ import { extractRoute } from '../util.mjs';
6
+
7
+ const MODE_PATTERNS = {
8
+ static: /\bstatic(?:ally rendered)?\b|prerender(?:ed)?\b/i,
9
+ isr: /\bISR\b|incremental[- ]?static|revalidate\s*:\s*\d/i,
10
+ ssr: /\bSSR\b|server[- ]?side rendered|dynamic\s*=\s*['"]force-dynamic['"]/i,
11
+ };
12
+
13
+ export const metadata = {
14
+ id: 'rendering-mode-mislabel',
15
+ description: 'Catch recs that blame the wrong rendering mode (e.g. "convert from ISR" on a static page).',
16
+ };
17
+
18
+ export function apply(rec, ctx = {}) {
19
+ const route = extractRoute(rec);
20
+ if (!route) return {};
21
+ const routes = ctx?.signals?.codebase?.routes ?? [];
22
+ const match = routes.find((r) => r.routePath === route);
23
+ const actualMode = match?.renderingMode;
24
+ if (!actualMode) return {};
25
+
26
+ const text = [rec.what, rec.why, rec.fix, rec.currentBehavior, rec.desiredBehavior]
27
+ .filter((s) => typeof s === 'string')
28
+ .join('\n');
29
+ const claimedModes = Object.entries(MODE_PATTERNS)
30
+ .filter(([, re]) => re.test(text))
31
+ .map(([m]) => m);
32
+
33
+ if (claimedModes.length === 0 || claimedModes.includes(actualMode)) return {};
34
+
35
+ const warning = `\n\n_⚠ Rendering-mode mismatch: this rec describes the route as \`${claimedModes.join(', ')}\` but the scanner classified it as \`${actualMode}\`. Verify the rendering mode before applying._`;
36
+ if (typeof rec.fix === 'string') rec.fix += warning;
37
+ return { tag: `rendering-mode-mislabel:${claimedModes.join(',')}!=${actualMode}`, needsReview: true };
38
+ }
@@ -0,0 +1,78 @@
1
+ // Prepend `npm i <pkg>` when the fix imports a package missing from
2
+ // package.json — otherwise pasted code hits a runtime error.
3
+
4
+ const IMPORT_RE = /\bimport\s+(?:[\w*{}\s,]+\s+from\s+)?["']([^"']+)["']/g;
5
+ const REQUIRE_RE = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g;
6
+ // Captures package root from `pkg/sub` and `@scope/pkg/sub`.
7
+ const PKG_ROOT_RE = /^(@[^/]+\/[^/]+|[^/]+)/;
8
+ const NODE_BUILTINS = new Set([
9
+ 'fs', 'fs/promises', 'path', 'os', 'crypto', 'http', 'https', 'http2', 'net',
10
+ 'dns', 'tls', 'util', 'url', 'stream', 'buffer', 'events', 'process', 'child_process',
11
+ 'cluster', 'worker_threads', 'inspector', 'perf_hooks', 'assert', 'console',
12
+ 'querystring', 'string_decoder', 'tty', 'vm', 'zlib', 'readline', 'punycode',
13
+ 'module', 'timers', 'async_hooks', 'v8', 'test', 'diagnostics_channel',
14
+ ]);
15
+
16
+ export const metadata = {
17
+ id: 'undeclared-dep',
18
+ description: 'Prepend `npm i <pkg>` when fix imports a package not in package.json.',
19
+ };
20
+
21
+ export function apply(rec, ctx = {}) {
22
+ const pkg = ctx?.package ?? ctx?.signals?.package ?? null;
23
+ if (!pkg) return {};
24
+
25
+ const known = new Set([
26
+ ...Object.keys(pkg.dependencies ?? {}),
27
+ ...Object.keys(pkg.devDependencies ?? {}),
28
+ ...Object.keys(pkg.peerDependencies ?? {}),
29
+ ...Object.keys(pkg.optionalDependencies ?? {}),
30
+ ]);
31
+
32
+ const text = [rec.fix, rec.currentBehavior, rec.desiredBehavior]
33
+ .filter((s) => typeof s === 'string')
34
+ .join('\n');
35
+ const codeBlocks = extractCodeBlocks(text);
36
+ const importedRoots = new Set();
37
+ for (const block of codeBlocks) {
38
+ for (const m of block.matchAll(IMPORT_RE)) {
39
+ const root = pkgRoot(m[1]);
40
+ if (root) importedRoots.add(root);
41
+ }
42
+ for (const m of block.matchAll(REQUIRE_RE)) {
43
+ const root = pkgRoot(m[1]);
44
+ if (root) importedRoots.add(root);
45
+ }
46
+ }
47
+
48
+ const undeclared = [...importedRoots]
49
+ .filter((r) => !r.startsWith('.'))
50
+ .filter((r) => !NODE_BUILTINS.has(r))
51
+ .filter((r) => !r.startsWith('node:'))
52
+ .filter((r) => !known.has(r));
53
+
54
+ if (undeclared.length === 0) return {};
55
+
56
+ const installLines = undeclared.map((p) => `\`npm i ${p}\``).join(', ');
57
+ const prepend = `**Add dependency first**: ${installLines}\n\n`;
58
+ if (typeof rec.fix === 'string') rec.fix = prepend + rec.fix;
59
+ else rec.fix = prepend.trim();
60
+ return { tags: undeclared.map((p) => `undeclared-dep:${p}`), needsReview: true };
61
+ }
62
+
63
+ function pkgRoot(specifier) {
64
+ if (!specifier) return null;
65
+ if (specifier.startsWith('.')) return specifier;
66
+ const m = specifier.match(PKG_ROOT_RE);
67
+ return m ? m[1] : null;
68
+ }
69
+
70
+ function extractCodeBlocks(text) {
71
+ const out = [];
72
+ const re = /```[\w-]*\n?([\s\S]*?)```/g;
73
+ let m;
74
+ while ((m = re.exec(text)) !== null) out.push(m[1]);
75
+ // Also scan raw text for rare inline imports outside code blocks.
76
+ out.push(text);
77
+ return out;
78
+ }
@@ -0,0 +1,37 @@
1
+ // Strip Cache-Control directives Vercel's CDN silently ignores
2
+ // (stale-if-error, proxy-revalidate, must-revalidate). s-maxage/max-age/
3
+ // stale-while-revalidate/no-store/private/public are honored — leave them.
4
+
5
+ import { escapeRegex } from '../util.mjs';
6
+
7
+ const STRIP_DIRECTIVES = ['stale-if-error', 'proxy-revalidate', 'must-revalidate'];
8
+
9
+ export const metadata = {
10
+ id: 'vercel-directive-strip',
11
+ description: 'Strip cache-control directives Vercel\'s CDN does not honor.',
12
+ };
13
+
14
+ export function apply(rec, _ctx = {}) {
15
+ const fields = ['fix', 'currentBehavior', 'desiredBehavior'];
16
+ const strippedSet = new Set();
17
+ for (const f of fields) {
18
+ if (typeof rec[f] !== 'string') continue;
19
+ for (const directive of STRIP_DIRECTIVES) {
20
+ const re = new RegExp(`(?:,\\s*)?\\b${escapeRegex(directive)}\\b(?:\\s*,)?`, 'g');
21
+ if (re.test(rec[f])) {
22
+ rec[f] = rec[f]
23
+ .replace(new RegExp(`\\b${escapeRegex(directive)}\\b`, 'g'), '')
24
+ .replace(/,\s*,/g, ',')
25
+ .replace(/(['"])\s*,\s*/g, '$1, ')
26
+ .replace(/,\s*(['"])/g, ', $1')
27
+ .replace(/(['"])\s*,\s*(['"])/g, '$1, $2')
28
+ .replace(/\b(Cache-Control|cache-control)\b:\s*,\s*/g, '$1: ')
29
+ .replace(/(['"])\s*,\s*\1/g, '$1');
30
+ strippedSet.add(directive);
31
+ }
32
+ }
33
+ }
34
+ const stripped = [...strippedSet];
35
+ if (stripped.length === 0) return {};
36
+ return { tags: stripped.map((d) => `vercel-directive-strip:${d}`) };
37
+ }
@@ -0,0 +1,32 @@
1
+ // The metrics window is fixed by collect-signals (currently 14d). Do not let
2
+ // agent prose turn observed counts into monthly counts.
3
+
4
+ import { normalizeObservedWindowUnits } from '../display-labels.mjs';
5
+
6
+ export const metadata = {
7
+ id: 'window-units',
8
+ description: 'Rewrite observed /mo or monthly count units to /window so reports do not imply extrapolated monthly data.',
9
+ };
10
+
11
+ const STRING_FIELDS = [
12
+ 'what',
13
+ 'why',
14
+ 'fix',
15
+ 'currentBehavior',
16
+ 'desiredBehavior',
17
+ 'verify',
18
+ ];
19
+
20
+ export function apply(rec) {
21
+ const tags = [];
22
+ for (const field of STRING_FIELDS) {
23
+ if (typeof rec?.[field] !== 'string') continue;
24
+ const before = rec[field];
25
+ const after = normalizeObservedWindowUnits(before);
26
+ if (after !== before) {
27
+ rec[field] = after;
28
+ tags.push(`window-units:${field}`);
29
+ }
30
+ }
31
+ return tags.length > 0 ? { tags } : {};
32
+ }
@@ -0,0 +1,109 @@
1
+ // Detects the Cache Components anti-pattern where `'use cache'` doesn't dedupe across
2
+ // separate `<Suspense>` boundaries — each boundary triggers a separate evaluation of the
3
+ // "shared" cached function, multiplying invocations and ISR write pressure.
4
+ //
5
+ // Simplified single-file heuristic (cross-file segment analysis is out of scope):
6
+ // File contains `'use cache'` directive (or `use cache` keyword)
7
+ // AND file has 2+ `<Suspense ...>` boundaries
8
+ // AND a repeated fetch URL or function call appears in the body.
9
+ //
10
+ // False positives are tolerable: the support-topic body recommends a known-good remediation
11
+ // (hoist promise to page, or move to `'use cache: remote'`) whether or not the specific call
12
+ // site is the exact one paying the cost. The verifier abstains when the file structure
13
+ // doesn't match the pitfall.
14
+
15
+ import { lineOf } from '../util.mjs';
16
+
17
+ export const metadata = {
18
+ id: 'cache-components-suspense-dedupe',
19
+ title: "'use cache' with multiple Suspense boundaries on the same data",
20
+ severity: 'medium',
21
+ billingDimension: 'function-duration',
22
+ trafficIndependent: false,
23
+ description:
24
+ "Default `'use cache'` does not dedupe identical calls across separate `<Suspense>` boundaries on the same render. Each boundary re-invokes the cached function, multiplying function-duration cost and inflating ISR write churn when the output is large.",
25
+ fix:
26
+ "Hoist the promise to the page level (`const dataPromise = fetchData()` at the top, passed down to each Suspense child) OR move the shared fetch into a `'use cache: remote'` data-access layer so cross-request and cross-boundary dedupe applies.",
27
+ citations: [
28
+ 'https://nextjs.org/docs/app/api-reference/directives/use-cache',
29
+ 'https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents',
30
+ 'https://nextjs.org/docs/app/guides/migrating-to-cache-components',
31
+ ],
32
+ excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**', '**/*.test.*', '**/*.spec.*'],
33
+ includeGlobs: [
34
+ '**/page.{ts,tsx,js,jsx}',
35
+ '**/layout.{ts,tsx,js,jsx}',
36
+ '**/components/**/*.{tsx,jsx}',
37
+ ],
38
+ };
39
+
40
+ const USE_CACHE_RE = /^[\t ]*['"]use cache['"]/m;
41
+ const SUSPENSE_TAG_RE = /<Suspense\b/g;
42
+ const FETCH_LITERAL_RE = /fetch\s*\(\s*(['"`])([^'"`]{6,200})\1/g;
43
+ // Helper function calls that look like data-fetchers (lowercase camel, no JSX/HTML noise).
44
+ const HELPER_CALL_RE = /\b(get|fetch|load|find|query|read)[A-Z][A-Za-z0-9_]+\s*\(/g;
45
+
46
+ export function scan({ files }) {
47
+ const out = [];
48
+ for (const { path, content } of files) {
49
+ if (!USE_CACHE_RE.test(content)) continue;
50
+
51
+ const suspenseCount = countMatches(content, SUSPENSE_TAG_RE);
52
+ if (suspenseCount < 2) continue;
53
+
54
+ const repeated = findRepeated(content);
55
+ if (repeated.length === 0) continue;
56
+
57
+ // Anchor the finding to the first repeated call site so the customer
58
+ // can locate the duplicate quickly.
59
+ const first = repeated[0];
60
+ out.push({
61
+ pattern: metadata.id,
62
+ file: path,
63
+ line: lineOf(content, first.firstIdx),
64
+ evidence: first.kind === 'fetch'
65
+ ? `fetch("${truncate(first.token, 60)}") called ${first.count}× across Suspense boundaries`
66
+ : `${first.token}() called ${first.count}× across Suspense boundaries`,
67
+ trafficIndependent: metadata.trafficIndependent,
68
+ subtype: first.kind === 'fetch' ? 'fetch-literal' : 'helper-call',
69
+ });
70
+ }
71
+ return out;
72
+ }
73
+
74
+ function countMatches(content, re) {
75
+ re.lastIndex = 0;
76
+ let n = 0;
77
+ while (re.exec(content) !== null) n++;
78
+ return n;
79
+ }
80
+
81
+ function findRepeated(content) {
82
+ const tokens = new Map(); // token -> { kind, count, firstIdx }
83
+ let m;
84
+ FETCH_LITERAL_RE.lastIndex = 0;
85
+ while ((m = FETCH_LITERAL_RE.exec(content)) !== null) {
86
+ record(tokens, m[2], 'fetch', m.index);
87
+ }
88
+ HELPER_CALL_RE.lastIndex = 0;
89
+ while ((m = HELPER_CALL_RE.exec(content)) !== null) {
90
+ const name = m[0].replace(/\s*\($/, '').trim();
91
+ record(tokens, name, 'helper', m.index);
92
+ }
93
+ return [...tokens.values()]
94
+ .filter((t) => t.count >= 2)
95
+ .sort((a, b) => b.count - a.count);
96
+ }
97
+
98
+ function record(map, token, kind, idx) {
99
+ if (!token) return;
100
+ if (!map.has(token)) {
101
+ map.set(token, { token, kind, count: 0, firstIdx: idx });
102
+ }
103
+ map.get(token).count++;
104
+ }
105
+
106
+ function truncate(s, n) {
107
+ if (s.length <= n) return s;
108
+ return s.slice(0, n - 1) + '…';
109
+ }
@@ -0,0 +1,94 @@
1
+ // Flag node-only / heavy imports in edge-runtime files (either a
2
+ // middleware basename or an `export const runtime = 'edge'`). These
3
+ // either fail at deploy (node: builtins, native bindings) or inflate
4
+ // cold-start latency. Line-anchored matches + type-only-import skip
5
+ // keep FP low.
6
+
7
+ const EDGE_RUNTIME_RE = /export\s+const\s+runtime\s*=\s*['"]edge['"]/;
8
+ const IMPORT_RE = /^\s*import\s+(?:type\s+)?(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/gm;
9
+ const DYNAMIC_IMPORT_RE = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g;
10
+ const REQUIRE_RE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
11
+ const TYPE_IMPORT_RE = /^\s*import\s+type\s+/;
12
+
13
+ const HEAVY_PATTERNS = [
14
+ /^node:/,
15
+ /^sharp$/,
16
+ /^@aws-sdk\//,
17
+ /^@prisma\/client$/,
18
+ /^prisma$/,
19
+ /^pg$/,
20
+ /^mysql2(?:\/|$)/,
21
+ /^puppeteer(?:-core)?(?:\/|$)/,
22
+ /^playwright(?:-core)?(?:\/|$)/,
23
+ /^bcrypt$/,
24
+ /^jsonwebtoken$/,
25
+ /^canvas$/,
26
+ /^@google-cloud\//,
27
+ ];
28
+
29
+ export const metadata = {
30
+ id: 'edge-heavy-import',
31
+ title: 'Heavy / node-only import inside edge-runtime file',
32
+ severity: 'high',
33
+ billingDimension: 'function-duration',
34
+ trafficIndependent: true,
35
+ description:
36
+ 'Edge runtime is a constrained sandbox with no node: builtins and a much smaller cold-start budget than Node functions. Heavy SDKs (sharp, @aws-sdk/*, @prisma/client, pg, puppeteer) either fail at deploy or inflate cold-start latency. Move the import to a Node runtime function, or replace with an edge-compatible alternative (e.g., neon-driver instead of pg).',
37
+ fix:
38
+ 'Either (a) drop the `export const runtime = \'edge\'` so the route runs on Node (default in 2026), or (b) replace the heavy import with an edge-compatible alternative. For DB: use @neondatabase/serverless or @planetscale/database instead of pg/mysql2. For image: do the work in a Node route handler. For auth signing: use jose (Web Crypto) instead of jsonwebtoken.',
39
+ citations: [
40
+ 'https://vercel.com/docs/functions/runtimes/edge-runtime',
41
+ 'https://vercel.com/docs/fluid-compute',
42
+ ],
43
+ excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**', '**/*.test.*', '**/*.spec.*'],
44
+ includeGlobs: ['**/*.{ts,tsx,js,mjs}'],
45
+ };
46
+
47
+ export function scan({ files }) {
48
+ const out = [];
49
+ for (const { path, content } of files) {
50
+ if (!isEdgeRuntimeFile(path, content)) continue;
51
+ const lines = content.split('\n');
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i];
54
+ // Type-only imports are erased at compile, never reach runtime.
55
+ if (TYPE_IMPORT_RE.test(line)) continue;
56
+ const specifiers = extractSpecifiers(line);
57
+ for (const spec of specifiers) {
58
+ const match = HEAVY_PATTERNS.find((re) => re.test(spec));
59
+ if (!match) continue;
60
+ out.push({
61
+ pattern: metadata.id,
62
+ file: path,
63
+ line: i + 1,
64
+ evidence: `import "${spec}" in edge-runtime file`,
65
+ edgeReason: isMiddleware(path) ? 'middleware (always edge)' : 'export const runtime = "edge"',
66
+ importedModule: spec,
67
+ trafficIndependent: metadata.trafficIndependent,
68
+ });
69
+ }
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+
75
+ function isEdgeRuntimeFile(path, content) {
76
+ return isMiddleware(path) || EDGE_RUNTIME_RE.test(content);
77
+ }
78
+
79
+ function isMiddleware(path) {
80
+ return /(?:^|\/)middleware\.(ts|tsx|js|mjs)$/.test(path);
81
+ }
82
+
83
+ function extractSpecifiers(line) {
84
+ const out = new Set();
85
+ // IMPORT_RE has `gm` flag — reset lastIndex per call.
86
+ IMPORT_RE.lastIndex = 0;
87
+ let m;
88
+ while ((m = IMPORT_RE.exec(line)) !== null) out.add(m[1]);
89
+ DYNAMIC_IMPORT_RE.lastIndex = 0;
90
+ while ((m = DYNAMIC_IMPORT_RE.exec(line)) !== null) out.add(m[1]);
91
+ REQUIRE_RE.lastIndex = 0;
92
+ while ((m = REQUIRE_RE.exec(line)) !== null) out.add(m[1]);
93
+ return [...out];
94
+ }
@@ -0,0 +1,42 @@
1
+ export const metadata = {
2
+ id: 'force-dynamic',
3
+ title: "export const dynamic = 'force-dynamic'",
4
+ severity: 'medium',
5
+ billingDimension: 'function-duration',
6
+ trafficIndependent: false,
7
+ description:
8
+ "force-dynamic disables static + ISR rendering. The route runs the function on every request. Sometimes necessary (cookies, headers, real-time data), often a habit that costs function-duration and edge-requests at scale.",
9
+ fix:
10
+ "Audit the route. If dynamic behavior comes from cookies()/headers()/searchParams, force-dynamic may be redundant — Next infers dynamic automatically. Consider revalidate / 'use cache' / generateStaticParams if any portion can be pre-rendered.",
11
+ citations: [
12
+ 'https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config',
13
+ ],
14
+ excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**'],
15
+ includeGlobs: ['**/route.{ts,tsx,js,jsx}', '**/page.{ts,tsx,js,jsx}'],
16
+ };
17
+
18
+ const RE = /export\s+const\s+dynamic\s*=\s*["']force-dynamic["']/;
19
+
20
+ export function scan({ files }) {
21
+ const out = [];
22
+ for (const { path, content } of files) {
23
+ if (!isApplicable(path)) continue;
24
+ const m = RE.exec(content);
25
+ if (m) {
26
+ out.push({
27
+ pattern: metadata.id,
28
+ file: path,
29
+ line: lineOf(content, m.index),
30
+ evidence: 'export const dynamic = "force-dynamic"',
31
+ trafficIndependent: metadata.trafficIndependent,
32
+ });
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ import { lineOf } from '../util.mjs';
39
+
40
+ function isApplicable(path) {
41
+ return /(\/route|\/page)\.(tsx?|jsx?)$/.test(path);
42
+ }
@@ -0,0 +1,44 @@
1
+ import { lineOf } from '../util.mjs';
2
+
3
+ export const metadata = {
4
+ id: 'headers-in-page',
5
+ title: 'Dynamic API call forcing dynamic rendering',
6
+ severity: 'medium',
7
+ billingDimension: 'function-duration',
8
+ trafficIndependent: false,
9
+ description:
10
+ 'headers(), cookies(), and draftMode() are dynamic APIs. Reading them in a page/layout makes the entire segment dynamic — no ISR, no static generation, and a function invocation on every request.',
11
+ fix:
12
+ 'Move the dynamic API call into a child Server Component that lives inside a Suspense boundary. The parent can stay static; only the leaf re-renders dynamically.',
13
+ citations: [
14
+ 'https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config',
15
+ 'https://nextjs.org/docs/app/building-your-application/caching',
16
+ ],
17
+ excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**'],
18
+ includeGlobs: ['**/{page,layout,template}.{tsx,jsx}'],
19
+ };
20
+
21
+ const RE = /\b(cookies|headers|draftMode)\s*\(\s*\)/g;
22
+
23
+ export function scan({ files }) {
24
+ const out = [];
25
+ for (const { path, content } of files) {
26
+ if (!isApplicable(path)) continue;
27
+ let m;
28
+ RE.lastIndex = 0;
29
+ while ((m = RE.exec(content)) !== null) {
30
+ out.push({
31
+ pattern: metadata.id,
32
+ file: path,
33
+ line: lineOf(content, m.index),
34
+ evidence: `${m[1]}()`,
35
+ trafficIndependent: metadata.trafficIndependent,
36
+ });
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function isApplicable(path) {
43
+ return /\/(page|layout|template)\.(tsx|jsx)$/.test(path);
44
+ }
@@ -0,0 +1,35 @@
1
+ import * as unoptimizedImage from './unoptimized-image.mjs';
2
+ import * as forceDynamic from './force-dynamic.mjs';
3
+ import * as middlewareBroad from './middleware-broad-matcher.mjs';
4
+ import * as missingCacheHeaders from './missing-cache-headers.mjs';
5
+ import * as maxAgeNoSMaxage from './max-age-without-s-maxage.mjs';
6
+ import * as headersInPage from './headers-in-page.mjs';
7
+ import * as sourceMapsProd from './source-maps-production.mjs';
8
+ import * as prismaIncludeTree from './prisma-include-tree.mjs';
9
+ import * as sveltekitPrerenderMissing from './sveltekit-prerender-missing.mjs';
10
+ import * as largeStaticAsset from './large-static-asset.mjs';
11
+ import * as edgeHeavyImport from './edge-heavy-import.mjs';
12
+ import * as useCacheDateStamp from './use-cache-date-stamp.mjs';
13
+ import * as cacheComponentsSuspenseDedupe from './cache-components-suspense-dedupe.mjs';
14
+ import * as turboForceBypass from './turbo-force-bypass.mjs';
15
+ import * as regionPinInConfig from './region-pin-in-config.mjs';
16
+
17
+ // `use-client-cascade` is intentionally NOT registered: 0.3% conversion
18
+ // rate, and client-bundle size isn't billed on Vercel.
19
+ export const scanners = [
20
+ unoptimizedImage,
21
+ forceDynamic,
22
+ middlewareBroad,
23
+ missingCacheHeaders,
24
+ maxAgeNoSMaxage,
25
+ headersInPage,
26
+ sourceMapsProd,
27
+ prismaIncludeTree,
28
+ sveltekitPrerenderMissing,
29
+ largeStaticAsset,
30
+ edgeHeavyImport,
31
+ useCacheDateStamp,
32
+ cacheComponentsSuspenseDedupe,
33
+ turboForceBypass,
34
+ regionPinInConfig,
35
+ ];
@@ -0,0 +1,92 @@
1
+ // Flag oversized assets under public/. Pure fs.stat — no parsing, no LLM.
2
+ // 500 KB threshold is where bandwidth/first-paint start to bite (Vercel
3
+ // Doctor's 4 KB triggers on favicons).
4
+
5
+ import { readdir, stat } from 'node:fs/promises';
6
+ import { join, relative, extname } from 'node:path';
7
+
8
+ const THRESHOLD_BYTES = 500_000;
9
+ const TOP_N = 20;
10
+
11
+ const SKIP_EXTENSIONS = new Set(['.html', '.txt', '.xml', '.json', '.webmanifest', '.ico']);
12
+ const SKIP_PATH_PREFIXES = ['.well-known/'];
13
+
14
+ export const metadata = {
15
+ id: 'large-static-asset',
16
+ title: 'Large file in public/',
17
+ severity: 'medium',
18
+ billingDimension: 'bandwidth',
19
+ trafficIndependent: true,
20
+ description:
21
+ 'Static assets in `public/` over 500 KB ship as-is from the CDN. Whether the cost is meaningful depends on traffic, but the candidate is binary — the file is either needed at that size or it can be optimized (compressed image, video transcode, or moved off the critical path).',
22
+ fix:
23
+ 'Verify the asset is reachable on the customer-facing hot path. Then choose: (a) compress (convert PNG → AVIF/WebP; transcode MP4 to lower bitrate); (b) host externally (Vercel Blob, S3, or a media CDN with per-asset signed URLs); (c) lazy-load (defer to client-side fetch instead of bundling into initial HTML).',
24
+ citations: [
25
+ 'https://vercel.com/docs/manage-cdn-usage',
26
+ 'https://vercel.com/docs/image-optimization',
27
+ ],
28
+ excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**'],
29
+ includeGlobs: ['public/**/*'],
30
+ };
31
+
32
+ // Walks public/ directly because collectFiles only emits text-readable
33
+ // extensions — binary assets never reach the shared `files` array.
34
+ export async function scan({ rootDir }) {
35
+ if (!rootDir) return [];
36
+ const root = join(rootDir, 'public');
37
+ const out = [];
38
+ try {
39
+ for await (const entry of walk(root)) {
40
+ if (shouldSkip(entry.relPath)) continue;
41
+ if (entry.size < THRESHOLD_BYTES) continue;
42
+ out.push({
43
+ pattern: metadata.id,
44
+ file: join('public', entry.relPath),
45
+ line: 1,
46
+ evidence: `${formatBytes(entry.size)} (${extname(entry.relPath) || 'no-ext'})`,
47
+ trafficIndependent: metadata.trafficIndependent,
48
+ sizeBytes: entry.size,
49
+ });
50
+ }
51
+ } catch {
52
+ return [];
53
+ }
54
+ out.sort((a, b) => b.sizeBytes - a.sizeBytes);
55
+ return out.slice(0, TOP_N);
56
+ }
57
+
58
+ async function* walk(dir, base = '') {
59
+ let entries;
60
+ try {
61
+ entries = await readdir(dir, { withFileTypes: true });
62
+ } catch {
63
+ return;
64
+ }
65
+ for (const e of entries) {
66
+ const full = join(dir, e.name);
67
+ const rel = base ? `${base}/${e.name}` : e.name;
68
+ if (e.isDirectory()) {
69
+ yield* walk(full, rel);
70
+ continue;
71
+ }
72
+ if (!e.isFile()) continue;
73
+ try {
74
+ const s = await stat(full);
75
+ yield { relPath: rel, size: s.size };
76
+ } catch { /* skip unreadable */ }
77
+ }
78
+ }
79
+
80
+ function shouldSkip(relPath) {
81
+ if (SKIP_PATH_PREFIXES.some((p) => relPath.startsWith(p))) return true;
82
+ const ext = extname(relPath).toLowerCase();
83
+ if (SKIP_EXTENSIONS.has(ext)) return true;
84
+ return false;
85
+ }
86
+
87
+ function formatBytes(b) {
88
+ if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB';
89
+ if (b >= 1e6) return (b / 1e6).toFixed(2) + ' MB';
90
+ if (b >= 1e3) return (b / 1e3).toFixed(1) + ' KB';
91
+ return b + ' B';
92
+ }