opencode-skills-collection 3.0.35 → 3.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +15 -1
  2. package/bundled-skills/accesslint-audit/SKILL.md +115 -0
  3. package/bundled-skills/accesslint-diff/SKILL.md +81 -0
  4. package/bundled-skills/accesslint-scan/SKILL.md +47 -0
  5. package/bundled-skills/composition-patterns/SKILL.md +87 -0
  6. package/bundled-skills/composition-patterns/rules/_sections.md +29 -0
  7. package/bundled-skills/composition-patterns/rules/_template.md +24 -0
  8. package/bundled-skills/composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
  9. package/bundled-skills/composition-patterns/rules/architecture-compound-components.md +112 -0
  10. package/bundled-skills/composition-patterns/rules/patterns-children-over-render-props.md +87 -0
  11. package/bundled-skills/composition-patterns/rules/patterns-explicit-variants.md +100 -0
  12. package/bundled-skills/composition-patterns/rules/react19-no-forwardref.md +42 -0
  13. package/bundled-skills/composition-patterns/rules/state-context-interface.md +191 -0
  14. package/bundled-skills/composition-patterns/rules/state-decouple-implementation.md +113 -0
  15. package/bundled-skills/composition-patterns/rules/state-lift-state.md +125 -0
  16. package/bundled-skills/debugging-toolkit/SKILL.md +35 -0
  17. package/bundled-skills/deploy-to-vercel/SKILL.md +304 -0
  18. package/bundled-skills/deploy-to-vercel/resources/deploy-codex.sh +301 -0
  19. package/bundled-skills/deploy-to-vercel/resources/deploy.sh +301 -0
  20. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  21. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  22. package/bundled-skills/docs/maintainers/backups/README-2026-06-02.md +687 -0
  23. package/bundled-skills/docs/maintainers/repo-growth-seo.md +4 -4
  24. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  25. package/bundled-skills/docs/users/bundles.md +245 -1
  26. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  27. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  28. package/bundled-skills/docs/users/getting-started.md +1 -1
  29. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  30. package/bundled-skills/docs/users/plugins.md +21 -13
  31. package/bundled-skills/docs/users/specialized-plugin-roadmap.md +95 -0
  32. package/bundled-skills/docs/users/usage.md +4 -4
  33. package/bundled-skills/docs/users/visual-guide.md +4 -4
  34. package/bundled-skills/polis-protocol/SKILL.md +93 -0
  35. package/bundled-skills/python-development/SKILL.md +35 -0
  36. package/bundled-skills/radix-ui-design-system/SKILL.md +2 -2
  37. package/bundled-skills/react-native-skills/SKILL.md +120 -0
  38. package/bundled-skills/react-native-skills/rules/_sections.md +86 -0
  39. package/bundled-skills/react-native-skills/rules/_template.md +28 -0
  40. package/bundled-skills/react-native-skills/rules/animation-derived-value.md +53 -0
  41. package/bundled-skills/react-native-skills/rules/animation-gesture-detector-press.md +95 -0
  42. package/bundled-skills/react-native-skills/rules/animation-gpu-properties.md +65 -0
  43. package/bundled-skills/react-native-skills/rules/design-system-compound-components.md +66 -0
  44. package/bundled-skills/react-native-skills/rules/fonts-config-plugin.md +71 -0
  45. package/bundled-skills/react-native-skills/rules/imports-design-system-folder.md +68 -0
  46. package/bundled-skills/react-native-skills/rules/js-hoist-intl.md +61 -0
  47. package/bundled-skills/react-native-skills/rules/list-performance-callbacks.md +44 -0
  48. package/bundled-skills/react-native-skills/rules/list-performance-function-references.md +132 -0
  49. package/bundled-skills/react-native-skills/rules/list-performance-images.md +53 -0
  50. package/bundled-skills/react-native-skills/rules/list-performance-inline-objects.md +97 -0
  51. package/bundled-skills/react-native-skills/rules/list-performance-item-expensive.md +94 -0
  52. package/bundled-skills/react-native-skills/rules/list-performance-item-memo.md +82 -0
  53. package/bundled-skills/react-native-skills/rules/list-performance-item-types.md +104 -0
  54. package/bundled-skills/react-native-skills/rules/list-performance-virtualize.md +67 -0
  55. package/bundled-skills/react-native-skills/rules/monorepo-native-deps-in-app.md +46 -0
  56. package/bundled-skills/react-native-skills/rules/monorepo-single-dependency-versions.md +63 -0
  57. package/bundled-skills/react-native-skills/rules/navigation-native-navigators.md +188 -0
  58. package/bundled-skills/react-native-skills/rules/react-compiler-destructure-functions.md +50 -0
  59. package/bundled-skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md +48 -0
  60. package/bundled-skills/react-native-skills/rules/react-state-dispatcher.md +91 -0
  61. package/bundled-skills/react-native-skills/rules/react-state-fallback.md +56 -0
  62. package/bundled-skills/react-native-skills/rules/react-state-minimize.md +65 -0
  63. package/bundled-skills/react-native-skills/rules/rendering-no-falsy-and.md +74 -0
  64. package/bundled-skills/react-native-skills/rules/rendering-text-in-text-component.md +36 -0
  65. package/bundled-skills/react-native-skills/rules/scroll-position-no-state.md +82 -0
  66. package/bundled-skills/react-native-skills/rules/state-ground-truth.md +80 -0
  67. package/bundled-skills/react-native-skills/rules/ui-expo-image.md +66 -0
  68. package/bundled-skills/react-native-skills/rules/ui-image-gallery.md +104 -0
  69. package/bundled-skills/react-native-skills/rules/ui-measure-views.md +78 -0
  70. package/bundled-skills/react-native-skills/rules/ui-menus.md +174 -0
  71. package/bundled-skills/react-native-skills/rules/ui-native-modals.md +77 -0
  72. package/bundled-skills/react-native-skills/rules/ui-pressable.md +61 -0
  73. package/bundled-skills/react-native-skills/rules/ui-safe-area-scroll.md +65 -0
  74. package/bundled-skills/react-native-skills/rules/ui-scrollview-content-inset.md +45 -0
  75. package/bundled-skills/react-native-skills/rules/ui-styling.md +87 -0
  76. package/bundled-skills/skill-issue/SKILL.md +73 -0
  77. package/bundled-skills/tdd-workflows/SKILL.md +35 -0
  78. package/bundled-skills/vercel-cli-with-tokens/SKILL.md +361 -0
  79. package/bundled-skills/vercel-optimize/CONTRIBUTING.md +41 -0
  80. package/bundled-skills/vercel-optimize/SKILL.md +331 -0
  81. package/bundled-skills/vercel-optimize/lib/auth-route.mjs +23 -0
  82. package/bundled-skills/vercel-optimize/lib/budget-summary.mjs +182 -0
  83. package/bundled-skills/vercel-optimize/lib/citations.mjs +139 -0
  84. package/bundled-skills/vercel-optimize/lib/cost-coverage.mjs +143 -0
  85. package/bundled-skills/vercel-optimize/lib/dedup-recs.mjs +325 -0
  86. package/bundled-skills/vercel-optimize/lib/deep-dive.mjs +350 -0
  87. package/bundled-skills/vercel-optimize/lib/display-labels.mjs +185 -0
  88. package/bundled-skills/vercel-optimize/lib/extract-claims.mjs +550 -0
  89. package/bundled-skills/vercel-optimize/lib/framework-support.mjs +67 -0
  90. package/bundled-skills/vercel-optimize/lib/gates/build-minutes-fanout.mjs +69 -0
  91. package/bundled-skills/vercel-optimize/lib/gates/cold-start.mjs +66 -0
  92. package/bundled-skills/vercel-optimize/lib/gates/contract.mjs +79 -0
  93. package/bundled-skills/vercel-optimize/lib/gates/cwv-poor.mjs +87 -0
  94. package/bundled-skills/vercel-optimize/lib/gates/external-api-slow.mjs +55 -0
  95. package/bundled-skills/vercel-optimize/lib/gates/hard-gates.mjs +73 -0
  96. package/bundled-skills/vercel-optimize/lib/gates/index.mjs +45 -0
  97. package/bundled-skills/vercel-optimize/lib/gates/isr-overrevalidation.mjs +62 -0
  98. package/bundled-skills/vercel-optimize/lib/gates/middleware-heavy.mjs +51 -0
  99. package/bundled-skills/vercel-optimize/lib/gates/observability-events-attribution.mjs +56 -0
  100. package/bundled-skills/vercel-optimize/lib/gates/platform-bot-protection.mjs +115 -0
  101. package/bundled-skills/vercel-optimize/lib/gates/platform-fluid-compute.mjs +83 -0
  102. package/bundled-skills/vercel-optimize/lib/gates/region-misconfig.mjs +64 -0
  103. package/bundled-skills/vercel-optimize/lib/gates/route-errors.mjs +80 -0
  104. package/bundled-skills/vercel-optimize/lib/gates/scanner-driven.mjs +122 -0
  105. package/bundled-skills/vercel-optimize/lib/gates/select-candidates.mjs +134 -0
  106. package/bundled-skills/vercel-optimize/lib/gates/slow-route.mjs +88 -0
  107. package/bundled-skills/vercel-optimize/lib/gates/types.d.ts +38 -0
  108. package/bundled-skills/vercel-optimize/lib/gates/uncached-route.mjs +93 -0
  109. package/bundled-skills/vercel-optimize/lib/gates/usage-spike-triage.mjs +121 -0
  110. package/bundled-skills/vercel-optimize/lib/grade-recommendation.mjs +155 -0
  111. package/bundled-skills/vercel-optimize/lib/impact-label.mjs +126 -0
  112. package/bundled-skills/vercel-optimize/lib/impact-magnitude.mjs +60 -0
  113. package/bundled-skills/vercel-optimize/lib/investigation-brief.mjs +610 -0
  114. package/bundled-skills/vercel-optimize/lib/observation-safety.mjs +174 -0
  115. package/bundled-skills/vercel-optimize/lib/project-facts.mjs +99 -0
  116. package/bundled-skills/vercel-optimize/lib/queries.mjs +315 -0
  117. package/bundled-skills/vercel-optimize/lib/reconcile-candidates.mjs +372 -0
  118. package/bundled-skills/vercel-optimize/lib/render-report.mjs +955 -0
  119. package/bundled-skills/vercel-optimize/lib/repo-root.mjs +86 -0
  120. package/bundled-skills/vercel-optimize/lib/route-normalize.mjs +220 -0
  121. package/bundled-skills/vercel-optimize/lib/sanitizers/bot-protection-certainty.mjs +38 -0
  122. package/bundled-skills/vercel-optimize/lib/sanitizers/cache-tag-invalidation-certainty.mjs +30 -0
  123. package/bundled-skills/vercel-optimize/lib/sanitizers/count-correct.mjs +52 -0
  124. package/bundled-skills/vercel-optimize/lib/sanitizers/function-duration-invocations.mjs +38 -0
  125. package/bundled-skills/vercel-optimize/lib/sanitizers/index.mjs +79 -0
  126. package/bundled-skills/vercel-optimize/lib/sanitizers/middleware-conflict.mjs +36 -0
  127. package/bundled-skills/vercel-optimize/lib/sanitizers/missing-citation.mjs +16 -0
  128. package/bundled-skills/vercel-optimize/lib/sanitizers/pre-release.mjs +74 -0
  129. package/bundled-skills/vercel-optimize/lib/sanitizers/rate-limit.mjs +67 -0
  130. package/bundled-skills/vercel-optimize/lib/sanitizers/rendering-mode-mislabel.mjs +38 -0
  131. package/bundled-skills/vercel-optimize/lib/sanitizers/undeclared-dep.mjs +78 -0
  132. package/bundled-skills/vercel-optimize/lib/sanitizers/vercel-directive-strip.mjs +37 -0
  133. package/bundled-skills/vercel-optimize/lib/sanitizers/window-units.mjs +32 -0
  134. package/bundled-skills/vercel-optimize/lib/scanners/cache-components-suspense-dedupe.mjs +109 -0
  135. package/bundled-skills/vercel-optimize/lib/scanners/edge-heavy-import.mjs +94 -0
  136. package/bundled-skills/vercel-optimize/lib/scanners/force-dynamic.mjs +42 -0
  137. package/bundled-skills/vercel-optimize/lib/scanners/headers-in-page.mjs +44 -0
  138. package/bundled-skills/vercel-optimize/lib/scanners/index.mjs +35 -0
  139. package/bundled-skills/vercel-optimize/lib/scanners/large-static-asset.mjs +92 -0
  140. package/bundled-skills/vercel-optimize/lib/scanners/max-age-without-s-maxage.mjs +42 -0
  141. package/bundled-skills/vercel-optimize/lib/scanners/middleware-broad-matcher.mjs +55 -0
  142. package/bundled-skills/vercel-optimize/lib/scanners/missing-cache-headers.mjs +90 -0
  143. package/bundled-skills/vercel-optimize/lib/scanners/prisma-include-tree.mjs +42 -0
  144. package/bundled-skills/vercel-optimize/lib/scanners/region-pin-in-config.mjs +88 -0
  145. package/bundled-skills/vercel-optimize/lib/scanners/source-maps-production.mjs +36 -0
  146. package/bundled-skills/vercel-optimize/lib/scanners/sveltekit-prerender-missing.mjs +43 -0
  147. package/bundled-skills/vercel-optimize/lib/scanners/turbo-force-bypass.mjs +129 -0
  148. package/bundled-skills/vercel-optimize/lib/scanners/unoptimized-image.mjs +113 -0
  149. package/bundled-skills/vercel-optimize/lib/scanners/use-cache-date-stamp.mjs +106 -0
  150. package/bundled-skills/vercel-optimize/lib/support-topics.mjs +355 -0
  151. package/bundled-skills/vercel-optimize/lib/throttle.mjs +273 -0
  152. package/bundled-skills/vercel-optimize/lib/util.mjs +17 -0
  153. package/bundled-skills/vercel-optimize/lib/vercel.mjs +784 -0
  154. package/bundled-skills/vercel-optimize/lib/verify-claim.mjs +1296 -0
  155. package/bundled-skills/vercel-optimize/lib/workspace-resolver.mjs +521 -0
  156. package/bundled-skills/vercel-optimize/references/candidates.md +176 -0
  157. package/bundled-skills/vercel-optimize/references/data-collection.md +218 -0
  158. package/bundled-skills/vercel-optimize/references/docs-library.json +683 -0
  159. package/bundled-skills/vercel-optimize/references/doctrine.md +105 -0
  160. package/bundled-skills/vercel-optimize/references/observability-plus.md +108 -0
  161. package/bundled-skills/vercel-optimize/references/playbooks/README.md +53 -0
  162. package/bundled-skills/vercel-optimize/references/playbooks/ai-application.md +32 -0
  163. package/bundled-skills/vercel-optimize/references/playbooks/api-service.md +30 -0
  164. package/bundled-skills/vercel-optimize/references/playbooks/content-site.md +30 -0
  165. package/bundled-skills/vercel-optimize/references/playbooks/ecommerce.md +30 -0
  166. package/bundled-skills/vercel-optimize/references/playbooks/marketing.md +30 -0
  167. package/bundled-skills/vercel-optimize/references/playbooks/saas.md +31 -0
  168. package/bundled-skills/vercel-optimize/references/playbooks/sveltekit.md +75 -0
  169. package/bundled-skills/vercel-optimize/references/recommendations.md +203 -0
  170. package/bundled-skills/vercel-optimize/references/scanner-patterns.md +251 -0
  171. package/bundled-skills/vercel-optimize/references/scoring.md +205 -0
  172. package/bundled-skills/vercel-optimize/references/support-topics/README.md +46 -0
  173. package/bundled-skills/vercel-optimize/references/support-topics/astro-edge-middleware-scope.md +22 -0
  174. package/bundled-skills/vercel-optimize/references/support-topics/astro-output-mode-and-isr.md +22 -0
  175. package/bundled-skills/vercel-optimize/references/support-topics/auth-preserving-parallelization.md +22 -0
  176. package/bundled-skills/vercel-optimize/references/support-topics/bot-protection-product-guardrails.md +22 -0
  177. package/bundled-skills/vercel-optimize/references/support-topics/build-minutes-monorepo-fanout.md +23 -0
  178. package/bundled-skills/vercel-optimize/references/support-topics/cache-components-static-shell-boundaries.md +22 -0
  179. package/bundled-skills/vercel-optimize/references/support-topics/cache-components-suspense-dedupe-pitfall.md +23 -0
  180. package/bundled-skills/vercel-optimize/references/support-topics/cdn-cache-auth-safety.md +22 -0
  181. package/bundled-skills/vercel-optimize/references/support-topics/cold-start-initialization-bundle.md +22 -0
  182. package/bundled-skills/vercel-optimize/references/support-topics/core-web-vitals-client-bottlenecks.md +22 -0
  183. package/bundled-skills/vercel-optimize/references/support-topics/database-egress-pooling-region.md +22 -0
  184. package/bundled-skills/vercel-optimize/references/support-topics/dynamic-rendering-traps.md +22 -0
  185. package/bundled-skills/vercel-optimize/references/support-topics/external-api-critical-path-platform.md +22 -0
  186. package/bundled-skills/vercel-optimize/references/support-topics/external-api-critical-path.md +22 -0
  187. package/bundled-skills/vercel-optimize/references/support-topics/fast-data-transfer-payloads.md +22 -0
  188. package/bundled-skills/vercel-optimize/references/support-topics/fluid-compute-caveats.md +22 -0
  189. package/bundled-skills/vercel-optimize/references/support-topics/function-duration-io-and-after.md +22 -0
  190. package/bundled-skills/vercel-optimize/references/support-topics/function-invocation-reduction.md +22 -0
  191. package/bundled-skills/vercel-optimize/references/support-topics/function-region-misconfiguration-ttfb.md +23 -0
  192. package/bundled-skills/vercel-optimize/references/support-topics/image-optimization-cost-control.md +22 -0
  193. package/bundled-skills/vercel-optimize/references/support-topics/isr-revalidation-static-generation.md +22 -0
  194. package/bundled-skills/vercel-optimize/references/support-topics/middleware-proxy-edge-cost.md +22 -0
  195. package/bundled-skills/vercel-optimize/references/support-topics/next-fetch-revalidate-floor.md +22 -0
  196. package/bundled-skills/vercel-optimize/references/support-topics/next-font-cls-self-hosting.md +23 -0
  197. package/bundled-skills/vercel-optimize/references/support-topics/next-heavy-ui-lazy-load-boundaries.md +23 -0
  198. package/bundled-skills/vercel-optimize/references/support-topics/next-image-lcp-preload-sizes.md +23 -0
  199. package/bundled-skills/vercel-optimize/references/support-topics/next-route-handler-get-cache-defaults.md +22 -0
  200. package/bundled-skills/vercel-optimize/references/support-topics/next-script-third-party-strategy.md +23 -0
  201. package/bundled-skills/vercel-optimize/references/support-topics/nextjs-version-cache-semantics.md +22 -0
  202. package/bundled-skills/vercel-optimize/references/support-topics/not-found-catchall-request-waste.md +23 -0
  203. package/bundled-skills/vercel-optimize/references/support-topics/nuxt-route-rules-cache-isr.md +22 -0
  204. package/bundled-skills/vercel-optimize/references/support-topics/observability-events-cost-attribution.md +22 -0
  205. package/bundled-skills/vercel-optimize/references/support-topics/post-response-work-waituntil.md +22 -0
  206. package/bundled-skills/vercel-optimize/references/support-topics/route-error-durable-offload.md +22 -0
  207. package/bundled-skills/vercel-optimize/references/support-topics/route-error-runtime-limits.md +22 -0
  208. package/bundled-skills/vercel-optimize/references/support-topics/runtime-cache-reusable-data.md +22 -0
  209. package/bundled-skills/vercel-optimize/references/support-topics/sveltekit-isr-prerender-safety.md +22 -0
  210. package/bundled-skills/vercel-optimize/references/support-topics/sveltekit-split-cold-start-tradeoff.md +22 -0
  211. package/bundled-skills/vercel-optimize/references/support-topics/usage-spike-triage.md +22 -0
  212. package/bundled-skills/vercel-optimize/references/support-topics/use-cache-date-stamp-isr-write-amplifier.md +23 -0
  213. package/bundled-skills/vercel-optimize/references/support-topics/use-cache-remote-shared-origin-data.md +22 -0
  214. package/bundled-skills/vercel-optimize/references/support-topics/workflow-resumable-stream-routes.md +23 -0
  215. package/bundled-skills/vercel-optimize/references/verification.md +102 -0
  216. package/bundled-skills/vercel-optimize/references/voice.md +76 -0
  217. package/bundled-skills/vercel-optimize/scripts/budget-summary.mjs +56 -0
  218. package/bundled-skills/vercel-optimize/scripts/build-docs.mjs +74 -0
  219. package/bundled-skills/vercel-optimize/scripts/check-citations.mjs +81 -0
  220. package/bundled-skills/vercel-optimize/scripts/check-docs-fresh.mjs +93 -0
  221. package/bundled-skills/vercel-optimize/scripts/collect-signals.mjs +576 -0
  222. package/bundled-skills/vercel-optimize/scripts/collect-sub-agent-outputs.mjs +296 -0
  223. package/bundled-skills/vercel-optimize/scripts/deep-dive.mjs +319 -0
  224. package/bundled-skills/vercel-optimize/scripts/gate-investigations.mjs +166 -0
  225. package/bundled-skills/vercel-optimize/scripts/merge-signals.mjs +192 -0
  226. package/bundled-skills/vercel-optimize/scripts/prepare-investigation-brief.mjs +231 -0
  227. package/bundled-skills/vercel-optimize/scripts/reconcile-candidates.mjs +62 -0
  228. package/bundled-skills/vercel-optimize/scripts/render-report.mjs +437 -0
  229. package/bundled-skills/vercel-optimize/scripts/scan-codebase.mjs +313 -0
  230. package/bundled-skills/vercel-optimize/scripts/verify-and-regen.mjs +346 -0
  231. package/bundled-skills/vercel-optimize/scripts/verify-finding.mjs +19 -0
  232. package/bundled-skills/vercel-react-view-transitions/SKILL.md +327 -0
  233. package/bundled-skills/vercel-react-view-transitions/references/css-recipes.md +242 -0
  234. package/bundled-skills/vercel-react-view-transitions/references/implementation.md +182 -0
  235. package/bundled-skills/vercel-react-view-transitions/references/nextjs.md +176 -0
  236. package/bundled-skills/vercel-react-view-transitions/references/patterns.md +262 -0
  237. package/package.json +1 -1
  238. package/skills_index.json +312 -0
