270// Per-kind hints tell the investigator which comparison to draw first.
271export const KIND_INTERPRETATION_HINTS = {
272 slow_route: [
273 'Compare `cpu.p95` vs `latency.p95`. If cpu << latency, the bottleneck is wall-clock / external IO / awaits — look for sequential awaits, slow DB queries, slow external APIs. If cpu ≈ latency, look for in-process compute (rendering, JSON serialization, crypto).',
274 'Compare `ttfb.p95` vs `latency.p95`. If ttfb ≈ latency, response generation finishes near the end — streaming or `after()` may shift perceived latency.',
275 'For streaming, SSE, resumable chat, or other intentionally long-lived routes, do not treat high wall-clock duration alone as a bug. Recommend a change only when evidence shows avoidable pre-first-byte work, high active CPU, duplicate invocations, or post-response work that can move out of the user-visible path.',
276 'Inspect `perDeployment`: a 2x step between deployments points to a regression introduced in the newer deployment. Frame the rec as "regression introduced in <deployment_id>" rather than a generic perf claim.',
277 'Inspect `startTypeSplit.cold` share. >5% cold means cold starts contribute meaningfully — Fluid Compute or warmer keep-alive is on the table.',
278 'Inspect `statusDistribution`. A non-trivial 3xx/4xx slice may be inflating p95 because redirects/auth bounces still count as invocations.',
279 'Inspect `cacheBreakdown`. If the route uses Next.js `dynamic = \'error\'` (or otherwise static) but the breakdown shows substantial MISS/BYPASS counts, the latency lives on the cache-miss path — investigate the origin fetch / ISR revalidation cost, NOT in-handler compute. `bandwidthByCache` tells you the byte cost of those misses.',
280 ],
281 uncached_route: [
282 '`cacheBreakdown` tells you what fraction is BYPASS vs HIT vs MISS. BYPASS without explicit `Cache-Control` directives in the response is the canonical fix.',
283 '`methodDistribution`: GET-only routes are cacheable; POST/PUT/DELETE are not. If the route is GET-heavy but BYPASSing, the cache headers are missing or wrong.',
284 '`botShare` (bandwidth by bot_category): if bots dominate uncached bandwidth, the right rec may be Bot Protection rather than route caching.',
285 '`bandwidthByCache`: pair with cacheBreakdown to confirm the dollar/bandwidth impact of moving uncached → cached.',
286 'A ready cache recommendation must name a positive cache policy. If the right answer is `no-store`, emit no recommendation / observation instead of a cache fix.',
287 ],
288 cold_start: [
289 '`startTypeSplit`: cold vs hot vs prewarmed. Fluid Compute meaningfully helps when cold > 5%.',
290 '`coldVsWarmLatencyP95`: how much SLOWER is cold than warm. If 5x+, cold starts are amplifying tail latency, not just adding fixed overhead.',
291 '`coldByDeployment`: if cold-start cluster around the newest deployment, the slowdown is a regression — check imports, init code, framework upgrade.',
292 ],
293 route_errors: [
294 '`errorStatusPattern`: distinguishes 500 (app crash) vs 502/503 (gateway/upstream timeout) vs 504 (downstream timeout).',
295 '`errorCodes`: a non-empty error_code dimension narrows to a specific failure class (e.g., FUNCTION_INVOCATION_TIMEOUT).',
296 '`errorsByDeployment`: a deployment-localized spike points to a regression.',
297 ],
298 external_api_slow: [
299 '`latency.p95` vs `latency.p99`: spreads point to flaky upstream; narrow gap points to slow-by-design.',
300 '`callersByRoute` (`origin_route` dim): which of OUR routes call this upstream — that\'s where the rec should land.',
301 '`transferBytes`: large payloads suggest caching or partial-response opportunities at our edge.',
302 ],
303 isr_overrevalidation: [
304 '`writePattern` (write_units by cache_result) — STALE writes vs HIT writes. STALE-write means the revalidate ran on every stale request.',
305 '`readPattern` (read_units by cache_result) — HIT vs MISS. Low MISS means cache fills are not the issue.',
306 'If writes / reads > 0.5, the revalidate cadence is too aggressive; lengthen `revalidate` or switch to on-demand `revalidateTag`.',
307 ],
308 cwv_poor: [
309 '`lcp`/`inp`/`cls` percentiles. p75 > Web Vitals "Good" threshold is the bar.',
310 'LCP > 2500ms → server response or critical image. INP > 200ms → long tasks / heavy JS on interaction. CLS > 0.1 → layout shift, usually images/ads/fonts.',
311 ],
312 middleware_heavy: [
313 '`topMiddlewarePaths`: paths that hit middleware most. If non-asset paths dominate, the matcher is too broad — narrow to the request shapes that actually need middleware.',
314 ],
315 platform_fluid_compute: [
316 'Cross-check the broad-pass `fnStartTypeByRoute` for cold-rate concentration. If a few routes carry most cold starts, frame the rec around those routes rather than fleet-wide.',
317 ],
318 platform_bot_protection: [
319 '`wafRuleFirings`: which managed rules are already firing (challenge/block). If `bot_filter` is already challenging but you still see significant bot bandwidth, BotID adds a verified-human signal that lets the WAF do its job.',
349 lines.push('You are a Vercel-optimize investigation sub-agent. Your job is to investigate ONE evidence-backed candidate and emit ONE recommendation JSON. Stay narrow. Stay grounded. Do NOT widen the search.');
397 if ([...routeFiles].length > 0 && workspaceImportFiles.length > 0) {
398 lines.push('');
399 lines.push('_The route file is often a thin shell that re-exports from a workspace package. If the route file has no awaits / heavy imports / data fetching of its own, the bottleneck almost certainly lives in one of the (workspace import) files above — read those._');
400 }
401 } else {
402 lines.push('**Files:** none mapped to this candidate. Either the gate is account-scope (platform_*) or the scanner could not resolve a route→file mapping (legitimate data gap). Work from the deep-dive evidence alone.');
418 lines.push('## Project config (already on — do NOT recommend toggling)');
419 lines.push('');
420 lines.push('These settings are already enabled on the project. A recommendation that says "enable X" or "turn on X" for any of these is wrong and will be rejected by the verifier. Treat them as the starting state for your investigation.');
421 lines.push('');
422 for (const f of projectFacts) lines.push(`- ${f.briefLine}`);
423 lines.push('');
424 }
425
426 lines.push('## Deep-dive evidence (already collected — do NOT re-query)');
433 lines.push(`> The base evidence below is still valid — \`o11ySignal=${candidate.o11ySignal ?? '(unset)'}\` came directly from the gate's broad-pass query and is unaffected. Investigate against that signal and any deep-dive keys that DID populate. Do not conflate "missing data" with "no bottleneck": if the data didn't come back, abstain on the missing dimensions, not on the candidate as a whole.`);
434 lines.push('');
435 }
436 lines.push('Treat these as ground truth. Cite the specific paths and values verbatim in `why` and `verify`. Numeric values are rounded to 4 decimal places.');
437 lines.push('');
438 lines.push('**Units legend** — all duration/timing fields below are in **milliseconds** (`latency.*`, `ttfb.*`, `cpu.p95`, `memory.*`). All `value` fields under `startTypeSplit` / `statusDistribution` / `methodDistribution` / `cacheBreakdown` are **invocation counts**. `botShare` / `bandwidthByCache` values are **bytes**. `perDeployment.value` is **p95 latency in ms** for that deployment.');
455 lines.push('Pick the narrowest cache mechanism that matches the source. Do not default to `no-store`; if data is unsafe to cache, abstain or emit a no-change observation.');
456 lines.push('');
457 for (const h of cachePolicyHints) lines.push(`- ${h}`);
464 lines.push('## Citation library (USE ONLY THESE)');
465 lines.push('');
466 lines.push(`You may cite ONLY these URLs and skill-rule references. They are filtered for \`${framework}@${version}\` and the candidate kind \`${kind}\`. Any other URL will be stripped by the \`unknown-citation\` sanitizer; any URL whose version range doesn't cover \`${framework}@${version}\` will be stripped by \`version-mismatch\`.`);
467 lines.push('');
468 lines.push('### URLs');
469 if (citations.urls.length === 0) {
470 lines.push('_(no URLs match this kind + version — investigate, but the rec may fail `missing-citation`; consider abstaining)_');
488 lines.push(`## Playbook hint (\`${playbookId}\`)`);
489 lines.push('');
490 lines.push(playbookBody.trim());
491 lines.push('');
492 lines.push('_Use the playbook to tilt phrasing and pattern priority. NEVER invent a claim because the playbook mentions a pattern — only emit it if the evidence supports it._');
493 lines.push('');
494 }
495 if (frameworkPlaybookId && frameworkPlaybookBody) {
500 lines.push(`_Framework-shaped advice for ${framework}. Same rule: evidence-grounded only._`);
501 lines.push('');
502 }
503
504 lines.push('## Two valid outcomes');
505 lines.push('');
506 lines.push('Your job is to answer the gate question above. There are exactly two valid outcomes:');
507 lines.push('');
508 lines.push('**A. Emit a recommendation** (schema below) — ONLY when you found a verifiable file:line cause that the deep-dive evidence supports.');
509 lines.push('');
510 lines.push('**B. Abstain** — when the gate\'s hypothesis does not survive contact with the source. Emit:');
511 lines.push('```json');
512 lines.push(`{"abstain": true, "candidateRef": "${candidateRef}", "reason": "<one-sentence explanation grounded in what you found vs what the gate assumed>"}`);
513 lines.push('```');
514 lines.push('Abstaining is the RIGHT call when evidence is ambiguous, when the bottleneck isn\'t in the resolved files, or when the gate\'s hypothesis was wrong (e.g. an "uncached_route" candidate where the route is mostly POST traffic and uncacheable by protocol). Abstention is preferred over a speculative rec. The orchestrator surfaces abstentions in the trust section of the final report.');
515 lines.push('');
516 lines.push('**B1. Abstain with an observation** — when you find something real while abstaining (e.g., perDeployment regression, error-rate spike, infrastructure config gap) that the customer should know about but isn\'t a perf rec in the gate\'s framing. Emit:');
517 lines.push('```json');
518 lines.push(`{
519 "abstain": true,
520 "candidateRef": "${candidateRef}",
521 "reason": "<why you abstained from a perf rec>",
522 "observation": {
523 "summary": "<one-line headline — what you noticed>",
524 "evidence": "<the deep-dive datum or file:line that backs it>",
525 "suggestedAction": "<what the customer should do next>",
530 lines.push('Use `observation` ONLY when the finding is grounded in specific evidence the gate already gave you. Do NOT invent observations to fill the slot. The renderer surfaces these in a separate "Observations from investigation" section.');
531 lines.push('');
532
533 lines.push('## Investigation protocol');
534 lines.push('');
535 lines.push('1. **Read ONLY the files listed under "Files you may read".** Do NOT `grep -r` the repo. If you find yourself wanting to widen the search, stop and re-read the gate question. If it doesn\'t constrain the search, abstain.');
536 lines.push('2. Read each file, then run targeted `grep` / `ast-grep` inside it to count patterns. Verify line numbers exactly.');
537 lines.push('3. Follow imports within the chain only when relevant to the gate question (one level deep max).');
538 lines.push('4. Stop after 5 files exhausted, or when you have a verified root cause.');
539 lines.push('5. Drop findings that fail mechanical verification (file missing, pattern not present, etc.).');
540 lines.push('6. **Zero-finding case:** if you read the named file(s) and find no mechanism that matches the gate question, abstain (Outcome B). Do NOT invent a rec to fill the slot.');
541 lines.push('7. **Evidence-contradicts-source case:** if the deep-dive shows a real signal (e.g. high p95) but the source looks fine (no awaits, no heavy imports, small render), the bottleneck is upstream (DB, external API, or in code not shown). Abstain with reason "evidence and source diverge."');
542 lines.push('');
543
544 lines.push('## Pre-emit self-check');
545 lines.push('');
546 lines.push('Before emitting a recommendation (Outcome A), verify ALL of:');
547 lines.push('- Every file in `affectedFiles` appears in "Files you may read" as a repo-relative path. If a line shows `(scan path: ...)`, do not use the scan path in JSON.');
548 lines.push('- `why` quotes at least one specific `file:line` AND at least one deep-dive datum (e.g. `ttfb.p95=576ms`).');
549 lines.push('- Every citation appears in the library above. No invented URLs.');
550 lines.push('- `currentBehavior` snippet appears in the actual file (not a paraphrase).');
551 lines.push('- No `$N` dollar literals in any customer-facing field.');
552 lines.push('');
553 lines.push('If ANY of these fails, fix the rec OR switch to Outcome B (abstain).');
554 lines.push('');
555
556 lines.push('## Required output (one JSON object, no prose around it)');
573 "billingDimension": "function-duration" // see references/recommendations.md schema
574}`);
575 lines.push('```');
576 lines.push('');
577
578 lines.push('## Critical rules');
579 lines.push('');
580 lines.push('Ordered by priority — top is most important.');
581 lines.push('');
582 lines.push('1. **`why` must cite a specific `file:line` AND a specific deep-dive datum.** Both. Not one or the other. This is THE quality gate — recs without both will be dropped by the verifier.');
583 lines.push(`2. **No invented citations.** Only URLs and refs from the library above. The \`unknown-citation\` sanitizer strips anything else.`);
584 lines.push(`3. **No version-mismatched features.** This project is \`${framework}@${version}\` — do not recommend APIs unavailable in that version. The version-aware library above is your filter.`);
585 lines.push(`4. **No \`$N\` dollar literals** in customer-facing fields. Use magnitude phrases ("hundreds of dollars per month at current traffic"). The \`$-strip\` sanitizer strips them, but emitting them is wasted output.`);
586 lines.push('5. **Stay within scope.** Do not investigate other routes or fleet-wide patterns; that is the orchestrator\'s job. If this candidate doesn\'t yield a finding, abstain (Outcome B above).');
587 lines.push('6. **Vercel voice.** Sharp teammate, clear, competent, no fluff. Lead with observed metrics and the user action. Avoid marketing language (`leverage`, `streamline`, `powerful`), filler adverbs (`just`, `simply`, `actually`), hedged starts (`Consider`, `You may want to`), rhetorical reframes, and arrows in prose. Do not expose internal terms like `sub-agent`, `abstention`, `passRate`, or `quality score`. Product names: `Observability Plus`, `Vercel Functions`, `fluid compute` mid-sentence, `BotID`, `AI Gateway`, `AI SDK`, `Edge Config`, `Routing Middleware`, `Web Analytics`. Explain `function invocations` and `95th percentile`; do not use `inv` or `p95` in customer output. See `references/voice.md`.');
598 'Whole public GET response: recommend `Cache-Control` / `CDN-Cache-Control` with `s-maxage` and `stale-while-revalidate`; name the TTL/freshness window and required `Vary` headers. Avoid high-cardinality `Vary` headers such as `X-Vercel-IP-Latitude` or `X-Vercel-IP-Longitude`; use coarser geography only when the product can tolerate it.',
599 'Fallback, 404, auth, preview, webhook, mutation, and per-user branches: keep them uncached or short-lived while caching only the safe success branch.',
600 ];
601 if (framework === 'next') {
602 if (cacheComponents) {
603 hints.push('Next.js with Cache Components: for reusable data inside the render path, prefer `use cache` / `use cache: remote` plus `cacheLife()` and `cacheTag()` when invalidation evidence exists.');
604 } else {
605 hints.push('Next.js data fetch path: use `fetch(..., { next: { revalidate: seconds } })` or route-level `revalidate` only when it matches the project version and route semantics. Before recommending route-level `export const revalidate`, inspect the page/layout route chain for `cookies()`, `headers()`, `draftMode()`, `connection()`, and auth helpers; if any parent layout is request-time dynamic, require `next build` or manifest proof that the route is still ISR/static, otherwise abstain.');
606 }
607 }
608 hints.push('Reusable server data where whole-response CDN caching is unsafe: recommend Runtime Cache only when the same result is reused across requests and the freshness/invalidation story is explicit.');