@@ -0,0 +1,955 @@
1
+ // Deterministic markdown renderer. Same inputs → byte-identical output (modulo caller-supplied generatedAt).
2
+
3
+ import { createHash } from 'node:crypto';
4
+ import { computeImpactLabel } from './impact-label.mjs';
5
+ import { deriveProjectFacts } from './project-facts.mjs';
6
+ import { canonicalizeRoute } from './route-normalize.mjs';
7
+ import { computeCostCoverage, renderCostCoverageMarkdown } from './cost-coverage.mjs';
8
+ import { gates as registeredGates } from './gates/index.mjs';
9
+ import { formatCandidateLabel, formatKind, formatPublicText, formatRoute, formatSignal } from './display-labels.mjs';
10
+ import { splitCustomerSafeObservations } from './observation-safety.mjs';
11
+
12
+ const PLATFORM_CAP = 3;
13
+ const GATED_TARGET_PREVIEW = 5;
14
+
15
+ export function renderReport({ recommendations = [], gated = [], abstentions = [], observations = [], signals = {}, candidates = [], opts = {} } = {}) {
16
+ const safety = splitCustomerSafeObservations(observations, abstentions, signals);
17
+ observations = safety.observations;
18
+ abstentions = [...abstentions, ...safety.heldBackObservations];
19
+ assertValidObservations(observations);
20
+
21
+ const projectName = opts.projectName ?? signals.project?.name ?? '<project>';
22
+ const stack = signals.stack ?? signals.codebase?.stack ?? {};
23
+ const usage = signals.usage ?? null;
24
+ const plan = signals.plan ?? { plan: 'unknown', reason: '(not detected)' };
25
+
26
+ // Sub-agents don't always propagate o11ySignal/aliasRoutes — look them up by candidateRef and canonicalize the displayed ref.
27
+ recommendations = recommendations.map((r) => enrichRecFromCandidates(r, candidates));
28
+ const { needsEvidenceRows, noChangeRows } = splitInvestigationOutcomes(abstentions);
29
+
30
+ const lines = [];
31
+ lines.push(`# Vercel Optimization Report — ${projectName}`);
32
+ lines.push('');
33
+ lines.push(renderMetadataLine(stack, plan, usage, signals));
34
+ const coverageLine = renderCoverageLine(candidates, recommendations, signals, {
35
+ abstentions,
36
+ heldBackCount: opts.heldBackCount,
37
+ noChangeCount: opts.noChangeCount,
38
+ });
39
+ if (coverageLine) lines.push(coverageLine);
40
+ if (opts.generatedAt) {
41
+ lines.push('');
42
+ lines.push(`_Generated ${opts.generatedAt}_`);
43
+ }
44
+ lines.push('');
45
+
46
+ lines.push(...renderCostHeader(signals));
47
+ lines.push('');
48
+ lines.push(...renderCostBreakdown(usage, signals));
49
+ if (usage) {
50
+ const coverage = computeCostCoverage(usage, registeredGates);
51
+ lines.push(...renderCostCoverageMarkdown(coverage));
52
+ }
53
+ lines.push('');
54
+
55
+ const platformRecs = recommendations.filter(isPlatformScope).slice(0, PLATFORM_CAP);
56
+ const codeRecs = recommendations.filter((r) => !isPlatformScope(r));
57
+ const sorted = sortRecs(codeRecs);
58
+ const high = sorted.filter((r) => r.impactTier === 'high');
59
+ const medium = sorted.filter((r) => r.impactTier === 'medium');
60
+ const low = sorted.filter((r) => r.impactTier === 'low' || !r.impactTier);
61
+ lines.push('## Highest-impact recommendations');
62
+ lines.push('');
63
+ if (sorted.length === 0) {
64
+ lines.push('_No recommendations are ready to apply from this run._');
65
+ } else {
66
+ const top = sorted.slice(0, 5);
67
+ top.forEach((rec, i) => {
68
+ const candidate = candidateForDisplay(rec);
69
+ const signal = formatSignal(rec.o11ySignal ?? signalFromRec(rec) ?? '', candidate);
70
+ lines.push(`${i + 1}. **${formatCandidateLabel(candidate)}** — ${signal}`);
71
+ lines.push(` - **What to do**: ${formatRecommendationText(rec.what ?? '')}`);
72
+ lines.push(` - **Impact**: ${formatRecommendationText(impactString(rec, signals))}`);
73
+ if (rec.effort) lines.push(` - **Effort**: ${rec.effort}`);
74
+ const cites = asArray(rec.citations);
75
+ if (cites.length > 0) lines.push(` - **Citations**: ${cites.join(', ')}`);
76
+ });
77
+ }
78
+ lines.push('');
79
+
80
+ lines.push('## Recommendations');
81
+ lines.push('');
82
+ lines.push('### High impact');
83
+ lines.push('');
84
+ lines.push(...renderRecTable(high, signals));
85
+ lines.push('');
86
+ lines.push('### Medium impact');
87
+ lines.push('');
88
+ lines.push(...renderRecTable(medium, signals));
89
+ lines.push('');
90
+ if (low.length > 0) {
91
+ lines.push('### Low impact');
92
+ lines.push('');
93
+ lines.push(...renderRecTable(low, signals));
94
+ lines.push('');
95
+ }
96
+
97
+ lines.push('## Detailed recommendations');
98
+ lines.push('');
99
+ if (sorted.length === 0) {
100
+ lines.push('_No recommendations are ready to apply from this run._');
101
+ } else {
102
+ for (const [i, rec] of sorted.entries()) {
103
+ lines.push(...renderRecDetail(rec, i + 1, { signals }));
104
+ }
105
+ }
106
+ lines.push('');
107
+
108
+ lines.push('## Platform recommendations');
109
+ lines.push('');
110
+ if (platformRecs.length === 0) {
111
+ lines.push('_(none — the gate did not surface any platform-scope recommendations)_');
112
+ } else {
113
+ for (const [i, rec] of platformRecs.entries()) {
114
+ lines.push(...renderRecDetail(rec, i + 1, { compact: true, signals }));
115
+ }
116
+ }
117
+ lines.push('');
118
+
119
+ // Observations carry actionable signal discovered during investigation.
120
+ if (observations.length > 0) {
121
+ lines.push('## Observations from investigation');
122
+ lines.push('');
123
+ lines.push('These are real signals from the audit, but they are not ready-to-apply recommendations.');
124
+ lines.push('');
125
+ lines.push('| Candidate | Observation | Evidence | Suggested action | Kind |');
126
+ lines.push('|---|---|---|---|---|');
127
+ for (const o of observations) {
128
+ const ref = o.candidateRef ?? '(unspecified)';
129
+ lines.push(`| ${escape(displayCandidateRef(ref))} | ${escape(formatEvidenceText(o.summary))} | ${escape(formatEvidenceText(o.evidence ?? '_(none recorded)_'))} | ${escape(formatEvidenceText(o.suggestedAction ?? '_(none recorded)_'))} | ${escape(formatKind(o.kind ?? 'other'))} |`);
130
+ }
131
+ lines.push('');
132
+ }
133
+
134
+ // Trust mechanism: customer sees what was investigated and why no rec emerged.
135
+ if (needsEvidenceRows.length > 0) {
136
+ lines.push('## Needs more evidence');
137
+ lines.push('');
138
+ lines.push('These candidates were investigated, but automated checks kept the change out of the ready-to-apply list.');
139
+ lines.push('');
140
+ lines.push('| Candidate | Why it was held back |');
141
+ lines.push('|---|---|');
142
+ for (const a of needsEvidenceRows) {
143
+ const ref = a.candidateRef ?? '(unspecified)';
144
+ const reason = publicNoRecommendationReason(a.reason ?? '(no reason recorded)');
145
+ lines.push(`| ${escape(displayCandidateRef(ref))} | ${escape(reason)} |`);
146
+ }
147
+ lines.push('');
148
+ }
149
+
150
+ if (noChangeRows.length > 0) {
151
+ lines.push('## Investigated, no change recommended');
152
+ lines.push('');
153
+ lines.push('These candidates were checked and did not produce a supported change.');
154
+ lines.push('');
155
+ lines.push('| Candidate | Why no recommendation shipped |');
156
+ lines.push('|---|---|');
157
+ for (const a of noChangeRows) {
158
+ const ref = a.candidateRef ?? '(unspecified)';
159
+ const reason = publicNoRecommendationReason(a.reason ?? '(no reason recorded)');
160
+ lines.push(`| ${escape(displayCandidateRef(ref))} | ${escape(reason)} |`);
161
+ }
162
+ lines.push('');
163
+ }
164
+
165
+ lines.push('## Not investigated in this run');
166
+ lines.push('');
167
+ lines.push(...renderGatedTable(gated));
168
+ lines.push('');
169
+
170
+ lines.push('## Strengths');
171
+ lines.push('');
172
+ lines.push(...renderStrengths(signals));
173
+ lines.push('');
174
+
175
+ const configNotes = renderConfigurationNotes(signals);
176
+ if (configNotes.length > 0) {
177
+ lines.push('## Configuration notes');
178
+ lines.push('');
179
+ lines.push(...configNotes);
180
+ lines.push('');
181
+ }
182
+
183
+ lines.push('## Data gaps');
184
+ lines.push('');
185
+ lines.push(...renderDataGaps(signals));
186
+
187
+ return lines.join('\n');
188
+ }
189
+
190
+ function assertValidObservations(observations) {
191
+ if (!Array.isArray(observations)) {
192
+ throw new TypeError('renderReport observations must be an array');
193
+ }
194
+ for (const [i, o] of observations.entries()) {
195
+ if (!o || typeof o !== 'object') {
196
+ throw new TypeError(`renderReport observations[${i}] must be an object`);
197
+ }
198
+ if (typeof o.summary !== 'string' || o.summary.trim() === '') {
199
+ throw new TypeError(`renderReport observations[${i}].summary is required`);
200
+ }
201
+ }
202
+ }
203
+
204
+ export function buildFinalReportMessage({ reportPath, markdown, recommendations = [], signals = {}, maxRecommendations = 10 } = {}) {
205
+ const destination = reportPath || 'report.md';
206
+ const coverageLine = extractCoverageLine(markdown);
207
+ const lines = [`Report saved: ${destination}`];
208
+ if (coverageLine) {
209
+ lines.push('');
210
+ lines.push(stripDetailsLink(coverageLine));
211
+ } else {
212
+ lines.push('');
213
+ lines.push('Open the report for details. No coverage summary was available.');
214
+ }
215
+ const readyPreview = renderFinalRecommendationPreview(recommendations, signals, maxRecommendations);
216
+ if (readyPreview.length > 0) {
217
+ lines.push('');
218
+ lines.push(...readyPreview);
219
+ }
220
+ const body = lines.join('\n');
221
+ return {
222
+ body,
223
+ lineCount: lines.length,
224
+ sha256: createHash('sha256').update(body).digest('hex'),
225
+ reportPath: destination,
226
+ coverageLine: coverageLine ?? null,
227
+ recommendationsShown: readyPreview.filter((line) => /^\d+\./.test(line)).length,
228
+ };
229
+ }
230
+
231
+ function renderFinalRecommendationPreview(recommendations, signals, maxRecommendations) {
232
+ const ready = Array.isArray(recommendations)
233
+ ? sortRecs(recommendations.filter((r) => r && r.abstain !== true))
234
+ : [];
235
+ if (ready.length === 0) return [];
236
+ const max = Math.max(1, Math.min(Number.isInteger(maxRecommendations) ? maxRecommendations : 5, 10));
237
+ const shown = ready.slice(0, max);
238
+ const lines = ['Ready recommendations:'];
239
+ for (const [i, rec] of shown.entries()) {
240
+ lines.push(`${i + 1}. ${compactFinalText(rec.what ?? displayCandidate(rec))}`);
241
+ const impact = impactString(rec, signals);
242
+ if (impact && !/^_\(no impact framing recorded\)_$/.test(impact)) {
243
+ lines.push(` Impact: ${compactFinalText(impact)}`);
244
+ }
245
+ }
246
+ const hidden = ready.length - shown.length;
247
+ if (hidden > 0) {
248
+ lines.push(`Open the report for ${hidden} more ready recommendation${hidden === 1 ? '' : 's'} and the full evidence.`);
249
+ }
250
+ return lines;
251
+ }
252
+
253
+ function compactFinalText(value) {
254
+ const text = formatRecommendationText(String(value ?? ''))
255
+ .replace(/\s+/g, ' ')
256
+ .trim();
257
+ if (text.length <= 220) return text;
258
+ return `${text.slice(0, 217).trimEnd()}...`;
259
+ }
260
+
261
+ function extractCoverageLine(markdown) {
262
+ if (typeof markdown !== 'string') return null;
263
+ return markdown
264
+ .split('\n')
265
+ .find((line) => line.startsWith('**Coverage**:')) ?? null;
266
+ }
267
+
268
+ function stripDetailsLink(line) {
269
+ return String(line).replace(/\s*·\s*\[details\]\(#not-investigated-in-this-run\)\s*$/, '');
270
+ }
271
+
272
+ // Hidden when no candidates exist (e.g., observability blocker — nothing to cover).
273
+ function renderCoverageLine(candidates, recommendations, signals, opts = {}) {
274
+ if (!Array.isArray(candidates) || candidates.length === 0) return null;
275
+ const launched = candidates.filter((c) => !c.gatedReason && !c.disqualified && c.scope !== 'account');
276
+ const skippedByBudget = candidates.filter(
277
+ (c) => typeof c.gatedReason === 'string' && c.gatedReason.startsWith('skippedByBudget')
278
+ );
279
+ const coveredByDedup = candidates.filter(
280
+ (c) => typeof c.gatedReason === 'string' && c.gatedReason.startsWith('coveredBy')
281
+ );
282
+ const disqualified = candidates.filter(
283
+ (c) => typeof c.gatedReason === 'string' && c.gatedReason === c.disqualifyReason
284
+ );
285
+ const total = launched.length + skippedByBudget.length;
286
+ if (total === 0) return null;
287
+ const parts = [];
288
+ parts.push(`Found **${total}** potential issue${total === 1 ? '' : 's'} to check`);
289
+ parts.push(`${launched.length} investigated`);
290
+ if (skippedByBudget.length > 0) {
291
+ parts.push(`${skippedByBudget.length} left for a larger run — re-run with \`--max-candidates all\` to see the rest`);
292
+ }
293
+ if (coveredByDedup.length > 0) {
294
+ parts.push(`${coveredByDedup.length} similar route variant${coveredByDedup.length === 1 ? '' : 's'} grouped`);
295
+ }
296
+ const recCount = (recommendations ?? []).filter((r) => !r.abstain && !isPlatformScope(r)).length;
297
+ parts.push(`${recCount} recommendation${recCount === 1 ? '' : 's'} ready`);
298
+ const rawHeldBackCount = Number.isInteger(opts.heldBackCount)
299
+ ? opts.heldBackCount
300
+ : (Array.isArray(opts.abstentions) ? opts.abstentions.filter((a) => a?.needsEvidence === true).length : 0);
301
+ const heldBackCount = Math.min(rawHeldBackCount, Math.max(0, launched.length - recCount));
302
+ if (heldBackCount > 0) {
303
+ parts.push(`${heldBackCount} need more evidence`);
304
+ }
305
+ const rawNoChangeCount = Number.isInteger(opts.noChangeCount)
306
+ ? opts.noChangeCount
307
+ : (Array.isArray(opts.abstentions) ? opts.abstentions.length : 0);
308
+ const noChangeCount = Math.min(rawNoChangeCount, Math.max(0, launched.length - recCount - heldBackCount));
309
+ if (noChangeCount > 0) {
310
+ parts.push(`${noChangeCount} investigated, no change recommended`);
311
+ }
312
+ return `**Coverage**: ${parts.join(' · ')} · [details](#not-investigated-in-this-run)`;
313
+ }
314
+
315
+ function renderMetadataLine(stack, plan, usage, signals = {}) {
316
+ const fw = `${stack.framework ?? 'unknown'}@${stack.frameworkVersion ?? '?'}`;
317
+ const router = stack.hasAppRouter ? 'app-router' : stack.hasPagesRouter ? 'pages-router' : null;
318
+ const orm = stack.orm && stack.orm !== 'none' ? stack.orm : null;
319
+ const stackParts = [fw, router, orm].filter(Boolean).join(' | ');
320
+ const period = usage?.period
321
+ ? `${usage.period.from ?? '?'} → ${usage.period.to ?? '?'}`
322
+ : '(unavailable)';
323
+ const oplusLabel = observabilityLabel(signals, usage);
324
+ // Plan-inference reason is debug detail — only surface when plan is uncertain.
325
+ const planLabel = plan.plan === 'uncertain'
326
+ ? `${plan.plan} (${plan.reason ?? 'no signal'})`
327
+ : (plan.plan ?? 'unknown');
328
+ return `**Stack**: ${stackParts} · **Plan**: ${planLabel} · **Period**: ${period} · **Observability**: ${oplusLabel}`;
329
+ }
330
+
331
+ function observabilityLabel(signals, usage) {
332
+ if (signals.observabilityPlusUsable === true) {
333
+ return 'Observability Plus enabled — per-route metrics included';
334
+ }
335
+ if (signals.observabilityPlusUsable === false) {
336
+ if (usage) {
337
+ return 'Per-route metrics unavailable — analysis based on billing + scanner findings';
338
+ }
339
+ if (signals.usageError === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED') {
340
+ return 'Per-route metrics unavailable — audit paused before metric-backed route ranking';
341
+ }
342
+ return 'Per-route metrics unavailable — limited analysis based on scanner findings';
343
+ }
344
+ if (signals.observabilityPlus === true) {
345
+ return 'Observability Plus enabled — per-route metrics included';
346
+ }
347
+ if (signals.usageError === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK') {
348
+ return 'Not checked — audit paused at unsupported-framework preflight';
349
+ }
350
+ if (signals.usageError === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED') {
351
+ return 'Per-route metrics unavailable — audit paused before metric-backed route ranking';
352
+ }
353
+ if (usage) {
354
+ return 'Not enabled — analysis based on billing + scanner findings';
355
+ }
356
+ if (signals.observabilityPlus === false) {
357
+ return 'Not enabled — limited analysis only';
358
+ }
359
+ return 'Not checked — limited analysis only';
360
+ }
361
+
362
+ function renderCostHeader(signals) {
363
+ const scope = signals.usageScope;
364
+ if (scope === 'project') {
365
+ return ['## Cost breakdown (this project)'];
366
+ }
367
+ if (scope === 'team' && signals.usage) {
368
+ return [
369
+ '## Cost breakdown (team-wide — `vercel usage` has no per-project filter)',
370
+ '',
371
+ '_The Vercel CLI\'s `vercel usage` reports team-wide billing without a project filter (verified May 2026). This breakdown is the whole team\'s bill for the window. Per-route metrics in the rest of this report are project-scoped via `vercel metrics`._',
372
+ ];
373
+ }
374
+ return ['## Cost breakdown'];
375
+ }
376
+
377
+ function renderCostBreakdown(usage, signals) {
378
+ const lines = [];
379
+ const services = Array.isArray(usage?.services) ? usage.services : null;
380
+ if (services && services.length > 0) {
381
+ const chargedRows = services.filter((s) => {
382
+ const cost = serviceCost(s);
383
+ return cost === null || costRoundsToCents(cost) > 0;
384
+ });
385
+ if (chargedRows.length > 0) {
386
+ return renderServiceCostRows(chargedRows, {
387
+ costLabel: 'Billed cost',
388
+ costOf: serviceCost,
389
+ omittedZeroRows: services.length - chargedRows.length,
390
+ total: usage.totals?.billedCost,
391
+ totalLabel: 'Total billed',
392
+ totalSuffix: ' _(precise observed cost; future-savings framing is magnitude, never precise)_',
393
+ });
394
+ }
395
+
396
+ const effectiveRows = services.filter((s) => costRoundsToCents(serviceEffectiveCost(s)) > 0);
397
+ if (effectiveRows.length > 0) {
398
+ lines.push('_Net billed cost is $0.00 after included credits or allotments. Showing effective usage cost so active cost drivers are still visible._');
399
+ lines.push('');
400
+ return [
401
+ ...lines,
402
+ ...renderServiceCostRows(effectiveRows, {
403
+ costLabel: 'Effective cost',
404
+ costOf: serviceEffectiveCost,
405
+ omittedZeroRows: services.length - effectiveRows.length,
406
+ total: usage.totals?.effectiveCost,
407
+ totalLabel: 'Total effective cost',
408
+ totalSuffix: ' _(usage cost before included-credit or allotment offsets)_',
409
+ }),
410
+ ];
411
+ }
412
+
413
+ if (chargedRows.length === 0) {
414
+ const scope = signals.usageScope === 'team' ? 'team-wide ' : '';
415
+ lines.push(`_\`vercel usage\` returned a ${scope}billing payload, but every reported service cost was $0.00 for this window._`);
416
+ return lines;
417
+ }
418
+ }
419
+
420
+ // Fallback to o11y-derived ranking when usage payload missing.
421
+ const gbHr = signals.metrics?.fnGbHrByRoute?.rows ?? [];
422
+ const usageGap = missingUsageSentence(signals);
423
+ if (gbHr.length === 0) {
424
+ lines.push(`_${usageGap} Without per-route function GB-hour data, this report cannot rank cost drivers._`);
425
+ return lines;
426
+ }
427
+ const top = groupGbHoursByCanonicalRoute(gbHr)
428
+ .sort((a, b) => (b.value ?? 0) - (a.value ?? 0))
429
+ .slice(0, 10);
430
+ lines.push(`_${usageGap} Ranking by \`function_duration_gbhr\` instead. These do not translate to dollars directly, but they show which routes consume billable units._`);
431
+ lines.push('');
432
+ lines.push('| Route | GB-hr (sum, 14d) |');
433
+ lines.push('|---|---|');
434
+ for (const r of top) {
435
+ lines.push(`| ${escape(r.route ?? '(unnamed)')} | ${(r.value ?? 0).toFixed(4)} |`);
436
+ }
437
+ return lines;
438
+ }
439
+
440
+ function renderServiceCostRows(services, { costLabel, costOf, omittedZeroRows = 0, total = null, totalLabel, totalSuffix = '' }) {
441
+ const lines = [];
442
+ const rows = services.slice().sort((a, b) => (costOf(b) ?? 0) - (costOf(a) ?? 0));
443
+ // Drop Usage column when every cell is "(unspecified)" — happens when CLI emits pricingUnit=USD.
444
+ const usageCells = rows.map((s) => formatUsage(s));
445
+ const hasRealUsage = usageCells.some((c) => c !== '(unspecified)');
446
+ if (hasRealUsage) {
447
+ lines.push(`| Service | Usage | ${costLabel} |`);
448
+ lines.push('|---|---|---|');
449
+ for (let i = 0; i < rows.length; i++) {
450
+ const s = rows[i];
451
+ const costValue = costOf(s);
452
+ const cost = typeof costValue === 'number' ? `$${costValue.toFixed(2)}` : '(n/a)';
453
+ lines.push(`| ${escape(s.name ?? '(unnamed)')} | ${escape(usageCells[i])} | ${cost} |`);
454
+ }
455
+ } else {
456
+ lines.push(`| Service | ${costLabel} |`);
457
+ lines.push('|---|---|');
458
+ for (const s of rows) {
459
+ const costValue = costOf(s);
460
+ const cost = typeof costValue === 'number' ? `$${costValue.toFixed(2)}` : '(n/a)';
461
+ lines.push(`| ${escape(s.name ?? '(unnamed)')} | ${cost} |`);
462
+ }
463
+ }
464
+ if (omittedZeroRows > 0) {
465
+ lines.push('');
466
+ lines.push(`_${omittedZeroRows} zero-cost service ${omittedZeroRows === 1 ? 'row was' : 'rows were'} omitted._`);
467
+ }
468
+ if (typeof total === 'number') {
469
+ lines.push('');
470
+ lines.push(`**${totalLabel}: $${total.toFixed(2)}**${totalSuffix}`);
471
+ }
472
+ return lines;
473
+ }
474
+
475
+ function serviceCost(service) {
476
+ if (typeof service?.billedCost === 'number') return service.billedCost;
477
+ if (typeof service?.cost === 'number') return service.cost;
478
+ return null;
479
+ }
480
+
481
+ function serviceEffectiveCost(service) {
482
+ if (typeof service?.effectiveCost === 'number') return service.effectiveCost;
483
+ if (typeof service?.pricingQuantity === 'number' && service?.pricingUnit === 'USD') return service.pricingQuantity;
484
+ return 0;
485
+ }
486
+
487
+ function costRoundsToCents(cost) {
488
+ if (typeof cost !== 'number' || !Number.isFinite(cost)) return 0;
489
+ return Math.round(cost * 100) / 100;
490
+ }
491
+
492
+ function renderRecTable(recs, signals = {}) {
493
+ if (recs.length === 0) return ['_(none)_'];
494
+ const lines = [];
495
+ lines.push('| # | Bucket | What | Impact | Effort | Citations |');
496
+ lines.push('|---|---|---|---|---|---|');
497
+ recs.forEach((r, i) => {
498
+ const cites = asArray(r.citations).slice(0, 2).join('<br>');
499
+ lines.push(`| ${i + 1} | ${r.bucket ?? '?'} | ${escape(formatRecommendationText(r.what ?? ''))} | ${escape(formatRecommendationText(impactString(r, signals)))} | ${r.effort ?? '?'} | ${cites} |`);
500
+ });
501
+ return lines;
502
+ }
503
+
504
+ function renderRecDetail(rec, index, { compact = false, signals = {} } = {}) {
505
+ const lines = [];
506
+ lines.push(`### ${index}. ${formatRecommendationText(rec.what ?? '(no `what`)')}`);
507
+ lines.push('');
508
+ const meta = [
509
+ rec.bucket ? `**${rec.bucket}**` : null,
510
+ rec.effort ? `effort: ${rec.effort}` : null,
511
+ rec.impactTier ? `impact tier: ${rec.impactTier}` : null,
512
+ rec.candidateRef ? `candidate: ${displayCandidate(rec)}` : null,
513
+ rec.corroborationCount > 1 ? `corroborated: ${rec.corroborationCount}` : null,
514
+ ].filter(Boolean);
515
+ if (meta.length > 0) lines.push(`_${meta.join(' · ')}_`);
516
+ lines.push('');
517
+ const appliesAlsoTo = asArray(rec.appliesAlsoTo);
518
+ if (appliesAlsoTo.length > 0) {
519
+ const refs = appliesAlsoTo
520
+ .map((a) => a?.candidateRef)
521
+ .filter(Boolean)
522
+ .slice(0, 4);
523
+ if (refs.length > 0) {
524
+ const suffix = appliesAlsoTo.length > refs.length ? `, +${appliesAlsoTo.length - refs.length} more` : '';
525
+ lines.push(`_Also applies to: ${refs.map(displayCandidateRef).join(', ')}${suffix}._`);
526
+ lines.push('');
527
+ }
528
+ }
529
+ if (rec.why) {
530
+ lines.push('**Why**');
531
+ lines.push('');
532
+ lines.push(formatRecommendationText(rec.why));
533
+ lines.push('');
534
+ }
535
+ lines.push('**Impact**');
536
+ lines.push('');
537
+ lines.push(formatRecommendationText(impactString(rec, signals)));
538
+ lines.push('');
539
+ if (!compact && rec.fix) {
540
+ lines.push('**Fix**');
541
+ lines.push('');
542
+ lines.push(rec.fix);
543
+ lines.push('');
544
+ }
545
+ if (!compact && rec.currentBehavior) {
546
+ lines.push('**Before**');
547
+ lines.push('');
548
+ lines.push(rec.currentBehavior);
549
+ lines.push('');
550
+ }
551
+ if (!compact && rec.desiredBehavior) {
552
+ lines.push('**After**');
553
+ lines.push('');
554
+ lines.push(rec.desiredBehavior);
555
+ lines.push('');
556
+ }
557
+ if (rec.verify) {
558
+ lines.push('**Verify**');
559
+ lines.push('');
560
+ lines.push(formatRecommendationText(rec.verify));
561
+ lines.push('');
562
+ }
563
+ const cites = asArray(rec.citations);
564
+ if (cites.length > 0) {
565
+ lines.push('**Citations**');
566
+ lines.push('');
567
+ for (const c of cites) lines.push(`- \`${c}\``);
568
+ lines.push('');
569
+ }
570
+ lines.push('');
571
+ return lines;
572
+ }
573
+
574
+ function renderGatedTable(gated) {
575
+ if (!Array.isArray(gated) || gated.length === 0) {
576
+ return ['_(no candidates were held back)_'];
577
+ }
578
+ const groups = groupGatedCandidates(gated);
579
+ const lines = [];
580
+ lines.push('| Candidate type | Why not investigated | Targets | Count |');
581
+ lines.push('|---|---|---|---:|');
582
+ for (const group of groups) {
583
+ lines.push(`| ${escape(group.kind)} | ${escape(group.reason)} | ${formatGatedTargets(group.targets, group.count)} | ${group.count} |`);
584
+ }
585
+ return lines;
586
+ }
587
+
588
+ function formatGatedTargets(targets, count) {
589
+ const unique = [...new Set(targets.map((t) => String(t)))];
590
+ const shown = unique.slice(0, GATED_TARGET_PREVIEW).map((target) => escape(target));
591
+ const hidden = Math.max(0, count - shown.length);
592
+ if (hidden > 0) shown.push(`+${hidden} more`);
593
+ return shown.join('<br>');
594
+ }
595
+
596
+ function groupGatedCandidates(gated) {
597
+ const byKey = new Map();
598
+ for (const g of gated) {
599
+ const kind = formatKind(g.kind ?? '?');
600
+ const reason = publicGatedReason(g.gatedReason ?? g.disqualifyReason ?? '(no reason recorded)');
601
+ const target = formatRoute(g);
602
+ const key = `${kind}\u0000${reason}`;
603
+ const existing = byKey.get(key);
604
+ if (existing) {
605
+ existing.count += 1;
606
+ existing.targets.push(String(target));
607
+ } else {
608
+ byKey.set(key, { kind: String(kind), reason: String(reason), targets: [String(target)], count: 1 });
609
+ }
610
+ }
611
+ return Array.from(byKey.values());
612
+ }
613
+
614
+ function publicNoRecommendationReason(reason) {
615
+ return formatEvidenceText(String(reason))
616
+ .replace(/\bDropped at render:\s*/gi, '')
617
+ .replace(/\bverifier flagged for regen, but no regen happened\b/gi, 'needs stronger evidence before it is safe to apply')
618
+ .replace(/\bRe-run with a refreshed brief\.?/gi, 'Re-run the investigation after refreshing the evidence.')
619
+ .replace(/\bregen\b/gi, 're-check')
620
+ .replace(/\bverifier\b/gi, 'verification')
621
+ .replace(/\brec\b/gi, 'recommendation')
622
+ .replace(/\bsub[- ]agent\b/gi, 'investigation')
623
+ .replace(/\babstentions?\b/gi, 'no-change findings')
624
+ .replace(/\babstaining\b/gi, 'not recommending a change')
625
+ .replace(/\babstain(?:ed)?\b/gi, 'found no supported change')
626
+ .replace(/\bquality\s*\+\s*verification\b/gi, 'verification')
627
+ .replace(/\bquality\b/gi, 'review')
628
+ .replace(/\bsanitizers?\b/gi, 'checks');
629
+ }
630
+
631
+ function splitInvestigationOutcomes(abstentions) {
632
+ const rows = Array.isArray(abstentions) ? abstentions : [];
633
+ return {
634
+ needsEvidenceRows: rows.filter((a) => a?.needsEvidence === true),
635
+ noChangeRows: rows.filter((a) => a?.needsEvidence !== true),
636
+ };
637
+ }
638
+
639
+ function publicGatedReason(reason) {
640
+ return formatPublicText(String(reason))
641
+ .replace(/\bhardGated:\s*/gi, '')
642
+ .replace(/skippedByBudget\s*\(max-candidates=([^);]+)(?:;[^)]*)?\)/i, 'left for a larger run (max candidates: $1)')
643
+ .replace(/skippedByBudget\b/gi, 'left for a larger run')
644
+ .replace(/\s*;\s*raise with --max-candidates N or =all/gi, '')
645
+ .replace(/=all/g, 'all')
646
+ .replace(/\bcoveredBy\b/gi, 'covered by a higher-priority candidate')
647
+ .replace(/\bdisqualified\b/gi, 'not eligible');
648
+ }
649
+
650
+ function formatEvidenceText(value) {
651
+ const expanded = formatPublicText(value)
652
+ .replace(/\bdeepDive\b/gi, 'follow-up metric')
653
+ .replace(/\bdeep-dive\b/gi, 'follow-up metric')
654
+ .replace(/\blatency p95\b/gi, '95th percentile latency')
655
+ .replace(/\bttfb p95\b/gi, '95th percentile TTFB')
656
+ .replace(/\bcpu p95\b/gi, '95th percentile CPU time')
657
+ .replace(/\bp95\b/gi, '95th percentile')
658
+ .replace(/\bgate signal\b/gi, 'broad metric signal')
659
+ .replace(/\bo11ySignal\b/gi, 'observed signal')
660
+ .replace(/\bperDeployment\b/gi, 'per-deployment')
661
+ .replace(/\bstartTypeSplit\b/gi, 'start-type breakdown')
662
+ .replace(/\bstatusDistribution\b/gi, 'status distribution')
663
+ .replace(/\bcacheBreakdown\b/gi, 'cache breakdown')
664
+ .replace(/\bfunctionRoutes\b/gi, 'function routes')
665
+ .replace(/\bfnGbHrByRoute\b/gi, 'function duration by route');
666
+ return formatPublicText(expanded);
667
+ }
668
+
669
+ function formatRecommendationText(value) {
670
+ return formatEvidenceText(value)
671
+ .replace(/\bthe gate fires\b/gi, 'this audit flags the signal')
672
+ .replace(/\bships immediately\b/gi, 'can ship sooner');
673
+ }
674
+
675
+ function displayCandidate(value) {
676
+ return formatCandidateLabel(candidateForDisplay(value));
677
+ }
678
+
679
+ function candidateForDisplay(value) {
680
+ const parsed = parseCandidateRef(value?.candidateRef);
681
+ return parsed
682
+ ? displayCandidateObject(value, parsed)
683
+ : value;
684
+ }
685
+
686
+ function displayCandidateRef(ref) {
687
+ const parsed = parseCandidateRef(ref);
688
+ if (!parsed) return String(ref ?? '(unspecified)');
689
+ return formatCandidateLabel(displayCandidateObject({}, parsed));
690
+ }
691
+
692
+ function parseCandidateRef(ref) {
693
+ if (typeof ref !== 'string') return null;
694
+ const [kind, ...rest] = ref.split(':');
695
+ const target = rest.join(':');
696
+ if (!kind || !target) return null;
697
+ return { kind, target };
698
+ }
699
+
700
+ function displayCandidateObject(base, parsed) {
701
+ if (parsed.target.startsWith('<account>#')) {
702
+ return { ...base, kind: parsed.kind, files: [parsed.target.slice('<account>#'.length)] };
703
+ }
704
+ if (parsed.target === '<account>') {
705
+ return { ...base, kind: parsed.kind };
706
+ }
707
+ return { ...base, kind: parsed.kind, route: parsed.target };
708
+ }
709
+
710
+ function groupGbHoursByCanonicalRoute(rows) {
711
+ const byRoute = new Map();
712
+ for (const row of rows) {
713
+ const route = row?.route ? canonicalizeRoute(row.route) : '(unnamed)';
714
+ byRoute.set(route, (byRoute.get(route) ?? 0) + (row?.value ?? 0));
715
+ }
716
+ return [...byRoute.entries()].map(([route, value]) => ({ route, value }));
717
+ }
718
+
719
+ function renderStrengths(signals) {
720
+ const lines = [];
721
+
722
+ // Stops agent from emitting "verify Fluid is on" recs. Source: defaultResourceConfig from project API.
723
+ const projectFacts = deriveProjectFacts(signals);
724
+ for (const f of projectFacts) {
725
+ if (String(f.id ?? '').startsWith('memory_')) continue;
726
+ lines.push(`- ${f.strength}`);
727
+ }
728
+
729
+ const cache = signals.metrics?.fdtByCache?.rows ?? [];
730
+ const hit = cache.find((r) => r.cache_result === 'HIT' || r.cache_result === 'STALE');
731
+ const miss = cache.find((r) => r.cache_result === 'MISS' || r.cache_result === 'BYPASS');
732
+ if (hit && miss && (hit.value ?? 0) > (miss.value ?? 0)) {
733
+ lines.push(`- Cache hit-rate is healthy at the bandwidth tier — HIT/STALE bandwidth (${formatBytes(hit.value)}) exceeds MISS/BYPASS (${formatBytes(miss.value)}).`);
734
+ }
735
+ const cold = signals.metrics?.fnStartTypeByRoute?.rows ?? [];
736
+ const totalInv = cold.reduce((s, r) => s + (r.total ?? 0), 0);
737
+ const totalCold = cold.reduce((s, r) => s + (r.coldCount ?? 0), 0);
738
+ if (totalInv > 1000) {
739
+ const coldPct = totalCold / totalInv;
740
+ if (coldPct < 0.02) lines.push(`- Cold-start rate is very low (${(coldPct * 100).toFixed(2)}%) — Fluid Compute or warm-instance reuse is doing its job.`);
741
+ }
742
+ const errors = signals.metrics?.requestsByRouteStatus?.rows ?? [];
743
+ const total5xx = errors.filter((r) => /^5/.test(r.http_status ?? '')).reduce((s, r) => s + (r.value ?? 0), 0);
744
+ const totalReq = errors.reduce((s, r) => s + (r.value ?? 0), 0);
745
+ if (totalReq > 1000) {
746
+ const rate = total5xx / totalReq;
747
+ if (rate < 0.001) lines.push(`- 5xx rate is very low (${(rate * 100).toFixed(3)}%) on ${formatNum(totalReq)} requests.`);
748
+ }
749
+ if (lines.length === 0) lines.push('_(no headline strengths to call out — see the gated table for signals we considered)_');
750
+ return lines;
751
+ }
752
+
753
+ function renderConfigurationNotes(signals) {
754
+ const projectFacts = deriveProjectFacts(signals);
755
+ return projectFacts
756
+ .filter((f) => String(f.id ?? '').startsWith('memory_'))
757
+ .map((f) => `- ${f.strength}`);
758
+ }
759
+
760
+ function renderDataGaps(signals) {
761
+ const lines = [];
762
+ const observabilityGap = observabilityDataGap(signals);
763
+ if (observabilityGap) lines.push(observabilityGap);
764
+ if (!signals.usage) lines.push(`- ${missingUsageSentence(signals)}`);
765
+ const cwvMetric = metricState(signals, 'cwvCount');
766
+ if (cwvMetric.failed) {
767
+ lines.push(`- Speed Insights metrics were not usable (\`${cwvMetric.code}\`), so LCP/INP/CLS analysis was skipped.`);
768
+ } else if (cwvMetric.collected) {
769
+ const cwv = cwvMetric.rows?.[0]?.value ?? 0;
770
+ if (cwv === 0) lines.push('- No Speed Insights measurements — Core Web Vitals analysis dormant. Wire up Speed Insights to enable LCP/INP/CLS recommendations.');
771
+ }
772
+ const isrMetric = metricState(signals, 'isrReadsByRoute');
773
+ if (isrMetric.collected) {
774
+ const isrR = isrMetric.rows ?? [];
775
+ if (isrR.length === 0) lines.push('- No ISR activity observed — either the project does not use ISR or no eligible routes had traffic in the window.');
776
+ }
777
+ const imageMetric = metricState(signals, 'imageCount');
778
+ if (imageMetric.collected) {
779
+ const images = imageMetric.rows?.[0]?.value ?? 0;
780
+ if (images === 0) lines.push('- No image transformations observed — either `next/image` is not used or no images served in the window.');
781
+ }
782
+ const middlewareMetric = metricState(signals, 'middlewareCount');
783
+ if (middlewareMetric.collected) {
784
+ const middleware = middlewareMetric.rows ?? [];
785
+ if (middleware.length === 0) lines.push('- No middleware invocations — either no `middleware.ts` is shipped or its matcher excludes all observed traffic.');
786
+ }
787
+ if (lines.length === 0) lines.push('_(no relevant gaps — every signal had data)_');
788
+ return lines;
789
+ }
790
+
791
+ function observabilityDataGap(signals = {}) {
792
+ if (signals.usageError === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK') {
793
+ return '- Observability Plus was not checked because the audit paused at the unsupported-framework preflight.';
794
+ }
795
+ if (signals.observabilityPlusUsable === false) {
796
+ const blocker = signals.observabilityPlusBlocker;
797
+ if (blocker === 'project_disabled') {
798
+ return '- Per-route metrics unavailable — Observability Plus is disabled for this project.';
799
+ }
800
+ if (blocker === 'forbidden' || blocker === 'project_not_found') {
801
+ return '- Per-route metrics unavailable — the authenticated Vercel scope cannot read this project.';
802
+ }
803
+ if (blocker === 'not_linked') {
804
+ return '- Per-route metrics unavailable — the app directory is not linked to the Vercel project.';
805
+ }
806
+ if (blocker === 'no_oplus_probe') {
807
+ return '- Per-route metrics unavailable — Observability Plus was not detected for this scope.';
808
+ }
809
+ if (blocker === 'payment_required') {
810
+ return '- Per-route metrics unavailable — Observability Plus metrics were not usable for this scope.';
811
+ }
812
+ if (blocker === 'daily_quota_exceeded') {
813
+ return '- Per-route metrics unavailable — the Observability Plus query quota is exhausted for today.';
814
+ }
815
+ if (blocker === 'no_traffic') {
816
+ return '- Per-route metrics sparse — no route-level traffic was returned in the metrics window.';
817
+ }
818
+ if (blocker === 'all_failed_other') {
819
+ return '- Per-route metrics unavailable — all Observability Plus metric queries failed.';
820
+ }
821
+ if (blocker) {
822
+ return `- Per-route metrics unavailable — Observability Plus metrics returned \`${blocker}\`.`;
823
+ }
824
+ return '- Per-route metrics unavailable — Observability Plus data was not usable for this run.';
825
+ }
826
+ if (signals.observabilityPlus === false) {
827
+ return '- Observability Plus not enabled — per-route latency / cache-hit / cold-start metrics unavailable.';
828
+ }
829
+ return null;
830
+ }
831
+
832
+ function missingUsageSentence(signals = {}) {
833
+ const code = signals.usageError;
834
+ if (code === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED') {
835
+ return '`vercel usage` was not collected because the audit paused before billing collection on the Observability Plus blocker.';
836
+ }
837
+ if (code === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK') {
838
+ return '`vercel usage` was not collected because the audit paused at the unsupported-framework preflight.';
839
+ }
840
+ if (code === 'USAGE_CONTEXT_MISMATCH') {
841
+ return '`vercel usage` returned data for a different team context, so the billing breakdown was not used.';
842
+ }
843
+ if (code === 'USAGE_UNAVAILABLE') {
844
+ return '`vercel usage` returned `USAGE_UNAVAILABLE`; no billing breakdown was available from the Vercel CLI.';
845
+ }
846
+ if (typeof code === 'string' && code.trim() !== '') {
847
+ return `\`vercel usage\` returned \`${code}\`; no billing breakdown was available from the Vercel CLI.`;
848
+ }
849
+ return '`vercel usage` did not return a billing payload.';
850
+ }
851
+
852
+ function metricState(signals, id) {
853
+ const metrics = signals.metrics ?? {};
854
+ if (!Object.prototype.hasOwnProperty.call(metrics, id)) {
855
+ return { collected: false, failed: false, rows: null, code: null };
856
+ }
857
+ const metric = metrics[id] ?? {};
858
+ const failed = metric.ok === false;
859
+ return {
860
+ collected: !failed,
861
+ failed,
862
+ rows: Array.isArray(metric.rows) ? metric.rows : [],
863
+ code: metric.code ?? 'UNKNOWN',
864
+ };
865
+ }
866
+
867
+ function sortRecs(recs) {
868
+ return recs.slice().sort((a, b) => priorityScore(b) - priorityScore(a));
869
+ }
870
+
871
+ function priorityScore(rec) {
872
+ return typeof rec.priority === 'number' ? rec.priority : tierScore(rec.impactTier);
873
+ }
874
+ function tierScore(t) { return ({ high: 100, medium: 50, low: 10 })[t] ?? 0; }
875
+
876
+ function isPlatformScope(rec) {
877
+ const k = String(rec.candidateRef ?? '').split(':')[0];
878
+ return k.startsWith('platform_') || rec.scope === 'account';
879
+ }
880
+
881
+ function impactString(rec, signals = {}) {
882
+ const label = computeImpactLabel(rec, signals);
883
+ if (label) return label;
884
+ return '_(no impact framing recorded)_';
885
+ }
886
+
887
+ function signalFromRec(rec) {
888
+ return rec.findingRefs?.[0] ?? null;
889
+ }
890
+
891
+ function formatUsage(s) {
892
+ if (typeof s.usage === 'string') return s.usage;
893
+ if (typeof s.usage === 'number') return formatNum(s.usage) + (s.unit ? ` ${s.unit}` : '');
894
+ return '(unspecified)';
895
+ }
896
+
897
+ function formatNum(n) {
898
+ if (!Number.isFinite(n)) return String(n);
899
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + 'M';
900
+ if (n >= 1_000) return (n / 1_000).toFixed(2) + 'K';
901
+ return String(n);
902
+ }
903
+
904
+ function formatBytes(b) {
905
+ if (!Number.isFinite(b)) return '(n/a)';
906
+ if (b >= 1e12) return (b / 1e12).toFixed(2) + ' TB';
907
+ if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB';
908
+ if (b >= 1e6) return (b / 1e6).toFixed(2) + ' MB';
909
+ if (b >= 1e3) return (b / 1e3).toFixed(2) + ' KB';
910
+ return Math.round(b) + ' B';
911
+ }
912
+
913
+ function escape(s) {
914
+ if (typeof s !== 'string') return String(s ?? '');
915
+ return s.replace(/\|/g, '\\|').replace(/\n/g, ' ');
916
+ }
917
+
918
+ function asArray(v) { return Array.isArray(v) ? v : []; }
919
+
920
+ function enrichRecFromCandidates(rec, candidates) {
921
+ if (!rec || typeof rec !== 'object') return rec;
922
+ if (!Array.isArray(candidates) || candidates.length === 0) return rec;
923
+ const ref = rec.candidateRef ?? null;
924
+ // Match on raw OR canonical kind:route so pre-dedup-canonicalization refs still resolve.
925
+ let match = null;
926
+ if (ref) {
927
+ const [kind, route] = String(ref).split(':');
928
+ const canonical = route ? canonicalizeRoute(route) : null;
929
+ match = candidates.find((c) => {
930
+ if (!c || c.kind !== kind) return false;
931
+ const cRoute = c.route ?? c.hostname ?? '<account>';
932
+ return cRoute === route || cRoute === canonical || canonicalizeRoute(cRoute) === canonical;
933
+ });
934
+ }
935
+ const canonicalRef = ref ? canonicalRefOf(ref) : ref;
936
+ const merged = { ...rec, candidateRef: canonicalRef };
937
+ if (match) {
938
+ if (!merged.o11ySignal && match.o11ySignal) merged.o11ySignal = match.o11ySignal;
939
+ if (!merged.displayRoute && match.displayRoute) merged.displayRoute = match.displayRoute;
940
+ if (!merged.aliasRoutes && Array.isArray(match.aliasRoutes) && match.aliasRoutes.length > 0) {
941
+ merged.aliasRoutes = match.aliasRoutes;
942
+ }
943
+ if (!merged.mergedCount && typeof match.mergedCount === 'number') {
944
+ merged.mergedCount = match.mergedCount;
945
+ }
946
+ }
947
+ return merged;
948
+ }
949
+
950
+ function canonicalRefOf(ref) {
951
+ const [kind, ...rest] = String(ref).split(':');
952
+ const route = rest.join(':');
953
+ if (!route) return ref;
954
+ return `${kind}:${canonicalizeRoute(route)}`;
955
+ }