AGENTS.md
# vercel-optimize
Cross-agent entry point for the Vercel Optimize skill. The full procedure is in [SKILL.md](./SKILL.md).
Use this skill when the user asks to optimize a Vercel project, reduce a Vercel bill, investigate slow or expensive routes, find caching opportunities, reduce function invocations, or produce a Vercel cost/performance report.
Do not use it for projects that are not deployed on Vercel, greenfield projects with no traffic, or general code review.
## Requirements
- Node.js 20+
- Vercel CLI with `vercel metrics`, `vercel usage`, `vercel contract`, and `vercel api` support; v53+ is this skill's compatibility floor
- Authenticated Vercel CLI session
- Linked Vercel project directory (`vercel link`) for route metrics. `VERCEL_PROJECT_ID` can help resolve project config, but it does not replace directory linkage for `vercel metrics`. The project must resolve to a CLI-safe team or personal scope so `vercel metrics`, `vercel usage`, and `vercel contract` all run against the same account.
- Observability Plus for per-route metric analysis
## Procedure
1. Read [SKILL.md](./SKILL.md).
2. Collect Vercel signals before reading source files.
3. Gate candidates with deterministic scripts.
4. Investigate only files named by launched candidates.
5. Verify recommendations mechanically before rendering the report.
The hard rules are in [references/doctrine.md](./references/doctrine.md): observability first, deterministic gates, candidate-bound scope, and version-aware citations.
## Install
Preferred:
```bash
npx skills add vercel-labs/agent-skills --skill vercel-optimize
```
Manual project install:
```bash
mkdir -p .agents/skills
cp -R <agent-skills-repo>/skills/vercel-optimize .agents/skills/
```
Then add this to the project `AGENTS.md`:
```md
When optimizing Vercel cost or performance, follow
`.agents/skills/vercel-optimize/SKILL.md` before proposing changes.
Collect Vercel metrics before reading source files.
```
CONTRIBUTING.md
# Contributing to `vercel-optimize`
Keep changes small, metric-grounded, and fixture-tested. Runtime code lives in `skills/vercel-optimize`; tests and fixtures live in `packages/vercel-optimize-tests` so installed skills stay small.
## Common changes
| Change | Edit | Test |
|---|---|---|
| Gate | `lib/gates/<id>.mjs`, `lib/gates/index.mjs` | `node --test packages/vercel-optimize-tests/test/*gate*.test.mjs` |
| Scanner | `lib/scanners/<id>.mjs`, `lib/scanners/index.mjs` | Scanner-specific test in `packages/vercel-optimize-tests/test/` |
| Citation | `references/docs-library.json` | `node skills/vercel-optimize/scripts/check-citations.mjs` |
| Support topic | `references/support-topics/<id>.md` | `node --test packages/vercel-optimize-tests/test/support-topics.test.mjs` |
| Playbook | `references/playbooks/<profile>.md` and selection matrix in `references/scoring.md` | `node --test packages/vercel-optimize-tests/test/support-topics.test.mjs packages/vercel-optimize-tests/test/investigation-brief.test.mjs` |
| Renderer or verifier | `lib/render-report.mjs`, `lib/verify-claim.mjs`, or related module | Focused test plus full test suite |
Generated docs:
```bash
node skills/vercel-optimize/scripts/build-docs.mjs
node skills/vercel-optimize/scripts/check-docs-fresh.mjs
```
Full test loop:
```bash
node --test packages/vercel-optimize-tests/test/*.test.mjs
node skills/vercel-optimize/scripts/check-docs-fresh.mjs
node skills/vercel-optimize/scripts/check-citations.mjs
```
## Rules
- No runtime dependencies. Scripts use Node.js 20+ built-ins and the Vercel CLI.
- No recommendation without a Vercel metric signal, code evidence when code changes are proposed, and an allow-listed citation.
- No invented URLs, exact savings projections, or version-mismatched framework APIs.
- No internal repo paths, service names, customer names, or captured private output in fixtures.
- Keep generated report copy customer-facing. Put debug details behind `--debug-out`.
## Output contracts
Every JSON-emitting script must be deterministic: stable key order, stable sort order, 2-space indentation, trailing newline. If a consumed schema changes, update the schema version and the fixture tests in the same PR.
lib/auth-route.mjs
// Auth routes carry user state and must not be cached at CDN edge.
export const AUTH_ROUTE_REGEX =
/(login|logout|auth|account|dashboard|checkout|cart|profile|session|me)(?:\/|$)/i;
export function isAuthRoute(route) {
return AUTH_ROUTE_REGEX.test(String(route ?? ''));
}
// Non-cache candidates pass through — errors/slowness on auth routes still warrant investigation.
export function applyAuthDisqualifier(candidate) {
const cacheKinds = new Set(['uncached_route', 'cache_header_gap']);
if (!cacheKinds.has(candidate.kind)) return candidate;
if (!candidate.route) return candidate;
if (isAuthRoute(candidate.route)) {
return {
...candidate,
disqualified: true,
disqualifyReason: 'auth-like route — should not be cached at edge',
};
}
return candidate;
}
lib/budget-summary.mjs
// Checkpoint between gate and deep-dive. Asks only when budget was default AND >=1 candidate got skipped — every question is a tax on the user.
import { createHash } from 'node:crypto';
import { formatCandidateLine } from './display-labels.mjs';
const TOP_INVESTIGATING_PREVIEW = 5;
const MAX_FULL_INVESTIGATING_PREVIEW = 10;
export function buildBudgetSummary(gate) {
const toLaunch = Array.isArray(gate?.toLaunch) ? gate.toLaunch : [];
const gated = Array.isArray(gate?.gated) ? gate.gated : [];
const budgetSource = gate?.budget?.source ?? 'default';
const currentBudget =
typeof gate?.budget?.maxCandidates === 'number'
? gate.budget.maxCandidates
: (gate?.budget?.maxCandidates === 'all' ? Infinity : 6);
// Only budget skips can be reached by raising the budget; disqualified/coveredBy can't.
const skippedByBudget = gated.filter((g) =>
typeof g.gatedReason === 'string' && g.gatedReason.startsWith('skippedByBudget')
);
const skipped = skippedByBudget.length;
const totalPassed = toLaunch.length + skipped;
const reasonParts = [];
if (budgetSource !== 'default') reasonParts.push(`user pre-set budget via ${budgetSource}`);
if (skipped === 0) reasonParts.push('no candidates skipped by budget');
const shouldAsk = budgetSource === 'default' && skipped > 0;
const reason = shouldAsk
? `default budget skipped ${skipped} candidate(s); ask user whether to expand`
: reasonParts.join('; ') || 'no expansion possible';
const summarize = (c) => ({
kind: c.kind,
route: c.route ?? c.hostname ?? null,
displayRoute: c.displayRoute ?? null,
o11ySignal: c.o11ySignal ?? null,
priority: c.priority ?? null,
});
const investigatingPreviewCount = typeof currentBudget === 'number' && currentBudget <= MAX_FULL_INVESTIGATING_PREVIEW
? currentBudget
: TOP_INVESTIGATING_PREVIEW;
const topInvestigating = toLaunch.slice(0, investigatingPreviewCount).map(summarize);
const topSkipped = skippedByBudget.map(summarize);
const options = buildOptions(toLaunch.length, skipped);
const questionText = buildQuestionText({ shouldAsk, totalPassed, currentBudget });
const printContract = shouldAsk
? 'Print chatPreview verbatim by copying exactChatMessage.body as a chat message before asking questionText. Do not summarize, truncate, reorder, shorten, or rewrite options.'
: null;
const questionPayload = shouldAsk ? buildQuestionPayload(questionText, options) : null;
const chatPreview = buildChatPreview({ shouldAsk, totalPassed, currentBudget, skipped, topInvestigating, topSkipped, reason });
const exactChatMessage = buildExactChatMessage(chatPreview);
return {
shouldAsk,
reason,
totalPassed,
currentBudget: currentBudget === Infinity ? 'all' : currentBudget,
budgetSource,
skipped,
topInvestigating,
topSkipped,
options,
printContract,
chatPreview,
exactChatMessage,
printCheck: shouldAsk ? buildPrintCheck({ exactChatMessage, skipped }) : null,
questionText,
questionPayload,
};
}
function buildChatPreview({ shouldAsk, totalPassed, currentBudget, skipped, topInvestigating, topSkipped, reason }) {
if (!shouldAsk) return `Audit scope: no question needed — ${reason}.`;
const lines = [];
lines.push(`Found ${totalPassed} potential issue${totalPassed === 1 ? '' : 's'} worth checking. By default I'll inspect the ${currentBudget} strongest now; ${skipped} will stay in the report for a larger run.`);
lines.push(`Choose a larger scope if you want broader coverage. More checks take longer.`);
if (topInvestigating.length > 0) {
lines.push('');
lines.push(`Checking now${topInvestigating.length < currentBudget ? ` (${topInvestigating.length} shown)` : ''}:`);
topInvestigating.forEach((c, i) => lines.push(` ${i + 1}. ${formatCandidateLine(c)}`));
}
if (topSkipped.length > 0) {
lines.push('');
lines.push(`Only checked if you expand this run (${topSkipped.length}):`);
topSkipped.forEach((c, i) => lines.push(` ${i + 1}. ${formatCandidateLine(c)}`));
}
return lines.join('\n');
}
function buildExactChatMessage(body) {
return {
body,
lineCount: body.split('\n').length,
sha256: createHash('sha256').update(body).digest('hex'),
};
}
function buildPrintCheck({ exactChatMessage, skipped }) {
return {
bodyField: 'exactChatMessage.body',
sameAs: 'chatPreview',
requiredLineCount: exactChatMessage.lineCount,
requiredSha256: exactChatMessage.sha256,
requiredSkippedRows: skipped,
requiredSkippedHeading: `Only checked if you expand this run (${skipped}):`,
forbiddenSummaryPatterns: [
'\\btop skipped\\b',
'\\bmore (?:candidate|candidates|routes|entries|items|in gated list)\\b',
'\\b\\d+\\s*[-–—]\\s*\\d+\\.\\s+\\d+\\s+more\\b',
'\\betc\\.\\b',
],
instruction: 'The budget message is valid only when every line from exactChatMessage.body is preserved exactly. If you cannot verify that, print exactChatMessage.body again before asking the question.',
};
}
function buildQuestionText({ shouldAsk, totalPassed, currentBudget }) {
if (!shouldAsk) return '';
return `How many potential issues should I check in this run?`;
}
function buildOptions(currentCount, skippedCount) {
if (skippedCount === 0) return [];
const total = currentCount + skippedCount;
return [
{
label: `Check ${currentCount} (default)`,
value: currentCount,
recommended: true,
description: 'Fastest first pass; checks the strongest cost and performance signals.',
rationale: 'fastest first pass; checks the strongest cost and performance signals',
},
{
label: `Check all ${total}`,
value: 'all',
recommended: false,
description: 'Most complete; takes longer because every flagged route is investigated.',
rationale: 'most complete; takes longer because every flagged route is investigated',
},
{
label: 'Pick a number',
value: 'custom',
recommended: false,
description: `Check more than ${currentCount} without running the full ${total}.`,
rationale: `checks more than ${currentCount} without running the full ${total}`,
},
];
}
function buildQuestionPayload(questionText, options) {
return {
questions: [{
question: questionText,
header: 'Audit scope',
multiSelect: false,
options: options.map((o) => ({
label: o.label,
description: o.description ?? o.rationale,
})),
}],
};
}
export function renderBudgetSummaryMarkdown(s) {
const lines = [];
lines.push(`## Audit scope`);
lines.push('');
if (!s.shouldAsk) {
lines.push(`_No question needed — ${s.reason}._`);
return lines.join('\n');
}
for (const ln of s.chatPreview.split('\n')) lines.push(ln);
lines.push('');
lines.push('### Options');
lines.push('');
for (const o of s.options) {
const tag = o.recommended ? ' (recommended)' : '';
lines.push(`- **${o.label}${tag}** — ${o.rationale}`);
}
lines.push('');
lines.push(`**Question:** ${s.questionText}`);
return lines.join('\n');
}
lib/citations.mjs
// Curated doc library — the allow-list for recommender citations.
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const HERE = dirname(fileURLToPath(import.meta.url));
const LIBRARY_PATH = join(HERE, '..', 'references', 'docs-library.json');
let cached;
export async function loadLibrary() {
if (cached) return cached;
const raw = await readFile(LIBRARY_PATH, 'utf-8');
cached = JSON.parse(raw);
return cached;
}
export async function isKnownUrl(url) {
const lib = await loadLibrary();
return lib.urls.some(e => e.url === url);
}
export async function lookupUrl(url) {
const lib = await loadLibrary();
return lib.urls.find(e => e.url === url);
}
export async function lookupSkillRule(ref) {
const lib = await loadLibrary();
const m = ref.match(/^([\w-]+):([\w-]+)$/);
if (!m) return undefined;
return lib.ruleSkillRefs.find(r => r.skill === m[1] && r.rule === m[2]);
}
// Narrow semver subset: "*", "fw@*", "fw@14", "fw@>=15.0.0", "fw@<X", "fw@X.Y", "fw@X.Y.Z", "a || b".
export function matchesFrameworkVersion(pattern, framework, version) {
if (pattern === '*') return true;
if (pattern.includes('||')) {
return pattern.split('||').map(p => p.trim()).some(p =>
matchesFrameworkVersion(p, framework, version)
);
}
const m = pattern.match(/^([\w-]+)@(.+)$/);
if (!m) return false;
const [, fw, range] = m;
if (fw !== framework) return false;
if (range === '*') return true;
const verParts = parseVersion(version);
if (!verParts) return false;
let m2 = range.match(/^>=\s*(.+)$/);
if (m2) {
const min = parseVersion(m2[1]);
return min ? compareVersion(verParts, min) >= 0 : false;
}
m2 = range.match(/^<\s*(.+)$/);
if (m2) {
const max = parseVersion(m2[1]);
return max ? compareVersion(verParts, max) < 0 : false;
}
if (/^\d+$/.test(range)) {
return verParts[0] === Number(range);
}
m2 = range.match(/^(\d+)\.(\d+)$/);
if (m2) {
return verParts[0] === Number(m2[1]) && verParts[1] === Number(m2[2]);
}
const exact = parseVersion(range);
if (exact) return compareVersion(verParts, exact) === 0;
return false;
}
function parseVersion(v) {
const m = String(v).replace(/^[v^~]+/, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
if (!m) return null;
return [Number(m[1]) || 0, Number(m[2]) || 0, Number(m[3]) || 0];
}
function compareVersion(a, b) {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) return a[i] - b[i];
}
return 0;
}
// Filtered subset embedded in recommender prompt — LLM never sees URLs for features not in user's stack.
export async function libraryForStack(framework, version) {
const lib = await loadLibrary();
const matches = (frameworks) =>
frameworks.some(p => matchesFrameworkVersion(p, framework, version) || p === '*');
return {
urls: lib.urls.filter(e => matches(e.applicableFrameworks)),
ruleSkillRefs: lib.ruleSkillRefs.filter(r => matches(r.applicableFrameworks)),
};
}
export async function sanitizeCitations(rec, framework, version) {
const lib = await loadLibrary();
const strippedUnknown = [];
const strippedVersion = [];
const kept = [];
for (const cite of rec.citations ?? []) {
const ruleRef = await lookupSkillRule(cite);
if (ruleRef) {
if (matchesFrameworkVersion(ruleRef.applicableFrameworks.join(' || '), framework, version) || ruleRef.applicableFrameworks.includes('*')) {
kept.push(cite);
} else {
strippedVersion.push(cite);
}
continue;
}
const entry = lib.urls.find(e => e.url === cite);
if (!entry) {
strippedUnknown.push(cite);
continue;
}
if (entry.applicableFrameworks.includes('*') ||
entry.applicableFrameworks.some(p => matchesFrameworkVersion(p, framework, version))) {
kept.push(cite);
} else {
strippedVersion.push(cite);
}
}
rec.citations = kept;
return { rec, strippedUnknown, strippedVersion };
}
lib/cost-coverage.mjs
// Maps billing line items to gate coverage so report surfaces uncovered dimensions (Sandbox, AI Gateway, Build, …) as blind spots.
// Service → billing dimension. dim=null means uncovered. Substring match — Vercel billing names are stable but untyped.
const SERVICE_DIMENSION = [
{ match: /^Function Duration$/i, dim: 'function-duration' },
{ match: /^Function Invocations$/i, dim: 'function-duration' },
{ match: /^Fluid Active CPU$/i, dim: 'function-duration' },
{ match: /^Fluid Provisioned Memory$/i, dim: 'function-duration' },
{ match: /^Edge Requests$/i, dim: 'edge-requests' },
{ match: /^Edge Requests.*Additional CPU Duration/i, dim: 'edge-requests' },
{ match: /^Edge Function Execution Units$/i, dim: 'edge-requests' },
{ match: /^Edge Middleware Invocations$/i, dim: 'edge-requests' },
{ match: /^ISR (Reads|Writes)$/i, dim: 'isr' },
{ match: /^Speed Insights( Data Points)?$/i, dim: 'speed-insights' },
{ match: /^Image Optimization/i, dim: 'image-optimization' },
// Indirect: bot-protection gate addresses bandwidth/edge spend.
{ match: /^Fast Data Transfer$/i, dim: 'edge-requests' },
{ match: /^Fast Origin Transfer$/i, dim: 'edge-requests' },
// Uncovered.
{ match: /^Sandbox/i, dim: null, family: 'sandbox' },
{ match: /^AI Gateway$/i, dim: null, family: 'ai-gateway' },
{ match: /^Build Minutes$/i, dim: 'build', family: 'build' },
{ match: /^Build CPU Minutes$/i, dim: 'build', family: 'build' },
{ match: /^Private Data Transfer$/i, dim: null, family: 'private-network' },
{ match: /^Secure Compute Network$/i, dim: null, family: 'private-network' },
{ match: /^Drains Volume$/i, dim: null, family: 'drains' },
{ match: /^Observability Events$/i, dim: 'observability-events', family: 'observability-events' },
{ match: /^Blob/i, dim: null, family: 'blob' },
{ match: /^Edge Config (Reads|Writes)$/i, dim: null, family: 'edge-config' },
{ match: /^Runtime Cache/i, dim: null, family: 'runtime-cache' },
{ match: /^Microfrontends/i, dim: null, family: 'microfrontends' },
{ match: /^Workflow/i, dim: null, family: 'workflow' },
{ match: /^Queue/i, dim: null, family: 'queues' },
{ match: /^Flag Requests$/i, dim: null, family: 'flags' },
{ match: /^Flags Explorer/i, dim: null, family: 'flags' },
{ match: /^BotID/i, dim: null, family: 'botid' },
{ match: /^Firewall/i, dim: null, family: 'firewall' },
{ match: /^Vercel Agent$/i, dim: null, family: 'vercel-agent' },
// Fixed costs (seats, contracts) — not actionable.
{ match: /^v0 /i, dim: null, family: 'fixed', actionable: false },
{ match: /^Additional Team Seats$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^SAML$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^HIPAA BAA$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^SIEM Integration$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^Web Analytics/i, dim: null, family: 'fixed', actionable: false },
{ match: /^Static IPs$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^Bulk Redirects$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^Preview Deployment Suffix$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^Rolling Releases$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^Observability Plus$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^Platform Customer Usage$/i, dim: null, family: 'fixed', actionable: false },
{ match: /^Advanced Deployment Protection$/i, dim: null, family: 'fixed', actionable: false },
];
export function classifyService(serviceName, activeDims) {
if (!serviceName) return { covered: false, family: 'unknown' };
for (const e of SERVICE_DIMENSION) {
if (e.match.test(serviceName)) {
if (e.dim && activeDims.has(e.dim)) return { covered: true, dim: e.dim };
return { covered: false, family: e.family ?? 'unknown', actionable: e.actionable ?? true };
}
}
return { covered: false, family: 'unknown', actionable: true };
}
export function computeCostCoverage(usage, gates) {
const services = Array.isArray(usage?.services) ? usage.services : [];
const activeDims = new Set(
(gates ?? [])
.map((g) => g?.metadata?.billingDimension)
.filter((d) => typeof d === 'string' && d !== 'mixed')
);
let total = 0;
let covered = 0;
let uncovered = 0;
const byFamily = new Map();
for (const s of services) {
const billed = Number(s.billedCost ?? 0);
if (!Number.isFinite(billed) || billed <= 0) continue;
total += billed;
const c = classifyService(s.name, activeDims);
if (c.covered) {
covered += billed;
continue;
}
uncovered += billed;
const key = c.family;
const prev = byFamily.get(key) ?? { family: key, billed: 0, services: [], actionable: c.actionable !== false };
prev.billed += billed;
prev.services.push({ name: s.name, billed });
prev.actionable = prev.actionable && (c.actionable !== false);
byFamily.set(key, prev);
}
const uncoveredByFamily = [...byFamily.values()]
.sort((a, b) => b.billed - a.billed)
.map((f) => ({ ...f, services: f.services.sort((a, b) => b.billed - a.billed) }));
// Pick top gaps globally so multiple families surface (Sandbox + AI Gateway + Build, not 5 Sandbox sub-services). Exclude fixed costs — seats aren't actionable workload.
const allActionableServices = [];
for (const family of uncoveredByFamily) {
if (!family.actionable) continue;
for (const s of family.services) {
allActionableServices.push({ name: s.name, billed: s.billed, family: family.family });
}
}
allActionableServices.sort((a, b) => b.billed - a.billed);
const topGaps = allActionableServices.slice(0, 5).map((s) => ({
...s,
share: total > 0 ? s.billed / total : 0,
}));
return { totalBilled: total, coveredBilled: covered, uncoveredBilled: uncovered, uncoveredByFamily, topGaps };
}
export function renderCostCoverageMarkdown(coverage) {
if (!coverage || !Number.isFinite(coverage.totalBilled) || coverage.totalBilled <= 0) return [];
const { totalBilled, coveredBilled, uncoveredBilled, topGaps } = coverage;
const actionableGaps = topGaps.filter((g) => g.share >= 0.01); // 1%+ share
if (actionableGaps.length === 0) return [];
const lines = [];
lines.push('');
lines.push('### Coverage gaps');
lines.push('');
const coveredPct = totalBilled > 0 ? (coveredBilled / totalBilled) * 100 : 0;
const uncoveredPct = totalBilled > 0 ? (uncoveredBilled / totalBilled) * 100 : 0;
lines.push(`This audit has metric coverage for **$${coveredBilled.toFixed(0)} (${coveredPct.toFixed(0)}%)** of this bill via function-duration, edge-requests, ISR, middleware, and image-optimization dimensions. **$${uncoveredBilled.toFixed(0)} (${uncoveredPct.toFixed(0)}%)** sits in billed areas this run cannot analyze safely, including the top actionable items below:`);
lines.push('');
lines.push('| Service | Billed | Share | Family | Coverage |');
lines.push('|---|---|---|---|---|');
for (const g of actionableGaps) {
lines.push(`| ${escapeCell(g.name)} | $${g.billed.toFixed(2)} | ${(g.share * 100).toFixed(1)}% | ${g.family} | _not analyzed in this run_ |`);
}
lines.push('');
lines.push('_Recommendations in this report address the covered dimensions. The uncovered rows are not ignored; they need a separate investigation before we can make safe recommendations._');
return lines;
}
function escapeCell(s) {
return String(s ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
}
lib/dedup-recs.mjs
const NO_VALUE = '<none>';
export function dedupeRecommendations(recommendations = []) {
if (!Array.isArray(recommendations)) {
throw new TypeError('dedupeRecommendations recommendations must be an array');
}
const byKey = new Map();
const order = [];
for (const rec of recommendations) {
if (!rec || typeof rec !== 'object' || rec.abstain === true) {
order.push(rec);
continue;
}
const key = recommendationKey(rec);
if (!byKey.has(key)) {
const normalized = withDedupMetadata(rec);
byKey.set(key, normalized);
order.push({ __dedupKey: key });
continue;
}
const current = byKey.get(key);
const merged = mergeDuplicateRecs(current, rec);
byKey.set(key, merged);
}
return order.map((entry) => entry?.__dedupKey ? byKey.get(entry.__dedupKey) : entry);
}
export function recommendationKey(rec) {
const intent = dedupIntent(rec);
const bucket = intent === 'cache-control:s-maxage'
? NO_VALUE
: String(rec?.bucket ?? NO_VALUE);
return JSON.stringify([
bucket,
dedupEditTarget(rec),
primarySkillRule(rec),
intent,
]);
}
export function normalizePath(path) {
if (typeof path !== 'string' || path.trim() === '') return NO_VALUE;
return path
.trim()
.replace(/\\/g, '/')
.replace(/^\.\//, '')
.replace(/\/+/g, '/')
.replace(/:(\d+)(?::\d+)?$/, '');
}
export function primarySkillRule(rec) {
const citations = Array.isArray(rec?.citations) ? rec.citations : [];
return citations.find((c) => typeof c === 'string' && /^[A-Za-z][\w-]*:[A-Za-z][\w-]*$/.test(c)) ?? NO_VALUE;
}
export function fixShape(rec) {
if (typeof rec?.fixShape === 'string' && rec.fixShape.trim()) {
return normalizeFixText(rec.fixShape);
}
const primaryText = [rec?.fix, rec?.desiredBehavior]
.filter((v) => typeof v === 'string' && v.trim())
.join('\n');
const text = primaryText || rec?.what;
return normalizeFixText(text);
}
export function dedupIntent(rec) {
if (isSMaxageCacheHeaderRec(rec)) return 'cache-control:s-maxage';
if (isCacheLifeRec(rec)) return cacheLifeIntent(rec);
const sharedFunction = sharedFunctionTarget(rec);
if (sharedFunction) return `parallel-shared-helper:${sharedFunction}`;
return fixShape(rec);
}
export function dedupEditTarget(rec) {
return sharedFunctionTarget(rec) ?? normalizePath(firstAffectedFile(rec));
}
function firstAffectedFile(rec) {
const direct = affectedFiles(rec);
const editTarget = referencedCodeFiles(rec, ['fix', 'desiredBehavior', 'currentBehavior'])[0];
if (editTarget) return editTarget;
const referenced = referencedCodeFiles(rec)
.find((file) => direct.includes(file));
if (referenced) return referenced;
return Array.isArray(rec?.affectedFiles) ? rec.affectedFiles[0] : null;
}
function affectedFiles(rec) {
return Array.isArray(rec?.affectedFiles)
? rec.affectedFiles.map(normalizePath).filter((file) => file !== NO_VALUE)
: [];
}
function referencedCodeFiles(rec, fields = ['what', 'why', 'fix', 'currentBehavior', 'desiredBehavior', 'verify']) {
const text = fields
.map((field) => rec?.[field])
.filter((v) => typeof v === 'string' && v.trim())
.join('\n');
const matches = text.match(/(?:^|[\s`'"(])((?:\.{1,2}\/|[A-Za-z0-9_.@-]+\/)[A-Za-z0-9_./@[\]()-]+\.(?:mjs|cjs|js|jsx|ts|tsx))/g) ?? [];
return unique(matches.map((m) =>
normalizePath(m.replace(/^[\s`'"(]+/, ''))
).filter((file) => file !== NO_VALUE));
}
function isSMaxageCacheHeaderRec(rec) {
const text = [
rec?.what,
rec?.why,
rec?.fix,
rec?.desiredBehavior,
...(Array.isArray(rec?.citations) ? rec.citations : []),
].filter(Boolean).join('\n');
return /\bs-maxage\b/i.test(text) &&
/\b(?:Cache-Control|CDN cache|cdn-cache|caching\/cdn-cache)\b/i.test(text);
}
function isCacheLifeRec(rec) {
const text = [
rec?.candidateRef,
rec?.what,
rec?.why,
rec?.fix,
rec?.desiredBehavior,
...(Array.isArray(rec?.citations) ? rec.citations : []),
].filter(Boolean).join('\n');
return /^isr_overrevalidation:/.test(String(rec?.candidateRef ?? '')) &&
/\bcacheLife\s*\(|\bcacheLife\b/i.test(text);
}
function sharedFunctionTarget(rec) {
const rule = primarySkillRule(rec);
if (!/(?:^|:)async-parallel$|(?:^|:)server-parallel-fetching$|(?:^|:)async-suspense-boundaries$/.test(rule)) {
return null;
}
const text = [
rec?.what,
rec?.why,
rec?.fix,
rec?.currentBehavior,
rec?.desiredBehavior,
].filter((v) => typeof v === 'string' && v.trim()).join('\n');
const names = [
...text.matchAll(/\b(?:get|fetch|load|read|render|create|generate|filter|resolve)[A-Z][A-Za-z0-9_]*\b/g),
].map((m) => m[0]);
const stop = new Set([
'getPayload',
'draftMode',
'notFound',
'redirect',
'Promise',
'Response',
'NextResponse',
]);
const candidates = names.filter((name) => !stop.has(name));
if (candidates.length === 0) return null;
const score = new Map();
for (const name of candidates) {
score.set(name, (score.get(name) ?? 0) + 1);
}
return [...score.entries()]
.sort((a, b) => b[1] - a[1] || text.indexOf(a[0]) - text.indexOf(b[0]))
.map(([name]) => `function:${name}`)[0] ?? null;
}
function cacheLifeIntent(rec) {
const text = [
rec?.what,
rec?.why,
rec?.fix,
rec?.desiredBehavior,
rec?.verify,
].filter(Boolean).join('\n');
const profiles = unique(
[...text.matchAll(/\bcacheLife\s*\(\s*['"`]([^'"`]+)['"`]/g)]
.map((m) => m[1])
);
const tags = unique([
...[...text.matchAll(/\bcacheTag\s*\(([^)]*)\)/gs)].flatMap((m) => {
const args = m[1] ?? '';
return [
...[...args.matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]),
...[...args.matchAll(/`([^`]+)`/g)].map((x) => x[1].includes('${') ? `${x[1].split('${')[0]}*` : x[1]),
];
}),
]);
const invalidation = /\b(?:revalidateTag|updateTag)\s*\(/.test(text) ? 'with-invalidation-api' : 'no-invalidation-api';
return [
'next-cache:cache-life',
profiles.join('|') || NO_VALUE,
tags.join('|') || NO_VALUE,
invalidation,
].join(':');
}
function unique(values) {
return Array.from(new Set(values.filter((v) => typeof v === 'string' && v.trim()).map((v) => v.trim()))).sort();
}
function normalizeFixText(text) {
if (typeof text !== 'string' || text.trim() === '') return NO_VALUE;
return text
.toLowerCase()
.replace(/```[\s\S]*?```/g, ' codeblock ')
.replace(/`[^`]*`/g, ' code ')
.replace(/\b\d+(?:\.\d+)?(?:ms|s|%|kb|mb|gb|k|m)?\b/g, '#')
.replace(/[^a-z0-9#]+/g, ' ')
.trim()
.split(/\s+/)
.slice(0, 80)
.join(' ') || NO_VALUE;
}
function withDedupMetadata(rec) {
const existing = normalizedAppliesAlsoTo(rec.appliesAlsoTo);
const count = Math.max(
numericCount(rec.corroborationCount),
1 + existing.length,
);
return existing.length > 0 || count > 1
? { ...rec, appliesAlsoTo: existing, corroborationCount: count }
: { ...rec };
}
function mergeDuplicateRecs(a, b) {
const aScore = recScore(a);
const bScore = recScore(b);
const winner = bScore > aScore ? b : a;
const loser = winner === a ? b : a;
const winnerExisting = normalizedAppliesAlsoTo(winner.appliesAlsoTo);
const loserExisting = normalizedAppliesAlsoTo(loser.appliesAlsoTo);
const appliesAlsoTo = uniqueAppliesAlsoTo([
...winnerExisting,
appliesAlsoEntry(loser),
...loserExisting,
]);
const corroborationCount =
numericCount(winner.corroborationCount) + numericCount(loser.corroborationCount);
return {
...winner,
appliesAlsoTo,
corroborationCount: Math.max(corroborationCount, 1 + appliesAlsoTo.length),
};
}
function recScore(rec) {
const priority = typeof rec?.priority === 'number' ? rec.priority : 0;
const quality = typeof rec?.quality?.overall === 'number' ? rec.quality.overall : 0;
return (priority * 1_000_000_000_000) + signalMagnitude(rec) + quality;
}
function signalMagnitude(rec) {
const text = [
rec?.o11ySignal,
rec?.why,
rec?.what,
rec?.impact,
].filter((v) => typeof v === 'string' && v.trim()).join('\n');
const inv = parseNumber(text, /(?:inv|invocations?|function invocations?|requests?)[:=]\s*([\d,]+)/i);
const p95 = parseNumber(text, /(?:p95|95th percentile(?: duration)?)[:=]?\s*([\d,]+)\s*ms/i);
const errors = parseNumber(text, /(?:errs|errors?)[:=]\s*([\d,]+)/i);
const writes = parseNumber(text, /writes[:=]\s*([\d,]+)/i);
const reads = parseNumber(text, /reads[:=]\s*([\d,]+)/i);
if (inv != null && p95 != null) return inv * p95;
if (errors != null) return errors;
if (writes != null && reads != null) return writes + reads;
if (inv != null) return inv;
return 0;
}
function parseNumber(text, re) {
const match = re.exec(text);
if (!match) return null;
const value = Number(String(match[1]).replace(/,/g, ''));
return Number.isFinite(value) ? value : null;
}
function numericCount(value) {
return Number.isFinite(value) && value > 0 ? value : 1;
}
function appliesAlsoEntry(rec) {
return {
candidateRef: rec?.candidateRef ?? null,
affectedFiles: Array.isArray(rec?.affectedFiles)
? rec.affectedFiles.map(normalizePath).filter((p) => p !== NO_VALUE)
: [],
o11ySignal: rec?.o11ySignal ?? null,
what: rec?.what ?? null,
};
}
function normalizedAppliesAlsoTo(entries) {
if (!Array.isArray(entries)) return [];
return entries
.filter((e) => e && typeof e === 'object')
.map((e) => ({
candidateRef: e.candidateRef ?? null,
affectedFiles: Array.isArray(e.affectedFiles)
? e.affectedFiles.map(normalizePath).filter((p) => p !== NO_VALUE)
: [],
o11ySignal: e.o11ySignal ?? null,
what: e.what ?? null,
}));
}
function uniqueAppliesAlsoTo(entries) {
const seen = new Set();
const out = [];
for (const entry of entries) {
const key = JSON.stringify([
entry.candidateRef ?? NO_VALUE,
entry.affectedFiles?.join(',') ?? NO_VALUE,
entry.what ?? NO_VALUE,
]);
if (seen.has(key)) continue;
seen.add(key);
out.push(entry);
}
return out;
}
lib/deep-dive.mjs
// Per-candidate deep-dive query specs. Runs after gate, before sub-agent reads source.
//
// CLI quirks:
// - Multi `-a` flag is NOT supported. One percentile per query.
// - External-API "calling route" dim is `origin_route` (NOT `route`).
// Same window as broad pass so rolls are comparable.
import { TIME_WINDOW } from './queries.mjs';
export { TIME_WINDOW };
// Per-query is scoped to one route/hostname, so cardinality stays small — higher than broad-pass caps.
const DEPLOYMENT_LIMIT = 10;
const ERROR_DEPLOYMENT_LIMIT = 30;
const ERROR_CODE_LIMIT = 50;
const WAF_RULE_LIMIT = 20;
const MIDDLEWARE_PATH_LIMIT = 50;
const CALLER_LIMIT = 20;
// OData escapes a literal `'` inside a string by doubling it (`it's` → `it''s`).
export function escapeODataString(s) {
if (typeof s !== 'string') return '';
return s.replace(/'/g, "''");
}
export function odataEq(dim, value) {
return `${dim} eq '${escapeODataString(value)}'`;
}
export function odataAnd(...conds) {
return conds.filter(Boolean).join(' and ');
}
export const SPEC_GENERATORS = {
slow_route(c) {
const route = c.route;
if (!route) return [];
const f = odataEq('route', route);
// cacheBreakdown/bandwidthByCache let sub-agent see miss-path cost on static routes (dynamic='error' can still show p95=900ms over millions of requests).
return [
...latencyPercentiles('latency', 'vercel.function_invocation.function_duration_ms', f),
...latencyPercentiles('ttfb', 'vercel.function_invocation.ttfb_ms', f),
...latencyPercentiles('cpu', 'vercel.function_invocation.function_cpu_time_ms', f, ['p95']),
{
id: 'startTypeSplit',
metricId: 'vercel.function_invocation.count',
aggregation: 'sum',
groupBy: ['function_start_type'],
filter: f,
broadPassEquivalent: { key: 'fnStartTypeByRoute', routeFilter: route, projectDims: ['function_start_type'] },
},
// function-invocation status (5xx from function) — distinct from request-level status, can't reuse broad-pass.
{
id: 'statusDistribution',
metricId: 'vercel.function_invocation.count',
aggregation: 'sum',
groupBy: ['http_status'],
filter: f,
},
{
id: 'perDeployment',
metricId: 'vercel.function_invocation.function_duration_ms',
aggregation: 'p95',
groupBy: ['deployment_id'],
filter: f,
limit: DEPLOYMENT_LIMIT,
},
{
id: 'cacheBreakdown',
metricId: 'vercel.request.count',
aggregation: 'sum',
groupBy: ['cache_result'],
filter: f,
broadPassEquivalent: { key: 'requestsByRouteCache', routeFilter: route, projectDims: ['cache_result'] },
},
// broad-pass bandwidthByCacheResult is account-wide, so per-route still required.
{
id: 'bandwidthByCache',
metricId: 'vercel.request.fdt_total_bytes',
aggregation: 'sum',
groupBy: ['cache_result'],
filter: f,
},
];
},
uncached_route(c) {
const route = c.route;
if (!route) return [];
const f = odataEq('route', route);
return [
{
id: 'cacheBreakdown',
metricId: 'vercel.request.count',
aggregation: 'sum',
groupBy: ['cache_result'],
filter: f,
broadPassEquivalent: { key: 'requestsByRouteCache', routeFilter: route, projectDims: ['cache_result'] },
},
{
id: 'methodDistribution',
metricId: 'vercel.request.count',
aggregation: 'sum',
groupBy: ['request_method'],
filter: f,
broadPassEquivalent: { key: 'requestsByRouteMethod', routeFilter: route, projectDims: ['request_method'] },
},
{
id: 'botShare',
metricId: 'vercel.request.fdt_total_bytes',
aggregation: 'sum',
groupBy: ['bot_category'],
filter: f,
},
{
id: 'bandwidthByCache',
metricId: 'vercel.request.fdt_total_bytes',
aggregation: 'sum',
groupBy: ['cache_result'],
filter: f,
},
];
},
cold_start(c) {
const route = c.route;
if (!route) return [];
const f = odataEq('route', route);
return [
{
id: 'startTypeSplit',
metricId: 'vercel.function_invocation.count',
aggregation: 'sum',
groupBy: ['function_start_type'],
filter: f,
},
{
id: 'coldVsWarmLatencyP95',
metricId: 'vercel.function_invocation.function_duration_ms',
aggregation: 'p95',
groupBy: ['function_start_type'],
filter: f,
},
{
id: 'coldByDeployment',
metricId: 'vercel.function_invocation.count',
aggregation: 'sum',
groupBy: ['deployment_id'],
filter: odataAnd(f, odataEq('function_start_type', 'cold')),
limit: DEPLOYMENT_LIMIT,
},
];
},
route_errors(c) {
const route = c.route;
if (!route) return [];
const f = odataEq('route', route);
return [
{
id: 'errorStatusPattern',
metricId: 'vercel.request.count',
aggregation: 'sum',
groupBy: ['http_status'],
filter: odataAnd(f, "http_status ge '500'"),
},
{
id: 'errorCodes',
metricId: 'vercel.function_invocation.count',
aggregation: 'sum',
groupBy: ['error_code'],
filter: f,
limit: ERROR_CODE_LIMIT,
},
{
id: 'errorsByDeployment',
metricId: 'vercel.function_invocation.count',
aggregation: 'sum',
groupBy: ['deployment_id', 'http_status'],
filter: f,
limit: ERROR_DEPLOYMENT_LIMIT,
},
];
},
external_api_slow(c) {
const host = c.hostname;
if (!host) return [];
const f = odataEq('origin_hostname', host);
return [
...latencyPercentiles('latency', 'vercel.external_api_request.request_duration_ms', f),
{
// "calling route" dim is origin_route (verified via metrics schema).
id: 'callersByRoute',
metricId: 'vercel.external_api_request.count',
aggregation: 'sum',
groupBy: ['origin_route'],
filter: f,
limit: CALLER_LIMIT,
},
{
id: 'transferBytes',
metricId: 'vercel.external_api_request.transfer_bytes',
aggregation: 'sum',
groupBy: [],
filter: f,
},
];
},
isr_overrevalidation(c) {
const route = c.route;
if (!route) return [];
const f = odataEq('route', route);
return [
{
id: 'writePattern',
metricId: 'vercel.isr_operation.write_units',
aggregation: 'sum',
groupBy: ['cache_result'],
filter: f,
},
{
id: 'readPattern',
metricId: 'vercel.isr_operation.read_units',
aggregation: 'sum',
groupBy: ['cache_result'],
filter: f,
},
];
},
cwv_poor(c) {
const route = c.route;
if (!route) return [];
const f = odataEq('route', route);
return [
...latencyPercentiles('lcp', 'vercel.speed_insights_metric.lcp', f, ['p50', 'p75', 'p95']),
...latencyPercentiles('inp', 'vercel.speed_insights_metric.inp', f, ['p50', 'p75', 'p95']),
...latencyPercentiles('cls', 'vercel.speed_insights_metric.cls', f, ['p50', 'p75', 'p95']),
];
},
middleware_heavy(_c) {
// Account-scope. Surface top middleware-paths so recommender has named targets.
return [
{
id: 'topMiddlewarePaths',
metricId: 'vercel.middleware_invocation.count',
aggregation: 'sum',
groupBy: ['request_path'],
limit: MIDDLEWARE_PATH_LIMIT,
},
];
},
platform_fluid_compute(_c) {
// Broad-pass fnStartTypeByRoute already covers this account-scope rec; runner notes reuse.
return [];
},
platform_bot_protection(_c) {
return [
{
id: 'wafRuleFirings',
metricId: 'vercel.firewall_action.count',
aggregation: 'sum',
groupBy: ['waf_rule_id'],
limit: WAF_RULE_LIMIT,
},
];
},
observability_events_attribution(_c) {
// Account-scope billing signal; broad-pass usage and existing route/cache/middleware metrics carry the evidence.
return [];
},
usage_spike_triage(_c) {
// Daily billing breakdown is already in the gate evidence; no per-candidate metrics query exists.
return [];
},
build_minutes_fanout(_c) {
// Account-scope billing signal + scanner findings carry the evidence; no per-candidate query.
return [];
},
region_misconfig(_c) {
// Branch 2 (scanner-only) — per-region TTFB metric unavailable today, so no deep-dive query.
return [];
},
};
// Scanner-driven kinds skip deep-dive — evidence already in scanner findings (file + line).
export const SCANNER_KINDS = new Set([
'image_optimization',
'cache_header_gap',
'rendering_candidate',
'use_cache_date_stamp',
'cache_components_suspense_dedupe',
]);
export function specsForCandidate(candidate) {
const kind = candidate?.kind;
if (!kind) return [];
if (SCANNER_KINDS.has(kind)) return [];
const gen = SPEC_GENERATORS[kind];
if (!gen) return [];
return gen(candidate).map((s) => ({ since: TIME_WINDOW, ...s }));
}
// One spec per percentile — CLI does not support `-a p50 -a p95` multi-aggregation.
function latencyPercentiles(idPrefix, metricId, filter, percentiles = ['p50', 'p75', 'p95', 'p99']) {
return percentiles.map((p) => ({
id: `${idPrefix}.${p}`,
metricId,
aggregation: p,
groupBy: [],
filter,
}));
}
// Dot-notation spec ids (`latency.p95`) nest under their group prefix.
export function mergeIntoEvidence(results) {
const out = {};
for (const r of results) {
const id = r?.spec?.id;
if (!id) continue;
const dot = id.indexOf('.');
if (dot > -1) {
const head = id.slice(0, dot);
const leaf = id.slice(dot + 1);
if (!out[head]) out[head] = {};
out[head][leaf] = simplify(r);
} else {
out[id] = simplify(r);
}
}
return out;
}
// Avoid leaking raw CLI payload / candidate+spec wrapper into evidence — keep summary-only.
function simplify(r) {
if (!r || r.ok === false) return { error: r?.error ?? 'unknown' };
// Check rows before value so tabular results with both stay tabular.
if (Array.isArray(r.rows)) return r.rows;
if ('value' in r) return r.value;
return null;
}
lib/display-labels.mjs
import { canonicalizeRoute } from './route-normalize.mjs';
const KIND_LABELS = new Map([
['slow_route', 'Slow route'],
['uncached_route', 'Low cache-hit route'],
['cold_start', 'Cold starts'],
['route_errors', 'Route errors'],
['cache_header_gap', 'Missing cache headers'],
['image_optimization', 'Image optimization'],
['external_api_slow', 'Slow external API'],
['isr_overrevalidation', 'ISR over-revalidation'],
['middleware_heavy', 'Heavy middleware'],
['cwv_poor', 'Poor Core Web Vitals'],
['platform_fluid_compute', 'Fluid Compute usage'],
['platform_bot_protection', 'Bot traffic'],
['rendering_candidate', 'Rendering opportunity'],
['missing_cache_headers', 'Missing cache headers'],
['max_age_without_s_maxage', 'Browser-only cache header'],
['force_dynamic', 'Forced dynamic rendering'],
['headers_in_page', 'Dynamic API in page'],
['unoptimized_image', 'Image optimization gap'],
['large_static_asset', 'Large static asset'],
['source_maps_production', 'Production source maps'],
['edge_heavy_import', 'Heavy Edge import'],
]);
const SIGNAL_LABELS = new Map([
['inv', 'function invocations'],
['runs', 'function invocations'],
['middleware_inv', 'middleware invocations'],
['total_req', 'total requests'],
['requests', 'requests'],
['p95', '95th percentile duration'],
['p75', '75th percentile duration'],
['5xx', '5xx error rate'],
['errs', '5xx errors'],
['rate', '5xx error rate'],
['cache', 'cache hit rate'],
['get', 'GET request share'],
['cold', 'cold start rate'],
['writes', 'ISR write units'],
['reads', 'ISR read units'],
['w/r', 'ISR writes per read'],
['ratio', 'ratio'],
['host', 'host'],
['calls', 'external API calls'],
['edge_cost', 'Edge Request cost units'],
['bot_protection', 'Bot Protection'],
['bot_fdt_pct', 'bot Fast Data Transfer share'],
['LCP', 'Largest Contentful Paint (LCP)'],
['INP', 'Interaction to Next Paint (INP)'],
['CLS', 'Cumulative Layout Shift (CLS)'],
]);
const REQUEST_COUNT_KINDS = new Set([
'uncached_route',
]);
const PUBLIC_ASSIGNMENT_LABELS = new Map([
...SIGNAL_LABELS,
['deepDive.latency.p95', 'deepDive latency p95'],
['deepDive.cpu.p95', 'deepDive CPU p95'],
['deepDive.ttfb.p95', 'deepDive TTFB p95'],
['cpu.p95', 'CPU p95'],
['latency.p95', 'latency p95'],
['ttfb.p95', 'TTFB p95'],
['cache_result', 'cache result'],
['http_status', 'HTTP status'],
['error_code', 'error code'],
['status', 'status'],
['count', 'count'],
]);
export function formatKind(kind) {
if (!kind) return 'Candidate';
if (KIND_LABELS.has(kind)) return KIND_LABELS.get(kind);
return String(kind)
.split(/[_-]+/g)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ') || 'Candidate';
}
export function formatRoute(candidate) {
const route = candidate?.displayRoute ?? candidate?.route ?? candidate?.hostname ?? null;
if (route) return String(canonicalizeRoute(route));
if (Array.isArray(candidate?.files) && candidate.files.length > 0) return candidate.files[0];
return 'account-wide';
}
export function formatSignal(signal, context = {}) {
if (typeof signal !== 'string' || signal.trim() === '') return 'no signal recorded';
const parts = signal
.split(',')
.map((part) => part.trim())
.filter(Boolean)
.map((part) => formatSignalPart(part, context));
return parts.length > 0 ? parts.join('; ') : signal;
}
export function formatPublicText(value) {
if (value == null) return '';
return normalizeObservedWindowUnits(String(value))
.replace(/\bo11y\b/gi, 'observability')
.replace(/\bcache[- ]components gotcha\b/gi, 'Cache Components edge case')
.replace(/\bcache_result\b(?!\s*=)/g, 'cache result')
.replace(/\bhttp_status\b(?!\s*=)/g, 'HTTP status')
.replace(/\berror_code\b(?!\s*=)/g, 'error code')
.replace(/,(?=\s*([A-Za-z0-9][\w./-]*)=)/g, (match, key) =>
PUBLIC_ASSIGNMENT_LABELS.has(key) ? '; ' : match
)
.replace(/\b([A-Za-z0-9][\w./-]*)=([^,;\s]+)/g, (match, key, rawValue) => {
const label = PUBLIC_ASSIGNMENT_LABELS.get(key);
if (!label) return match;
return `${label}: ${formatSignalValue(key, rawValue)}`;
})
.replace(/\b(cache breakdown[^.!?\n;]{0,160}?)\b(?:function\s+)?invocations\b/gi, (match, prefix) =>
/\bstatus distribution\b/i.test(prefix) ? match : `${prefix}requests`
)
.replace(/\b(cache breakdown[^.!?\n;]{0,220}?\bout of\s+[\d,]+)\s+invocations\b/gi, '$1 requests')
.replace(/\b(cache hits over\s+[\d,.]+(?:\s?(?:K|M|B))?)\s+invocations\b/gi, '$1 requests')
.replace(/\b(?:function\s+)?invocations\b([^.!?\n;]{0,120}\b(?:empty\s+)?cache result(?: label)?\b)/gi, 'requests$1');
}
export function normalizeObservedWindowUnits(value) {
if (value == null) return '';
return String(value)
.replace(/(?<!\$)\b(\d[\d,.]*(?:\s?(?:K|M|B|KB|MB|GB|TB))?)\/mo\b/gi, '$1/window')
.replace(/\bmonthly\s+function\s+invocations\b/gi, 'function invocations/window')
.replace(/\b(requests?|invocations?|GETs|bytes|egress|bandwidth|writes?|reads?|errors?)\/mo\b/gi, '$1/window')
.replace(/\bmonthly\s+(requests?|invocations?|GETs|bytes|egress|bandwidth|writes?|reads?|errors?)\b/gi, '$1/window')
.replace(/\b(\d[\d,.]*(?:\s?(?:K|M|B|KB|MB|GB|TB))?)\s+function\s+invocations\s+per month\b/gi, '$1 function invocations/window')
.replace(/\b(\d[\d,.]*(?:\s?(?:K|M|B|KB|MB|GB|TB))?)\s+(requests?|GETs|invocations?|bytes|writes?|reads?|errors?)\s+per month\b/gi, '$1 $2/window')
.replace(/\b(\d[\d,.]*(?:\s?(?:K|M|B|KB|MB|GB|TB))?)\/window\s+(requests?|GETs|(?:function\s+)?invocations?|bytes|egress|bandwidth|writes?|reads?|errors?)\b/gi, '$1 $2 in this window')
.replace(/\b(\d[\d,.]*(?:\s?(?:K|M|B|KB|MB|GB|TB))?)\s+(requests?|GETs|(?:function\s+)?invocations?|bytes|egress|bandwidth|writes?|reads?|errors?)\/window\b/gi, '$1 $2 in this window')
.replace(/\b(\d[\d,.]*(?:\s?(?:K|M|B|KB|MB|GB|TB))?)\/window\b/gi, '$1 in this window')
.replace(/\b(requests?|invocations?|GETs|bytes|egress|bandwidth|writes?|reads?|errors?)\/window\b/gi, '$1 in this window');
}
export function formatCandidateLine(candidate) {
return `${formatKind(candidate?.kind)} on ${formatRoute(candidate)} - ${formatSignal(candidate?.o11ySignal, candidate)}`;
}
export function formatCandidateLabel(candidate) {
return `${formatKind(candidate?.kind)} on ${formatRoute(candidate)}`;
}
function formatSignalPart(part, context = {}) {
const eq = part.indexOf('=');
if (eq === -1) return part;
const key = part.slice(0, eq).trim();
const value = part.slice(eq + 1).trim();
const label = signalLabel(key, context);
return `${label}: ${formatSignalValue(key, value)}`;
}
function signalLabel(key, context = {}) {
const kind = typeof context === 'string' ? context : context?.kind;
if (key === 'inv' && REQUEST_COUNT_KINDS.has(kind)) return 'requests';
return SIGNAL_LABELS.get(key) ?? humanizeKey(key);
}
function humanizeKey(key) {
return String(key)
.replaceAll('.', ' ')
.replaceAll('_', ' ')
.replaceAll('-', ' ')
.trim();
}
function formatSignalValue(key, value) {
if (key === 'inv' || key === 'runs' || key === 'middleware_inv' || key === 'total_req' || key === 'requests' || key === 'calls' || key === 'errs' || key === 'writes' || key === 'reads') {
return formatNumberLike(value);
}
return value;
}
function formatNumberLike(value) {
const n = Number(value);
if (!Number.isFinite(n)) return value;
return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(n);
}
lib/extract-claims.mjs
// Extract mechanically-verifiable claims from a rec without parsing LLM prose. High precision, not recall.
export function extractClaims(rec, ctx = {}) {
const claims = [];
const repoRoot = ctx.repoRoot;
const projectRootDirectory = normalizeProjectRootDirectory(ctx.projectRootDirectory);
const framework = ctx.framework;
const frameworkVersion = ctx.version;
const cacheComponents = ctx.cacheComponents;
const signals = ctx.signals;
const projectFacts = Array.isArray(ctx.projectFacts) ? ctx.projectFacts : [];
// One synthetic claim asserts rec doesn't contradict any already-on project fact (Fluid, in-function concurrency, …).
if (projectFacts.length > 0) {
claims.push({
type: 'does_not_contradict_project_config',
rec,
projectFacts,
sourceField: 'projectFacts',
});
}
for (const cite of asArray(rec.citations)) {
// Skill-rule refs are filtered upstream — skip version check.
if (/^[\w-]+:[\w-]+$/.test(cite)) {
claims.push({ type: 'citation_in_library', url: cite, sourceField: 'citations' });
continue;
}
claims.push({ type: 'citation_in_library', url: cite, sourceField: 'citations' });
if (framework && frameworkVersion) {
claims.push({
type: 'citation_applies_to_version',
url: cite,
framework,
frameworkVersion,
sourceField: 'citations',
});
}
}
for (const f of asArray(rec.affectedFiles)) {
claims.push({ type: 'file_exists', file: f, repoRoot, projectRootDirectory, sourceField: 'affectedFiles' });
}
// findingRefs lack a pattern, so we only check file existence.
for (const ref of asArray(rec.findingRefs)) {
const m = String(ref).match(/^(.+?):\d+$/);
if (m && !claims.some((c) => c.type === 'file_exists' && c.file === m[1])) {
claims.push({ type: 'file_exists', file: m[1], repoRoot, projectRootDirectory, sourceField: 'findingRefs' });
}
}
const cacheFiles = cacheRecommendationFiles(rec);
if (isCacheCandidate(rec)) {
claims.push({
type: 'cache_policy_positive_or_no_ready_rec',
rec,
sourceField: 'cache-policy',
});
}
if (cacheFiles.length > 0) {
claims.push({
type: 'cache_vary_matches_dynamic_inputs',
rec,
files: cacheFiles,
repoRoot,
projectRootDirectory,
sourceField: 'cache-safety',
});
if (mentionsVaryHeader(rec)) {
claims.push({
type: 'cache_vary_cardinality_safe',
rec,
sourceField: 'cache-vary-cardinality',
});
}
claims.push({
type: 'cache_rec_not_error_dominated_or_acknowledged',
rec,
signals,
sourceField: 'cache-error-safety',
});
claims.push({
type: 'cache_control_header_syntax',
rec,
sourceField: 'cache-header-syntax',
});
claims.push({
type: 'cache_control_headers_citation',
rec,
sourceField: 'cache-header-citation',
});
if (mentionsCachedNotFoundOr404(rec)) {
claims.push({
type: 'cache_404_long_ttl_safety',
rec,
sourceField: 'cache-404-safety',
});
}
}
if (mentionsNextCachedNotFound(rec)) {
claims.push({
type: 'next_cached_not_found_causal_support',
rec,
framework,
frameworkVersion,
sourceField: 'next-cache-not-found',
});
}
if (mentionsNextStableCacheApi(rec)) {
claims.push({
type: 'next_stable_cache_api_for_version',
rec,
framework,
frameworkVersion,
sourceField: 'next-cache-api-version',
});
}
if (mentionsNext16RuntimeCacheApiMismatch(rec)) {
claims.push({
type: 'next_runtime_cache_api_for_version',
rec,
framework,
frameworkVersion,
sourceField: 'next-runtime-cache-api-version',
});
}
if (mentionsRuntimeCacheWhenCacheComponents(rec)) {
claims.push({
type: 'next_cache_components_runtime_cache_preference',
rec,
framework,
frameworkVersion,
cacheComponents,
sourceField: 'next-cache-components-runtime-cache-preference',
});
}
if (mentionsMultipleCacheLifeCalls(rec)) {
claims.push({
type: 'next_cache_life_single_execution',
rec,
framework,
frameworkVersion,
sourceField: 'next-cache-life-single-execution',
});
}
if (mentionsCacheLifetimeChange(rec)) {
claims.push({
type: 'next_cache_lifetime_freshness_supported',
rec,
files: recommendationFiles(rec),
repoRoot,
projectRootDirectory,
sourceField: 'next-cache-lifetime-freshness',
});
}
if (mentionsNextCacheComponentsStaticShellTarget(rec)) {
claims.push({
type: 'next_cache_components_route_chain_file',
rec,
framework,
frameworkVersion,
cacheComponents,
signals,
sourceField: 'next-cache-components-route-chain',
});
}
if (mentionsCacheLifeCdnHeaderClaim(rec)) {
claims.push({
type: 'next_cache_life_cdn_header_semantics',
rec,
framework,
frameworkVersion,
sourceField: 'next-cache-life-cdn-header-semantics',
});
}
if (mentionsImageResponseHeaders(rec)) {
claims.push({
type: 'image_response_headers_citation',
rec,
framework,
frameworkVersion,
sourceField: 'image-response-headers',
});
}
if (mentionsNextImagePriorityRecommendation(rec)) {
claims.push({
type: 'next_image_priority_api_for_version',
rec,
framework,
frameworkVersion,
sourceField: 'next-image-priority-api',
});
}
if (mentionsNextCacheComponentsRouteSegmentConfig(rec)) {
claims.push({
type: 'next_cache_components_route_segment_config',
rec,
framework,
frameworkVersion,
cacheComponents,
sourceField: 'next-route-segment-config',
});
}
if (mentionsRouteLevelRevalidate(rec)) {
claims.push({
type: 'next_route_revalidate_static_prereq',
rec,
framework,
frameworkVersion,
cacheComponents,
repoRoot,
projectRootDirectory,
sourceField: 'next-route-revalidate-static-prereq',
});
}
if (mentionsExistingCacheTagInvalidation(rec)) {
claims.push({
type: 'next_cache_tag_invalidation_supported',
rec,
repoRoot,
projectRootDirectory,
sourceField: 'next-cache-tag-invalidation',
});
}
if (mentionsUnsafeImmutableDynamicRoute(rec)) {
claims.push({
type: 'immutable_dynamic_route_safety',
rec,
sourceField: 'immutable-dynamic-route',
});
}
if (mentionsAuthSensitiveParallelization(rec)) {
claims.push({
type: 'auth_guard_parallelization_safety',
rec,
sourceField: 'auth-parallelization',
});
}
if (mentionsParallelizationImpactOverclaim(rec)) {
claims.push({
type: 'parallelization_impact_not_overclaimed',
rec,
sourceField: 'parallelization-impact',
});
}
if (mentionsCpuBoundParallelization(rec)) {
claims.push({
type: 'parallelization_not_cpu_bound_work',
rec,
sourceField: 'parallelization-cpu-bound',
});
}
if (mentionsRuntimeErrorCause(rec)) {
claims.push({
type: 'runtime_error_cause_supported',
rec,
sourceField: 'runtime-error-cause',
});
}
if (mentionsCatchToNotFound(rec)) {
claims.push({
type: 'route_error_not_found_status_and_scope',
rec,
sourceField: 'route-error-catch-safety',
});
}
if (mentionsIgnoredBuildStepRecommendation(rec)) {
claims.push({
type: 'vercel_ignore_command_project_state',
rec,
signals,
sourceField: 'ignored-build-step-state',
});
}
if (mentionsTurboBuildCacheRecommendation(rec)) {
claims.push({
type: 'turbo_build_cache_safety',
rec,
files: recommendationFiles(rec),
repoRoot,
projectRootDirectory,
framework,
sourceField: 'turbo-build-cache-safety',
});
}
for (const c of asArray(rec.verifiableClaims)) {
if (c && typeof c === 'object' && typeof c.type === 'string') {
claims.push({
...c,
repoRoot: c.repoRoot ?? repoRoot,
projectRootDirectory: c.projectRootDirectory ?? projectRootDirectory,
sourceField: 'verifiableClaims',
});
}
}
return claims;
}
function normalizeProjectRootDirectory(value) {
if (typeof value !== 'string' || value.trim() === '') return null;
return value.replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
}
function cacheRecommendationFiles(rec) {
if (!recommendsSharedCache(rec)) return [];
return recommendationFiles(rec);
}
function isCacheCandidate(rec) {
return /^(?:uncached_route|cache_header_gap):/.test(String(rec?.candidateRef ?? ''));
}
function recommendationFiles(rec) {
const files = [
...asArray(rec.affectedFiles),
...asArray(rec.findingRefs)
.map((ref) => String(ref).match(/^(.+?):\d+$/)?.[1])
.filter(Boolean),
];
return Array.from(new Set(files));
}
function recommendsSharedCache(rec) {
const haystack = [
rec?.what,
rec?.why,
rec?.fix,
rec?.desiredBehavior,
rec?.verify,
].filter(Boolean).join('\n');
return /\b(?:s-maxage|CDN-Cache-Control|Vercel-CDN-Cache-Control|Cache-Control)\b/i.test(haystack);
}
function mentionsVaryHeader(rec) {
return /\bVary\b/i.test(recText(rec));
}
function mentionsNextCachedNotFound(rec) {
const haystack = recText(rec);
return /\bnotFound\b/.test(haystack) &&
/['"`]use cache['"`]|\buse cache\b/i.test(haystack) &&
/\b(?:500|5xx|error rate|errors?)\b/i.test(haystack);
}
function mentionsNextStableCacheApi(rec) {
const haystack = recText(rec);
return /\bunstable_(?:cacheLife|cacheTag)\b/.test(haystack) ||
/\brevalidateTag\s*\([^)]*['"`][^'"`]+['"`]\s*\)/.test(haystack);
}
function mentionsNext16RuntimeCacheApiMismatch(rec) {
const haystack = recText(rec);
const citations = asArray(rec?.citations).join('\n');
return /\bunstable_cache\b/.test(haystack) &&
(/\bRuntime Cache\b/i.test(haystack) || /vercel\.com\/docs\/caching\/runtime-cache/i.test(citations));
}
function mentionsRuntimeCacheWhenCacheComponents(rec) {
const haystack = recText(rec);
const citations = asArray(rec?.citations).join('\n');
return /\b(?:Runtime Cache|@vercel\/functions|getCache\s*\(|setCache\s*\()\b/i.test(haystack) ||
/vercel\.com\/docs\/caching\/runtime-cache/i.test(citations);
}
function mentionsMultipleCacheLifeCalls(rec) {
const haystack = recText(rec);
const matches = haystack.match(/\bcacheLife\s*\(/g) ?? [];
return matches.length > 1;
}
function mentionsCacheLifetimeChange(rec) {
return /\bcacheLife\s*\(/.test(recText(rec));
}
function mentionsCacheLifeCdnHeaderClaim(rec) {
const haystack = recText(rec);
if (!/\bcacheLife\b/.test(haystack)) return false;
return /\bcacheLife\b[^.\n]{0,240}\b(?:Cache-Control|s-maxage|CDN|edge cache|cache breakdown|x-vercel-cache|HIT|MISS|function (?:still )?runs per request|every request invokes the function)\b/i.test(haystack) ||
/\b(?:Cache-Control|s-maxage|CDN|edge cache|cache breakdown|x-vercel-cache|HIT|MISS|function (?:still )?runs per request|every request invokes the function)\b[^.\n]{0,240}\bcacheLife\b/i.test(haystack) ||
/\b(?:no|never|without|missing)\s+cacheLife\b[^.\n]{0,240}\b(?:no|not|never|0%|every|per request|function)\b[^.\n]{0,120}\b(?:cache|cached|hit|runs?|invoke)/i.test(haystack);
}
function mentionsNextCacheComponentsStaticShellTarget(rec) {
const haystack = recText(rec);
if (!/\b(?:cacheComponents|Cache Components|cacheLife|cacheTag|['"`]use cache['"`]|use cache|static shell|pre[- ]?render|prerender)\b/i.test(haystack)) {
return false;
}
const files = [
...asArray(rec?.affectedFiles),
...asArray(rec?.findingRefs).map((ref) => String(ref).match(/^(.+?):\d+$/)?.[1]).filter(Boolean),
];
return files.some((file) => /(^|\/)layout\.(?:tsx?|jsx?)$/.test(String(file)));
}
function mentionsImageResponseHeaders(rec) {
const haystack = recText(rec);
return /\bImageResponse\b/.test(haystack) &&
/\bheaders?\b[\s\S]{0,200}\b(?:Cache-Control|s-maxage|CDN|response)\b|\b(?:Cache-Control|s-maxage|CDN)\b[\s\S]{0,200}\bheaders?\b/i.test(haystack);
}
function mentionsNextImagePriorityRecommendation(rec) {
const haystack = recText(rec);
if (!/\b(?:next\/image|<Image\b|Image component|image)\b/i.test(haystack)) return false;
if (!/\bpriority\b/i.test(haystack)) return false;
if (/\b(?:deprecated|replace|remove|avoid)\b[^.\n]{0,120}\bpriority\b/i.test(haystack) ||
/\bpriority\b[^.\n]{0,120}\b(?:deprecated|replace|remove|avoid)\b/i.test(haystack)) {
return false;
}
return /\b(?:set|add|use|enable|mark|make|turn on|with)\b[^.\n]{0,120}\bpriority\b/i.test(haystack) ||
/<Image\b[^>]*\bpriority(?:\s|=|>)/i.test(haystack);
}
function mentionsNextCacheComponentsRouteSegmentConfig(rec) {
const haystack = recText(rec);
return /\b(?:export\s+const\s+)?(?:dynamicParams|fetchCache)\s*=/.test(haystack) ||
/\bexport\s+const\s+(?:dynamic|revalidate)\b/.test(haystack) ||
/\b(?:set|add|configure|use)\s+[^.\n]{0,80}\b(?:dynamicParams|fetchCache)\b/i.test(haystack) ||
/\broute segment config options?\b[^.\n]{0,120}\b(?:Route Handlers?|handlers?)\b[^.\n]{0,120}\b(?:no longer apply|do not apply|removed)\b/i.test(haystack) ||
/\b(?:revalidate|dynamic|fetchCache)\b[^.\n]{0,80}\broute segment (?:config|export)\b/i.test(haystack);
}
function mentionsRouteLevelRevalidate(rec) {
const haystack = recText(rec);
return /\bexport\s+const\s+revalidate\b/.test(haystack) ||
/\broute[- ]level\s+revalidate\b/i.test(haystack) ||
/\brevalidate\s*(?:=|:)\s*\d+\b[^.\n]{0,120}\b(?:page|layout|route segment|segment export)\b/i.test(haystack);
}
function mentionsExistingCacheTagInvalidation(rec) {
const haystack = recText(rec);
if (!/\bcacheTag\s*\(/.test(haystack)) return false;
if (!/\b(?:revalidateTag|updateTag|invalidate|invalidation|revalidation|webhook|CMS|content-sync|content sync|publish|deploy)\b/i.test(haystack)) {
return false;
}
return /\b(?:existing|current|already|keep|keeps|preserve|preserves|continue|continues|maintain|maintains|via)\b[\s\S]{0,180}\b(?:revalidateTag|updateTag|invalidate|invalidation|revalidation|event-driven|webhook|CMS|content-sync|content sync|publish|deploy|tags?)\b/i.test(haystack) ||
/\b(?:invalidation|revalidation)\s+is\s+already\b/i.test(haystack) ||
/\balready\s+event-driven\b/i.test(haystack);
}
function mentionsUnsafeImmutableDynamicRoute(rec) {
const haystack = recText(rec);
if (!/\bimmutable\b/i.test(haystack)) return false;
const files = [
...asArray(rec?.affectedFiles),
...asArray(rec?.findingRefs).map((ref) => String(ref).match(/^(.+?):\d+$/)?.[1]).filter(Boolean),
];
const routeHandler = files.some((file) => /(?:^|\/)route\.[cm]?[jt]sx?$/.test(String(file)));
const apiRoute = /^cache_header_gap:\/api\//.test(String(rec?.candidateRef ?? ''));
return routeHandler || apiRoute;
}
function mentionsAuthSensitiveParallelization(rec) {
const haystack = recText(rec);
if (!/\b(?:parallelize|Promise\.all|run concurrently|start .* early)\b/i.test(haystack)) return false;
if (!/\b(?:auth|authorize|authorization|ownership|owns|owner|private|session|permission|access)\b/i.test(haystack)) return false;
return /\b(?:private|secret|token|registrant|account|user|ticket|payment|session)\w*\b/i.test(haystack);
}
function mentionsParallelizationImpactOverclaim(rec) {
const haystack = recText(rec);
if (!/\b(?:parallelize|Promise\.all|run concurrently|start .* early)\b/i.test(haystack)) return false;
return /\b(?:drop|drops|reduce|reduces|reduction|save|saves|shave|shaves)\b[^.\n]{0,200}\b(?:roughly|approximately|about|around|equal\s+to)?\s*(?:the\s+)?(?:duration\s+of\s+[A-Za-z_$][\w$]*\s*\(\s*\)|min\s*\([^)]*duration[^)]*\)|one\s+[\w-]+\s+round[- ]trip|one\s+await|one\s+network\s+call|one\s+database\s+query)/i.test(haystack);
}
function mentionsCpuBoundParallelization(rec) {
const haystack = recText(rec);
if (!/\b(?:parallelize|Promise\.all|run concurrently|start .* early)\b/i.test(haystack)) return false;
return /\b(?:cpu\.p95|CPU p95|cpu p95|CPU-bound|compute-bound|in-process compute|compileMDX|MDX compilation|compilation|render compute)\b/i.test(haystack);
}
function mentionsCachedNotFoundOr404(rec) {
const haystack = recText(rec);
if (!/\b(?:s-maxage|CDN-Cache-Control|Vercel-CDN-Cache-Control|Cache-Control)\b/i.test(haystack)) return false;
return /\b(?:404|not[- ]found|notFound|not found branch|not-found branch)\b/i.test(haystack);
}
function mentionsRuntimeErrorCause(rec) {
if (!/^route_errors:/.test(String(rec?.candidateRef ?? ''))) return false;
const haystack = recText(rec);
return /\b(?:ENOENT|ETIMEDOUT|ECONNRESET|outputFileTracing|missing\s+(?:file|mdx|module)|no\s+(?:matching|corresponding)\s+(?:file|mdx|post)|does\s+not\s+exist|signature\s+of|root cause|caused by|unhandled\s+exceptions?|uncaught(?:-exception)?|throws?|bubbles?\s+to\s+the\s+runtime|reads?\s+[^.]{0,80}(?:filePath|filesystem|file system|disk)|readFile)\b/i.test(haystack);
}
function mentionsCatchToNotFound(rec) {
if (!/^route_errors:/.test(String(rec?.candidateRef ?? ''))) return false;
const haystack = recText(rec);
return /\bcatch\b/i.test(haystack) &&
/\b(?:404|not[- ]found|not found|notFound)\b/i.test(haystack);
}
function mentionsIgnoredBuildStepRecommendation(rec) {
const haystack = recText(rec);
return /\b(?:Ignored Build Step|ignoreCommand|turbo-ignore|skip unaffected|unaffected projects?)\b/i.test(haystack) &&
/\b(?:add|set|configure|enable|use|introduce|wire|adopt|turn on)\b[^.\n]{0,180}\b(?:Ignored Build Step|ignoreCommand|turbo-ignore|skip unaffected|unaffected projects?)\b/i.test(haystack);
}
function mentionsTurboBuildCacheRecommendation(rec) {
const haystack = recText(rec);
if (!/\b(?:Turbo|Turborepo|turbo\.json|tasks\.build|build cache|build caching)\b/i.test(haystack)) return false;
return /\b(?:enable|re-enable|restore|turn on|set|remove)\b[^.\n]{0,220}\b(?:cache\s*:\s*false|tasks\.build\.cache|build cache|build caching|Turbo cache|Turborepo cache)\b/i.test(haystack) ||
/\b(?:cache\s*:\s*false|tasks\.build\.cache|build cache|build caching|Turbo cache|Turborepo cache)\b[^.\n]{0,220}\b(?:enable|re-enable|restore|turn on|set|remove)\b/i.test(haystack);
}
function recText(rec) {
return [
rec?.what,
rec?.why,
rec?.fix,
rec?.currentBehavior,
rec?.desiredBehavior,
rec?.verify,
].filter(Boolean).join('\n');
}
function asArray(v) {
return Array.isArray(v) ? v : [];
}
export function summarizeClaimResults(results) {
const counts = { verified: 0, failed: 0, unsupported: 0, unverifiable: 0 };
for (const r of results) {
if (r?.disposition && counts[r.disposition] !== undefined) counts[r.disposition]++;
}
const verifiable = counts.verified + counts.failed;
const passRate = verifiable > 0 ? counts.verified / verifiable : 1;
return { ...counts, verifiable, passRate, total: results.length };
}
lib/framework-support.mjs
export const CORE_SUPPORTED_FRAMEWORKS = ['next', 'sveltekit', 'nuxt'];
export const LIMITED_FRAMEWORKS = ['astro'];
const LABELS = {
next: 'Next.js',
sveltekit: 'SvelteKit',
nuxt: 'Nuxt',
astro: 'Astro',
hono: 'Hono',
remix: 'Remix',
unknown: 'unknown framework',
};
export function frameworkLabel(framework) {
return LABELS[normalizeFramework(framework)] ?? String(framework ?? 'unknown');
}
export function classifyFrameworkSupport(stack = {}) {
const framework = normalizeFramework(stack.framework);
const label = frameworkLabel(framework);
const supportedLabels = CORE_SUPPORTED_FRAMEWORKS.map(frameworkLabel);
const limitedLabels = LIMITED_FRAMEWORKS.map(frameworkLabel);
if (CORE_SUPPORTED_FRAMEWORKS.includes(framework)) {
return {
ok: true,
status: 'supported',
blocker: null,
framework,
label,
supportedFrameworks: supportedLabels,
limitedFrameworks: limitedLabels,
detail: `${label} is supported for metric-backed route-to-file investigations.`,
};
}
if (LIMITED_FRAMEWORKS.includes(framework)) {
return {
ok: true,
status: 'limited',
blocker: null,
framework,
label,
supportedFrameworks: supportedLabels,
limitedFrameworks: limitedLabels,
detail: `${label} support is limited. The skill can use Vercel metrics and generic platform checks, but framework-specific route-to-file recommendations may be sparse.`,
};
}
return {
ok: false,
status: 'unsupported',
blocker: 'unsupported_framework',
framework,
label,
supportedFrameworks: supportedLabels,
limitedFrameworks: limitedLabels,
detail: `${label} is not supported for metric-backed route-to-file investigations. Supported frameworks: ${supportedLabels.join(', ')}. Limited support: ${limitedLabels.join(', ')}.`,
};
}
function normalizeFramework(value) {
const raw = String(value ?? 'unknown').trim().toLowerCase();
if (raw === 'nextjs' || raw === 'next.js') return 'next';
if (raw === 'svelte' || raw === 'svelte-kit') return 'sveltekit';
return raw || 'unknown';
}
lib/gates/build-minutes-fanout.mjs
// Build Minutes climb on monorepos when Turborepo cache is bypassed or every project rebuilds on every commit.
// Threshold: Build Minutes line > 15% of total bill OR scanner emits any turbo-force-bypass finding (even at lower share).
// Account-scoped because the lever is project-settings (Ignored Build Step, Elastic Build Machines), not code.
export const metadata = {
id: 'build_minutes_fanout',
threshold: 'Build Minutes share > 0.15 OR turbo-force-bypass finding present',
billingDimension: 'build',
scope: 'account',
sourceCitation: 'vercel-optimize gate threshold',
description:
'Build Minutes line dominates the bill or Turborepo cache is bypassed. On monorepos, unchanged work should be skipped through Vercel skip-unaffected behavior, a verified Ignored Build Step, and a complete Turbo cache contract.',
};
const BUILD_RE = /^Build (CPU )?Minutes$/i;
const SCANNER_PATTERN = 'turbo-force-bypass';
const SHARE_FLOOR = 0.15;
export function gate(signals) {
const services = signals?.usage?.services;
const total = Array.isArray(services)
? services.reduce((acc, s) => acc + Number(s.billedCost ?? s.cost ?? 0), 0)
: 0;
const buildBilled = Array.isArray(services)
? services
.filter((s) => BUILD_RE.test(String(s?.name ?? '')))
.reduce((acc, s) => acc + Number(s.billedCost ?? s.cost ?? 0), 0)
: 0;
const buildShare = total > 0 ? buildBilled / total : 0;
const findings = (signals?.codebase?.findings ?? []).filter((f) => f.pattern === SCANNER_PATTERN);
if (buildShare <= SHARE_FLOOR && findings.length === 0) return [];
const subtypes = unique(findings.map((f) => f.subtype).filter(Boolean));
const sampleFiles = unique(findings.map((f) => f.file).filter(Boolean)).slice(0, 4);
const reason = findings.length > 0
? (buildShare > SHARE_FLOOR
? 'Build Minutes share is high and Turborepo cache bypass detected in repo'
: 'Turborepo cache bypass detected in repo')
: 'Build Minutes line exceeds 15% of total billed cost';
return [{
kind: metadata.id,
scope: 'account',
files: sampleFiles,
priority: findings.length > 0 ? 65 : 50,
confidence: findings.length > 0 ? 0.86 : 0.74,
o11ySignal: `build_minutes_share=${(buildShare * 100).toFixed(0)}% scanner_findings=${findings.length}`,
reason,
question: findings.length > 0
? `Turborepo cache bypass detected (${subtypes.join(', ')}). Which build pipeline forces a rebuild on every commit, and can Ignored Build Step + cache re-enable cut the project fan-out?`
: 'Build Minutes exceed 15% of the bill. Is Ignored Build Step configured? Is Turborepo cache active across builds? Would Elastic Build Machines reduce duration on hot builds?',
evidence: {
metric: 'usage.services',
buildBilled,
totalBilled: total,
buildShare,
scannerFindings: findings.length,
scannerSubtypes: subtypes,
sampleFiles,
},
}];
}
function unique(values) {
return [...new Set(values)];
}
lib/gates/cold-start.mjs
// Signal: `function_start_type` dimension on `vercel.function_invocation.count` (cold|hot|prewarmed).
// Threshold WHY: 40%+ cold is fixable via Fluid keep-warm; 30% is the noise floor for serverless without keep-warm.
// total>=1000/14d (~3/hr) keeps Poisson CI on cold rate at ~±5% near the 40% threshold.
export const metadata = {
id: 'cold_start',
threshold: 'coldPct > 0.4 AND total >= 1000',
billingDimension: 'function-duration',
scope: 'route',
sourceCitation: 'vercel-optimize gate threshold',
description:
'Routes where > 40% of invocations are cold-start, at meaningful traffic (>=1,000 total invocations in window). Cold starts add 200-800ms per request and break the perceived latency budget on cache-miss paths. The 40% threshold is where cold-rate becomes a real signal vs Poisson noise on serverless. Sourced from vercel.function_invocation.count grouped by function_start_type.',
};
export function gate(signals) {
const cs = extractColdStarts(signals);
return cs
.filter((r) => r.coldPct > 0.4 && r.total >= 1000)
.map((r) => ({
kind: metadata.id,
scope: 'route',
route: r.route,
files: [],
priority: Math.round(r.total * r.coldPct),
confidence: 0.92,
o11ySignal: `cold=${(r.coldPct * 100).toFixed(0)}%,inv=${r.total}`,
reason: 'high cold-start rate on hot route',
question: `What initialization or bundle overhead makes ${r.route} cold-start ${(r.coldPct * 100).toFixed(0)}% of ${r.total} invocations?`,
evidence: { metric: 'fnStartTypeByRoute', route: r.route, coldPct: r.coldPct, total: r.total, coldCount: r.coldCount ?? null },
}));
}
function extractColdStarts(signals) {
const live = signals.metrics?.fnStartTypeByRoute;
if (Array.isArray(live?.rows) && live.rows.some((r) => 'coldCount' in r || 'coldPct' in r)) {
return live.rows
.filter((r) => r.route)
.map((r) => ({
route: r.route,
total: r.total ?? 0,
coldCount: r.coldCount ?? 0,
coldPct: r.coldPct ?? 0,
}));
}
// Legacy fixture: pre-derived coldStartByRoute rows.
const direct = signals.metrics?.coldStartByRoute;
if (Array.isArray(direct?.rows)) {
return direct.rows
.filter((r) => r.route)
.map((r) => ({ route: r.route, coldPct: r.coldPct ?? 0, total: r.total ?? 0 }));
}
// Older legacy fixture: series + summary shape.
const legacy = signals.metrics?.coldStarts;
if (Array.isArray(legacy?.series)) {
return legacy.series
.map((s) => {
const total = s.summary?.count ?? 0;
const coldCount = s.summary?.coldCount ?? s.summary?.sum ?? 0;
return { route: s.groupValues?.route, total, coldPct: total > 0 ? coldCount / total : 0 };
})
.filter((r) => r.route);
}
return [];
}
lib/gates/contract.mjs
const VALID_SCOPES = new Set(['route', 'file', 'account']);
export class CandidateContractError extends Error {
constructor(errors) {
super(`gate candidate contract failed:\n${errors.map((e) => `- ${e}`).join('\n')}`);
this.name = 'CandidateContractError';
this.errors = errors;
}
}
export function validateCandidates(candidates, ctx = {}) {
if (!Array.isArray(candidates)) {
throw new CandidateContractError([`${ctx.source ?? 'gate'}: expected candidate array`]);
}
const errors = [];
for (let i = 0; i < candidates.length; i++) {
errors.push(...validateCandidate(candidates[i], { ...ctx, index: i }).errors);
}
if (errors.length > 0) throw new CandidateContractError(errors);
return candidates;
}
export function validateCandidate(candidate, ctx = {}) {
const label = candidateLabel(candidate, ctx);
const errors = [];
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
return { ok: false, errors: [`${label}: candidate must be an object`] };
}
if (!nonEmptyString(candidate.kind)) errors.push(`${label}: kind must be a non-empty string`);
if (!VALID_SCOPES.has(candidate.scope)) {
errors.push(`${label}: scope must be one of route, file, account`);
}
if (!Number.isFinite(candidate.priority)) errors.push(`${label}: priority must be a finite number`);
if (!Number.isFinite(candidate.confidence)) errors.push(`${label}: confidence must be a finite number`);
if (Array.isArray(candidate.files)) {
if (!candidate.files.every((f) => typeof f === 'string' && f.length > 0)) {
errors.push(`${label}: files must contain only non-empty strings`);
}
} else {
errors.push(`${label}: files must be an array`);
}
if (!nonEmptyString(candidate.reason)) errors.push(`${label}: reason must be a non-empty string`);
if (!nonEmptyString(candidate.question)) errors.push(`${label}: question must be a non-empty string`);
if (candidate.scope === 'route') {
const hasRoute = nonEmptyString(candidate.route);
const hasHostname = nonEmptyString(candidate.hostname);
if (!hasRoute && !hasHostname) {
errors.push(`${label}: route-scoped candidates must set route or hostname`);
}
}
if (candidate.scope === 'file') {
if (candidate.route != null || candidate.hostname != null) {
errors.push(`${label}: file-scoped candidates must not set route or hostname`);
}
if (!Array.isArray(candidate.files) || candidate.files.length === 0) {
errors.push(`${label}: file-scoped candidates must include at least one file`);
}
}
if (candidate.scope === 'account') {
if (candidate.route != null || candidate.hostname != null) {
errors.push(`${label}: account-scoped candidates must not set route or hostname`);
}
}
return { ok: errors.length === 0, errors };
}
function candidateLabel(candidate, ctx) {
const source = ctx.source ?? 'gate';
const index = ctx.index == null ? '?' : ctx.index;
const kind = candidate?.kind ?? '?';
return `${source}[${index}] ${kind}`;
}
function nonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
lib/gates/cwv-poor.mjs
// Thresholds are Google's "Poor" band (https://web.dev/articles/vitals): LCP p75 > 2500ms, INP > 200ms, CLS > 0.1.
// When Speed Insights isn't wired up the metrics come back empty and the gate is a no-op.
import { withRouteShapeWarnings } from '../route-normalize.mjs';
export const metadata = {
id: 'cwv_poor',
threshold: 'LCP p75>2500 OR INP p75>200 OR CLS p75>0.1, AND speed_insights count > 50',
billingDimension: 'speed-insights',
scope: 'route',
sourceCitation: 'https://web.dev/articles/vitals',
description:
'Routes where Core Web Vitals fall into Google\'s "Poor" band on real-user traffic. LCP > 2500ms, INP > 200ms, or CLS > 0.1 each hurt SEO and conversion. Surfaces one candidate per (route, metric) pair to keep recommendations focused.',
};
// Below this floor p75 is too noisy to act on.
const MIN_PER_ROUTE_SAMPLES = 50;
export function gate(signals) {
const totalSamples = sumRows(signals.metrics?.cwvCount?.rows);
if (totalSamples === 0) return [];
const countByRoute = byRoute(signals.metrics?.cwvCountByRoute?.rows);
const lcpBy = byRoute(signals.metrics?.cwvLcpByRoute?.rows);
const inpBy = byRoute(signals.metrics?.cwvInpByRoute?.rows);
const clsBy = byRoute(signals.metrics?.cwvClsByRoute?.rows);
const routes = new Set([...lcpBy.keys(), ...inpBy.keys(), ...clsBy.keys()]);
const out = [];
for (const route of routes) {
const routeSamples = countByRoute.get(route) ?? 0;
if (routeSamples < MIN_PER_ROUTE_SAMPLES) continue;
const lcp = lcpBy.get(route);
const inp = inpBy.get(route);
const cls = clsBy.get(route);
const issues = [];
if (lcp != null && lcp > 2500) issues.push({ metric: 'LCP', value: Math.round(lcp), threshold: 2500, unit: 'ms' });
if (inp != null && inp > 200) issues.push({ metric: 'INP', value: Math.round(inp), threshold: 200, unit: 'ms' });
if (cls != null && cls > 0.1) issues.push({ metric: 'CLS', value: round2(cls), threshold: 0.1, unit: '' });
if (issues.length === 0) continue;
const summary = issues.map((i) => `${i.metric}=${i.value}${i.unit}`).join(',');
out.push(withRouteShapeWarnings({
kind: metadata.id,
scope: 'route',
route,
files: [],
priority: issues.reduce((s, i) => s + ratioOverThreshold(i), 0) * 10,
confidence: 0.82,
o11ySignal: summary,
reason: 'real-user Core Web Vitals in poor band',
question: `On ${route}, ${summary}. Which client-side work (bundle weight, blocking scripts, layout shifts, hydration) is responsible, and which change would land first?`,
evidence: {
metric: 'cwv',
route,
lcpMs: lcp != null ? Math.round(lcp) : null,
inpMs: inp != null ? Math.round(inp) : null,
cls: cls != null ? round2(cls) : null,
issues,
totalSpeedInsightsSamples: totalSamples,
routeSpeedInsightsSamples: routeSamples,
},
}, signals));
}
return out;
}
function byRoute(rows) {
const m = new Map();
for (const r of rows ?? []) {
if (!r.route || r.value == null) continue;
m.set(r.route, r.value);
}
return m;
}
function sumRows(rows) {
if (!Array.isArray(rows)) return 0;
return rows.reduce((s, r) => s + (r.value ?? 0), 0);
}
function round2(n) {
return Math.round(n * 100) / 100;
}
function ratioOverThreshold(i) {
return i.value / (i.threshold || 1);
}
lib/gates/external-api-slow.mjs
// Volume floor pairs p75 with call_count so a single 5s cron/day doesn't fire the gate.
const MIN_CALL_COUNT = 500;
export const metadata = {
id: 'external_api_slow',
threshold: `p75Ms > 2000 AND callCount >= ${MIN_CALL_COUNT}`,
billingDimension: 'function-duration',
scope: 'route',
sourceCitation: 'vercel-optimize gate threshold',
description:
'External API hostnames with p75 latency above 2 seconds AND at least 500 calls in the window. External API latency is a primary driver of function duration cost when the upstream is on a hot path; a single slow stale call isn\'t worth recommending against.',
};
export function gate(signals) {
const apis = extractExternalApis(signals);
const calls = extractCallCounts(signals);
return apis
.map((a) => ({ ...a, callCount: calls.get(a.hostname) ?? 0 }))
.filter((a) => a.p75Ms > 2000 && a.callCount >= MIN_CALL_COUNT)
.map((a) => ({
kind: metadata.id,
scope: 'route',
route: null,
files: [],
hostname: a.hostname,
// Weight by latency × call volume so 100k-call/2.1s outranks 1k-call/8s.
priority: Math.round((a.p75Ms * a.callCount) / 1000),
confidence: 0.88,
o11ySignal: `host=${a.hostname},p75=${a.p75Ms}ms,calls=${a.callCount}`,
reason: 'slow external dependency on hot path',
question: `Which routes call ${a.hostname} (p75=${a.p75Ms}ms across ${a.callCount} calls), and can the call be parallelized, cached, or moved off the critical path?`,
evidence: { metric: 'externalApiP75', hostname: a.hostname, p75Ms: a.p75Ms, callCount: a.callCount },
}));
}
function extractExternalApis(signals) {
const m = signals.metrics?.externalApiP75;
if (!m?.ok && !Array.isArray(m?.rows)) return [];
return (m?.rows ?? [])
.map((r) => ({
hostname: r.origin_hostname,
p75Ms: Math.round(r.value ?? 0),
}))
.filter((a) => a.hostname);
}
function extractCallCounts(signals) {
const m = signals.metrics?.externalApiCount;
const out = new Map();
if (!m) return out;
for (const r of m.rows ?? []) {
if (r?.origin_hostname) out.set(r.origin_hostname, r.value ?? 0);
}
return out;
}
lib/gates/hard-gates.mjs
import { canonicalizeRoute } from '../route-normalize.mjs';
export const FLAGS_ENDPOINT = '/.well-known/vercel/flags';
export const VERCEL_FLAGS_PACKAGES = [
'@vercel/flags',
'@vercel/flags/next',
'@vercel/flags/sveltekit',
'@vercel/flags/nuxt',
];
export const WORKFLOW_ENDPOINT_PREFIXES = [
'/.well-known/workflow',
'/api/.well-known/workflow',
];
export function applyHardGates(candidates, signals = {}) {
const allowed = [];
const gated = [];
for (const candidate of candidates) {
if (isFlagsEndpointCandidate(candidate)) {
gated.push({
...candidate,
gatedReason: flagsEndpointReason(signals),
});
continue;
}
if (isWorkflowRuntimeEndpointCandidate(candidate)) {
gated.push({
...candidate,
gatedReason: workflowEndpointReason(signals),
});
continue;
}
allowed.push(candidate);
}
return { allowed, gated };
}
export function isFlagsEndpointCandidate(candidate) {
if (!candidate || candidate.scope === 'account') return false;
const route = normalizeRoute(candidate.route);
return route === FLAGS_ENDPOINT;
}
export function isWorkflowRuntimeEndpointCandidate(candidate) {
if (!candidate || candidate.scope === 'account') return false;
const route = normalizeRoute(candidate.route);
if (!route) return false;
return WORKFLOW_ENDPOINT_PREFIXES.some((prefix) => (
route === prefix || route.startsWith(`${prefix}/`)
));
}
function normalizeRoute(route) {
if (typeof route !== 'string') return null;
const normalized = canonicalizeRoute(route).replace(/\/+$/, '');
return normalized === '' ? '/' : normalized;
}
function flagsEndpointReason(signals) {
const packages = signals.stack?.vercelFlagsPackages;
if (Array.isArray(packages) && packages.length > 0) {
return `hardGated: ${FLAGS_ENDPOINT} is the Vercel Flags endpoint (${packages.join(', ')} detected), not an optimization target`;
}
return `hardGated: ${FLAGS_ENDPOINT} is the Vercel Flags endpoint, not an optimization target`;
}
function workflowEndpointReason(signals) {
const packages = signals.stack?.workflowPackages;
if (Array.isArray(packages) && packages.length > 0) {
return `hardGated: Vercel Workflow runtime endpoint (${packages.join(', ')} detected); long-running step/flow requests are expected orchestration, not an app-route optimization target`;
}
return 'hardGated: Vercel Workflow runtime endpoint; long-running step/flow requests are expected orchestration, not an app-route optimization target';
}
lib/gates/index.mjs
import * as uncachedRoute from './uncached-route.mjs';
import * as slowRoute from './slow-route.mjs';
import * as routeErrors from './route-errors.mjs';
import * as coldStart from './cold-start.mjs';
import * as isrOverrevalidation from './isr-overrevalidation.mjs';
import * as cwvPoor from './cwv-poor.mjs';
import * as platformFluidCompute from './platform-fluid-compute.mjs';
import * as platformBotProtection from './platform-bot-protection.mjs';
import * as middlewareHeavy from './middleware-heavy.mjs';
import * as externalApiSlow from './external-api-slow.mjs';
import * as scannerDriven from './scanner-driven.mjs';
import * as observabilityEventsAttribution from './observability-events-attribution.mjs';
import * as usageSpikeTriage from './usage-spike-triage.mjs';
import * as buildMinutesFanout from './build-minutes-fanout.mjs';
import * as regionMisconfig from './region-misconfig.mjs';
// Intentionally NOT registered:
// - `oversized_memory`: Fluid Compute floor is 2GB; per-route memory right-sizing isn't a customer lever.
// - `deploy_regression`: overlaps Vercel Agent Investigations; `vercel inspect` 404s across teams. slow_route deep-dive already carries per-deployment p95 trend.
export const gates = [
uncachedRoute,
slowRoute,
routeErrors,
coldStart,
isrOverrevalidation,
cwvPoor,
externalApiSlow,
scannerDriven,
// Account-scoped last so platform-scoped sort doesn't dilute code-scoped priority ordering during budget application.
platformFluidCompute,
platformBotProtection,
middlewareHeavy,
observabilityEventsAttribution,
usageSpikeTriage,
buildMinutesFanout,
regionMisconfig,
];
// Overridable via `--max-candidates N` or `VERCEL_OPTIMIZE_MAX_CANDIDATES` (accepts `all`).
// `MAX_CODE_CANDIDATES` is a back-compat alias for tests importing the old name.
export const DEFAULT_MAX_CODE_CANDIDATES = 6;
export const MAX_CODE_CANDIDATES = DEFAULT_MAX_CODE_CANDIDATES;
// Bump on any threshold change so report + iteration baselines can detect gate-logic drift.
export const GATE_VERSION = '1.8.0';
lib/gates/isr-overrevalidation.mjs
// ISR writes re-execute the page render. A w/r ratio above 0.5 means writes are
// happening at least once for every two reads — high enough for the default
// audit to spend investigation budget. writes>100 avoids flapping on quiet routes.
export const metadata = {
id: 'isr_overrevalidation',
threshold: 'writes/reads > 0.5 AND writes > 100',
billingDimension: 'isr',
scope: 'route',
sourceCitation: 'https://vercel.com/docs/incremental-static-regeneration',
description:
'ISR routes with > 1 write per 2 reads. The revalidate interval is too aggressive relative to read traffic — many reads pay to regenerate. Investigate whether the page can tolerate a longer revalidate window or on-demand revalidation via revalidateTag.',
};
export function gate(signals) {
const rows = extractRows(signals);
return rows
.filter((r) => r.writes > 100 && r.reads > 0 && r.writes / r.reads > 0.5)
.map((r) => {
const ratio = r.writes / r.reads;
return {
kind: metadata.id,
scope: 'route',
route: r.route,
files: [],
priority: Math.round(r.writes),
confidence: 0.88,
o11ySignal: `writes=${r.writes},reads=${r.reads},w/r=${ratio.toFixed(2)}`,
reason: 'ISR revalidating faster than read traffic justifies',
question: `On ${r.route}, ${r.writes} ISR writes against ${r.reads} reads (${(ratio * 100).toFixed(0)} writes per 100 reads) — what is the current revalidate interval and can it be lengthened or switched to on-demand?`,
evidence: {
metric: 'isrWritesByRoute',
route: r.route,
writes: r.writes,
reads: r.reads,
ratio,
},
};
});
}
function extractRows(signals) {
const writes = signals.metrics?.isrWritesByRoute?.rows ?? [];
const reads = signals.metrics?.isrReadsByRoute?.rows ?? [];
const writeByRoute = new Map();
for (const r of writes) {
if (!r.route) continue;
writeByRoute.set(r.route, (writeByRoute.get(r.route) ?? 0) + (r.value ?? 0));
}
const readByRoute = new Map();
for (const r of reads) {
if (!r.route) continue;
readByRoute.set(r.route, (readByRoute.get(r.route) ?? 0) + (r.value ?? 0));
}
const routes = new Set([...writeByRoute.keys(), ...readByRoute.keys()]);
return [...routes].map((route) => ({
route,
writes: writeByRoute.get(route) ?? 0,
reads: readByRoute.get(route) ?? 0,
}));
}
lib/gates/middleware-heavy.mjs
// Middleware runs in front of every matching request and is billed as edge invocations.
// If >50% of traffic hits middleware, the matcher is probably broader than necessary.
export const metadata = {
id: 'middleware_heavy',
threshold: 'middlewareInv/totalInv > 0.5 AND middlewareInv > 1000',
billingDimension: 'edge-requests',
scope: 'account',
sourceCitation: 'https://nextjs.org/docs/app/building-your-application/routing/middleware',
description:
'Middleware invocations cover > 50% of total requests at non-trivial volume. The matcher is probably broader than necessary; narrow it to the paths that actually need auth/rewrites/headers.',
};
export function gate(signals) {
const middlewareInv = sumRows(signals.metrics?.middlewareCount?.rows);
if (middlewareInv < 1000) return [];
const totalInv = sumRows(signals.metrics?.requestsByRouteCache?.rows);
if (totalInv === 0) return [];
const ratio = middlewareInv / totalInv;
if (ratio <= 0.5) return [];
const top = [...(signals.metrics?.middlewareCount?.rows ?? [])]
.filter((r) => r.request_path)
.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))
.slice(0, 5)
.map((r) => ({ request_path: r.request_path, count: r.value ?? 0 }));
return [{
kind: metadata.id,
scope: 'account',
files: [],
priority: Math.round(middlewareInv / 1000),
confidence: 0.84,
o11ySignal: `middleware_inv=${middlewareInv},total_req=${totalInv},ratio=${(ratio * 100).toFixed(0)}%`,
reason: 'middleware ran on more than half of all requests',
question: `Middleware invocations (${middlewareInv}) are ${(ratio * 100).toFixed(0)}% of all requests (${totalInv}). Which paths in middleware.ts require interception, and can the matcher be narrowed to exclude static assets, images, and routes that do not need rewriting?`,
evidence: {
metric: 'middlewareCount',
middlewareInv,
totalInv,
ratio,
topPaths: top,
},
}];
}
function sumRows(rows) {
if (!Array.isArray(rows)) return 0;
return rows.reduce((s, r) => s + (r.value ?? 0), 0);
}
lib/gates/observability-events-attribution.mjs
// Observability Events is the metered SKU under Observability Plus.
// Threshold at >20% surfaces material spend; >30% is the critical band.
// Drivers correlate with low cache hit rate, high middleware invocation, and high custom-span cardinality.
export const metadata = {
id: 'observability_events_attribution',
threshold: 'observabilityEventsShare > 0.20 (critical at > 0.30)',
billingDimension: 'observability-events',
scope: 'account',
sourceCitation: 'vercel-optimize gate threshold',
description:
'Observability Events line item exceeds 20% of total billed cost. High share usually traces to low cache hit rate, middleware-heavy traffic, or unconstrained custom-span cardinality. No sampling lever exists for Observability Plus; reduce upstream invocations instead.',
};
const EVENTS_RE = /^Observability Events$/i;
export function gate(signals) {
const services = signals?.usage?.services;
if (!Array.isArray(services) || services.length === 0) return [];
const total = sumBilled(services);
if (total <= 0) return [];
const eventsBilled = services
.filter((s) => EVENTS_RE.test(String(s?.name ?? '')))
.reduce((acc, s) => acc + Number(s.billedCost ?? s.cost ?? 0), 0);
if (eventsBilled <= 0) return [];
const share = eventsBilled / total;
if (share <= 0.20) return [];
const critical = share > 0.30;
return [{
kind: metadata.id,
scope: 'account',
files: [],
priority: critical ? 70 : 55,
confidence: 0.82,
o11ySignal: `observability_events_share=${(share * 100).toFixed(0)}%`,
reason: critical
? 'observability events exceed 30% of total billed cost'
: 'observability events exceed 20% of total billed cost',
question: `Observability Events are ${(share * 100).toFixed(0)}% of the bill. Which routes drive event volume — low-cache-hit traffic, broad middleware invocation, or high custom-span cardinality — and can event volume be reduced upstream of the meter?`,
evidence: {
metric: 'usage.services',
eventsBilled,
totalBilled: total,
observabilityEventsShare: share,
critical,
},
}];
}
function sumBilled(services) {
return services.reduce((acc, s) => acc + Number(s.billedCost ?? s.cost ?? 0), 0);
}
lib/gates/platform-bot-protection.mjs
// Recommend BotID only when there's EVIDENCE of bot traffic or scale large enough that the rec is defensible.
// Without an evidence gate the rec fires on quiet hobby sites and erodes trust.
const MIN_BOT_PCT = 0.05;
const MIN_EDGE_COST = 25; // halved for 14d window
const MIN_TOTAL_REQUESTS = 14_000; // ~14k/14d matches the prior 30k/30d rate
const MIN_TOTAL_FDT_BYTES = 1_000_000;
export const metadata = {
id: 'platform_bot_protection',
threshold: 'botIdEnabled=false AND (botPct >= 0.05 OR edge_cost >= $25/window OR requests >= 14k/14d)',
billingDimension: 'edge-requests',
scope: 'account',
sourceCitation: 'vercel-optimize gate threshold',
description:
'When BotID is disabled AND there is evidence (observed bot bandwidth share, edge cost, or substantial request volume) that bot traffic is non-trivial. Bot traffic inflates edge request counts without delivering user value; staged bot protection can reduce waste on bot-heavy projects. Skipped on quiet projects with no bot evidence — the recommendation would be noise.',
};
export function gate(signals) {
// BotID surfaces under several legacy fields; check all.
const botEnabled =
signals.project?.security?.botIdEnabled === true
|| signals.project?.security?.botProtection === true
|| signals.project?.botProtection?.enabled === true
|| signals.project?.delegatedProtection?.bot === true;
if (botEnabled) return [];
// Project config failed — we can't tell if BotID is on, so stay silent.
if (signals.project?.error) return [];
const totalRequests = totalRequestsFromSignals(signals);
const botShare = computeBotShare(signals);
const edgeService = (signals.usage?.services ?? []).find(
(s) => /edge.request/i.test(s.name ?? '')
);
const edgeCost = edgeService?.billedCost ?? null;
// Require observable bot share, edge cost, OR substantial traffic — otherwise rec is just config nagging.
const hasObservedBots = botShare?.botPct != null && botShare.botPct >= MIN_BOT_PCT;
const hasMaterialEdgeCost = edgeCost != null && edgeCost >= MIN_EDGE_COST;
const hasSubstantialTraffic = totalRequests >= MIN_TOTAL_REQUESTS;
if (!hasObservedBots && !hasMaterialEdgeCost && !hasSubstantialTraffic) return [];
const challengeRule = signals.project?.security?.managedRules?.bot_filter;
const ruleNote = challengeRule?.active
? `firewall bot_filter rule active (action=${challengeRule.action ?? '?'})`
: 'no firewall bot_filter rule';
// Kicker on high observed bot share — harder evidence than config alone.
let priority = edgeCost != null ? Math.max(20, Math.round(edgeCost)) : 30;
if (botShare?.botPct != null && botShare.botPct > 0.2) priority += 20;
// Confidence bumps when we can SEE bot traffic, not just infer from config.
let confidence = edgeCost != null ? 0.85 : 0.6;
if (botShare?.botPct != null && botShare.botPct > 0.2) confidence = Math.min(0.95, confidence + 0.05);
const botShareNote = botShare?.botPct != null
? `bot_fdt_pct=${(botShare.botPct * 100).toFixed(0)}%`
: 'bot_fdt_pct=unknown';
return [{
kind: metadata.id,
scope: 'account',
files: [],
priority,
confidence,
o11ySignal: edgeCost != null
? `edge_cost=${edgeCost.toFixed(0)},bot_protection=disabled,${botShareNote},${ruleNote}`
: `requests=${totalRequests},bot_protection=disabled,${botShareNote},${ruleNote}`,
reason: botShare?.botPct != null && botShare.botPct > 0.2
? 'BotID disabled with observable bot bandwidth share'
: 'BotID disabled with observable traffic',
question: botShare?.botPct != null && botShare.botPct > 0.2
? `Bot traffic accounts for ${(botShare.botPct * 100).toFixed(0)}% of FDT bytes (top category: ${botShare.topCategory ?? 'unknown'}). Would enabling BotID + a challenge rule reduce that share?`
: 'Would enabling BotID (Bot Protection) reduce edge request volume from automated traffic?',
evidence: {
botEnabled: false,
edgeCost,
totalRequests,
managedRules: challengeRule ?? null,
botShare: botShare ?? null,
},
}];
}
function totalRequestsFromSignals(signals) {
const rows = signals.metrics?.requestsByRouteCache?.rows;
if (!Array.isArray(rows)) return 0;
return rows.reduce((s, r) => s + (r.value ?? 0), 0);
}
// CLI convention: bot_category="" means "not classified as a bot" (human + unclassified); any non-empty = bot.
function computeBotShare(signals) {
const rows = signals.metrics?.fdtByBot?.rows;
if (!Array.isArray(rows) || rows.length === 0) return null;
let humanBytes = 0;
let botBytes = 0;
let topCategory = null;
let topBytes = 0;
for (const r of rows) {
const v = r.value ?? 0;
const cat = r.bot_category ?? '';
if (cat === '') {
humanBytes += v;
} else {
botBytes += v;
if (v > topBytes) {
topBytes = v;
topCategory = cat;
}
}
}
const total = humanBytes + botBytes;
if (total < MIN_TOTAL_FDT_BYTES) return null;
return { humanBytes, botBytes, botPct: botBytes / total, topCategory };
}
lib/gates/platform-fluid-compute.mjs
// Second branch (slow p95 + traffic floor) keeps the gate useful on teams where
// cold-start isn't directly observable — common on CLI v53 — trading specificity for coverage.
export const metadata = {
id: 'platform_fluid_compute',
threshold: 'fluid=false AND (any cold_start signal OR any route with p95>1000ms AND inv>1000)',
billingDimension: 'function-duration',
scope: 'account',
sourceCitation: 'vercel-optimize gate threshold',
description:
'When Fluid Compute is disabled on a project that shows cold-start pressure (high cold-start rate) or sustained slow function p95 on hot routes. Fluid Compute reduces cold starts via instance reuse — recommend turning it on at the project level rather than per-route.',
};
export function gate(signals) {
// If project config failed to load we can't tell if Fluid is on; recommending it when already-on
// erodes trust badly, so stay silent and let Strengths note the gap.
if (signals.project?.error) return [];
const fluidEnabled =
signals.project?.resourceConfig?.fluid === true
|| signals.project?.defaultResourceConfig?.fluid === true;
if (fluidEnabled) return [];
const cold = extractHighColdRoutes(signals);
const slow = extractSlowHotRoutes(signals);
if (cold.length === 0 && slow.length === 0) return [];
return [{
kind: metadata.id,
scope: 'account',
files: [],
priority: 50,
confidence: cold.length > 0 ? 0.85 : 0.65,
o11ySignal: cold.length > 0
? `${cold.length} route(s) with high cold-start rate`
: `${slow.length} hot route(s) with p95>1s; cold-start not directly observable`,
reason: cold.length > 0
? 'cold starts observed and Fluid Compute is disabled'
: 'slow hot routes and Fluid Compute is disabled',
question: 'Would enabling Fluid Compute reduce cold-start and warm-instance reuse overhead for the observed hot routes?',
evidence: { fluidEnabled, highColdRoutes: cold.slice(0, 5), slowHotRoutes: slow.slice(0, 5) },
}];
}
function extractHighColdRoutes(signals) {
const live = signals.metrics?.fnStartTypeByRoute?.rows;
if (Array.isArray(live) && live.some((r) => 'coldCount' in r || 'coldPct' in r)) {
return live.filter((r) => r.route && (r.coldPct ?? 0) > 0.3 && (r.total ?? 0) > 100);
}
// Legacy pre-derived fixture shape.
const direct = signals.metrics?.coldStartByRoute?.rows;
if (Array.isArray(direct)) {
return direct.filter((r) => r.route && (r.coldPct ?? 0) > 0.3 && (r.total ?? 0) > 100);
}
const legacy = signals.metrics?.coldStarts?.series;
if (Array.isArray(legacy)) {
return legacy
.map((s) => {
const total = s.summary?.count ?? 0;
const coldCount = s.summary?.coldCount ?? s.summary?.sum ?? 0;
return { route: s.groupValues?.route, total, coldPct: total > 0 ? coldCount / total : 0 };
})
.filter((r) => r.route && r.coldPct > 0.3 && r.total > 100);
}
return [];
}
function extractSlowHotRoutes(signals) {
const dur = signals.metrics?.fnDurationP95ByRoute?.rows;
const cache = signals.metrics?.requestsByRouteCache?.rows;
if (!Array.isArray(dur)) return [];
// Sum requests per route across cache_result.
const inv = new Map();
for (const r of (cache ?? [])) {
if (!r.route) continue;
inv.set(r.route, (inv.get(r.route) ?? 0) + (r.value ?? 0));
}
return dur
.filter((r) => r.route)
.map((r) => ({ route: r.route, p95Ms: Math.round(r.value ?? 0), invocations: inv.get(r.route) ?? 0 }))
// inv>500 floor is the 14d-window equivalent of the old 1000/30d.
.filter((r) => r.p95Ms > 1000 && r.invocations > 500);
}
lib/gates/region-misconfig.mjs
// Region-misconfig gate. Branch 2 (scanner-only) — per-region TTFB data gap.
//
// The intended Branch 1 (region-grouped TTFB metric) was preflight-tested but the
// CLI returned INTERNAL_ERROR for the `--group-by route --group-by function_region`
// combination, and SAML re-auth blocked single-dim verification (see Phase 0 in
// plans/wild-splashing-flamingo.md). Ship scanner-only with `evidence.dataGap` and
// add the query later when verifiable.
//
// Fires when a single-region pin is found AND the project has meaningful surface area
// (routes.length > 20). Skips multi-region configs (informational only).
export const metadata = {
id: 'region_misconfig',
threshold: 'single-region pin found AND routes.length > 20 (scanner-only branch)',
billingDimension: 'function-duration',
scope: 'account',
sourceCitation: 'vercel-optimize gate threshold',
description:
"A single function region is pinned in `vercel.json` or per-route `preferredRegion`. Without per-region TTFB data (data gap), the gate can't quantify the geographic latency cost — but a single-region pin on a project with 20+ routes is worth auditing against Speed Insights traffic geo.",
};
const ROUTE_FLOOR = 20;
const SCANNER_PATTERN = 'region-pin-in-config';
export function gate(signals) {
const findings = (signals?.codebase?.findings ?? []).filter((f) => f.pattern === SCANNER_PATTERN);
if (findings.length === 0) return [];
const routes = signals?.codebase?.routes ?? [];
if (routes.length < ROUTE_FLOOR) return [];
const singleRegionFindings = findings.filter((f) => Array.isArray(f.regions) && f.regions.length === 1);
if (singleRegionFindings.length === 0) return [];
const allPinned = new Set();
for (const f of singleRegionFindings) {
for (const r of f.regions ?? []) allPinned.add(r);
}
const regionList = [...allPinned];
// If multiple distinct single-region pins exist across files, the surface is partly
// multi-region by accident; that's noteworthy but lower priority.
const homogeneous = regionList.length === 1;
return [{
kind: metadata.id,
scope: 'account',
files: singleRegionFindings.map((f) => f.file).slice(0, 6),
priority: homogeneous ? 42 : 38,
confidence: 0.6, // low — no per-region TTFB data
o11ySignal: `pinned_regions=${regionList.join(',')} routes=${routes.length}`,
reason: homogeneous
? `all functions pinned to a single region (${regionList[0]}) on a project with ${routes.length} routes`
: `${regionList.length} different single-region pins across files`,
question: 'Are the pinned function regions aligned with the dominant user geography and the data source location? Speed Insights TTFB-by-country can ground the comparison.',
evidence: {
metric: 'codebase.findings',
pinnedRegions: regionList,
findingsCount: singleRegionFindings.length,
routeCount: routes.length,
sampleFiles: singleRegionFindings.slice(0, 3).map((f) => ({ file: f.file, regions: f.regions, subtype: f.subtype })),
dataGap: 'region-grouped-TTFB-unavailable',
},
}];
}
lib/gates/route-errors.mjs
// Errored function invocations still bill at full duration, so high-volume 5xx is a cost issue, not just reliability.
import { withRouteShapeWarnings } from '../route-normalize.mjs';
const MIN_VOLUME_FOR_RATE_EMISSION = 1000;
export const metadata = {
id: 'route_errors',
threshold: `count > 250 OR (totalRequests >= ${MIN_VOLUME_FOR_RATE_EMISSION} AND errorRate > 0.01)`,
billingDimension: 'function-duration',
scope: 'route',
sourceCitation: 'vercel-optimize gate threshold',
description:
'Routes producing > 250 5xx errors over the window, or with > 1% error rate on at least 1,000 total requests. Errored function invocations still bill at full duration; high error rates also poison user experience.',
};
export function gate(signals) {
const errors = extractErrors(signals);
return errors
.filter((e) => e.count > 250 || (e.total >= MIN_VOLUME_FOR_RATE_EMISSION && (e.errorRate ?? 0) > 0.01))
.map((e) => withRouteShapeWarnings({
kind: metadata.id,
scope: 'route',
route: e.route,
files: [],
priority: e.count,
confidence: 0.93,
o11ySignal: e.errorRate != null
? `errs=${e.count},rate=${(e.errorRate * 100).toFixed(1)}%`
: `errs=${e.count}`,
reason: 'concentrated 5xx errors',
question: `Why does ${e.route} produce ${e.count} 5xx errors over the window, and what code path is failing?`,
evidence: { metric: e.metric, route: e.route, count: e.count, totalRequests: e.total, errorRate: e.errorRate },
}, signals));
}
function extractErrors(signals) {
const fnStatus = signals.metrics?.fnStatusByRoute;
if (Array.isArray(fnStatus?.rows)) return extractFromStatusRows(fnStatus.rows, 'fnStatusByRoute');
const m = signals.metrics?.requestsByRouteStatus;
const cache = signals.metrics?.requestsByRouteCache;
if (!m?.ok && !Array.isArray(m?.rows)) return [];
const errors = extractFromStatusRows(m?.rows ?? [], 'requestsByRouteStatus');
// cache rollup is summed across cache_result, giving per-route total request count.
const totalByRoute = new Map();
for (const row of (cache?.rows ?? [])) {
if (!row.route) continue;
totalByRoute.set(row.route, (totalByRoute.get(row.route) ?? 0) + (row.value ?? 0));
}
return errors.map((e) => {
const total = totalByRoute.get(e.route) ?? 0;
return {
...e,
total,
errorRate: total > 0 ? e.count / total : null,
};
});
}
function extractFromStatusRows(rows, metric) {
const errByRoute = new Map();
const totalByRoute = new Map();
for (const row of rows) {
const route = row.route;
if (!route) continue;
const v = row.value ?? 0;
const status = String(row.http_status ?? '');
if (/^5\d\d$/.test(status)) errByRoute.set(route, (errByRoute.get(route) ?? 0) + v);
totalByRoute.set(route, (totalByRoute.get(route) ?? 0) + v);
}
return [...errByRoute.entries()].map(([route, count]) => {
const total = totalByRoute.get(route) ?? 0;
const errorRate = total > 0 ? count / total : null;
return { route, count, total, errorRate, metric };
});
}
lib/gates/scanner-driven.mjs
// Signal source is the codebase itself, not traffic. COLD-PATH and NO-ROUTE-MAPPING findings
// are dropped unless the scanner sets trafficIndependent (build configs, middleware matchers, etc.).
// Annotation happens in scan-codebase.mjs; gates here just read scanner.o11ySignal.
export const SCANNER_GATES = [
{ id: 'image_optimization', patterns: ['unoptimized-image'], threshold: 2,
billingDimension: 'image-optimization', priority: 30 },
{ id: 'cache_header_gap', patterns: ['max-age-without-s-maxage', 'missing-cache-headers'], threshold: 1,
billingDimension: 'edge-requests', priority: 40 },
{ id: 'rendering_candidate', patterns: ['force-dynamic', 'headers-in-page'], threshold: 3,
billingDimension: 'function-duration', priority: 35 },
{ id: 'use_cache_date_stamp', patterns: ['use-cache-date-stamp'], threshold: 1,
billingDimension: 'isr', priority: 45 },
{ id: 'cache_components_suspense_dedupe', patterns: ['cache-components-suspense-dedupe'], threshold: 1,
billingDimension: 'function-duration', priority: 38 },
];
export const metadata = {
id: 'scanner-driven',
threshold: 'per-kind: scanner matches.length >= threshold',
billingDimension: 'mixed',
scope: 'mixed',
sourceCitation: 'vercel-optimize gate threshold',
description:
'Configured kinds emitted from scanner output. Each requires a minimum match count to avoid noise. Findings on cold-path or unmappable files are dropped unless the underlying scanner is trafficIndependent.',
};
export function gate(signals) {
const findings = signals.codebase?.findings ?? [];
if (findings.length === 0) return [];
const candidates = [];
for (const cfg of SCANNER_GATES) {
const matched = findings.filter((f) => {
if (!cfg.patterns.includes(f.pattern)) return false;
if (!f.trafficIndependent) {
if (!f.o11ySignal || f.o11ySignal === 'scanner-only') return false;
if (f.o11ySignal === 'COLD-PATH') return false;
if (f.o11ySignal === 'NO-ROUTE-MAPPING') return false;
}
if (cfg.id === 'cache_header_gap' && observedCacheHitRate(f.o11ySignal) >= 90) return false;
return true;
});
for (const group of groupFindings(cfg, matched)) {
if (group.findings.length < cfg.threshold) continue;
candidates.push(candidateForGroup(cfg, group));
}
}
return candidates;
}
function groupFindings(cfg, findings) {
const groups = new Map();
for (const finding of findings) {
const scope = finding.route ? 'route' : 'file';
const target = scope === 'route' ? finding.route : finding.file;
if (!target) continue;
const key = `${cfg.id}:${scope}:${target}`;
if (!groups.has(key)) groups.set(key, { scope, target, findings: [] });
groups.get(key).findings.push(finding);
}
return [...groups.values()];
}
function candidateForGroup(cfg, group) {
const matched = group.findings;
const route = group.scope === 'route' ? group.target : null;
return {
kind: cfg.id,
scope: group.scope,
route,
files: uniqueStrings(matched.map((m) => m.file)).slice(0, 6),
priority: cfg.priority + Math.min(matched.length, 10),
confidence: 0.88,
o11ySignal: matched
.map((m) => m.o11ySignal)
.find((s) => s && s !== 'COLD-PATH' && s !== 'NO-ROUTE-MAPPING')
?? 'scanner-only',
reason: `${matched.length} ${cfg.patterns.join('+')} finding(s)`,
question: questionFor(cfg.id, matched),
evidence: {
scannerMatches: matched.length,
patterns: cfg.patterns,
scope: group.scope,
route,
sampleFiles: matched.slice(0, 3).map((m) => ({ file: m.file, line: m.line })),
},
};
}
function questionFor(kindId, matched) {
const sample = matched.slice(0, 3).map((m) => m.file).join(', ');
switch (kindId) {
case 'image_optimization':
return `Which raw <img> tags in ${sample} should move to next/image (or the framework's image component)?`;
case 'cache_header_gap':
return `Should the route handlers in ${sample} set Cache-Control with s-maxage to serve from the CDN?`;
case 'rendering_candidate':
return `Why are the routes in ${sample} forced to dynamic rendering, and can any of them tolerate ISR or static generation?`;
case 'use_cache_date_stamp':
return `Which 'use cache' boundaries in ${sample} embed new Date()/Date.now()/Math.random() that destabilizes cache keys, and can the timestamps be hoisted to a build constant or moved into a client useEffect?`;
case 'cache_components_suspense_dedupe':
return `In ${sample}, which repeated fetch or helper is being re-invoked across separate <Suspense> boundaries, and can the promise be hoisted to the page level or moved to 'use cache: remote' for cross-boundary dedupe?`;
default:
return `Investigate ${matched.length} ${kindId} finding(s).`;
}
}
function uniqueStrings(values) {
return [...new Set(values.filter((v) => typeof v === 'string' && v.length > 0))];
}
function observedCacheHitRate(signal) {
if (typeof signal !== 'string') return null;
const m = /\bcache=([\d.]+)%/.exec(signal);
if (!m) return null;
const n = Number(m[1]);
return Number.isFinite(n) ? n : null;
}
lib/gates/select-candidates.mjs
// Deterministic launch selection for the code-scope investigation budget.
//
// Raw priority still orders candidates inside each pass. The default budget is
// impact-first, with failure-mode diversity when a kind's top signal is large
// enough to justify taking a first-pass slot.
const DEFAULT_KIND_CAPS = new Map([
['slow_route', 2],
['uncached_route', 2],
['route_errors', 2],
]);
const DIVERSITY_ELIGIBILITY = new Map([
// A handful of 5xx errors can pass the route_errors gate because the rate is
// high, but that should not displace much larger cost/performance signals in
// the default six-candidate pass.
['route_errors', (candidate) => numberFromEvidence(candidate, 'count') >= 1000],
// Scanner-driven cache findings are valuable, but the default pass should
// spend a slot only when observability shows meaningful route traffic or a
// very slow route handler.
['cache_header_gap', (candidate) => {
const invocations = numberFromSignal(candidate?.o11ySignal, 'inv');
const p95Ms = durationMsFromSignal(candidate?.o11ySignal, 'p95');
return invocations >= 50_000 || p95Ms >= 2000;
}],
['rendering_candidate', (candidate) => numberFromSignal(candidate?.o11ySignal, 'inv') >= 50_000],
]);
export function selectLaunchCandidates(candidates, budget, { diversify = false } = {}) {
const pool = Array.isArray(candidates) ? candidates : [];
if (budget === Infinity) {
return { selected: pool, skipped: [], selectionMode: 'all' };
}
if (!Number.isInteger(budget) || budget < 1) {
throw new TypeError('selectLaunchCandidates budget must be a positive integer or Infinity');
}
if (!diversify) {
return {
selected: pool.slice(0, budget),
skipped: pool.slice(budget),
selectionMode: 'priority',
};
}
const selected = [];
const selectedKeys = new Set();
const countsByKind = new Map();
const add = (candidate) => {
const key = candidateIdentity(candidate);
if (selectedKeys.has(key)) return false;
selectedKeys.add(key);
selected.push(candidate);
const kind = candidate.kind ?? '<unknown>';
countsByKind.set(kind, (countsByKind.get(kind) ?? 0) + 1);
return true;
};
// First pass: one candidate per failure mode, preserving the existing sorted
// order. This is where the default run gets broad coverage, but only for
// kinds whose signal is strong enough for a default slot.
for (const candidate of pool) {
if (selected.length >= budget) break;
const kind = candidate.kind ?? '<unknown>';
if ((countsByKind.get(kind) ?? 0) > 0) continue;
if (!isDiversityEligible(candidate)) continue;
add(candidate);
}
// Second pass: allow a second entry for high-frequency families, but avoid
// letting slow_route consume the entire default budget when other kinds exist.
for (const candidate of pool) {
if (selected.length >= budget) break;
const kind = candidate.kind ?? '<unknown>';
const cap = DEFAULT_KIND_CAPS.get(kind) ?? 1;
if ((countsByKind.get(kind) ?? 0) >= cap) continue;
if (!isDiversityEligible(candidate)) continue;
add(candidate);
}
// Final fill: if the project only has one or two candidate kinds, use the
// whole requested budget rather than leaving slots empty.
for (const candidate of pool) {
if (selected.length >= budget) break;
add(candidate);
}
return {
selected,
skipped: pool.filter((candidate) => !selectedKeys.has(candidateIdentity(candidate))),
selectionMode: 'diverse-default',
};
}
function candidateIdentity(candidate) {
return [
candidate?.kind ?? '',
candidate?.route ?? '',
candidate?.hostname ?? '',
candidate?.scope ?? '',
candidate?.o11ySignal ?? '',
].join('\u0000');
}
function isDiversityEligible(candidate) {
const fn = DIVERSITY_ELIGIBILITY.get(candidate?.kind);
return fn ? fn(candidate) : true;
}
function numberFromEvidence(candidate, key) {
const value = candidate?.evidence?.[key];
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
}
function numberFromSignal(signal, key) {
if (typeof signal !== 'string') return 0;
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`(?:^|,)${escaped}=([\\d.]+)`);
const m = re.exec(signal);
if (!m) return 0;
const n = Number(m[1]);
return Number.isFinite(n) ? n : 0;
}
function durationMsFromSignal(signal, key) {
if (typeof signal !== 'string') return 0;
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`(?:^|,)${escaped}=([\\d.]+)ms`);
const m = re.exec(signal);
if (!m) return 0;
const n = Number(m[1]);
return Number.isFinite(n) ? n : 0;
}
lib/gates/slow-route.mjs
// Primary threshold (p95>500 AND inv>=1400) WHY: 1.4k/14d is the floor where p95 stabilizes statistically
// and a 3-5x performance win still pays for engineering time. Secondary (p95>1500 AND inv>=250) catches "catastrophically
// slow at any volume" — usually a broken sync call or cold-start chain the customer wants to know about.
//
// 5xx disqualifier: when error rate >50% the route is failing, not slow — latency reflects crash time,
// not work time. route_errors covers it independently; we disqualify here so budget isn't spent on a
// sub-agent that will correctly abstain.
import { withRouteShapeWarnings } from '../route-normalize.mjs';
const ERROR_RATE_DISQUALIFY_THRESHOLD = 0.5;
export const metadata = {
id: 'slow_route',
threshold: '(p95 > 500 AND inv >= 1400) OR (p95 > 1500 AND inv >= 250); disqualified when 5xx rate > 50%; Vercel Workflow runtime endpoints are hard-gated',
billingDimension: 'function-duration',
scope: 'route',
sourceCitation: 'vercel-optimize gate threshold',
description:
'Routes with p95 function duration above 500ms at meaningful traffic (>=1,400 invocations in window), OR catastrophically slow routes (>1500ms p95 at any volume >=250). High duration drives both function-duration cost and user-perceived latency. Investigate sequential awaits, slow external APIs, missing caching, N+1 patterns. Routes with >50% 5xx rate are disqualified — those are reliability problems, not performance tuning targets, and surface via route_errors instead. Vercel Workflow runtime endpoints (`/.well-known/workflow/v1/*`) are hard-gated before launch because long-running step/flow requests are expected orchestration, not app-route bottlenecks.',
};
export function gate(signals) {
const routes = extractFunctionRoutes(signals);
const errorRates = extractErrorRatesByRoute(signals);
return routes
.filter((r) => (r.p95Ms > 500 && r.invocations >= 1400) || (r.p95Ms > 1500 && r.invocations >= 250))
.map((r) => {
const errorRate = errorRates.get(r.route);
const candidate = {
kind: metadata.id,
scope: 'route',
route: r.route,
files: [],
priority: Math.round(r.p95Ms * Math.max(r.invocations, 1) / 1000),
confidence: 0.94,
o11ySignal: `inv=${r.invocations},p95=${r.p95Ms}ms${errorRate != null ? `,5xx=${(errorRate * 100).toFixed(0)}%` : ''}`,
reason: 'slow high-traffic route',
question: `What is the concrete bottleneck in ${r.route} (p95=${r.p95Ms}ms over ${r.invocations} invocations), and which file-level change would reduce it?`,
evidence: { metric: 'fnDurationP95ByRoute', route: r.route, p95Ms: r.p95Ms, invocations: r.invocations, errorRate },
};
if (errorRate != null && errorRate > ERROR_RATE_DISQUALIFY_THRESHOLD) {
candidate.disqualified = true;
candidate.disqualifyReason = `high error rate (${(errorRate * 100).toFixed(0)}% 5xx — reliability issue, not performance; covered by route_errors gate)`;
}
return withRouteShapeWarnings(candidate, signals);
});
}
// Routes without status data are absent from the map → gate falls back to "no disqualification".
function extractErrorRatesByRoute(signals) {
const m = signals.metrics?.fnStatusByRoute;
const out = new Map();
if (!Array.isArray(m?.rows)) return out;
const perRoute = new Map();
for (const row of m.rows) {
if (!row?.route) continue;
const v = row.value ?? 0;
const prior = perRoute.get(row.route) ?? { errors5xx: 0, total: 0 };
if (/^5/.test(String(row.http_status ?? ''))) prior.errors5xx += v;
prior.total += v;
perRoute.set(row.route, prior);
}
for (const [route, r] of perRoute) {
if (r.total > 0) out.set(route, r.errors5xx / r.total);
}
return out;
}
function extractFunctionRoutes(signals) {
const dur = signals.metrics?.fnDurationP95ByRoute;
if (!dur?.ok && !Array.isArray(dur?.rows)) return [];
const req = signals.metrics?.requestsByRouteCache;
const invByRoute = new Map();
for (const row of (req?.rows ?? [])) {
if (!row.route) continue;
invByRoute.set(row.route, (invByRoute.get(row.route) ?? 0) + (row.value ?? 0));
}
return (dur?.rows ?? [])
.filter((r) => r.route)
.map((r) => ({
route: r.route,
p95Ms: Math.round(r.value ?? 0),
invocations: invByRoute.get(r.route) ?? 0,
}));
}
lib/gates/types.d.ts
export type CandidateScope = 'route' | 'file' | 'account';
export interface GateMetadata {
id: string;
threshold: string;
billingDimension: string;
scope: CandidateScope | 'mixed';
sourceCitation?: string;
description?: string;
}
export interface Candidate {
kind: string;
scope: CandidateScope;
route?: string | null;
hostname?: string | null;
files: string[];
priority: number;
confidence: number;
o11ySignal?: string;
reason: string;
question: string;
evidence?: Record<string, unknown>;
disqualified?: boolean;
disqualifyReason?: string;
warnings?: string[];
}
export interface Signals {
metrics?: Record<string, unknown>;
codebase?: {
findings?: Array<Record<string, unknown>>;
routes?: Array<Record<string, unknown>>;
};
project?: Record<string, unknown>;
usage?: Record<string, unknown>;
stack?: Record<string, unknown>;
}
lib/gates/uncached-route.mjs
// getShare>0.20 filter WHY: a route that's >80% POST/PUT/DELETE is a mutation endpoint
// where 0% cache is correct — recommending caching there is wrong.
// cache_result values STALE/REVALIDATED/BYPASS fold into "total but not HIT" — matches the "uncached" framing.
import { withRouteShapeWarnings } from '../route-normalize.mjs';
const MIN_GET_SHARE = 0.20;
/** @type {import('./types.d.ts').GateMetadata} */
export const metadata = {
id: 'uncached_route',
threshold: `requests > 500 AND hitRate < 0.5 AND getShare > ${MIN_GET_SHARE} (missing getShare is gated)`,
billingDimension: 'edge-requests',
scope: 'route',
sourceCitation: 'vercel-optimize gate threshold',
description:
'Routes serving > 500 requests/period at < 50% cache hit AND at least 20% GET traffic. Each uncached GET request reaches the function, costing edge requests + function duration. Routes that are mostly POST/PUT/DELETE (Server Actions, mutations) are skipped — 0% cache is correct behavior there. Routes with missing method-share data are gated instead of launched. Auth-gated routes are disqualified separately.',
};
/**
* @param {import('./types.d.ts').Signals} signals
* @returns {import('./types.d.ts').Candidate[]}
*/
export function gate(signals) {
const rates = extractCacheHitRates(signals);
const methods = extractMethodShares(signals);
return rates
.map((r) => ({ ...r, getShare: methods.get(r.route) ?? null }))
.filter((r) => r.requests > 500 && r.hitRate < 0.5)
.filter((r) => r.getShare === null || r.getShare > MIN_GET_SHARE)
.map((r) => {
const candidate = withRouteShapeWarnings({
kind: metadata.id,
scope: 'route',
route: r.route,
files: [],
priority: Math.round(r.requests * (1 - r.hitRate)),
confidence: r.getShare === null ? 0.5 : 0.92,
o11ySignal: `requests=${r.requests},cache=${(r.hitRate * 100).toFixed(0)}%${r.getShare !== null ? `,get=${(r.getShare * 100).toFixed(0)}%` : ''}`,
reason: 'uncached high-traffic route',
question: `Why does ${r.route} have ${(r.hitRate * 100).toFixed(0)}% cache hit rate on ${r.requests} requests in this metrics window, and is it safe to cache at the edge?`,
evidence: { metric: 'requestsByRouteCache', route: r.route, requests: r.requests, hitRate: r.hitRate, getShare: r.getShare },
}, signals);
if (r.getShare !== null) return candidate;
return {
...candidate,
disqualified: true,
disqualifyReason: 'missing GET-share data — route method mix is required before recommending edge caching',
warnings: [...new Set([...(candidate.warnings ?? []), 'method-share:missing'])],
};
});
}
function extractCacheHitRates(signals) {
const m = signals.metrics?.requestsByRouteCache;
if (!m?.ok && !Array.isArray(m?.rows)) return [];
const perRoute = new Map();
for (const row of (m?.rows ?? [])) {
const route = row.route;
if (!route) continue;
const value = row.value ?? 0;
const prior = perRoute.get(route) ?? { route, hits: 0, total: 0 };
if (row.cache_result === 'HIT') prior.hits += value;
prior.total += value;
perRoute.set(route, prior);
}
return [...perRoute.values()].map((r) => ({
route: r.route,
requests: r.total,
hitRate: r.total > 0 ? r.hits / r.total : 0,
}));
}
function extractMethodShares(signals) {
const m = signals.metrics?.requestsByRouteMethod;
const out = new Map();
if (!Array.isArray(m?.rows)) return out;
const perRoute = new Map();
for (const row of m.rows) {
if (!row?.route) continue;
const v = row.value ?? 0;
const prior = perRoute.get(row.route) ?? { gets: 0, total: 0 };
if ((row.request_method ?? '').toUpperCase() === 'GET') prior.gets += v;
prior.total += v;
perRoute.set(row.route, prior);
}
for (const [route, r] of perRoute) {
if (r.total > 0) out.set(route, r.gets / r.total);
}
return out;
}
lib/gates/usage-spike-triage.mjs
// Detects per-day billing spikes by inspecting usage.breakdown.data[] (daily granularity).
// Fires when any single day's total bill > 2× the window mean, OR a single SKU's day value > 3× its window mean.
// Emits one candidate per spiking SKU (or 'total' when the spike is broad).
// Degrades gracefully when daily data is unavailable — common path because the skill prefers --group-by project, which omits daily breakdown.
export const metadata = {
id: 'usage_spike_triage',
threshold: 'any-day total > 2x mean OR any-day SKU > 3x SKU mean',
billingDimension: 'mixed',
scope: 'account',
sourceCitation: 'vercel-optimize gate threshold',
description:
'A single day in the billing window deviates sharply from the window baseline. Triage branches: bot or AI crawler spike, viral moment, pricing-model migration (legacy SKU → new), code regression. Without daily-granularity data, this gate stays dormant.',
};
const TOTAL_MULTIPLIER = 2;
const SKU_MULTIPLIER = 3;
const MIN_BILLED_FLOOR = 5; // skip spikes whose absolute value is too small to matter
export function gate(signals) {
const days = signals?.usage?.breakdown?.data;
if (!Array.isArray(days) || days.length < 3) return [];
const dayTotals = days.map(dayTotal);
const mean = dayTotals.reduce((a, b) => a + b, 0) / dayTotals.length;
if (mean <= MIN_BILLED_FLOOR) return [];
const totalSpikeDays = dayTotals
.map((value, idx) => ({ idx, value }))
.filter((d) => d.value > mean * TOTAL_MULTIPLIER && d.value > MIN_BILLED_FLOOR);
const skuStats = aggregateSkuStats(days);
const skuSpikes = [];
for (const stat of skuStats) {
if (stat.mean <= MIN_BILLED_FLOOR) continue;
for (const sample of stat.samples) {
if (sample.value > stat.mean * SKU_MULTIPLIER && sample.value > MIN_BILLED_FLOOR) {
skuSpikes.push({
name: stat.name,
dayIndex: sample.idx,
dayValue: sample.value,
skuMean: stat.mean,
multiplier: stat.mean > 0 ? sample.value / stat.mean : null,
});
}
}
}
if (totalSpikeDays.length === 0 && skuSpikes.length === 0) return [];
const candidates = [];
if (totalSpikeDays.length > 0) {
const peak = totalSpikeDays.reduce((a, b) => (a.value > b.value ? a : b));
candidates.push({
kind: metadata.id,
scope: 'account',
files: [],
priority: 60,
confidence: 0.78,
o11ySignal: `total_spike day_idx=${peak.idx} day_billed=${peak.value.toFixed(2)} window_mean=${mean.toFixed(2)} mult=${(peak.value / mean).toFixed(1)}x`,
reason: 'total billed cost on one day exceeds 2× the window mean',
question: 'Which workload generated the day-over-day spike — bot or AI-crawler traffic on a cacheable route, a viral event, a pricing-model migration, or a code regression?',
evidence: {
metric: 'usage.breakdown.data.total',
spikeDay: peak.idx,
spikeBilled: peak.value,
windowMean: mean,
multiplier: peak.value / mean,
skuName: 'total',
},
});
}
// Up to 3 SKU-specific candidates; the rest fold into 'multiple SKUs spiking' framing.
const orderedSkuSpikes = skuSpikes.sort((a, b) => b.dayValue - a.dayValue).slice(0, 3);
for (const spike of orderedSkuSpikes) {
candidates.push({
kind: metadata.id,
scope: 'account',
files: [],
priority: 55,
confidence: 0.78,
o11ySignal: `sku_spike sku="${spike.name}" day_idx=${spike.dayIndex} day_billed=${spike.dayValue.toFixed(2)} sku_mean=${spike.skuMean.toFixed(2)} mult=${spike.multiplier.toFixed(1)}x`,
reason: `${spike.name} on one day exceeds 3× its window mean`,
question: `${spike.name} spiked ${spike.multiplier.toFixed(1)}× on day ${spike.dayIndex}. Which event (bot traffic, viral content, deploy regression, integration sync) drove it, and is the spiking SKU one the skill already covers?`,
evidence: {
metric: 'usage.breakdown.data.services',
skuName: spike.name,
spikeDay: spike.dayIndex,
spikeBilled: spike.dayValue,
skuMean: spike.skuMean,
multiplier: spike.multiplier,
},
});
}
return candidates;
}
function dayTotal(day) {
if (Array.isArray(day?.services)) {
return day.services.reduce((a, s) => a + Number(s.billedCost ?? s.cost ?? 0), 0);
}
return Number(day?.billedCost ?? day?.cost ?? 0);
}
function aggregateSkuStats(days) {
const byName = new Map();
days.forEach((day, idx) => {
const services = Array.isArray(day?.services) ? day.services : [];
for (const svc of services) {
const name = String(svc?.name ?? '').trim();
if (!name) continue;
const value = Number(svc.billedCost ?? svc.cost ?? 0);
if (!byName.has(name)) byName.set(name, { name, samples: [] });
byName.get(name).samples.push({ idx, value });
}
});
for (const stat of byName.values()) {
const sum = stat.samples.reduce((a, s) => a + s.value, 0);
stat.mean = stat.samples.length > 0 ? sum / stat.samples.length : 0;
}
return [...byName.values()];
}
lib/grade-recommendation.mjs
// 4-axis rubric (specificity, actionability, grounding, evidence) → bucket. See references/recommendations.md.
// Account-scope (platform_*) recs use a separate grounding/evidence pair — they structurally cannot produce file:line.
const HEDGE_WORDS = /\b(consider|might|may|could|perhaps|maybe|likely|probably)\b/gi;
const VERB_OPENERS = /^\s*(?:[-*]\s+|\d+[.)]\s+|[*_]+)?(?:add|set|enable|disable|replace|remove|move|wrap|cache|defer|parallelize|introduce|configure|update|change|switch|opt[-\s]?in|opt[-\s]?out|export|import|install|run|delete|rename)/im;
const COUNT_WORDS_RE = /\b(errors?|queries|invocations|requests|reads|writes|bytes|fetch(?:es)?|calls?|hits?|misses?|seconds?|images?|deployments?|cold[- ]?starts?|users?)\b/gi;
const UNIT_RE = /\b\d[\d.,]*\s*(?:%|ms|s|sec|seconds?|min|minutes?|h|hours?|GB|MB|KB|K|M|B|rps|qps|req\/s|reqs?\/min)\b/gi;
const CODE_FENCE_RE = /```[\s\S]*?```/g;
const INLINE_CODE_RE = /`[^`\n]{10,}`/g;
const FILE_LINE_RE = /[\w/.\-()\[\]]+\.\w+:\d+/g;
// Grounding + evidence are lie-detectors — weighted higher than specificity/actionability, which LLMs can game with fluff.
const W = { grounding: 0.35, evidence: 0.30, specificity: 0.20, actionability: 0.15 };
export function gradeRecommendation(rec, ctx = {}) {
const accountScope = isAccountScope(rec);
const specificity = scoreSpecificity(rec);
const actionability = scoreActionability(rec);
const grounding = accountScope ? scoreGroundingAccount(rec) : scoreGrounding(rec, ctx);
const evidence = accountScope ? scoreEvidenceAccount(rec) : scoreEvidence(rec);
const overall = roundTo(
grounding * W.grounding + evidence * W.evidence + specificity * W.specificity + actionability * W.actionability,
4,
);
return {
specificity, actionability, grounding, evidence, overall,
grade: grade(overall),
scope: accountScope ? 'account' : 'route',
};
}
function isAccountScope(rec) {
if (rec?.scope === 'account') return true;
const ref = rec?.candidateRef;
if (typeof ref === 'string' && ref.startsWith('platform_')) return true;
return false;
}
function grade(overall) {
if (overall >= 0.85) return 'Excellent';
if (overall >= 0.70) return 'Good';
if (overall >= 0.55) return 'Fair';
return 'Poor';
}
function scoreSpecificity(rec) {
let s = 0;
const codeText = [rec.fix, rec.currentBehavior, rec.desiredBehavior].filter((x) => typeof x === 'string').join('\n');
const hasFence = CODE_FENCE_RE.test(codeText);
CODE_FENCE_RE.lastIndex = 0;
if (hasFence) s += 0.5;
if (INLINE_CODE_RE.test(codeText)) s += 0.2;
INLINE_CODE_RE.lastIndex = 0;
if (Array.isArray(rec.affectedFiles) && rec.affectedFiles.length > 0) s += 0.2;
if (Array.isArray(rec.findingRefs) && rec.findingRefs.some((r) => /:\d+/.test(r))) s += 0.3;
return Math.min(1, roundTo(s, 4));
}
function scoreActionability(rec) {
const text = typeof rec.fix === 'string' ? rec.fix : '';
if (!text) return 0;
let s = 0;
if (VERB_OPENERS.test(text)) s += 0.35;
const stepCount = (text.match(/(?:^|\n)\s*(?:\d+[.)]\s+|[-*]\s+)/g) ?? []).length;
if (stepCount >= 2) s += 0.35;
else if (stepCount === 1) s += 0.15;
const hedges = (text.match(HEDGE_WORDS) ?? []).length;
HEDGE_WORDS.lastIndex = 0;
s -= Math.min(0.3, hedges * 0.1);
// Baseline so a verb-only one-liner still scores.
s += 0.3;
return Math.max(0, Math.min(1, roundTo(s, 4)));
}
function scoreGrounding(rec, ctx) {
let s = 0;
const knownFindings = Array.isArray(ctx.knownFindings) ? ctx.knownFindings : [];
const findingKeys = new Set(knownFindings.map((f) => `${f.file}:${f.line}`));
const refs = Array.isArray(rec.findingRefs) ? rec.findingRefs : [];
const matched = refs.filter((r) => findingKeys.has(r));
if (matched.length > 0) s += 0.5;
else if (refs.length > 0) s += 0.25;
if (Array.isArray(rec.affectedFiles) && rec.affectedFiles.length > 0) s += 0.25;
const fenceText = [rec.currentBehavior, rec.desiredBehavior].filter((x) => typeof x === 'string').join('\n');
if (CODE_FENCE_RE.test(fenceText)) s += 0.25;
CODE_FENCE_RE.lastIndex = 0;
if (typeof rec.candidateRef === 'string' && rec.candidateRef.length > 0) s += 0.1;
return Math.min(1, roundTo(s, 4));
}
function scoreEvidence(rec) {
const text = [rec.what, rec.why, rec.fix, rec.verify]
.filter((x) => typeof x === 'string').join('\n');
if (!text) return 0;
const counts = (text.match(COUNT_WORDS_RE) ?? []).length;
COUNT_WORDS_RE.lastIndex = 0;
const units = (text.match(UNIT_RE) ?? []).length;
UNIT_RE.lastIndex = 0;
const filelines = (text.match(FILE_LINE_RE) ?? []).length;
FILE_LINE_RE.lastIndex = 0;
// file:line is the gold standard.
let s = Math.min(0.5, filelines * 0.2)
+ Math.min(0.3, units * 0.075)
+ Math.min(0.2, counts * 0.05);
return Math.min(1, roundTo(s, 4));
}
// No findingRefs/code fences possible — grade structural tie to gate + signal-quoting.
function scoreGroundingAccount(rec) {
let s = 0;
if (typeof rec.candidateRef === 'string' && rec.candidateRef.startsWith('platform_')) s += 0.4;
else if (typeof rec.candidateRef === 'string' && rec.candidateRef.length > 0) s += 0.2;
// Quoting deep-dive data in why/fix is the account-scope equivalent of citing file:line.
const text = [rec.why, rec.fix, rec.verify].filter((x) => typeof x === 'string').join('\n');
const units = (text.match(UNIT_RE) ?? []).length;
UNIT_RE.lastIndex = 0;
if (units >= 3) s += 0.4;
else if (units >= 1) s += 0.2;
const citations = Array.isArray(rec.citations) ? rec.citations.length : 0;
if (citations >= 2) s += 0.2;
else if (citations >= 1) s += 0.1;
return Math.min(1, roundTo(s, 4));
}
// Heavily weighted toward magnitude quoting — vague platform recs should score low.
function scoreEvidenceAccount(rec) {
const text = [rec.what, rec.why, rec.fix, rec.verify]
.filter((x) => typeof x === 'string').join('\n');
if (!text) return 0;
const counts = (text.match(COUNT_WORDS_RE) ?? []).length;
COUNT_WORDS_RE.lastIndex = 0;
const units = (text.match(UNIT_RE) ?? []).length;
UNIT_RE.lastIndex = 0;
// Higher weight than route-scope variant — file:line gold standard isn't available.
let s = Math.min(0.55, units * 0.15) + Math.min(0.35, counts * 0.08);
if (typeof rec.o11ySignal === 'string' && rec.o11ySignal.length > 0) s += 0.1;
return Math.min(1, roundTo(s, 4));
}
function roundTo(n, d) {
const f = 10 ** d;
return Math.round(n * f) / f;
}
// 0.55 = Poor/Fair boundary. Recommending Poor-graded items erodes trust faster than the marginal recall benefit.
export function applyQualityFloor(recs, floor = 0.55) {
const kept = [];
const dropped = [];
for (const rec of recs) {
const o = rec?.quality?.overall ?? 0;
if (o < floor) dropped.push({ rec, reason: `quality.overall=${o} < floor=${floor}` });
else kept.push(rec);
}
return { kept, dropped };
}
lib/impact-label.mjs
import { impactMagnitude } from './impact-magnitude.mjs';
const SLOW_ROUTE_P95_THRESHOLD_MS = 500;
const CACHE_HIT_THRESHOLD_PCT = 50;
const COLD_START_THRESHOLD_PCT = 40;
const RELIABILITY_TARGET_PCT = 0.1;
export function computeImpactLabel(rec, signals = {}) {
const il = rec?.impactLabel ?? {};
if (il.performance) return il.performance;
if (il.costPhrase) return il.costPhrase;
if (typeof rec?.estimatedSavingsUsd === 'number' && rec.impactTier) {
const magnitude = impactMagnitude({
currentCost: rec.estimatedSavingsUsd,
impactTier: rec.impactTier,
});
return magnitude.phrase;
}
return synthesizeImpactFromSignal(rec, signals);
}
export function synthesizeImpactFromSignal(rec, signals = {}) {
const tier = rec?.impactTier;
const sig = [
rec?.o11ySignal,
rec?.evidence?.o11ySignal,
rec?.why,
rec?.what,
].filter((v) => typeof v === 'string' && v.trim()).join('\n') || null;
if (!sig || !tier) return null;
const m = String(sig);
const inv = parseSigNumber(m, /inv=([\d,]+)/);
const p95 = parseSigNumber(m, /p95=([\d,]+)ms/);
const cachePct = parseSigNumber(m, /cache=([\d.]+)%/);
const coldPct = parseSigNumber(m, /cold=([\d.]+)%/);
const buildSharePct = parseSigNumber(m, /build_minutes_share=([\d.]+)%/i) ??
parseSigNumber(m, /build(?: CPU)? minutes share:?\s*([\d.]+)%/i);
const errors = parseSigNumber(m, /errs=([\d,]+)/);
const errorRatePct = parseSigNumber(m, /rate=([\d.]+)%/);
const writes = parseSigNumber(m, /writes=([\d,]+)/);
const reads = parseSigNumber(m, /reads=([\d,]+)/);
const cwvIssues = [
cwvIssue('LCP', parseSigNumber(m, /LCP=([\d.]+)ms/i), 2500, 'ms'),
cwvIssue('INP', parseSigNumber(m, /INP=([\d.]+)ms/i), 200, 'ms'),
cwvIssue('CLS', parseSigNumber(m, /CLS=([\d.]+)/i), 0.1, ''),
].filter(Boolean);
if (cwvIssues.length > 0) {
return `${tier} impact — bring ${joinEnglish(cwvIssues.map(formatCwvIssue))}.`;
}
if (errors != null && errorRatePct != null) {
const reductionPct = Math.ceil(Math.max(0, (1 - (RELIABILITY_TARGET_PCT / errorRatePct)) * 100));
return `${tier} impact — cut 5xx rate by ~${reductionPct}% to get below ${RELIABILITY_TARGET_PCT}% (current ${errorRatePct}%, ${formatInteger(errors)} errors in this window).`;
}
if (errors != null) {
return `${tier} impact — resolve ${formatInteger(errors)} billed 5xx errors in this window.`;
}
if (cachePct != null && inv != null) {
if (cachePct < CACHE_HIT_THRESHOLD_PCT) {
return `${tier} impact — current cache hit rate is ${cachePct}% across ${formatInteger(inv)} requests in this window; the gate fires below ${CACHE_HIT_THRESHOLD_PCT}%.`;
}
if (p95 == null) {
return `${tier} impact — current cache hit rate is ${cachePct}% across ${formatInteger(inv)} requests in this window; this recommendation targets the remaining uncached traffic.`;
}
}
if (writes != null && reads != null) {
const ratio = reads > 0 ? writes / reads : null;
return ratio == null
? `${tier} impact — ${formatInteger(writes)} ISR write units with no recorded read units in this window.`
: `${tier} impact — ${formatInteger(writes)} ISR write units vs ${formatInteger(reads)} read units in this window (${round2(ratio)} writes per read).`;
}
if (p95 != null && inv != null) {
const multiple = p95 / SLOW_ROUTE_P95_THRESHOLD_MS;
return `${tier} impact — current 95th percentile duration is ${formatInteger(p95)}ms across ${formatInteger(inv)} function invocations in this window (${round1(multiple)}x the ${formatInteger(SLOW_ROUTE_P95_THRESHOLD_MS)}ms slow-route threshold).`;
}
if (coldPct != null) {
return `${tier} impact — current cold-start share is ${coldPct}%; the gate fires above ${COLD_START_THRESHOLD_PCT}%.`;
}
if (rec?.candidateRef?.startsWith('build_minutes_fanout:') || rec?.kind === 'build_minutes_fanout') {
return buildSharePct != null
? `${tier} impact — Build CPU Minutes account for ${buildSharePct}% of observed billed cost in this window.`
: `${tier} impact — Build CPU Minutes exceeded the gate threshold in this window.`;
}
return `${tier} impact — see follow-up metrics for magnitude.`;
}
function cwvIssue(metric, value, threshold, unit) {
if (value == null || value <= threshold) return null;
return { metric, value, threshold, unit };
}
function formatCwvIssue(i) {
const current = i.metric === 'CLS' ? round2(i.value) : Math.round(i.value);
if (i.unit === 'ms') {
return `${i.metric} below ${formatInteger(i.threshold)}ms (current ${formatInteger(current)}ms)`;
}
return `${i.metric} below ${i.threshold}${i.unit} (current ${current}${i.unit})`;
}
function joinEnglish(parts) {
if (parts.length <= 1) return parts[0] ?? '';
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
return `${parts.slice(0, -1).join(', ')}, and ${parts.at(-1)}`;
}
function parseSigNumber(s, re) {
const m = re.exec(s);
if (!m) return null;
const n = Number(String(m[1]).replace(/,/g, ''));
return Number.isFinite(n) ? n : null;
}
function round1(n) {
return Math.round(n * 10) / 10;
}
function round2(n) {
return Math.round(n * 100) / 100;
}
function formatInteger(n) {
if (!Number.isFinite(n)) return String(n);
return Math.round(n).toLocaleString('en-US');
}
lib/impact-magnitude.mjs
// Rule: precision for performance, magnitudes for cost. Customer-facing phrases never contain $N literals.
export function impactMagnitude({ currentCost, impactTier }) {
const fraction = { high: 0.4, medium: 0.2, low: 0.1 }[impactTier] ?? 0.2;
const estUsd = (currentCost ?? 0) * fraction;
if (estUsd < 5) {
return { magnitude: 'negligible', phrase: 'small cost impact at current traffic' };
}
if (estUsd < 50) {
return { magnitude: 'small', phrase: 'low-tens of dollars per month at current traffic' };
}
if (estUsd < 500) {
return { magnitude: 'medium', phrase: 'hundreds of dollars per month at current traffic' };
}
if (estUsd < 5000) {
return { magnitude: 'large', phrase: 'low-thousands of dollars per month at current traffic' };
}
return { magnitude: 'very-large', phrase: 'thousands+ of dollars per month at current traffic' };
}
// Preserves Postgres placeholders ($1, $2, …) — digits with no comma/period/k/m suffix.
export function stripDollarLiterals(text) {
if (!text || typeof text !== 'string') return { text, stripped: 0 };
let count = 0;
const cleaned = text.replace(
/\$[\d][\d.,]*(?:[kKmMbB])?(?:\/[\dA-Za-z]+)?/g,
(m) => {
if (/^\$\d+$/.test(m)) return m;
count++;
return 'the billed cost';
}
);
return { text: cleaned, stripped: count };
}
export function applyDollarStrip(rec) {
const fields = ['what', 'why', 'fix', 'impact', 'currentBehavior', 'desiredBehavior', 'before', 'after'];
let totalStripped = 0;
for (const f of fields) {
if (typeof rec[f] !== 'string') continue;
// Preserve code-fence content so example snippets aren't mangled.
const fences = [];
rec[f] = rec[f].replace(/```[\s\S]*?```/g, (m) => {
fences.push(m);
return `__FENCE_${fences.length - 1}__`;
});
const { text, stripped } = stripDollarLiterals(rec[f]);
rec[f] = text;
totalStripped += stripped;
rec[f] = rec[f].replace(/__FENCE_(\d+)__/g, (_, i) => fences[Number(i)]);
}
if (totalStripped > 0) {
rec.sanitizerTrail = rec.sanitizerTrail ?? [];
rec.sanitizerTrail.push(`$-strip:${totalStripped}`);
}
return rec;
}
lib/investigation-brief.mjs
// Sub-agent investigation brief — the entire prompt the sub-agent sees.
// Constraints: target ≤ 12 KB per brief; deterministic (same input → byte-identical output, modulo generatedAt).
import { isAbsolute, join, normalize, relative } from 'node:path';
import { loadLibrary, matchesFrameworkVersion } from './citations.mjs';
import { deriveProjectFacts } from './project-facts.mjs';
import { renderSupportTopics } from './support-topics.mjs';
const NON_LAYOUT_FILE_CAP = 12;
const LAYOUT_FILE_CAP = 3;
// Playbook is a tilt, not a requirement.
export function inferPlaybook(signals) {
const deps = signals?.stack?.deps ?? {};
const codebaseStack = signals?.codebase?.stack ?? {};
const routes = signals?.codebase?.routes ?? [];
const routePaths = routes.map((r) => r.routePath ?? '');
const services = Array.isArray(signals?.usage?.services) ? signals.usage.services : [];
const has = (name) => Boolean(deps[name]);
const hasPrefix = (prefix) => Object.keys(deps).some((k) => k === prefix || k.startsWith(prefix));
const anyRouteMatches = (re) => routePaths.some((p) => re.test(p));
const usageHas = (re, minBilled = 0) =>
services.some((s) => re.test(String(s?.name ?? '')) && Number(s?.billedCost ?? s?.cost ?? 0) > minBilled);
// AI app first — billing shape (AI Gateway > Sandbox > Function Duration) overrides
// the ecommerce/saas tilt when both apply (an "AI shopping assistant" lives in ai-application's
// priority patterns, not the cart-checkout ones).
const aiDep =
has('@vercel/sandbox')
|| has('@vercel/ai-gateway')
|| has('ai')
|| has('openai')
|| has('@anthropic-ai/sdk')
|| hasPrefix('@ai-sdk/');
const aiUsage = usageHas(/^AI Gateway$/i) || usageHas(/^Sandbox/i);
if (aiDep || aiUsage) {
return 'ai-application';
}
if (has('stripe') || has('@stripe/stripe-js') || has('react-stripe-js') ||
anyRouteMatches(/^\/(cart|checkout|products?)\b/i)) {
return 'ecommerce';
}
if (has('next-auth') || has('@clerk/nextjs') || has('@workos-inc/authkit-nextjs') ||
anyRouteMatches(/^\/(admin|dashboard|settings|account|billing)\b/i)) {
return 'saas';
}
if (routes.length > 0 && routes.every((r) => /^\/api\//.test(r.routePath ?? ''))) {
return 'api-service';
}
if (codebaseStack.hasAppRouter || codebaseStack.hasPagesRouter) {
if (anyRouteMatches(/^\/(blog|docs|posts?|articles?|guides?)\b/i)) return 'content-site';
if (anyRouteMatches(/^\/\(?marketing\)?\b/i)) return 'marketing';
}
return null;
}
// SvelteKit/Nuxt/Astro have framework-shaped advice that doesn't fit the Next.js-flavored profile playbooks — both can ship together.
export function inferFrameworkPlaybook(signals) {
const stack = signals?.stack ?? signals?.codebase?.stack ?? {};
switch (stack.framework) {
case 'sveltekit': return 'sveltekit';
default: return null;
}
}
// Empty result = no source files; investigate via evidence only (legitimate for platform_* candidates).
// Workspace imports expand one level deep — keeps brief small. A thin shell page.tsx delegates work; bottleneck usually lives in workspace files.
export function resolveFiles(candidate, signals) {
const route = candidate.route;
const routes = signals?.codebase?.routes ?? [];
if (Array.isArray(candidate.files) && candidate.files.length > 0) {
return capBriefFiles(candidate.files, route ? closestAncestorLayoutFiles(route, routes) : [], routes);
}
if (!route) return [];
const nonLayoutRoutes = routes.filter((r) => r.type !== 'layout');
const layoutFiles = closestAncestorLayoutFiles(route, routes);
let matched = nonLayoutRoutes.filter((r) => r.routePath === route);
if (matched.length === 0) {
// Fuzzy: prefer max literal-segment matches so `/event/[code]/teaser` beats `/event/[code]/[location]` when candidate is `/event/[*]/teaser`.
const scored = nonLayoutRoutes
.map((r) => ({ r, score: routePathMatchScore(r.routePath, route) }))
.filter((x) => x.score > 0);
if (scored.length === 0) return capBriefFiles([], layoutFiles, routes);
const top = Math.max(...scored.map((s) => s.score));
matched = scored.filter((s) => s.score === top).map((s) => s.r);
}
const direct = matched.map((r) => r.file).filter(Boolean);
const workspaceImports = matched
.flatMap((r) => Array.isArray(r.workspaceImports) ? r.workspaceImports : [])
.filter(Boolean);
return capBriefFiles(uniq([...direct, ...workspaceImports]), layoutFiles, routes);
}
// literal-segment match × 10, dynamic × 1, pure equality = sentinel that always wins.
export function routePathMatchScore(routePath, metricPath) {
if (typeof routePath !== 'string' || typeof metricPath !== 'string') return 0;
if (routePath === metricPath) return 1000 + routePath.split('/').filter(Boolean).length;
const rTokens = routePath.split('/').filter(Boolean);
const mTokens = metricPath.split('/').filter(Boolean);
let ri = 0, mi = 0, literals = 0, dynamicMatches = 0;
while (ri < rTokens.length && mi < mTokens.length) {
const r = rTokens[ri];
const m = mTokens[mi];
if (isCatchAllPlaceholder(r)) return 1 + literals * 10 + dynamicMatches;
if (r === m) { literals++; ri++; mi++; continue; }
// Route patterns may match concrete metric paths, and route/metric dynamic
// placeholders may match each other. A metric-side placeholder must not
// match a static route literal: that would let `/docs/[...slug]` traffic
// attach to an unrelated static scanner route like `/docs/llms.txt`.
if (isDynamicPlaceholder(r) && !isCatchAllPlaceholder(m)) { dynamicMatches++; ri++; mi++; continue; }
return 0;
}
if (ri === rTokens.length - 1 && /^\[\[\.\.\..+\]\]$/.test(rTokens[ri]) && mi === mTokens.length) {
return 1 + literals * 10 + dynamicMatches;
}
if (ri !== rTokens.length || mi !== mTokens.length) {
return trailingSingleDynamicPartialScore(rTokens, mTokens, ri, mi, literals, dynamicMatches);
}
return 1 + literals * 10 + dynamicMatches;
}
export function routePathsMatch(routePath, metricPath) {
return routePathMatchScore(routePath, metricPath) > 0;
}
function isDynamicPlaceholder(token) {
return /^\[.*\]$/.test(token);
}
function isSingleDynamicPlaceholder(token) {
return /^\[[^[.\].][^\]]*\]$/.test(token);
}
function isCatchAllPlaceholder(token) {
return /^\[(?:\[\.\.\..+\]|\.\.\..+)\]$/.test(token) || /^\[\.\.\..+\]$/.test(token) || /^\[\[\.\.\..+\]\]$/.test(token);
}
function trailingSingleDynamicPartialScore(rTokens, mTokens, ri, mi, literals, dynamicMatches) {
const rRemaining = rTokens.length - ri;
const mRemaining = mTokens.length - mi;
if (Math.abs(rRemaining - mRemaining) !== 1) return 0;
if (rRemaining !== 0 && mRemaining !== 0) return 0;
const lastRouteToken = rTokens[ri - 1];
const lastMetricToken = mTokens[mi - 1];
if (!isSingleDynamicPlaceholder(lastRouteToken) && !isSingleDynamicPlaceholder(lastMetricToken)) return 0;
return literals * 10 + dynamicMatches;
}
function uniq(xs) { return Array.from(new Set(xs)); }
function briefRoots(signals) {
const codebase = signals?.codebase ?? {};
const appRoot = typeof codebase.rootDir === 'string' && codebase.rootDir.length > 0
? normalize(codebase.rootDir)
: null;
const repoRoot = typeof codebase.monorepoRoot === 'string' && codebase.monorepoRoot.length > 0
? normalize(codebase.monorepoRoot)
: appRoot;
return { appRoot, repoRoot };
}
function absoluteBriefPath(file, roots) {
if (typeof file !== 'string' || file.length === 0) return null;
if (isAbsolute(file)) return normalize(file);
const base = isRepoRelativePath(file) ? roots.repoRoot : roots.appRoot;
return base ? normalize(join(base, file)) : null;
}
function repoRelativeBriefPath(file, roots) {
if (typeof file !== 'string' || file.length === 0) return null;
const normalized = normalize(file);
if (isRepoRelativePath(normalized)) return normalized;
const abs = absoluteBriefPath(file, roots);
if (!abs || !roots.repoRoot) return normalized;
const rel = normalize(relative(roots.repoRoot, abs));
return rel.startsWith('..') ? normalized : rel;
}
function isRepoRelativePath(file) {
return /^(apps|packages)\//.test(file);
}
function capBriefFiles(nonLayoutCandidates, layoutCandidates, routes) {
const knownLayoutFiles = new Set(routes.filter((r) => r.type === 'layout').map((r) => r.file).filter(Boolean));
const nonLayout = [];
const layouts = [];
for (const f of uniq(nonLayoutCandidates)) {
if (knownLayoutFiles.has(f) || isLayoutPath(f)) layouts.push(f);
else nonLayout.push(f);
}
for (const f of layoutCandidates) layouts.push(f);
return [
...uniq(nonLayout).slice(0, NON_LAYOUT_FILE_CAP),
...uniq(layouts).slice(0, LAYOUT_FILE_CAP),
];
}
function closestAncestorLayoutFiles(route, routes) {
if (!route) return [];
return routes
.filter((r) => r.type === 'layout' && r.file && layoutAppliesToRoute(r.routePath, route))
.sort((a, b) =>
routeDepth(b.routePath) - routeDepth(a.routePath)
|| a.file.localeCompare(b.file)
)
.map((r) => r.file);
}
function layoutAppliesToRoute(layoutPath, routePath) {
if (typeof layoutPath !== 'string' || typeof routePath !== 'string') return false;
if (layoutPath === '/') return true;
const layoutTokens = layoutPath.split('/').filter(Boolean);
const routeTokens = routePath.split('/').filter(Boolean);
if (layoutTokens.length > routeTokens.length) return false;
for (let i = 0; i < layoutTokens.length; i++) {
const l = layoutTokens[i];
const r = routeTokens[i];
if (isCatchAllPlaceholder(l)) return true;
if (l === r) continue;
if (isDynamicPlaceholder(l) || isDynamicPlaceholder(r)) continue;
return false;
}
return true;
}
function routeDepth(routePath) {
return String(routePath ?? '').split('/').filter(Boolean).length;
}
function isLayoutPath(file) {
return /(^|\/)(?:\+layout(?:\.server)?|layout)\.(?:svelte|tsx?|jsx?)$/.test(String(file ?? ''));
}
// Tells the sub-agent which signals are missing so it doesn't conflate "no data" with "no bottleneck."
export function summarizeDeepDiveFailures(deepDive) {
if (!deepDive || typeof deepDive !== 'object') return null;
const entries = Object.entries(deepDive);
if (entries.length === 0) return null;
const failures = entries.filter(([, v]) => isFailureEntry(v));
if (failures.length === 0) return null;
// Surface when ≥50% failed OR ≥3 distinct signals failed.
if (failures.length / entries.length < 0.5 && failures.length < 3) return null;
const failedIds = failures.map(([k]) => k).slice(0, 6).join(', ');
const codes = uniq(failures.map(([, v]) => v?.code ?? v?.error ?? 'unknown')).slice(0, 3).join(' / ');
return `${failures.length} of ${entries.length} deep-dive signals failed (${failedIds}${failures.length > 6 ? ', …' : ''}) — error: ${codes}.`;
}
function isFailureEntry(v) {
if (!v || typeof v !== 'object') return false;
if (v.ok === false) return true;
if (typeof v.code === 'string' && v.code !== 'OK') return true;
if (typeof v.error === 'string' && v.error.length > 0) return true;
return false;
}
export async function citationSubset(candidateKind, framework, version) {
const lib = await loadLibrary();
const versionOk = (entry) =>
entry.applicableFrameworks.includes('*') ||
entry.applicableFrameworks.some((p) => matchesFrameworkVersion(p, framework, version));
const kindOk = (entry) => {
const at = Array.isArray(entry.appliesTo) ? entry.appliesTo : [];
return at.length === 0 || at.includes(candidateKind);
};
return {
urls: lib.urls.filter((e) => versionOk(e) && kindOk(e)),
ruleSkillRefs: lib.ruleSkillRefs.filter((r) => versionOk(r) && kindOk(r)),
};
}
// Per-kind hints tell the investigator which comparison to draw first.
export const KIND_INTERPRETATION_HINTS = {
slow_route: [
'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).',
'Compare `ttfb.p95` vs `latency.p95`. If ttfb ≈ latency, response generation finishes near the end — streaming or `after()` may shift perceived latency.',
'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.',
'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.',
'Inspect `startTypeSplit.cold` share. >5% cold means cold starts contribute meaningfully — Fluid Compute or warmer keep-alive is on the table.',
'Inspect `statusDistribution`. A non-trivial 3xx/4xx slice may be inflating p95 because redirects/auth bounces still count as invocations.',
'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.',
],
uncached_route: [
'`cacheBreakdown` tells you what fraction is BYPASS vs HIT vs MISS. BYPASS without explicit `Cache-Control` directives in the response is the canonical fix.',
'`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.',
'`botShare` (bandwidth by bot_category): if bots dominate uncached bandwidth, the right rec may be Bot Protection rather than route caching.',
'`bandwidthByCache`: pair with cacheBreakdown to confirm the dollar/bandwidth impact of moving uncached → cached.',
'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.',
],
cold_start: [
'`startTypeSplit`: cold vs hot vs prewarmed. Fluid Compute meaningfully helps when cold > 5%.',
'`coldVsWarmLatencyP95`: how much SLOWER is cold than warm. If 5x+, cold starts are amplifying tail latency, not just adding fixed overhead.',
'`coldByDeployment`: if cold-start cluster around the newest deployment, the slowdown is a regression — check imports, init code, framework upgrade.',
],
route_errors: [
'`errorStatusPattern`: distinguishes 500 (app crash) vs 502/503 (gateway/upstream timeout) vs 504 (downstream timeout).',
'`errorCodes`: a non-empty error_code dimension narrows to a specific failure class (e.g., FUNCTION_INVOCATION_TIMEOUT).',
'`errorsByDeployment`: a deployment-localized spike points to a regression.',
],
external_api_slow: [
'`latency.p95` vs `latency.p99`: spreads point to flaky upstream; narrow gap points to slow-by-design.',
'`callersByRoute` (`origin_route` dim): which of OUR routes call this upstream — that\'s where the rec should land.',
'`transferBytes`: large payloads suggest caching or partial-response opportunities at our edge.',
],
isr_overrevalidation: [
'`writePattern` (write_units by cache_result) — STALE writes vs HIT writes. STALE-write means the revalidate ran on every stale request.',
'`readPattern` (read_units by cache_result) — HIT vs MISS. Low MISS means cache fills are not the issue.',
'If writes / reads > 0.5, the revalidate cadence is too aggressive; lengthen `revalidate` or switch to on-demand `revalidateTag`.',
],
cwv_poor: [
'`lcp`/`inp`/`cls` percentiles. p75 > Web Vitals "Good" threshold is the bar.',
'LCP > 2500ms → server response or critical image. INP > 200ms → long tasks / heavy JS on interaction. CLS > 0.1 → layout shift, usually images/ads/fonts.',
],
middleware_heavy: [
'`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.',
],
platform_fluid_compute: [
'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.',
],
platform_bot_protection: [
'`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.',
],
};
export function buildBrief({
candidate,
candidateIndex,
candidateGroup,
files,
signals,
citations,
playbookId,
playbookBody,
frameworkPlaybookId,
frameworkPlaybookBody,
supportTopics = [],
generatedAt,
}) {
const stack = signals?.stack ?? signals?.codebase?.stack ?? {};
const framework = stack.framework ?? 'unknown';
const version = stack.frameworkVersion ?? 'unknown';
const kind = candidate.kind;
const routeOrHost = candidate.route ?? candidate.hostname ?? null;
const interp = KIND_INTERPRETATION_HINTS[kind] ?? [];
const candidateRef = candidate.candidateRef ?? `${kind}:${routeOrHost ?? '<account>'}`;
const roots = briefRoots(signals);
const lines = [];
lines.push(`# Investigation brief — ${kind}${routeOrHost ? ` — ${routeOrHost}` : ''}`);
lines.push('');
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.');
lines.push('');
lines.push(`Brief id: \`${candidateGroup}#${candidateIndex}\` · candidateRef: \`${candidateRef}\``);
if (generatedAt) lines.push(`Generated: ${generatedAt}`);
lines.push('');
lines.push('## Candidate');
lines.push('');
lines.push(`- **Kind:** \`${kind}\``);
lines.push(`- **Scope:** ${candidate.scope ?? 'route'}`);
if (routeOrHost) lines.push(`- **Target:** \`${routeOrHost}\``);
if (roots.repoRoot) lines.push(`- **Repo root:** \`${roots.repoRoot}\``);
if (roots.appRoot) lines.push(`- **App root:** \`${roots.appRoot}\``);
if (candidate.o11ySignal) lines.push(`- **o11y signal at gate-time:** \`${candidate.o11ySignal}\``);
lines.push(`- **Confidence:** ${candidate.confidence ?? 'n/a'}`);
lines.push(`- **Priority:** ${candidate.priority ?? 'n/a'}`);
if (candidate.disqualified) {
lines.push(`- **⚠ Disqualifier present:** ${candidate.disqualifyReason ?? 'disqualified'}`);
}
lines.push('');
lines.push(`**Gate question (the hypothesis you're verifying):** ${candidate.question ?? '(no question)'}`);
lines.push('');
if (Array.isArray(files) && files.length > 0) {
lines.push('**Files you may read (read ONLY these — open each one directly, NOT a repo-wide grep):**');
lines.push(`_Capped at ${NON_LAYOUT_FILE_CAP} non-layout files + up to ${LAYOUT_FILE_CAP} layouts._`);
// Tag route vs workspace-import — workspace files are usually where the bottleneck lives.
const routes = signals?.codebase?.routes ?? [];
const routeScores = routes.filter((r) => r.type !== 'layout').map((r) => ({
r,
score: routePathMatchScore(r.routePath, routeOrHost),
})).filter((x) => x.score > 0);
const topScore = routeScores.length > 0 ? Math.max(...routeScores.map((x) => x.score)) : 0;
const routeFiles = new Set(
routeScores.filter((x) => x.score === topScore).map((x) => x.r.file).filter(Boolean)
);
const layoutFiles = new Set(closestAncestorLayoutFiles(routeOrHost, routes));
const workspaceImportFiles = [];
for (const f of files) {
const tag = layoutFiles.has(f) || isLayoutPath(f)
? '(layout)'
: routeFiles.has(f) ? '(route)' : '(workspace import)';
if (tag === '(workspace import)') workspaceImportFiles.push(f);
const repoRel = repoRelativeBriefPath(f, roots) ?? f;
const abs = absoluteBriefPath(f, roots);
const sourceSuffix = repoRel !== f ? ` (scan path: \`${f}\`)` : '';
const absSuffix = abs && abs !== repoRel ? ` — open \`${abs}\`` : '';
lines.push(`- \`${repoRel}\` ${tag}${sourceSuffix}${absSuffix}`);
}
if ([...routeFiles].length > 0 && workspaceImportFiles.length > 0) {
lines.push('');
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._');
}
} else {
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.');
}
lines.push('');
lines.push('## Stack context');
lines.push('');
lines.push(`- **Framework:** \`${framework}@${version}\``);
if (stack.hasAppRouter) lines.push('- **Router:** App Router');
if (stack.hasPagesRouter) lines.push('- **Router:** Pages Router');
if (stack.orm && stack.orm !== 'none') lines.push(`- **ORM:** ${stack.orm}`);
if (stack.isMonorepo) lines.push('- **Monorepo:** yes (watch for cross-package effects)');
lines.push('');
// Negative-space filter: sub-agent must not recommend toggling on something already on.
const projectFacts = deriveProjectFacts(signals);
if (projectFacts.length > 0) {
lines.push('## Project config (already on — do NOT recommend toggling)');
lines.push('');
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.');
lines.push('');
for (const f of projectFacts) lines.push(`- ${f.briefLine}`);
lines.push('');
}
lines.push('## Deep-dive evidence (already collected — do NOT re-query)');
lines.push('');
const deepDive = candidate?.evidence?.deepDive ?? {};
const failureNotice = summarizeDeepDiveFailures(deepDive);
if (failureNotice) {
lines.push(`> ⚠ **Deep-dive partly incomplete.** ${failureNotice}`);
lines.push('>');
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.`);
lines.push('');
}
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.');
lines.push('');
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.');
lines.push('');
lines.push('```json');
lines.push(JSON.stringify(deepDive, null, 2));
lines.push('```');
lines.push('');
if (interp.length > 0) {
lines.push('**How to read the evidence for this candidate kind:**');
lines.push('');
for (const h of interp) lines.push(`- ${h}`);
lines.push('');
}
const cachePolicyHints = cachePolicyGuidance(kind, stack);
if (cachePolicyHints.length > 0) {
lines.push('## Cache-policy decision');
lines.push('');
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.');
lines.push('');
for (const h of cachePolicyHints) lines.push(`- ${h}`);
lines.push('');
}
lines.push(...renderSupportTopics(supportTopics));
if (supportTopics.length > 0) lines.push('');
lines.push('## Citation library (USE ONLY THESE)');
lines.push('');
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\`.`);
lines.push('');
lines.push('### URLs');
if (citations.urls.length === 0) {
lines.push('_(no URLs match this kind + version — investigate, but the rec may fail `missing-citation`; consider abstaining)_');
} else {
for (const e of citations.urls) {
lines.push(`- \`${e.url}\` — ${e.topic}`);
}
}
lines.push('');
lines.push('### Skill-rule references');
if (citations.ruleSkillRefs.length === 0) {
lines.push('_(none applicable)_');
} else {
for (const r of citations.ruleSkillRefs) {
lines.push(`- \`${r.skill}:${r.rule}\` — ${r.topic}`);
}
}
lines.push('');
if (playbookId && playbookBody) {
lines.push(`## Playbook hint (\`${playbookId}\`)`);
lines.push('');
lines.push(playbookBody.trim());
lines.push('');
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._');
lines.push('');
}
if (frameworkPlaybookId && frameworkPlaybookBody) {
lines.push(`## Framework-specific playbook (\`${frameworkPlaybookId}\`)`);
lines.push('');
lines.push(frameworkPlaybookBody.trim());
lines.push('');
lines.push(`_Framework-shaped advice for ${framework}. Same rule: evidence-grounded only._`);
lines.push('');
}
lines.push('## Two valid outcomes');
lines.push('');
lines.push('Your job is to answer the gate question above. There are exactly two valid outcomes:');
lines.push('');
lines.push('**A. Emit a recommendation** (schema below) — ONLY when you found a verifiable file:line cause that the deep-dive evidence supports.');
lines.push('');
lines.push('**B. Abstain** — when the gate\'s hypothesis does not survive contact with the source. Emit:');
lines.push('```json');
lines.push(`{"abstain": true, "candidateRef": "${candidateRef}", "reason": "<one-sentence explanation grounded in what you found vs what the gate assumed>"}`);
lines.push('```');
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.');
lines.push('');
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:');
lines.push('```json');
lines.push(`{
"abstain": true,
"candidateRef": "${candidateRef}",
"reason": "<why you abstained from a perf rec>",
"observation": {
"summary": "<one-line headline — what you noticed>",
"evidence": "<the deep-dive datum or file:line that backs it>",
"suggestedAction": "<what the customer should do next>",
"kind": "regression | error_storm | config_gap | upstream_dependency | other"
}
}`);
lines.push('```');
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.');
lines.push('');
lines.push('## Investigation protocol');
lines.push('');
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.');
lines.push('2. Read each file, then run targeted `grep` / `ast-grep` inside it to count patterns. Verify line numbers exactly.');
lines.push('3. Follow imports within the chain only when relevant to the gate question (one level deep max).');
lines.push('4. Stop after 5 files exhausted, or when you have a verified root cause.');
lines.push('5. Drop findings that fail mechanical verification (file missing, pattern not present, etc.).');
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.');
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."');
lines.push('');
lines.push('## Pre-emit self-check');
lines.push('');
lines.push('Before emitting a recommendation (Outcome A), verify ALL of:');
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.');
lines.push('- `why` quotes at least one specific `file:line` AND at least one deep-dive datum (e.g. `ttfb.p95=576ms`).');
lines.push('- Every citation appears in the library above. No invented URLs.');
lines.push('- `currentBehavior` snippet appears in the actual file (not a paraphrase).');
lines.push('- No `$N` dollar literals in any customer-facing field.');
lines.push('');
lines.push('If ANY of these fails, fix the rec OR switch to Outcome B (abstain).');
lines.push('');
lines.push('## Required output (one JSON object, no prose around it)');
lines.push('');
lines.push('```json');
lines.push(`{
"what": "...", // 1 line, verb-first, scope-explicit. NO "$N" literals.
"why": "...", // 1-2 sentences. MUST cite ≥1 file:line AND ≥1 deep-dive datum (e.g. "ttfb.p95=576ms while cpu.p95=117ms").
"fix": "...", // step-by-step. Reference the specific files.
"bucket": "performance", // "cost" | "performance" | "reliability"
"effort": "medium", // "low" | "medium" | "high"
"affectedFiles": ["..."], // repo-relative paths from the Files list above
"currentBehavior": "\`\`\`ts\\n...current snippet...\\n\`\`\`",
"desiredBehavior": "\`\`\`ts\\n...target snippet...\\n\`\`\`",
"verify": "Re-run \`vercel metrics ...\` and watch the named metric.",
"citations": ["<url-from-library>", "skill:rule"],
"candidateRef": "${candidateRef}",
"findingRefs": ["src/.../file.ts:42"],
"impactTier": "high", // "high" | "medium" | "low"
"billingDimension": "function-duration" // see references/recommendations.md schema
}`);
lines.push('```');
lines.push('');
lines.push('## Critical rules');
lines.push('');
lines.push('Ordered by priority — top is most important.');
lines.push('');
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.');
lines.push(`2. **No invented citations.** Only URLs and refs from the library above. The \`unknown-citation\` sanitizer strips anything else.`);
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.`);
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.`);
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).');
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`.');
lines.push('');
return lines.join('\n');
}
function cachePolicyGuidance(kind, stack = {}) {
if (!['uncached_route', 'cache_header_gap'].includes(kind)) return [];
const framework = stack.framework ?? 'unknown';
const cacheComponents = stack.cacheComponents === true;
const hints = [
'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.',
'Fallback, 404, auth, preview, webhook, mutation, and per-user branches: keep them uncached or short-lived while caching only the safe success branch.',
];
if (framework === 'next') {
if (cacheComponents) {
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.');
} else {
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.');
}
}
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.');
return hints;
}
lib/observation-safety.mjs
export function splitCustomerSafeObservations(observations, abstentions = [], signals = {}) {
const safe = [];
const heldBack = [];
for (const observation of Array.isArray(observations) ? observations : []) {
const unsafeReason = unsupportedObservationReason(observation, abstentions, signals);
if (unsafeReason) {
heldBack.push({
candidateRef: observation?.candidateRef ?? null,
reason: unsafeReason,
needsEvidence: true,
});
} else {
safe.push(observation);
}
}
return { observations: safe, heldBackObservations: heldBack };
}
function unsupportedObservationReason(observation, abstentions = [], signals = {}) {
if (contradictsNoChangeReason(observation, abstentions)) {
return 'This observation repeated an action that another investigation rejected. Re-run with a single scoped candidate before applying it.';
}
if (hasUnsupportedWafBotCategoryClaim(observation)) {
return 'This observation described an Observability bot category as a WAF-rule condition without supported rule evidence. Re-run with documented WAF condition evidence before applying it.';
}
if (hasUnsafeBotProtectionObservation(observation)) {
return 'This observation recommended Bot Protection or WAF changes without a staged safe-rollout plan and allowlist review. Promote it to a verified platform recommendation before applying it.';
}
if (hasStaleNextCacheApiObservation(observation, signals)) {
return 'This observation used a cache API that does not match the detected framework-version evidence. Re-run with the current Next.js cache evidence before applying it.';
}
if (hasUnsupportedFrameworkCausalClaim(observation)) {
return 'This observation made a framework-specific cause claim that verification could not support. Re-run with runtime logs or official framework evidence before applying it.';
}
if (hasUnsupportedStaticGenerationClaim(observation)) {
return 'This observation made a static-generation behavior claim that verification could not support. Re-run with route-manifest or runtime evidence before applying it.';
}
if (hasUnsupportedSourceAbsenceClaim(observation)) {
return 'This observation made a source-file absence claim that verification could not support. Re-run with a file-existence check or runtime logs before applying it.';
}
if (hasUnsupportedCacheLifeCdnClaim(observation)) {
return 'This observation depended on an unsupported cacheLife-to-CDN claim. Re-run with production header evidence before applying it.';
}
if (hasUnsupportedRuntimeRootCauseClaim(observation)) {
return 'This observation made a runtime root-cause claim that needs log, stack, or upstream response evidence before it can ship.';
}
if (hasImplementationGradeObservationAction(observation)) {
return 'This observation described an implementation change that needs the ready-to-apply recommendation evidence bar before it can ship.';
}
return null;
}
function observationText(observation) {
return [
observation?.summary,
observation?.evidence,
observation?.suggestedAction,
].filter(Boolean).join(' ');
}
function evidenceText(observation) {
return [
observation?.summary,
observation?.evidence,
].filter(Boolean).join(' ');
}
function hasUnsupportedWafBotCategoryClaim(observation) {
const text = observationText(observation);
if (!/\bWAF\b/i.test(text)) return false;
return /\bbot_category\s*=/i.test(text) ||
/\btarget(?:ing)?\s+(?:browser_impersonation|automated_browser|ecommerce|monitor)\b/i.test(text);
}
function hasUnsafeBotProtectionObservation(observation) {
const text = observationText(observation);
if (!/\b(?:Bot Protection|BotID|bot_filter|WAF|managed bot rules?)\b/i.test(text)) return false;
const recommendsAction = /\b(?:enable|add|create|configure|challenge|deny|block|rate-limit|target)\b/i.test(String(observation?.suggestedAction ?? ''));
if (!recommendsAction) return false;
const hasSafeRollout = /\b(?:staged|log mode|log action|dry run)\b/i.test(text);
const hasAllowlist = /\ballowlist|exclusions?\b/i.test(text);
return !(hasSafeRollout && hasAllowlist);
}
function hasStaleNextCacheApiObservation(observation, signals = {}) {
const text = observationText(observation);
if (!/\bunstable_cache\b/.test(text)) return false;
if (signals?.stack?.framework !== 'next') return false;
const major = parseInt(String(signals?.stack?.frameworkVersion ?? '').match(/\d+/)?.[0] ?? '', 10);
return Number.isFinite(major) && major >= 16;
}
function hasImplementationGradeObservationAction(observation) {
const action = String(observation?.suggestedAction ?? '');
if (action.trim() === '') return false;
if (/\b(?:use cache:\s*remote|unstable_cache|Cache-Control|s-maxage|cacheLife|export const revalidate|checkBotId|BotID)\b/i.test(action)) return true;
return /\b(?:enable|add|wrap|apply|move|parallelize|set|create|configure|deny|challenge|block|fix|replace|refactor|rewrite|upgrade|downgrade)\b/i.test(action) ||
/\bcache\s+(?:the|this|that|shared|public|origin|response|route|data|lookup|fetch|helper)\b/i.test(action) ||
/\bturn\s+(?:on|off)\b/i.test(action) ||
/\bswitch\s+(?:to|from|the|this)\b/i.test(action) ||
/\buse\s+Promise\.all\b/i.test(action) ||
/\b(?:raise|lower|increase|decrease)\s+(?:the\s+)?(?:TTL|timeout|memory|CPU|cache|cache lifetime|duration)\b/i.test(action);
}
function contradictsNoChangeReason(observation, abstentions) {
const target = candidateTarget(observation?.candidateRef);
if (!target) return false;
const lowerObservationText = observationText(observation).toLowerCase();
const relevantReasons = abstentions
.filter((a) => candidateTarget(a?.candidateRef) === target)
.map((a) => String(a?.reason ?? '').toLowerCase());
if (relevantReasons.length === 0) return false;
if (/\bparalleliz(?:e|ing)\b/.test(lowerObservationText) &&
/\bgetsession\b/.test(lowerObservationText) &&
relevantReasons.some((reason) => /\bgetsession\b/.test(reason) && /\b(?:gates?|redirect|auth-preserving|blocked)\b/.test(reason))) {
return true;
}
return false;
}
function candidateTarget(ref) {
if (typeof ref !== 'string') return null;
const idx = ref.indexOf(':');
if (idx === -1) return null;
return ref.slice(idx + 1);
}
function hasUnsupportedFrameworkCausalClaim(observation) {
const text = observationText(observation).toLowerCase();
if (!text.includes('notfound') || !text.includes('use cache')) return false;
return (
/known next\.js cache components edge case/.test(text) ||
/next\.js\s+\d+(?:\.\d+)?[^.]{0,120}treats[^.]{0,120}dynamic api/.test(text) ||
/can surface as 5xx/.test(text) ||
/surface as 500/.test(text) ||
/instead of throwing inside (?:the )?cache/.test(text) ||
/cache boundary/.test(text)
);
}
function hasUnsupportedStaticGenerationClaim(observation) {
const text = observationText(observation).toLowerCase();
if (!/\bgeneratestaticparams\b/.test(text)) return false;
return /\b(?:returns?\s*(?:an\s+)?empty|\[\])\b[^.\n]{0,240}\b(?:every request|on[- ]demand|no params? (?:are )?prebuilt|populate generatestaticparams|served from (?:the )?cdn|hit bucket|cachebreakdown)\b/i.test(text) ||
/\b(?:every request|on[- ]demand|no params? (?:are )?prebuilt|populate generatestaticparams|served from (?:the )?cdn|hit bucket|cachebreakdown)\b[^.\n]{0,240}\b(?:returns?\s*(?:an\s+)?empty|\[\])\b/i.test(text) ||
/\bdynamic\s*=\s*['"`]error['"`]\b[^.\n]{0,240}\b(?:generatestaticparams|dynamicparams|every request|on[- ]demand)\b/i.test(text);
}
function hasUnsupportedSourceAbsenceClaim(observation) {
const ref = String(observation?.candidateRef ?? '');
if (!ref.startsWith('route_errors:')) return false;
const text = observationText(observation).toLowerCase();
return /\b(?:enoent|no\s+(?:matching|corresponding)\s+(?:mdx|file|post)|missing\s+(?:mdx|file|post)|does\s+not\s+exist|not\s+found\s+on\s+disk)\b/.test(text);
}
function hasUnsupportedCacheLifeCdnClaim(observation) {
return hasUnsupportedCacheLifeCdnText(observationText(observation));
}
export function hasUnsupportedCacheLifeCdnText(text) {
if (typeof text !== 'string' || !/\bcacheLife\b/i.test(text)) return false;
if (/\btoLaunch-\d+\b/i.test(text)) return true;
return /\bcacheLife\b[^.\n]{0,240}\b(?:Cache-Control|s-maxage|CDN|edge cache|cache breakdown|x-vercel-cache|HIT|MISS|function (?:still )?runs per request|every request invokes the function|canonical|toLaunch-\d+)\b/i.test(text) ||
/\b(?:Cache-Control|s-maxage|CDN|edge cache|cache breakdown|x-vercel-cache|HIT|MISS|function (?:still )?runs per request|every request invokes the function|canonical|toLaunch-\d+)\b[^.\n]{0,240}\bcacheLife\b/i.test(text) ||
/\b(?:no|never|without|missing)\s+cacheLife\b[^.\n]{0,240}\b(?:no|not|never|0%|every|per request|function)\b[^.\n]{0,120}\b(?:cache|cached|hit|runs?|invoke)/i.test(text);
}
function hasUnsupportedRuntimeRootCauseClaim(observation) {
const text = observationText(observation);
if (!/\b(?:caused by|root cause|responsible for failures|would produce)\b/i.test(text)) return false;
if (!/\b(?:5xx|500|error|failures?)\b/i.test(text)) return false;
return !/\b(?:logs?\s+(?:show|confirm|include|contain)|stack\s+(?:shows|trace|evidence)|trace\s+(?:shows|confirms)|exception\s+(?:shows|confirms)|response body\s+(?:shows|confirms)|runtime evidence)\b/i.test(evidenceText(observation));
}
lib/project-facts.mjs
// Single source for "already on" project facts. Feeds report Strengths, sub-agent brief, and verifier contradiction check.
// contradictPhrases must lowercase exactly — verifier does case-insensitive substring match.
// Stable order = byte-identical brief output. Empty result when project config didn't load — don't pretend.
export function deriveProjectFacts(signals) {
const out = [];
const cfg = signals?.project?.defaultResourceConfig;
const projectErr = signals?.project?.error;
if (!cfg || projectErr) return out;
if (cfg.fluid === true) {
out.push({
id: 'fluid_compute',
strength: 'Fluid Compute is enabled (`defaultResourceConfig.fluid=true`) — instance reuse + reduced cold starts active.',
briefLine: 'Fluid Compute is ENABLED on this project (`defaultResourceConfig.fluid=true`). Do not recommend toggling it on.',
contradictPhrases: [
'enable fluid compute',
'enable fluid',
'turn on fluid compute',
'switch to fluid compute',
'migrate to fluid compute',
'opt in to fluid compute',
],
});
}
if (cfg.elasticConcurrencyEnabled === true) {
out.push({
id: 'in_function_concurrency',
strength: 'In-function concurrency is enabled — multiple invocations share a single function instance, lowering active CPU costs on I/O-bound work.',
briefLine: 'In-function concurrency is ENABLED. Do not recommend toggling it on.',
contradictPhrases: [
'enable in-function concurrency',
'enable elastic concurrency',
'turn on in-function concurrency',
'enable concurrent invocations',
],
});
}
if (cfg.functionDefaultMemoryType === 'standard') {
out.push({
id: 'memory_standard',
strength: 'Function memory tier: Standard (2GB) — the cost-efficient default; upgrade to Performance (4GB) only with memory, CPU-bound, or latency-sensitive route evidence.',
briefLine: 'Function memory tier is Standard (2GB), the cost-efficient default. Recommending an upgrade to Performance (4GB) requires memory, CPU-bound, or latency-sensitive route evidence.',
contradictPhrases: [],
});
} else if (cfg.functionDefaultMemoryType === 'performance') {
out.push({
id: 'memory_performance',
strength: 'Function memory tier: **Performance (4GB)** — verify this is intentional; Performance costs ~2x Standard. If your routes don\'t saturate Standard\'s memory headroom, downgrade.',
briefLine: 'Function memory tier is Performance (4GB). Do not recommend upgrading further — the next valid tier change is downgrading to Standard.',
contradictPhrases: [
'upgrade memory to performance',
'upgrade to performance memory',
'switch to performance memory',
'enable performance memory',
],
});
}
if (Array.isArray(cfg.functionDefaultRegions) && cfg.functionDefaultRegions.length > 0) {
const r = cfg.functionDefaultRegions;
out.push({
id: 'function_regions',
strength: `Function regions: \`${r.join(', ')}\` (${r.length === 1 ? 'single region' : 'multi-region'}).`,
briefLine: `Function regions configured: ${r.join(', ')}. If your rec hinges on region placement, it must accept this configuration as the starting point.`,
contradictPhrases: [],
});
}
if (cfg.functionZeroConfigFailover === true) {
out.push({
id: 'zero_config_failover',
strength: 'Function failover is enabled in project config.',
briefLine: 'Function failover is ENABLED in project config. Do not recommend enabling it.',
contradictPhrases: [
'enable zero-config failover',
'enable multi-region failover',
'turn on zero-config failover',
],
});
}
return out;
}
// `why` excluded — citing a fact as evidence ("fluid is on, so …") is legitimate, not contradiction.
export function findRecContradictions(rec, facts) {
if (!rec || typeof rec !== 'object') return [];
if (!Array.isArray(facts) || facts.length === 0) return [];
const haystack = [
rec.what,
rec.fix,
rec.desiredBehavior,
rec.currentBehavior,
]
.map((s) => (typeof s === 'string' ? s.toLowerCase() : ''))
.join('\n');
if (!haystack) return [];
return facts.filter((f) =>
(f.contradictPhrases ?? []).some((p) => haystack.includes(p.toLowerCase()))
);
}
lib/queries.mjs
// Declarative metric-query registry. Single source for every `vercel metrics ...` call.
//
// CLI default --since is 1h. Mixing 1h with 14d windows silently produces incompatible rollups — every query MUST pass since: TIME_WINDOW. test/time-window.test.mjs enforces this.
// 14d: long enough for weekly cycles, short enough to surface recent regressions before stale data dilutes them.
import { normalizeSummary } from './vercel.mjs';
export const TIME_WINDOW = '14d';
// CLI default cardinality cap is 10 — too small for a typical app.
const ROUTE_LIMIT = 200;
const HOST_LIMIT = 50;
const DIM_LIMIT = 50;
// CLI emits value under `<metric_id_with_underscores>_<aggregation>` (e.g. `vercel_request_count_sum`).
function defaultNormalize(metricId, aggregation, groupBy) {
return (resp) => ({ rows: normalizeSummary(resp, metricId, aggregation, groupBy) });
}
// Collapse (route × function_start_type) rows into one row per route. Observed values: "cold", "hot", "prewarmed".
function normalizeColdStart(metricId, aggregation) {
return (resp) => {
const rows = normalizeSummary(resp, metricId, aggregation, ['route', 'function_start_type']);
const byRoute = new Map();
for (const r of rows) {
if (!r.route) continue;
const prior = byRoute.get(r.route) ?? { route: r.route, total: 0, coldCount: 0, warmCount: 0, prewarmedCount: 0 };
const v = r.value ?? 0;
prior.total += v;
if (r.function_start_type === 'cold') prior.coldCount += v;
else if (r.function_start_type === 'hot') prior.warmCount += v;
else if (r.function_start_type === 'prewarmed') prior.prewarmedCount += v;
byRoute.set(r.route, prior);
}
return {
rows: [...byRoute.values()].map((r) => ({
...r,
coldPct: r.total > 0 ? r.coldCount / r.total : 0,
})),
};
};
}
export const QUERIES = [
{
id: 'requestsByRouteCache',
metricId: 'vercel.request.count',
aggregation: 'sum',
groupBy: ['route', 'cache_result'],
limit: ROUTE_LIMIT,
description: 'Request count per route × cache_result. Source of cache hit rate; total invocations folds across cache_result.',
},
{
id: 'fnDurationP95ByRoute',
metricId: 'vercel.function_invocation.function_duration_ms',
aggregation: 'p95',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'p95 wall-clock function duration per route. Canonical slow-route signal.',
},
{
id: 'requestsByRouteStatus',
metricId: 'vercel.request.count',
aggregation: 'sum',
groupBy: ['route', 'http_status'],
limit: ROUTE_LIMIT,
description: 'Request count per route × http_status. Compatibility fallback for older route_errors fixtures.',
},
{
id: 'fnStatusByRoute',
metricId: 'vercel.function_invocation.count',
aggregation: 'sum',
groupBy: ['route', 'http_status'],
limit: ROUTE_LIMIT,
description: 'Function invocation count per route × http_status. Canonical 5xx source for slow_route disqualification and route_errors.',
},
{
id: 'requestsByRouteMethod',
metricId: 'vercel.request.count',
aggregation: 'sum',
groupBy: ['route', 'request_method'],
limit: ROUTE_LIMIT,
description: 'Request count per route × request_method. Uncached_route gate uses this to skip mostly-POST routes (Server Actions, mutations) where 0% cache is correct behavior.',
},
{
id: 'externalApiP75',
metricId: 'vercel.external_api_request.request_duration_ms',
aggregation: 'p75',
groupBy: ['origin_hostname'],
limit: HOST_LIMIT,
description: 'p75 external API duration per origin hostname.',
},
{
id: 'fnStartTypeByRoute',
metricId: 'vercel.function_invocation.count',
aggregation: 'sum',
groupBy: ['route', 'function_start_type'],
limit: ROUTE_LIMIT,
description: 'Function invocation count split by cold | hot | prewarmed. Feeds cold_start gate.',
normalizer: normalizeColdStart('vercel.function_invocation.count', 'sum'),
},
{
id: 'fnGbHrByRoute',
metricId: 'vercel.function_invocation.function_duration_gbhr',
aggregation: 'sum',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'Billed GB-hours per route (function duration in Fluid billing).',
},
{
id: 'fnCpuMsByRoute',
metricId: 'vercel.function_invocation.function_cpu_time_ms',
aggregation: 'sum',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'Active CPU time per route. Fluid Compute bills on this; high CPU = expensive route.',
},
{
id: 'fnPeakMemoryByRoute',
metricId: 'vercel.function_invocation.peak_memory_mb',
aggregation: 'max',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'Peak memory observed per route. Compared against provisioned to right-size.',
},
{
id: 'fnProvisionedMemoryByRoute',
metricId: 'vercel.function_invocation.provisioned_memory_mb',
aggregation: 'max',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'Provisioned memory per route. Feeds oversized_memory gate.',
},
{
id: 'fnTtfbP95ByRoute',
metricId: 'vercel.function_invocation.ttfb_ms',
aggregation: 'p95',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'Server-measured time-to-first-byte per route. Complements function_duration_ms p95.',
},
{
id: 'fdtByRoute',
metricId: 'vercel.request.fdt_total_bytes',
aggregation: 'sum',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'Fast Data Transfer bytes per route. Bandwidth cost driver.',
},
{
id: 'fdtByBot',
metricId: 'vercel.request.fdt_total_bytes',
aggregation: 'sum',
groupBy: ['bot_category'],
limit: DIM_LIMIT,
description: 'FDT bytes by bot category. Empty `bot_category` = human traffic; non-empty = bots.',
},
{
id: 'fdtByCache',
metricId: 'vercel.request.fdt_total_bytes',
aggregation: 'sum',
groupBy: ['cache_result'],
limit: DIM_LIMIT,
description: 'FDT bytes by cache_result. Uncached vs cached bandwidth.',
},
{
id: 'middlewareCount',
metricId: 'vercel.middleware_invocation.count',
aggregation: 'sum',
groupBy: ['request_path'],
limit: ROUTE_LIMIT,
description: 'Middleware invocations per request_path. Heavy middleware traffic = missing matcher.',
},
{
id: 'middlewareDurationP95',
metricId: 'vercel.middleware_invocation.duration_ms',
aggregation: 'p95',
groupBy: ['request_path'],
limit: ROUTE_LIMIT,
description: 'p95 middleware duration per request_path.',
},
{
id: 'isrReadsByRoute',
metricId: 'vercel.isr_operation.read_units',
aggregation: 'sum',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'ISR read units per route. Healthy when high relative to writes.',
},
{
id: 'isrWritesByRoute',
metricId: 'vercel.isr_operation.write_units',
aggregation: 'sum',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'ISR write units per route. High writes/reads = over-aggressive revalidate.',
},
{
id: 'imageCount',
metricId: 'vercel.image_transformation.count',
aggregation: 'sum',
groupBy: [],
limit: 1,
description: 'Total image transformations performed.',
},
{
id: 'imageByHost',
metricId: 'vercel.image_transformation.count',
aggregation: 'sum',
groupBy: ['source_image_hostname'],
limit: HOST_LIMIT,
description: 'Image transformations per source hostname. Identify which hosts dominate the bill.',
},
{
id: 'imageSourceBytes',
metricId: 'vercel.image_transformation.source_size_bytes',
aggregation: 'sum',
groupBy: [],
limit: 1,
description: 'Bytes of source images optimized. High = ingress bandwidth cost.',
},
{
id: 'cwvLcpByRoute',
metricId: 'vercel.speed_insights_metric.lcp',
aggregation: 'p75',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'p75 Largest Contentful Paint per route. > 2500ms = poor.',
},
{
id: 'cwvInpByRoute',
metricId: 'vercel.speed_insights_metric.inp',
aggregation: 'p75',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'p75 Interaction to Next Paint per route. > 200ms = poor.',
},
{
id: 'cwvClsByRoute',
metricId: 'vercel.speed_insights_metric.cls',
aggregation: 'p75',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'p75 Cumulative Layout Shift per route. > 0.1 = poor.',
},
{
id: 'cwvTtfbByRoute',
metricId: 'vercel.speed_insights_metric.ttfb_ms',
aggregation: 'p75',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'p75 client-measured TTFB per route.',
},
{
id: 'cwvCount',
metricId: 'vercel.speed_insights_metric.count',
aggregation: 'sum',
groupBy: [],
limit: 1,
description: 'Total Speed Insights measurements. Use to decide whether CWV gates have enough signal.',
},
{
id: 'cwvCountByRoute',
metricId: 'vercel.speed_insights_metric.count',
aggregation: 'sum',
groupBy: ['route'],
limit: ROUTE_LIMIT,
description: 'Speed Insights measurements per route. CWV route gates require at least 50 samples on the specific route.',
},
{
id: 'firewallByAction',
metricId: 'vercel.firewall_action.count',
aggregation: 'sum',
groupBy: ['waf_action'],
limit: DIM_LIMIT,
description: 'Firewall action count per waf_action (allow | challenge | block | log).',
},
{
id: 'botIdChecks',
metricId: 'vercel.bot_id_check.count',
aggregation: 'sum',
groupBy: [],
limit: 1,
description: 'Total BotID checks. > 0 confirms BotID is wired up; = 0 confirms it is not.',
},
{
id: 'externalApiCount',
metricId: 'vercel.external_api_request.count',
aggregation: 'sum',
groupBy: ['origin_hostname'],
limit: HOST_LIMIT,
description: 'External API call count per origin hostname.',
},
{
id: 'externalApiBytes',
metricId: 'vercel.external_api_request.transfer_bytes',
aggregation: 'sum',
groupBy: ['origin_hostname'],
limit: HOST_LIMIT,
description: 'Outbound bytes per external API hostname.',
},
];
export function normalizerFor(entry) {
if (entry.normalizer) return entry.normalizer;
return defaultNormalize(entry.metricId, entry.aggregation, entry.groupBy);
}
lib/reconcile-candidates.mjs
// Deterministic post-deep-dive reconciliation.
//
// Runs after metrics deep-dive and before investigation briefs. Its job is to
// prevent weak candidates from consuming investigator budget when the
// follow-up metric evidence already disproves or reframes the gate hypothesis.
const SLOW_ROUTE_P95_THRESHOLD_MS = 500;
const ERROR_RATE_DOMINATES_THRESHOLD = 0.5;
const DEPLOYMENT_OUTLIER_MULTIPLE = 2;
const DEPLOYMENT_OUTLIER_MIN_MS = 1000;
const ROUTE_ERROR_CONFIRMATION_RATIO = 0.1;
const UNCACHED_HEALTHY_HIT_RATE = 0.9;
const UNCACHED_MIN_GET_SHARE = 0.2;
const ISR_WRITE_FLOOR = 100;
const ISR_WRITE_READ_RATIO_THRESHOLD = 0.5;
const SCANNER_ONLY_KINDS = new Set([
'cache_header_gap',
'image_optimization',
'rendering_candidate',
]);
export function reconcileInvestigation(investigation, { gate = null } = {}) {
if (!investigation || typeof investigation !== 'object') {
throw new TypeError('reconcileInvestigation investigation must be an object');
}
const preResolvedRecords = [];
const reconciliation = {
droppedBeforeInvestigation: 0,
reasons: {},
};
const reconcilePool = (pool, group) => {
if (!Array.isArray(pool)) return [];
const kept = [];
for (let i = 0; i < pool.length; i++) {
const candidate = pool[i];
const decision = reconcileCandidate(candidate, { group, index: i, gate });
if (decision.keep) {
kept.push(candidate);
continue;
}
reconciliation.droppedBeforeInvestigation++;
reconciliation.reasons[decision.reasonCode] = (reconciliation.reasons[decision.reasonCode] ?? 0) + 1;
preResolvedRecords.push(decision.record);
}
return kept;
};
const priorPreResolved = Array.isArray(investigation.preResolvedRecords)
? investigation.preResolvedRecords
: [];
return {
...investigation,
toLaunch: reconcilePool(investigation.toLaunch, 'toLaunch'),
platform: reconcilePool(investigation.platform, 'platform'),
preResolvedRecords: [...priorPreResolved, ...preResolvedRecords],
reconciliation: {
...(investigation.reconciliation ?? {}),
...reconciliation,
},
};
}
export function reconcileCandidate(candidate, ctx = {}) {
if (!candidate || typeof candidate !== 'object') return { keep: true };
const scannerOnly = scannerOnlyDecision(candidate, ctx);
if (scannerOnly) return scannerOnly;
if (candidate.kind === 'slow_route') {
const errorDecision = slowRouteErrorDecision(candidate, ctx);
if (errorDecision) return errorDecision;
const mismatchDecision = slowRouteMetricMismatchDecision(candidate, ctx);
if (mismatchDecision) return mismatchDecision;
const regressionDecision = deploymentRegressionDecision(candidate, ctx);
if (regressionDecision) return regressionDecision;
}
if (candidate.kind === 'route_errors') {
const mismatchDecision = routeErrorsMetricMismatchDecision(candidate, ctx);
if (mismatchDecision) return mismatchDecision;
}
if (candidate.kind === 'uncached_route') {
const cacheDecision = uncachedRouteCacheDecision(candidate, ctx);
if (cacheDecision) return cacheDecision;
const methodDecision = uncachedRouteMethodDecision(candidate, ctx);
if (methodDecision) return methodDecision;
}
if (candidate.kind === 'isr_overrevalidation') {
const isrDecision = isrOverrevalidationDecision(candidate, ctx);
if (isrDecision) return isrDecision;
}
return { keep: true };
}
function scannerOnlyDecision(candidate, ctx) {
if (!SCANNER_ONLY_KINDS.has(candidate.kind)) return null;
if (candidate.o11ySignal !== 'scanner-only') return null;
return dropWithObservation(candidate, ctx, {
reasonCode: 'scanner_only_no_metric',
reason: 'Static scanner found a possible optimization, but no Vercel metric tied traffic or cost to this target.',
observation: {
kind: 'scanner_only_no_metric',
summary: `${targetLabel(candidate)} has a static scanner finding, but no route-level Vercel metric signal.`,
evidence: `gate signal=${candidate.o11ySignal}`,
suggestedAction: 'Do not ship a recommendation from this finding unless a Vercel metric shows material traffic, cost, or latency for the same target.',
},
});
}
function slowRouteMetricMismatchDecision(candidate, ctx) {
const p95 = numberAt(candidate, ['evidence', 'deepDive', 'latency', 'p95']);
if (p95 == null) return null;
if (p95 >= SLOW_ROUTE_P95_THRESHOLD_MS) return null;
return dropWithObservation(candidate, ctx, {
reasonCode: 'metric_mismatch',
reason: `Deep-dive p95 (${formatMs(p95)}) is below the slow-route threshold, so the broad gate did not survive follow-up verification.`,
observation: {
kind: 'metric_mismatch',
summary: `${targetLabel(candidate)} was flagged as slow in the broad pass, but follow-up p95 is below threshold.`,
evidence: `${candidate.o11ySignal ?? 'gate signal unavailable'}; deepDive.latency.p95=${formatMs(p95)}`,
suggestedAction: 'Skip code investigation for this run. Re-check only if the broad and follow-up windows converge in a later run.',
},
});
}
function slowRouteErrorDecision(candidate, ctx) {
const rows = arrayAt(candidate, ['evidence', 'deepDive', 'statusDistribution']);
if (rows.length === 0) return null;
let total = 0;
let errors = 0;
for (const row of rows) {
const value = numberValue(row?.value);
if (value == null) continue;
total += value;
if (/^5/.test(String(row.http_status ?? ''))) errors += value;
}
if (total <= 0) return null;
const rate = errors / total;
if (rate <= ERROR_RATE_DOMINATES_THRESHOLD) return null;
return dropWithObservation(candidate, ctx, {
reasonCode: 'error_storm',
reason: `Function-level 5xx responses dominate this route (${formatPct(rate)}), so this is a reliability finding rather than a slow-route finding.`,
observation: {
kind: 'error_storm',
summary: `${targetLabel(candidate)} latency is dominated by function-level 5xx responses.`,
evidence: `deepDive.statusDistribution: ${formatInteger(errors)} 5xx of ${formatInteger(total)} function invocations (${formatPct(rate)})`,
suggestedAction: 'Investigate as route_errors with runtime logs and error classification before making performance recommendations.',
},
});
}
function deploymentRegressionDecision(candidate, ctx) {
const rows = arrayAt(candidate, ['evidence', 'deepDive', 'perDeployment'])
.filter((row) => row && typeof row.deployment_id === 'string' && numberValue(row.value) != null)
.map((row) => ({ deploymentId: row.deployment_id, p95: numberValue(row.value) }))
.sort((a, b) => b.p95 - a.p95);
if (rows.length < 3) return null;
const [worst, second] = rows;
if (!worst || !second || worst.p95 < DEPLOYMENT_OUTLIER_MIN_MS) return null;
if (worst.p95 < second.p95 * DEPLOYMENT_OUTLIER_MULTIPLE) return null;
return dropWithObservation(candidate, ctx, {
reasonCode: 'deployment_regression',
reason: `One deployment is a large latency outlier (${worst.deploymentId} at ${formatMs(worst.p95)}), so the next action is regression triage rather than generic code optimization.`,
observation: {
kind: 'deployment_regression',
summary: `${targetLabel(candidate)} p95 is concentrated in one deployment.`,
evidence: `${worst.deploymentId} p95=${formatMs(worst.p95)} vs next highest ${second.deploymentId} p95=${formatMs(second.p95)}`,
suggestedAction: 'Compare the outlier deployment against the prior deployment and inspect runtime logs before recommending a code-level performance change.',
},
});
}
function routeErrorsMetricMismatchDecision(candidate, ctx) {
const broadErrors = numberAt(candidate, ['evidence', 'count']) ?? parseSignalNumber(candidate.o11ySignal, 'errs');
if (broadErrors == null || broadErrors < 1000) return null;
const rows = [
...arrayAt(candidate, ['evidence', 'deepDive', 'errorStatusPattern']),
...arrayAt(candidate, ['evidence', 'deepDive', 'errorsByDeployment']),
];
if (rows.length === 0) return null;
let confirmed5xx = 0;
for (const row of rows) {
if (!/^5\d\d$/.test(String(row?.http_status ?? ''))) continue;
const value = numberValue(row?.value);
if (value != null) confirmed5xx += value;
}
// errorStatusPattern and errorsByDeployment can both be present; avoid
// double-count inflation by taking the lower non-zero route-level view when available.
const statusRows = arrayAt(candidate, ['evidence', 'deepDive', 'errorStatusPattern']);
const status5xx = sumRows(statusRows, (row) => /^5\d\d$/.test(String(row?.http_status ?? '')));
if (status5xx > 0) confirmed5xx = status5xx;
if (confirmed5xx >= broadErrors * ROUTE_ERROR_CONFIRMATION_RATIO) return null;
return dropWithObservation(candidate, ctx, {
reasonCode: 'metric_mismatch',
reason: `Deep-dive 5xx volume (${formatInteger(confirmed5xx)}) does not confirm the broad route_errors gate (${formatInteger(broadErrors)}).`,
observation: {
kind: 'metric_mismatch',
summary: `${targetLabel(candidate)} was flagged for 5xx errors, but follow-up status data did not confirm the volume.`,
evidence: `${candidate.o11ySignal ?? 'gate signal unavailable'}; deepDive.confirmed5xx=${formatInteger(confirmed5xx)}`,
suggestedAction: 'Skip code recommendations from this run. Re-run with refreshed status metrics or runtime logs if the route is still suspected.',
},
});
}
function uncachedRouteCacheDecision(candidate, ctx) {
const rows = arrayAt(candidate, ['evidence', 'deepDive', 'cacheBreakdown']);
if (rows.length === 0) return null;
const total = sumRows(rows);
if (total <= 0) return null;
const hits = sumRows(rows, (row) => ['HIT', 'STALE'].includes(String(row?.cache_result ?? '').toUpperCase()));
const hitRate = hits / total;
if (hitRate < UNCACHED_HEALTHY_HIT_RATE) return null;
return dropWithObservation(candidate, ctx, {
reasonCode: 'metric_mismatch',
reason: `Deep-dive cache hit rate (${formatPct(hitRate)}) is already healthy, so the uncached-route gate did not survive follow-up verification.`,
observation: {
kind: 'metric_mismatch',
summary: `${targetLabel(candidate)} was flagged as low-cache, but follow-up cache data is already healthy.`,
evidence: `${candidate.o11ySignal ?? 'gate signal unavailable'}; deepDive.cacheHitRate=${formatPct(hitRate)}`,
suggestedAction: 'Skip cache recommendations for this candidate unless a later run shows sustained MISS/BYPASS traffic.',
},
});
}
function uncachedRouteMethodDecision(candidate, ctx) {
const rows = arrayAt(candidate, ['evidence', 'deepDive', 'methodDistribution']);
if (rows.length === 0) return null;
const total = sumRows(rows);
if (total <= 0) return null;
const gets = sumRows(rows, (row) => String(row?.request_method ?? '').toUpperCase() === 'GET');
const getShare = gets / total;
if (getShare >= UNCACHED_MIN_GET_SHARE) return null;
return dropWithObservation(candidate, ctx, {
reasonCode: 'protocol_mismatch',
reason: `Deep-dive GET share (${formatPct(getShare)}) is below the cacheable-route floor, so this is not a good shared-cache candidate.`,
observation: {
kind: 'protocol_mismatch',
summary: `${targetLabel(candidate)} is not GET-heavy enough for a shared-cache recommendation.`,
evidence: `${candidate.o11ySignal ?? 'gate signal unavailable'}; deepDive.getShare=${formatPct(getShare)}`,
suggestedAction: 'Do not recommend CDN caching for this route from aggregate traffic alone. Investigate write-path cost only if another metric gate fires.',
},
});
}
function isrOverrevalidationDecision(candidate, ctx) {
const writeRows = arrayAt(candidate, ['evidence', 'deepDive', 'writePattern']);
const readRows = arrayAt(candidate, ['evidence', 'deepDive', 'readPattern']);
if (writeRows.length === 0 && readRows.length === 0) return null;
const writes = sumRows(writeRows);
const reads = sumRows(readRows);
const ratio = reads > 0 ? writes / reads : (writes > 0 ? Infinity : 0);
if (reads <= 0) {
return dropWithObservation(candidate, ctx, {
reasonCode: 'metric_mismatch',
reason: 'Deep-dive ISR read units were not present, so the write/read over-revalidation signal was not confirmed.',
observation: {
kind: 'metric_mismatch',
summary: `${targetLabel(candidate)} was flagged for ISR over-revalidation, but follow-up ISR read data was empty.`,
evidence: `${candidate.o11ySignal ?? 'gate signal unavailable'}; deepDive.isrWrites=${formatInteger(writes)}; deepDive.isrReads=${formatInteger(reads)}`,
suggestedAction: 'Skip ISR recommendations for this candidate unless a later run confirms both ISR writes and reads for the same route.',
},
});
}
if (writes >= ISR_WRITE_FLOOR && ratio > ISR_WRITE_READ_RATIO_THRESHOLD) return null;
const ratioLabel = ratio === Infinity ? 'Infinity' : ratio.toFixed(2);
return dropWithObservation(candidate, ctx, {
reasonCode: 'metric_mismatch',
reason: `Deep-dive ISR writes per read (${ratioLabel}) no longer crosses the over-revalidation threshold.`,
observation: {
kind: 'metric_mismatch',
summary: `${targetLabel(candidate)} was flagged for ISR over-revalidation, but follow-up ISR data did not confirm it.`,
evidence: `${candidate.o11ySignal ?? 'gate signal unavailable'}; deepDive.isrWrites=${formatInteger(writes)}; deepDive.isrReads=${formatInteger(reads)}; ratio=${ratioLabel}`,
suggestedAction: 'Skip ISR recommendations for this candidate unless a later run shows sustained write amplification.',
},
});
}
function dropWithObservation(candidate, ctx, { reasonCode, reason, observation }) {
return {
keep: false,
reasonCode,
record: {
abstain: true,
candidateRef: candidateRefFor(candidate),
reason,
observation,
reconciliation: {
droppedBeforeInvestigation: true,
reasonCode,
group: ctx.group ?? null,
index: Number.isInteger(ctx.index) ? ctx.index : null,
},
},
};
}
export function candidateRefFor(candidate, files = candidate?.files) {
if (!candidate || typeof candidate !== 'object') return 'unknown:<unknown>';
const target = candidate.route
?? candidate.hostname
?? (Array.isArray(files) && files.length > 0 ? `<account>#${files[0]}` : '<account>');
return `${candidate.kind ?? 'unknown'}:${target}`;
}
function targetLabel(candidate) {
return candidate.route ?? candidate.hostname ?? candidate.files?.[0] ?? 'account-level target';
}
function arrayAt(obj, path) {
let cur = obj;
for (const p of path) cur = cur?.[p];
return Array.isArray(cur) ? cur : [];
}
function numberAt(obj, path) {
let cur = obj;
for (const p of path) cur = cur?.[p];
return numberValue(cur);
}
function numberValue(value) {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function sumRows(rows, predicate = () => true) {
if (!Array.isArray(rows)) return 0;
let total = 0;
for (const row of rows) {
if (!predicate(row)) continue;
const value = numberValue(row?.value);
if (value != null) total += value;
}
return total;
}
function parseSignalNumber(signal, key) {
if (typeof signal !== 'string') return null;
const re = new RegExp(`(?:^|,)${key}=([\\d,.]+)`);
const m = signal.match(re);
if (!m) return null;
const n = Number(m[1].replace(/,/g, ''));
return Number.isFinite(n) ? n : null;
}
function formatMs(value) {
const n = numberValue(value);
if (n == null) return String(value);
return `${Math.round(n)}ms`;
}
function formatPct(value) {
const n = numberValue(value);
if (n == null) return String(value);
return `${(n * 100).toFixed(n >= 0.1 ? 1 : 2)}%`;
}
function formatInteger(value) {
const n = numberValue(value);
if (n == null) return String(value);
return Math.round(n).toLocaleString('en-US');
}
lib/render-report.mjs
// Deterministic markdown renderer. Same inputs → byte-identical output (modulo caller-supplied generatedAt).
import { createHash } from 'node:crypto';
import { computeImpactLabel } from './impact-label.mjs';
import { deriveProjectFacts } from './project-facts.mjs';
import { canonicalizeRoute } from './route-normalize.mjs';
import { computeCostCoverage, renderCostCoverageMarkdown } from './cost-coverage.mjs';
import { gates as registeredGates } from './gates/index.mjs';
import { formatCandidateLabel, formatKind, formatPublicText, formatRoute, formatSignal } from './display-labels.mjs';
import { splitCustomerSafeObservations } from './observation-safety.mjs';
const PLATFORM_CAP = 3;
const GATED_TARGET_PREVIEW = 5;
export function renderReport({ recommendations = [], gated = [], abstentions = [], observations = [], signals = {}, candidates = [], opts = {} } = {}) {
const safety = splitCustomerSafeObservations(observations, abstentions, signals);
observations = safety.observations;
abstentions = [...abstentions, ...safety.heldBackObservations];
assertValidObservations(observations);
const projectName = opts.projectName ?? signals.project?.name ?? '<project>';
const stack = signals.stack ?? signals.codebase?.stack ?? {};
const usage = signals.usage ?? null;
const plan = signals.plan ?? { plan: 'unknown', reason: '(not detected)' };
// Sub-agents don't always propagate o11ySignal/aliasRoutes — look them up by candidateRef and canonicalize the displayed ref.
recommendations = recommendations.map((r) => enrichRecFromCandidates(r, candidates));
const { needsEvidenceRows, noChangeRows } = splitInvestigationOutcomes(abstentions);
const lines = [];
lines.push(`# Vercel Optimization Report — ${projectName}`);
lines.push('');
lines.push(renderMetadataLine(stack, plan, usage, signals));
const coverageLine = renderCoverageLine(candidates, recommendations, signals, {
abstentions,
heldBackCount: opts.heldBackCount,
noChangeCount: opts.noChangeCount,
});
if (coverageLine) lines.push(coverageLine);
if (opts.generatedAt) {
lines.push('');
lines.push(`_Generated ${opts.generatedAt}_`);
}
lines.push('');
lines.push(...renderCostHeader(signals));
lines.push('');
lines.push(...renderCostBreakdown(usage, signals));
if (usage) {
const coverage = computeCostCoverage(usage, registeredGates);
lines.push(...renderCostCoverageMarkdown(coverage));
}
lines.push('');
const platformRecs = recommendations.filter(isPlatformScope).slice(0, PLATFORM_CAP);
const codeRecs = recommendations.filter((r) => !isPlatformScope(r));
const sorted = sortRecs(codeRecs);
const high = sorted.filter((r) => r.impactTier === 'high');
const medium = sorted.filter((r) => r.impactTier === 'medium');
const low = sorted.filter((r) => r.impactTier === 'low' || !r.impactTier);
lines.push('## Highest-impact recommendations');
lines.push('');
if (sorted.length === 0) {
lines.push('_No recommendations are ready to apply from this run._');
} else {
const top = sorted.slice(0, 5);
top.forEach((rec, i) => {
const candidate = candidateForDisplay(rec);
const signal = formatSignal(rec.o11ySignal ?? signalFromRec(rec) ?? '', candidate);
lines.push(`${i + 1}. **${formatCandidateLabel(candidate)}** — ${signal}`);
lines.push(` - **What to do**: ${formatRecommendationText(rec.what ?? '')}`);
lines.push(` - **Impact**: ${formatRecommendationText(impactString(rec, signals))}`);
if (rec.effort) lines.push(` - **Effort**: ${rec.effort}`);
const cites = asArray(rec.citations);
if (cites.length > 0) lines.push(` - **Citations**: ${cites.join(', ')}`);
});
}
lines.push('');
lines.push('## Recommendations');
lines.push('');
lines.push('### High impact');
lines.push('');
lines.push(...renderRecTable(high, signals));
lines.push('');
lines.push('### Medium impact');
lines.push('');
lines.push(...renderRecTable(medium, signals));
lines.push('');
if (low.length > 0) {
lines.push('### Low impact');
lines.push('');
lines.push(...renderRecTable(low, signals));
lines.push('');
}
lines.push('## Detailed recommendations');
lines.push('');
if (sorted.length === 0) {
lines.push('_No recommendations are ready to apply from this run._');
} else {
for (const [i, rec] of sorted.entries()) {
lines.push(...renderRecDetail(rec, i + 1, { signals }));
}
}
lines.push('');
lines.push('## Platform recommendations');
lines.push('');
if (platformRecs.length === 0) {
lines.push('_(none — the gate did not surface any platform-scope recommendations)_');
} else {
for (const [i, rec] of platformRecs.entries()) {
lines.push(...renderRecDetail(rec, i + 1, { compact: true, signals }));
}
}
lines.push('');
// Observations carry actionable signal discovered during investigation.
if (observations.length > 0) {
lines.push('## Observations from investigation');
lines.push('');
lines.push('These are real signals from the audit, but they are not ready-to-apply recommendations.');
lines.push('');
lines.push('| Candidate | Observation | Evidence | Suggested action | Kind |');
lines.push('|---|---|---|---|---|');
for (const o of observations) {
const ref = o.candidateRef ?? '(unspecified)';
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'))} |`);
}
lines.push('');
}
// Trust mechanism: customer sees what was investigated and why no rec emerged.
if (needsEvidenceRows.length > 0) {
lines.push('## Needs more evidence');
lines.push('');
lines.push('These candidates were investigated, but automated checks kept the change out of the ready-to-apply list.');
lines.push('');
lines.push('| Candidate | Why it was held back |');
lines.push('|---|---|');
for (const a of needsEvidenceRows) {
const ref = a.candidateRef ?? '(unspecified)';
const reason = publicNoRecommendationReason(a.reason ?? '(no reason recorded)');
lines.push(`| ${escape(displayCandidateRef(ref))} | ${escape(reason)} |`);
}
lines.push('');
}
if (noChangeRows.length > 0) {
lines.push('## Investigated, no change recommended');
lines.push('');
lines.push('These candidates were checked and did not produce a supported change.');
lines.push('');
lines.push('| Candidate | Why no recommendation shipped |');
lines.push('|---|---|');
for (const a of noChangeRows) {
const ref = a.candidateRef ?? '(unspecified)';
const reason = publicNoRecommendationReason(a.reason ?? '(no reason recorded)');
lines.push(`| ${escape(displayCandidateRef(ref))} | ${escape(reason)} |`);
}
lines.push('');
}
lines.push('## Not investigated in this run');
lines.push('');
lines.push(...renderGatedTable(gated));
lines.push('');
lines.push('## Strengths');
lines.push('');
lines.push(...renderStrengths(signals));
lines.push('');
const configNotes = renderConfigurationNotes(signals);
if (configNotes.length > 0) {
lines.push('## Configuration notes');
lines.push('');
lines.push(...configNotes);
lines.push('');
}
lines.push('## Data gaps');
lines.push('');
lines.push(...renderDataGaps(signals));
return lines.join('\n');
}
function assertValidObservations(observations) {
if (!Array.isArray(observations)) {
throw new TypeError('renderReport observations must be an array');
}
for (const [i, o] of observations.entries()) {
if (!o || typeof o !== 'object') {
throw new TypeError(`renderReport observations[${i}] must be an object`);
}
if (typeof o.summary !== 'string' || o.summary.trim() === '') {
throw new TypeError(`renderReport observations[${i}].summary is required`);
}
}
}
export function buildFinalReportMessage({ reportPath, markdown, recommendations = [], signals = {}, maxRecommendations = 10 } = {}) {
const destination = reportPath || 'report.md';
const coverageLine = extractCoverageLine(markdown);
const lines = [`Report saved: ${destination}`];
if (coverageLine) {
lines.push('');
lines.push(stripDetailsLink(coverageLine));
} else {
lines.push('');
lines.push('Open the report for details. No coverage summary was available.');
}
const readyPreview = renderFinalRecommendationPreview(recommendations, signals, maxRecommendations);
if (readyPreview.length > 0) {
lines.push('');
lines.push(...readyPreview);
}
const body = lines.join('\n');
return {
body,
lineCount: lines.length,
sha256: createHash('sha256').update(body).digest('hex'),
reportPath: destination,
coverageLine: coverageLine ?? null,
recommendationsShown: readyPreview.filter((line) => /^\d+\./.test(line)).length,
};
}
function renderFinalRecommendationPreview(recommendations, signals, maxRecommendations) {
const ready = Array.isArray(recommendations)
? sortRecs(recommendations.filter((r) => r && r.abstain !== true))
: [];
if (ready.length === 0) return [];
const max = Math.max(1, Math.min(Number.isInteger(maxRecommendations) ? maxRecommendations : 5, 10));
const shown = ready.slice(0, max);
const lines = ['Ready recommendations:'];
for (const [i, rec] of shown.entries()) {
lines.push(`${i + 1}. ${compactFinalText(rec.what ?? displayCandidate(rec))}`);
const impact = impactString(rec, signals);
if (impact && !/^_\(no impact framing recorded\)_$/.test(impact)) {
lines.push(` Impact: ${compactFinalText(impact)}`);
}
}
const hidden = ready.length - shown.length;
if (hidden > 0) {
lines.push(`Open the report for ${hidden} more ready recommendation${hidden === 1 ? '' : 's'} and the full evidence.`);
}
return lines;
}
function compactFinalText(value) {
const text = formatRecommendationText(String(value ?? ''))
.replace(/\s+/g, ' ')
.trim();
if (text.length <= 220) return text;
return `${text.slice(0, 217).trimEnd()}...`;
}
function extractCoverageLine(markdown) {
if (typeof markdown !== 'string') return null;
return markdown
.split('\n')
.find((line) => line.startsWith('**Coverage**:')) ?? null;
}
function stripDetailsLink(line) {
return String(line).replace(/\s*·\s*\[details\]\(#not-investigated-in-this-run\)\s*$/, '');
}
// Hidden when no candidates exist (e.g., observability blocker — nothing to cover).
function renderCoverageLine(candidates, recommendations, signals, opts = {}) {
if (!Array.isArray(candidates) || candidates.length === 0) return null;
const launched = candidates.filter((c) => !c.gatedReason && !c.disqualified && c.scope !== 'account');
const skippedByBudget = candidates.filter(
(c) => typeof c.gatedReason === 'string' && c.gatedReason.startsWith('skippedByBudget')
);
const coveredByDedup = candidates.filter(
(c) => typeof c.gatedReason === 'string' && c.gatedReason.startsWith('coveredBy')
);
const disqualified = candidates.filter(
(c) => typeof c.gatedReason === 'string' && c.gatedReason === c.disqualifyReason
);
const total = launched.length + skippedByBudget.length;
if (total === 0) return null;
const parts = [];
parts.push(`Found **${total}** potential issue${total === 1 ? '' : 's'} to check`);
parts.push(`${launched.length} investigated`);
if (skippedByBudget.length > 0) {
parts.push(`${skippedByBudget.length} left for a larger run — re-run with \`--max-candidates all\` to see the rest`);
}
if (coveredByDedup.length > 0) {
parts.push(`${coveredByDedup.length} similar route variant${coveredByDedup.length === 1 ? '' : 's'} grouped`);
}
const recCount = (recommendations ?? []).filter((r) => !r.abstain && !isPlatformScope(r)).length;
parts.push(`${recCount} recommendation${recCount === 1 ? '' : 's'} ready`);
const rawHeldBackCount = Number.isInteger(opts.heldBackCount)
? opts.heldBackCount
: (Array.isArray(opts.abstentions) ? opts.abstentions.filter((a) => a?.needsEvidence === true).length : 0);
const heldBackCount = Math.min(rawHeldBackCount, Math.max(0, launched.length - recCount));
if (heldBackCount > 0) {
parts.push(`${heldBackCount} need more evidence`);
}
const rawNoChangeCount = Number.isInteger(opts.noChangeCount)
? opts.noChangeCount
: (Array.isArray(opts.abstentions) ? opts.abstentions.length : 0);
const noChangeCount = Math.min(rawNoChangeCount, Math.max(0, launched.length - recCount - heldBackCount));
if (noChangeCount > 0) {
parts.push(`${noChangeCount} investigated, no change recommended`);
}
return `**Coverage**: ${parts.join(' · ')} · [details](#not-investigated-in-this-run)`;
}
function renderMetadataLine(stack, plan, usage, signals = {}) {
const fw = `${stack.framework ?? 'unknown'}@${stack.frameworkVersion ?? '?'}`;
const router = stack.hasAppRouter ? 'app-router' : stack.hasPagesRouter ? 'pages-router' : null;
const orm = stack.orm && stack.orm !== 'none' ? stack.orm : null;
const stackParts = [fw, router, orm].filter(Boolean).join(' | ');
const period = usage?.period
? `${usage.period.from ?? '?'} → ${usage.period.to ?? '?'}`
: '(unavailable)';
const oplusLabel = observabilityLabel(signals, usage);
// Plan-inference reason is debug detail — only surface when plan is uncertain.
const planLabel = plan.plan === 'uncertain'
? `${plan.plan} (${plan.reason ?? 'no signal'})`
: (plan.plan ?? 'unknown');
return `**Stack**: ${stackParts} · **Plan**: ${planLabel} · **Period**: ${period} · **Observability**: ${oplusLabel}`;
}
function observabilityLabel(signals, usage) {
if (signals.observabilityPlusUsable === true) {
return 'Observability Plus enabled — per-route metrics included';
}
if (signals.observabilityPlusUsable === false) {
if (usage) {
return 'Per-route metrics unavailable — analysis based on billing + scanner findings';
}
if (signals.usageError === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED') {
return 'Per-route metrics unavailable — audit paused before metric-backed route ranking';
}
return 'Per-route metrics unavailable — limited analysis based on scanner findings';
}
if (signals.observabilityPlus === true) {
return 'Observability Plus enabled — per-route metrics included';
}
if (signals.usageError === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK') {
return 'Not checked — audit paused at unsupported-framework preflight';
}
if (signals.usageError === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED') {
return 'Per-route metrics unavailable — audit paused before metric-backed route ranking';
}
if (usage) {
return 'Not enabled — analysis based on billing + scanner findings';
}
if (signals.observabilityPlus === false) {
return 'Not enabled — limited analysis only';
}
return 'Not checked — limited analysis only';
}
function renderCostHeader(signals) {
const scope = signals.usageScope;
if (scope === 'project') {
return ['## Cost breakdown (this project)'];
}
if (scope === 'team' && signals.usage) {
return [
'## Cost breakdown (team-wide — `vercel usage` has no per-project filter)',
'',
'_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`._',
];
}
return ['## Cost breakdown'];
}
function renderCostBreakdown(usage, signals) {
const lines = [];
const services = Array.isArray(usage?.services) ? usage.services : null;
if (services && services.length > 0) {
const chargedRows = services.filter((s) => {
const cost = serviceCost(s);
return cost === null || costRoundsToCents(cost) > 0;
});
if (chargedRows.length > 0) {
return renderServiceCostRows(chargedRows, {
costLabel: 'Billed cost',
costOf: serviceCost,
omittedZeroRows: services.length - chargedRows.length,
total: usage.totals?.billedCost,
totalLabel: 'Total billed',
totalSuffix: ' _(precise observed cost; future-savings framing is magnitude, never precise)_',
});
}
const effectiveRows = services.filter((s) => costRoundsToCents(serviceEffectiveCost(s)) > 0);
if (effectiveRows.length > 0) {
lines.push('_Net billed cost is $0.00 after included credits or allotments. Showing effective usage cost so active cost drivers are still visible._');
lines.push('');
return [
...lines,
...renderServiceCostRows(effectiveRows, {
costLabel: 'Effective cost',
costOf: serviceEffectiveCost,
omittedZeroRows: services.length - effectiveRows.length,
total: usage.totals?.effectiveCost,
totalLabel: 'Total effective cost',
totalSuffix: ' _(usage cost before included-credit or allotment offsets)_',
}),
];
}
if (chargedRows.length === 0) {
const scope = signals.usageScope === 'team' ? 'team-wide ' : '';
lines.push(`_\`vercel usage\` returned a ${scope}billing payload, but every reported service cost was $0.00 for this window._`);
return lines;
}
}
// Fallback to o11y-derived ranking when usage payload missing.
const gbHr = signals.metrics?.fnGbHrByRoute?.rows ?? [];
const usageGap = missingUsageSentence(signals);
if (gbHr.length === 0) {
lines.push(`_${usageGap} Without per-route function GB-hour data, this report cannot rank cost drivers._`);
return lines;
}
const top = groupGbHoursByCanonicalRoute(gbHr)
.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))
.slice(0, 10);
lines.push(`_${usageGap} Ranking by \`function_duration_gbhr\` instead. These do not translate to dollars directly, but they show which routes consume billable units._`);
lines.push('');
lines.push('| Route | GB-hr (sum, 14d) |');
lines.push('|---|---|');
for (const r of top) {
lines.push(`| ${escape(r.route ?? '(unnamed)')} | ${(r.value ?? 0).toFixed(4)} |`);
}
return lines;
}
function renderServiceCostRows(services, { costLabel, costOf, omittedZeroRows = 0, total = null, totalLabel, totalSuffix = '' }) {
const lines = [];
const rows = services.slice().sort((a, b) => (costOf(b) ?? 0) - (costOf(a) ?? 0));
// Drop Usage column when every cell is "(unspecified)" — happens when CLI emits pricingUnit=USD.
const usageCells = rows.map((s) => formatUsage(s));
const hasRealUsage = usageCells.some((c) => c !== '(unspecified)');
if (hasRealUsage) {
lines.push(`| Service | Usage | ${costLabel} |`);
lines.push('|---|---|---|');
for (let i = 0; i < rows.length; i++) {
const s = rows[i];
const costValue = costOf(s);
const cost = typeof costValue === 'number' ? `$${costValue.toFixed(2)}` : '(n/a)';
lines.push(`| ${escape(s.name ?? '(unnamed)')} | ${escape(usageCells[i])} | ${cost} |`);
}
} else {
lines.push(`| Service | ${costLabel} |`);
lines.push('|---|---|');
for (const s of rows) {
const costValue = costOf(s);
const cost = typeof costValue === 'number' ? `$${costValue.toFixed(2)}` : '(n/a)';
lines.push(`| ${escape(s.name ?? '(unnamed)')} | ${cost} |`);
}
}
if (omittedZeroRows > 0) {
lines.push('');
lines.push(`_${omittedZeroRows} zero-cost service ${omittedZeroRows === 1 ? 'row was' : 'rows were'} omitted._`);
}
if (typeof total === 'number') {
lines.push('');
lines.push(`**${totalLabel}: $${total.toFixed(2)}**${totalSuffix}`);
}
return lines;
}
function serviceCost(service) {
if (typeof service?.billedCost === 'number') return service.billedCost;
if (typeof service?.cost === 'number') return service.cost;
return null;
}
function serviceEffectiveCost(service) {
if (typeof service?.effectiveCost === 'number') return service.effectiveCost;
if (typeof service?.pricingQuantity === 'number' && service?.pricingUnit === 'USD') return service.pricingQuantity;
return 0;
}
function costRoundsToCents(cost) {
if (typeof cost !== 'number' || !Number.isFinite(cost)) return 0;
return Math.round(cost * 100) / 100;
}
function renderRecTable(recs, signals = {}) {
if (recs.length === 0) return ['_(none)_'];
const lines = [];
lines.push('| # | Bucket | What | Impact | Effort | Citations |');
lines.push('|---|---|---|---|---|---|');
recs.forEach((r, i) => {
const cites = asArray(r.citations).slice(0, 2).join('<br>');
lines.push(`| ${i + 1} | ${r.bucket ?? '?'} | ${escape(formatRecommendationText(r.what ?? ''))} | ${escape(formatRecommendationText(impactString(r, signals)))} | ${r.effort ?? '?'} | ${cites} |`);
});
return lines;
}
function renderRecDetail(rec, index, { compact = false, signals = {} } = {}) {
const lines = [];
lines.push(`### ${index}. ${formatRecommendationText(rec.what ?? '(no `what`)')}`);
lines.push('');
const meta = [
rec.bucket ? `**${rec.bucket}**` : null,
rec.effort ? `effort: ${rec.effort}` : null,
rec.impactTier ? `impact tier: ${rec.impactTier}` : null,
rec.candidateRef ? `candidate: ${displayCandidate(rec)}` : null,
rec.corroborationCount > 1 ? `corroborated: ${rec.corroborationCount}` : null,
].filter(Boolean);
if (meta.length > 0) lines.push(`_${meta.join(' · ')}_`);
lines.push('');
const appliesAlsoTo = asArray(rec.appliesAlsoTo);
if (appliesAlsoTo.length > 0) {
const refs = appliesAlsoTo
.map((a) => a?.candidateRef)
.filter(Boolean)
.slice(0, 4);
if (refs.length > 0) {
const suffix = appliesAlsoTo.length > refs.length ? `, +${appliesAlsoTo.length - refs.length} more` : '';
lines.push(`_Also applies to: ${refs.map(displayCandidateRef).join(', ')}${suffix}._`);
lines.push('');
}
}
if (rec.why) {
lines.push('**Why**');
lines.push('');
lines.push(formatRecommendationText(rec.why));
lines.push('');
}
lines.push('**Impact**');
lines.push('');
lines.push(formatRecommendationText(impactString(rec, signals)));
lines.push('');
if (!compact && rec.fix) {
lines.push('**Fix**');
lines.push('');
lines.push(rec.fix);
lines.push('');
}
if (!compact && rec.currentBehavior) {
lines.push('**Before**');
lines.push('');
lines.push(rec.currentBehavior);
lines.push('');
}
if (!compact && rec.desiredBehavior) {
lines.push('**After**');
lines.push('');
lines.push(rec.desiredBehavior);
lines.push('');
}
if (rec.verify) {
lines.push('**Verify**');
lines.push('');
lines.push(formatRecommendationText(rec.verify));
lines.push('');
}
const cites = asArray(rec.citations);
if (cites.length > 0) {
lines.push('**Citations**');
lines.push('');
for (const c of cites) lines.push(`- \`${c}\``);
lines.push('');
}
lines.push('');
return lines;
}
function renderGatedTable(gated) {
if (!Array.isArray(gated) || gated.length === 0) {
return ['_(no candidates were held back)_'];
}
const groups = groupGatedCandidates(gated);
const lines = [];
lines.push('| Candidate type | Why not investigated | Targets | Count |');
lines.push('|---|---|---|---:|');
for (const group of groups) {
lines.push(`| ${escape(group.kind)} | ${escape(group.reason)} | ${formatGatedTargets(group.targets, group.count)} | ${group.count} |`);
}
return lines;
}
function formatGatedTargets(targets, count) {
const unique = [...new Set(targets.map((t) => String(t)))];
const shown = unique.slice(0, GATED_TARGET_PREVIEW).map((target) => escape(target));
const hidden = Math.max(0, count - shown.length);
if (hidden > 0) shown.push(`+${hidden} more`);
return shown.join('<br>');
}
function groupGatedCandidates(gated) {
const byKey = new Map();
for (const g of gated) {
const kind = formatKind(g.kind ?? '?');
const reason = publicGatedReason(g.gatedReason ?? g.disqualifyReason ?? '(no reason recorded)');
const target = formatRoute(g);
const key = `${kind}\u0000${reason}`;
const existing = byKey.get(key);
if (existing) {
existing.count += 1;
existing.targets.push(String(target));
} else {
byKey.set(key, { kind: String(kind), reason: String(reason), targets: [String(target)], count: 1 });
}
}
return Array.from(byKey.values());
}
function publicNoRecommendationReason(reason) {
return formatEvidenceText(String(reason))
.replace(/\bDropped at render:\s*/gi, '')
.replace(/\bverifier flagged for regen, but no regen happened\b/gi, 'needs stronger evidence before it is safe to apply')
.replace(/\bRe-run with a refreshed brief\.?/gi, 'Re-run the investigation after refreshing the evidence.')
.replace(/\bregen\b/gi, 're-check')
.replace(/\bverifier\b/gi, 'verification')
.replace(/\brec\b/gi, 'recommendation')
.replace(/\bsub[- ]agent\b/gi, 'investigation')
.replace(/\babstentions?\b/gi, 'no-change findings')
.replace(/\babstaining\b/gi, 'not recommending a change')
.replace(/\babstain(?:ed)?\b/gi, 'found no supported change')
.replace(/\bquality\s*\+\s*verification\b/gi, 'verification')
.replace(/\bquality\b/gi, 'review')
.replace(/\bsanitizers?\b/gi, 'checks');
}
function splitInvestigationOutcomes(abstentions) {
const rows = Array.isArray(abstentions) ? abstentions : [];
return {
needsEvidenceRows: rows.filter((a) => a?.needsEvidence === true),
noChangeRows: rows.filter((a) => a?.needsEvidence !== true),
};
}
function publicGatedReason(reason) {
return formatPublicText(String(reason))
.replace(/\bhardGated:\s*/gi, '')
.replace(/skippedByBudget\s*\(max-candidates=([^);]+)(?:;[^)]*)?\)/i, 'left for a larger run (max candidates: $1)')
.replace(/skippedByBudget\b/gi, 'left for a larger run')
.replace(/\s*;\s*raise with --max-candidates N or =all/gi, '')
.replace(/=all/g, 'all')
.replace(/\bcoveredBy\b/gi, 'covered by a higher-priority candidate')
.replace(/\bdisqualified\b/gi, 'not eligible');
}
function formatEvidenceText(value) {
const expanded = formatPublicText(value)
.replace(/\bdeepDive\b/gi, 'follow-up metric')
.replace(/\bdeep-dive\b/gi, 'follow-up metric')
.replace(/\blatency p95\b/gi, '95th percentile latency')
.replace(/\bttfb p95\b/gi, '95th percentile TTFB')
.replace(/\bcpu p95\b/gi, '95th percentile CPU time')
.replace(/\bp95\b/gi, '95th percentile')
.replace(/\bgate signal\b/gi, 'broad metric signal')
.replace(/\bo11ySignal\b/gi, 'observed signal')
.replace(/\bperDeployment\b/gi, 'per-deployment')
.replace(/\bstartTypeSplit\b/gi, 'start-type breakdown')
.replace(/\bstatusDistribution\b/gi, 'status distribution')
.replace(/\bcacheBreakdown\b/gi, 'cache breakdown')
.replace(/\bfunctionRoutes\b/gi, 'function routes')
.replace(/\bfnGbHrByRoute\b/gi, 'function duration by route');
return formatPublicText(expanded);
}
function formatRecommendationText(value) {
return formatEvidenceText(value)
.replace(/\bthe gate fires\b/gi, 'this audit flags the signal')
.replace(/\bships immediately\b/gi, 'can ship sooner');
}
function displayCandidate(value) {
return formatCandidateLabel(candidateForDisplay(value));
}
function candidateForDisplay(value) {
const parsed = parseCandidateRef(value?.candidateRef);
return parsed
? displayCandidateObject(value, parsed)
: value;
}
function displayCandidateRef(ref) {
const parsed = parseCandidateRef(ref);
if (!parsed) return String(ref ?? '(unspecified)');
return formatCandidateLabel(displayCandidateObject({}, parsed));
}
function parseCandidateRef(ref) {
if (typeof ref !== 'string') return null;
const [kind, ...rest] = ref.split(':');
const target = rest.join(':');
if (!kind || !target) return null;
return { kind, target };
}
function displayCandidateObject(base, parsed) {
if (parsed.target.startsWith('<account>#')) {
return { ...base, kind: parsed.kind, files: [parsed.target.slice('<account>#'.length)] };
}
if (parsed.target === '<account>') {
return { ...base, kind: parsed.kind };
}
return { ...base, kind: parsed.kind, route: parsed.target };
}
function groupGbHoursByCanonicalRoute(rows) {
const byRoute = new Map();
for (const row of rows) {
const route = row?.route ? canonicalizeRoute(row.route) : '(unnamed)';
byRoute.set(route, (byRoute.get(route) ?? 0) + (row?.value ?? 0));
}
return [...byRoute.entries()].map(([route, value]) => ({ route, value }));
}
function renderStrengths(signals) {
const lines = [];
// Stops agent from emitting "verify Fluid is on" recs. Source: defaultResourceConfig from project API.
const projectFacts = deriveProjectFacts(signals);
for (const f of projectFacts) {
if (String(f.id ?? '').startsWith('memory_')) continue;
lines.push(`- ${f.strength}`);
}
const cache = signals.metrics?.fdtByCache?.rows ?? [];
const hit = cache.find((r) => r.cache_result === 'HIT' || r.cache_result === 'STALE');
const miss = cache.find((r) => r.cache_result === 'MISS' || r.cache_result === 'BYPASS');
if (hit && miss && (hit.value ?? 0) > (miss.value ?? 0)) {
lines.push(`- Cache hit-rate is healthy at the bandwidth tier — HIT/STALE bandwidth (${formatBytes(hit.value)}) exceeds MISS/BYPASS (${formatBytes(miss.value)}).`);
}
const cold = signals.metrics?.fnStartTypeByRoute?.rows ?? [];
const totalInv = cold.reduce((s, r) => s + (r.total ?? 0), 0);
const totalCold = cold.reduce((s, r) => s + (r.coldCount ?? 0), 0);
if (totalInv > 1000) {
const coldPct = totalCold / totalInv;
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.`);
}
const errors = signals.metrics?.requestsByRouteStatus?.rows ?? [];
const total5xx = errors.filter((r) => /^5/.test(r.http_status ?? '')).reduce((s, r) => s + (r.value ?? 0), 0);
const totalReq = errors.reduce((s, r) => s + (r.value ?? 0), 0);
if (totalReq > 1000) {
const rate = total5xx / totalReq;
if (rate < 0.001) lines.push(`- 5xx rate is very low (${(rate * 100).toFixed(3)}%) on ${formatNum(totalReq)} requests.`);
}
if (lines.length === 0) lines.push('_(no headline strengths to call out — see the gated table for signals we considered)_');
return lines;
}
function renderConfigurationNotes(signals) {
const projectFacts = deriveProjectFacts(signals);
return projectFacts
.filter((f) => String(f.id ?? '').startsWith('memory_'))
.map((f) => `- ${f.strength}`);
}
function renderDataGaps(signals) {
const lines = [];
const observabilityGap = observabilityDataGap(signals);
if (observabilityGap) lines.push(observabilityGap);
if (!signals.usage) lines.push(`- ${missingUsageSentence(signals)}`);
const cwvMetric = metricState(signals, 'cwvCount');
if (cwvMetric.failed) {
lines.push(`- Speed Insights metrics were not usable (\`${cwvMetric.code}\`), so LCP/INP/CLS analysis was skipped.`);
} else if (cwvMetric.collected) {
const cwv = cwvMetric.rows?.[0]?.value ?? 0;
if (cwv === 0) lines.push('- No Speed Insights measurements — Core Web Vitals analysis dormant. Wire up Speed Insights to enable LCP/INP/CLS recommendations.');
}
const isrMetric = metricState(signals, 'isrReadsByRoute');
if (isrMetric.collected) {
const isrR = isrMetric.rows ?? [];
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.');
}
const imageMetric = metricState(signals, 'imageCount');
if (imageMetric.collected) {
const images = imageMetric.rows?.[0]?.value ?? 0;
if (images === 0) lines.push('- No image transformations observed — either `next/image` is not used or no images served in the window.');
}
const middlewareMetric = metricState(signals, 'middlewareCount');
if (middlewareMetric.collected) {
const middleware = middlewareMetric.rows ?? [];
if (middleware.length === 0) lines.push('- No middleware invocations — either no `middleware.ts` is shipped or its matcher excludes all observed traffic.');
}
if (lines.length === 0) lines.push('_(no relevant gaps — every signal had data)_');
return lines;
}
function observabilityDataGap(signals = {}) {
if (signals.usageError === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK') {
return '- Observability Plus was not checked because the audit paused at the unsupported-framework preflight.';
}
if (signals.observabilityPlusUsable === false) {
const blocker = signals.observabilityPlusBlocker;
if (blocker === 'project_disabled') {
return '- Per-route metrics unavailable — Observability Plus is disabled for this project.';
}
if (blocker === 'forbidden' || blocker === 'project_not_found') {
return '- Per-route metrics unavailable — the authenticated Vercel scope cannot read this project.';
}
if (blocker === 'not_linked') {
return '- Per-route metrics unavailable — the app directory is not linked to the Vercel project.';
}
if (blocker === 'no_oplus_probe') {
return '- Per-route metrics unavailable — Observability Plus was not detected for this scope.';
}
if (blocker === 'payment_required') {
return '- Per-route metrics unavailable — Observability Plus metrics were not usable for this scope.';
}
if (blocker === 'daily_quota_exceeded') {
return '- Per-route metrics unavailable — the Observability Plus query quota is exhausted for today.';
}
if (blocker === 'no_traffic') {
return '- Per-route metrics sparse — no route-level traffic was returned in the metrics window.';
}
if (blocker === 'all_failed_other') {
return '- Per-route metrics unavailable — all Observability Plus metric queries failed.';
}
if (blocker) {
return `- Per-route metrics unavailable — Observability Plus metrics returned \`${blocker}\`.`;
}
return '- Per-route metrics unavailable — Observability Plus data was not usable for this run.';
}
if (signals.observabilityPlus === false) {
return '- Observability Plus not enabled — per-route latency / cache-hit / cold-start metrics unavailable.';
}
return null;
}
function missingUsageSentence(signals = {}) {
const code = signals.usageError;
if (code === 'NOT_COLLECTED_OBSERVABILITY_BLOCKED') {
return '`vercel usage` was not collected because the audit paused before billing collection on the Observability Plus blocker.';
}
if (code === 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK') {
return '`vercel usage` was not collected because the audit paused at the unsupported-framework preflight.';
}
if (code === 'USAGE_CONTEXT_MISMATCH') {
return '`vercel usage` returned data for a different team context, so the billing breakdown was not used.';
}
if (code === 'USAGE_UNAVAILABLE') {
return '`vercel usage` returned `USAGE_UNAVAILABLE`; no billing breakdown was available from the Vercel CLI.';
}
if (typeof code === 'string' && code.trim() !== '') {
return `\`vercel usage\` returned \`${code}\`; no billing breakdown was available from the Vercel CLI.`;
}
return '`vercel usage` did not return a billing payload.';
}
function metricState(signals, id) {
const metrics = signals.metrics ?? {};
if (!Object.prototype.hasOwnProperty.call(metrics, id)) {
return { collected: false, failed: false, rows: null, code: null };
}
const metric = metrics[id] ?? {};
const failed = metric.ok === false;
return {
collected: !failed,
failed,
rows: Array.isArray(metric.rows) ? metric.rows : [],
code: metric.code ?? 'UNKNOWN',
};
}
function sortRecs(recs) {
return recs.slice().sort((a, b) => priorityScore(b) - priorityScore(a));
}
function priorityScore(rec) {
return typeof rec.priority === 'number' ? rec.priority : tierScore(rec.impactTier);
}
function tierScore(t) { return ({ high: 100, medium: 50, low: 10 })[t] ?? 0; }
function isPlatformScope(rec) {
const k = String(rec.candidateRef ?? '').split(':')[0];
return k.startsWith('platform_') || rec.scope === 'account';
}
function impactString(rec, signals = {}) {
const label = computeImpactLabel(rec, signals);
if (label) return label;
return '_(no impact framing recorded)_';
}
function signalFromRec(rec) {
return rec.findingRefs?.[0] ?? null;
}
function formatUsage(s) {
if (typeof s.usage === 'string') return s.usage;
if (typeof s.usage === 'number') return formatNum(s.usage) + (s.unit ? ` ${s.unit}` : '');
return '(unspecified)';
}
function formatNum(n) {
if (!Number.isFinite(n)) return String(n);
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + 'M';
if (n >= 1_000) return (n / 1_000).toFixed(2) + 'K';
return String(n);
}
function formatBytes(b) {
if (!Number.isFinite(b)) return '(n/a)';
if (b >= 1e12) return (b / 1e12).toFixed(2) + ' TB';
if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB';
if (b >= 1e6) return (b / 1e6).toFixed(2) + ' MB';
if (b >= 1e3) return (b / 1e3).toFixed(2) + ' KB';
return Math.round(b) + ' B';
}
function escape(s) {
if (typeof s !== 'string') return String(s ?? '');
return s.replace(/\|/g, '\\|').replace(/\n/g, ' ');
}
function asArray(v) { return Array.isArray(v) ? v : []; }
function enrichRecFromCandidates(rec, candidates) {
if (!rec || typeof rec !== 'object') return rec;
if (!Array.isArray(candidates) || candidates.length === 0) return rec;
const ref = rec.candidateRef ?? null;
// Match on raw OR canonical kind:route so pre-dedup-canonicalization refs still resolve.
let match = null;
if (ref) {
const [kind, route] = String(ref).split(':');
const canonical = route ? canonicalizeRoute(route) : null;
match = candidates.find((c) => {
if (!c || c.kind !== kind) return false;
const cRoute = c.route ?? c.hostname ?? '<account>';
return cRoute === route || cRoute === canonical || canonicalizeRoute(cRoute) === canonical;
});
}
const canonicalRef = ref ? canonicalRefOf(ref) : ref;
const merged = { ...rec, candidateRef: canonicalRef };
if (match) {
if (!merged.o11ySignal && match.o11ySignal) merged.o11ySignal = match.o11ySignal;
if (!merged.displayRoute && match.displayRoute) merged.displayRoute = match.displayRoute;
if (!merged.aliasRoutes && Array.isArray(match.aliasRoutes) && match.aliasRoutes.length > 0) {
merged.aliasRoutes = match.aliasRoutes;
}
if (!merged.mergedCount && typeof match.mergedCount === 'number') {
merged.mergedCount = match.mergedCount;
}
}
return merged;
}
function canonicalRefOf(ref) {
const [kind, ...rest] = String(ref).split(':');
const route = rest.join(':');
if (!route) return ref;
return `${kind}:${canonicalizeRoute(route)}`;
}
lib/repo-root.mjs
// Auto-detect repo-root for claim verification. Priority: Vercel API rootDirectory > --repo-root > walk-up.
// In a monorepo the sub-agent emits paths like `apps/<app>/src/...` and the verifier needs the prefix root, not the app dir.
import { access } from 'node:fs/promises';
import { join, dirname, resolve, normalize } from 'node:path';
// Prefer affectedFiles[0] over findingRefs — findingRefs often share the same file.
export function pickProbeFile(recs) {
for (const r of (recs ?? [])) {
if (r?.abstain) continue;
const af = Array.isArray(r?.affectedFiles) ? r.affectedFiles[0] : null;
if (typeof af === 'string' && af.length > 0) return af;
const ref = Array.isArray(r?.findingRefs) ? r.findingRefs[0] : null;
if (typeof ref === 'string' && ref.length > 0) {
const m = ref.match(/^(.+?):\d+$/);
if (m) return m[1];
}
}
return null;
}
export async function fileResolvesAt(root, file) {
try {
await access(join(root, file));
return true;
} catch {
return false;
}
}
export async function detectRepoRoot(probeFile, startDir, maxDepth = 10) {
let dir = resolve(startDir);
for (let depth = 0; depth < maxDepth; depth++) {
if (await fileResolvesAt(dir, probeFile)) return dir;
const parent = dirname(dir);
if (parent === dir) return null;
dir = parent;
}
return null;
}
// rootDirectory "apps/fixture-site" + cwd .../monorepo/apps/fixture-site → repo root .../monorepo.
export function deriveRootFromSignals(signals, cwd = process.cwd()) {
const dir = signals?.project?.rootDirectory;
if (!dir || typeof dir !== 'string') return null;
const offset = normalize(dir).replace(/^\.\/?/, '').replace(/\/$/, '');
if (!offset) return null;
const cwdAbs = resolve(cwd);
// Match `<root>/<offset>` OR `<root>/<offset>/<more>` — orchestrator may run from a subdir.
const parts = cwdAbs.split('/');
const offsetParts = offset.split('/');
for (let start = parts.length - offsetParts.length; start >= 0; start--) {
const slice = parts.slice(start, start + offsetParts.length).join('/');
if (slice === offset) {
const root = parts.slice(0, start).join('/');
return root || '/';
}
}
return null;
}
export async function resolveRepoRoot(recs, suppliedRoot, cwd = process.cwd(), signals = null) {
if (signals) {
const apiRoot = deriveRootFromSignals(signals, cwd);
if (apiRoot) {
return { root: apiRoot, source: 'api', probe: null, apiOffset: signals?.project?.rootDirectory ?? null };
}
}
const probe = pickProbeFile(recs);
if (!probe) {
return { root: suppliedRoot ?? '.', source: suppliedRoot ? 'supplied' : 'default', probe: null };
}
if (suppliedRoot && await fileResolvesAt(suppliedRoot, probe)) {
return { root: suppliedRoot, source: 'supplied', probe };
}
const detected = await detectRepoRoot(probe, suppliedRoot ?? cwd);
if (detected) {
return {
root: detected,
source: suppliedRoot ? 'corrected' : 'auto-detected',
probe,
};
}
return { root: suppliedRoot ?? '.', source: suppliedRoot ? 'supplied' : 'default', probe };
}
lib/route-normalize.mjs
// Canonicalize Next.js 16 segment-tree metric route paths so gate dedup doesn't burn budget on N copies of the same source file.
//
// Next.js folds flag state into base64 prefix, dynamic placeholders into `$d$X`, route groups into `!K..p`, cache-lifecycle leaves into `__PAGE__.segment` / `_tree.segment` / `_index.segment`. ~4-10x dupes per page without canonicalization.
//
// This module is the ONLY place we touch Next.js metric path encoding — every gate calls canonicalizeRoute before aggregating.
export const ROUTE_SHAPE_RE = /(?:^.{200,}$)|[\s'"`,;&=<>(){}!\\^|\u0000-\u001F]|%(?:22|5B|5C|7B|7D|20|3C|3E|26)|localhost:|https?:\/|\/\/(?!$)|[:,\$\s]$|\.segments?\/|__PAGE__|@[a-z]/i;
export function isSegmentTreePath(route) {
if (typeof route !== 'string') return false;
return /\.segments(\/|$)/.test(route);
}
export function canonicalizeRoute(route) {
if (typeof route !== 'string' || route.length === 0) return route;
if (!isSegmentTreePath(route)) return stripRouteGroups(replaceBase64WithDynamic(route));
// Discard prefix (flag-state + dynamic value, both noise). Tail is the segment-tree node.
const idx = route.indexOf('.segments');
if (idx < 0) return route;
const segmentTail = route.slice(idx + '.segments'.length).replace(/^\//, '');
// _tree.segment / _index.segment have no per-segment tail — fall back to static head of prefix.
if (segmentTail === '_tree.segment' || segmentTail === '_index.segment') {
return canonicalizeBranchPrefix(route, idx);
}
const parts = segmentTail.split('/').filter((p) => p && !isMetricLeaf(p));
if (parts.length === 0) return canonicalizeBranchPrefix(route, idx);
const decoded = parts.map(decodeSegmentToken).filter(Boolean);
if (decoded.length === 0) return canonicalizeBranchPrefix(route, idx);
// scan-codebase's routePath enumeration drops route groups — match it or the route→file lookup breaks.
return stripRouteGroups('/' + decoded.join('/'));
}
function canonicalizeBranchPrefix(route, segmentsIdx) {
const prefix = route.slice(0, segmentsIdx);
const parts = prefix.split('/').filter(Boolean);
// Drop trailing dynamic value (e.g. "london") and base64 flag-state — neither is a route segment.
const cleaned = parts
.filter((p) => !isBase64FlagState(p))
.slice(0, -1);
if (cleaned.length === 0) return prefix || '/';
return '/' + cleaned.join('/');
}
function isMetricLeaf(token) {
return (
token === '__PAGE__.segment' ||
token === '_tree.segment' ||
token === '_index.segment' ||
token === '__page__.segment' ||
token.endsWith('.segment') && token.startsWith('_')
);
}
// Conservative heuristic for `eyJoYXNTZXNzaW9uIjpmYWxzZX0`-shape tokens: URL-safe base64 alphabet, length ≥16, mixed case.
function isBase64FlagState(token) {
if (typeof token !== 'string') return false;
if (token.length < 16) return false;
return /^[A-Za-z0-9_-]+$/.test(token) && /[A-Z]/.test(token) && /[a-z]/.test(token);
}
// `/event/<base64>/teaser` → `/event/[*]/teaser`. Stripping entirely (old behavior) corrupted segment count and broke route→file lookup.
function replaceBase64WithDynamic(route) {
if (typeof route !== 'string' || !route.startsWith('/')) return route;
const parts = route.split('/');
let mutated = false;
const replaced = parts.map((p, i) => {
if (i === 0) return p;
if (isBase64FlagState(p)) { mutated = true; return '[*]'; }
return p;
});
if (!mutated) return route;
return replaced.join('/') || '/';
}
// Route groups `(default)` never appear in rendered URLs — scan-codebase drops them, so canonical form must match.
function stripRouteGroups(route) {
if (typeof route !== 'string' || !route.includes('(')) return route;
const parts = route.split('/');
const kept = parts.filter((p) => !/^\([^)]+\)$/.test(p));
const joined = kept.join('/');
return joined.startsWith('/') ? (joined || '/') : '/' + joined;
}
// $d$X → [X] · $oc$X → [[...X]] · $c$X → [...X] · !K…p → (group) · metric-leaves → dropped.
function decodeSegmentToken(token) {
if (isMetricLeaf(token)) return '';
let t = token.endsWith('.segment') ? token.slice(0, -'.segment'.length) : token;
if (/^\$d\$/.test(t)) return `[${t.slice(3)}]`;
if (/^\$oc\$/.test(t)) return `[[...${t.slice(4)}]]`;
if (/^\$c\$/.test(t)) return `[...${t.slice(3)}]`;
// `!` is segment-tree marker; body is base64 of `(default)` etc. Accept only when decoded looks like `(name)`.
if (t.startsWith('!') && t.length > 1) {
const body = t.slice(1);
try {
const decoded = Buffer.from(body, 'base64').toString('utf-8');
if (/^\(.*\)$/.test(decoded)) return decoded;
} catch {
/* fall through on decode failure */
}
}
return t;
}
export function candidateKey(candidate) {
const route = candidate?.route ?? candidate?.hostname ?? null;
const kind = candidate?.kind ?? '?';
const canonical = route ? canonicalizeRoute(route) : '<account>';
return `${kind}::${canonical}`;
}
// Records alternates so the brief shows "all 4 cities collapse here" rather than a single per-city dupe.
export function mergeCandidates(a, b) {
if (!a) return b;
if (!b) return a;
const winner = (b.priority ?? 0) > (a.priority ?? 0) ? b : a;
const loser = winner === a ? b : a;
const altRoutes = new Set([
...(Array.isArray(winner.aliasRoutes) ? winner.aliasRoutes : []),
...(Array.isArray(loser.aliasRoutes) ? loser.aliasRoutes : []),
]);
if (loser.route && loser.route !== winner.route) altRoutes.add(loser.route);
return {
...winner,
// Canonicalize so briefs/reports/deep-dives see the clean path.
route: canonicalizeRoute(winner.route),
aliasRoutes: [...altRoutes].sort(),
mergedCount: (winner.mergedCount ?? 1) + (loser.mergedCount ?? 1),
};
}
// Input assumed sorted priority desc; output preserves that order. Account-scope pass through unchanged.
export function dedupeCandidates(candidates) {
const byKey = new Map();
const order = [];
const dropped = [];
for (const c of candidates) {
if (!c || c.scope === 'account' || (!c.route && !c.hostname)) {
order.push(c);
continue;
}
const key = candidateKey(c);
if (byKey.has(key)) {
const merged = mergeCandidates(byKey.get(key), c);
byKey.set(key, merged);
dropped.push({
candidate: c,
mergedInto: key,
reason: 'duplicate of higher-priority sibling (same source route after canonicalization)',
});
} else {
byKey.set(key, { ...c, route: c.route ? canonicalizeRoute(c.route) : c.route });
order.push({ __key: key });
}
}
const deduped = order.map((c) => (c && c.__key ? byKey.get(c.__key) : c));
return { deduped, dropped };
}
export function isLikelyNextRouteShape(route) {
return typeof route === 'string' && route.length > 0 && !ROUTE_SHAPE_RE.test(route);
}
export function routeShapeWarning(route, signals = {}) {
return routeShapeWarnings(route, signals)[0] ?? null;
}
export function routeShapeWarnings(route, signals = {}) {
if (typeof route !== 'string' || route.length === 0) return [];
const warnings = [];
if (ROUTE_SHAPE_RE.test(route)) warnings.push('route-shape:suspicious-metric-label');
const first = firstRouteSegment(canonicalizeRoute(route));
if (first && shouldWarnUnknownFirstSegment(first, signals)) {
warnings.push(`route-shape:unknown-first-segment:${first}`);
}
return warnings;
}
export function withRouteShapeWarnings(candidate, signals = {}) {
const warnings = routeShapeWarnings(candidate?.route, signals);
if (warnings.length === 0) return candidate;
return {
...candidate,
warnings: [...new Set([...(Array.isArray(candidate.warnings) ? candidate.warnings : []), ...warnings])],
};
}
function firstRouteSegment(route) {
if (typeof route !== 'string') return null;
return route.split('/').filter(Boolean)[0] ?? null;
}
function shouldWarnUnknownFirstSegment(first, signals) {
const exempt = new Set(['_next', '_vercel', 'api', '.well-known']);
if (exempt.has(first)) return false;
const known = knownFirstSegments(signals);
if (known.size === 0) return false;
if ([...known].some(isDynamicPlaceholder)) return false;
return !known.has(first);
}
function knownFirstSegments(signals) {
const out = new Set();
const routes = signals.codebase?.routes ?? [];
for (const route of routes) {
const first = firstRouteSegment(route?.routePath);
if (first) out.add(first);
}
return out;
}
function isDynamicPlaceholder(segment) {
return /^\[.*\]$/.test(segment);
}
lib/sanitizers/bot-protection-certainty.mjs
// Bot Protection evidence is usually account-level summary data. Avoid turning
// observed bot traffic into unsupported statements about exact WAF rule state.
export const metadata = {
id: 'bot-protection-certainty',
description: 'Soften unsupported Bot Protection / WAF certainty and require a staged rollout caveat.',
};
const STRING_FIELDS = ['what', 'why', 'fix', 'currentBehavior', 'desiredBehavior', 'verify'];
export function apply(rec) {
if (!String(rec?.candidateRef ?? '').startsWith('platform_bot_protection:')) return {};
const tags = [];
for (const field of STRING_FIELDS) {
if (typeof rec?.[field] !== 'string') continue;
const before = rec[field];
let after = before
.replace(/\bno\s+(?:firewall\s+)?bot_filter\s+rule\b/gi, 'the collected firewall summary did not show an enforced bot-filter rule')
.replace(/\b(?:bots?|bot traffic)\s+(?:is|are)\s+the\s+cause\b/gi, 'bot traffic is a likely contributor')
.replace(/\bwithout\s+false[- ]positive\s+risk\b/gi, 'with false-positive risk monitored during rollout')
.replace(/\bno\s+false[- ]positive\s+risk\b/gi, 'false-positive risk still needs rollout monitoring');
if (after !== before) {
rec[field] = after;
tags.push(`bot-protection-certainty:${field}`);
}
}
const text = STRING_FIELDS.map((field) => rec?.[field]).filter((s) => typeof s === 'string').join('\n');
if (/\b(?:Bot Protection|BotID|bot_filter|WAF)\b/i.test(text) &&
!/\bstaged\b[\s\S]{0,80}\b(?:log|allowlist|exclusions?)\b/i.test(text)) {
const caveat = ' Use a staged rollout that starts in Log mode where available, then moves to the appropriate Challenge or Deny action only after allowlist/exclusion review for known monitoring and partner clients.';
if (typeof rec.fix === 'string') rec.fix += caveat;
else rec.fix = caveat.trim();
tags.push('bot-protection-certainty:staged-rollout');
}
return tags.length > 0 ? { tags, needsReview: true } : {};
}
lib/sanitizers/cache-tag-invalidation-certainty.mjs
// A cacheTag() in the cached function is not proof that CMS edits invalidate it.
// The report must not claim "existing tags preserve instant updates" unless the
// investigation verifies matching revalidateTag/updateTag paths.
export const metadata = {
id: 'cache-tag-invalidation-certainty',
description: 'Remove unsupported certainty that existing cache tags already preserve CMS/on-demand invalidation.',
};
const STRING_FIELDS = ['what', 'why', 'fix', 'currentBehavior', 'desiredBehavior', 'verify'];
const UNSUPPORTED_TAG_CERTAINTY =
/\b(?:existing|current)\s+(?:cache\s+)?tags?\b[^.!?\n]{0,160}\b(?:preserve|keep|cover|maintain|ensure)\b[^.!?\n]{0,160}\b(?:instant|on-demand|CMS|content|publish|update|updates|invalidation|revalidation)\b[^.!?\n]*(?:[.!?]|$)/gi;
const SAFE_REPLACEMENT =
'Confirm a matching revalidateTag() or updateTag() path for each cacheTag() before increasing the cache lifetime.';
export function apply(rec) {
const text = STRING_FIELDS.map((field) => rec?.[field]).filter((s) => typeof s === 'string').join('\n');
if (!/\bcache(?:Life|Tag)\b/.test(text)) return {};
const tags = [];
for (const field of STRING_FIELDS) {
if (typeof rec?.[field] !== 'string') continue;
const before = rec[field];
const after = before.replace(UNSUPPORTED_TAG_CERTAINTY, SAFE_REPLACEMENT);
if (after !== before) {
rec[field] = after;
tags.push(`cache-tag-invalidation-certainty:${field}`);
}
}
return tags.length > 0 ? { tags, needsReview: true } : {};
}
lib/sanitizers/count-correct.mjs
// Rewrite verifier-failed count claims to ground truth (or "a number of"
// when actual isn't numeric) so we don't ship false precision.
import { escapeRegex } from '../util.mjs';
export const metadata = {
id: 'count-correct',
description: 'Rewrite count claims to verified ground truth (count-correct) or "a number of" (count-strip) when verifier disagrees.',
};
const COUNT_CLAIM_TYPES = new Set(['pattern_count', 'repo_count', 'cited_count_literal']);
export function apply(rec, ctx = {}) {
const results = ctx.verifyResults ?? rec.verifyResults ?? rec.verification?.failed ?? null;
if (!Array.isArray(results) || results.length === 0) return {};
const tags = [];
for (const r of results) {
if (!r) continue;
const type = r.type ?? r.claimType;
if (!COUNT_CLAIM_TYPES.has(type)) continue;
const disp = r.disposition ?? (r.actual !== r.expected ? 'failed' : 'verified');
if (disp !== 'failed') continue;
const expected = r.expected;
const actual = r.actual;
const token = r.token ?? r.text ?? expected;
if (expected == null || token == null) continue;
if (typeof actual === 'number' && Number.isFinite(actual)) {
rewriteCount(rec, token, expected, `~${actual}`);
tags.push(`count-correct:${token}:${expected}->${actual}`);
} else {
rewriteCount(rec, token, expected, 'a number of');
tags.push(`count-strip:${token}`);
}
}
if (tags.length === 0) return {};
return { tags };
}
function rewriteCount(rec, token, oldCount, replacement) {
const fields = ['what', 'why', 'fix', 'currentBehavior', 'desiredBehavior'];
// Matches "60", "~60", and "60+" — LLM commonly writes "60+ icons".
const oldEsc = escapeRegex(String(oldCount));
const re = new RegExp(`\\b~?${oldEsc}\\+?\\s+${escapeRegex(token)}\\b`, 'g');
for (const f of fields) {
if (typeof rec[f] !== 'string') continue;
rec[f] = rec[f].replace(re, `${replacement} ${token}`);
}
}
lib/sanitizers/function-duration-invocations.mjs
// A function-duration optimization can reduce p95/CPU/GB-hr. It does not by
// itself reduce function invocation count unless the fix also adds CDN/static
// response caching.
export const metadata = {
id: 'function-duration-invocations',
description: 'Remove false claims that slow-route data-cache fixes reduce function invocation count.',
};
const STRING_FIELDS = [
'what',
'why',
'fix',
'currentBehavior',
'desiredBehavior',
'verify',
];
const BAD_INVOCATION_CLAIM =
/\bfunction invocations?\b[^.!?\n]{0,120}\b(?:drop|drops|fall|falls|decrease|decreases|decline|declines|reduce|reduces|reduced|cut|cuts)\b[^.!?\n]*(?:[.!?]|$)|\b(?:drop|drops|fall|falls|decrease|decreases|decline|declines|reduce|reduces|reduced|cut|cuts)\b[^.!?\n]{0,120}\bfunction invocations?\b[^.!?\n]*(?:[.!?]|$)/gi;
const SAFE_REPLACEMENT =
'95th percentile duration should drop; function invocation count may stay flat unless a separate CDN or static-rendering change is made.';
export function apply(rec) {
if (!String(rec?.candidateRef ?? '').startsWith('slow_route:')) return {};
const tags = [];
for (const field of STRING_FIELDS) {
if (typeof rec?.[field] !== 'string') continue;
const before = rec[field];
const after = before.replace(BAD_INVOCATION_CLAIM, SAFE_REPLACEMENT);
if (after !== before) {
rec[field] = after;
tags.push(`function-duration-invocations:${field}`);
}
}
return tags.length > 0 ? { tags } : {};
}
lib/sanitizers/index.mjs
// Sanitizer orchestrator. Order matters: citation strippers must run
// before missing-citation so an emptied citations[] still drops the rec.
import { applyDollarStrip } from '../impact-magnitude.mjs';
import { sanitizeCitations } from '../citations.mjs';
import * as vercelDirectiveStrip from './vercel-directive-strip.mjs';
import * as rateLimit from './rate-limit.mjs';
import * as preRelease from './pre-release.mjs';
import * as middlewareConflict from './middleware-conflict.mjs';
import * as undeclaredDep from './undeclared-dep.mjs';
import * as countCorrect from './count-correct.mjs';
import * as renderingModeMislabel from './rendering-mode-mislabel.mjs';
import * as windowUnits from './window-units.mjs';
import * as functionDurationInvocations from './function-duration-invocations.mjs';
import * as botProtectionCertainty from './bot-protection-certainty.mjs';
import * as cacheTagInvalidationCertainty from './cache-tag-invalidation-certainty.mjs';
import * as missingCitation from './missing-citation.mjs';
export const SANITIZERS = [
vercelDirectiveStrip,
rateLimit,
preRelease,
middlewareConflict,
undeclaredDep,
countCorrect,
renderingModeMislabel,
windowUnits,
functionDurationInvocations,
botProtectionCertainty,
cacheTagInvalidationCertainty,
];
export function recordSanitizer(rec, tag) {
rec.sanitizerTrail = rec.sanitizerTrail ?? [];
rec.sanitizerTrail.push(tag);
}
export async function applySanitizers(rec, ctx = {}) {
applyDollarStrip(rec);
for (const s of SANITIZERS) {
const result = s.apply(rec, ctx) ?? {};
const tags = result.tags ?? (result.tag ? [result.tag] : []);
for (const t of tags) recordSanitizer(rec, t);
if (result.needsReview) rec.needsReview = true;
if (result.dropped) {
return { kept: false, rec, dropReason: tags[0] ?? `dropped-by:${s.metadata?.id ?? 'unknown'}` };
}
}
if (ctx.framework && ctx.version) {
const before = (rec.citations ?? []).slice();
const { strippedUnknown, strippedVersion } = await sanitizeCitations(rec, ctx.framework, ctx.version);
for (const u of strippedUnknown) recordSanitizer(rec, `unknown-citation:${u}`);
for (const u of strippedVersion) recordSanitizer(rec, `version-mismatch:${u}`);
const lostAny = strippedUnknown.length > 0 || strippedVersion.length > 0;
const lostAll = lostAny && (rec.citations ?? []).length === 0 && before.length > 0;
if (lostAll) rec.needsReview = true;
}
// missing-citation runs LAST so citation strippers above can starve a rec.
const missing = missingCitation.apply(rec, ctx) ?? {};
if (missing.dropped) {
return { kept: false, rec, dropReason: missing.tag ?? 'missing-citation' };
}
return { kept: true, rec };
}
export async function applySanitizersBatch(recs, ctx = {}) {
const kept = [];
const dropped = [];
for (const rec of recs) {
const r = await applySanitizers(rec, ctx);
if (r.kept) kept.push(r.rec);
else dropped.push({ rec: r.rec, dropReason: r.dropReason });
}
return { kept, dropped };
}
lib/sanitizers/middleware-conflict.mjs
// Append a caveat when a rec targets a route covered by middleware: e.g.
// middleware setting Set-Cookie downstream poisons cache headers the rec
// adds. Trusts finding.routesCovered rather than re-implementing Next's
// matcher algorithm.
import { extractRoute } from '../util.mjs';
export const metadata = {
id: 'middleware-conflict',
description: 'Append caveat when rec targets a route covered by middleware.',
};
export function apply(rec, ctx = {}) {
const findings = ctx?.signals?.codebase?.findings ?? [];
const middlewareFinding = findings.find((f) => f?.scannerId === 'middleware-broad-matcher' || f?.id === 'middleware-broad-matcher');
if (!middlewareFinding) return {};
const route = extractRoute(rec);
if (!route) return {};
const matcher = middlewareFinding.detail?.matcher
?? middlewareFinding.matcher
?? '(unspecified matcher)';
const middlewareFile = middlewareFinding.file ?? middlewareFinding.path ?? 'middleware.ts';
const covered = middlewareFinding.detail?.routesCovered ?? middlewareFinding.routesCovered;
if (Array.isArray(covered) && covered.length > 0 && !covered.includes(route)) {
return {};
}
const tag = `middleware-conflict:${matcher}`;
const caveat = `\n\n_Caveat: Middleware at \`${middlewareFile}\` (matcher: \`${matcher}\`) may intercept \`${route}\` and alter request/response before this fix takes effect. Verify the middleware does not set headers (e.g. \`Set-Cookie\`) that would invalidate caching._`;
if (typeof rec.fix === 'string') rec.fix += caveat;
return { tag, needsReview: true };
}
lib/sanitizers/missing-citation.mjs
// Final-gate sanitizer: drops a rec with no citations left after
// unknown-citation + version-mismatch have run. Every rec must carry ≥1
// citation.
export const metadata = {
id: 'missing-citation',
description: 'Drop rec when citations[] is empty after other sanitizers.',
};
export function apply(rec, _ctx = {}) {
const cites = Array.isArray(rec.citations) ? rec.citations : [];
if (cites.length === 0) {
return { dropped: true, tag: 'missing-citation' };
}
return {};
}
lib/sanitizers/pre-release.mjs
// Append a caveat (don't drop — customer may opt in to canary) when a
// fix needs a canary/rc/beta dep version.
import { matchesFrameworkVersion } from '../citations.mjs';
const PRE_RELEASE_FEATURES = [
{
match: /\bppr\b|partial[- ]?prerendering/i,
requires: 'next@canary',
message: 'PPR is experimental — verify your Next.js version supports it as stable',
},
{
match: /\buse cache['"]?\s*directive\b|"use cache"|'use cache'/i,
requires: 'next@>=15.0.0',
message: 'use cache directive is stable in 15+',
},
{
match: /\bcacheLife\(/i,
requires: 'next@>=15.0.0',
message: 'cacheLife is stable in 15+',
},
{
match: /\bcacheTag\(/i,
requires: 'next@>=15.0.0',
message: 'cacheTag is stable in 15+',
},
];
const SEMVER_PRE_RELEASE_RE = /\b([\w-]+)@(\d+\.\d+\.\d+-(?:rc|beta|canary|alpha|next|exp)[\w.-]*)/g;
export const metadata = {
id: 'pre-release',
description: 'Append caveat when fix targets a canary/rc/beta feature.',
};
export function apply(rec, ctx = {}) {
const text = [rec.fix, rec.currentBehavior, rec.desiredBehavior]
.filter((s) => typeof s === 'string')
.join('\n');
if (!text) return {};
const tags = [];
const caveats = [];
for (const feat of PRE_RELEASE_FEATURES) {
if (feat.match.test(text)) {
if (featureAvailableForStack(feat, ctx)) continue;
const tag = `pre-release:${feat.requires}`;
if (!tags.includes(tag)) {
tags.push(tag);
caveats.push(`Requires ${feat.requires} (${feat.message}).`);
}
}
}
for (const m of text.matchAll(SEMVER_PRE_RELEASE_RE)) {
const [, pkg, version] = m;
const tag = `pre-release:${pkg}@${version}`;
if (!tags.includes(tag)) {
tags.push(tag);
caveats.push(`Requires pre-release version: \`${pkg}@${version}\`.`);
}
}
if (tags.length === 0) return {};
const caveatBlock = '\n\n_Note: ' + caveats.join(' ') + '_';
if (typeof rec.fix === 'string') rec.fix += caveatBlock;
return { tags, needsReview: true };
}
function featureAvailableForStack(feat, ctx) {
if (!ctx?.framework || !ctx?.version) return false;
return matchesFrameworkVersion(feat.requires, ctx.framework, ctx.version);
}
lib/sanitizers/rate-limit.mjs
// Prepend a caveat (don't drop — customer may be on a higher tier) when a
// rec prescribes concurrency above a known provider rate limit.
const PROVIDER_LIMITS = {
notion: { rps: 3, label: 'Notion', doc: 'https://developers.notion.com/reference/request-limits' },
openai: { rps: 30, label: 'OpenAI', doc: 'https://platform.openai.com/docs/guides/rate-limits' },
stripe: { rps: 100, label: 'Stripe', doc: 'https://docs.stripe.com/rate-limits' },
anthropic: { rps: 10, label: 'Anthropic', doc: 'https://docs.anthropic.com/en/api/rate-limits' },
};
export const metadata = {
id: 'rate-limit',
description: 'Prepend caveat when a rec prescribes concurrency above a known provider rate limit.',
};
const PROVIDER_RE = new RegExp(`\\b(${Object.keys(PROVIDER_LIMITS).join('|')})\\b`, 'gi');
const CONCURRENCY_RE = /\b(?:concurrency|parallel|in\s+parallel|simultaneous|simultaneously|fan[- ]?out|Promise\.all)\b[^\d]{0,40}(\d{1,4})\b/gi;
const CONCURRENCY_RE_REVERSE = /\b(\d{1,4})\s*(?:concurrent|parallel|simultaneous|in flight)\b/gi;
export function apply(rec, _ctx = {}) {
const text = collectText(rec);
const providers = matchProviders(text);
if (providers.length === 0) return {};
const concurrency = matchConcurrency(text);
if (concurrency === null) return {};
const tags = [];
let prepend = '';
for (const key of providers) {
const limit = PROVIDER_LIMITS[key];
if (!limit) continue;
if (concurrency > limit.rps) {
const tag = `rate-limit:${limit.label}:${concurrency}/${limit.rps}`;
tags.push(tag);
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`;
}
}
if (tags.length === 0) return {};
if (typeof rec.fix === 'string') rec.fix = prepend + rec.fix;
else rec.fix = prepend.trim();
return { tags, needsReview: true };
}
function collectText(rec) {
return [rec.what, rec.why, rec.fix, rec.currentBehavior, rec.desiredBehavior]
.filter((s) => typeof s === 'string')
.join('\n');
}
function matchProviders(text) {
const out = new Set();
for (const m of text.matchAll(PROVIDER_RE)) out.add(m[1].toLowerCase());
return [...out];
}
function matchConcurrency(text) {
let max = null;
for (const m of text.matchAll(CONCURRENCY_RE)) {
const n = Number(m[1]);
if (Number.isFinite(n) && (max === null || n > max)) max = n;
}
for (const m of text.matchAll(CONCURRENCY_RE_REVERSE)) {
const n = Number(m[1]);
if (Number.isFinite(n) && (max === null || n > max)) max = n;
}
return max;
}
lib/sanitizers/rendering-mode-mislabel.mjs
// Warn when a rec's claimed rendering mode (static/ISR/SSR) contradicts
// the scanner-tagged mode for that route. No-op when the scanner didn't
// tag a renderingMode — full AST inference isn't implemented yet.
import { extractRoute } from '../util.mjs';
const MODE_PATTERNS = {
static: /\bstatic(?:ally rendered)?\b|prerender(?:ed)?\b/i,
isr: /\bISR\b|incremental[- ]?static|revalidate\s*:\s*\d/i,
ssr: /\bSSR\b|server[- ]?side rendered|dynamic\s*=\s*['"]force-dynamic['"]/i,
};
export const metadata = {
id: 'rendering-mode-mislabel',
description: 'Catch recs that blame the wrong rendering mode (e.g. "convert from ISR" on a static page).',
};
export function apply(rec, ctx = {}) {
const route = extractRoute(rec);
if (!route) return {};
const routes = ctx?.signals?.codebase?.routes ?? [];
const match = routes.find((r) => r.routePath === route);
const actualMode = match?.renderingMode;
if (!actualMode) return {};
const text = [rec.what, rec.why, rec.fix, rec.currentBehavior, rec.desiredBehavior]
.filter((s) => typeof s === 'string')
.join('\n');
const claimedModes = Object.entries(MODE_PATTERNS)
.filter(([, re]) => re.test(text))
.map(([m]) => m);
if (claimedModes.length === 0 || claimedModes.includes(actualMode)) return {};
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._`;
if (typeof rec.fix === 'string') rec.fix += warning;
return { tag: `rendering-mode-mislabel:${claimedModes.join(',')}!=${actualMode}`, needsReview: true };
}
lib/sanitizers/undeclared-dep.mjs
// Prepend `npm i <pkg>` when the fix imports a package missing from
// package.json — otherwise pasted code hits a runtime error.
const IMPORT_RE = /\bimport\s+(?:[\w*{}\s,]+\s+from\s+)?["']([^"']+)["']/g;
const REQUIRE_RE = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g;
// Captures package root from `pkg/sub` and `@scope/pkg/sub`.
const PKG_ROOT_RE = /^(@[^/]+\/[^/]+|[^/]+)/;
const NODE_BUILTINS = new Set([
'fs', 'fs/promises', 'path', 'os', 'crypto', 'http', 'https', 'http2', 'net',
'dns', 'tls', 'util', 'url', 'stream', 'buffer', 'events', 'process', 'child_process',
'cluster', 'worker_threads', 'inspector', 'perf_hooks', 'assert', 'console',
'querystring', 'string_decoder', 'tty', 'vm', 'zlib', 'readline', 'punycode',
'module', 'timers', 'async_hooks', 'v8', 'test', 'diagnostics_channel',
]);
export const metadata = {
id: 'undeclared-dep',
description: 'Prepend `npm i <pkg>` when fix imports a package not in package.json.',
};
export function apply(rec, ctx = {}) {
const pkg = ctx?.package ?? ctx?.signals?.package ?? null;
if (!pkg) return {};
const known = new Set([
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.devDependencies ?? {}),
...Object.keys(pkg.peerDependencies ?? {}),
...Object.keys(pkg.optionalDependencies ?? {}),
]);
const text = [rec.fix, rec.currentBehavior, rec.desiredBehavior]
.filter((s) => typeof s === 'string')
.join('\n');
const codeBlocks = extractCodeBlocks(text);
const importedRoots = new Set();
for (const block of codeBlocks) {
for (const m of block.matchAll(IMPORT_RE)) {
const root = pkgRoot(m[1]);
if (root) importedRoots.add(root);
}
for (const m of block.matchAll(REQUIRE_RE)) {
const root = pkgRoot(m[1]);
if (root) importedRoots.add(root);
}
}
const undeclared = [...importedRoots]
.filter((r) => !r.startsWith('.'))
.filter((r) => !NODE_BUILTINS.has(r))
.filter((r) => !r.startsWith('node:'))
.filter((r) => !known.has(r));
if (undeclared.length === 0) return {};
const installLines = undeclared.map((p) => `\`npm i ${p}\``).join(', ');
const prepend = `**Add dependency first**: ${installLines}\n\n`;
if (typeof rec.fix === 'string') rec.fix = prepend + rec.fix;
else rec.fix = prepend.trim();
return { tags: undeclared.map((p) => `undeclared-dep:${p}`), needsReview: true };
}
function pkgRoot(specifier) {
if (!specifier) return null;
if (specifier.startsWith('.')) return specifier;
const m = specifier.match(PKG_ROOT_RE);
return m ? m[1] : null;
}
function extractCodeBlocks(text) {
const out = [];
const re = /```[\w-]*\n?([\s\S]*?)```/g;
let m;
while ((m = re.exec(text)) !== null) out.push(m[1]);
// Also scan raw text for rare inline imports outside code blocks.
out.push(text);
return out;
}
lib/sanitizers/vercel-directive-strip.mjs
// Strip Cache-Control directives Vercel's CDN silently ignores
// (stale-if-error, proxy-revalidate, must-revalidate). s-maxage/max-age/
// stale-while-revalidate/no-store/private/public are honored — leave them.
import { escapeRegex } from '../util.mjs';
const STRIP_DIRECTIVES = ['stale-if-error', 'proxy-revalidate', 'must-revalidate'];
export const metadata = {
id: 'vercel-directive-strip',
description: 'Strip cache-control directives Vercel\'s CDN does not honor.',
};
export function apply(rec, _ctx = {}) {
const fields = ['fix', 'currentBehavior', 'desiredBehavior'];
const strippedSet = new Set();
for (const f of fields) {
if (typeof rec[f] !== 'string') continue;
for (const directive of STRIP_DIRECTIVES) {
const re = new RegExp(`(?:,\\s*)?\\b${escapeRegex(directive)}\\b(?:\\s*,)?`, 'g');
if (re.test(rec[f])) {
rec[f] = rec[f]
.replace(new RegExp(`\\b${escapeRegex(directive)}\\b`, 'g'), '')
.replace(/,\s*,/g, ',')
.replace(/(['"])\s*,\s*/g, '$1, ')
.replace(/,\s*(['"])/g, ', $1')
.replace(/(['"])\s*,\s*(['"])/g, '$1, $2')
.replace(/\b(Cache-Control|cache-control)\b:\s*,\s*/g, '$1: ')
.replace(/(['"])\s*,\s*\1/g, '$1');
strippedSet.add(directive);
}
}
}
const stripped = [...strippedSet];
if (stripped.length === 0) return {};
return { tags: stripped.map((d) => `vercel-directive-strip:${d}`) };
}
lib/sanitizers/window-units.mjs
// The metrics window is fixed by collect-signals (currently 14d). Do not let
// agent prose turn observed counts into monthly counts.
import { normalizeObservedWindowUnits } from '../display-labels.mjs';
export const metadata = {
id: 'window-units',
description: 'Rewrite observed /mo or monthly count units to /window so reports do not imply extrapolated monthly data.',
};
const STRING_FIELDS = [
'what',
'why',
'fix',
'currentBehavior',
'desiredBehavior',
'verify',
];
export function apply(rec) {
const tags = [];
for (const field of STRING_FIELDS) {
if (typeof rec?.[field] !== 'string') continue;
const before = rec[field];
const after = normalizeObservedWindowUnits(before);
if (after !== before) {
rec[field] = after;
tags.push(`window-units:${field}`);
}
}
return tags.length > 0 ? { tags } : {};
}
lib/scanners/cache-components-suspense-dedupe.mjs
// Detects the Cache Components anti-pattern where `'use cache'` doesn't dedupe across
// separate `<Suspense>` boundaries — each boundary triggers a separate evaluation of the
// "shared" cached function, multiplying invocations and ISR write pressure.
//
// Simplified single-file heuristic (cross-file segment analysis is out of scope):
// File contains `'use cache'` directive (or `use cache` keyword)
// AND file has 2+ `<Suspense ...>` boundaries
// AND a repeated fetch URL or function call appears in the body.
//
// False positives are tolerable: the support-topic body recommends a known-good remediation
// (hoist promise to page, or move to `'use cache: remote'`) whether or not the specific call
// site is the exact one paying the cost. The verifier abstains when the file structure
// doesn't match the pitfall.
import { lineOf } from '../util.mjs';
export const metadata = {
id: 'cache-components-suspense-dedupe',
title: "'use cache' with multiple Suspense boundaries on the same data",
severity: 'medium',
billingDimension: 'function-duration',
trafficIndependent: false,
description:
"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.",
fix:
"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.",
citations: [
'https://nextjs.org/docs/app/api-reference/directives/use-cache',
'https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents',
'https://nextjs.org/docs/app/guides/migrating-to-cache-components',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**', '**/*.test.*', '**/*.spec.*'],
includeGlobs: [
'**/page.{ts,tsx,js,jsx}',
'**/layout.{ts,tsx,js,jsx}',
'**/components/**/*.{tsx,jsx}',
],
};
const USE_CACHE_RE = /^[\t ]*['"]use cache['"]/m;
const SUSPENSE_TAG_RE = /<Suspense\b/g;
const FETCH_LITERAL_RE = /fetch\s*\(\s*(['"`])([^'"`]{6,200})\1/g;
// Helper function calls that look like data-fetchers (lowercase camel, no JSX/HTML noise).
const HELPER_CALL_RE = /\b(get|fetch|load|find|query|read)[A-Z][A-Za-z0-9_]+\s*\(/g;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!USE_CACHE_RE.test(content)) continue;
const suspenseCount = countMatches(content, SUSPENSE_TAG_RE);
if (suspenseCount < 2) continue;
const repeated = findRepeated(content);
if (repeated.length === 0) continue;
// Anchor the finding to the first repeated call site so the customer
// can locate the duplicate quickly.
const first = repeated[0];
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, first.firstIdx),
evidence: first.kind === 'fetch'
? `fetch("${truncate(first.token, 60)}") called ${first.count}× across Suspense boundaries`
: `${first.token}() called ${first.count}× across Suspense boundaries`,
trafficIndependent: metadata.trafficIndependent,
subtype: first.kind === 'fetch' ? 'fetch-literal' : 'helper-call',
});
}
return out;
}
function countMatches(content, re) {
re.lastIndex = 0;
let n = 0;
while (re.exec(content) !== null) n++;
return n;
}
function findRepeated(content) {
const tokens = new Map(); // token -> { kind, count, firstIdx }
let m;
FETCH_LITERAL_RE.lastIndex = 0;
while ((m = FETCH_LITERAL_RE.exec(content)) !== null) {
record(tokens, m[2], 'fetch', m.index);
}
HELPER_CALL_RE.lastIndex = 0;
while ((m = HELPER_CALL_RE.exec(content)) !== null) {
const name = m[0].replace(/\s*\($/, '').trim();
record(tokens, name, 'helper', m.index);
}
return [...tokens.values()]
.filter((t) => t.count >= 2)
.sort((a, b) => b.count - a.count);
}
function record(map, token, kind, idx) {
if (!token) return;
if (!map.has(token)) {
map.set(token, { token, kind, count: 0, firstIdx: idx });
}
map.get(token).count++;
}
function truncate(s, n) {
if (s.length <= n) return s;
return s.slice(0, n - 1) + '…';
}
lib/scanners/edge-heavy-import.mjs
// Flag node-only / heavy imports in edge-runtime files (either a
// middleware basename or an `export const runtime = 'edge'`). These
// either fail at deploy (node: builtins, native bindings) or inflate
// cold-start latency. Line-anchored matches + type-only-import skip
// keep FP low.
const EDGE_RUNTIME_RE = /export\s+const\s+runtime\s*=\s*['"]edge['"]/;
const IMPORT_RE = /^\s*import\s+(?:type\s+)?(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/gm;
const DYNAMIC_IMPORT_RE = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g;
const REQUIRE_RE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
const TYPE_IMPORT_RE = /^\s*import\s+type\s+/;
const HEAVY_PATTERNS = [
/^node:/,
/^sharp$/,
/^@aws-sdk\//,
/^@prisma\/client$/,
/^prisma$/,
/^pg$/,
/^mysql2(?:\/|$)/,
/^puppeteer(?:-core)?(?:\/|$)/,
/^playwright(?:-core)?(?:\/|$)/,
/^bcrypt$/,
/^jsonwebtoken$/,
/^canvas$/,
/^@google-cloud\//,
];
export const metadata = {
id: 'edge-heavy-import',
title: 'Heavy / node-only import inside edge-runtime file',
severity: 'high',
billingDimension: 'function-duration',
trafficIndependent: true,
description:
'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).',
fix:
'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.',
citations: [
'https://vercel.com/docs/functions/runtimes/edge-runtime',
'https://vercel.com/docs/fluid-compute',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**', '**/*.test.*', '**/*.spec.*'],
includeGlobs: ['**/*.{ts,tsx,js,mjs}'],
};
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!isEdgeRuntimeFile(path, content)) continue;
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Type-only imports are erased at compile, never reach runtime.
if (TYPE_IMPORT_RE.test(line)) continue;
const specifiers = extractSpecifiers(line);
for (const spec of specifiers) {
const match = HEAVY_PATTERNS.find((re) => re.test(spec));
if (!match) continue;
out.push({
pattern: metadata.id,
file: path,
line: i + 1,
evidence: `import "${spec}" in edge-runtime file`,
edgeReason: isMiddleware(path) ? 'middleware (always edge)' : 'export const runtime = "edge"',
importedModule: spec,
trafficIndependent: metadata.trafficIndependent,
});
}
}
}
return out;
}
function isEdgeRuntimeFile(path, content) {
return isMiddleware(path) || EDGE_RUNTIME_RE.test(content);
}
function isMiddleware(path) {
return /(?:^|\/)middleware\.(ts|tsx|js|mjs)$/.test(path);
}
function extractSpecifiers(line) {
const out = new Set();
// IMPORT_RE has `gm` flag — reset lastIndex per call.
IMPORT_RE.lastIndex = 0;
let m;
while ((m = IMPORT_RE.exec(line)) !== null) out.add(m[1]);
DYNAMIC_IMPORT_RE.lastIndex = 0;
while ((m = DYNAMIC_IMPORT_RE.exec(line)) !== null) out.add(m[1]);
REQUIRE_RE.lastIndex = 0;
while ((m = REQUIRE_RE.exec(line)) !== null) out.add(m[1]);
return [...out];
}
lib/scanners/force-dynamic.mjs
export const metadata = {
id: 'force-dynamic',
title: "export const dynamic = 'force-dynamic'",
severity: 'medium',
billingDimension: 'function-duration',
trafficIndependent: false,
description:
"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.",
fix:
"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.",
citations: [
'https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**'],
includeGlobs: ['**/route.{ts,tsx,js,jsx}', '**/page.{ts,tsx,js,jsx}'],
};
const RE = /export\s+const\s+dynamic\s*=\s*["']force-dynamic["']/;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!isApplicable(path)) continue;
const m = RE.exec(content);
if (m) {
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, m.index),
evidence: 'export const dynamic = "force-dynamic"',
trafficIndependent: metadata.trafficIndependent,
});
}
}
return out;
}
import { lineOf } from '../util.mjs';
function isApplicable(path) {
return /(\/route|\/page)\.(tsx?|jsx?)$/.test(path);
}
lib/scanners/headers-in-page.mjs
import { lineOf } from '../util.mjs';
export const metadata = {
id: 'headers-in-page',
title: 'Dynamic API call forcing dynamic rendering',
severity: 'medium',
billingDimension: 'function-duration',
trafficIndependent: false,
description:
'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.',
fix:
'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.',
citations: [
'https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config',
'https://nextjs.org/docs/app/building-your-application/caching',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**'],
includeGlobs: ['**/{page,layout,template}.{tsx,jsx}'],
};
const RE = /\b(cookies|headers|draftMode)\s*\(\s*\)/g;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!isApplicable(path)) continue;
let m;
RE.lastIndex = 0;
while ((m = RE.exec(content)) !== null) {
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, m.index),
evidence: `${m[1]}()`,
trafficIndependent: metadata.trafficIndependent,
});
}
}
return out;
}
function isApplicable(path) {
return /\/(page|layout|template)\.(tsx|jsx)$/.test(path);
}
lib/scanners/index.mjs
import * as unoptimizedImage from './unoptimized-image.mjs';
import * as forceDynamic from './force-dynamic.mjs';
import * as middlewareBroad from './middleware-broad-matcher.mjs';
import * as missingCacheHeaders from './missing-cache-headers.mjs';
import * as maxAgeNoSMaxage from './max-age-without-s-maxage.mjs';
import * as headersInPage from './headers-in-page.mjs';
import * as sourceMapsProd from './source-maps-production.mjs';
import * as prismaIncludeTree from './prisma-include-tree.mjs';
import * as sveltekitPrerenderMissing from './sveltekit-prerender-missing.mjs';
import * as largeStaticAsset from './large-static-asset.mjs';
import * as edgeHeavyImport from './edge-heavy-import.mjs';
import * as useCacheDateStamp from './use-cache-date-stamp.mjs';
import * as cacheComponentsSuspenseDedupe from './cache-components-suspense-dedupe.mjs';
import * as turboForceBypass from './turbo-force-bypass.mjs';
import * as regionPinInConfig from './region-pin-in-config.mjs';
// `use-client-cascade` is intentionally NOT registered: 0.3% conversion
// rate, and client-bundle size isn't billed on Vercel.
export const scanners = [
unoptimizedImage,
forceDynamic,
middlewareBroad,
missingCacheHeaders,
maxAgeNoSMaxage,
headersInPage,
sourceMapsProd,
prismaIncludeTree,
sveltekitPrerenderMissing,
largeStaticAsset,
edgeHeavyImport,
useCacheDateStamp,
cacheComponentsSuspenseDedupe,
turboForceBypass,
regionPinInConfig,
];
lib/scanners/large-static-asset.mjs
// Flag oversized assets under public/. Pure fs.stat — no parsing, no LLM.
// 500 KB threshold is where bandwidth/first-paint start to bite (Vercel
// Doctor's 4 KB triggers on favicons).
import { readdir, stat } from 'node:fs/promises';
import { join, relative, extname } from 'node:path';
const THRESHOLD_BYTES = 500_000;
const TOP_N = 20;
const SKIP_EXTENSIONS = new Set(['.html', '.txt', '.xml', '.json', '.webmanifest', '.ico']);
const SKIP_PATH_PREFIXES = ['.well-known/'];
export const metadata = {
id: 'large-static-asset',
title: 'Large file in public/',
severity: 'medium',
billingDimension: 'bandwidth',
trafficIndependent: true,
description:
'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).',
fix:
'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).',
citations: [
'https://vercel.com/docs/manage-cdn-usage',
'https://vercel.com/docs/image-optimization',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**'],
includeGlobs: ['public/**/*'],
};
// Walks public/ directly because collectFiles only emits text-readable
// extensions — binary assets never reach the shared `files` array.
export async function scan({ rootDir }) {
if (!rootDir) return [];
const root = join(rootDir, 'public');
const out = [];
try {
for await (const entry of walk(root)) {
if (shouldSkip(entry.relPath)) continue;
if (entry.size < THRESHOLD_BYTES) continue;
out.push({
pattern: metadata.id,
file: join('public', entry.relPath),
line: 1,
evidence: `${formatBytes(entry.size)} (${extname(entry.relPath) || 'no-ext'})`,
trafficIndependent: metadata.trafficIndependent,
sizeBytes: entry.size,
});
}
} catch {
return [];
}
out.sort((a, b) => b.sizeBytes - a.sizeBytes);
return out.slice(0, TOP_N);
}
async function* walk(dir, base = '') {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
const full = join(dir, e.name);
const rel = base ? `${base}/${e.name}` : e.name;
if (e.isDirectory()) {
yield* walk(full, rel);
continue;
}
if (!e.isFile()) continue;
try {
const s = await stat(full);
yield { relPath: rel, size: s.size };
} catch { /* skip unreadable */ }
}
}
function shouldSkip(relPath) {
if (SKIP_PATH_PREFIXES.some((p) => relPath.startsWith(p))) return true;
const ext = extname(relPath).toLowerCase();
if (SKIP_EXTENSIONS.has(ext)) return true;
return false;
}
function formatBytes(b) {
if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB';
if (b >= 1e6) return (b / 1e6).toFixed(2) + ' MB';
if (b >= 1e3) return (b / 1e3).toFixed(1) + ' KB';
return b + ' B';
}
lib/scanners/max-age-without-s-maxage.mjs
import { lineOf } from '../util.mjs';
export const metadata = {
id: 'max-age-without-s-maxage',
title: 'Cache-Control: max-age without s-maxage',
severity: 'medium',
billingDimension: 'edge-requests',
trafficIndependent: false,
description:
'max-age caches in the browser; s-maxage caches at the CDN. Without s-maxage, every uncached visitor request invokes the function. Adding s-maxage often cuts function invocations by 80%+ on read-heavy routes.',
fix:
'Add s-maxage to the Cache-Control header. Example: Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=86400. Pair with explicit cache-bust strategy if content can change.',
citations: [
'https://vercel.com/docs/caching/cdn-cache',
'https://vercel.com/docs/caching/cache-control-headers',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**', '*.config.*'],
includeGlobs: ['**/*.{ts,tsx,js,jsx,mjs}'],
};
const RE = /Cache-Control[^"'`]*?max-age\s*=\s*\d+/gi;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (/\.test\.|\.spec\./.test(path)) continue;
let m;
RE.lastIndex = 0;
while ((m = RE.exec(content)) !== null) {
const hit = m[0];
if (/s-maxage/i.test(hit) || /CDN-Cache-Control/i.test(content.slice(Math.max(0, m.index - 100), m.index + hit.length + 100))) continue;
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, m.index),
evidence: hit.slice(0, 160),
trafficIndependent: metadata.trafficIndependent,
});
}
}
return out;
}
lib/scanners/middleware-broad-matcher.mjs
export const metadata = {
id: 'middleware-broad-matcher',
title: 'Middleware matcher missing or too broad',
severity: 'high',
billingDimension: 'edge-requests',
trafficIndependent: true,
description:
'middleware.ts without a config.matcher (or matcher: ["/(.*)"]) runs on every request including _next/static, _next/image, favicon.ico, and image asset fetches. Edge-request cost scales accordingly.',
fix:
'Scope the matcher to actual application paths. Example: matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"]',
citations: [
'https://nextjs.org/docs/app/building-your-application/routing/middleware',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**'],
includeGlobs: ['middleware.{ts,js,mjs}', 'src/middleware.{ts,js,mjs}'],
};
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!isApplicable(path)) continue;
const exportsMiddleware = /export\s+(default\s+)?(async\s+)?function\s+middleware/.test(content)
|| /export\s+const\s+middleware\s*=/.test(content);
if (!exportsMiddleware) continue;
const configBlock = content.match(/export\s+const\s+config\s*=\s*\{([\s\S]*?)\}/);
const matcherStr = configBlock && configBlock[1].match(/matcher\s*:\s*([^,}]+)/);
let problem = null;
if (!configBlock || !matcherStr) {
problem = 'no config.matcher (runs on every request)';
} else {
const m = matcherStr[1];
if (/['"`]\s*\/\s*['"`]/.test(m) || /['"`]\/\(\.\*\)['"`]/.test(m)) {
problem = 'matcher = "/" or "/(.*)" (still covers everything)';
}
}
if (problem) {
out.push({
pattern: metadata.id,
file: path,
line: 1,
evidence: problem,
trafficIndependent: metadata.trafficIndependent,
});
}
}
return out;
}
function isApplicable(path) {
return /(^|\/)middleware\.(ts|js|mjs)$/.test(path);
}
lib/scanners/missing-cache-headers.mjs
// Two checks emitted as `missing-cache-headers`:
// A. GET handler with no Cache-Control AND no auth signal.
// B. fetch() with `cache:'no-store'` / `next:{revalidate:0}` outside an
// auth window (~10 lines above, 5 below).
export const metadata = {
id: 'missing-cache-headers',
title: 'Cacheable route or fetch with no caching (Cache-Control absent or no-store)',
severity: 'medium',
billingDimension: 'edge-requests',
trafficIndependent: false,
description:
'Two antipatterns: (a) GET handlers without explicit Cache-Control headers serve uncached; (b) fetch() calls with cache:"no-store" or next:{revalidate:0} opt out of caching even on cacheable upstream data. For non-auth routes / fetches, both are leaving cache hits on the floor.',
fix:
'For GET handlers: return a Response with Cache-Control: public, s-maxage=<seconds>, stale-while-revalidate=<window>. For fetch(): drop cache:"no-store" (use { next: { revalidate: <seconds> } } in Next.js) so the response is cached by the framework + CDN.',
citations: [
'https://vercel.com/docs/caching/cdn-cache',
'https://vercel.com/docs/caching/cache-control-headers',
'https://nextjs.org/docs/app/building-your-application/caching',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**', '**/*.test.*', '**/*.spec.*'],
// page/layout included: no-store fetches commonly hide in Server Components.
includeGlobs: [
'**/route.{ts,tsx,js,jsx}',
'**/api/**/*.{ts,tsx,js,jsx}',
'**/page.{ts,tsx,js,jsx}',
'**/layout.{ts,tsx,js,jsx}',
],
};
// Covers NextAuth, Clerk, JWT, Bearer, plus Next dynamic-render APIs.
// FP on cacheable routes that read a session is acceptable — verifier decides.
const AUTH_RE = /\b(cookies\(\)|headers\(\)|getSession\(|getServerSession\(|currentUser\(|clerkClient|auth\(\)|verifyJWT|verifyToken|jwt\.verify|decode\(|Bearer\s|Authorization|supabase\.auth\.)/i;
const NO_STORE_RE = /cache\s*:\s*['"]no-store['"]/;
const REVALIDATE_ZERO_RE = /next\s*:\s*\{[^}]*revalidate\s*:\s*0\b/;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!isApplicable(path)) continue;
const hasGetHandler =
/export\s+(async\s+)?function\s+GET/.test(content)
|| /export\s+const\s+GET\s*=/.test(content);
if (hasGetHandler) {
const hasCacheControl =
/Cache-Control/i.test(content)
|| /CDN-Cache-Control/i.test(content)
|| /export\s+const\s+revalidate\s*=/.test(content);
if (!hasCacheControl && !AUTH_RE.test(content)) {
out.push({
pattern: metadata.id,
subtype: 'get-handler-no-cache-control',
file: path,
line: 1,
evidence: 'GET handler with no Cache-Control / revalidate / auth signal',
trafficIndependent: metadata.trafficIndependent,
});
}
}
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const noStoreHit = NO_STORE_RE.test(line);
const revalidateZeroHit = REVALIDATE_ZERO_RE.test(line);
if (!noStoreHit && !revalidateZeroHit) continue;
const start = Math.max(0, i - 10);
const end = Math.min(lines.length, i + 5);
const window = lines.slice(start, end).join('\n');
if (AUTH_RE.test(window)) continue;
// Mutation verbs legitimately don't cache.
if (/method\s*:\s*['"](?:POST|PUT|PATCH|DELETE)['"]/i.test(window)) continue;
out.push({
pattern: metadata.id,
subtype: noStoreHit ? 'fetch-no-store' : 'fetch-revalidate-zero',
file: path,
line: i + 1,
evidence: line.trim().slice(0, 200),
trafficIndependent: metadata.trafficIndependent,
});
}
}
return out;
}
function isApplicable(path) {
return /\/(route|index|page|layout)\.(ts|tsx|js|jsx)$/.test(path) || /\/api\//.test(path);
}
lib/scanners/prisma-include-tree.mjs
import { lineOf } from '../util.mjs';
export const metadata = {
id: 'prisma-include-tree-bloat',
title: 'Deep Prisma include tree (3+ levels)',
severity: 'high',
billingDimension: 'function-duration',
trafficIndependent: false,
description:
'Nested .include({ x: { include: { y: { include: { z: ... } } } } }) makes Prisma issue a single huge join that scales O(N*M*K). Function duration explodes, memory spikes, often causes timeouts.',
fix:
'Replace with explicit .findMany() calls or scoped .include() of only what the consumer reads. Consider Prisma.select() to project specific fields. For lists, batch with DataLoader patterns.',
citations: [
'vercel-react-best-practices:server-parallel-fetching',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**'],
includeGlobs: ['**/*.{ts,tsx,js,jsx}'],
};
// Catches 3+ nesting levels of `include:` within a single object literal.
const RE = /include\s*:\s*\{[\s\S]*?include\s*:\s*\{[\s\S]*?include\s*:/g;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (/\.test\.|\.spec\./.test(path)) continue;
let m;
RE.lastIndex = 0;
while ((m = RE.exec(content)) !== null) {
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, m.index),
evidence: '3+ levels of nested Prisma .include()',
trafficIndependent: metadata.trafficIndependent,
});
// One finding per file is enough — agent investigates holistically.
break;
}
}
return out;
}
lib/scanners/region-pin-in-config.mjs
// Detects pinned function regions in vercel.json or per-route segment config.
// Provides the "configured region" signal for the region-misconfig gate when a TTFB
// breakdown by function_region isn't available (current state — see Phase 0 preflight
// in plans/wild-splashing-flamingo.md).
//
// Subtypes:
// vercel-json-single — vercel.json `regions: ["iad1"]` (single region, no failover)
// vercel-json-list — vercel.json `regions: [...]` (multi-region; informational)
// segment-preferred — `export const preferredRegion = 'iad1'` (or array)
import { lineOf } from '../util.mjs';
export const metadata = {
id: 'region-pin-in-config',
title: 'Function region pinned in config',
severity: 'low',
billingDimension: 'function-duration',
trafficIndependent: true,
description:
"vercel.json `regions` or per-route `preferredRegion` is set. If the pinned region is far from the dominant user geo (or far from a data source) p95 TTFB suffers. This scanner provides the configured-region signal so the region-misconfig gate can recommend an audit.",
fix:
"Audit the pinned region against traffic geography (Speed Insights or Web Analytics by country) and data-source location. Consider multi-region if data lives in a fixed location and users are global; consider relocating if users are concentrated in one geography.",
citations: [
'https://vercel.com/docs/functions/configuring-functions/region',
'https://vercel.com/docs/functions/configuring-functions/region',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**'],
includeGlobs: [
'vercel.json',
'**/vercel.json',
'**/page.{ts,tsx,js,jsx}',
'**/route.{ts,tsx,js,jsx}',
'**/layout.{ts,tsx,js,jsx}',
],
};
// Matches `regions: ["iad1"]`, `regions: ['iad1', 'sfo1']`, or `"regions": ["iad1"]`
const VERCEL_JSON_REGIONS_RE = /['"]?regions['"]?\s*:\s*\[([^\]]+)\]/;
// `export const preferredRegion = 'iad1'` OR `= ['iad1', 'sfo1']`
const PREFERRED_REGION_RE = /export\s+const\s+preferredRegion\s*=\s*(['"][^'"]+['"]|\[[^\]]+\])/;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
const name = path.split('/').pop();
if (name === 'vercel.json') {
const m = VERCEL_JSON_REGIONS_RE.exec(content);
if (m) {
const regions = parseRegionList(m[1]);
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, m.index),
evidence: `vercel.json regions: [${regions.join(', ')}]`,
trafficIndependent: metadata.trafficIndependent,
subtype: regions.length === 1 ? 'vercel-json-single' : 'vercel-json-list',
regions,
});
}
continue;
}
// Segment config files (page.tsx, route.ts, layout.tsx).
const m = PREFERRED_REGION_RE.exec(content);
if (m) {
const raw = m[1];
const regions = raw.startsWith('[') ? parseRegionList(raw.slice(1, -1)) : [raw.replace(/['"]/g, '')];
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, m.index),
evidence: `preferredRegion = ${raw}`,
trafficIndependent: metadata.trafficIndependent,
subtype: 'segment-preferred',
regions,
});
}
}
return out;
}
function parseRegionList(inner) {
return inner
.split(',')
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
.filter(Boolean);
}
lib/scanners/source-maps-production.mjs
import { lineOf } from '../util.mjs';
export const metadata = {
id: 'source-maps-production',
title: 'Source maps enabled in production',
severity: 'low',
billingDimension: 'edge-requests',
trafficIndependent: true,
description:
'productionBrowserSourceMaps: true ships .map files in the production bundle, increasing transfer size 30-100% per visitor. Useful for error reporting via Sentry; not useful for users.',
fix:
'Keep source maps generation but exclude them from the public bundle. Upload to your error tracker via build-time CI step; do not serve them with the deployment.',
citations: [
'https://nextjs.org/docs/messages/improper-devtool',
],
excludeGlobs: [],
includeGlobs: ['next.config.{js,mjs,ts}', 'svelte.config.{js,mjs,ts}'],
};
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!/^next\.config\.(js|mjs|ts)$/.test(path.split('/').pop() ?? '')) continue;
const m = /productionBrowserSourceMaps\s*:\s*true/.exec(content);
if (m) {
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, m.index),
evidence: 'productionBrowserSourceMaps: true',
trafficIndependent: metadata.trafficIndependent,
});
}
}
return out;
}
lib/scanners/sveltekit-prerender-missing.mjs
// Flag SvelteKit pages that haven't declared prerender/ssr/config — they
// default to per-request function execution. Pages that have already opted
// in or out are skipped; the investigator agent decides actual staticness.
export const metadata = {
id: 'sveltekit-prerender-missing',
title: 'SvelteKit page without explicit prerender / ISR config',
severity: 'low',
billingDimension: 'function-duration',
trafficIndependent: false,
description:
'SvelteKit page or +page.server.ts is missing an explicit `prerender`, `ssr`, or adapter `config.isr` declaration. Default is per-request function execution — investigate whether the route could be prerendered or ISR-cached.',
fix:
'If the page is static (no per-user / per-request data), add `export const prerender = true` in +page.ts or +page.server.ts. If the data refreshes on a schedule, prefer adapter-vercel\'s ISR option via `export const config = { isr: { expiration: 60 } }`.',
citations: [
'https://kit.svelte.dev/docs/page-options',
'https://kit.svelte.dev/docs/adapter-vercel',
'https://vercel.com/docs/incremental-static-regeneration',
],
excludeGlobs: ['node_modules/**', '.svelte-kit/**', 'build/**', '__tests__/**'],
includeGlobs: ['src/routes/**/+page.svelte', 'src/routes/**/+page.server.{ts,js}', 'src/routes/+page.svelte', 'src/routes/+page.server.{ts,js}'],
};
const PRERENDER_RE = /export\s+const\s+prerender\b/;
const SSR_RE = /export\s+const\s+ssr\b/;
const CONFIG_RE = /export\s+const\s+config\s*=\s*\{[^}]*\b(isr|prerender|runtime|split|regions)\b/;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!path.includes('/routes/')) continue;
if (PRERENDER_RE.test(content) || SSR_RE.test(content) || CONFIG_RE.test(content)) continue;
out.push({
pattern: metadata.id,
file: path,
// Absence-finding — no specific line, placeholder 1.
line: 1,
evidence: 'No `prerender`, `ssr`, or `config = { isr | runtime | ... }` export found',
trafficIndependent: metadata.trafficIndependent,
});
}
return out;
}
lib/scanners/turbo-force-bypass.mjs
// Detects Turborepo cache bypass patterns that cause every commit to rebuild every project,
// driving Build Minutes to dominate the bill on monorepos.
//
// Three signal subtypes:
// force-flag — `TURBO_FORCE=true` env var or `turbo run ... --force` in build script
// cache-disabled — `turbo.json` declares `"cache": false` for the build pipeline
// no-ignore-step — repo has turbo.json and no repo-declared ignoreCommand;
// verify Vercel's skip-unaffected project setting before recommending one
//
// This pattern has caused full-monorepo rebuilds on every commit. Build-skip
// settings and right-sized build machines can reduce Build Minutes when the
// project is rebuilding unchanged work.
export const metadata = {
id: 'turbo-force-bypass',
title: 'Turborepo cache bypass on a monorepo',
severity: 'high',
billingDimension: 'build',
trafficIndependent: true, // build-time, fires regardless of route traffic
description:
"Turborepo's per-task cache can be bypassed by an explicit force flag, a `cache: false` config, or missing build-skip configuration. Every commit can rebuild unchanged work; Build Minutes climb with project count.",
fix:
"Remove `TURBO_FORCE=true` from build env/scripts unless intentional. Set `tasks.build.cache: true` in `turbo.json` (or remove the override), and include generated outputs in Turbo's cache contract. Prefer Vercel's skip-unaffected monorepo behavior when available; use `ignoreCommand` only when that setting cannot cover the project.",
citations: [
'https://vercel.com/docs/monorepos',
'https://vercel.com/docs/builds',
'https://turborepo.dev/docs/crafting-your-repository/caching',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**'],
includeGlobs: ['turbo.json', '**/turbo.json', 'package.json', '**/package.json', 'vercel.json', '**/vercel.json'],
};
const FORCE_ENV_RE = /TURBO_FORCE\s*=\s*(?:true|1)\b/;
const FORCE_FLAG_RE = /\bturbo\s+(?:run\s+)?[a-z:_-]+[^\n&|;]*\s--force\b/;
export function scan({ files }) {
const out = [];
let hasTurboJson = false;
let vercelJsonFile = null;
let vercelJsonContent = null;
for (const { path, content } of files) {
const name = path.split('/').pop();
if (name === 'turbo.json') {
hasTurboJson = true;
const buildCacheDisabled = detectBuildCacheDisabled(content);
if (buildCacheDisabled) {
out.push({
pattern: metadata.id,
file: path,
line: buildCacheDisabled.line,
evidence: 'turbo.json: tasks.build.cache = false',
trafficIndependent: metadata.trafficIndependent,
subtype: 'cache-disabled',
});
}
continue;
}
if (name === 'package.json') {
const scripts = safeScripts(content);
for (const [scriptName, body] of Object.entries(scripts)) {
if (FORCE_ENV_RE.test(body) || FORCE_FLAG_RE.test(body)) {
const line = lineOfMatch(content, body) ?? 1;
out.push({
pattern: metadata.id,
file: path,
line,
evidence: `package.json scripts.${scriptName}: ${truncate(body, 80)}`,
trafficIndependent: metadata.trafficIndependent,
subtype: 'force-flag',
});
}
}
continue;
}
if (name === 'vercel.json') {
vercelJsonFile = path;
vercelJsonContent = content;
}
}
// No-ignore-step: repo has turbo.json AND vercel.json lacks an ignoreCommand.
// This is an investigation prompt, not proof that the dashboard skip setting is off.
if (hasTurboJson && vercelJsonFile && !/"ignoreCommand"\s*:/.test(vercelJsonContent ?? '')) {
out.push({
pattern: metadata.id,
file: vercelJsonFile,
line: 1,
evidence: 'turbo repo without ignoreCommand in vercel.json; verify Vercel skip-unaffected setting',
trafficIndependent: metadata.trafficIndependent,
subtype: 'no-ignore-step',
});
}
return out;
}
function detectBuildCacheDisabled(content) {
// Tolerate JSONC comments and trailing commas — light scan, not full parse.
// Match `"build": { ... "cache": false ... }` within reasonable lookahead.
const buildTask = /"build"\s*:\s*\{([\s\S]{0,400}?)\}/.exec(content);
if (!buildTask) return null;
if (!/"cache"\s*:\s*false/.test(buildTask[1])) return null;
const lineNum = content.slice(0, buildTask.index).split('\n').length;
return { line: lineNum };
}
function safeScripts(content) {
try {
const parsed = JSON.parse(content);
return parsed?.scripts && typeof parsed.scripts === 'object' ? parsed.scripts : {};
} catch {
return {};
}
}
function lineOfMatch(haystack, needle) {
const idx = haystack.indexOf(needle);
if (idx < 0) return null;
return haystack.slice(0, idx).split('\n').length;
}
function truncate(s, n) {
if (typeof s !== 'string') return '';
return s.length > n ? s.slice(0, n - 1) + '…' : s;
}
lib/scanners/unoptimized-image.mjs
// Four image-optimization checks emitted as `unoptimized-image` findings.
// `subtype` distinguishes raw-img / global-unoptimized / image-fill-no-sizes
// / image-svg-no-unoptimized so the recommender can frame each separately.
export const metadata = {
id: 'unoptimized-image',
title: 'Image optimization gap (raw <img>, global flag, missing sizes, or SVG mis-routed)',
severity: 'high',
billingDimension: 'image-optimization',
trafficIndependent: false,
description:
'Four shapes of image-cost waste: raw <img> tags bypass the framework Image component; `images.unoptimized: true` disables Vercel image optimization globally; <Image fill> without `sizes` forces serving the largest source variant; <Image src=".svg"> without `unoptimized` routes vector data through the raster pipeline.',
fix:
'For raw <img>: switch to next/image, enhanced-img (SvelteKit), <Image /> (Astro), or NuxtImg. For global unoptimized:true: remove the flag unless the project is hosted outside Vercel. For fill without sizes: add `sizes="(max-width: 768px) 100vw, 50vw"` or whatever matches your layout. For SVG: add `unoptimized` so the raw SVG ships instead of rastering it.',
citations: [
'https://nextjs.org/docs/app/api-reference/components/image',
'https://vercel.com/docs/image-optimization',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**', 'cypress/**', '*.test.*'],
includeGlobs: ['**/*.{tsx,jsx,html,svelte,astro,vue,js,mjs,ts}'],
};
const IMG_RE = /<img\s+[^>]*src\s*=\s*["'{`]/g;
const GLOBAL_UNOPT_RE = /images\s*:\s*\{[^}]*\bunoptimized\s*:\s*true/;
const IMAGE_TAG_RE = /<Image\b[^>]*?\/?>/g;
const NEXT_IMAGE_IMPORT_RE = /from\s+['"]next\/image['"]/;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (isJsxLike(path)) {
let m;
IMG_RE.lastIndex = 0;
while ((m = IMG_RE.exec(content)) !== null) {
out.push({
pattern: metadata.id,
subtype: 'raw-img',
file: path,
line: lineOf(content, m.index),
evidence: snippet(content, m.index),
trafficIndependent: metadata.trafficIndependent,
});
}
}
if (isNextConfig(path)) {
const match = GLOBAL_UNOPT_RE.exec(content);
if (match) {
out.push({
pattern: metadata.id,
subtype: 'global-unoptimized',
file: path,
line: lineOf(content, match.index),
evidence: 'images: { unoptimized: true } — disables Vercel image optimization for the entire project',
// Config-level flag affects every image regardless of route.
trafficIndependent: true,
});
}
}
// Only fire if next/image is imported — otherwise `Image` is some
// other component.
if (isJsxLike(path) && NEXT_IMAGE_IMPORT_RE.test(content)) {
let m;
IMAGE_TAG_RE.lastIndex = 0;
while ((m = IMAGE_TAG_RE.exec(content)) !== null) {
const tag = m[0];
const hasFill = /\bfill\b/.test(tag);
const hasSizes = /\bsizes\s*=/.test(tag);
if (hasFill && !hasSizes) {
out.push({
pattern: metadata.id,
subtype: 'image-fill-no-sizes',
file: path,
line: lineOf(content, m.index),
evidence: tag.slice(0, 200),
trafficIndependent: metadata.trafficIndependent,
});
}
// Inline data: URLs never round-trip through the optimizer.
const srcMatch = /\bsrc\s*=\s*["']([^"']+)["']/.exec(tag);
if (srcMatch) {
const src = srcMatch[1];
if (/\.svg(\?|$)/i.test(src) && !src.startsWith('data:') && !/\bunoptimized\b/.test(tag)) {
out.push({
pattern: metadata.id,
subtype: 'image-svg-no-unoptimized',
file: path,
line: lineOf(content, m.index),
evidence: tag.slice(0, 200),
trafficIndependent: metadata.trafficIndependent,
});
}
}
}
}
}
return out;
}
import { lineOf } from '../util.mjs';
function isJsxLike(path) {
return /\.(tsx|jsx|html|svelte|astro|vue)$/.test(path);
}
function isNextConfig(path) {
return /(?:^|\/)next\.config\.(js|mjs|ts|cjs)$/.test(path);
}
function snippet(text, idx) {
const start = text.lastIndexOf('\n', idx) + 1;
const end = text.indexOf('\n', idx);
return text.slice(start, end === -1 ? text.length : end).trim().slice(0, 160);
}
lib/scanners/use-cache-date-stamp.mjs
// Detects time/randomness primitives that destabilize `'use cache'` cache keys,
// which manifests as ISR write amplification when the cached output embeds a timestamp
// that changes per request.
//
// Triggers when a file contains the `'use cache'` directive AND uses `new Date(`,
// `Date.now(`, or `Math.random(` outside client-only hooks (useEffect / useCallback /
// useMemo). Replacing module-scope `new Date().getFullYear()` with a build-time
// `buildYear` constant, and removing dates passed as `'use cache'` function
// arguments, prevents repeated writes when the rendered output is otherwise stable.
import { lineOf } from '../util.mjs';
export const metadata = {
id: 'use-cache-date-stamp',
title: "new Date() / Date.now() / Math.random() inside a 'use cache' file",
severity: 'high',
billingDimension: 'isr',
trafficIndependent: false,
description:
"`'use cache'` memoizes by argument identity AND prerender output. A timestamp baked into the cached output (`new Date().getFullYear()` in a footer, `Date.now()` in a payload field) forces a fresh ISR write on every regeneration even when the underlying data is unchanged. Random values have the same failure mode.",
fix:
"Replace module-scope `new Date()` with a build-time constant (`const buildYear = new Date().getFullYear()`) or move per-request timestamps into a client component inside `useEffect`. Do not pass dates as arguments to `'use cache'` functions — they invalidate the cache every call.",
citations: [
'https://nextjs.org/docs/app/api-reference/directives/use-cache',
'https://nextjs.org/docs/app/api-reference/functions/cacheLife',
],
excludeGlobs: ['node_modules/**', '.next/**', 'dist/**', '__tests__/**', '**/*.test.*', '**/*.spec.*'],
includeGlobs: [
'**/page.{ts,tsx,js,jsx}',
'**/layout.{ts,tsx,js,jsx}',
'**/route.{ts,tsx,js,jsx}',
'**/lib/**/*.{ts,tsx,js,jsx}',
'**/app/**/*.{ts,tsx,js,jsx}',
'**/components/**/*.{ts,tsx,js,jsx}',
],
};
const USE_CACHE_RE = /^[\t ]*['"]use cache['"]/m;
const SUSPECT_RE = /\b(new Date\(|Date\.now\(|Math\.random\()/g;
// Client-only hooks that don't affect server-side cache keys.
const CLIENT_HOOK_RE = /\b(useEffect|useCallback|useMemo|useLayoutEffect)\s*\(/g;
export function scan({ files }) {
const out = [];
for (const { path, content } of files) {
if (!USE_CACHE_RE.test(content)) continue;
const clientHookRanges = collectRanges(content, CLIENT_HOOK_RE);
let match;
SUSPECT_RE.lastIndex = 0;
while ((match = SUSPECT_RE.exec(content)) !== null) {
if (isInsideAnyRange(match.index, clientHookRanges)) continue;
out.push({
pattern: metadata.id,
file: path,
line: lineOf(content, match.index),
evidence: match[0],
trafficIndependent: metadata.trafficIndependent,
subtype: classifySubtype(content, match.index),
});
}
}
return out;
}
function collectRanges(content, hookRe) {
const ranges = [];
hookRe.lastIndex = 0;
let m;
while ((m = hookRe.exec(content)) !== null) {
const open = content.indexOf('(', m.index);
if (open < 0) continue;
const close = findMatchingParen(content, open);
if (close < 0) continue;
ranges.push([open, close]);
}
return ranges;
}
function findMatchingParen(content, openIdx) {
let depth = 0;
for (let i = openIdx; i < content.length; i++) {
const c = content[i];
if (c === '(') depth++;
else if (c === ')') {
depth--;
if (depth === 0) return i;
}
}
return -1;
}
function isInsideAnyRange(idx, ranges) {
for (const [a, b] of ranges) {
if (idx >= a && idx <= b) return true;
}
return false;
}
// `module-scope` if the suspect appears before the first function/class declaration.
// `in-cache-fn` otherwise (likely inside a render or helper function body).
function classifySubtype(content, idx) {
const head = content.slice(0, idx);
if (!/\bfunction\b|\bclass\b|=>\s*\{/.test(head)) return 'module-scope';
return 'in-cache-fn';
}
lib/support-topics.mjs
import { readdir, readFile } from 'node:fs/promises';
import { dirname, join, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gates } from './gates/index.mjs';
import { SCANNER_GATES } from './gates/scanner-driven.mjs';
import {
loadLibrary,
lookupSkillRule,
lookupUrl,
matchesFrameworkVersion,
} from './citations.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const TOPICS_DIR = join(HERE, '..', 'references', 'support-topics');
export const SUPPORT_TOPIC_LIMIT = 3;
export const SUPPORT_TOPIC_TOTAL_CHAR_LIMIT = 2400;
const DEFAULT_MAX_BRIEF_CHARS = 900;
export const KNOWN_CANDIDATE_KINDS = new Set([
...gates
.map((g) => g.metadata?.id)
.filter((id) => id && id !== 'scanner-driven'),
...SCANNER_GATES.map((g) => g.id),
]);
export async function supportTopicSubset({
candidate,
signals = {},
framework,
version,
profile,
frameworkPlaybookId,
maxTopics = SUPPORT_TOPIC_LIMIT,
maxChars = SUPPORT_TOPIC_TOTAL_CHAR_LIMIT,
} = {}) {
const stack = signals?.stack ?? signals?.codebase?.stack ?? {};
const fw = framework ?? stack.framework ?? 'unknown';
const fwVersion = version ?? stack.frameworkVersion ?? 'unknown';
const candidates = await loadSupportTopics();
const selected = [];
let usedChars = 0;
const sorted = candidates
.filter((t) => t.status === 'active')
.filter((t) => matchesCandidateKind(t, candidate?.kind))
.filter((t) => matchesFrameworks(t.frameworks, fw, fwVersion))
.filter((t) => matchesOptionalList(t.profiles, profile))
.filter((t) => matchesOptionalList(t.frameworkPlaybooks, frameworkPlaybookId))
.filter((t) => matchesRouter(t.routers, stack))
.filter((t) => matchesCandidateMetrics(t.metrics, candidate))
.filter((t) => matchesCandidateRoutePatterns(t.routePatterns, candidate))
.filter((t) => matchesScannerPatterns(t.scannerPatterns, candidate))
.sort((a, b) => b.priority - a.priority || a.id.localeCompare(b.id));
for (const topic of sorted) {
if (!await topicCitationsApply(topic, candidate?.kind, fw, fwVersion)) continue;
if (selected.length >= maxTopics) break;
const renderedChars = topic.title.length + topic.body.length + topic.id.length + 20;
if (selected.length > 0 && usedChars + renderedChars > maxChars) continue;
selected.push(topic);
usedChars += renderedChars;
}
return selected;
}
export async function loadSupportTopics({ includeDraft = false } = {}) {
let names = [];
try {
names = await readdir(TOPICS_DIR);
} catch (err) {
if (err?.code === 'ENOENT') return [];
throw err;
}
const topics = [];
for (const name of names.sort()) {
if (!name.endsWith('.md') || name === 'README.md') continue;
const path = join(TOPICS_DIR, name);
const raw = await readFile(path, 'utf-8');
const topic = parseSupportTopic(raw, path);
if (includeDraft || topic.status === 'active') topics.push(topic);
}
return topics.sort((a, b) => a.id.localeCompare(b.id));
}
export async function validateSupportTopics() {
const topics = await loadSupportTopics({ includeDraft: true });
const errors = [];
const seen = new Set();
for (const topic of topics) {
errors.push(...await validateSupportTopic(topic));
if (seen.has(topic.id)) errors.push(`${topic.path}: duplicate topic id "${topic.id}"`);
seen.add(topic.id);
}
return { ok: errors.length === 0, errors, topics };
}
export function renderSupportTopics(topics = []) {
if (!Array.isArray(topics) || topics.length === 0) return [];
const lines = [];
lines.push('## Support topics (investigation guardrails)');
lines.push('');
lines.push('These are deterministic, candidate-scoped hints selected from `references/support-topics/`. They do not create recommendations. Use them only to decide what evidence to check, what to rule out, and when to abstain.');
lines.push('');
for (const topic of topics) {
lines.push(`### ${topic.title} (\`${topic.id}\`)`);
lines.push('');
lines.push(topic.body.trim());
lines.push('');
}
return lines;
}
export function parseSupportTopic(raw, path = '<memory>') {
const { frontmatter, body } = splitFrontmatter(raw, path);
const metadata = parseFrontmatter(frontmatter, path);
return normalizeTopic({ ...metadata, body: body.trim(), path });
}
function splitFrontmatter(raw, path) {
const text = String(raw ?? '');
if (!text.startsWith('---\n')) {
throw new Error(`${path}: support topic must start with --- frontmatter`);
}
const end = text.indexOf('\n---\n', 4);
if (end === -1) {
throw new Error(`${path}: support topic frontmatter must end with ---`);
}
return {
frontmatter: text.slice(4, end),
body: text.slice(end + '\n---\n'.length),
};
}
function parseFrontmatter(src, path) {
const out = {};
for (const rawLine of src.split('\n')) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const m = line.match(/^([A-Za-z][A-Za-z0-9]*):\s*(.*)$/);
if (!m) throw new Error(`${path}: unsupported frontmatter line "${rawLine}"`);
const [, key, value] = m;
out[key] = parseFrontmatterValue(value, path, key);
}
return out;
}
function parseFrontmatterValue(value, path, key) {
if (value.startsWith('[')) {
try {
const parsed = JSON.parse(value);
if (!Array.isArray(parsed)) throw new Error('not an array');
return parsed;
} catch (err) {
throw new Error(`${path}: ${key} must use strict JSON array syntax (${err.message})`);
}
}
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
if (value === 'true') return true;
if (value === 'false') return false;
if (value === 'null') return null;
const quoted = value.match(/^"(.*)"$/) ?? value.match(/^'(.*)'$/);
return quoted ? quoted[1] : value;
}
function normalizeTopic(topic) {
const maxBriefChars = Number.isFinite(topic.maxBriefChars)
? topic.maxBriefChars
: DEFAULT_MAX_BRIEF_CHARS;
return {
id: topic.id,
title: topic.title,
status: topic.status,
candidateKinds: toStringArray(topic.candidateKinds),
frameworks: toStringArray(topic.frameworks),
profiles: toStringArray(topic.profiles),
frameworkPlaybooks: toStringArray(topic.frameworkPlaybooks),
routers: toStringArray(topic.routers),
metrics: toStringArray(topic.metrics),
routePatterns: toStringArray(topic.routePatterns),
scannerPatterns: toStringArray(topic.scannerPatterns),
billingDimensions: toStringArray(topic.billingDimensions),
citations: toStringArray(topic.citations),
priority: Number(topic.priority),
maxBriefChars,
body: topic.body,
path: topic.path,
};
}
async function validateSupportTopic(topic) {
const errors = [];
const label = topic.path ?? topic.id ?? '<topic>';
const fileId = basename(label).replace(/\.md$/, '');
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(topic.id ?? '')) {
errors.push(`${label}: id must be kebab-case`);
}
if (fileId !== topic.id) errors.push(`${label}: filename must match id`);
if (!nonEmptyString(topic.title)) errors.push(`${label}: title is required`);
if (!['active', 'draft', 'deprecated'].includes(topic.status)) {
errors.push(`${label}: status must be active, draft, or deprecated`);
}
if (!Number.isFinite(topic.priority)) errors.push(`${label}: priority must be a number`);
if (!Number.isFinite(topic.maxBriefChars) || topic.maxBriefChars < 200 || topic.maxBriefChars > 1400) {
errors.push(`${label}: maxBriefChars must be between 200 and 1400`);
}
if (!nonEmptyArray(topic.candidateKinds)) {
errors.push(`${label}: candidateKinds must be a non-empty array`);
} else {
for (const kind of topic.candidateKinds) {
if (kind !== '*' && !KNOWN_CANDIDATE_KINDS.has(kind)) {
errors.push(`${label}: unknown candidate kind "${kind}"`);
}
}
}
if (!nonEmptyArray(topic.frameworks)) {
errors.push(`${label}: frameworks must be a non-empty array`);
} else {
for (const fw of topic.frameworks) {
if (fw !== '*' && !/^[\w-]+@/.test(fw)) {
errors.push(`${label}: framework "${fw}" must be "*" or "framework@range"`);
}
}
}
if (!nonEmptyArray(topic.citations)) {
errors.push(`${label}: citations must be a non-empty array`);
} else {
for (const citation of topic.citations) {
if (!await knownCitation(citation)) {
errors.push(`${label}: unknown citation "${citation}"`);
}
}
}
for (const pattern of topic.routePatterns) {
try {
new RegExp(pattern);
} catch (err) {
errors.push(`${label}: invalid routePatterns regex "${pattern}" (${err.message})`);
}
}
for (const heading of [
'## Investigation Brief',
'## Evidence To Check',
'## Do Not Recommend When',
'## Verification',
]) {
if (!topic.body.includes(heading)) errors.push(`${label}: missing heading "${heading}"`);
}
if (topic.body.length > topic.maxBriefChars) {
errors.push(`${label}: body length ${topic.body.length} exceeds maxBriefChars ${topic.maxBriefChars}`);
}
if (/https?:\/\//.test(topic.body)) {
errors.push(`${label}: put URLs in frontmatter citations, not body text`);
}
if (/\/Users\/|(?:^|[\s`"'])apps\/[^/\s`"']+\/|[A-Za-z0-9_-]+\.ts:\d+/.test(topic.body)) {
errors.push(`${label}: body leaks internal implementation details`);
}
return errors;
}
function matchesCandidateKind(topic, candidateKind) {
if (!candidateKind) return false;
return topic.candidateKinds.includes('*') || topic.candidateKinds.includes(candidateKind);
}
function matchesFrameworks(frameworks, framework, version) {
return frameworks.some((pattern) =>
pattern === '*' || matchesFrameworkVersion(pattern, framework, version)
);
}
function matchesOptionalList(values, actual) {
if (!Array.isArray(values) || values.length === 0) return true;
return values.includes('*') || (actual != null && values.includes(actual));
}
function matchesRouter(routers, stack) {
if (!Array.isArray(routers) || routers.length === 0) return true;
if (routers.includes('*')) return true;
return (routers.includes('app') && stack?.hasAppRouter)
|| (routers.includes('pages') && stack?.hasPagesRouter);
}
function matchesCandidateMetrics(metrics, candidate) {
if (!Array.isArray(metrics) || metrics.length === 0) return true;
if (metrics.includes('*')) return true;
const observed = new Set([
candidate?.evidence?.metric,
...(candidate?.evidence?.issues ?? []).map((i) => i?.metric),
].filter(Boolean).map((m) => String(m).toUpperCase()));
return metrics.some((m) => observed.has(String(m).toUpperCase()));
}
function matchesCandidateRoutePatterns(patterns, candidate) {
if (!Array.isArray(patterns) || patterns.length === 0) return true;
if (patterns.includes('*')) return true;
const route = candidate?.route ?? candidate?.path;
if (typeof route !== 'string' || route.length === 0) return false;
return patterns.some((p) => new RegExp(p).test(route));
}
function matchesScannerPatterns(patterns, candidate) {
if (!Array.isArray(patterns) || patterns.length === 0) return true;
const observed = new Set([
...(candidate?.evidence?.patterns ?? []),
...(candidate?.evidence?.deepDive?.patterns ?? []),
].filter(Boolean));
if (observed.size === 0) return false;
return patterns.some((p) => observed.has(p));
}
function topicCitationsApply(topic, candidateKind, framework, version) {
if (!candidateKind) return false;
return topic.citations.every((citation) =>
citationApplies(citation, candidateKind, framework, version)
);
}
async function citationApplies(citation, candidateKind, framework, version) {
const lib = await loadLibrary();
const rule = lib.ruleSkillRefs.find((r) => `${r.skill}:${r.rule}` === citation);
if (rule) {
return rule.applicableFrameworks.includes('*')
|| rule.applicableFrameworks.some((p) => matchesFrameworkVersion(p, framework, version));
}
const url = lib.urls.find((u) => u.url === citation);
if (!url) return false;
const kindOk = !Array.isArray(url.appliesTo)
|| url.appliesTo.length === 0
|| url.appliesTo.includes(candidateKind);
const versionOk = url.applicableFrameworks.includes('*')
|| url.applicableFrameworks.some((p) => matchesFrameworkVersion(p, framework, version));
return kindOk && versionOk;
}
async function knownCitation(citation) {
return Boolean(await lookupUrl(citation) || await lookupSkillRule(citation));
}
function toStringArray(value) {
if (!Array.isArray(value)) return [];
return value.filter((v) => typeof v === 'string' && v.length > 0);
}
function nonEmptyArray(value) {
return Array.isArray(value) && value.length > 0;
}
function nonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
lib/throttle.mjs
// Zero-dependency concurrency + rate-limit primitives for `vercel metrics`. API cap is 100 req / 60s / team.
// CLI fails fast on 429 and doesn't surface Retry-After, so we back off blind.
const DEFAULT_CONCURRENCY = 8;
const DEFAULT_MAX_RETRIES = 3;
// Wait most of a 60s window when rate-limited — we don't know how much headroom remains. Jitter prevents lockstep retry.
const BASE_BACKOFF_MS = 60_000;
const JITTER_MS = 15_000;
// 20% headroom under the 100/60s cap for the user's other concurrent CLI usage.
const DEFAULT_RATE_LIMIT = 80;
const DEFAULT_RATE_WINDOW_MS = 60_000;
const DAILY_OBSERVABILITY_LIMIT_RE = /daily.*observability.*query limit/i;
let dailyQuotaBlock = null;
export function resolveConcurrency() {
return parsePositiveIntEnv('VERCEL_OPTIMIZE_METRIC_CONCURRENCY', DEFAULT_CONCURRENCY);
}
// Format: VERCEL_OPTIMIZE_METRIC_RATE=N or N/60s.
export function resolveRateLimit() {
const env = process.env.VERCEL_OPTIMIZE_METRIC_RATE;
if (env == null || env === '') return { maxCalls: DEFAULT_RATE_LIMIT, windowMs: DEFAULT_RATE_WINDOW_MS };
const m = String(env).trim().match(/^(\d+)(?:\/(\d+)([sm])?)?$/);
if (!m) return { maxCalls: DEFAULT_RATE_LIMIT, windowMs: DEFAULT_RATE_WINDOW_MS };
const maxCalls = Number(m[1]);
if (!Number.isInteger(maxCalls) || maxCalls < 1) {
return { maxCalls: DEFAULT_RATE_LIMIT, windowMs: DEFAULT_RATE_WINDOW_MS };
}
if (!m[2]) return { maxCalls, windowMs: DEFAULT_RATE_WINDOW_MS };
const unit = m[3] === 'm' ? 60_000 : 1_000;
const windowMs = Number(m[2]) * unit;
return { maxCalls, windowMs };
}
function parsePositiveIntEnv(name, defaultValue) {
const env = process.env[name];
if (env == null || env === '') return defaultValue;
const n = Number(env);
if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) return defaultValue;
return n;
}
// FIFO semaphore. Caller MUST call returned release() exactly once.
export class SemaphoreAbortError extends Error {
constructor(result) {
super('Semaphore acquire aborted');
this.name = 'SemaphoreAbortError';
this.result = result;
}
}
export class Semaphore {
constructor(max) {
if (!Number.isInteger(max) || max < 1) {
throw new Error(`Semaphore: max must be a positive integer (got ${max})`);
}
this.max = max;
this.inFlight = 0;
this.waiters = [];
}
async acquire(opts = {}) {
const abortIf = opts.abortIf;
const preAbort = abortIf?.();
if (preAbort) throw new SemaphoreAbortError(preAbort);
if (this.inFlight < this.max) {
this.inFlight++;
return () => this.release();
}
await new Promise((resolve) => this.waiters.push(resolve));
const postAbort = abortIf?.();
if (postAbort) {
this.wakeNext();
throw new SemaphoreAbortError(postAbort);
}
this.inFlight++;
return () => this.release();
}
release() {
this.inFlight--;
this.wakeNext();
}
wakeNext() {
const next = this.waiters.shift();
if (next) next();
}
async run(fn, opts = {}) {
const release = await this.acquire(opts);
try {
return await fn();
} finally {
release();
}
}
}
// Load-bearing — semaphore alone is insufficient (8 concurrent × ~1s queries = 480/min, well above the 100/min cap).
export class SlidingWindowRateLimiter {
constructor(maxCalls, windowMs, opts = {}) {
if (!Number.isInteger(maxCalls) || maxCalls < 1) {
throw new Error(`SlidingWindowRateLimiter: maxCalls must be >=1 (got ${maxCalls})`);
}
if (!Number.isFinite(windowMs) || windowMs < 1) {
throw new Error(`SlidingWindowRateLimiter: windowMs must be >0 (got ${windowMs})`);
}
this.maxCalls = maxCalls;
this.windowMs = windowMs;
this.timestamps = []; // ascending order
this.now = opts.now ?? (() => Date.now());
this.sleep = opts.sleep ?? defaultSleep;
}
async acquire() {
while (true) {
this.prune();
if (this.timestamps.length < this.maxCalls) {
this.timestamps.push(this.now());
return;
}
// Small buffer avoids racing the window boundary.
const oldestExpiresAt = this.timestamps[0] + this.windowMs;
const sleepMs = Math.max(50, oldestExpiresAt - this.now() + 100);
await this.sleep(sleepMs);
}
}
prune() {
const cutoff = this.now() - this.windowMs;
while (this.timestamps.length > 0 && this.timestamps[0] < cutoff) {
this.timestamps.shift();
}
}
}
// Composes Semaphore + RateLimiter: bounds both burst (8 concurrent) and sustained throughput (80/60s).
let metricThrottleSingleton = null;
export function getMetricThrottle() {
if (!metricThrottleSingleton) {
const semaphore = new Semaphore(resolveConcurrency());
const { maxCalls, windowMs } = resolveRateLimit();
const rateLimiter = new SlidingWindowRateLimiter(maxCalls, windowMs);
metricThrottleSingleton = {
semaphore,
rateLimiter,
maxCalls,
windowMs,
async run(fn) {
const cached = getDailyQuotaBlock();
if (cached) return dailyQuotaResult(cached);
let release;
try {
release = await semaphore.acquire({ abortIf: () => {
const block = getDailyQuotaBlock();
return block ? dailyQuotaResult(block) : null;
} });
} catch (err) {
if (err instanceof SemaphoreAbortError) return err.result;
throw err;
}
try {
const afterAcquire = getDailyQuotaBlock();
if (afterAcquire) return dailyQuotaResult(afterAcquire);
await rateLimiter.acquire();
const result = await fn();
if (isDailyQuotaExceeded(result)) {
const block = setDailyQuotaBlocked(result);
return dailyQuotaResult(block, result);
}
return result;
} finally {
release();
}
},
};
}
return metricThrottleSingleton;
}
// Back-compat alias — returns the throttle object (compatible `.run(fn)` shape).
export const getMetricSemaphore = getMetricThrottle;
export function _resetMetricSemaphoreForTests() {
metricThrottleSingleton = null;
dailyQuotaBlock = null;
}
export async function retryOnRateLimit(fn, opts = {}) {
const maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
const baseBackoffMs = opts.baseBackoffMs ?? BASE_BACKOFF_MS;
const jitterMs = opts.jitterMs ?? JITTER_MS;
const sleep = opts.sleep ?? defaultSleep;
const onRetry = opts.onRetry;
let attempt = 0;
while (true) {
const result = await fn();
if (!isRateLimited(result) || attempt >= maxRetries) return result;
attempt++;
// attempt 1 = 1x, 2 = 1.5x, 3 = 2x of base.
const factor = 1 + (attempt - 1) * 0.5;
const jitter = jitterMs > 0 ? Math.random() * jitterMs : 0;
const delay = Math.round(baseBackoffMs * factor + jitter);
if (onRetry) onRetry(attempt, delay, result);
await sleep(delay);
}
}
// Variants: code='RATE_LIMITED' (canonical), 'rate_limited', or 'EXIT_1' + stderr match.
export function isRateLimited(result) {
if (!result || result.ok !== false) return false;
const code = String(result.code ?? '').toLowerCase();
if (code === 'rate_limited' || code === '429') return true;
const stderr = String(result.stderr ?? '').toLowerCase();
if (stderr.includes('rate limit') || stderr.includes('rate_limited') || stderr.includes('too many requests')) {
return true;
}
return false;
}
export function isDailyQuotaExceeded(result) {
if (!result || result.ok !== false) return false;
const code = String(result.code ?? '');
if (code.toUpperCase() === 'DAILY_QUOTA_EXCEEDED') return true;
const haystack = [
result.message,
result.stderr,
result.stdout,
result.detail,
].filter(Boolean).join('\n');
return DAILY_OBSERVABILITY_LIMIT_RE.test(haystack);
}
export function setDailyQuotaBlocked(result, nowMs = Date.now()) {
dailyQuotaBlock = {
untilMs: utcMidnightAfter(nowMs),
originalCode: result?.code ?? null,
message: result?.message || result?.stderr || 'Daily Observability query limit reached.',
};
return dailyQuotaBlock;
}
export function getDailyQuotaBlock(nowMs = Date.now()) {
if (!dailyQuotaBlock) return null;
if (dailyQuotaBlock.untilMs <= nowMs) {
dailyQuotaBlock = null;
return null;
}
return dailyQuotaBlock;
}
export function utcMidnightAfter(nowMs) {
const d = new Date(nowMs);
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1);
}
function dailyQuotaResult(block, sourceResult = null) {
return {
...(sourceResult && typeof sourceResult === 'object' ? sourceResult : {}),
ok: false,
code: 'DAILY_QUOTA_EXCEEDED',
message: block.message,
cachedUntil: new Date(block.untilMs).toISOString(),
originalCode: sourceResult?.originalCode ?? sourceResult?.code ?? block.originalCode ?? undefined,
};
}
function defaultSleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
lib/util.mjs
// Shared scanner + sanitizer helpers. Keep tiny — add only when duplicated 3+ times.
// 1-based line number of `idx` in a multi-line string.
export function lineOf(text, idx) {
return text.slice(0, idx).split('\n').length;
}
export function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// `slow_route:/api/products` → `/api/products`.
export function extractRoute(rec) {
if (typeof rec?.candidateRef !== 'string') return null;
const m = rec.candidateRef.match(/^[^:]+:(.+)$/);
return m ? m[1] : null;
}
lib/vercel.mjs
// Vercel CLI helpers. All shell-outs use execFile (not exec) — no shell injection. Error detection: exit code + JSON-parse first; stderr grep only as fallback (CLI error strings aren't a stable contract).
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { existsSync, readFileSync } from 'node:fs';
import { readFile, access } from 'node:fs/promises';
import { join, win32 } from 'node:path';
import { getMetricThrottle, isDailyQuotaExceeded, retryOnRateLimit } from './throttle.mjs';
const exec = promisify(execFile);
// On Windows, execFile cannot run the `vercel.cmd` shim directly: PATHEXT is not
// applied by execFile, and .cmd/.bat require `shell: true` since Node 20 — but a
// shell would mangle args containing spaces or URL query strings (e.g.
// `vercel api '/v9/projects/:id?teamId=:org'`, `-f 'http_status ge 500'`). So we
// resolve the Vercel package's JS entry from PATH and run it via `node` directly:
// no shell, every arg passed verbatim. POSIX is unchanged (`vercel` on PATH).
export function resolveVercelCommand({
platform = process.platform,
env = process.env,
execPath = process.execPath,
exists = existsSync,
readText = (file) => readFileSync(file, 'utf-8'),
} = {}) {
if (platform !== 'win32') return { file: 'vercel', prefix: [] };
const pathValue = env.PATH || env.Path || env.path || '';
for (const dir of pathValue.split(win32.delimiter).filter(Boolean)) {
const entry = resolveVercelPackageEntry(dir, exists);
if (entry) return { file: execPath, prefix: [entry] };
const shimEntry = resolveVercelShimEntry(dir, exists, readText);
if (shimEntry) return { file: execPath, prefix: [shimEntry] };
}
return { file: execPath, prefix: [], missing: true };
}
function resolveVercelPackageEntry(dir, exists) {
const packageRoots = [
win32.join(dir, 'node_modules', 'vercel'),
win32.join(win32.dirname(dir), 'vercel'),
];
for (const root of packageRoots) {
for (const rel of ['dist/vc.js', 'dist/index.js']) {
const entry = win32.join(root, rel);
if (exists(entry)) return entry;
}
}
return null;
}
function resolveVercelShimEntry(dir, exists, readText) {
for (const bin of ['vercel.cmd', 'vc.cmd']) {
const shim = win32.join(dir, bin);
if (!exists(shim)) continue;
let raw;
try {
raw = readText(shim);
} catch {
continue;
}
const match = raw.match(/["']([^"'\r\n]*vercel[\\/]+dist[\\/]+(?:vc|index)\.js)["']/i);
if (!match) continue;
const entry = normalizeWindowsShimTarget(match[1], dir);
if (exists(entry)) return entry;
}
return null;
}
function normalizeWindowsShimTarget(target, dir) {
const baseDir = `${dir}${win32.sep}`;
const expanded = target
.replace(/%~dp0/gi, baseDir)
.replace(/%dp0%/gi, baseDir)
.replace(/\$basedir/g, dir);
return win32.normalize(win32.isAbsolute(expanded) ? expanded : win32.resolve(dir, expanded));
}
async function runVercel(args, opts = {}) {
const command = resolveVercelCommand({ env: opts.env });
if (command.missing) {
const err = new Error('VERCEL_NOT_INSTALLED: `vercel` CLI not found in PATH. Install with `npm i -g vercel@latest`.');
err.code = 'ENOENT';
throw err;
}
return await exec(command.file, [...command.prefix, ...args], { windowsHide: true, ...opts });
}
const MIN_CLI_VERSION = [53, 0, 0];
// Pre-v53 lacks `vercel metrics` and `vercel contract`.
export async function checkCliVersion() {
let raw;
try {
const { stdout } = await runVercel(['--version']);
raw = stdout.trim();
} catch (err) {
throw new Error('VERCEL_NOT_INSTALLED: `vercel` CLI not found in PATH. Install with `npm i -g vercel@latest`.');
}
const m = raw.match(/(\d+)\.(\d+)\.(\d+)/);
if (!m) throw new Error(`VERCEL_VERSION_UNPARSEABLE: ${raw}`);
const v = [Number(m[1]), Number(m[2]), Number(m[3])];
for (let i = 0; i < 3; i++) {
if (v[i] > MIN_CLI_VERSION[i]) return v;
if (v[i] < MIN_CLI_VERSION[i]) {
throw new Error(
`VERCEL_CLI_TOO_OLD: have ${v.join('.')}, need >= ${MIN_CLI_VERSION.join('.')}. Upgrade with \`npm i -g vercel@latest\`.`
);
}
}
return v;
}
export async function checkAuth() {
try {
await runVercel(['whoami']);
} catch {
throw new Error('NOT_AUTH: run `vercel login`.');
}
}
export async function getCliIdentity() {
const r = await runVercelJson(['whoami', '--format', 'json']);
return r.ok ? r.data : null;
}
// Supports newer `.vercel/repo.json` (multi-project) + legacy `.vercel/project.json` (single-project).
export async function readProjectJson(cwd = process.cwd()) {
try {
const raw = await readFile(join(cwd, '.vercel', 'repo.json'), 'utf-8');
const parsed = JSON.parse(raw);
const projects = Array.isArray(parsed?.projects) ? parsed.projects.filter((p) => p?.id) : [];
if (projects.length > 1) {
throw new Error('AMBIGUOUS_PROJECT_LINK: `.vercel/repo.json` contains multiple projects. Run from the linked app directory, or pass the intended projectId together with VERCEL_ORG_ID.');
}
const first = projects[0];
if (first?.id) {
return { projectId: first.id, orgId: first.orgId ?? null, source: 'repo.json' };
}
} catch (err) {
if (err?.message?.startsWith('AMBIGUOUS_PROJECT_LINK:')) throw err;
/* fall through */
}
// Legacy single-project format.
try {
const raw = await readFile(join(cwd, '.vercel', 'project.json'), 'utf-8');
const parsed = JSON.parse(raw);
if (parsed?.projectId) {
return { projectId: parsed.projectId, orgId: parsed.orgId ?? null, source: 'project.json' };
}
} catch { /* fall through */ }
return null;
}
// Does NOT auto-run `vercel link` — interactive surprises bad.
export async function resolveProjectId(explicit, cwd = process.cwd()) {
if (explicit) {
const linked = process.env.VERCEL_ORG_ID
? null
: await readLinkedOwnerForProjectId(explicit, cwd);
return {
projectId: explicit,
orgId: process.env.VERCEL_ORG_ID || linked?.orgId || null,
source: linked?.source ? `arg+${linked.source}` : 'arg',
};
}
if (process.env.VERCEL_PROJECT_ID) {
const linked = process.env.VERCEL_ORG_ID
? null
: await readLinkedOwnerForProjectId(process.env.VERCEL_PROJECT_ID, cwd);
return {
projectId: process.env.VERCEL_PROJECT_ID,
orgId: process.env.VERCEL_ORG_ID || linked?.orgId || null,
source: linked?.source ? `env+${linked.source}` : 'env',
};
}
return await readProjectJson(cwd);
}
async function readLinkedOwnerForProjectId(projectId, cwd = process.cwd()) {
try {
const raw = await readFile(join(cwd, '.vercel', 'repo.json'), 'utf-8');
const parsed = JSON.parse(raw);
const matches = (Array.isArray(parsed?.projects) ? parsed.projects : [])
.filter((p) => p?.id && String(p.id) === String(projectId));
if (matches.length > 1) {
throw new Error('AMBIGUOUS_PROJECT_LINK: `.vercel/repo.json` contains multiple entries for the requested projectId. Ask the user to confirm the intended Vercel team/personal scope.');
}
const match = matches[0];
if (match?.orgId) return { orgId: match.orgId, source: 'repo.json' };
} catch (err) {
if (err?.message?.startsWith('AMBIGUOUS_PROJECT_LINK:')) throw err;
/* fall through */
}
try {
const raw = await readFile(join(cwd, '.vercel', 'project.json'), 'utf-8');
const parsed = JSON.parse(raw);
if (String(parsed?.projectId ?? '') === String(projectId) && parsed?.orgId) {
return { orgId: parsed.orgId, source: 'project.json' };
}
} catch { /* fall through */ }
return null;
}
export async function resolveCommandScope(project = {}) {
const orgId = project?.orgId ?? null;
if (!orgId) {
return {
ok: false,
cliScope: null,
source: 'missing-org-scope',
required: true,
error: 'PROJECT_SCOPE_UNRESOLVED',
detail: 'The project was resolved without an owner account, so the collector cannot prove which Vercel scope to query.',
};
}
const identity = await getCliIdentity();
const currentTeam = identity?.team ?? null;
if (String(orgId).startsWith('team_')) {
if (currentTeam?.id === orgId && currentTeam?.slug) {
return {
ok: true,
cliScope: currentTeam.slug,
source: 'whoami-current-team',
required: true,
teamId: orgId,
detail: 'Resolved linked team ID to the current CLI team slug.',
};
}
const team = await getTeamInfo(orgId);
if (team.ok && team.slug) {
return {
ok: true,
cliScope: team.slug,
source: 'team-api',
required: true,
teamId: orgId,
detail: 'Resolved linked team ID to a Vercel CLI scope slug.',
};
}
return {
ok: false,
cliScope: null,
source: 'team-api',
required: true,
teamId: orgId,
error: team.error ?? 'TEAM_SCOPE_UNRESOLVED',
detail: 'Could not resolve the linked team ID to a Vercel CLI scope slug.',
};
}
if (String(orgId).startsWith('usr_')) {
const user = identity?.user ?? identity ?? {};
const userId = user.id ?? identity?.id ?? null;
const username = user.username ?? identity?.username ?? null;
if ((!userId || userId === orgId) && username) {
return {
ok: true,
cliScope: username,
source: 'whoami-user',
required: true,
userId: orgId,
detail: 'Resolved linked user ID to a Vercel CLI username scope.',
};
}
return {
ok: false,
cliScope: null,
source: 'whoami-user',
required: true,
userId: orgId,
error: 'USER_SCOPE_UNRESOLVED',
detail: 'Could not resolve the linked user ID to the authenticated Vercel username.',
};
}
return {
ok: true,
cliScope: orgId,
source: 'linked-org-scope',
required: true,
detail: 'Using the linked org value as the Vercel CLI scope.',
};
}
async function getTeamInfo(teamIdOrSlug) {
const r = await runVercelJson(['api', `/v2/teams/${encodeURIComponent(teamIdOrSlug)}`]);
if (!r.ok) return { ok: false, error: r.code ?? 'UNKNOWN' };
const team = r.data?.team ?? r.data ?? {};
return {
ok: true,
id: team.id ?? null,
slug: team.slug ?? null,
name: team.name ?? null,
};
}
// Some commands emit `{error: {...}}` on stdout AND exit non-zero — parse stdout first; embedded `error` is the most reliable signal.
// 32 MiB buffer: 14d function-duration timeseries across many routes exceeds Node's 1 MiB default.
export async function runVercelJson(args, opts = {}) {
let stdout = '';
let stderr = '';
let exitCode = 0;
try {
const r = await runVercel(args, { maxBuffer: 32 * 1024 * 1024, ...opts });
stdout = r.stdout;
stderr = r.stderr;
} catch (err) {
stdout = err.stdout || '';
stderr = err.stderr || '';
exitCode = err.code ?? err.exitCode ?? 1;
}
const safeStderr = redactSensitiveText(stderr);
if (stdout && stdout.trim().startsWith('{')) {
try {
const data = JSON.parse(stdout);
if (data && typeof data === 'object' && data.error) {
const failure = {
ok: false,
code: data.error.code || `EXIT_${exitCode}`,
message: redactSensitiveText(data.error.message || ''),
allowedValues: data.error.allowedValues,
stderr: safeStderr,
};
return isDailyQuotaExceeded(failure)
? { ...failure, code: 'DAILY_QUOTA_EXCEEDED', originalCode: failure.code }
: failure;
}
if (exitCode === 0) return { ok: true, data };
// Exit non-zero, no `error` key, parseable stdout → still useful.
return { ok: true, data };
} catch {
/* fall through to stderr categorization */
}
}
// Metrics schema returns a top-level array.
if (stdout && stdout.trim().startsWith('[')) {
try {
const data = JSON.parse(stdout);
if (exitCode === 0) return { ok: true, data };
} catch { /* fall through */ }
}
return {
ok: false,
code: categorizeError(exitCode, stderr),
stderr: safeStderr,
};
}
export function redactSensitiveText(value) {
return String(value ?? '')
.replace(/\b(Bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, '$1 [REDACTED]')
.replace(/\b(Authorization:\s*)[^\r\n]+/gi, '$1[REDACTED]')
.replace(/\b(x-vercel-id:\s*)[^\r\n]+/gi, '$1[REDACTED]')
.replace(/\b(VERCEL_TOKEN|TURBO_TOKEN|NPM_TOKEN|NODE_AUTH_TOKEN|GITHUB_TOKEN)=("[^"]+"|'[^']+'|[^\s"'`]+)/g, '$1=[REDACTED]')
.replace(/(--token(?:=|\s+))("[^"]+"|'[^']+'|[^\s"'`]+)/gi, '$1[REDACTED]')
.replace(/\b(prj|team|usr)_[A-Za-z0-9]{8,}\b/g, '$1_[REDACTED]')
.replace(/("token"\s*:\s*")[^"]{8,}(")/gi, '$1[REDACTED]$2');
}
// CLI doesn't emit machine-readable error codes for these states — stderr substring is fallback only.
function categorizeError(exitCode, stderr) {
const lc = (stderr || '').toLowerCase();
if (isDailyQuotaExceeded({ ok: false, stderr })) return 'DAILY_QUOTA_EXCEEDED';
if (lc.includes('observability plus')) return 'OPLUS_REQUIRED';
if (lc.includes('costs not found')) return 'USAGE_UNAVAILABLE';
if (lc.includes('project not found')) return 'PROJECT_NOT_FOUND';
if (lc.includes('not linked') || lc.includes('no project')) return 'NOT_LINKED';
if (lc.includes('log in') || lc.includes('credentials')) return 'NOT_AUTH';
if (lc.includes('rate limit') || lc.includes('429')) return 'RATE_LIMIT';
if (lc.includes('permission') || lc.includes('not authorized') || lc.includes('403'))
return 'FORBIDDEN';
return `EXIT_${exitCode}`;
}
// Schema is global per team — pass scope so we hit the right team rather than user's currentTeam.
export async function hasObservabilityPlus(scope) {
const r = await runVercelJson(scopedArgs(['metrics', 'schema', '--format', 'json'], scope));
return r.ok;
}
export async function getMetricsSchema(scope) {
const r = await runVercelJson(scopedArgs(['metrics', 'schema', '--format', 'json'], scope));
return r.ok ? r.data : null;
}
export async function checkObservabilityPlusConfiguration({ orgId, projectId } = {}) {
if (!orgId) {
return {
ok: false,
source: 'observability-configuration-api',
blocker: 'unknown',
detail: 'No team ID was available for the Observability Plus configuration preflight.',
};
}
if (String(orgId).startsWith('usr_')) {
return {
ok: false,
source: 'observability-configuration-api',
access: null,
blocker: 'unknown',
detail: 'The Observability Plus team configuration preflight is not available for a user-owned project; falling back to the scoped metrics probe.',
};
}
const qs = `?teamId=${encodeURIComponent(orgId)}`;
const r = await runVercelJson(['api', `/v1/observability/manage/configuration/projects${qs}`]);
return classifyObservabilityPlusConfiguration(r, { projectId });
}
export function classifyObservabilityPlusConfiguration(result, { projectId } = {}) {
const source = 'observability-configuration-api';
if (result?.ok) {
const disabledProjects = Array.isArray(result.data?.disabledProjects) ? result.data.disabledProjects : [];
const disabled = projectId
? disabledProjects.find((p) => String(p?.id ?? '') === String(projectId))
: null;
if (disabled) {
return {
ok: true,
source,
access: false,
blocker: 'project_disabled',
detail: 'Observability Plus is enabled for the team but disabled for this project.',
disabledProject: {
id: disabled.id,
name: disabled.name ?? null,
disabledAt: disabled.disabledAt ?? null,
},
};
}
return {
ok: true,
source,
access: true,
blocker: null,
detail: 'Observability Plus is enabled for this team/project.',
};
}
const code = String(result?.code ?? 'unknown').toLowerCase();
const text = `${result?.message ?? ''}\n${result?.stderr ?? ''}`.toLowerCase();
const mentionsObservabilityPlusNotEnabled =
/observability plus[\s\S]{0,160}not enabled/.test(text) ||
/not enabled[\s\S]{0,160}observability plus/.test(text) ||
/subscription to observability plus[\s\S]{0,160}required/.test(text);
if (code === 'oplus_required' || ((code === 'not_found' || code === '404') && mentionsObservabilityPlusNotEnabled)) {
return {
ok: true,
source,
access: false,
blocker: 'no_oplus_probe',
detail: 'Route-level metrics are unavailable because Observability Plus is not enabled for this team.',
};
}
if (/forbidden|not_authorized|403/.test(code) || /forbidden|not authorized|permission|403/.test(text)) {
return {
ok: false,
source,
access: null,
blocker: 'forbidden',
detail: 'Could not read Observability Plus configuration for this team. Run `vercel switch <team>` and verify access.',
};
}
if (/not_auth|unauthorized|401/.test(code) || /unauthorized|log in|credentials|401/.test(text)) {
return {
ok: false,
source,
access: null,
blocker: 'forbidden',
detail: 'Could not read Observability Plus configuration because the Vercel CLI is not authenticated.',
};
}
return {
ok: false,
source,
access: null,
blocker: 'unknown',
detail: `Could not determine Observability Plus configuration before querying metrics (code=${code}).`,
};
}
// Returns `{ok, ...}`. CLI summary defaults to top 10 groups under --group-by; widen via opts.limit.
export async function queryMetric(metricId, opts = {}) {
const args = ['metrics', metricId, '--format', 'json'];
if (opts.aggregation) args.push('-a', opts.aggregation);
for (const dim of opts.groupBy ?? []) args.push('--group-by', dim);
if (opts.filter) args.push('-f', opts.filter);
if (opts.since) args.push('--since', opts.since);
if (opts.until) args.push('--until', opts.until);
if (opts.limit) args.push('--limit', String(opts.limit));
// 3-layer protection: semaphore (8 concurrent) + sliding-window (80/60s) + retryOnRateLimit (3× 60-90s jitter). payment_required is terminal.
const throttle = getMetricThrottle();
const onRetry = (attempt, delayMs) => {
console.error(`[queryMetric] ${metricId} hit RATE_LIMITED; retry ${attempt}/3 after ${(delayMs / 1000).toFixed(0)}s`);
};
return await throttle.run(() =>
retryOnRateLimit(() => runVercelJson(scopedArgs(args, opts.scope)), { onRetry })
);
}
// Team-owned projects need `?teamId=<orgId>` to avoid current-team drift. User-
// owned projects use the authenticated user context and should not pass teamId.
export async function getProjectConfig(projectId, orgId) {
const qs = orgId && !String(orgId).startsWith('usr_')
? `?teamId=${encodeURIComponent(orgId)}`
: '';
const r = await runVercelJson(['api', `/v9/projects/${projectId}${qs}`]);
return r.ok ? r.data : { error: r.code, stderr: r.stderr };
}
// USAGE_UNAVAILABLE distinguishes "no Costs feature" from genuine emptiness.
export async function getUsage({ days = 14, scope, groupByProject = true } = {}) {
const toDate = new Date();
const fromDate = new Date(toDate.getTime() - days * 86400000);
const fmt = (d) => d.toISOString().slice(0, 10);
const args = [
'usage',
'--format', 'json',
'--from', fmt(fromDate),
'--to', fmt(toDate),
];
// The CLI rejects --breakdown with --group-by. Project grouping is higher
// value for this skill because every recommendation must be project-scoped.
if (groupByProject) args.push('--group-by', 'project');
else args.push('--breakdown', 'daily');
return await runVercelJson(scopedArgs(args, scope));
}
// CLI `--group-by project` returns project buckets under groupBy.data. Older
// breakdown-shaped fixtures tag service rows with projectId; keep both paths.
export function filterUsageByProject(usage, projectId, projectName = null) {
if (!usage || !projectId) return { filtered: null, matched: false, unattributedTotal: 0 };
if (usage.groupBy?.dimension === 'project' && Array.isArray(usage.groupBy.data)) {
const project = usage.groupBy.data.find((entry) => projectMatches(entry, projectId, projectName));
if (!project) return { filtered: null, matched: false, unattributedTotal: 0 };
return {
filtered: {
...usage,
groupBy: { ...usage.groupBy, data: [project] },
services: Array.isArray(project.services) ? project.services : [],
totals: project.totals ?? null,
project: { name: project.name ?? projectName ?? null, projectId: project.projectId ?? projectId },
},
matched: true,
unattributedTotal: 0,
};
}
const breakdown = usage.breakdown;
if (!breakdown || !Array.isArray(breakdown.data)) {
return { filtered: null, matched: false, unattributedTotal: 0 };
}
const out = {
...usage,
breakdown: { ...breakdown, data: [] },
};
let matchedAny = false;
let projectTotal = 0;
let unattributedTotal = 0;
for (const day of breakdown.data) {
const services = Array.isArray(day.services) ? day.services : [];
const projectRows = services.filter((s) => projectMatches(s, projectId, projectName));
const unattributedRows = services.filter((s) => !s.projectId && !s.project);
for (const r of projectRows) projectTotal += (r.billedCost ?? r.cost ?? 0);
for (const r of unattributedRows) unattributedTotal += (r.billedCost ?? r.cost ?? 0);
if (projectRows.length === 0) continue;
matchedAny = true;
out.breakdown.data.push({ ...day, services: projectRows });
}
if (!matchedAny) return { filtered: null, matched: false, unattributedTotal };
out.services = aggregateServicesByName(out.breakdown.data);
out.totals = { billedCost: projectTotal };
return { filtered: out, matched: true, unattributedTotal };
}
function projectMatches(serviceRow, projectId, projectName = null) {
if (!serviceRow) return false;
if (serviceRow.projectId === projectId) return true;
if (projectName && serviceRow.name === projectName) return true;
if (projectName && serviceRow.project === projectName) return true;
if (serviceRow.project === projectId) return true;
if (serviceRow.project && (serviceRow.project.id === projectId || serviceRow.project.projectId === projectId || serviceRow.project.name === projectName)) return true;
return false;
}
function aggregateServicesByName(days) {
const byName = new Map();
for (const day of days) {
for (const s of (day.services ?? [])) {
const key = s.name ?? '(unnamed)';
const prev = byName.get(key) ?? { name: key, billedCost: 0, pricingQuantity: 0, pricingUnit: s.pricingUnit ?? null };
prev.billedCost += (s.billedCost ?? s.cost ?? 0);
prev.pricingQuantity += (s.pricingQuantity ?? 0);
byName.set(key, prev);
}
}
return Array.from(byName.values()).sort((a, b) => (b.billedCost ?? 0) - (a.billedCost ?? 0));
}
export async function getContract(scope) {
const r = await runVercelJson(scopedArgs(['contract', '--format', 'json'], scope));
return r.ok ? r.data : null;
}
export async function getAccountPlan(scope) {
const currentTeamId = scope ? null : await getCurrentTeamId();
const teamScope = scope || currentTeamId;
if (teamScope && !String(teamScope).startsWith('usr_')) {
const team = await getBillingPlanFromPath(`/v2/teams/${encodeURIComponent(teamScope)}`, 'team.billing.plan');
if (team.plan !== 'unknown' || !/not_found|404/i.test(String(team.error ?? ''))) {
return team;
}
// Older project links can carry a user/org id instead of a team id. If the
// team lookup misses, fall back to the authenticated user's billing record.
}
return await getBillingPlanFromPath('/v2/user', 'user.billing.plan');
}
async function getCurrentTeamId() {
const identity = await getCliIdentity();
return identity?.team?.id ?? null;
}
async function getBillingPlanFromPath(path, source) {
const r = await runVercelJson(['api', path]);
if (!r.ok) {
return {
plan: 'unknown',
reason: `${source} unavailable (${r.code ?? 'unknown'})`,
source,
error: r.code ?? 'unknown',
};
}
const parsed = extractBillingPlan(r.data);
if (!parsed) {
return {
plan: 'unknown',
reason: `${source} missing from Vercel API response`,
source,
};
}
return {
...parsed,
reason: `${source}=${parsed.plan}`,
source,
};
}
export function extractBillingPlan(data) {
const raw =
data?.billing?.plan ??
data?.team?.billing?.plan ??
data?.user?.billing?.plan ??
null;
const plan = normalizeBillingPlan(raw);
return plan ? { plan, rawPlan: raw } : null;
}
function normalizeBillingPlan(raw) {
const value = String(raw ?? '').trim().toLowerCase();
if (value === 'hobby' || value === 'pro' || value === 'enterprise') return value;
return null;
}
// Primary source: billing.plan from `/v2/teams/:team` or `/v2/user`.
// Fallbacks: contract category, then recent billed usage for legacy CLI/API gaps.
export function inferPlan(contract, opts = {}) {
const accountPlan = extractPlanOption(opts?.accountPlan);
if (accountPlan) {
return {
plan: accountPlan.plan,
reason: accountPlan.reason ?? `${accountPlan.source ?? 'billing.plan'}=${accountPlan.plan}`,
};
}
const commits = contract?.commitments ?? [];
if (commits.length > 0) {
const c0 = commits[0] ?? {};
// category field names are tentative — try several.
const category = c0.category ?? c0.commitmentCategory ?? c0.type ?? null;
if (category === 'Spend' || category === 'spend') {
return { plan: 'pro', reason: `commitment category=${category}` };
}
if (category === 'Usage' || category === 'usage') {
return { plan: 'enterprise', reason: `commitment category=${category}` };
}
return { plan: 'uncertain', reason: `unknown commitment category=${category}` };
}
const totalCost = opts?.usageTotalCost;
if (typeof totalCost === 'number' && totalCost > 0) {
return {
plan: 'pro',
reason: `commitments=[] but usage=$${totalCost.toFixed(2)}/window — Pro pay-as-you-go (Hobby teams don't bill)`,
};
}
return {
plan: 'uncertain',
reason: typeof totalCost === 'number' && totalCost === 0
? 'no commitments and no billed usage in window (could be Hobby, or Pro with no recent billing)'
: 'no commitments on contract; usage unavailable',
};
}
function extractPlanOption(accountPlan) {
if (!accountPlan) return null;
if (typeof accountPlan === 'string') {
const plan = normalizeBillingPlan(accountPlan);
return plan ? { plan, reason: `billing.plan=${plan}` } : null;
}
const plan = normalizeBillingPlan(accountPlan.plan);
if (!plan) return null;
return {
plan,
reason: accountPlan.reason ?? (
accountPlan.source
? `${accountPlan.source}=${plan}`
: `billing.plan=${plan}`
),
source: accountPlan.source ?? null,
};
}
export async function detectStack(cwd = process.cwd()) {
const pkgPath = join(cwd, 'package.json');
let pkg = {};
try {
pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
} catch {
return baselineStack();
}
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const framework =
deps.next ? 'next' :
deps.nuxt ? 'nuxt' :
deps.astro ? 'astro' :
deps['@sveltejs/kit'] ? 'sveltekit' :
deps['@remix-run/react'] ? 'remix' :
deps.hono ? 'hono' :
'unknown';
const frameworkVersion = (() => {
const m = { next: 'next', nuxt: 'nuxt', astro: 'astro', sveltekit: '@sveltejs/kit', remix: '@remix-run/react', hono: 'hono' };
const dep = m[framework];
if (!dep) return null;
return (deps[dep] || '').replace(/^[\^~]/, '') || null;
})();
const hasAppRouter = await pathExists(join(cwd, 'app')) || await pathExists(join(cwd, 'src/app'));
const hasPagesRouter = await pathExists(join(cwd, 'pages')) || await pathExists(join(cwd, 'src/pages'));
const typescript = await pathExists(join(cwd, 'tsconfig.json'));
const cacheComponents = framework === 'next'
? await detectNextCacheComponents(cwd)
: null;
const orm =
deps.prisma || deps['@prisma/client'] ? 'prisma' :
deps['drizzle-orm'] ? 'drizzle' :
deps.kysely ? 'kysely' :
'none';
const vercelFlagsPackages = [
'@vercel/flags',
'@vercel/flags/next',
'@vercel/flags/sveltekit',
'@vercel/flags/nuxt',
].filter((name) => deps[name]);
const workflowPackages = Object.keys(deps)
.filter((name) => name === 'workflow' || name.startsWith('@workflow/'))
.sort();
const isMonorepo =
!!pkg.workspaces ||
await pathExists(join(cwd, 'pnpm-workspace.yaml')) ||
await pathExists(join(cwd, 'lerna.json'));
return {
framework,
frameworkVersion,
hasAppRouter,
hasPagesRouter,
cacheComponents,
typescript,
orm,
isMonorepo,
rootDirectory: null,
hasVercelFlagsPackage: vercelFlagsPackages.length > 0,
vercelFlagsPackages,
hasWorkflowPackage: workflowPackages.length > 0,
workflowPackages,
};
}
function baselineStack() {
return {
framework: 'unknown', frameworkVersion: null,
hasAppRouter: false, hasPagesRouter: false, cacheComponents: null, typescript: false,
orm: 'none', isMonorepo: false, rootDirectory: null,
hasVercelFlagsPackage: false, vercelFlagsPackages: [],
hasWorkflowPackage: false, workflowPackages: [],
};
}
async function detectNextCacheComponents(cwd) {
for (const name of ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']) {
try {
const content = await readFile(join(cwd, name), 'utf-8');
if (/\bcacheComponents\s*:\s*true\b/.test(content)) return true;
if (/\bcacheComponents\s*:\s*false\b/.test(content)) return false;
} catch {}
}
return null;
}
async function pathExists(p) {
try { await access(p); return true; } catch { return false; }
}
// `--scope <teamId>` is buggy on several subcommands (silently falls back to
// currentTeam). Resolve raw account IDs to slugs/usernames before scoped calls.
function scopedArgs(args, scope) {
if (!scope) return args;
if (typeof scope === 'string' && /^(team|usr)_/.test(scope)) {
throw new Error('RAW_ID_SCOPE_UNRESOLVED: resolve the linked org/user ID to a CLI scope slug before running Vercel commands.');
}
return [...args, '--scope', scope];
}
// CLI summary field is `<metric_id_with_underscores>_<aggregation>` (e.g. `vercel_request_count_sum`).
export function normalizeSummary(metricResponse, metricId, aggregation, groupBy = []) {
if (!metricResponse || metricResponse.error) return [];
const field = `${metricId.replace(/\./g, '_')}_${aggregation}`;
const rows = Array.isArray(metricResponse.summary) ? metricResponse.summary : [];
return rows.map((row) => {
const out = { value: row[field] ?? null };
for (const dim of groupBy) {
if (row[dim] !== undefined) out[dim] = row[dim];
}
return out;
});
}
lib/verify-claim.mjs
// Pure async claim verifier. No LLM, no network — fs + grep only.
import { readFile, access, readdir } from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { dirname, isAbsolute, join, normalize } from 'node:path';
import { promisify } from 'node:util';
import { isKnownUrl, sanitizeCitations } from './citations.mjs';
import { findRecContradictions } from './project-facts.mjs';
import { canonicalizeRoute } from './route-normalize.mjs';
const execFileP = promisify(execFile);
const cacheInvalidationFileCache = new Map();
// Bad inputs surface as `unsupported` — never throws.
export async function verifyClaim(claim) {
if (!claim || typeof claim !== 'object') {
return { disposition: 'unverifiable', reason: 'claim is not an object' };
}
switch (claim.type) {
case 'file_exists': return verifyFileExists(claim);
case 'pattern_count': return verifyPatternCount(claim);
case 'pattern_exists': return verifyPatternExists(claim);
case 'pattern_absent': return verifyPatternAbsent(claim);
case 'code_snippet': return verifyCodeSnippet(claim);
case 'repo_count': return verifyRepoCount(claim);
case 'citation_in_library': return verifyCitationInLibrary(claim);
case 'citation_applies_to_version': return verifyCitationAppliesToVersion(claim);
case 'cache_vary_matches_dynamic_inputs': return verifyCacheVaryMatchesDynamicInputs(claim);
case 'cache_vary_cardinality_safe': return verifyCacheVaryCardinalitySafe(claim);
case 'next_cached_not_found_causal_support': return verifyNextCachedNotFoundCausalSupport(claim);
case 'next_stable_cache_api_for_version': return verifyNextStableCacheApiForVersion(claim);
case 'next_runtime_cache_api_for_version': return verifyNextRuntimeCacheApiForVersion(claim);
case 'next_cache_components_runtime_cache_preference': return verifyNextCacheComponentsRuntimeCachePreference(claim);
case 'next_cache_life_single_execution': return verifyNextCacheLifeSingleExecution(claim);
case 'next_cache_lifetime_freshness_supported': return verifyNextCacheLifetimeFreshnessSupported(claim);
case 'next_cache_components_route_chain_file': return verifyNextCacheComponentsRouteChainFile(claim);
case 'next_cache_life_cdn_header_semantics': return verifyNextCacheLifeCdnHeaderSemantics(claim);
case 'image_response_headers_citation': return verifyImageResponseHeadersCitation(claim);
case 'next_image_priority_api_for_version': return verifyNextImagePriorityApiForVersion(claim);
case 'next_cache_components_route_segment_config': return verifyNextCacheComponentsRouteSegmentConfig(claim);
case 'next_route_revalidate_static_prereq': return verifyNextRouteRevalidateStaticPrereq(claim);
case 'next_cache_tag_invalidation_supported': return verifyNextCacheTagInvalidationSupported(claim);
case 'cache_rec_not_error_dominated_or_acknowledged': return verifyCacheRecNotErrorDominatedOrAcknowledged(claim);
case 'cache_control_header_syntax': return verifyCacheControlHeaderSyntax(claim);
case 'cache_control_headers_citation': return verifyCacheControlHeadersCitation(claim);
case 'cache_policy_positive_or_no_ready_rec': return verifyCachePolicyPositiveOrNoReadyRec(claim);
case 'cache_404_long_ttl_safety': return verifyCache404LongTtlSafety(claim);
case 'route_error_not_found_status_and_scope': return verifyRouteErrorNotFoundStatusAndScope(claim);
case 'immutable_dynamic_route_safety': return verifyImmutableDynamicRouteSafety(claim);
case 'auth_guard_parallelization_safety': return verifyAuthGuardParallelizationSafety(claim);
case 'parallelization_impact_not_overclaimed': return verifyParallelizationImpactNotOverclaimed(claim);
case 'parallelization_not_cpu_bound_work': return verifyParallelizationNotCpuBoundWork(claim);
case 'runtime_error_cause_supported': return verifyRuntimeErrorCauseSupported(claim);
case 'vercel_ignore_command_project_state': return verifyVercelIgnoreCommandProjectState(claim);
case 'turbo_build_cache_safety': return verifyTurboBuildCacheSafety(claim);
case 'does_not_contradict_project_config': return verifyNoProjectConfigContradiction(claim);
default:
return { disposition: 'unverifiable', reason: `unknown claim type: ${claim.type}` };
}
}
// Catches "enable fluid compute" recs that the brief negative-space filter let through.
async function verifyNoProjectConfigContradiction({ rec, projectFacts }) {
if (!rec) return { disposition: 'unsupported', reason: 'no rec attached to claim' };
if (!Array.isArray(projectFacts) || projectFacts.length === 0) {
return { disposition: 'unverifiable', reason: 'no project facts available' };
}
const hits = findRecContradictions(rec, projectFacts);
if (hits.length === 0) {
return { disposition: 'verified', reason: 'rec does not contradict any already-on project setting' };
}
const ids = hits.map((h) => h.id).join(', ');
return {
disposition: 'failed',
reason: `rec contradicts project config: recommends toggling on already-enabled ${ids}`,
};
}
async function verifyFileExists(claim) {
const { file } = claim;
if (!file) return { disposition: 'unsupported', reason: 'file_exists requires file' };
try {
await firstAccessiblePath(claim);
return { disposition: 'verified', reason: `${file} exists` };
} catch {
return { disposition: 'failed', reason: `${file} does not exist` };
}
}
async function verifyPatternCount(claim) {
const { file, pattern, expected } = claim;
if (!file || !pattern) return { disposition: 'unsupported', reason: 'pattern_count requires file + pattern' };
let content;
try { ({ content } = await readClaimFile(claim)); }
catch { return { disposition: 'failed', reason: `cannot read ${file}` }; }
// "42" alone (from `filename:42`) is a line number, not a pattern.
if (/^\d+$/.test(pattern.trim())) {
return { disposition: 'unsupported', reason: 'pattern looks like a line number, not a pattern' };
}
const re = compilePattern(pattern, 'g');
const matches = content.match(re) ?? [];
const actual = matches.length;
return actual === expected
? { disposition: 'verified', actual, expected, reason: 'exact count match' }
: { disposition: 'failed', actual, expected, reason: `count mismatch: claim=${expected}, actual=${actual}` };
}
async function verifyPatternExists(claim) {
const { file, pattern } = claim;
if (!file || !pattern) return { disposition: 'unsupported', reason: 'pattern_exists requires file + pattern' };
try {
const { content } = await readClaimFile(claim);
const found = compilePattern(pattern, '').test(content);
return { disposition: found ? 'verified' : 'failed', reason: found ? 'pattern present' : 'pattern not found' };
} catch {
return { disposition: 'failed', reason: `cannot read ${file}` };
}
}
async function verifyPatternAbsent(claim) {
const { file, pattern } = claim;
if (!file || !pattern) return { disposition: 'unsupported', reason: 'prose-of-absence: claim requires file + pattern to verify' };
try {
const { content } = await readClaimFile(claim);
const found = compilePattern(pattern, '').test(content);
return { disposition: !found ? 'verified' : 'failed', reason: !found ? 'pattern absent as claimed' : 'pattern present despite claim of absence' };
} catch {
return { disposition: 'failed', reason: `cannot read ${file}` };
}
}
async function verifyCodeSnippet(claim) {
const { file, snippet, repoRoot = '.' } = claim;
if (!file || !snippet) return { disposition: 'unsupported', reason: 'code_snippet requires file + snippet' };
try {
const { content } = await readClaimFile(claim);
const norm = (s) => s.replace(/\s+/g, ' ').trim();
if (norm(content).includes(norm(snippet))) {
return { disposition: 'verified', reason: 'snippet found in cited file' };
}
const elsewhere = await snippetFoundElsewhere(repoRoot, snippet, file);
if (elsewhere) {
return { disposition: 'unsupported', reason: `snippet exists in ${elsewhere}, not in cited ${file}` };
}
return { disposition: 'failed', reason: 'snippet not found in cited file or repo' };
} catch {
return { disposition: 'failed', reason: `cannot read ${file}` };
}
}
async function verifyRepoCount({ pattern, expected, repoRoot = '.' }) {
if (!pattern || expected == null) return { disposition: 'unsupported', reason: 'repo_count requires pattern + expected' };
let actual = 0;
const re = compilePattern(pattern, '');
for await (const path of walkFiles(repoRoot)) {
try {
const content = await readFile(path, 'utf-8');
if (re.test(content)) actual++;
} catch {}
}
return actual === expected
? { disposition: 'verified', actual, expected, reason: 'exact file count match' }
: { disposition: 'failed', actual, expected, reason: `file count: claim=${expected}, actual=${actual}` };
}
async function verifyCitationInLibrary({ url }) {
if (!url) return { disposition: 'unsupported', reason: 'citation_in_library requires url' };
if (/^[\w-]+:[\w-]+$/.test(url)) {
return { disposition: 'verified', reason: 'skill-rule reference format (allowed)' };
}
const known = await isKnownUrl(url);
return known
? { disposition: 'verified', reason: 'URL in curated library' }
: { disposition: 'failed', reason: 'URL not in curated library — likely hallucination' };
}
async function verifyCitationAppliesToVersion({ url, framework, frameworkVersion }) {
if (!url || !framework || !frameworkVersion) {
return { disposition: 'unsupported', reason: 'requires url + framework + frameworkVersion' };
}
const fakeRec = { citations: [url] };
const { rec, strippedVersion, strippedUnknown } = await sanitizeCitations(fakeRec, framework, frameworkVersion);
if (strippedUnknown.length > 0) {
return { disposition: 'failed', reason: 'URL not in library' };
}
if (strippedVersion.length > 0) {
return { disposition: 'failed', reason: `URL not applicable to ${framework}@${frameworkVersion}` };
}
return rec.citations.length > 0
? { disposition: 'verified', reason: `URL applies to ${framework}@${frameworkVersion}` }
: { disposition: 'unsupported', reason: 'sanitizer stripped all citations for unknown reason' };
}
async function verifyCacheVaryMatchesDynamicInputs({ rec, files, repoRoot = '.', projectRootDirectory = null }) {
if (!rec || !Array.isArray(files) || files.length === 0) {
return { disposition: 'unsupported', reason: 'cache_vary_matches_dynamic_inputs requires rec + files' };
}
let usesVercelGeo = false;
for (const file of files) {
try {
const { content } = await readClaimFile({ file, repoRoot, projectRootDirectory });
if (/\bgeolocation\s*\(/.test(content) ||
/\b\w+\.geo\??\./.test(content) ||
/['"]x-vercel-ip-(?:country|country-region|city|latitude|longitude|postal-code|timezone)['"]/i.test(content)) {
usesVercelGeo = true;
break;
}
} catch {}
}
if (!usesVercelGeo) {
return { disposition: 'verified', reason: 'cache rec does not touch Vercel geolocation inputs' };
}
const text = [rec.what, rec.why, rec.fix, rec.currentBehavior, rec.desiredBehavior, rec.verify]
.filter(Boolean)
.join('\n');
const hasCoarseGeoVary = hasHeaderValue(text, 'Vary', /(?:^|,\s*)X-Vercel-IP-(?:Country|Country-Region|City)(?:\s*,|$)/i);
if (hasCoarseGeoVary) {
return { disposition: 'verified', reason: 'cache rec varies by a coarse Vercel geolocation header for geolocation-dependent output' };
}
return {
disposition: 'failed',
reason: 'cache rec touches Vercel geolocation but does not vary by a coarse Vercel geolocation header such as X-Vercel-IP-Country, X-Vercel-IP-Country-Region, or X-Vercel-IP-City',
};
}
async function verifyCacheVaryCardinalitySafe({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'cache_vary_cardinality_safe requires rec' };
const text = recText(rec);
const varyValues = extractHeaderValues(text, 'Vary').join(', ');
if (!varyValues) {
return { disposition: 'verified', reason: 'no concrete Vary header value detected' };
}
if (/\bX-Vercel-IP-(?:Latitude|Longitude|Postal-Code)\b/i.test(varyValues)) {
return {
disposition: 'failed',
reason: 'Vary on X-Vercel-IP-Latitude, X-Vercel-IP-Longitude, or X-Vercel-IP-Postal-Code creates very high-cardinality CDN cache keys; use a coarser geography header when safe, or leave the response uncached',
};
}
return { disposition: 'verified', reason: 'Vary header avoids known high-cardinality geolocation headers' };
}
async function verifyNextCachedNotFoundCausalSupport({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_cached_not_found_causal_support requires rec' };
const text = recText(rec);
const citations = Array.isArray(rec.citations) ? rec.citations.join('\n') : '';
const hasSpecificCitation = /nextjs\.org\/docs\/app\/api-reference\/functions\/not-found/i.test(citations) &&
/nextjs\.org\/docs\/app\/api-reference\/directives\/use-cache/i.test(citations);
const hasRuntimeStack = /\b(?:stack|logs?|trace)\b[\s\S]{0,120}\b(?:NEXT_|notFound|NEXT_HTTP_ERROR_FALLBACK|Error:)\b/i.test(text);
if (hasSpecificCitation || hasRuntimeStack) {
return { disposition: 'verified', reason: 'cached notFound causal claim has Next-specific citation or runtime stack evidence' };
}
return {
disposition: 'failed',
reason: 'notFound() inside use cache was claimed as a 5xx cause without Next-specific citation or runtime stack evidence',
};
}
async function verifyNextStableCacheApiForVersion({ rec, framework, frameworkVersion }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_stable_cache_api_for_version requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
const major = parseInt(String(frameworkVersion ?? '').match(/\d+/)?.[0] ?? '', 10);
if (!Number.isFinite(major) || major < 16) {
return { disposition: 'verified', reason: 'stable Next.js 16 cache API requirement does not apply' };
}
const text = recText(rec);
if (/\bunstable_(?:cacheLife|cacheTag)\b/.test(text)) {
return {
disposition: 'failed',
reason: 'Next.js 16 rec uses unstable cache API; use cacheLife/cacheTag from next/cache',
};
}
if (/\brevalidateTag\s*\([^)]*['"`][^'"`]+['"`]\s*\)/.test(text) &&
!/\brevalidateTag\s*\([^)]*['"`][^'"`]+['"`]\s*,/.test(text)) {
return {
disposition: 'failed',
reason: 'Next.js 16 revalidateTag examples must include the cache-life profile argument',
};
}
return { disposition: 'verified', reason: 'Next.js 16 cache API usage matches stable names' };
}
async function verifyNextRuntimeCacheApiForVersion({ rec, framework, frameworkVersion }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_runtime_cache_api_for_version requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
const major = parseInt(String(frameworkVersion ?? '').match(/\d+/)?.[0] ?? '', 10);
if (!Number.isFinite(major) || major < 16) {
return { disposition: 'verified', reason: 'Next.js 16 Runtime Cache API requirement does not apply' };
}
const text = recText(rec);
const citations = Array.isArray(rec.citations) ? rec.citations.join('\n') : '';
if (/\bunstable_cache\b/.test(text) &&
(/\bRuntime Cache\b/i.test(text) || /vercel\.com\/docs\/caching\/runtime-cache/i.test(citations))) {
return {
disposition: 'failed',
reason: 'Next.js 16 Runtime Cache recommendations must use use cache: remote or fetch with force-cache, not unstable_cache',
};
}
return { disposition: 'verified', reason: 'Next.js Runtime Cache API usage matches project version' };
}
async function verifyNextCacheComponentsRuntimeCachePreference({ rec, framework, cacheComponents }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_cache_components_runtime_cache_preference requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
if (cacheComponents !== true) {
return { disposition: 'verified', reason: 'Cache Components not detected as enabled' };
}
const text = recText(rec);
if (/\buse cache:\s*remote\b/i.test(text)) {
return { disposition: 'verified', reason: 'recommendation uses framework-native remote cache for Cache Components project' };
}
if (/\b(?:fallback|only if|when Cache Components (?:is|are) unavailable|if cacheComponents is false)\b[^.\n]{0,180}\b(?:Runtime Cache|@vercel\/functions|getCache\s*\()/i.test(text)) {
return { disposition: 'verified', reason: 'Runtime Cache is framed as a fallback, not the primary Cache Components path' };
}
return {
disposition: 'failed',
reason: 'Next.js Cache Components is enabled; prefer `use cache: remote` before recommending lower-level Runtime Cache APIs',
};
}
async function verifyNextCacheLifeSingleExecution({ rec, framework, frameworkVersion }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_cache_life_single_execution requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
const major = parseInt(String(frameworkVersion ?? '').match(/\d+/)?.[0] ?? '', 10);
if (!Number.isFinite(major) || major < 16) {
return { disposition: 'verified', reason: 'Next.js 16 cacheLife execution rule does not apply' };
}
const text = recText(rec);
const calls = [...text.matchAll(/\bcacheLife\s*\(/g)].map((m) => m.index ?? -1).filter((i) => i >= 0);
if (calls.length <= 1) {
return { disposition: 'verified', reason: 'at most one cacheLife() call appears in the recommendation' };
}
for (let i = 0; i < calls.length - 1; i++) {
const between = text.slice(calls[i], calls[i + 1]);
if (/\b(?:if|else|switch|case)\b|[?:]/.test(between)) continue;
return {
disposition: 'failed',
reason: 'multiple cacheLife() calls appear on one recommended code path; only one should execute per function invocation',
};
}
return { disposition: 'verified', reason: 'multiple cacheLife() calls appear only in separate control-flow branches' };
}
async function verifyNextCacheLifetimeFreshnessSupported({ rec, files, repoRoot = '.', projectRootDirectory = null }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_cache_lifetime_freshness_supported requires rec' };
const text = recText(rec);
if (!/\bcacheLife\s*\(/.test(text)) {
return { disposition: 'verified', reason: 'no cacheLife() lifetime change detected' };
}
const tags = dedupeCacheTags([
...extractCacheTags(text),
...await extractCacheTagsFromFiles(files, repoRoot, projectRootDirectory),
]);
if (tags.length === 0) {
if (cacheLifeNeedsContentFreshnessProof(text)) {
return {
disposition: 'failed',
reason: 'cacheLife() lengthens content-derived data without cacheTag/revalidateTag evidence; add invalidation evidence or keep the finding out of the ready-to-apply list',
};
}
return { disposition: 'unverifiable', reason: 'cacheLife() rec has no cacheTag evidence to verify against invalidation' };
}
const recTextAsFile = [{ path: '<recommendation>', content: text }];
const invalidationFiles = [
...recTextAsFile,
...await readCacheInvalidationFiles(repoRoot, projectRootDirectory),
];
const missing = tags.filter((tag) => !tagHasMatchingInvalidation(tag, invalidationFiles));
if (missing.length === 0) {
return { disposition: 'verified', reason: 'cacheLife() freshness change has matching cache tag invalidation evidence' };
}
return {
disposition: 'failed',
reason: `cacheLife() would lengthen tagged content without matching revalidateTag/updateTag evidence for: ${missing.map((t) => t.label).join(', ')}`,
};
}
async function verifyNextCacheComponentsRouteChainFile({ rec, framework, frameworkVersion, cacheComponents, signals }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_cache_components_route_chain_file requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
const major = parseInt(String(frameworkVersion ?? '').match(/\d+/)?.[0] ?? '', 10);
if (!Number.isFinite(major) || major < 16) {
return { disposition: 'verified', reason: 'Cache Components route-chain check does not apply' };
}
if (cacheComponents !== true) {
return { disposition: 'verified', reason: 'Cache Components not detected as enabled' };
}
const targetRoute = routeFromCandidateRef(rec.candidateRef);
if (!targetRoute) {
return { disposition: 'unverifiable', reason: 'Cache Components layout recommendation has no route candidateRef' };
}
const routeRows = Array.isArray(signals?.codebase?.routes) ? signals.codebase.routes : [];
if (routeRows.length === 0) {
return { disposition: 'unverifiable', reason: 'codebase route map unavailable for layout route-chain check' };
}
const layoutFiles = recommendationFilesFromRec(rec)
.filter((file) => /(^|\/)layout\.(?:tsx?|jsx?)$/.test(String(file)));
if (layoutFiles.length === 0) {
return { disposition: 'verified', reason: 'no layout files named in recommendation' };
}
const layoutRoutes = routeRows.filter((route) =>
route?.type === 'layout' &&
route?.file &&
layoutFiles.some((file) => pathSuffixMatches(file, route.file))
);
if (layoutRoutes.length === 0) {
return {
disposition: 'failed',
reason: 'Cache Components recommendation cites a layout file that is not present in the scanned route map',
};
}
const target = normalizeRouteForLayoutMatch(targetRoute);
const matchingLayout = layoutRoutes.find((layout) =>
layoutAppliesToCandidateRoute(layout.routePath, target)
);
if (matchingLayout) {
return {
disposition: 'verified',
reason: `layout ${matchingLayout.file} is in the observed route chain for ${targetRoute}`,
};
}
return {
disposition: 'failed',
reason: 'Cache Components recommendation cites a layout file outside the observed route chain for this candidate route',
};
}
async function verifyNextCacheLifeCdnHeaderSemantics({ rec, framework, frameworkVersion }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_cache_life_cdn_header_semantics requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
const major = parseInt(String(frameworkVersion ?? '').match(/\d+/)?.[0] ?? '', 10);
if (!Number.isFinite(major) || major < 15) {
return { disposition: 'verified', reason: 'Cache Components cacheLife semantics do not apply to this Next.js version' };
}
return {
disposition: 'failed',
reason: 'cacheLife() controls the Cache Components lifetime and defaults to the default profile when omitted; do not claim it emits CDN Cache-Control headers or that missing cacheLife alone makes a route run per request without production header evidence',
};
}
async function verifyImageResponseHeadersCitation({ rec, framework }) {
if (!rec) return { disposition: 'unsupported', reason: 'image_response_headers_citation requires rec' };
if (framework && framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
const citations = Array.isArray(rec.citations) ? rec.citations.join('\n') : '';
if (/nextjs\.org\/docs\/app\/api-reference\/functions\/image-response/i.test(citations)) {
return { disposition: 'verified', reason: 'ImageResponse header option is backed by the ImageResponse API reference' };
}
return {
disposition: 'failed',
reason: 'ImageResponse header changes need the ImageResponse API reference citation',
};
}
async function verifyNextImagePriorityApiForVersion({ rec, framework, frameworkVersion }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_image_priority_api_for_version requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
const major = parseInt(String(frameworkVersion ?? '').match(/\d+/)?.[0] ?? '', 10);
if (!Number.isFinite(major) || major < 16) {
return { disposition: 'verified', reason: 'next/image priority deprecation does not apply before Next.js 16' };
}
const text = recText(rec);
if (/\b(?:preload|fetchPriority|loading\s*=\s*['"`]eager['"`]|loading:\s*['"`]eager['"`])\b/.test(text) &&
!/<Image\b[^>]*\bpriority(?:\s|=|>)/i.test(text)) {
return { disposition: 'verified', reason: 'Next.js 16 image preload guidance uses the replacement API' };
}
return {
disposition: 'failed',
reason: 'Next.js 16 deprecates next/image priority; use preload, fetchPriority, or loading="eager" based on the image loading intent',
};
}
async function verifyNextCacheComponentsRouteSegmentConfig({ rec, framework, frameworkVersion, cacheComponents }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_cache_components_route_segment_config requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
const major = parseInt(String(frameworkVersion ?? '').match(/\d+/)?.[0] ?? '', 10);
if (!Number.isFinite(major) || major < 16) {
return { disposition: 'verified', reason: 'Cache Components route segment config restriction does not apply' };
}
if (cacheComponents !== true) {
return { disposition: 'verified', reason: 'Cache Components not detected as enabled' };
}
const text = recText(rec);
if (/\broute segment config options?\b[^.\n]{0,120}\b(?:Route Handlers?|handlers?)\b[^.\n]{0,120}\b(?:no longer apply|do not apply|removed)\b/i.test(text)) {
return {
disposition: 'failed',
reason: 'Route Segment Config still has Route Handler options; with Cache Components only dynamic, revalidate, and fetchCache are removed',
};
}
const blocked = [
/\bdynamicParams\b/.test(text) ? 'dynamicParams' : null,
/\bfetchCache\b/.test(text) ? 'fetchCache' : null,
/\bexport\s+const\s+dynamic\b/.test(text) ? 'dynamic' : null,
/\bexport\s+const\s+revalidate\b/.test(text) ? 'revalidate' : null,
].filter(Boolean);
if (blocked.length === 0) {
return { disposition: 'verified', reason: 'no removed route segment config option detected' };
}
return {
disposition: 'failed',
reason: `Next.js ${major} project has Cache Components enabled; route segment config option(s) ${blocked.join(', ')} are removed and must not be recommended`,
};
}
async function verifyNextRouteRevalidateStaticPrereq({ rec, framework, cacheComponents, repoRoot = '.', projectRootDirectory = null }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_route_revalidate_static_prereq requires rec' };
if (framework !== 'next') return { disposition: 'verified', reason: 'not a Next.js project' };
if (cacheComponents === true) {
return { disposition: 'verified', reason: 'Cache Components route-segment restrictions are handled separately' };
}
const files = recommendationFilesFromRec(rec)
.filter((file) => /(^|\/)app\/.+\/(?:page|layout|template)\.(?:tsx?|jsx?)$/.test(String(file)) ||
/(^|\/)(?:page|layout|template)\.(?:tsx?|jsx?)$/.test(String(file)));
if (files.length === 0) {
return { disposition: 'verified', reason: 'route-level revalidate recommendation does not target a page/layout/template file' };
}
const dynamicHits = [];
for (const file of files) {
const routeChain = await readNextRouteChainFiles(file, repoRoot, projectRootDirectory);
if (routeChain.length === 0) {
return { disposition: 'unverifiable', reason: `could not inspect route chain for ${file}` };
}
for (const entry of routeChain) {
const hit = firstDynamicRouteChainReason(entry.content);
if (hit) dynamicHits.push(`${entry.relative}:${hit}`);
}
}
if (dynamicHits.length > 0) {
return {
disposition: 'failed',
reason: `route-level revalidate can be defeated by request-time APIs or auth helpers in the route chain (${dynamicHits.slice(0, 3).join(', ')}); prove the route is ISR/static from next build output or move the dynamic read out before recommending revalidate`,
};
}
return { disposition: 'verified', reason: 'no request-time API or common auth helper detected in the recommended route chain' };
}
async function verifyNextCacheTagInvalidationSupported({ rec, repoRoot = '.', projectRootDirectory = null }) {
if (!rec) return { disposition: 'unsupported', reason: 'next_cache_tag_invalidation_supported requires rec' };
const tags = extractCacheTags(recText(rec));
if (tags.length === 0) {
return { disposition: 'unsupported', reason: 'cache invalidation claim did not include parseable cacheTag() values' };
}
let files;
try {
files = await readCacheInvalidationFiles(repoRoot, projectRootDirectory);
} catch {
return { disposition: 'unverifiable', reason: 'could not scan repo for matching revalidateTag/updateTag calls' };
}
const missing = [];
for (const tag of tags) {
if (!tagHasMatchingInvalidation(tag, files)) missing.push(tag.label);
}
if (missing.length === 0) {
return { disposition: 'verified', reason: 'every claimed cacheTag has a matching revalidateTag/updateTag path' };
}
return {
disposition: 'failed',
reason: `cache invalidation was claimed for tag(s) without matching revalidateTag/updateTag evidence: ${missing.join(', ')}`,
};
}
async function verifyCacheRecNotErrorDominatedOrAcknowledged({ rec, signals }) {
if (!rec) return { disposition: 'unsupported', reason: 'cache_rec_not_error_dominated_or_acknowledged requires rec' };
const route = routeFromCandidateRef(rec.candidateRef);
if (!route) return { disposition: 'unverifiable', reason: 'cache recommendation has no route candidateRef' };
const status = functionStatusForRoute(signals, route);
if (!status || status.total <= 0) {
return { disposition: 'unverifiable', reason: 'no function status metrics available for cache route' };
}
const errorRate = status.errors / status.total;
if (errorRate <= 0.2) {
return { disposition: 'verified', reason: `function 5xx rate is not dominant (${formatPct(errorRate)})` };
}
const text = recText(rec);
if (/\b(?:5xx|500|errors?|error-rate|non-error|successful|2xx|after\s+(?:fixing|resolving)\s+errors?)\b/i.test(text)) {
return { disposition: 'verified', reason: `cache recommendation acknowledges high 5xx share (${formatPct(errorRate)})` };
}
return {
disposition: 'failed',
reason: `route has high function 5xx share (${formatPct(errorRate)}); cache impact must exclude or acknowledge error traffic`,
};
}
async function verifyCacheControlHeaderSyntax({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'cache_control_header_syntax requires rec' };
const values = [
...extractHeaderValues(recText(rec), 'Cache-Control'),
...extractHeaderValues(recText(rec), 'CDN-Cache-Control'),
...extractHeaderValues(recText(rec), 'Vercel-CDN-Cache-Control'),
];
if (values.length === 0) {
return { disposition: 'unverifiable', reason: 'no parseable Cache-Control header value in recommendation' };
}
const invalid = values.find((value) => hasEmptyCacheDirective(value));
if (invalid) {
return {
disposition: 'failed',
reason: `Cache-Control header contains an empty directive: ${invalid}`,
};
}
return { disposition: 'verified', reason: 'cache header directives are syntactically non-empty' };
}
async function verifyCacheControlHeadersCitation({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'cache_control_headers_citation requires rec' };
const citations = Array.isArray(rec.citations) ? rec.citations.join('\n') : '';
if (/vercel\.com\/docs\/caching\/(?:cache-control-headers|cdn-cache)/i.test(citations)) {
return { disposition: 'verified', reason: 'Cache-Control change is backed by Vercel cache documentation' };
}
return {
disposition: 'failed',
reason: 'Cache-Control header changes need Vercel cache documentation citation',
};
}
async function verifyCachePolicyPositiveOrNoReadyRec({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'cache_policy_positive_or_no_ready_rec requires rec' };
const text = recText(rec);
const positivePolicy = /\b(?:s-maxage|stale-while-revalidate|CDN-Cache-Control|Vercel-CDN-Cache-Control|Cache-Control:\s*public|next:\s*\{\s*revalidate|revalidate\s*[:=]\s*\d|cacheLife\s*\(|cacheTag\s*\(|['"`]use cache(?::\s*remote)?['"`]|Runtime Cache|getCache\s*\(|force-cache)\b/i.test(text);
if (positivePolicy) {
return { disposition: 'verified', reason: 'cache recommendation names a positive cache policy' };
}
if (/\b(?:no-store|no-cache|cache:\s*['"`]no-store['"`])\b/i.test(text)) {
return {
disposition: 'failed',
reason: 'cache candidates must not ship a no-store-only recommendation; if no-store is correct, report no change instead',
};
}
return {
disposition: 'failed',
reason: 'cache candidate recommendation does not name a cache policy; specify CDN headers, framework cache, Runtime Cache, or report no change',
};
}
async function verifyCache404LongTtlSafety({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'cache_404_long_ttl_safety requires rec' };
const text = recText(rec);
if (/\b(?:leave|keep|leaving|keeping)\b[^.\n]{0,120}\b(?:404|not[- ]found|notFound|not found branch|not-found branch)\b[^.\n]{0,120}\b(?:uncached|no-store|no-cache|short|separate)\b/i.test(text) ||
/\b(?:404|not[- ]found|notFound|not found branch|not-found branch)\b[^.\n]{0,120}\b(?:uncached|no-store|no-cache|short|separate)\b/i.test(text)) {
return { disposition: 'verified', reason: 'recommendation keeps 404/not-found caching separate or uncached' };
}
if (/\b(?:both|all)\b[^.\n]{0,120}\bResponse\b[^.\n]{0,120}\b(?:404|not[- ]found|notFound|not found branch|not-found branch)\b/i.test(text) ||
/\b(?:add|set|include)\b[^.\n]{0,160}\b(?:Cache-Control|s-maxage|stale-while-revalidate|CDN-Cache-Control|Vercel-CDN-Cache-Control)\b[^.\n]{0,220}\b(?:each|every|all|both|\d+|four)\b[^.\n]{0,120}\bResponse\b[^.\n]{0,160}\b(?:404|not[- ]found|notFound|not found branch|not-found branch)\b/i.test(text) ||
/\b(?:404|not[- ]found|notFound|not found branch|not-found branch)\b[^.\n]{0,160}\b(?:s-maxage|stale-while-revalidate|CDN-Cache-Control|Vercel-CDN-Cache-Control)\b/i.test(text)) {
return {
disposition: 'failed',
reason: 'long shared caching for 404/not-found branches needs explicit freshness evidence; leave those branches uncached or short-lived',
};
}
return {
disposition: 'failed',
reason: 'cache recommendation mentions a 404/not-found branch without explicitly keeping that branch uncached or short-lived',
};
}
async function verifyRouteErrorNotFoundStatusAndScope({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'route_error_not_found_status_and_scope requires rec' };
const text = recText(rec);
const hasExplicit404 = /\bstatus\s*:\s*404\b/i.test(text);
if (!hasExplicit404) {
return {
disposition: 'failed',
reason: 'not-found error handling must set an explicit 404 status; a markdown/body-only Response defaults to 200',
};
}
if (routeErrorFixExplicitlyConvertsUnexpectedErrorsToNotFound(text)) {
return {
disposition: 'failed',
reason: 'route-error 404 fixes must not convert unexpected exceptions into not-found responses; classify expected misses and preserve 5xx behavior for unknown errors',
};
}
const classifiesKnownMiss = /\b(?:known|expected|missing|not[- ]found|not found|ENOENT|NoSuchKey|content[- ]miss|file[- ]miss)\b[^.\n]{0,160}\b(?:only|separate|classif|branch|guard|case)\b/i.test(text) ||
/\b(?:only|separate|classif|branch|guard|case)\b[^.\n]{0,160}\b(?:known|expected|missing|not[- ]found|not found|ENOENT|NoSuchKey|content[- ]miss|file[- ]miss)\b/i.test(text);
const preservesUnknownErrors = /\b(?:unknown|unexpected|all other|other)\b[^.\n]{0,180}\b(?:rethrow|throw|500|5xx|surface|preserv|remain visible|do not convert)\b/i.test(text) ||
/\b(?:rethrow|throw|500|5xx|surface|preserv|remain visible|do not convert)\b[^.\n]{0,180}\b(?:unknown|unexpected|all other|other)\b/i.test(text);
if (classifiesKnownMiss && preservesUnknownErrors) {
return { disposition: 'verified', reason: 'catch path separates expected misses from unknown errors and sets status 404' };
}
if (routeErrorFixBroadlyCatchesNotFound(text)) {
return {
disposition: 'failed',
reason: 'route-error 404 fixes must classify expected misses before returning not-found and must not turn generic catch blocks into 404 responses',
};
}
return {
disposition: 'failed',
reason: 'route-error 404 fixes must classify expected misses separately and preserve logging or 5xx behavior for unknown errors',
};
}
function routeErrorFixExplicitlyConvertsUnexpectedErrorsToNotFound(text) {
return /\bunexpected\s+exceptions?\b[^.\n]{0,180}\b(?:degrade|convert|return|become|map)\b[^.\n]{0,160}\b(?:404|not[- ]found|not found|notFound)\b/i.test(text) ||
/\b(?:404|not[- ]found|not found|notFound)\b[^.\n]{0,160}\b(?:for|on)\b[^.\n]{0,80}\b(?:any|all|unexpected|unknown)\b[^.\n]{0,80}\bexceptions?\b/i.test(text) ||
/\b(?:any|all|unexpected|unknown)\b[^.\n]{0,80}\bexceptions?\b[^.\n]{0,160}\b(?:404|not[- ]found|not found|notFound)\b/i.test(text);
}
function routeErrorFixBroadlyCatchesNotFound(text) {
return /\b(?:catch|catch\s*\([^)]*\))\b[^.\n]{0,220}\b(?:return|respond|degrade|convert)\b[^.\n]{0,160}\b(?:404|not[- ]found|not found|notFound)\b/i.test(text);
}
async function verifyImmutableDynamicRouteSafety({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'immutable_dynamic_route_safety requires rec' };
const text = recText(rec);
if (/\b(?:content[- ]hash(?:ed)?|hashed|fingerprint(?:ed)?|versioned\s+URL|URL\s+changes\s+when\s+bytes\s+change)\b/i.test(text)) {
return { disposition: 'verified', reason: 'immutable cache header is tied to a byte-versioned URL' };
}
if (/\bVercel-CDN-Cache-Control\b/i.test(text) && !/(?:^|[^A-Za-z-])Cache-Control\s*:\s*[^.\n]*\bimmutable\b/i.test(text)) {
return { disposition: 'verified', reason: 'immutable directive is scoped away from browser Cache-Control' };
}
return {
disposition: 'failed',
reason: 'immutable browser caching on a dynamic route requires a content-hashed or otherwise byte-versioned URL',
};
}
async function verifyAuthGuardParallelizationSafety({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'auth_guard_parallelization_safety requires rec' };
const text = recText(rec);
if (/\b(?:query|lookup|fetch)\b[^.\n]{0,120}\b(?:constrained|scoped|filtered)\b[^.\n]{0,120}\b(?:email|user|owner|ownership|session|account|tenant|permission|auth)/i.test(text) ||
/\b(?:preserve|keep|retain)\b[^.\n]{0,120}\b(?:auth|authorization|ownership|permission|access)\s+(?:check|guard|gate)\b[^.\n]{0,120}\b(?:before|ahead of|prior to|sequential|not parallel)/i.test(text)) {
return { disposition: 'verified', reason: 'parallelization recommendation preserves the auth/ownership guard' };
}
if (/\bPromise\.all\s*\([\s\S]{0,500}(?:private|secret|token|registrant|ticket|payment|account|user)\w*[\s\S]{0,500}(?:owns|owner|ownership|authorize|auth|permission|access)\w*/i.test(text) ||
/\bPromise\.all\s*\([\s\S]{0,500}(?:owns|owner|ownership|authorize|auth|permission|access)\w*[\s\S]{0,500}(?:private|secret|token|registrant|ticket|payment|account|user)\w*/i.test(text)) {
return {
disposition: 'failed',
reason: 'parallelization may fetch private data before the ownership/auth check has passed; combine the authorized query or keep the guard sequential',
};
}
return {
disposition: 'unverifiable',
reason: 'auth-sensitive parallelization needs explicit evidence that private data is not fetched before authorization',
};
}
async function verifyParallelizationImpactNotOverclaimed({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'parallelization_impact_not_overclaimed requires rec' };
const text = recText(rec);
if (/\b(?:measured|trace|span|profile|instrumented)\b[^.\n]{0,120}\b(?:duration|round[- ]trip|query|helper|await)\b/i.test(text)) {
return { disposition: 'verified', reason: 'parallelization impact claim cites measured helper/span duration' };
}
return {
disposition: 'failed',
reason: 'parallelization impact promises a helper/round-trip-sized drop without measured helper or span timing',
};
}
async function verifyParallelizationNotCpuBoundWork({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'parallelization_not_cpu_bound_work requires rec' };
const text = recText(rec);
if (/\b(?:measured|trace|span|profile|instrumented)\b[^.\n]{0,160}\b(?:wait|I\/O|io|network|fetch|database|query|CMS|API)\b/i.test(text)) {
return { disposition: 'verified', reason: 'parallelization target cites measured wait/I/O time' };
}
if (/\b(?:cpu\.p95|CPU p95|cpu p95|CPU-bound|compute-bound|in-process compute|compileMDX|MDX compilation|compilation|render compute)\b/i.test(text)) {
return {
disposition: 'failed',
reason: 'parallelization targets CPU/compile work without measured independent wait time; Promise.all is not a safe latency fix for CPU-bound work',
};
}
return { disposition: 'verified', reason: 'parallelization target is not described as CPU-bound work' };
}
async function verifyRuntimeErrorCauseSupported({ rec }) {
if (!rec) return { disposition: 'unsupported', reason: 'runtime_error_cause_supported requires rec' };
const text = recText(rec);
const hasRuntimeStack = /\b(?:stack|logs?|trace)\b[\s\S]{0,220}\b(?:Error:|ENOENT|ETIMEDOUT|ECONNRESET|NEXT_|at\s+[\w./[\]()-]+(?::\d+)?)/i.test(text);
if (hasRuntimeStack) {
return { disposition: 'verified', reason: 'runtime error cause is backed by logs or stack evidence' };
}
return {
disposition: 'failed',
reason: 'runtime error root cause was claimed without runtime logs or stack evidence',
};
}
async function verifyVercelIgnoreCommandProjectState({ rec, signals }) {
if (!rec) return { disposition: 'unsupported', reason: 'vercel_ignore_command_project_state requires rec' };
const project = signals?.project;
if (!project || typeof project !== 'object') {
return { disposition: 'unverifiable', reason: 'project configuration unavailable for Ignored Build Step check' };
}
const text = recText(rec);
if (typeof project.commandForIgnoringBuildStep === 'string' && project.commandForIgnoringBuildStep.trim() !== '') {
return {
disposition: 'failed',
reason: 'project already has an Ignored Build Step command configured; do not recommend adding another without evidence the current command is insufficient',
};
}
if (project.enableAffectedProjectsDeployments === true &&
/\b(?:Ignored Build Step|ignoreCommand|turbo-ignore|skip unaffected|unaffected projects?)\b/i.test(text)) {
return {
disposition: 'failed',
reason: 'project already has Vercel skip-unaffected deployments enabled; do not recommend another build-skipping change without evidence that automatic skipping is unavailable or insufficient',
};
}
return { disposition: 'verified', reason: 'project config does not contradict Ignored Build Step recommendation' };
}
async function verifyTurboBuildCacheSafety({ rec, files, repoRoot = '.', projectRootDirectory = null, framework }) {
if (!rec) return { disposition: 'unsupported', reason: 'turbo_build_cache_safety requires rec' };
const candidateFiles = Array.isArray(files) ? files : [];
const turboFiles = candidateFiles.filter((file) => /(^|\/)turbo\.json$/.test(String(file)));
if (turboFiles.length === 0) {
return { disposition: 'unverifiable', reason: 'Turbo build-cache recommendation has no turbo.json file to inspect' };
}
const text = recText(rec);
for (const turboFile of turboFiles) {
let turbo;
try {
const { content } = await readClaimFile({ file: turboFile, repoRoot, projectRootDirectory });
turbo = parseJsonLike(content);
} catch {
return { disposition: 'unverifiable', reason: `cannot parse ${turboFile} for Turbo cache safety` };
}
const buildTask = turbo?.tasks?.build ?? turbo?.pipeline?.build ?? null;
const outputs = Array.isArray(buildTask?.outputs) ? buildTask.outputs.map(String) : [];
const pkgFile = siblingPackageJson(turboFile);
const pkg = await readOptionalJsonFile({ file: pkgFile, repoRoot, projectRootDirectory });
const buildScript = typeof pkg?.scripts?.build === 'string' ? pkg.scripts.build : '';
const hasNext = framework === 'next' || Boolean(pkg?.dependencies?.next || pkg?.devDependencies?.next);
if (buildScriptHasMigrationSideEffect(buildScript) && !recSeparatesTurboBuildSideEffects(text)) {
return {
disposition: 'failed',
reason: 'Turbo build caching is unsafe for this build task because the package build script runs migrations or other side effects; split those steps before caching the build output',
};
}
if (hasNext && outputs.length > 0 && !outputs.some((output) => /\.next(?:\/|\*\*)/.test(output))) {
return {
disposition: 'failed',
reason: 'Turbo build cache outputs do not include Next.js build output (`.next/**`); fix the output contract before enabling build caching',
};
}
}
return { disposition: 'verified', reason: 'Turbo build cache recommendation does not conflict with local build scripts or outputs' };
}
function siblingPackageJson(file) {
return join(dirname(String(file)), 'package.json');
}
async function readOptionalJsonFile(claim) {
try {
const { content } = await readClaimFile(claim);
return JSON.parse(content);
} catch {
return null;
}
}
function parseJsonLike(content) {
return JSON.parse(
String(content)
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/.*$/gm, '$1')
.replace(/,\s*([}\]])/g, '$1')
);
}
function buildScriptHasMigrationSideEffect(script) {
return /\b(?:payload\s+migrate|prisma\s+migrate|knex\s+migrate|sequelize\s+db:migrate|db:migrate|migrate(?::|\s|$)|migration)\b/i.test(String(script));
}
function recSeparatesTurboBuildSideEffects(text) {
return /\b(?:split|separate|move|keep)\b[^.\n]{0,180}\b(?:migrations?|side effects?|payload migrate|prisma migrate)\b[^.\n]{0,180}\b(?:outside|before|uncached|separate)\b/i.test(text) ||
/\b(?:cache|enable caching for)\b[^.\n]{0,120}\b(?:buildonly|pure build|next build)\b[^.\n]{0,180}\b(?:not|without|after separating)\b[^.\n]{0,120}\b(?:migrations?|side effects?)\b/i.test(text);
}
function recText(rec) {
return [rec?.what, rec?.why, rec?.fix, rec?.currentBehavior, rec?.desiredBehavior, rec?.verify]
.filter(Boolean)
.join('\n');
}
function extractHeaderValues(text, header) {
const escaped = escapeRegExp(header);
const values = [];
const quotedKey = new RegExp(`['"\`]${escaped}['"\`]\\s*:\\s*['"\`]([^'"\`\\n]+)['"\`]`, 'gi');
for (const m of text.matchAll(quotedKey)) values.push(m[1].trim());
const bareKey = new RegExp(`\\b${escaped}\\b\\s*:\\s*['"\`]?([^'"\`\\n]+)['"\`]?`, 'gi');
for (const m of text.matchAll(bareKey)) values.push(cleanHeaderValue(m[1]));
return Array.from(new Set(values.filter(Boolean)));
}
function hasHeaderValue(text, header, valuePattern) {
return extractHeaderValues(text, header).some((value) => valuePattern.test(value));
}
function cleanHeaderValue(value) {
return String(value)
.replace(/[).;]+$/g, '')
.replace(/\s+and\s+.*$/i, '')
.trim();
}
function hasEmptyCacheDirective(value) {
return String(value).split(',').some((part) => part.trim() === '');
}
function extractCacheTags(text) {
const tags = [];
const callRe = /\bcacheTag\s*\(([^)]*)\)/gs;
for (const call of text.matchAll(callRe)) {
const args = call[1] ?? '';
for (const m of args.matchAll(/['"]([^'"]+)['"]/g)) {
tags.push({ kind: 'exact', value: m[1], label: m[1] });
}
for (const m of args.matchAll(/`([^`]+)`/g)) {
const raw = m[1];
const prefix = raw.split('${')[0];
if (raw.includes('${') && prefix) {
tags.push({ kind: 'prefix', value: prefix, label: raw });
} else if (!raw.includes('${')) {
tags.push({ kind: 'exact', value: raw, label: raw });
}
}
}
const seen = new Set();
return tags.filter((tag) => {
const key = `${tag.kind}\u0000${tag.value}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
async function extractCacheTagsFromFiles(files, repoRoot, projectRootDirectory) {
const out = [];
if (!Array.isArray(files)) return out;
for (const file of files) {
try {
const { content } = await readClaimFile({ file, repoRoot, projectRootDirectory });
out.push(...extractCacheTags(content));
} catch {}
}
return out;
}
function dedupeCacheTags(tags) {
const seen = new Set();
return tags.filter((tag) => {
const key = `${tag.kind}\u0000${tag.value}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
async function readCacheInvalidationFiles(repoRoot, projectRootDirectory) {
const cacheKey = `${normalize(repoRoot || '.')}\u0000${normalizeProjectRootDirectory(projectRootDirectory) ?? ''}`;
if (cacheInvalidationFileCache.has(cacheKey)) return cacheInvalidationFileCache.get(cacheKey);
const baseRoot = normalize(repoRoot || '.');
const projectRoot = normalizeProjectRootDirectory(projectRootDirectory);
const root = projectRoot ? join(baseRoot, projectRoot) : baseRoot;
try {
await access(root);
} catch {
cacheInvalidationFileCache.set(cacheKey, []);
return [];
}
const rgFiles = await rgRelevantFiles(root);
if (Array.isArray(rgFiles)) {
const files = [];
for (const path of rgFiles.slice(0, 500)) {
try {
files.push({ path, content: await readFile(path, 'utf-8') });
} catch {}
}
cacheInvalidationFileCache.set(cacheKey, files);
return files;
}
const files = [];
for await (const path of walkFiles(root)) {
try {
const content = await readFile(path, 'utf-8');
if (!/\b(?:revalidateTag|updateTag)\s*\(|\btags\s*:/.test(content)) continue;
files.push({ path, content });
} catch {}
}
cacheInvalidationFileCache.set(cacheKey, files);
return files;
}
async function rgRelevantFiles(root) {
try {
const { stdout } = await execFileP('rg', [
'-l',
'--glob', '!node_modules/**',
'--glob', '!.next/**',
'--glob', '!.vercel/**',
'--glob', '!.turbo/**',
'--glob', '!dist/**',
'--glob', '!build/**',
'--glob', '!coverage/**',
'--glob', '!content/**',
'--glob', '!fixtures/**',
'--glob', '!migrations/**',
'--glob', '!public/**',
'--glob', '*.{ts,tsx,js,jsx,mjs,cjs}',
String.raw`\b(?:revalidateTag|updateTag)\s*\(|\btags\s*:`,
root,
], { maxBuffer: 10 * 1024 * 1024 });
return stdout.split(/\r?\n/).filter(Boolean);
} catch (err) {
if (err?.code === 1) return [];
return null;
}
}
function tagHasMatchingInvalidation(tag, files) {
return files.some(({ content }) => {
if (hasLiteralInvalidation(content, tag)) return true;
return hasConfigDrivenInvalidation(content, tag, files);
});
}
function hasLiteralInvalidation(content, tag) {
if (tag.kind === 'exact') {
const escaped = escapeRegExp(tag.value);
return new RegExp(`\\b(?:revalidateTag|updateTag)\\s*\\(\\s*['"\`]${escaped}['"\`]`).test(content);
}
const escaped = escapeRegExp(tag.value);
return new RegExp(`\\b(?:revalidateTag|updateTag)\\s*\\(\\s*\`?${escaped}`).test(content);
}
function hasConfigDrivenInvalidation(content, tag, files) {
if (!/\brevalidateTag\s*\(\s*\w+/.test(content)) return false;
return files.some((file) => configContainsTag(file.content, tag));
}
function configContainsTag(content, tag) {
if (tag.kind === 'exact') {
const escaped = escapeRegExp(tag.value);
return new RegExp(`\\btags\\s*:\\s*\\[[^\\]]*['"\`]${escaped}['"\`]`, 's').test(content);
}
const escaped = escapeRegExp(tag.value);
return new RegExp(`\\btags\\s*:\\s*\\[[^\\]]*\`?${escaped}`, 's').test(content);
}
function routeFromCandidateRef(ref) {
if (typeof ref !== 'string') return null;
const idx = ref.indexOf(':');
if (idx < 0) return null;
const route = ref.slice(idx + 1);
return route && route !== '<account>' && !route.startsWith('<account>#') ? route : null;
}
function functionStatusForRoute(signals, route) {
const rows = signals?.metrics?.fnStatusByRoute?.rows;
if (!Array.isArray(rows)) return null;
const target = canonicalizeRoute(route);
let total = 0;
let errors = 0;
for (const row of rows) {
const rowRoute = row?.route ?? row?.path;
if (!rowRoute || canonicalizeRoute(rowRoute) !== target) continue;
const value = numberValue(row?.value);
if (value == null) continue;
total += value;
if (/^5/.test(String(row?.http_status ?? ''))) errors += value;
}
return total > 0 ? { total, errors } : null;
}
function numberValue(value) {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim() !== '') {
const n = Number(value.replace(/,/g, ''));
return Number.isFinite(n) ? n : null;
}
return null;
}
function asArray(v) {
return Array.isArray(v) ? v : [];
}
function formatPct(n) {
return `${(n * 100).toFixed(1)}%`;
}
function cacheLifeNeedsContentFreshnessProof(text) {
return /\bcacheLife\s*\(\s*['"`](?:hours|days|weeks|max)['"`]\s*\)/i.test(text) &&
/\b(?:CMS|Contentful|Payload|Sanity|WordPress|docs?|guides?|navigation|nav|content|article|blog|OpenAPI|metadata|get[A-Z][\w]*(?:By|For|From)?\w*)\b/.test(text);
}
function recommendationFilesFromRec(rec) {
return Array.from(new Set([
...asArray(rec?.affectedFiles),
...asArray(rec?.findingRefs).map((ref) => String(ref).match(/^(.+?):\d+$/)?.[1]).filter(Boolean),
]));
}
async function readNextRouteChainFiles(file, repoRoot, projectRootDirectory) {
const normalized = normalizeProjectRootDirectory(file);
if (!normalized) return [];
const appIdx = normalized.split('/').lastIndexOf('app');
if (appIdx === -1) {
try {
const { path, content } = await readClaimFile({ file, repoRoot, projectRootDirectory });
return [{ path, relative: normalized, content }];
} catch {
return [];
}
}
const parts = normalized.split('/');
const appParts = parts.slice(0, appIdx + 1);
const routeDirs = parts.slice(appIdx + 1, -1);
const candidates = new Set([normalized]);
for (let depth = 0; depth <= routeDirs.length; depth++) {
const dir = [...appParts, ...routeDirs.slice(0, depth)].join('/');
for (const base of ['layout', 'template']) {
for (const ext of ['tsx', 'ts', 'jsx', 'js']) candidates.add(`${dir}/${base}.${ext}`);
}
}
const out = [];
for (const candidate of candidates) {
try {
const { path, content } = await readClaimFile({ file: candidate, repoRoot, projectRootDirectory });
out.push({ path, relative: candidate, content });
} catch {}
}
return out;
}
function firstDynamicRouteChainReason(content) {
const text = String(content ?? '');
const direct = text.match(/\b(cookies|headers|draftMode|connection)\s*\(/);
if (direct) return `${direct[1]}()`;
const helper = text.match(/\b(withAuth|getServerSession|auth|currentUser)\s*\(/);
if (helper) return `${helper[1]}()`;
if (/from\s+['"]next\/headers['"]/.test(text)) return 'next/headers import';
return null;
}
function pathSuffixMatches(candidateFile, routeFile) {
const candidate = normalizeProjectRootDirectory(candidateFile);
const route = normalizeProjectRootDirectory(routeFile);
if (!candidate || !route) return false;
return candidate === route || candidate.endsWith(`/${route}`) || route.endsWith(`/${candidate}`);
}
function normalizeRouteForLayoutMatch(route) {
const normalized = canonicalizeRoute(String(route ?? ''));
return normalized.startsWith('/') ? normalized : `/${normalized}`;
}
function layoutAppliesToCandidateRoute(layoutPath, targetRoute) {
if (typeof layoutPath !== 'string' || typeof targetRoute !== 'string') return false;
const layout = normalizeRouteForLayoutMatch(layoutPath);
const target = normalizeRouteForLayoutMatch(targetRoute);
if (layout === '/') return true;
let layoutTokens = layout.split('/').filter(Boolean);
const targetTokens = target.split('/').filter(Boolean);
if (layoutTokens.length > targetTokens.length && isDynamicPlaceholder(layoutTokens[0])) {
layoutTokens = layoutTokens.slice(1);
} else if (layoutTokens.length > 0 &&
targetTokens.length > 0 &&
isDynamicPlaceholder(layoutTokens[0]) &&
layoutTokens[1] === targetTokens[0]) {
layoutTokens = layoutTokens.slice(1);
}
if (layoutTokens.length === 0) return true;
if (layoutTokens.length > targetTokens.length) return false;
let literalMatches = 0;
for (let i = 0; i < layoutTokens.length; i++) {
const layoutToken = layoutTokens[i];
const targetToken = targetTokens[i];
if (isCatchAllPlaceholder(layoutToken)) return literalMatches > 0;
if (layoutToken === targetToken) {
literalMatches += 1;
continue;
}
if (isDynamicPlaceholder(layoutToken)) continue;
return false;
}
return literalMatches > 0;
}
function isDynamicPlaceholder(segment) {
return /^\[(?:\.{3})?.+\]$/.test(String(segment ?? ''));
}
function isCatchAllPlaceholder(segment) {
return /^\[\[?\.{3}.+\]?\]$/.test(String(segment ?? ''));
}
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Supports `/pattern/flags` literal-regex form OR plain escaped string. Caller flags merge with embedded flags via Set dedup.
function compilePattern(pattern, flags) {
const m = pattern.match(/^\/(.+)\/([gimsu]*)$/);
if (m) {
const mergedFlags = [...new Set(((m[2] || '') + (flags || '')).split(''))].join('');
return new RegExp(m[1], mergedFlags);
}
return new RegExp(pattern.replace(/[.+^${}()|[\]\\?*]/g, '\\$&'), flags);
}
async function readClaimFile(claim) {
const path = await firstAccessiblePath(claim);
return { path, content: await readFile(path, 'utf-8') };
}
async function firstAccessiblePath({ repoRoot = '.', file, projectRootDirectory = null }) {
let lastErr;
for (const p of repoPaths(repoRoot, file, projectRootDirectory)) {
try {
await access(p);
return p;
} catch (err) {
lastErr = err;
}
}
throw lastErr ?? new Error(`cannot access ${file}`);
}
function repoPaths(repoRoot, file, projectRootDirectory = null) {
if (!file) return [];
if (isAbsolute(file)) return [file];
const out = [join(repoRoot, file)];
const projectRoot = normalizeProjectRootDirectory(projectRootDirectory);
const normalizedFile = normalizeProjectRootDirectory(file);
if (projectRoot && normalizedFile && !normalizedFile.startsWith(`${projectRoot}/`)) {
out.push(join(repoRoot, projectRoot, file));
}
return Array.from(new Set(out.map((p) => normalize(p))));
}
function normalizeProjectRootDirectory(value) {
if (typeof value !== 'string' || value.trim() === '') return null;
return value.replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
}
async function* walkFiles(root, skip = new Set([
'node_modules',
'.next',
'.vercel',
'.turbo',
'dist',
'build',
'coverage',
'.git',
'content',
'fixtures',
'migrations',
'public',
])) {
let entries;
try {
entries = await readdir(root, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
const path = join(root, e.name);
if (e.isDirectory()) {
if (skip.has(e.name)) continue;
yield* walkFiles(path, skip);
continue;
}
if (!e.isFile()) continue;
if (!/\.(tsx?|jsx?|mjs|cjs)$/.test(e.name)) continue;
yield path;
}
}
async function snippetFoundElsewhere(root, snippet, exceptFile) {
const norm = (s) => s.replace(/\s+/g, ' ').trim();
const target = norm(snippet);
if (target.length < 20) return null;
for await (const path of walkFiles(root)) {
if (path.endsWith(exceptFile)) continue;
try {
const content = await readFile(path, 'utf-8');
if (norm(content).includes(target)) return path;
} catch {}
}
return null;
}
lib/workspace-resolver.mjs
// Resolve workspace-package imports to actual source files. Sub-agents need this when the route file is a thin shell that re-exports from a workspace package.
//
// Bounded expansion keeps the brief allowlist small: package export resolution, pure-barrel
// traversal, and suffix fan-out for likely data-loading modules. This stays string-based
// and falls through ("couldn't resolve") on shapes that need a full TS resolver.
import { readFile, readdir, stat } from 'node:fs/promises';
import { dirname, join, resolve as pathResolve } from 'node:path';
const DEFAULT_RESOLVE_OPTIONS = {
pureBarrelDepth: 3,
suffixFanoutDepth: 2,
perSpecifierCap: 3,
};
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
const EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
const INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs'];
const SUFFIX_FANOUT_RE = /(^|\/)(content|data|loader|fetch|service|metadata|actions)\.tsx?$/;
const EXPORT_FORWARD_RE = /export\s+(?:type\s+)?(?:\*|\*\s+as\s+[A-Za-z_$][\w$]*|\{[^}]*\})\s+from\s+['"][^'"\n]+['"]\s*;?/gs;
export async function detectMonorepoRoot(startDir) {
let dir = pathResolve(startDir);
for (let depth = 0; depth < 15; depth++) {
if (await fileExists(join(dir, 'pnpm-workspace.yaml'))) return dir;
const pkg = await tryReadJson(join(dir, 'package.json'));
if (pkg && (Array.isArray(pkg.workspaces) || Array.isArray(pkg.workspaces?.packages))) {
return dir;
}
const parent = dirname(dir);
if (parent === dir) return null;
dir = parent;
}
return null;
}
// Zero-dependency — pnpm-workspace.yaml shape is predictable; not pulling in js-yaml.
export async function readWorkspaceGlobs(monorepoRoot) {
const pnpmPath = join(monorepoRoot, 'pnpm-workspace.yaml');
if (await fileExists(pnpmPath)) {
const text = await readFile(pnpmPath, 'utf-8');
return parsePnpmWorkspaceYaml(text);
}
const pkg = await tryReadJson(join(monorepoRoot, 'package.json'));
if (Array.isArray(pkg?.workspaces)) return pkg.workspaces;
if (Array.isArray(pkg?.workspaces?.packages)) return pkg.workspaces.packages;
return [];
}
// Handles `packages:` block with `- glob` entries. Not full YAML grammar.
export function parsePnpmWorkspaceYaml(text) {
const out = [];
let inPackages = false;
for (const rawLine of text.split('\n')) {
const line = rawLine.replace(/#.*$/, '').trimEnd();
if (!line.trim()) continue;
if (/^packages\s*:/.test(line)) { inPackages = true; continue; }
if (!inPackages) continue;
if (!/^\s/.test(line)) { inPackages = false; continue; }
const m = line.match(/^\s*-\s+['"]?([^'"\s]+)['"]?\s*$/);
if (m) out.push(m[1]);
}
return out;
}
export async function listWorkspacePackages(monorepoRoot) {
const globs = await readWorkspaceGlobs(monorepoRoot);
const dirs = new Set();
for (const g of globs) {
const expanded = await expandWorkspaceGlob(monorepoRoot, g);
for (const d of expanded) dirs.add(d);
}
const out = [];
for (const dir of dirs) {
const pkg = await tryReadJson(join(dir, 'package.json'));
if (pkg?.name) out.push({ name: pkg.name, dir, pkg });
}
return out.sort((a, b) => a.name.localeCompare(b.name));
}
// Handles workspace-shape globs only. `**` collapses to one level — npm/pnpm don't document deep `**`.
async function expandWorkspaceGlob(root, glob) {
const parts = glob.replace(/\\/g, '/').split('/');
return await expandParts(root, parts);
}
async function expandParts(currentDir, parts) {
if (parts.length === 0) return [currentDir];
const [head, ...rest] = parts;
if (head === '' || head === '.') return await expandParts(currentDir, rest);
if (head === '*' || head === '**') {
let entries = [];
try {
entries = await readdir(currentDir, { withFileTypes: true });
} catch { return []; }
const childDirs = entries.filter((e) => e.isDirectory()).map((e) => join(currentDir, e.name));
const out = [];
for (const d of childDirs) {
const more = await expandParts(d, rest);
out.push(...more);
}
return out;
}
const next = join(currentDir, head);
try {
const s = await stat(next);
if (!s.isDirectory()) return [];
} catch {
return [];
}
return await expandParts(next, rest);
}
export function buildResolver(packages) {
const byName = new Map();
for (const p of packages) {
byName.set(p.name, buildPackageLookup(p));
}
return function resolveSpecifier(specifier) {
if (typeof specifier !== 'string' || !specifier.length) return null;
// Longest-name match first so `@vercel/foo-bar` wins over `@vercel/foo`.
const candidates = [...byName.keys()]
.filter((name) => specifier === name || specifier.startsWith(name + '/'))
.sort((a, b) => b.length - a.length);
if (candidates.length === 0) return null;
const pkgName = candidates[0];
const subpath = specifier === pkgName ? '.' : './' + specifier.slice(pkgName.length + 1);
const lookup = byName.get(pkgName);
return lookup.resolveSubpath(subpath);
};
}
// Node spec: pattern key has exactly one `*`; target may have one or zero.
function buildPackageLookup(p) {
const exact = new Map();
const wildcards = [];
const exports = p.pkg.exports;
if (exports && typeof exports === 'object' && !Array.isArray(exports)) {
for (const [key, value] of Object.entries(exports)) {
const target = pickConditionalTarget(value);
if (typeof target !== 'string') continue;
if (key.includes('*')) {
const keyStarIdx = key.indexOf('*');
if (keyStarIdx !== key.lastIndexOf('*')) continue;
wildcards.push({
keyPrefix: key.slice(0, keyStarIdx),
keySuffix: key.slice(keyStarIdx + 1),
valueTemplate: target,
});
} else {
exact.set(key, target);
}
}
}
return {
resolveSubpath(subpath) {
const exactHit = exact.get(subpath);
if (exactHit) return joinPackagePath(p.dir, exactHit);
for (const w of wildcards) {
if (subpath.startsWith(w.keyPrefix) && subpath.endsWith(w.keySuffix)) {
const star = subpath.slice(w.keyPrefix.length, subpath.length - w.keySuffix.length);
if (!star) continue;
const target = w.valueTemplate.replace('*', star);
return joinPackagePath(p.dir, target);
}
}
// Unsafe to guess when no exports declared.
if (exact.size === 0 && wildcards.length === 0 && subpath !== '.') {
return null;
}
return null;
},
};
}
// Condition order matches what Next.js / Vite / esbuild would resolve.
function pickConditionalTarget(value) {
if (typeof value === 'string') return value;
if (Array.isArray(value)) {
for (const item of value) {
const target = pickConditionalTarget(item);
if (typeof target === 'string') return target;
}
return null;
}
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
for (const cond of ['default', 'import', 'node', 'browser', 'require', 'types']) {
const v = value[cond];
if (typeof v === 'string') return v;
}
return null;
}
export async function resolveWorkspaceImports(sourceFilePath, resolver, options = {}) {
let text;
try {
text = await readFile(sourceFilePath, 'utf-8');
} catch {
return [];
}
const opts = { ...DEFAULT_RESOLVE_OPTIONS, ...options };
const refs = extractModuleReferences(text);
const out = [];
const seen = new Set();
for (const ref of refs) {
const resolved = await resolveModuleSpecifier(sourceFilePath, ref.specifier, resolver);
if (!resolved) continue;
const expanded = await expandResolvedSpecifier(resolved, ref.importedNames, resolver, opts);
for (const file of expanded) {
if (seen.has(file)) continue;
seen.add(file);
out.push(file);
}
}
return out;
}
// Skips CommonJS `require('foo')` and template-literal dynamic imports (statically unresolvable).
export function extractImportSpecifiers(text) {
return [...new Set(extractModuleReferences(text).map((ref) => ref.specifier))];
}
function joinPackagePath(packageDir, relativeTarget) {
return join(packageDir, relativeTarget.replace(/^\.\//, ''));
}
async function expandResolvedSpecifier(startFile, importedNames, resolver, opts) {
const out = [];
const seen = new Set();
const barrelVisited = new Set();
const fanoutVisited = new Set();
const add = (file) => {
if (seen.has(file)) return false;
if (out.length > 0 && out.length - 1 >= opts.perSpecifierCap) return false;
seen.add(file);
out.push(file);
return true;
};
add(startFile);
await expandPureBarrel(startFile, importedNames, 0);
const fanoutSeeds = out.slice();
for (const file of fanoutSeeds) {
await expandSuffixFanout(file, 0);
}
return out;
async function expandPureBarrel(file, requestedNames, depth) {
if (depth >= opts.pureBarrelDepth) return;
if (barrelVisited.has(file)) return;
barrelVisited.add(file);
const text = await tryReadText(file);
if (text == null || !isPureBarrel(text)) return;
const refs = await selectRelevantForwards(file, extractExportForwardRefs(text), requestedNames, resolver);
for (const { ref, next } of refs) {
if (!add(next)) return;
await expandPureBarrel(next, requestedNamesForForward(ref, requestedNames), depth + 1);
}
}
async function expandSuffixFanout(file, depth) {
if (depth >= opts.suffixFanoutDepth) return;
if (!isSuffixFanoutFile(file)) return;
const visitKey = `${file}:${depth}`;
if (fanoutVisited.has(visitKey)) return;
fanoutVisited.add(visitKey);
const text = await tryReadText(file);
if (text == null) return;
for (const ref of extractModuleReferences(text)) {
const next = await resolveModuleSpecifier(file, ref.specifier, resolver);
if (!next) continue;
if (!add(next)) return;
if (isSuffixFanoutFile(next)) await expandSuffixFanout(next, depth + 1);
}
}
}
async function selectRelevantForwards(fromFile, refs, requestedNames, resolver) {
const resolved = [];
for (const [index, ref] of refs.entries()) {
const next = await resolveModuleSpecifier(fromFile, ref.specifier, resolver);
if (!next) continue;
let score = requestedNames && requestedNames.size > 0
? forwardRelevanceScore(ref, requestedNames, refs.length)
: 1;
if (requestedNames && requestedNames.size > 0 && await fileExportsAnyName(next, requestedNames)) {
score = Math.max(score, 75);
}
resolved.push({ ref, next, index, score });
}
if (!requestedNames || requestedNames.size === 0) return resolved;
const ranked = resolved
.filter((x) => x.score > 0)
.sort((a, b) => b.score - a.score || a.index - b.index);
return ranked.length > 0 ? ranked : resolved;
}
function forwardRelevanceScore(ref, requestedNames, siblingCount) {
if (!requestedNames || requestedNames.size === 0) return 1;
if (ref.exportedNames) {
for (const name of requestedNames) {
if (ref.exportedNames.has(name)) return 100;
}
}
if (specifierMatchesNames(ref.specifier, requestedNames)) return 50;
return siblingCount === 1 ? 1 : 0;
}
function requestedNamesForForward(ref, requestedNames) {
if (!requestedNames || requestedNames.size === 0) return null;
if (ref.star) return requestedNames;
const out = new Set();
for (const name of requestedNames) {
const source = ref.sourceNamesByExported?.get(name);
if (source) out.add(source);
}
return out.size > 0 ? out : requestedNames;
}
async function resolveModuleSpecifier(fromFile, specifier, resolver) {
const raw = specifier.startsWith('.')
? join(dirname(fromFile), specifier)
: resolver(specifier);
if (!raw) return null;
return await resolveExistingPath(raw);
}
async function resolveExistingPath(basePath) {
for (const ext of EXTENSIONS) {
const candidate = ext === '' ? basePath : basePath + ext;
if (!isSourcePath(candidate)) continue;
if (await isFile(candidate)) return candidate;
}
for (const indexFile of INDEX_FILES) {
const candidate = join(basePath, indexFile);
if (await isFile(candidate)) return candidate;
}
return null;
}
function extractModuleReferences(text) {
return [
...extractImportReferences(text),
...extractExportForwardRefs(text).map((ref) => ({
specifier: ref.specifier,
importedNames: ref.star ? null : ref.exportedNames,
})),
...extractDynamicImportReferences(text),
];
}
function extractImportReferences(text) {
const out = [];
const fromRe = /import\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"]([^'"\n]+)['"]/g;
let m;
while ((m = fromRe.exec(text)) !== null) {
out.push({ specifier: m[2], importedNames: parseImportNames(m[1]) });
}
const sideEffectRe = /import\s+['"]([^'"\n]+)['"]/g;
while ((m = sideEffectRe.exec(text)) !== null) {
out.push({ specifier: m[1], importedNames: null });
}
return out;
}
function extractDynamicImportReferences(text) {
const out = [];
const re = /import\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g;
let m;
while ((m = re.exec(text)) !== null) {
out.push({ specifier: m[1], importedNames: null });
}
return out;
}
function extractExportForwardRefs(text) {
const out = [];
const re = /export\s+(?:type\s+)?(\*|\*\s+as\s+[A-Za-z_$][\w$]*|\{[^}]*\})\s+from\s+['"]([^'"\n]+)['"]\s*;?/g;
let m;
while ((m = re.exec(text)) !== null) {
const clause = m[1].trim();
const star = clause.startsWith('*');
const names = star ? null : parseExportNames(clause);
out.push({
specifier: m[2],
star,
exportedNames: names?.exportedNames ?? null,
sourceNamesByExported: names?.sourceNamesByExported ?? null,
});
}
return out;
}
function parseImportNames(clause) {
const names = new Set();
const trimmed = clause.trim();
if (!trimmed) return null;
const named = /\{([^}]+)\}/s.exec(trimmed);
if (named) {
for (const part of splitImportList(named[1])) {
const cleaned = part.replace(/^type\s+/, '').trim();
if (!cleaned) continue;
const [source] = cleaned.split(/\s+as\s+/i);
if (source?.trim()) names.add(source.trim());
}
}
const withoutNamed = trimmed.replace(/\{[^}]*\}/s, '').replace(/,\s*$/, '').trim();
if (withoutNamed && !withoutNamed.startsWith('*')) names.add('default');
return names.size > 0 ? names : null;
}
function parseExportNames(clause) {
const body = clause.replace(/^\{|\}$/g, '');
const exportedNames = new Set();
const sourceNamesByExported = new Map();
for (const part of splitImportList(body)) {
const cleaned = part.replace(/^type\s+/, '').trim();
if (!cleaned) continue;
const [sourceRaw, exportedRaw] = cleaned.split(/\s+as\s+/i);
const source = sourceRaw.trim();
const exported = (exportedRaw ?? sourceRaw).trim();
if (!source || !exported) continue;
exportedNames.add(exported);
sourceNamesByExported.set(exported, source);
}
return { exportedNames, sourceNamesByExported };
}
function splitImportList(value) {
return value.split(',').map((part) => part.trim()).filter(Boolean);
}
function isPureBarrel(text) {
const refs = extractExportForwardRefs(text);
if (refs.length === 0) return false;
const withoutComments = text
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/^\s*\/\/.*$/gm, '');
return withoutComments.replace(EXPORT_FORWARD_RE, '').trim() === '';
}
function specifierMatchesNames(specifier, names) {
const normalizedSpecifier = normalizeName(specifier.split('/').at(-1) ?? specifier);
for (const name of names) {
const normalizedName = normalizeName(name);
if (normalizedSpecifier === normalizedName || normalizedSpecifier.endsWith(normalizedName)) {
return true;
}
}
return false;
}
function normalizeName(value) {
return String(value ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
}
function isSuffixFanoutFile(file) {
return SUFFIX_FANOUT_RE.test(file.replace(/\\/g, '/'));
}
async function fileExportsAnyName(file, names) {
const text = await tryReadText(file);
if (text == null) return false;
for (const name of names) {
if (textExportsName(text, name)) return true;
}
return false;
}
function textExportsName(text, name) {
const escaped = escapeRegExp(name);
const declaration = new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var|class|interface|type)\\s+${escaped}\\b`);
if (declaration.test(text)) return true;
const listRe = /export\s+\{([^}]+)\}(?!\s+from\b)/gs;
let m;
while ((m = listRe.exec(text)) !== null) {
const names = parseExportNames(`{${m[1]}}`).exportedNames;
if (names.has(name)) return true;
}
return false;
}
function isSourcePath(path) {
const match = /\.([A-Za-z0-9]+)$/.exec(path);
if (!match) return true;
return SOURCE_EXTENSIONS.has('.' + match[1]);
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function tryReadText(path) {
try {
return await readFile(path, 'utf-8');
} catch {
return null;
}
}
async function fileExists(p) {
try { await stat(p); return true; } catch { return false; }
}
async function isFile(p) {
try {
const s = await stat(p);
return s.isFile();
} catch {
return false;
}
}
async function tryReadJson(path) {
try {
const text = await readFile(path, 'utf-8');
return JSON.parse(text);
} catch {
return null;
}
}
metadata.json
{
"version": "1.2.0",
"organization": "Vercel Engineering",
"date": "May 2026",
"abstract": "Deep cost and performance optimization for supported Vercel projects. Pulls observability metrics, billing, and project config from the Vercel CLI, then investigates the codebase only where those metrics point. Produces ranked, verified recommendations with before/after code grounded in version-aware documentation citations. Avoids precise dollar projections (cost framed as order-of-magnitude) and avoids invented documentation URLs (curated allow-list).",
"references": [
"https://vercel.com/docs/cli/metrics",
"https://vercel.com/docs/cli/usage",
"https://vercel.com/docs/cli/api",
"https://vercel.com/docs/observability/observability-plus",
"https://nextjs.org/docs/app/building-your-application/caching",
"https://vercel.com/docs/frameworks/full-stack/sveltekit"
]
}
README.md
# vercel-optimize
Optimize cost and performance for supported projects on Vercel.
This skill uses Vercel metrics to find high-impact improvements in your app. Every recommendation is backed by observed data, scoped code evidence, and version-aware docs.
[](https://skills.sh/vercel-labs/agent-skills)
## Install
Install just this skill:
```bash
npx skills add vercel-labs/agent-skills --skill vercel-optimize
```
Manual install: copy `skills/vercel-optimize` into `.agents/skills/vercel-optimize` and reference `SKILL.md` from your project `AGENTS.md`.
## Requirements
- Node.js 20+
- Vercel CLI with `vercel metrics`, `vercel usage`, `vercel contract`, and `vercel api` support (`npm i -g vercel@latest`). The skill enforces v53+ as its compatibility floor.
- Authenticated Vercel CLI session (`vercel login`)
- Linked Vercel project directory (`vercel link`) for route metrics. `VERCEL_PROJECT_ID` can resolve project config, but it does not replace directory linkage for `vercel metrics`. The project must resolve to a CLI-safe team or personal scope so `vercel metrics`, `vercel usage`, and `vercel contract` all run against the same account.
- Observability Plus for metric-backed route ranking
- Code-backed recommendation coverage is strongest for Next.js and SvelteKit, supported for Nuxt route mapping with generic checks, and limited for Astro. Hono, Remix, and unknown frameworks pause up front.
If route-level metrics are unavailable, the skill pauses before scanner-only mode. Scanner-only can catch traffic-independent code issues, but it cannot rank hot routes or prove cost impact.
## Use
From the Vercel project directory, ask your coding agent:
```text
optimize this Vercel project
```
The agent should collect metrics first. If it starts by reading source files or guessing from `vercel.json`, the skill was not loaded correctly.
## Roadmap
| Attribute | Status |
|---|---|
| Route-level Vercel Function invocations, duration, TTFB, and cold starts | Supported |
| Vercel Function CPU, memory, and GB-hours | Supported |
| Request volume, cache hit rate, HTTP status, and method distribution | Supported |
| Fast Data Transfer and bot traffic patterns | Supported |
| ISR reads, writes, and over-revalidation | Supported |
| Routing Middleware volume and duration | Supported |
| External API latency, volume, and transfer bytes | Supported |
| Core Web Vitals from Speed Insights | Supported |
| Image Optimization usage, source hosts, and source bytes | Supported |
| Build Minutes fan-out | Supported |
| Usage spikes by billing service | Supported |
| Bot Protection and BotID configuration | Supported |
| Fluid Compute configuration and compute signals | Supported |
| Region pinning and project configuration mismatches | Supported |
| Observability Events cost attribution | Supported |
| Route-to-file recommendations for Next.js and SvelteKit | Supported |
| Nuxt route mapping with generic/platform checks | Supported |
| Generic route mapping and platform checks for Astro | Supported |
| Hono route-to-file mapping | Planned |
| Remix route-to-file mapping | Planned |
| AI Gateway usage and cost optimization | Planned |
| Sandbox usage and cost optimization | Planned |
| Blob, Edge Config, Runtime Cache, Workflows, Queues, Flags, and Microfrontends billing dimensions | Planned |
## What You Get
- Ranked recommendations tied to observed Vercel metrics
- Specific route and file references when source changes are justified
- Before/after code for ready recommendations
- Citations from a curated, version-aware documentation allow-list
- Held-back findings when evidence is real but not strong enough for a recommendation
- A concise final message plus a full Markdown report
## Trust Model
- Metrics come first. Code investigation starts only after signals are collected.
- Gates are deterministic JavaScript thresholds. No LLM decides whether a metric qualifies.
- Citations are allow-listed. Unknown URLs and version-mismatched framework docs are stripped.
- Project config contradictions are rejected. For example, the verifier blocks "enable Fluid Compute" when Fluid Compute is already on.
- Cost impact uses magnitude framing, not invented exact savings.
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md). New gates, scanners, playbooks, citations, and sanitizers need fixture coverage in `packages/vercel-optimize-tests`.
## License
MIT
references/candidates.md
<!-- THIS FILE IS GENERATED by scripts/build-docs.mjs. Do not edit by hand. -->
<!-- To change scanner descriptions, edit lib/scanners/*.mjs metadata exports. -->
<!-- To change gate thresholds, edit lib/gates/*.mjs metadata exports. -->
# Candidate gates
The deterministic threshold expressions that turn observability signals into investigation candidates. Pure JS, no LLM. Thresholds live in `lib/gates/*.mjs`.
Total gates: 15. Budget cap: `MAX_CODE_CANDIDATES = 6`. Gate version: `1.8.0`.
## Gates
### `build_minutes_fanout`
- **Threshold**: `Build Minutes share > 0.15 OR turbo-force-bypass finding present`
- **Billing dimension**: build
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`
Build Minutes line dominates the bill or Turborepo cache is bypassed. On monorepos, unchanged work should be skipped through Vercel skip-unaffected behavior, a verified Ignored Build Step, and a complete Turbo cache contract.
---
### `cold_start`
- **Threshold**: `coldPct > 0.4 AND total >= 1000`
- **Billing dimension**: function-duration
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`
Routes where > 40% of invocations are cold-start, at meaningful traffic (>=1,000 total invocations in window). Cold starts add 200-800ms per request and break the perceived latency budget on cache-miss paths. The 40% threshold is where cold-rate becomes a real signal vs Poisson noise on serverless. Sourced from vercel.function_invocation.count grouped by function_start_type.
---
### `cwv_poor`
- **Threshold**: `LCP p75>2500 OR INP p75>200 OR CLS p75>0.1, AND speed_insights count > 50`
- **Billing dimension**: speed-insights
- **Scope**: route
- **Source citation**: `https://web.dev/articles/vitals`
Routes where Core Web Vitals fall into Google's "Poor" band on real-user traffic. LCP > 2500ms, INP > 200ms, or CLS > 0.1 each hurt SEO and conversion. Surfaces one candidate per (route, metric) pair to keep recommendations focused.
---
### `external_api_slow`
- **Threshold**: `p75Ms > 2000 AND callCount >= 500`
- **Billing dimension**: function-duration
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`
External API hostnames with p75 latency above 2 seconds AND at least 500 calls in the window. External API latency is a primary driver of function duration cost when the upstream is on a hot path; a single slow stale call isn't worth recommending against.
---
### `isr_overrevalidation`
- **Threshold**: `writes/reads > 0.5 AND writes > 100`
- **Billing dimension**: isr
- **Scope**: route
- **Source citation**: `https://vercel.com/docs/incremental-static-regeneration`
ISR routes with > 1 write per 2 reads. The revalidate interval is too aggressive relative to read traffic — many reads pay to regenerate. Investigate whether the page can tolerate a longer revalidate window or on-demand revalidation via revalidateTag.
---
### `middleware_heavy`
- **Threshold**: `middlewareInv/totalInv > 0.5 AND middlewareInv > 1000`
- **Billing dimension**: edge-requests
- **Scope**: account
- **Source citation**: `https://nextjs.org/docs/app/building-your-application/routing/middleware`
Middleware invocations cover > 50% of total requests at non-trivial volume. The matcher is probably broader than necessary; narrow it to the paths that actually need auth/rewrites/headers.
---
### `observability_events_attribution`
- **Threshold**: `observabilityEventsShare > 0.20 (critical at > 0.30)`
- **Billing dimension**: observability-events
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`
Observability Events line item exceeds 20% of total billed cost. High share usually traces to low cache hit rate, middleware-heavy traffic, or unconstrained custom-span cardinality. No sampling lever exists for Observability Plus; reduce upstream invocations instead.
---
### `platform_bot_protection`
- **Threshold**: `botIdEnabled=false AND (botPct >= 0.05 OR edge_cost >= $25/window OR requests >= 14k/14d)`
- **Billing dimension**: edge-requests
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`
When BotID is disabled AND there is evidence (observed bot bandwidth share, edge cost, or substantial request volume) that bot traffic is non-trivial. Bot traffic inflates edge request counts without delivering user value; staged bot protection can reduce waste on bot-heavy projects. Skipped on quiet projects with no bot evidence — the recommendation would be noise.
---
### `platform_fluid_compute`
- **Threshold**: `fluid=false AND (any cold_start signal OR any route with p95>1000ms AND inv>1000)`
- **Billing dimension**: function-duration
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`
When Fluid Compute is disabled on a project that shows cold-start pressure (high cold-start rate) or sustained slow function p95 on hot routes. Fluid Compute reduces cold starts via instance reuse — recommend turning it on at the project level rather than per-route.
---
### `region_misconfig`
- **Threshold**: `single-region pin found AND routes.length > 20 (scanner-only branch)`
- **Billing dimension**: function-duration
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`
A single function region is pinned in `vercel.json` or per-route `preferredRegion`. Without per-region TTFB data (data gap), the gate can't quantify the geographic latency cost — but a single-region pin on a project with 20+ routes is worth auditing against Speed Insights traffic geo.
---
### `route_errors`
- **Threshold**: `count > 250 OR (totalRequests >= 1000 AND errorRate > 0.01)`
- **Billing dimension**: function-duration
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`
Routes producing > 250 5xx errors over the window, or with > 1% error rate on at least 1,000 total requests. Errored function invocations still bill at full duration; high error rates also poison user experience.
---
### `scanner-driven`
- **Threshold**: `per-kind: scanner matches.length >= threshold`
- **Billing dimension**: mixed
- **Scope**: mixed
- **Source citation**: `vercel-optimize gate threshold`
Configured kinds emitted from scanner output. Each requires a minimum match count to avoid noise. Findings on cold-path or unmappable files are dropped unless the underlying scanner is trafficIndependent.
---
### `slow_route`
- **Threshold**: `(p95 > 500 AND inv >= 1400) OR (p95 > 1500 AND inv >= 250); disqualified when 5xx rate > 50%; Vercel Workflow runtime endpoints are hard-gated`
- **Billing dimension**: function-duration
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`
Routes with p95 function duration above 500ms at meaningful traffic (>=1,400 invocations in window), OR catastrophically slow routes (>1500ms p95 at any volume >=250). High duration drives both function-duration cost and user-perceived latency. Investigate sequential awaits, slow external APIs, missing caching, N+1 patterns. Routes with >50% 5xx rate are disqualified — those are reliability problems, not performance tuning targets, and surface via route_errors instead. Vercel Workflow runtime endpoints (`/.well-known/workflow/v1/*`) are hard-gated before launch because long-running step/flow requests are expected orchestration, not app-route bottlenecks.
---
### `uncached_route`
- **Threshold**: `requests > 500 AND hitRate < 0.5 AND getShare > 0.2 (missing getShare is gated)`
- **Billing dimension**: edge-requests
- **Scope**: route
- **Source citation**: `vercel-optimize gate threshold`
Routes serving > 500 requests/period at < 50% cache hit AND at least 20% GET traffic. Each uncached GET request reaches the function, costing edge requests + function duration. Routes that are mostly POST/PUT/DELETE (Server Actions, mutations) are skipped — 0% cache is correct behavior there. Routes with missing method-share data are gated instead of launched. Auth-gated routes are disqualified separately.
---
### `usage_spike_triage`
- **Threshold**: `any-day total > 2x mean OR any-day SKU > 3x SKU mean`
- **Billing dimension**: mixed
- **Scope**: account
- **Source citation**: `vercel-optimize gate threshold`
A single day in the billing window deviates sharply from the window baseline. Triage branches: bot or AI crawler spike, viral moment, pricing-model migration (legacy SKU → new), code regression. Without daily-granularity data, this gate stays dormant.
---
references/data-collection.md
# Data collection
What the skill collects in Step 1, where each signal comes from, and how it degrades when a capability is missing.
All shapes here are covered by sanitized CLI fixtures in `packages/vercel-optimize-tests/test/fixtures/real-cli-output/`.
## Table of contents
- [The `signals.json` shape](#the-signalsjson-shape)
- [Per-signal source matrix](#per-signal-source-matrix)
- [Error states and fallbacks](#error-states-and-fallbacks)
- [Real JSON shapes](#real-json-shapes)
- [Why we avoid stderr grep](#why-we-avoid-stderr-grep)
## The `signals.json` shape
`node scripts/collect-signals.mjs` emits the Vercel-side signal document. `node scripts/scan-codebase.mjs <repo-root>` emits the local codebase scan. `node scripts/merge-signals.mjs vercel-signals.json codebase.json --out signals.json` combines them into the artifact consumed by the gate, deep-dive, verifier, and renderer. The merge step also annotates scanner findings with route-level observability, `COLD-PATH`, or `NO-ROUTE-MAPPING`; scanner gates reject non-traffic-independent findings that do not carry one of those deterministic annotations.
The merged `signals.json` has this top-level shape:
```json
{
"schemaVersion": "1.2",
"collectedAt": "2026-05-12T20:48:44.123Z",
"timeWindow": "14d",
"projectId": "prj_xxx",
"orgId": "team_xxx",
"projectIdSource": "repo.json" | "project.json" | "arg" | "env" | "arg+repo.json" | "arg+project.json" | "env+repo.json" | "env+project.json",
"commandScope": {
"ok": true,
"cliScope": "team-slug-or-username",
"source": "team-api" | "whoami-current-team" | "whoami-user" | "linked-org-scope" | "missing-org-scope",
"required": true,
"detail": "..."
},
"frameworkSupport": {
"ok": true,
"status": "supported" | "limited" | "unsupported",
"blocker": null | "unsupported_framework",
"framework": "next",
"label": "Next.js",
"detail": "..."
},
"frameworkSupportBlocker": null | "unsupported_framework",
"frameworkSupportDetail": "...",
"observabilityPlus": true | false | null,
"observabilityPlusPreflight": { /* CLI/API configuration probe result */ },
"observabilityPlusUsable": true | false | null,
"observabilityPlusBlocker": null | "no_oplus_probe" | "project_disabled" | "payment_required" | "forbidden" | "daily_quota_exceeded" | "project_not_found" | "not_linked" | "all_failed_other" | "no_traffic",
"observabilityPlusBlockerDetail": "...",
"plan": { "plan": "hobby" | "pro" | "enterprise" | "uncertain", "reason": "..." },
"project": { /* /v9/projects/:id response; team-owned projects include ?teamId */ },
"contract": { "context": "...", "commitments": [], "totalCommitments": 0 },
"usage": { /* vercel usage --format json --breakdown daily, or null */ },
"usageError": null | "USAGE_UNAVAILABLE" | "USAGE_CONTEXT_MISMATCH" | "NOT_COLLECTED_OBSERVABILITY_BLOCKED" | "NOT_COLLECTED_UNSUPPORTED_FRAMEWORK" | "EXIT_<n>" | "UNKNOWN",
"stack": { /* framework + version + router + ORM + monorepo */ },
"codebase": { /* scan-codebase output: stack + routes + findings */ },
"metrics": { /* per-metric query results (only when observabilityPlus=true) */ },
"metricsSchema": [ /* array of {id, description} */ ]
}
```
All metric queries use the same `timeWindow` constant (`14d`) — defined as `TIME_WINDOW` in [lib/queries.mjs](../lib/queries.mjs) and covered by the repo test suite. Mixing windows silently produces incompatible rollups; never pin a per-query `since`.
All Vercel CLI commands that accept scope must use `commandScope.cliScope` (`--scope <team-slug-or-username>`). Linked project files often contain raw `team_...` or `usr_...` IDs, but several CLI subcommands silently fall back to the current team when `--scope` receives a raw account ID. `collect-signals.mjs` resolves raw team IDs to slugs and raw user IDs to usernames before running `vercel metrics`, `vercel usage`, or `vercel contract`; `deep-dive.mjs` reuses the same scope for follow-up metric queries. If the project link lacks an owner account or the CLI-safe scope cannot be resolved, stop and ask the user which Vercel project and team/personal scope they want audited. Do not infer scope from the current `vercel whoami` team.
Downstream consumers reference `signals.<field>` paths verbatim. Bumping `schemaVersion` is required when any consumed path is renamed or removed.
## Per-signal source matrix
| Signal | CLI command | Required for | Fallback when missing |
|---|---|---|---|
| Auth | `vercel whoami` | Everything | Exit with "run `vercel login`" |
| CLI version | `vercel --version` | Everything | Exit with "upgrade to v53+" — v53 is the skill's compatibility floor |
| Project ID + Org ID | `.vercel/repo.json` (newer) or `.vercel/project.json` (legacy) → `VERCEL_PROJECT_ID` + `VERCEL_ORG_ID` → argv. When the user passes a project ID and multi-project `repo.json` contains exactly one matching entry, the collector uses that entry's owner account. | Everything | Exit with "run `vercel link` or pass projectId". Multi-project `repo.json` without an explicit matching project ID, or any project ID without owner account scope, is ambiguous; ask the user to clarify the intended project/account |
| Framework support | local `package.json` via `detectStack()` + `classifyFrameworkSupport()` | Code-backed route recommendations | Stop before metric fan-out on unsupported frameworks unless the user chooses `--continue-unsupported-framework` |
| CLI command scope | `vercel whoami --format json`, then `vercel api /v2/teams/:orgId` when a linked `team_...` ID must be converted to a slug | Keeps `vercel metrics`, `vercel usage`, and `vercel contract` on the linked project's account instead of the user's current/personal scope | `PROJECT_SCOPE_UNRESOLVED` or `SCOPE_UNRESOLVED`; stop and ask the user to clarify the intended project/account, then re-link under the intended team or personal account |
| Project/scope verification | `vercel api /v9/projects/:id?teamId=<orgId>` for team-owned projects; omit `teamId` for `usr_...` user-owned projects | Proves the resolved account can read the resolved project before Observability Plus or billing conclusions | `PROJECT_SCOPE_MISMATCH`; stop and ask the user to confirm the exact project and team/personal scope. Do not report Observability Plus as missing until this check passes |
| Observability Plus configuration | Vercel CLI/API probe plus one metric access check; user-owned projects skip the team configuration endpoint and rely on the scoped metrics probe | All `metrics.*` signals | Stop early when the account lacks Observability Plus or this project is disabled |
| Observability Plus metrics access | One canary `vercel metrics vercel.request.count --since 14d --limit 1`, then full fan-out only if it succeeds | All `metrics.*` signals | Set `observabilityPlusUsable=false` with blocker detail; emit a blocker document after project/scope verification but before billing collection unless `--continue-without-observability` is passed |
| Project config | Verified project API response from project/scope verification | Fluid Compute, BotID, Speed Insights, security flags | Stop on ownership mismatch; otherwise gates that need missing optional fields skip |
| Plan tier | `vercel api /v2/teams/:orgId` (or `/v2/user` for user-owned projects) → `billing.plan`, then scoped `vercel contract --format json` fallback → `inferPlan()` | Cost-context framing only | `plan="uncertain"`; cost magnitudes still computed from `usage.services[].billedCost` |
| Billing usage | Scoped `vercel usage --format json --from <14d> --to <today>` with best-effort project grouping when supported by the installed CLI | Cost magnitude framing, billing-driven candidates | `null` + `usageError` set when queried and unavailable; `NOT_COLLECTED_*` when a preflight stop happened before billing collection |
| Stack | local `package.json` + dir scan | Version-aware citation filtering, scanner gating | "unknown" framework → all framework-specific citations filtered |
| `metrics.fnDurationP95ByRoute` | `vercel metrics vercel.function_invocation.function_duration_ms -a p95 --group-by route --since 14d` | `slow_route`, `platform_fluid_compute` gates | `{ok:false}`; gate emits no candidates |
| `metrics.requestsByRouteCache` | `vercel metrics vercel.request.count --group-by route --group-by cache_result --since 14d` | `uncached_route`, traffic-total computation | `{ok:false}` |
| `metrics.fnStatusByRoute` | `vercel metrics vercel.function_invocation.count --group-by route --group-by http_status --since 14d` | Canonical function-level 5xx source for `route_errors` and `slow_route` error disqualification | `{ok:false}`; fall back to `requestsByRouteStatus` only for older fixtures |
| `metrics.requestsByRouteStatus` | `vercel metrics vercel.request.count --group-by route --group-by http_status --since 14d` | Compatibility fallback for request-level status | `{ok:false}` |
| `metrics.externalApiP75` | `vercel metrics vercel.external_api_request.request_duration_ms -a p75 --group-by origin_hostname --since 14d` | `external_api_slow` gate | `{ok:false}` |
| `metrics.fnStartTypeByRoute` | `vercel metrics vercel.function_invocation.count -a sum --group-by route --group-by function_start_type --since 14d` | `cold_start`, `platform_fluid_compute` | `{ok:false}`; gate dormant. **`function_start_type` ∈ {cold,hot,prewarmed}** is the public way to read cold-start rate on CLI v53.4.0+ (replaces the old "not derivable" gap). |
| `metrics.fnGbHrByRoute` | `vercel metrics vercel.function_invocation.function_duration_gbhr -a sum --group-by route --since 14d` | Cost ranking / report breakdown | `{ok:false}` |
| `metrics.fnCpuMsByRoute` | `vercel metrics vercel.function_invocation.function_cpu_time_ms -a sum --group-by route --since 14d` | Active CPU ranking (Fluid Compute billing unit) | `{ok:false}` |
| `metrics.fnPeakMemoryByRoute` | `vercel metrics vercel.function_invocation.peak_memory_mb -a max --group-by route --since 14d` | `oversized_memory` gate | `{ok:false}` |
| `metrics.fnProvisionedMemoryByRoute` | `vercel metrics vercel.function_invocation.provisioned_memory_mb -a max --group-by route --since 14d` | `oversized_memory` gate | `{ok:false}` |
| `metrics.fnTtfbP95ByRoute` | `vercel metrics vercel.function_invocation.ttfb_ms -a p95 --group-by route --since 14d` | TTFB cross-check for slow routes | `{ok:false}` |
| `metrics.fdtByRoute` | `vercel metrics vercel.request.fdt_total_bytes -a sum --group-by route --since 14d` | Bandwidth-cost ranking | `{ok:false}` |
| `metrics.fdtByBot` | `vercel metrics vercel.request.fdt_total_bytes -a sum --group-by bot_category --since 14d` | Strengthens `platform_bot_protection` with observed bot bandwidth share | `{ok:false}`; gate falls back to config-only signal |
| `metrics.fdtByCache` | `vercel metrics vercel.request.fdt_total_bytes -a sum --group-by cache_result --since 14d` | Uncached-bandwidth narrative | `{ok:false}` |
| `metrics.middlewareCount` | `vercel metrics vercel.middleware_invocation.count -a sum --group-by request_path --since 14d` | `middleware_heavy` gate | `{ok:false}`; gate dormant |
| `metrics.middlewareDurationP95` | `vercel metrics vercel.middleware_invocation.duration_ms -a p95 --group-by request_path --since 14d` | Middleware latency narrative | `{ok:false}` |
| `metrics.isrReadsByRoute` | `vercel metrics vercel.isr_operation.read_units -a sum --group-by route --since 14d` | `isr_overrevalidation` gate (denominator) | `{ok:false}` |
| `metrics.isrWritesByRoute` | `vercel metrics vercel.isr_operation.write_units -a sum --group-by route --since 14d` | `isr_overrevalidation` gate (numerator) | `{ok:false}` |
**ISR read:write ratio caveat.** `isrReadsByRoute` exposes the **origin-tier** read count only. CDN-tier reads (regional cache hits that never reach the ISR origin) are not separately surfaced today and can dominate total read volume. Before flagging "writes > reads" as inverted, the gate and report must (a) acknowledge CDN-tier reads aren't included, (b) corroborate with `requestsByRouteCache` `cache_result=HIT` share before alarming. A high origin-write rate alone does not imply pathological over-revalidation if the CDN is absorbing the steady-state read traffic.
| `metrics.imageCount`, `imageByHost`, `imageSourceBytes` | `vercel metrics vercel.image_transformation.*` | Image-optimization narrative | `{ok:false}` |
| `metrics.cwvLcpByRoute`, `cwvInpByRoute`, `cwvClsByRoute`, `cwvTtfbByRoute`, `cwvCount`, `cwvCountByRoute` | `vercel metrics vercel.speed_insights_metric.*` (`p75` for vitals, `sum` for counts) `--since 14d` | `cwv_poor` gate | Empty when no Speed Insights measurements are returned for the 14-day window — gate stays dormant; do not infer disabled vs no traffic unless another signal proves it |
| `metrics.firewallByAction` | `vercel metrics vercel.firewall_action.count -a sum --group-by waf_action --since 14d` | Bot-protection narrative; shows existing managed rule activity | `{ok:false}` |
| `metrics.botIdChecks` | `vercel metrics vercel.bot_id_check.count -a sum --since 14d` | Confirms whether BotID is actively running | `{ok:false}` |
| `metrics.externalApiCount`, `externalApiBytes` | `vercel metrics vercel.external_api_request.*` grouped by `origin_hostname` | External-dependency cost narrative | `{ok:false}` |
## Error states and fallbacks
`lib/vercel.mjs`'s `runVercelJson()` parses stdout as JSON first (the most reliable signal — the CLI emits structured error payloads even when exit code is non-zero), and only falls back to stderr substring matching when JSON parsing fails:
| Code | Meaning | Skill behavior |
|---|---|---|
| `unsupported_framework` | Detected framework cannot reliably map Vercel route metrics back to source files | Stop before metric fan-out; ask whether to continue with a limited platform/scanner audit |
| `PROJECT_SCOPE_UNRESOLVED` | The project was found without an owner account, or `.vercel/repo.json` contains multiple linked projects and no explicit matching project ID was supplied | Stop before `vercel metrics`, `vercel usage`, or `vercel contract`; ask the user which Vercel project and team/personal scope to audit |
| `SCOPE_UNRESOLVED` | The linked project belongs to a specific team/user, but the collector could not resolve a CLI-safe `--scope` value | Stop before `vercel metrics`, `vercel usage`, or `vercel contract`; ask the user to switch/re-link with the correct team |
| `PROJECT_SCOPE_MISMATCH` | The resolved team/personal account cannot read the resolved project, or the project API returns a different owner/project | Stop before Observability Plus, metrics, usage, or contract checks; ask the user to confirm the exact Vercel project and team/personal scope |
| `no_oplus_probe` | Observability Plus not enabled on team | Stop before full metric fan-out; ask whether to enable Observability Plus or run scanner-only |
| `project_disabled` | Observability Plus enabled for team but disabled for project | Stop before full metric fan-out; ask the user to enable Observability Plus for this project or continue scanner-only |
| `daily_quota_exceeded` | Observability Plus query quota is exhausted for the day | Stop before full metric fan-out; tell the user to retry after the next UTC midnight reset or ask whether to continue scanner-only |
| `USAGE_UNAVAILABLE` | `vercel usage` returned no Costs payload after billing usage was actually queried | `usage=null`; cost-tier gates emit lower-priority candidates; billing section of the report shows the exact usage error |
| `PROJECT_NOT_FOUND` | `vercel api /v9/projects/<id>` 404 (typically wrong scope) | `project={error}`; platform gates that depend on project config skip; report flags the data gap |
| `invalid_filter_dimension` / `invalid_dimension` | Metric query used a dimension the metric doesn't support | Metric returns `{ok:false, code, allowedValues}`; consumer can introspect and adjust |
| `NOT_LINKED` | The app directory is not linked in the way `vercel metrics` requires | Run `vercel link --yes --project <project-name-or-id> --cwd <app-dir>`; add `--team <team-id-or-slug>` when known. Passing only `VERCEL_PROJECT_ID` is not enough for route metrics if cwd is unlinked |
| `NOT_AUTH` | Session expired | Caller exits with "run `vercel login`" |
| `FORBIDDEN` | 403 — role lacks permission | Skip that endpoint; continue with degraded signal; surface in report |
| `RATE_LIMIT` | 429 from API | Treat as "missing data" (no retry implemented yet) |
| `EXIT_N` | Anything else | Treat as missing data; continue |
The skill never crashes the entire collection on a single endpoint failure. Every catch-block uses `?? null` or `?? {}` so the JSON output is always well-shaped.
## Real JSON shapes
### `vercel metrics <id> --format json`
```jsonc
{
"query": {
"metric": "vercel.request.count",
"aggregation": "sum",
"groupBy": ["route"],
"startTime": "2026-04-13T04:00:00.000Z",
"endTime": "2026-05-13T08:00:00.000Z",
"granularity": { "hours": 4 }
},
"summary": [
{ "route": "/dashboard/[sessionId]", "vercel_request_count_sum": 4923 },
{ "route": "/sw.js", "vercel_request_count_sum": 872 }
],
"data": [
{ "timestamp": "2026-04-13T04:00:00.000Z", "vercel_request_count_sum": 0, "route": "/dashboard/[sessionId]" }
/* ... */
],
"statistics": { "bytesRead": 10267, "rowsRead": 947, "dbTimeSeconds": 0 }
}
```
Field naming rule: the metric ID's dots become underscores, and the aggregation suffix is appended — `vercel.request.count` + `sum` → `vercel_request_count_sum`. `lib/vercel.mjs::normalizeSummary()` flattens `summary[]` into `[{<dim>: v, ..., value: <n>}]`.
### `vercel metrics schema --format json`
Array of `{id, description}` entries — NOT an object. Many metric IDs in earlier docs don't exist: there is no `vercel.function.cold_starts`, no `vercel.cache.hits`. Cache state is the `cache_result` dimension on `vercel.request.count`.
### `vercel metrics <id> --filter "<bad>"`
```jsonc
{
"error": {
"code": "invalid_filter_dimension",
"message": "Filter uses invalid dimension \"status\" for metric \"vercel.request.count\".",
"allowedValues": [ "asn_id", ..., "http_status", ..., "route" ]
}
}
```
Status filtering uses `http_status` (not `status`). Both `http_status eq '500'` and `http_status ge 500` work.
### `vercel api /v9/projects/<id>` / `?teamId=<orgId>`
Top-level keys relevant to the skill (real, verified):
- `framework` (string, e.g. `"nextjs"`)
- `resourceConfig.fluid` (boolean) — **Fluid Compute toggle**
- `defaultResourceConfig.fluid` — template for new functions
- `security.botIdEnabled` (boolean) — **BotID toggle**
- `security.managedRules.bot_filter` (`{active, action}`) — firewall rule
- `speedInsights` (`{id, hasData}`)
- `webAnalytics` (`{id}`) — installed but `features.webAnalytics` says enabled state
- `nodeVersion` (e.g. `"22.x"`)
Calling without `?teamId=` returns 404 when the project belongs to a team other than the user's `currentTeam`. For user-owned projects (`orgId` starts with `usr_`), omit `teamId` and let the CLI use the authenticated user context.
### `vercel contract --format json`
```jsonc
{ "context": "example-team", "commitments": [], "totalCommitments": 0 }
```
The direct account billing record is the primary plan signal: `billing.plan` from `vercel api /v2/teams/:orgId` or `vercel api /v2/user` is expected to be `hobby`, `pro`, or `enterprise`.
`vercel contract` is only a fallback. `commitments[]` field names are not stable, so `inferPlan()` tries `category`, `commitmentCategory`, and `type`; category `Spend` means Pro and `Usage` means Enterprise. Empty commitments no longer imply Hobby by themselves.
### `vercel usage --format json`
May return `Error: Costs not found (404)`. Treat that queried error as `USAGE_UNAVAILABLE` and degrade — the skill can still produce a useful report from metrics + scanner. Do not use this explanation when `usageError` is `NOT_COLLECTED_OBSERVABILITY_BLOCKED` or another `NOT_COLLECTED_*` value; those mean the audit stopped before `vercel usage` ran.
## Why we avoid stderr grep
CLI error message strings are not stable contracts — they can change between versions. Detecting `OPLUS_REQUIRED` by greping `stderr.includes('Observability Plus')` will break the moment Vercel rewords the message.
`runVercelJson()` therefore:
1. Always tries to **parse stdout as JSON** first. Most failures emit a structured `{error:{code,message,allowedValues}}` payload that's deterministic.
2. Only falls back to a lower-case stderr substring match when stdout was not parseable JSON.
3. Categorizes anything unrecognized as `EXIT_N` and treats it as "missing data, continue."
The skill is correct without precise category detection. The categories exist to give the user better error messages, not to drive control flow.
references/docs-library.json
{
"$schema": "Curated documentation allow-list for the vercel-optimize skill. Entries are version-aware via applicableFrameworks (semver). The recommender prompt receives only the subset valid for the user's stack. Citations outside this allow-list are stripped by the unknown-citation sanitizer.",
"version": "1.1.1",
"lastVerified": "2026-05-26",
"schemaVersion": "1.0",
"applicableFrameworksSyntax": "Semver-style. '*' = any framework. 'next@*' = any Next.js. 'next@14' = Next 14.x. 'next@>=15.0.0' = Next 15+. Multiple via '||': 'next@14 || next@>=15.0.0'.",
"urls": [
{
"url": "https://vercel.com/docs/caching/cdn-cache",
"topic": "Vercel CDN Cache — cacheable response criteria, static file caching, dynamic response caching, and cache limits",
"appliesTo": ["uncached_route", "cache_header_gap"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/headers/request-headers",
"topic": "Vercel request headers — documented geolocation header names including country, country-region, city, latitude, longitude, timezone, and postal code",
"appliesTo": ["uncached_route", "cache_header_gap"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/caching/cache-control-headers",
"topic": "Cache-Control headers on Vercel — s-maxage, CDN-Cache-Control, Vercel-CDN-Cache-Control, stale-while-revalidate",
"appliesTo": ["uncached_route", "cache_header_gap"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/caching/runtime-cache",
"topic": "Runtime Cache — reuse repeated API, database, and expensive computation results from Vercel Functions",
"appliesTo": ["slow_route", "external_api_slow"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/fluid-compute",
"topic": "Fluid Compute — reduced cold starts, active CPU pricing, function instance reuse, memory right-sizing",
"appliesTo": ["cold_start", "platform_fluid_compute", "slow_route", "oversized_memory"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions/configuring-functions/memory",
"topic": "Function memory configuration — Standard 2GB / Performance 4GB tiers, when to upgrade Performance, account-level setting",
"appliesTo": ["platform_fluid_compute"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/speed-insights",
"topic": "Vercel Speed Insights — LCP, INP, CLS, TTFB, FCP, FID on real-user traffic",
"appliesTo": ["cwv_poor"],
"applicableFrameworks": ["*"]
},
{
"url": "https://web.dev/articles/vitals",
"topic": "Core Web Vitals — LCP < 2500ms, INP < 200ms, CLS < 0.1 thresholds",
"appliesTo": ["cwv_poor"],
"applicableFrameworks": ["*"]
},
{
"url": "https://web.dev/articles/optimize-lcp",
"topic": "Optimize LCP — preload critical images, defer non-critical CSS, reduce server response",
"appliesTo": ["cwv_poor"],
"applicableFrameworks": ["*"]
},
{
"url": "https://web.dev/articles/optimize-inp",
"topic": "Optimize INP — break up long tasks, defer non-essential JavaScript",
"appliesTo": ["cwv_poor"],
"applicableFrameworks": ["*"]
},
{
"url": "https://web.dev/articles/optimize-cls",
"topic": "Optimize CLS — reserve space for images, fonts, ads; avoid layout-affecting injections",
"appliesTo": ["cwv_poor"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/vercel-firewall/vercel-waf/managed-rulesets",
"topic": "Vercel WAF managed rulesets — managed bot and AI-bot actions by ruleset",
"appliesTo": ["platform_bot_protection", "uncached_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/vercel-firewall/vercel-waf/custom-rules",
"topic": "Vercel WAF custom rules — log, deny, challenge, bypass, redirect, and persistent actions",
"appliesTo": ["platform_bot_protection", "uncached_route", "middleware_heavy"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions",
"topic": "Vercel Functions lifecycle — regions, memory, max duration, runtimes",
"appliesTo": ["slow_route", "cold_start", "route_errors"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions/debug-slow-functions",
"topic": "Debugging slow functions — isolate external latency, cold starts, bundle size, initialization, memory, and connection pooling",
"appliesTo": ["slow_route", "cold_start", "external_api_slow"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package",
"topic": "@vercel/functions helpers — waitUntil for post-response work and attachDatabasePool for pool lifecycle",
"appliesTo": ["slow_route", "cold_start", "external_api_slow", "route_errors"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions/limitations",
"topic": "Vercel Functions limits — duration, payload, runtime, and invocation constraints",
"appliesTo": ["cold_start", "route_errors", "slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions/runtimes",
"topic": "Runtime selection — node, python, ruby, go (edge runtime deprecated per 2026 platform notes)",
"appliesTo": ["cold_start", "slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/incremental-static-regeneration",
"topic": "Incremental Static Regeneration — reads vs writes, revalidate timing, on-demand revalidation",
"appliesTo": ["isr_overrevalidation", "uncached_route"],
"applicableFrameworks": ["next@*", "sveltekit@*", "astro@*", "nuxt@*"]
},
{
"url": "https://vercel.com/docs/image-optimization",
"topic": "Image Optimization on Vercel — supported formats, transformations, billing",
"appliesTo": ["image_optimization"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/image-optimization/managing-image-optimization-costs",
"topic": "Managing Image Optimization usage and costs — reduce unnecessary transformations and cache misses",
"appliesTo": ["image_optimization"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/image-optimization/limits-and-pricing",
"topic": "Image Optimization limits and pricing — source format, dimensions, transformed image size, and billing dimensions",
"appliesTo": ["image_optimization"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/observability/observability-plus",
"topic": "Observability Plus — path-level metrics, p75 sort, and retention",
"appliesTo": ["platform_fluid_compute", "observability_events_attribution"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/monorepos",
"topic": "Monorepos — skip unaffected projects and ignored build step tradeoffs",
"appliesTo": ["build_minutes_fanout"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/builds",
"topic": "Builds — build infrastructure, monorepo build controls, concurrency, and queues",
"appliesTo": ["build_minutes_fanout"],
"applicableFrameworks": ["*"]
},
{
"url": "https://turborepo.dev/docs/crafting-your-repository/caching",
"topic": "Turborepo caching — task-level cache config, force flags, remote cache",
"appliesTo": ["build_minutes_fanout"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/regions",
"topic": "Vercel Functions regions — single vs multi-region, defaults, latency tradeoffs",
"appliesTo": ["region_misconfig"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions/configuring-functions/region",
"topic": "Configuring function region — vercel.json regions, segment-level preferredRegion",
"appliesTo": ["region_misconfig"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/alerts",
"topic": "Alerts — per-metric alerting on routes/status/usage with supported destinations",
"appliesTo": ["observability_events_attribution", "usage_spike_triage"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/spend-management",
"topic": "Spend Management — usage thresholds, notifications, and project pause behavior",
"appliesTo": ["usage_spike_triage"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/cli/metrics",
"topic": "vercel metrics CLI — schema, query construction, OData filters",
"appliesTo": [],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/cli/usage",
"topic": "vercel usage CLI — billing line items, breakdown, effectiveCost vs billedCost",
"appliesTo": [],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/cli/inspect",
"topic": "vercel inspect — view a deployment's build logs, region, runtime, commit, creator",
"appliesTo": ["route_errors"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/manage-cdn-usage",
"topic": "Vercel Edge Network bandwidth — Fast Data Transfer, included quotas, overage pricing",
"appliesTo": ["large_static_asset", "uncached_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions/runtimes/edge-runtime",
"topic": "Vercel Edge Runtime — supported APIs, constraints, incompatible modules (node:* builtins, native deps)",
"appliesTo": ["edge_heavy_import", "cold_start"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/bot-management",
"topic": "Vercel Bot Protection — enabling, false-positive handling, billing implications",
"appliesTo": ["platform_bot_protection"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/botid",
"topic": "BotID — invisible challenge, Basic and Deep Analysis modes, checkBotId, observability, and billing",
"appliesTo": ["platform_bot_protection"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/functions/limitations",
"topic": "Platform limits — function payload size, max duration, regional caps",
"appliesTo": ["route_errors", "slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/project-configuration",
"topic": "vercel.json configuration — functions, crons, rewrites, redirects, headers, regions",
"appliesTo": ["uncached_route", "cache_header_gap"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/routing/",
"topic": "Vercel routing — firewall, bulk redirects, project routes, deployment routes, middleware, and filesystem order",
"appliesTo": ["uncached_route", "middleware_heavy", "cache_header_gap"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/redirects/bulk-redirects/",
"topic": "Bulk Redirects — framework-agnostic large redirect tables processed before project routes",
"appliesTo": ["uncached_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/routing-middleware",
"topic": "Vercel Routing Middleware — intercept requests before rendering and keep matcher scope intentional",
"appliesTo": ["middleware_heavy"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/workflow",
"topic": "Vercel Workflow — beta durable workflows, observability, Workflow Steps/Storage pricing, and compute billing caveats",
"appliesTo": ["route_errors", "slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://workflow-sdk.dev/docs/foundations/starting-workflows",
"topic": "Workflow SDK starting workflows — start() returns after enqueueing, returnValue waits for completion, streams/status can be read later",
"appliesTo": ["route_errors", "slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://workflow-sdk.dev/docs/foundations/workflows-and-steps",
"topic": "Workflow SDK workflows and steps — workflow orchestration, step execution, persisted results, retries, sleep, and suspension",
"appliesTo": ["route_errors", "slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://workflow-sdk.dev/docs/foundations/streaming",
"topic": "Workflow SDK streaming — durable streams, getReadable startIndex, getWritable, lock release, stream close, and retry caveats",
"appliesTo": ["slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://workflow-sdk.dev/docs/ai/resumable-streams",
"topic": "Workflow SDK resumable streams — reconnecting AI chat streams with run IDs, stream startIndex, and tail-index headers",
"appliesTo": ["slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/queues",
"topic": "Vercel Queues — asynchronous task queues for background work decoupled from the request path",
"appliesTo": ["route_errors"],
"applicableFrameworks": ["*"]
},
{
"url": "https://vercel.com/docs/concepts/edge-network/regions",
"topic": "Vercel function regions — single-region default, multi-region, db proximity",
"appliesTo": ["slow_route"],
"applicableFrameworks": ["*"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config",
"topic": "App Router route segment config — dynamic, revalidate, runtime, fetchCache, dynamicParams",
"appliesTo": ["uncached_route", "rendering_candidate", "force-dynamic"],
"applicableFrameworks": ["next@>=13.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/file-conventions/route",
"topic": "Route Handlers — GET handler caching defaults, route segment config, and request handling",
"appliesTo": ["uncached_route", "cache_header_gap"],
"applicableFrameworks": ["next@>=13.2.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/image-response",
"topic": "ImageResponse — dynamic OG image response options including status and headers",
"appliesTo": ["uncached_route", "cache_header_gap"],
"applicableFrameworks": ["next@>=13.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/generate-static-params",
"topic": "generateStaticParams — pre-render dynamic routes at build time",
"appliesTo": ["uncached_route", "rendering_candidate"],
"applicableFrameworks": ["next@>=13.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/fetch",
"topic": "Next.js fetch — server-side cache and next.revalidate semantics for App Router data requests",
"appliesTo": ["uncached_route", "isr_overrevalidation", "cache_header_gap"],
"applicableFrameworks": ["next@>=13.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/unstable_cache",
"topic": "unstable_cache — Next.js 14/15 cache primitive (replaced in 16 by 'use cache')",
"appliesTo": ["uncached_route", "function-duration"],
"applicableFrameworks": ["next@14 || next@15"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/directives/use-cache",
"topic": "'use cache' directive — opt-in persistent cache (15+)",
"appliesTo": ["uncached_route", "function-duration"],
"applicableFrameworks": ["next@>=15.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/directives/use-cache-remote",
"topic": "'use cache: remote' directive — remote cache storage for shared cached data with Cache Components",
"appliesTo": ["external_api_slow"],
"applicableFrameworks": ["next@>=16.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/cacheLife",
"topic": "cacheLife() — TTL annotation for 'use cache' segments",
"appliesTo": ["uncached_route", "isr_overrevalidation"],
"applicableFrameworks": ["next@>=15.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/cacheTag",
"topic": "cacheTag() — tag-based invalidation for 'use cache'",
"appliesTo": ["uncached_route", "isr_overrevalidation"],
"applicableFrameworks": ["next@>=15.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/revalidateTag",
"topic": "revalidateTag — on-demand invalidation by tag",
"appliesTo": ["isr_overrevalidation"],
"applicableFrameworks": ["next@>=13.4.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/revalidatePath",
"topic": "revalidatePath — on-demand invalidation by route",
"appliesTo": ["isr_overrevalidation"],
"applicableFrameworks": ["next@>=13.4.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/functions/after",
"topic": "after() — non-blocking work post-response (15+)",
"appliesTo": ["function-duration", "slow_route"],
"applicableFrameworks": ["next@>=15.0.0"]
},
{
"url": "https://react.dev/reference/react/cache",
"topic": "React cache() — per-request memoization in Server Components",
"appliesTo": ["function-duration", "slow_route"],
"applicableFrameworks": ["next@>=13.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/components/image",
"topic": "next/image component — formats, sizes, priority, lazy loading",
"appliesTo": ["image_optimization", "cwv_poor"],
"applicableFrameworks": ["next@*"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/components/font",
"topic": "next/font — self-hosted fonts, no CLS, build-time optimization",
"appliesTo": ["bundle_candidate", "cwv_poor"],
"applicableFrameworks": ["next@>=13.2.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/components/script",
"topic": "next/script component — loading strategies for third-party scripts and App Router worker caveats",
"appliesTo": ["cwv_poor"],
"applicableFrameworks": ["next@*"]
},
{
"url": "https://nextjs.org/docs/app/guides/lazy-loading",
"topic": "next/dynamic — lazy-load heavy client components, SSR opt-out",
"appliesTo": ["bundle_candidate", "slow_route", "cwv_poor"],
"applicableFrameworks": ["next@*"]
},
{
"url": "https://nextjs.org/docs/app/building-your-application/caching",
"topic": "Next.js App Router caching model — full-route cache, data cache, request memoization",
"appliesTo": ["uncached_route", "isr_overrevalidation", "function-duration"],
"applicableFrameworks": ["next@>=13.0.0"]
},
{
"url": "https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents",
"topic": "cacheComponents flag — explicit caching model for App Router prerendering and data access",
"appliesTo": ["rendering_candidate", "uncached_route"],
"applicableFrameworks": ["next@>=16.0.0"]
},
{
"url": "https://nextjs.org/docs/app/getting-started/caching",
"topic": "Cache Components caching guide — static shell, cached dynamic content, runtime dynamic content",
"appliesTo": ["rendering_candidate", "uncached_route"],
"applicableFrameworks": ["next@>=16.0.0"]
},
{
"url": "https://nextjs.org/docs/app/guides/migrating-to-cache-components",
"topic": "Migrating to Cache Components — route segment config replacements and static shell migration",
"appliesTo": ["rendering_candidate", "uncached_route"],
"applicableFrameworks": ["next@>=16.0.0"]
},
{
"url": "https://nextjs.org/docs/app/building-your-application/routing/middleware",
"topic": "Next.js middleware — matcher config, runtime, request modification",
"appliesTo": ["middleware-broad-matcher", "platform_bot_protection", "middleware_heavy"],
"applicableFrameworks": ["next@>=12.0.0"]
},
{
"url": "https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering",
"topic": "Partial Prerendering (PPR) — combine static shell + dynamic streaming (15+ experimental, stable later)",
"appliesTo": ["rendering_candidate", "slow_route"],
"applicableFrameworks": ["next@>=15.0.0"]
},
{
"url": "https://nextjs.org/docs/app/building-your-application/optimizing/bundle-analyzer",
"topic": "@next/bundle-analyzer — diagnose oversized client bundles",
"appliesTo": ["bundle_candidate"],
"applicableFrameworks": ["next@*"]
},
{
"url": "https://vercel.com/docs/frameworks/full-stack/sveltekit",
"topic": "SvelteKit on Vercel — adapter config, ISR, split function bundling, regions, and runtime settings",
"appliesTo": ["uncached_route", "isr_overrevalidation", "cold_start", "slow_route"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://svelte.dev/docs/kit/adapter-vercel",
"topic": "SvelteKit adapter-vercel — ISR, split, regions, max duration, and image config",
"appliesTo": ["uncached_route", "isr_overrevalidation", "cold_start", "slow_route"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://svelte.dev/docs/kit/page-options",
"topic": "SvelteKit page options — prerender, SSR, CSR, and server route prerendering",
"appliesTo": ["uncached_route", "isr_overrevalidation", "rendering_candidate"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/adapter-vercel",
"topic": "SvelteKit Vercel adapter — runtime, isr, split, regions, maxDuration config",
"appliesTo": ["slow_route", "cold_start", "uncached_route"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/page-options",
"topic": "SvelteKit page options — prerender, ssr, csr",
"appliesTo": ["rendering_candidate", "uncached_route"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/load",
"topic": "SvelteKit load functions — server-side data loading patterns, parallel loads",
"appliesTo": ["slow_route", "function-duration"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/routing",
"topic": "SvelteKit routing — +page.svelte, +page.server.ts, +server.ts route handlers, +layout.svelte",
"appliesTo": ["slow_route", "uncached_route", "rendering_candidate"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/hooks",
"topic": "SvelteKit hooks — handle (request middleware), handleFetch (per-fetch interception), handleError",
"appliesTo": ["middleware_heavy", "uncached_route", "route_errors"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/form-actions",
"topic": "SvelteKit form actions — server-side form handling without separate API endpoints",
"appliesTo": ["slow_route", "uncached_route"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/state-management",
"topic": "SvelteKit state management — request-scoped state, locals, avoiding cross-request leakage",
"appliesTo": ["slow_route", "route_errors"],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/cli",
"topic": "SvelteKit CLI — build, preview, sync. Use `vite build` for production output that adapter-vercel consumes.",
"appliesTo": [],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://kit.svelte.dev/docs/migrating",
"topic": "SvelteKit migration guides — for users on older majors who may not have ISR / per-route prerender",
"appliesTo": [],
"applicableFrameworks": ["sveltekit@*"]
},
{
"url": "https://docs.astro.build/en/guides/integrations-guide/vercel/",
"topic": "Astro Vercel adapter — ISR, image services, edge middleware, runtime config",
"appliesTo": ["slow_route", "cold_start", "uncached_route", "isr_overrevalidation", "rendering_candidate", "middleware_heavy"],
"applicableFrameworks": ["astro@*"]
},
{
"url": "https://vercel.com/docs/frameworks/frontend/astro",
"topic": "Astro on Vercel — output modes, ISR, edge middleware, and image optimization",
"appliesTo": ["uncached_route", "isr_overrevalidation", "rendering_candidate", "middleware_heavy", "image_optimization"],
"applicableFrameworks": ["astro@*"]
},
{
"url": "https://docs.astro.build/en/guides/on-demand-rendering/",
"topic": "Astro on-demand rendering — output server mode and per-route prerender controls",
"appliesTo": ["uncached_route", "isr_overrevalidation", "rendering_candidate"],
"applicableFrameworks": ["astro@*"]
},
{
"url": "https://docs.astro.build/en/reference/configuration-reference/",
"topic": "Astro configuration — output mode (static/server/hybrid), prerender, integrations",
"appliesTo": ["rendering_candidate", "uncached_route"],
"applicableFrameworks": ["astro@*"]
},
{
"url": "https://docs.astro.build/en/guides/server-side-rendering/",
"topic": "Astro SSR — hybrid mode, prerender export, dynamic routes",
"appliesTo": ["slow_route", "uncached_route", "rendering_candidate"],
"applicableFrameworks": ["astro@*"]
},
{
"url": "https://nuxt.com/docs/getting-started/deployment#vercel",
"topic": "Nuxt on Vercel — deployment, runtime configuration, ISR support",
"appliesTo": ["slow_route", "cold_start", "uncached_route"],
"applicableFrameworks": ["nuxt@*"]
},
{
"url": "https://vercel.com/docs/frameworks/full-stack/nuxt",
"topic": "Nuxt on Vercel — routeRules ISR, Vercel cache integration, deployment, and runtime configuration",
"appliesTo": ["uncached_route", "isr_overrevalidation", "slow_route", "cold_start"],
"applicableFrameworks": ["nuxt@>=3.0.0"]
},
{
"url": "https://nuxt.com/docs/api/composables/use-fetch",
"topic": "Nuxt useFetch / useAsyncData — automatic deduplication, parallel data loads, cache options",
"appliesTo": ["slow_route", "function-duration"],
"applicableFrameworks": ["nuxt@*"]
},
{
"url": "https://nuxt.com/docs/4.x/api/utils/define-route-rules",
"topic": "Nuxt defineRouteRules — per-route prerender, ISR, SWR, headers, and rendering controls",
"appliesTo": ["uncached_route", "isr_overrevalidation", "rendering_candidate"],
"applicableFrameworks": ["nuxt@>=3.0.0"]
},
{
"url": "https://nuxt.com/docs/4.x/guide/concepts/rendering",
"topic": "Nuxt rendering modes — universal rendering, routeRules, prerendering, and hybrid rendering",
"appliesTo": ["uncached_route", "isr_overrevalidation", "rendering_candidate"],
"applicableFrameworks": ["nuxt@>=3.0.0"]
},
{
"url": "https://nuxt.com/docs/api/utils/define-route-rules",
"topic": "Nuxt route rules — per-route SSR/SSG/ISR/swr/cache config in nuxt.config.ts",
"appliesTo": ["uncached_route", "isr_overrevalidation", "rendering_candidate"],
"applicableFrameworks": ["nuxt@*"]
},
{
"url": "https://docs.astro.build/en/guides/integrations-guide/vercel/",
"topic": "Astro Vercel adapter — SSR, ISR, image optimization on Vercel",
"appliesTo": ["slow_route", "image_optimization"],
"applicableFrameworks": ["astro@*"]
},
{
"url": "https://nuxt.com/docs/getting-started/deployment#vercel",
"topic": "Nuxt on Vercel — Nitro preset, ISR, caching headers",
"appliesTo": ["uncached_route", "rendering_candidate"],
"applicableFrameworks": ["nuxt@*"]
}
],
"ruleSkillRefs": [
{
"skill": "vercel-react-best-practices",
"rule": "async-parallel",
"topic": "Promise.all for independent awaits",
"applicableFrameworks": ["next@*", "react@*"]
},
{
"skill": "vercel-react-best-practices",
"rule": "async-suspense-boundaries",
"topic": "Suspense for streaming content",
"applicableFrameworks": ["next@>=13.0.0", "react@>=18.0.0"]
},
{
"skill": "vercel-react-best-practices",
"rule": "bundle-barrel-imports",
"topic": "Avoid barrel files, prefer direct imports",
"applicableFrameworks": ["next@*", "react@*"]
},
{
"skill": "vercel-react-best-practices",
"rule": "bundle-dynamic-imports",
"topic": "next/dynamic for heavy components",
"applicableFrameworks": ["next@*"]
},
{
"skill": "vercel-react-best-practices",
"rule": "bundle-defer-third-party",
"topic": "Defer analytics/logging scripts post-hydration",
"applicableFrameworks": ["next@*", "react@*"]
},
{
"skill": "vercel-react-best-practices",
"rule": "server-cache-react",
"topic": "React.cache() for request deduplication",
"applicableFrameworks": ["next@>=13.0.0"]
},
{
"skill": "vercel-react-best-practices",
"rule": "server-cache-lru",
"topic": "LRU for cross-request caching",
"applicableFrameworks": ["next@*"]
},
{
"skill": "vercel-react-best-practices",
"rule": "server-after-nonblocking",
"topic": "after() for non-blocking work post-response",
"applicableFrameworks": ["next@>=15.0.0"]
},
{
"skill": "vercel-react-best-practices",
"rule": "server-parallel-fetching",
"topic": "Restructure server components for parallel fetches",
"applicableFrameworks": ["next@>=13.0.0"]
},
{
"skill": "vercel-react-best-practices",
"rule": "server-hoist-static-io",
"topic": "Hoist static I/O (fonts, logos) to module level",
"applicableFrameworks": ["next@*"]
},
{
"skill": "vercel-react-best-practices",
"rule": "client-swr-dedup",
"topic": "SWR for client-side request deduplication",
"applicableFrameworks": ["next@*", "react@*"]
},
{
"skill": "vercel-react-best-practices",
"rule": "rendering-content-visibility",
"topic": "content-visibility for long lists",
"applicableFrameworks": ["*"]
},
{
"skill": "vercel-react-best-practices",
"rule": "rendering-resource-hints",
"topic": "React DOM resource hints for preloading",
"applicableFrameworks": ["next@*", "react@*"]
}
]
}
references/doctrine.md
# Doctrine
The four non-negotiable rules that shape every action this skill takes. If a future change conflicts with one of these, the change is wrong.
## Rule 1: Observability before investigation
The skill never reads a source file without an observability signal pointing at it. Step 1 (`node scripts/collect-signals.mjs`) is always first. Nothing reads source code until `signals.json` exists.
**Why this fails when skipped:** without metrics, the skill defaults to "grep the repo for known anti-patterns and complain." That produces noisy, low-impact recs that aren't tied to traffic, cost, or user pain. Metrics-first investigation keeps the skill focused on observed traffic, cost, and reliability signals.
### Four-check first-pass (Enterprise)
When `plan === 'enterprise'`, the gate run must surface these four checks before code-level recommendations. Field engineers confirm these are the highest-leverage account-level levers across every renewal audit:
1. **Observability Plus enabled?** From `signals.observabilityPlus`. If false, the whole audit degrades; surface as a top-of-report item.
2. **Reverse proxy in front?** Heuristic from response headers / CNAME chain (when collected). A non-Vercel CDN over Vercel ISR is usually a "dumb pipe" — wasted spend.
3. **WAF rules enabled?** From `signals.project.security`. BotID + managed rules absent on a project with bot evidence is the most common cost spike.
4. **ISR read:write ratio.** From `metrics.isrReadsByRoute` + `metrics.isrWritesByRoute`. Include CDN-tier reads (see [data-collection.md](data-collection.md)) before flagging "writes > reads."
These checks anchor the Enterprise-tier report's opening narrative; code-level recs follow.
## Rule 2: Deterministic gate before every sub-agent investigation
`node scripts/gate-investigations.mjs` is a pure-JS, LLM-free function. It reads `signals.json` and outputs `{toLaunch, platform, gated}`. Same input always produces byte-identical output (modulo `appliedAt`).
Every kind of candidate (uncached route, slow route, errors, cold starts, scanner findings, platform-level recs) has its threshold expression encoded as a `gate(signals) → Candidate[]` function in `lib/gates/<kind>.mjs`.
**Failed gates surface in the final report**, under "Not investigated in this run," with the exact reason they were held back. This is the user-facing trust mechanism: you see what we considered and chose to skip, and the reason.
**Why this matters:** the agent never decides "should I look at this route?" via LLM judgment. The threshold is mechanical. This eliminates the entire failure mode where the agent investigates routes it shouldn't (cold-path) and recommends fixes for routes that don't need them.
## Rule 3: Candidate-bound investigation scope
When the gate emits a candidate with `files: ['src/app/api/products/route.ts']`, the agent reads ONLY that file (and its imports as the chain unfolds). It does NOT `grep -r` across the repo.
If you find yourself wanting to grep the whole codebase, stop and re-read the current candidate's `question` field. If the question doesn't constrain the search, the candidate is malformed — log it as `gated` and skip. Do NOT compensate with a wider search.
**Why this matters:** the agent's job is to verify and explain the metric anomaly the gate found, not to do a general code review. Wandering investigations produce drift, hallucination, and recommendations untied to the cost and performance data.
### Scanner findings (the supplementary signal)
Static AST-grep scanners run in parallel with the metric-driven investigations. Their output is annotated with the per-file observability signal (`function invocations: 1.2M; 95th percentile duration: 850ms; cache hit rate: 0%` if the file maps to a hot route, `COLD-PATH` if it maps to a route with no traffic, `NO-ROUTE-MAPPING` if the file doesn't map to any route).
**Default rule:** scanner findings on `COLD-PATH` or `NO-ROUTE-MAPPING` files are dropped. They become recs only if the pattern is *traffic-independent*: build configuration, middleware matcher, source maps in production, raw script tags, React Compiler config. These don't care about traffic — they affect every request equally or affect the build itself.
The traffic-independent allow-list lives in each scanner's `metadata.trafficIndependent: boolean` field. Set it to `true` only when you can defend the claim.
## Rule 4: Doc-grounded, version-aware recommendations — no hallucinations
Every recommendation must carry at least one citation from `references/docs-library.json`. Anything else is dropped at sanitizer time.
The library has two parts:
- **URLs** — Vercel docs, Next.js docs, SvelteKit docs, etc. Each declares `applicableFrameworks` (e.g., `["next@>=15.0.0"]`).
- **Cross-skill rule references** — by name only (`vercel-react-best-practices:async-parallel`). The agent's host resolves these.
Three sanitizers enforce this:
- `missing-citation` — drops recs with empty `citations[]`.
- `unknown-citation` — strips URLs not in the library, marks `needsReview=true`.
- `version-mismatch` — strips URLs whose `applicableFrameworks` doesn't match the project's framework@version (parsed from `package.json`).
Two verifier claim types check it: `citation_in_library` (URL ∈ allow-list) and `citation_applies_to_version` (semver match).
**Why this matters:** LLMs cite plausible-looking URLs that 404, or recommend Next 15 features to Next 13 users. Both are trust-killers. The allow-list closes the first failure mode; the `applicableFrameworks` field closes the second.
### Performance citations cite observed data
Every performance claim cites the actual observability datum from `signals.json` — e.g., `functionRoutes[/api/products].p95Ms=850`. Estimated improvements are framed as ranges grounded in the observed baseline: `"Reduce /api/products 95th percentile duration from 850ms toward ~250-400ms based on similar cached routes."` Never an unanchored claim.
### Cost framing is magnitude, never precise
Cost claims like `$340/mo` are forbidden. The dollar noise floor on projections is too high to justify precision. The `impactMagnitude({currentCost, impactTier})` helper maps to phrases like `"hundreds of dollars per month at current traffic"` (computed against the user's actual `vercel usage` data).
The `$-strip` sanitizer enforces this at output time — any `$N` literal in customer-facing fields is stripped.
Performance numbers stay precise because they're observed, not extrapolated. We trust observed metrics; we don't trust dollar projections.
## What good looks like
A good run produces:
- A small number (5-15) of recommendations.
- Every rec ties to a specific route or file plus a specific metric signal.
- Every rec carries before/after code and ≥1 citation matching the user's framework version.
- Cost framing uses magnitude phrases. Performance framing uses precise observed numbers.
- The "Not investigated in this run" section explains every other signal we saw and why we chose not to dig (cache hit rate was below threshold, 95th percentile duration was already healthy, etc.).
- No `$N/mo` strings, no fabricated URLs, no Next.js 15 features recommended to a Next.js 13 user.
## What bad looks like (anti-patterns we will not ship)
- Recommendations from grepping the repo for known anti-patterns, without checking traffic.
- "Enable Fluid Compute" without a cold-start signal.
- "Add caching to /api/users" when the route has cookies() and is auth-gated.
- "Reduce the duration of `/.well-known/workflow/v1/step`" because a Workflow step is long-running. Workflow runtime endpoints are generated orchestration routes; high wall-clock duration there is expected unless a separate reliability/error signal points elsewhere.
- "Fix `/api/chat/[id]/stream` because it has high duration" without proving the stream does avoidable pre-first-byte work, high active CPU, duplicate invocations, or movable post-response work.
- "Save $340/mo by doing X" — invented precision.
- Citations to URLs that don't exist or that describe Next.js features the user's version doesn't have.
- Long lists of recs the user can't act on; every rec needs an evidence chain.
## Out of scope
The skill is bounded to runtime cost and performance optimization on Vercel-hosted projects. The following are explicit non-goals; if signals or scanner findings surface in these areas, route them out:
- **Deployment artifact size** in isolation. Bundle size matters only when it shows up as runtime cost (cold start, FDT) or performance (LCP, INP). If the only effect is "the .next directory is large," it's not in scope.
- **Build-time issues without runtime impact.** Slow builds, build-cache misses, monorepo build fan-out — these only enter scope when they show up as Build Minutes billing pressure (then they go through the `build-minutes-fanout` gate). A 6-minute build that completes successfully and ships a small artifact is not a target.
- **Security advisories and credential rotation.** RCE in `next-mdx-remote`, leaked env vars, OIDC vs explicit-key auth hygiene — refer to a security skill, not this one. Exception: when a security setting is also a documented cost lever (BotID = bot traffic = edge cost), it enters via the `platform_bot_protection` gate.
- **Commercial / billing-process trivia.** Discount sliders, seat reconciliation, contract renewal mechanics. The skill can quantify which SKU is expensive; it does not negotiate.
references/observability-plus.md
# Observability Plus Stop-And-Ask
Use this file only when `signals.observabilityPlusBlocker` is set. Do not silently continue into scanner-only mode unless the user chooses that path.
## Why This Check Exists
This is a data dependency, not an upgrade pitch. The skill ranks work by observed route behavior so it can separate hot, expensive paths from code that only looks suspicious. These gates need per-route metrics:
| Gate | Required signal |
|---|---|
| `slow_route` | Function duration and invocation count by route |
| `uncached_route` | Cache result and request count by route |
| `cold_start` | Function start type by route |
| `route_errors` | Function status by route |
| `isr_overrevalidation` | ISR reads and writes by route |
| `middleware_heavy` | Middleware invocations and duration |
| `cwv_poor` | Core Web Vitals by route |
| `platform_bot_protection` | Fast Data Transfer by bot category |
Scanner-only mode can still catch traffic-independent code issues, but it cannot rank the hottest routes or prove cost impact. Make that tradeoff explicit before continuing.
## User Template
Render this template first, then wait for the user's choice. Replace only `<detail>`. Do not add a preface; the heading is the opening line.
```md
**Per-route metrics are unavailable.**
<detail>
This audit needs route-level metrics to rank fixes by observed latency, cache hit rate, error rate, cold-start rate, and Incremental Static Regeneration reads and writes. Without them, I can run a scanner-only audit for traffic-independent code issues, but I cannot tell which routes matter most or prove cost impact.
Docs: https://vercel.com/docs/observability/observability-plus
Choose one:
1. Enable Observability Plus, then re-run the metric-backed audit.
2. Continue in scanner-only mode for a limited audit.
```
If the host supports a structured question tool, use this exact customer-facing copy. Do not rewrite it.
```json
{
"question": "Enable Observability Plus and re-run, or continue with a limited scanner-only audit?",
"header": "Observability Plus",
"options": [
{
"label": "Enable and re-run",
"description": "Use route-level metrics to rank the routes that matter most for cost and performance."
},
{
"label": "Run scanner-only",
"description": "Check traffic-independent code patterns without route ranking or proven cost impact."
}
]
}
```
Use the full product name in this question. Do not abbreviate product names or metrics in customer-facing blocker copy.
## After The User Chooses
If the user chooses **Enable and re-run**, stop after this short response:
```md
Enable Observability Plus from the Vercel dashboard's Observability tab, then tell me to rerun. I'll restart the metric-backed audit once route-level metrics are available.
```
Do not include raw team IDs, org IDs, project IDs, pricing language, dashboard screenshots, or extra persuasion. The docs link in the blocker message already covers availability and billing details.
If the user chooses **Run scanner-only**, continue with the scanner-only steps below.
## Blocker Copy
| Blocker | Detail |
|---|---|
| `payment_required` | `Detected: route-level metrics were recognized for this team, but these metric queries are not usable.` |
| `no_oplus_probe` | `Detected: this team does not expose the route-level metrics this audit needs.` |
| `not_linked` | `Detected: this app directory is not linked to a Vercel project.` |
| `forbidden` | `Detected: the Vercel CLI is authenticated to a team that cannot read this project.` |
| `project_not_found` | `Detected: the project ID is not visible to the authenticated team.` |
| `project_disabled` | `Detected: route-level metrics are enabled for the team but disabled for this project.` |
| `all_failed_other` | `Detected: every per-route metric query failed. Error code: <code>.` |
For `not_linked`, do not use the Observability Plus template. Link the app directory first:
```bash
vercel link --yes --project <project-name-or-id> --cwd <app-dir>
```
Add `--team <team-id-or-slug>` when the team is known. If the user supplied both app path and project name, run the link command instead of asking them what to do.
For `forbidden` and `project_not_found`, ask the user to confirm the exact Vercel project and team/personal scope before presenting the Observability Plus choice.
For `project_disabled`, do not present it as a team subscription problem. Ask the user to enable Observability Plus for this project, then re-run.
For `no_traffic`, do not use this template. Tell the user the project has no meaningful traffic in the 14-day window, then ask whether to run scanner-only mode now or come back after traffic accumulates.
## Scanner-Only Mode
If the user picks scanner-only mode:
1. Re-run `node scripts/collect-signals.mjs [projectId] --continue-without-observability > "$RUN_DIR/vercel-signals.json" 2> "$RUN_DIR/collect.stderr"` if the current `signals.json` stopped at the fast blocker (`usageError=NOT_COLLECTED_OBSERVABILITY_BLOCKED` or `project=null`).
2. Run code scanners.
3. Launch only traffic-independent findings.
4. Render a clear data gap: per-route metric gates were skipped because Observability Plus data was unavailable.
Do not imply the scanner-only report is a complete optimization audit.
references/playbooks/ai-application.md
# AI application
LLM-backed apps, agents, code-sandbox tools, RAG pipelines. Cost shape is dominated by per-token AI Gateway spend and Sandbox active-compute time, not edge requests or function duration. Many AI customers also have a SaaS surface (auth, dashboards), but the cost lever lives upstream of the dashboard.
## Typical billing shape
AI Gateway > Sandbox Active Compute > Function Duration > Function Invocations. Edge Requests usually quiet; ISR rarely applies. Observability Events can climb fast if every tool-call span is captured at full fidelity.
## Priority patterns
1. **Provider failover.** Configure AI Gateway with an active-active fallback chain across providers (OpenAI + Anthropic, or model-family pairs). Critical-path agents must not be single-provider — a 429 from one provider becomes a user-visible outage otherwise. Field example: MELI runs homegrown active-active routing because retry-on-error against a single provider degraded their NLP-on-support flow.
2. **OIDC keyless auth, not explicit API keys.** In production, use the AI Gateway OIDC binding so requests are signed by deployment identity. In local dev, `vercel env run -- <cmd>` rotates OIDC each run. An explicit `AI_GATEWAY_API_KEY` in repo env vars is a regression — it bypasses keyless and creates a long-lived secret.
3. **Sandbox reuse over per-request `Sandbox.create`.** Each fresh sandbox costs at least 1 minute of billed compute (boot + teardown rounded up). When isolation isn't required (single-tenant agents, shared workspaces), pool sandboxes by name (`sandbox.get(name)`) — auto-snapshot on death + auto-resume on next get is the persistence model.
4. **`after()` / `waitUntil()` for tool logging.** Tool-call telemetry, audit writes, and analytics should never block the user response. Use `after()` (Next 15+) or `waitUntil()` from `@vercel/functions` for any write that doesn't affect the streamed response.
5. **Fluid Compute for JIT/process warmth.** Streaming LLM responses benefit from warm processes; the GraphQL/Apollo JIT cache + persisted-document plans only pay back when processes survive across requests. Fluid is the default; disabling it on AI workloads is almost always wrong.
## Frequent gotchas
- **Single-provider lock-in.** "We're using AI Gateway" doesn't imply failover — the provider list still has to be configured. A single-provider gateway is a thinner wrapper, not multi-provider resilience.
- **Sandbox-per-request.** `new Sandbox(...)` inside a per-request handler with no `id` argument creates a fresh microVM each time. Cheaper to pool when isolation allows.
- **BYOK fallback cost invisible.** AI Gateway with BYOK silently falls back to system credits on 429 / provider outage; cost migrates from "free BYOK" to "billed credits" without a separate signal unless tracked.
- **Observability Events runaway.** Captured every tool call + every streamed delta at 100% sampling — events SKU climbs above 30% of bill. Cap span cardinality before scaling traffic.
## Cross-references
- [external-api-critical-path](../support-topics/external-api-critical-path.md) — sequential vs parallel calls; AI Gateway is one external API among others
- [fluid-compute-caveats](../support-topics/fluid-compute-caveats.md) — module-state hazards and shared-instance caveats
- [function-duration-io-and-after](../support-topics/function-duration-io-and-after.md) — `after()` for post-response tool logging
- [observability-events-cost-attribution](../support-topics/observability-events-cost-attribution.md) — when Observability Events climb above 20% of bill
- [use-cache-remote-shared-origin-data](../support-topics/use-cache-remote-shared-origin-data.md) — caching shared LLM context or embedding lookups
- `https://vercel.com/docs/ai-gateway` — provider configuration, failover chain
- `https://vercel.com/docs/vercel-sandbox` — `sandbox.get(name)` and active-compute billing
references/playbooks/api-service.md
# API service
Headless API backend. No UI routes. Often consumed by mobile apps, partner integrations, or other Vercel projects via rewrites.
## Typical billing shape
Function Duration dominates (every request is a function invocation). Edge Requests scale with API traffic. External API costs matter when the service is a thin shim over third-party APIs (Stripe, Twilio, etc.).
## Priority patterns
1. **Cache GET responses at the edge.** Idempotent GET endpoints (catalog reads, status checks, public data) should ship with `Cache-Control: public, s-maxage=<seconds>, stale-while-revalidate=<longer>`. The CDN serves repeat callers without invoking the function.
2. **Rate-limit at the edge, not the function.** Middleware with proper matcher scoping handles abusive clients before they hit your function-duration bill.
3. **Parallel external API calls.** A "checkout-like" endpoint that calls Stripe + inventory + email-service sequentially is the most common slow_route in this profile. `Promise.all` is the obvious fix.
4. **Background work post-response.** `after()` (Next 15+) for analytics, webhooks-to-self, and any write that doesn't affect the response.
5. **Connection pooling.** Direct PG connections from serverless function instances exhaust the database. Use PgBouncer / Prisma Accelerate / Neon's pooler.
## Frequent gotchas
- **No `Cache-Control` on the public GETs.** This is the most common finding in this profile, and the easiest fix.
- **Auth check serialized with data load.** `await checkAuth()` then `await loadData()` — these are often independent and can run in parallel if your auth path doesn't depend on the data.
- **External API fan-out for one user.** A "build me a profile" endpoint that calls 5 third parties sequentially. Even small latency improvements multiplied by every user are huge.
- **Long-running async operations on the request path.** Image generation, PDF rendering, big report computation. Move these to background queues or `after()`.
## Cross-references
- `https://vercel.com/docs/caching/cdn-cache` — the GET-handler Cache-Control fix
- `vercel-react-best-practices:async-parallel` — parallelize external API calls
- `vercel-react-best-practices:server-after-nonblocking` — `after()` for post-response work
- `https://vercel.com/docs/fluid-compute` — when cold starts on infrequently-called endpoints hurt
- `https://nextjs.org/docs/app/building-your-application/routing/middleware` — for rate-limit middleware
references/playbooks/content-site.md
# Content site
Documentation, blogs, knowledge bases, marketing-adjacent content with mostly static pages. Authoring may be headless-CMS-driven or markdown-in-repo.
## Typical billing shape
Edge Requests dominate (every page view is an edge request; static assets even more). Image Optimization is often the #2 line item. Function Duration tends to be low — most pages should be static or ISR.
## Priority patterns
1. **Pre-render everything that can be pre-rendered.** Blog index, individual posts, docs pages, category pages. Use `generateStaticParams` for App Router or `getStaticPaths` for Pages Router. Anything CMS-driven should run on a webhook revalidation, not on every request.
2. **ISR with a sensible cadence.** Pages that need fresh-ish content but don't need real-time accuracy go ISR. `revalidate: 3600` (hourly) is a good starting point for docs; `60s` for blog index pages.
3. **`next/image` for every image asset.** Hero images, author photos, post inline images, OG images. Even thumbnail-only sites benefit from format negotiation (WebP/AVIF).
4. **`next/font` for self-hosted fonts.** Eliminates FOIT/FOUT, eliminates the third-party request, prevents CLS.
5. **Prefetch on hover.** `next/link` does this by default. For other frameworks, consider intersection-observer-based prefetch on the visible link set.
## Frequent gotchas
- **`force-dynamic` on the blog index.** Almost never necessary. The index can ISR or be fully static.
- **Markdown rendering on every request.** If you're parsing MDX at request time, you're paying function-duration cost on what should be a static asset. Build-time MDX → static HTML.
- **Search rebuilt on every request.** Site search backed by a function that queries a CMS on every keystroke. Move to a search index (Algolia, Pagefind, build-time generated) and serve from the CDN.
- **CMS preview routes leaking into production traffic.** A `/preview/[slug]` route that's effectively another rendering path; sometimes called from production by mistake. Audit referrers.
## Cross-references
- `https://nextjs.org/docs/app/api-reference/functions/generate-static-params` — for pre-rendering
- `https://vercel.com/docs/incremental-static-regeneration` — for the ISR fix
- `https://nextjs.org/docs/app/api-reference/components/image` — image optimization
- `https://nextjs.org/docs/app/api-reference/components/font` — self-hosted fonts
- `vercel-react-best-practices:bundle-defer-third-party` — defer analytics/cookie banners
references/playbooks/ecommerce.md
# E-commerce
Storefronts with cart, checkout, product catalogs. Often Stripe-integrated. Traffic skews toward catalog browsing (cacheable) and checkout (uncacheable).
## Typical billing shape
Edge Requests dominate (catalog browsing, image asset traffic) → Image Optimization (product images) → Function Duration (cart/checkout APIs). ISR Reads matter when product pages use ISR.
## Priority patterns
1. **Catalog pages: aggressive ISR + image optimization.** Product list and product detail pages should be ISR with a sensible `revalidate` (60s-3600s). Every image should go through `next/image` (or the framework equivalent). For Vercel-hosted storefronts, image cost can dominate everything else.
2. **Checkout: keep dynamic, but parallelize external calls.** Cart/checkout/payment routes are correctly dynamic. The win is in reducing their function duration — `Promise.all` for independent calls to Stripe + inventory + tax services. Cite `vercel-react-best-practices:async-parallel`.
3. **Cart drawer hydration: lift `'use client'` to the leaf.** Cart components are interactive, but the page wrapping them shouldn't be. Hoist server-rendered parts upward; only the buttons/forms are client.
4. **Webhooks: separate, not on the user path.** Stripe/Shopify webhook handlers should live as their own routes with short duration limits. They don't share traffic patterns with the storefront.
5. **Edge middleware for A/B + region routing only.** Catalog locale routing is a fine fit. Auth/cart state belongs in the dynamic page, not middleware.
## Frequent gotchas
- **Product images served raw.** `<img src={product.imageUrl}>` for hundreds of variants costs more than the rest of the bill combined. Always next/image.
- **`force-dynamic` on the storefront homepage.** Often added during development to test cart-state behavior, never removed. Audit ruthlessly.
- **Sequential Stripe calls.** "Create customer" → "create subscription" → "create invoice" is often three sequential awaits where two could run in parallel.
- **Bot traffic on product search.** Marketing-driven traffic + bot traffic on search routes inflates edge request cost. Bot Protection often pays for itself within a month.
## Cross-references
- `vercel-react-best-practices:async-parallel` — parallelize Stripe/inventory/tax calls in checkout
- `vercel-react-best-practices:async-suspense-boundaries` — stream the checkout shell, fill cart drawer later
- `vercel-react-best-practices:bundle-defer-third-party` — defer analytics (GA, Mixpanel) post-hydration
- `https://nextjs.org/docs/app/api-reference/components/image` — for the catalog image fix
- `https://vercel.com/docs/bot-management` — for bot traffic on search/product routes
references/playbooks/marketing.md
# Marketing site
Landing pages, lead-capture forms, A/B-tested variants, region-routed homepages. Traffic is bursty (campaigns drive spikes). Bot traffic can be substantial.
## Typical billing shape
Edge Requests dominate. Image Optimization is high (hero images, illustrations, product screenshots). Bandwidth matters for video content. Function Duration is usually low — most pages are static or ISR.
## Priority patterns
1. **Aggressive caching at the edge.** Marketing pages rarely change between campaign updates. `Cache-Control: public, s-maxage=86400, stale-while-revalidate=604800` keeps the CDN warm for 24h and stale-serves for a week.
2. **Bot Protection.** Marketing campaigns attract competitor scrapers and bot traffic that inflates edge requests without delivering value. If edge cost is > $100/month and Bot Protection is disabled, this is almost always the top platform rec.
3. **ISR for content-driven sections.** Customer logos, testimonials, "latest blog post" widgets, pricing tables — anything coming from a CMS. Revalidate hourly or on webhook.
4. **A/B test logic at the edge, not in the page.** Edge Middleware for the variant assignment; cached static page per variant. Don't render the variant choice on every request.
5. **Defer all third-party JS post-hydration.** Analytics, chat widgets, marketing pixels, cookie banners. None of them block the LCP. Cite `vercel-react-best-practices:bundle-defer-third-party`.
## Frequent gotchas
- **Hero images served at native resolution.** A 4MP hero image on every viewport, including mobile. `next/image` with `sizes` is mandatory.
- **Cookie banner blocks first paint.** GDPR-compliant cookie banners often render synchronously in the head. Defer; render after hydration; persist consent state via a tiny inline script.
- **Tracking pixel waterfalls.** Three different analytics services loaded in a chain. Load them after hydration in parallel; better yet, replace some with server-side tracking via webhook.
- **`/api/contact` is the only function but runs hot.** Marketing sites are mostly static but the contact form gets bot-spammed. Rate limit at middleware; consider a queue for outgoing emails.
## Cross-references
- `https://vercel.com/docs/bot-management` — almost always the right platform rec
- `https://vercel.com/docs/incremental-static-regeneration` — for CMS-driven sections
- `https://nextjs.org/docs/app/api-reference/components/image` — hero/illustration optimization
- `vercel-react-best-practices:bundle-defer-third-party` — defer analytics/pixels
- `https://nextjs.org/docs/app/building-your-application/routing/middleware` — A/B variant routing at the edge
references/playbooks/README.md
# Playbooks
Application-profile-specific advice that shapes how recommendations are phrased and ordered. Playbooks never invent claims — every rec still traces to a verified candidate or finding. They tell the recommender what to emphasize when a project matches a profile.
## How a playbook gets applied
1. Step 1 detects the project's stack + dependencies.
2. The recommender heuristics infer an application profile (best guess from frameworks + dep signals).
3. The matching playbook(s) are included in the recommender's context.
4. Recommendations are shaped: ordering tilts toward the profile's priority list; phrasing nods to profile-specific concerns.
## Profile detection (best-effort heuristics)
| Signals → | Profile |
|---|---|
| `@vercel/sandbox`, `@ai-sdk/*`, `ai`, `openai`, `@anthropic-ai/sdk` deps OR AI Gateway / Sandbox SKU active in `usage.services` | `ai-application` |
| `stripe`, `@shopify/*`, `react-stripe-js`, "cart"/"checkout" routes | `ecommerce` |
| `next-auth`, `clerk`, dashboard routes, multi-tenant headers | `saas` |
| Only `pages/api/**` or `app/api/**`, no UI routes | `api-service` |
| Heavy MDX / markdown, mostly static routes | `content-site` |
| Lots of `/(marketing)/` route groups, A/B test deps | `marketing` |
`ai-application` is checked first — AI-shaped customers often share routes with SaaS/ecommerce surfaces, but the billing shape (AI Gateway dominant) and remediation set (provider failover, sandbox reuse, OIDC keyless) belong to this profile.
When detection is uncertain, no playbook is applied. The recommender works fine without one — the playbook is a tilt, not a requirement.
## Playbook schema
Each playbook is a Markdown file with a fixed shape so the recommender can parse it reliably. Required sections:
```markdown
# {Profile name}
## Typical billing shape
(Which dimensions dominate — e.g., "Edge Requests > Function Duration > Image Optimization")
## Priority patterns
(Ordered list of patterns this profile particularly benefits from)
## Frequent gotchas
(Anti-patterns specific to this profile)
## Cross-references
(Rec IDs from recommendations.md or rule names from vercel-react-best-practices)
```
## Contributing a new playbook
1. Identify a clear application profile and one or two representative project profiles that exemplify it.
2. Create `references/playbooks/<profile>.md` matching the schema.
3. Add detection signals to the table above (the heuristics live in the recommender code; document them here).
4. Update the playbook selection matrix in `references/scoring.md`.
5. Run `node --test packages/vercel-optimize-tests/test/support-topics.test.mjs packages/vercel-optimize-tests/test/investigation-brief.test.mjs`. No tests directly cover playbooks (they're content), but the schema validator runs in CI.
references/playbooks/saas.md
# SaaS
Multi-tenant applications with authenticated dashboards, settings, billing. Auth-gated by default. Traffic skews toward function duration (per-user data fetches) over edge requests.
## Typical billing shape
Function Duration dominates (every dashboard request runs the function fully — no edge caching for auth-gated content). Edge Requests grow with API surface. ISR rarely applies. Image Optimization rarely material.
## Priority patterns
1. **Per-request memoization with React.cache().** Server Components called from multiple places in the same request tree often re-query the database. `React.cache()` dedupes within the request. Cite `vercel-react-best-practices:server-cache-react`.
2. **Parallel data loads in Server Components.** Dashboards typically load user + org + billing + recent-activity. Run all four in parallel via `Promise.all`. Cite `vercel-react-best-practices:async-parallel` and `:server-parallel-fetching`.
3. **Fluid Compute.** Auth-gated routes have higher cold-start sensitivity (every cold start is a user waiting). If cold-start signal shows up in observability, Fluid Compute is usually the right account-level rec.
4. **Async work after response.** Activity logs, audit trails, analytics events — anything that doesn't block the user — should run via `after()` (Next 15+) or `waitUntil()` from `@vercel/functions`. Cite `vercel-react-best-practices:server-after-nonblocking`.
5. **Suspense boundaries around expensive widgets.** The dashboard shell renders fast; widgets stream in. This shifts perceived latency without changing the underlying queries.
## Frequent gotchas
- **N+1 ORM queries.** A list page that loops over results and fetches related records per-item. Especially common with Prisma's `.findUnique` inside a `.map`. Use `include` or batch via DataLoader.
- **Sequential session+permission checks.** `await getSession()` then `await checkPermissions()` then `await loadData()` — these can often be parallelized when the permissions check doesn't depend on the data load.
- **No connection pooling on serverless.** Prisma without a pooler exhausts the database under load. Connection pooling is mandatory.
- **Polling for state from the client.** Every poll is a function invocation. Replace with SWR + on-demand revalidation, or with `revalidateTag` triggered by the mutation that actually changes state.
## Cross-references
- `vercel-react-best-practices:server-cache-react` — per-request dedup
- `vercel-react-best-practices:server-parallel-fetching` — restructure for Promise.all
- `vercel-react-best-practices:async-suspense-boundaries` — stream the dashboard shell
- `vercel-react-best-practices:server-after-nonblocking` — defer audit/analytics writes (Next 15+)
- `vercel-react-best-practices:client-swr-dedup` — replace polling with SWR
- `https://vercel.com/docs/fluid-compute` — when cold starts hurt
references/playbooks/sveltekit.md
# SvelteKit
Framework-specific playbook for SvelteKit projects on Vercel. Applies in
addition to whichever application-profile playbook fits (saas, ecommerce,
content-site, etc.). SvelteKit-on-Vercel ships through
`@sveltejs/adapter-vercel`, so most platform-level recs map to adapter
config rather than per-route framework APIs.
## Typical billing shape
Function Duration dominates server-rendered routes (every `+page.server.ts`
`load` + `+server.ts` POST handler runs as a function). Edge Requests grow
with API surface (`+server.ts` and form actions). ISR is supported via the
adapter; when enabled, it converts to a cache_result HIT after first render.
Image Optimization is rarely a SvelteKit-specific lever (it's the same
Vercel image service Next.js uses).
## Priority patterns
1. **Adapter ISR for cacheable content.** Routes that don't depend on
per-request data are still served as functions by default. The
adapter accepts an `isr: { expiration: 60 }` option per route (set
in `+page.server.ts` via `export const config`). This converts
function invocations to cache hits. Cite
`https://kit.svelte.dev/docs/adapter-vercel` +
`https://vercel.com/docs/incremental-static-regeneration`.
2. **Prerender what's static.** `export const prerender = true` in
`+page.server.ts` or `+page.ts` moves a route from function to CDN.
Cite `https://kit.svelte.dev/docs/page-options`.
3. **Parallel `load` fetches.** A `load` function with multiple
sequential `await fetch(...)` calls leaves wall-clock time on the
table — wrap them in `Promise.all` (or return promises directly
from `load`, which SvelteKit streams). Cite
`https://kit.svelte.dev/docs/load`.
4. **Move per-request work to `+server.ts` action handlers and run
them via `fetch` from the client.** Reduces SSR cost when only a
slice of the page actually needs server data on every request.
5. **`hooks.server.ts` matcher hygiene.** Like Next.js middleware, the
`handle` hook intercepts every request unless filtered. Heavy
`handle` code multiplies cost by request volume. Move work into the
specific route's `load` when only that route needs it.
6. **Adapter runtime + region config.** Single-region default; if the
project's users skew to a different region, set `regions: [...]` on
the adapter to reduce TTFB by 100-300ms.
## Frequent gotchas
- **Per-route SSR when prerender would do.** Marketing pages, docs,
blog posts often end up as functions because nobody added
`prerender = true`. The scanner flags these.
- **`+layout.server.ts` data fetches blocking every child route.**
Auth-check + user-load in a layout makes EVERY function invocation
wait on those queries — even routes that don't read user. Push
user-load into the routes that need it.
- **Adapter version drift.** `adapter-vercel@5` adds new options (ISR,
split). `adapter-vercel@3` doesn't. The recommender must check the
installed version before suggesting `isr: ...`.
- **`fetch` calls in `load` to your own SvelteKit routes.** SvelteKit
optimizes these into direct module calls during SSR, but only if
the URL is relative. A hardcoded `https://your-domain.tld/api/...`
defeats this optimization.
- **No connection pooling on serverless.** Same as Next.js — Postgres
without a pooler exhausts the database under load.
## Cross-references
- `https://kit.svelte.dev/docs/adapter-vercel` — adapter config (ISR, regions, runtime)
- `https://kit.svelte.dev/docs/page-options` — prerender, ssr, csr
- `https://kit.svelte.dev/docs/load` — parallel fetches in load
- `https://kit.svelte.dev/docs/routing` — file conventions
- `https://kit.svelte.dev/docs/hooks` — handle / handleFetch
- `https://kit.svelte.dev/docs/form-actions` — server-side form handling
- `https://kit.svelte.dev/docs/state-management` — request-scoped state
- `https://vercel.com/docs/incremental-static-regeneration` — ISR on Vercel
- `https://vercel.com/docs/fluid-compute` — Fluid Compute (framework-agnostic)
references/recommendations.md
# Recommendations
How recommendations are shaped, written, sanitized, and graded.
## Table of contents
- [Schema](#schema)
- [Writing rules](#writing-rules)
- [The 12 sanitizers](#the-12-sanitizers)
- [Envelope-unwrap recovery](#envelope-unwrap-recovery)
- [Grading rubric](#grading-rubric)
- [Next.js version awareness](#nextjs-version-awareness)
## Schema
Every recommendation is a JSON object matching this TypeScript shape:
```ts
interface Recommendation {
// Customer-facing
what: string; // 1 line, lead with impact. Max 80 chars when feasible.
why: string; // 1-2 sentences. Root cause. Cites codebase findings + counts.
fix: string; // Step-by-step. Includes before/after code fences. Specific enough to implement.
bucket: 'cost' | 'performance' | 'reliability';
effort: 'low' | 'medium' | 'high';
affectedFiles: string[]; // Verified file paths, from candidate.files
currentBehavior: string; // What the code does now (with snippet)
desiredBehavior: string; // Target state (with snippet)
risk?: string; // Optional: e.g., "Removing force-dynamic may serve stale data on /admin"
verify: string; // How to confirm the fix worked. e.g., "Re-run `vercel metrics …` and watch p95"
// Impact (computed from impact-magnitude.mjs in Step 4)
impactLabel: {
performance?: string; // PRECISE: "Reduce /api/products p95 from 850ms toward ~250-400ms"
costMagnitude?: 'negligible' | 'small' | 'medium' | 'large' | 'very-large';
costPhrase?: string; // "hundreds of dollars per month at current traffic"
billingDimension?: string;
fractionReduced?: number;
};
impactTier: 'high' | 'medium' | 'low';
billingDimension?: 'edge-requests' | 'function-duration' | 'image-optimization' | 'isr-reads' | 'isr-writes' | 'bandwidth' | 'data-cache-reads' | 'cron-invocations' | string;
// Grounding
citations: string[]; // From references/docs-library.json allow-list. Required: ≥1 entry.
candidateRef?: string; // The gate candidate this rec traces to (e.g., "uncached_route:/api/products")
findingRefs?: string[]; // File:line markers from verifiedFindings.json
appliesAlsoTo?: Array<{ // Added by dedup when matching recs collapse into one customer-facing item.
candidateRef?: string;
affectedFiles?: string[];
o11ySignal?: string;
what?: string;
}>;
corroborationCount?: number; // Number of matching verified recs folded into this item, including itself.
// Verifier output (computed in Step 3.6)
verification?: {
passRate: number;
failed: Array<{ type: string; text: string; reason: string }>;
};
// Sanitizer audit trail (computed in Step 3.4)
sanitizerTrail?: string[]; // ["$-strip:2", "version-mismatch:next@15+:1", ...]
needsReview?: boolean; // Set when a sanitizer caught a hazard
// Grading (Step 3.5)
quality: {
specificity: number; // 0-1
actionability: number;
grounding: number;
evidence: number;
overall: number;
grade: 'Excellent' | 'Good' | 'Fair' | 'Poor';
};
}
```
## Writing rules
The recommender prompt explicitly tells the agent to follow these rules. Sanitizers enforce them after generation.
**Voice and tone** are governed by [`references/voice.md`](./voice.md). Read it before writing recommendation prose; it keeps reports direct, metric-grounded, and free of internal process terms.
### Lead with impact
The `what` field opens with the verb + the change, not the framing. Compare:
- ❌ "Consider enabling caching on the /api/products route" (filler before substance)
- ✅ "Add Cache-Control with s-maxage to /api/products" (verb-first, scope-explicit)
### Cite codebase findings with line numbers
The `why` must reference a verified finding from `verifiedFindings.json`:
- ❌ "The route is uncached" (could apply anywhere)
- ✅ "src/app/api/products/route.ts:22 returns Response without Cache-Control; observability shows 0% cache hit on 1.2M invocations/mo"
### No $ literals in customer fields
The user-mandated rule. `what`/`why`/`fix`/`impact`/`currentBehavior`/`desiredBehavior` must not contain `$N` money literals. Use magnitude framing from `impact-magnitude.mjs`.
- ❌ "Save $340/mo by adding s-maxage"
- ✅ "Hundreds of dollars per month at current traffic"
- ✅ (precise performance) "Move 1.2M monthly invocations to the CDN; expect p95 to drop from 850ms toward ~50ms on cache hits"
The `$-strip` sanitizer enforces this at output time, but the prompt should also instruct the LLM not to emit dollar literals in the first place.
### Before/after code fences required
`currentBehavior` shows the offending snippet. `desiredBehavior` shows the target. Language-tagged code fences. Keep both under ~20 lines.
### Cite at least one URL from the library
`citations[]` must contain at least one entry from `references/docs-library.json`. The `missing-citation` sanitizer drops uncited recs. The `unknown-citation` and `version-mismatch` sanitizers strip invalid citations.
### Match the user's framework version
Don't recommend `'use cache'` (Next 15+) to a Next 13 user. The recommender prompt receives only the citation subset valid for the user's stack — but the LLM can still hallucinate. The `version-mismatch` sanitizer catches stragglers.
## The 12 sanitizers
Each sanitizer records its action in `rec.sanitizerTrail` when it mutates a field. Tag format: `tag:detail`. Tags are lexically stable — downstream consumers grep them.
| # | Sanitizer | Trigger | Action | Trail tag |
|---|---|---|---|---|
| 1 | `$-strip` | Money-literal regex in customer field | Replace with "the billed cost" | `$-strip:N` |
| 2 | `vercel-directive-strip` | `stale-if-error` / `proxy-revalidate` in cache-control | Strip directive (Vercel CDN doesn't honor) | `vercel-directive-strip:directive` |
| 3 | `rate-limit` | Concurrency × delay > known provider rate limit | Prepend caveat, set needsReview | `rate-limit:provider:prescribed/limit` |
| 4 | `pre-release` | Fix enables `-rc`/`-beta`/`-canary` feature | Append "requires pre-release version" caveat | `pre-release:pkg@version` |
| 5 | `middleware-conflict` | Rec targets route covered by middleware matcher | Append "Middleware {matcher} may intercept" caveat | `middleware-conflict:matcher` |
| 6 | `undeclared-dep` | Fix imports a package not in package.json | Prepend "Add dependency first: npm i {pkg}" | `undeclared-dep:pkg` |
| 7 | `count-correct` | Cited count > verified count, ground-truth known | Rewrite to "~N" with verified count | `count-correct:token:cited→actual` |
| 8 | `count-strip` | Cited count > verified count, no ground truth | Rewrite to "a number of" | `count-strip:token` |
| 9 | `rendering-mode-mislabel` | Rec blames ISR/SSR on a static page | Append warning, set needsReview | `rendering-mode-mislabel` |
| 10 | `unknown-citation` | URL not in `references/docs-library.json` | Strip URL, set needsReview if all stripped | `unknown-citation:url` |
| 11 | `version-mismatch` | URL's `applicableFrameworks` doesn't match stack | Strip URL, set needsReview if all stripped | `version-mismatch:url` |
| 12 | `missing-citation` | `citations.length === 0` after other sanitizers | DROP rec entirely | (rec not emitted; counted at end) |
The sanitizer order matters: dollar-strip runs first (cheap, deterministic), then content sanitizers, then citation sanitizers last. This guarantees citation count is computed against the final state.
The `recordSanitizer(rec, tag)` helper is the single entry point — sanitizers MUST call it before mutating fields. Otherwise the audit trail rots.
### Provider rate limits
Used by sanitizer #3. These provider limits are public contract values:
| Provider | Limit | Doc URL |
|---|---|---|
| Notion | 3 rps | https://developers.notion.com/reference/request-limits |
| OpenAI | 30 rps | https://platform.openai.com/docs/guides/rate-limits |
| Stripe | 100 rps | https://docs.stripe.com/rate-limits |
| Anthropic | 10 rps | https://docs.anthropic.com/en/api/rate-limits |
Tiers/plans differ; these are first-tier defaults. The sanitizer prepends a caveat if the rec prescribes higher concurrency.
## Envelope-unwrap recovery
Not a sanitizer — a recovery step. LLMs occasionally wrap their JSON output in an envelope:
```json
{ "data": { "recommendations": [...] } }
{ "result": { "recommendations": [...] } }
{ "insights": { "recommendations": [...] } }
```
`attemptManualRecovery` peels one wrapping layer before schema validation. Increments `hygieneCounters.envelopeUnwraps`. Logs the unwrap to the run log.
This is the only "creative" parsing the skill does. Anything else that fails schema validation is rejected.
## Grading rubric
Each rec is scored on four axes, 0-1 each. Average → grade:
| Axis | What it measures | Strong (1.0) signal |
|---|---|---|
| Specificity | Concrete files, line numbers, code snippets | Triple-backtick code fence OR inline code ≥10 chars + verified file path |
| Actionability | Clear "do this then that" steps | Numbered steps; verbs present in each step; no "consider"/"might" |
| Grounding | Claims trace to findings or metric data | `sourceIndex` matches a finding OR rec has affectedFiles + code fences (presumed evidence) |
| Evidence | Numeric, observed claims | Count words (errors, queries, invocations) + units (% / ms / s / K / M) |
Grade thresholds:
- `Excellent` ≥ 0.85
- `Good` 0.70 – 0.85
- `Fair` 0.55 – 0.70
- `Poor` < 0.55 → dropped at quality floor in Step 4
## Next.js version awareness
The recommender's citation library is filtered by `signals.json.stack.framework@frameworkVersion`. The agent should still self-check the version when picking which APIs to recommend:
| Feature | Available | Notes |
|---|---|---|
| App Router | Next ≥ 13.0 | Default since 14 |
| `generateStaticParams` | Next ≥ 13.0 | Replaces getStaticPaths for App Router |
| Fetch `next: { revalidate }` | Next ≥ 13.0 | Note: default fetch caching flipped in Next 15 |
| `unstable_cache` | Next 14-15 | Replaced by 'use cache' in 16 |
| `'use cache'` directive | Next ≥ 15.0 | Persistent cache primitive |
| `cacheLife()`, `cacheTag()` | Next ≥ 15.0 | Pairs with 'use cache' |
| `after()` | Next ≥ 15.0 | Non-blocking post-response work |
| Partial Prerendering | Next ≥ 15.0 | Stable target later — verify per release |
| `revalidateTag` / `revalidatePath` | Next ≥ 13.4 | Tag-based on-demand invalidation |
| `cookies()` / `headers()` async | Next ≥ 15.0 | Async pattern in 15+ |
The skill's curated citation library encodes these constraints via `applicableFrameworks`. If a contributor adds a new Next.js feature URL, they MUST set the right semver range in `references/docs-library.json`.
references/scanner-patterns.md
<!-- THIS FILE IS GENERATED by scripts/build-docs.mjs. Do not edit by hand. -->
<!-- To change scanner descriptions, edit lib/scanners/*.mjs metadata exports. -->
<!-- To change gate thresholds, edit lib/gates/*.mjs metadata exports. -->
# Scanner patterns
AST/grep-style scanners run in parallel with metric-driven investigation. They find known anti-patterns. Findings on cold-path or unmappable files are dropped unless the scanner declares `trafficIndependent: true`.
Total scanners: 15.
## Patterns
### `cache-components-suspense-dedupe` — 'use cache' with multiple Suspense boundaries on the same data
- **Severity**: medium
- **Billing dimension**: function-duration
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** 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.
**Fix.** 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.
**Citations:**
- `https://nextjs.org/docs/app/api-reference/directives/use-cache`
- `https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents`
- `https://nextjs.org/docs/app/guides/migrating-to-cache-components`
---
### `edge-heavy-import` — Heavy / node-only import inside edge-runtime file
- **Severity**: high
- **Billing dimension**: function-duration
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
**Description.** 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).
**Fix.** 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.
**Citations:**
- `https://vercel.com/docs/functions/runtimes/edge-runtime`
- `https://vercel.com/docs/fluid-compute`
---
### `force-dynamic` — export const dynamic = 'force-dynamic'
- **Severity**: medium
- **Billing dimension**: function-duration
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** 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.
**Fix.** 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.
**Citations:**
- `https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config`
---
### `headers-in-page` — Dynamic API call forcing dynamic rendering
- **Severity**: medium
- **Billing dimension**: function-duration
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** 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.
**Fix.** 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.
**Citations:**
- `https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config`
- `https://nextjs.org/docs/app/building-your-application/caching`
---
### `large-static-asset` — Large file in public/
- **Severity**: medium
- **Billing dimension**: bandwidth
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
**Description.** 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).
**Fix.** 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).
**Citations:**
- `https://vercel.com/docs/manage-cdn-usage`
- `https://vercel.com/docs/image-optimization`
---
### `max-age-without-s-maxage` — Cache-Control: max-age without s-maxage
- **Severity**: medium
- **Billing dimension**: edge-requests
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** max-age caches in the browser; s-maxage caches at the CDN. Without s-maxage, every uncached visitor request invokes the function. Adding s-maxage often cuts function invocations by 80%+ on read-heavy routes.
**Fix.** Add s-maxage to the Cache-Control header. Example: Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=86400. Pair with explicit cache-bust strategy if content can change.
**Citations:**
- `https://vercel.com/docs/caching/cdn-cache`
- `https://vercel.com/docs/caching/cache-control-headers`
---
### `middleware-broad-matcher` — Middleware matcher missing or too broad
- **Severity**: high
- **Billing dimension**: edge-requests
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
**Description.** middleware.ts without a config.matcher (or matcher: ["/(.*)"]) runs on every request including _next/static, _next/image, favicon.ico, and image asset fetches. Edge-request cost scales accordingly.
**Fix.** Scope the matcher to actual application paths. Example: matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"]
**Citations:**
- `https://nextjs.org/docs/app/building-your-application/routing/middleware`
---
### `missing-cache-headers` — Cacheable route or fetch with no caching (Cache-Control absent or no-store)
- **Severity**: medium
- **Billing dimension**: edge-requests
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** Two antipatterns: (a) GET handlers without explicit Cache-Control headers serve uncached; (b) fetch() calls with cache:"no-store" or next:{revalidate:0} opt out of caching even on cacheable upstream data. For non-auth routes / fetches, both are leaving cache hits on the floor.
**Fix.** For GET handlers: return a Response with Cache-Control: public, s-maxage=<seconds>, stale-while-revalidate=<window>. For fetch(): drop cache:"no-store" (use { next: { revalidate: <seconds> } } in Next.js) so the response is cached by the framework + CDN.
**Citations:**
- `https://vercel.com/docs/caching/cdn-cache`
- `https://vercel.com/docs/caching/cache-control-headers`
- `https://nextjs.org/docs/app/building-your-application/caching`
---
### `prisma-include-tree-bloat` — Deep Prisma include tree (3+ levels)
- **Severity**: high
- **Billing dimension**: function-duration
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** Nested .include({ x: { include: { y: { include: { z: ... } } } } }) makes Prisma issue a single huge join that scales O(N*M*K). Function duration explodes, memory spikes, often causes timeouts.
**Fix.** Replace with explicit .findMany() calls or scoped .include() of only what the consumer reads. Consider Prisma.select() to project specific fields. For lists, batch with DataLoader patterns.
**Citations:**
- `vercel-react-best-practices:server-parallel-fetching`
---
### `region-pin-in-config` — Function region pinned in config
- **Severity**: low
- **Billing dimension**: function-duration
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
**Description.** vercel.json `regions` or per-route `preferredRegion` is set. If the pinned region is far from the dominant user geo (or far from a data source) p95 TTFB suffers. This scanner provides the configured-region signal so the region-misconfig gate can recommend an audit.
**Fix.** Audit the pinned region against traffic geography (Speed Insights or Web Analytics by country) and data-source location. Consider multi-region if data lives in a fixed location and users are global; consider relocating if users are concentrated in one geography.
**Citations:**
- `https://vercel.com/docs/functions/configuring-functions/region`
- `https://vercel.com/docs/functions/configuring-functions/region`
---
### `source-maps-production` — Source maps enabled in production
- **Severity**: low
- **Billing dimension**: edge-requests
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
**Description.** productionBrowserSourceMaps: true ships .map files in the production bundle, increasing transfer size 30-100% per visitor. Useful for error reporting via Sentry; not useful for users.
**Fix.** Keep source maps generation but exclude them from the public bundle. Upload to your error tracker via build-time CI step; do not serve them with the deployment.
**Citations:**
- `https://nextjs.org/docs/messages/improper-devtool`
---
### `sveltekit-prerender-missing` — SvelteKit page without explicit prerender / ISR config
- **Severity**: low
- **Billing dimension**: function-duration
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** SvelteKit page or +page.server.ts is missing an explicit `prerender`, `ssr`, or adapter `config.isr` declaration. Default is per-request function execution — investigate whether the route could be prerendered or ISR-cached.
**Fix.** If the page is static (no per-user / per-request data), add `export const prerender = true` in +page.ts or +page.server.ts. If the data refreshes on a schedule, prefer adapter-vercel's ISR option via `export const config = { isr: { expiration: 60 } }`.
**Citations:**
- `https://kit.svelte.dev/docs/page-options`
- `https://kit.svelte.dev/docs/adapter-vercel`
- `https://vercel.com/docs/incremental-static-regeneration`
---
### `turbo-force-bypass` — Turborepo cache bypass on a monorepo
- **Severity**: high
- **Billing dimension**: build
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
**Description.** Turborepo's per-task cache can be bypassed by an explicit force flag, a `cache: false` config, or missing build-skip configuration. Every commit can rebuild unchanged work; Build Minutes climb with project count.
**Fix.** Remove `TURBO_FORCE=true` from build env/scripts unless intentional. Set `tasks.build.cache: true` in `turbo.json` (or remove the override), and include generated outputs in Turbo's cache contract. Prefer Vercel's skip-unaffected monorepo behavior when available; use `ignoreCommand` only when that setting cannot cover the project.
**Citations:**
- `https://vercel.com/docs/monorepos`
- `https://vercel.com/docs/builds`
- `https://turborepo.dev/docs/crafting-your-repository/caching`
---
### `unoptimized-image` — Image optimization gap (raw <img>, global flag, missing sizes, or SVG mis-routed)
- **Severity**: high
- **Billing dimension**: image-optimization
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** Four shapes of image-cost waste: raw <img> tags bypass the framework Image component; `images.unoptimized: true` disables Vercel image optimization globally; <Image fill> without `sizes` forces serving the largest source variant; <Image src=".svg"> without `unoptimized` routes vector data through the raster pipeline.
**Fix.** For raw <img>: switch to next/image, enhanced-img (SvelteKit), <Image /> (Astro), or NuxtImg. For global unoptimized:true: remove the flag unless the project is hosted outside Vercel. For fill without sizes: add `sizes="(max-width: 768px) 100vw, 50vw"` or whatever matches your layout. For SVG: add `unoptimized` so the raw SVG ships instead of rastering it.
**Citations:**
- `https://nextjs.org/docs/app/api-reference/components/image`
- `https://vercel.com/docs/image-optimization`
---
### `use-cache-date-stamp` — new Date() / Date.now() / Math.random() inside a 'use cache' file
- **Severity**: high
- **Billing dimension**: isr
- **Traffic-independent**: no (cold-path findings get dropped)
**Description.** `'use cache'` memoizes by argument identity AND prerender output. A timestamp baked into the cached output (`new Date().getFullYear()` in a footer, `Date.now()` in a payload field) forces a fresh ISR write on every regeneration even when the underlying data is unchanged. Random values have the same failure mode.
**Fix.** Replace module-scope `new Date()` with a build-time constant (`const buildYear = new Date().getFullYear()`) or move per-request timestamps into a client component inside `useEffect`. Do not pass dates as arguments to `'use cache'` functions — they invalidate the cache every call.
**Citations:**
- `https://nextjs.org/docs/app/api-reference/directives/use-cache`
- `https://nextjs.org/docs/app/api-reference/functions/cacheLife`
---
references/scoring.md
## Step 4 — Score and report
This reference covers everything that happens after recommendations are drafted: quality floor, impact framing, sort order, the customer-facing report template, and the playbook selection matrix.
## Table of contents
- [Quality floor and prune rules](#quality-floor-and-prune-rules)
- [Impact framing — the magnitude rule](#impact-framing--the-magnitude-rule)
- [`impactLabel` schema](#impactlabel-schema)
- [Sort order and platform-rec cap](#sort-order-and-platform-rec-cap)
- [The customer-facing report template](#the-customer-facing-report-template)
- [Playbook selection matrix](#playbook-selection-matrix)
## Quality floor and prune rules
| Rule | Value | Why |
|---|---|---|
| Drop recommendations with `quality.overall < 0.55` | Hard cutoff (raised from 0.4 in May 2026 audit) | Bad-grade recs erode trust faster than they help. 0.55 matches the Poor/Fair grade boundary; recs below this are "Poor" and shouldn't ship. |
| Prune cap on findings | 30% of input | Stops the pruner from wiping the report when LLM merit-grades are noisy |
| Platform-rec cap | 3 | Account-level recs (Fluid, Bot Protection, Speed Insights) only have room for the top three |
| Quick-wins definition | `effort === 'low' AND priority > 40` | Surfaces fixes the user can ship in a single PR |
| Savings floor (internal ranking only) | $5/mo equivalent | Below this, even a "high" tier impact translates to "negligible" magnitude |
## Impact framing — the magnitude rule
**Performance: be precise.** Use observed numbers. Example:
> "Reduce /api/products p95 from 850ms toward ~250-400ms; cache hit would lift from 0% toward ~60% based on similar cached routes."
Performance numbers come from `signals.json.metrics.*` — they're observed, not extrapolated. Cite the exact route + metric value.
**Dollar cost: never precise.** Use MAGNITUDE BUCKETS via `lib/impact-magnitude.mjs`'s `impactMagnitude({currentCost, impactTier})`:
| Estimated reduction (USD) | Magnitude | Customer-facing phrase |
|---|---|---|
| < $5 | `negligible` | "small cost impact at current traffic" |
| $5 – $50 | `small` | "low-tens of dollars per month at current traffic" |
| $50 – $500 | `medium` | "hundreds of dollars per month at current traffic" |
| $500 – $5,000 | `large` | "low-thousands of dollars per month at current traffic" |
| > $5,000 | `very-large` | "thousands+ of dollars per month at current traffic" |
Reduction is computed as `currentCost × fraction` where `fraction = {high: 0.4, medium: 0.2, low: 0.1}[impactTier]`. The fraction is intentionally conservative — we'd rather under-promise than mislead.
### Discountable vs non-discountable SKUs
When the project is on a Flex Commit and the report frames savings against contract burndown, segment spend before phrasing. Field doctrine (May 2026): the Flex discount slider applies only to a subset of SKUs.
| Discountable (slider applies) | Non-discountable (raw rate) |
|---|---|
| Seats | Build CPU Minutes |
| Edge Requests | Fluid Active CPU |
| Fast Data Transfer | Fluid Provisioned Memory |
| Fast Origin Transfer | Raw Flex top-up |
| Image Optimization | |
| ISR Reads / Writes | |
| Observability Events | |
A recommendation that targets a non-discountable SKU should never frame savings as a percentage of contract; frame as absolute magnitude only. Conversely, a discountable-SKU recommendation may surface "applies to contract burndown" in the magnitude phrase.
**Why magnitudes:**
- Traffic varies. A "20% reduction in edge requests" is exact at today's traffic and meaningless next quarter.
- Pricing changes. Vercel's billing rates move; precise dollar projections rot.
- The user is smart. They'd rather see "hundreds of dollars per month" with a real metric backing it than `$340/mo` with a hand-wave behind it.
- The `$-strip` sanitizer enforces this at output time. Any `$N` literal that slips into customer-facing fields is replaced with "the billed cost" before rendering.
## `impactLabel` schema
```ts
type ImpactLabel = {
// PRECISE: performance recs
performance?: string;
// MAGNITUDE: cost recs
costMagnitude?: 'negligible' | 'small' | 'medium' | 'large' | 'very-large';
costPhrase?: string;
billingDimension?: string; // 'Edge Requests' | 'Function Duration' | ...
fractionReduced?: number; // 0.2 = ~20% — internal only, NOT rendered
};
```
Cost recs render `costPhrase`. Performance recs render `performance`. Reliability recs frame impact as observed error/timeout reduction (e.g., "Cuts 5xx rate from 0.4% to <0.1% based on current traffic").
When a rec spans buckets — e.g., a caching fix that reduces both cost AND latency — render both lines.
## Sort order and platform-rec cap
Internal sort key (never rendered): `priority = currentDimensionCost × fractionReduced × confidence`.
The list of recs the customer sees is sorted by this priority. The platform recommendations section is capped at 3, sorted the same way.
## The customer-facing report template
The agent renders this as the final output of Step 4. The shape is fixed; the content comes from the merged signals + verified recommendations + the `gated[]` list from Step 2.
```markdown
# Vercel Optimization Report — {projectName}
**Stack**: {framework}@{frameworkVersion} | {router} | {orm}
**Plan**: {plan.plan} ({plan.reason})
**Period**: {usage.period.from} → {usage.period.to}
**Observability**: {observability status}
## Cost breakdown
| Service | Usage | Billed Cost |
|---|---|---|
| (non-zero rows from usage.services, sorted by billedCost desc) |
Total billed: {usage.totals.billedCost} (we render the precise current cost — we just don't project future precise savings)
Omit zero-cost service rows from the table at the same cent precision shown to customers. If every row has `$0.00` billed cost but `effectiveCost` / USD `pricingQuantity` is non-zero, explain that net billed cost is `$0.00` after included credits or allotments and show the effective usage cost table instead. If both billed and effective costs are `$0.00`, replace the table with a concise note that `vercel usage` returned a billing payload but every reported service cost was `$0.00` for the window.
If `vercel usage` was queried and unavailable, the cost breakdown is replaced by an observability-derived cost ranking from `metrics.fnGbHrByRoute` + `metrics.fnCpuMsByRoute` + `metrics.fdtByRoute` when those metrics exist. These don't translate directly to dollars — they show *which routes consume the billable units* so the user knows what to attack first. If `usageError` is `NOT_COLLECTED_OBSERVABILITY_BLOCKED` or another `NOT_COLLECTED_*` value, say usage was not collected; do not describe it as a billing-plan or Costs-feature finding.
Render Observability status from the actual collection state:
- `Observability Plus enabled — per-route metrics included` when `observabilityPlusUsable=true`.
- `Per-route metrics unavailable — audit paused before metric-backed route ranking` when an Observability Plus blocker stopped the run before billing/scanner collection.
- `Per-route metrics unavailable — analysis based on billing + scanner findings` when the user accepted a limited audit and billing/scanner signals were collected.
- `Per-route metrics unavailable — limited analysis based on scanner findings` when the user accepted a limited audit but billing usage was queried and unavailable.
- `Not checked — audit paused at unsupported-framework preflight` when framework support stopped the run before the Observability Plus check.
- `Not enabled — analysis based on billing + scanner findings` only for legacy/limited reports where Observability Plus is known false and billing/scanner signals exist.
## Highest-impact recommendations
For each high-priority rec, in order:
1. **{route or file}** — {o11ySignal}
1. **{readable candidate label}** — {readable metric labels}
- **What to do**: {rec.what}
- **Impact**: {impactLabel.performance ?? impactLabel.costPhrase}
- **Effort**: {rec.effort}
- **Citations**: {rec.citations}
## Recommendations
### High impact
| # | Bucket | What | Impact | Effort | Citations |
|---|---|---|---|---|---|
### Medium impact
### Low impact
## Platform recommendations
(account-level recs from gate, capped at 3)
## Observations from investigation
Non-recommendation findings from reconciliation or investigation: deployment regressions, route-error storms, metric mismatches, and other real signals that should not become speculative performance recommendations.
Observations must not contain implementation-grade actions. If the suggested action says to enable, add, wrap, apply, move, configure, challenge, deny, or otherwise change code or project settings, the renderer must hold it back until it passes the ready-to-apply recommendation evidence bar. Customer-visible observations can ask for narrower evidence collection: inspect logs, compare deployments, check headers, or confirm cacheability.
## Investigated, no change recommended
Candidates that were checked but did not produce a supported recommendation. Use plain reasons; do not use "abstain" in customer-facing copy.
| Candidate | Why no recommendation shipped |
|---|---|
| Slow route on /docs | Detailed metrics did not support a code change |
## Not investigated in this run
This section earns the user's trust. For every metric signal we considered but didn't act on, group by candidate type and reason:
| Candidate type | Why not investigated | Targets | Count |
|---|---|---|---:|
| Low cache-hit route | hitRate 0.65 above threshold | /api/orders | 1 |
| Slow route | left for a larger run | /api/docs<br>/api/learn | 2 |
## Strengths
(what the project is doing right — caching is healthy on routes X/Y/Z; Fluid Compute is enabled; etc.)
## Data gaps
(what we couldn't measure — Observability Plus disabled means no per-route latency, etc.)
```
Common data gaps to call out when the underlying metric returned empty rows. If the metric query failed (`ok=false`), say the metric was not usable with the code; do not convert failed queries into "no measurements" or "not used" claims.
- **Core Web Vitals empty.** The Speed Insights metric returned no measurements for the 14-day window. The `cwv_poor` gate stayed dormant; no claims about LCP/INP/CLS are made.
- **ISR empty.** Project doesn't use Incremental Static Regeneration. The `isr_overrevalidation` gate stayed dormant.
- **Middleware empty.** No `middleware.ts` (or matcher excludes all observed traffic). The `middleware_heavy` gate stayed dormant.
- **Image transformations empty.** No `next/image` usage or no images served in the window.
- **BotID checks empty.** BotID is disabled — see the `platform_bot_protection` recommendation for the toggle.
- **Cold-start data near-zero.** Fluid Compute may already be enabled, or the project's traffic pattern keeps warm instances available; the `cold_start` gate evaluates the data but emits no candidate.
The "Not investigated in this run" section is critical. It comes directly from `gate.json` produced by the gate. It tells the user we considered everything; we didn't just pick the easy targets.
## Playbook selection matrix
The recommender selects 0-2 playbooks based on the project's `stack.applicationProfile` (inferred from frameworks + deps) and the top billing dimensions.
| Application profile | Likely top dimensions | Apply playbooks |
|---|---|---|
| `ai-application` (AI SDK, AI Gateway, Sandbox usage) | AI Gateway, Sandbox Active Compute, Function Duration | `playbooks/ai-application.md` |
| `ecommerce` (Stripe, Shopify, cart components) | Edge Requests, Function Duration | `playbooks/ecommerce.md` |
| `saas` (auth, dashboards, multi-tenant) | Function Duration, Bandwidth | `playbooks/saas.md` |
| `api-service` (mostly API routes, no UI) | Function Duration, Edge Requests | `playbooks/api-service.md` |
| `content-site` (blog, docs, mostly static) | Edge Requests, Image Optimization | `playbooks/content-site.md` |
| `marketing` (landing pages, A/B tests) | Edge Requests, ISR Reads | `playbooks/marketing.md` |
`ai-application` is checked first in `inferPlaybook()` — an AI-heavy SaaS or AI commerce app shares the AI playbook's billing shape (AI Gateway dominant) and gotchas, not the dashboard or cart-checkout patterns.
Playbooks shape phrasing and ordering of recommendations. They never invent claims — every rec must still trace back to verified findings.
references/support-topics/astro-edge-middleware-scope.md
---
id: astro-edge-middleware-scope
title: Astro edge middleware scope
status: active
candidateKinds: ["middleware_heavy"]
frameworks: ["astro@*"]
priority: 88
citations: ["https://vercel.com/docs/frameworks/frontend/astro", "https://docs.astro.build/en/guides/integrations-guide/vercel/"]
maxBriefChars: 800
---
## Investigation Brief
Astro middleware can run at the edge for broad request sets. If middleware volume is high, prove which paths actually need interception.
## Evidence To Check
Use middleware invocation share and top paths. Inspect adapter middleware mode, middleware source, auth/redirect logic, and whether static assets, prerendered pages, or public pages are being intercepted.
## Do Not Recommend When
Do not bypass required auth, locale, header, or routing logic. Do not move global middleware work into every page when the current scope is already minimal.
## Verification
Name the middleware share, dominant paths, current middleware mode, and exact source or config line to narrow.
references/support-topics/astro-output-mode-and-isr.md
---
id: astro-output-mode-and-isr
title: Astro output mode and ISR
status: active
candidateKinds: ["uncached_route", "rendering_candidate"]
frameworks: ["astro@*"]
priority: 90
citations: ["https://vercel.com/docs/frameworks/frontend/astro", "https://docs.astro.build/en/guides/on-demand-rendering/", "https://docs.astro.build/en/reference/configuration-reference/"]
maxBriefChars: 850
---
## Investigation Brief
Astro defaults to static output; `server` output makes pages render on demand unless route-level prerendering changes that. First decide whether the hot route truly needs SSR.
## Evidence To Check
Inspect `astro.config`, adapter options, `output`, route-level `prerender`, dynamic params, middleware, and whether the content is shared across visitors. Compare route cache result and request volume.
## Do Not Recommend When
Do not prerender or cache personalized, preview, cart, checkout, or auth-gated pages. Do not change output mode for the whole app when one route-level flag is enough.
## Verification
Name the Astro output mode, route-level prerender state, observed route signal, and exact config or page line.
references/support-topics/auth-preserving-parallelization.md
---
id: auth-preserving-parallelization
title: Authorization-preserving parallelization
status: active
candidateKinds: ["slow_route"]
frameworks: ["*"]
priority: 90
citations: ["vercel-react-best-practices:async-parallel", "vercel-react-best-practices:server-parallel-fetching"]
maxBriefChars: 900
---
## Investigation Brief
Parallelizing awaits is safe only when it does not move private data access ahead of the auth, ownership, tenant, or permission check protecting that data.
## Evidence To Check
List every awaited operation being reordered. If a private lookup currently runs after `getSession()`, an ownership query, a tenant check, or a redirect guard, prove the lookup itself enforces the same predicate before recommending `Promise.all`.
## Do Not Recommend When
Do not parallelize a private record fetch with the ownership check that authorizes that fetch. Instead, recommend combining the guard and data lookup into one query constrained by the authenticated user, tenant, or ownership key.
## Verification
The fix must preserve the sequential guard or replace it with a single authorized query. Do not promise a latency drop equal to a helper unless that helper duration was measured.
references/support-topics/bot-protection-product-guardrails.md
---
id: bot-protection-product-guardrails
title: Bot Protection product guardrails
status: active
candidateKinds: ["platform_bot_protection"]
frameworks: ["*"]
priority: 90
citations: ["https://vercel.com/docs/bot-management", "https://vercel.com/docs/vercel-firewall/vercel-waf/managed-rulesets", "https://vercel.com/docs/vercel-firewall/vercel-waf/custom-rules", "https://vercel.com/docs/botid"]
maxBriefChars: 800
---
## Investigation Brief
Bot Protection recommendations must be grounded in observed automated traffic or meaningful edge-request scale.
## Evidence To Check
Check bot bandwidth share, edge request volume, existing WAF managed rules, and whether BotID or Bot Protection is already enabled. Prefer a staged Log to Challenge or Deny path for rules whose false-positive risk is not proven.
## Do Not Recommend When
Do not recommend disabling Vercel security products to reduce cost. Do not recommend Bot Protection for quiet projects with no bot evidence.
## Verification
State the observed bot share or scale signal, current protection state, and any existing log, challenge, deny, or BotID check.
references/support-topics/build-minutes-monorepo-fanout.md
---
id: build-minutes-monorepo-fanout
title: Build Minutes monorepo fanout
status: active
candidateKinds: ["build_minutes_fanout"]
frameworks: ["*"]
scannerPatterns: ["turbo-force-bypass"]
priority: 90
citations: ["https://vercel.com/docs/monorepos", "https://vercel.com/docs/builds", "https://turborepo.dev/docs/crafting-your-repository/caching"]
maxBriefChars: 900
---
## Investigation Brief
Build Minutes climbs when commits rebuild unchanged work. Common causes: `TURBO_FORCE`, `cache: false`, missing outputs, or disabled build-skip settings.
## Evidence To Check
Confirm Build Minutes share and scanner subtype. Inspect `package.json`, `turbo.json`, outputs, `.gitignore`, `vercel.json`, and project settings. If `build` runs migrations, split them into an uncached step before recommending Turbo build caching.
## Do Not Recommend When
Skip under 5% bill share with no scanner finding. Skip single-project repos and intentional CI-only force flags. Do not recommend `ignoreCommand` from repo grep alone; dashboard-only skip-unaffected may be better.
## Verification
Name the offending file and pattern. Recommend only the verified fix: cache a pure build task, add generated `outputs`, enable skip-unaffected builds, or add `ignoreCommand` only when needed.
references/support-topics/cache-components-static-shell-boundaries.md
---
id: cache-components-static-shell-boundaries
title: Cache Components static shell boundaries
status: active
candidateKinds: ["rendering_candidate"]
frameworks: ["next@>=16.0.0"]
priority: 94
citations: ["https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents", "https://nextjs.org/docs/app/getting-started/caching", "https://nextjs.org/docs/app/guides/migrating-to-cache-components"]
maxBriefChars: 900
---
## Investigation Brief
On Next.js 16 with Cache Components, avoid older segment-config advice. The right question is whether the route can keep a static shell while dynamic data moves behind explicit cached or runtime boundaries.
## Evidence To Check
Check `cacheComponents`, `use cache`, `cacheLife`, request-time APIs, Suspense boundaries, and scanner evidence such as `force-dynamic` or `headers-in-page`.
## Do Not Recommend When
Do not suggest `dynamic`, `revalidate`, or `fetchCache` as the primary fix when Cache Components is enabled. Do not cache request-personalized content.
## Verification
Name the Next.js version, Cache Components state, dynamic trigger, and the exact boundary or directive that can change.
references/support-topics/cache-components-suspense-dedupe-pitfall.md
---
id: cache-components-suspense-dedupe-pitfall
title: Cache Components Suspense dedupe pitfall
status: active
candidateKinds: ["cache_components_suspense_dedupe"]
frameworks: ["next@>=16.0.0"]
scannerPatterns: ["cache-components-suspense-dedupe"]
priority: 87
citations: ["https://nextjs.org/docs/app/api-reference/directives/use-cache", "https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents", "https://nextjs.org/docs/app/guides/migrating-to-cache-components"]
maxBriefChars: 900
---
## Investigation Brief
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 and ISR write churn.
## Evidence To Check
Confirm the scanner finding's repeated fetch URL or helper name. Verify the call sites are within the same route segment and inside distinct `<Suspense>` boundaries. Cross-reference `fnDurationP95ByRoute` and `isrWritesByRoute` for the owning route.
## Do Not Recommend When
Skip if the repeated call is intentional (different parameters, different intent). Skip if the duplicate is in a single component body where in-request memoization already applies.
## Verification
Name the duplicated call, count, and either: (a) the page-level promise to hoist or (b) the function to move to `'use cache: remote'`.
references/support-topics/cdn-cache-auth-safety.md
---
id: cdn-cache-auth-safety
title: CDN cache auth safety
status: active
candidateKinds: ["uncached_route", "cache_header_gap"]
frameworks: ["*"]
priority: 100
citations: ["https://vercel.com/docs/caching/cdn-cache", "https://vercel.com/docs/caching/cache-control-headers", "https://vercel.com/docs/project-configuration"]
maxBriefChars: 900
---
## Investigation Brief
Treat edge caching as a safety question first. The route must be a public, cacheable GET path before a shared-cache recommendation is allowed.
## Evidence To Check
Use `methodDistribution`, `cacheBreakdown`, and headers. Before `s-maxage`, rule out cookies, sessions, authorization, draft state, and user-specific data.
## Do Not Recommend When
Do not cache mutations, dashboards, account data, request-personalized responses, or routes whose value changes per viewer. Do not mix `private` with shared-cache directives.
## Verification
Name GET share, cache mix, file line, and policy: mechanism, scope, TTL/freshness, and `Vary`. If the right policy is `no-store`, emit no-change/observation.
references/support-topics/cold-start-initialization-bundle.md
---
id: cold-start-initialization-bundle
title: Cold-start initialization and bundle weight
status: active
candidateKinds: ["cold_start"]
frameworks: ["*"]
priority: 92
citations: ["https://vercel.com/docs/functions/debug-slow-functions", "https://vercel.com/docs/functions/limitations", "https://vercel.com/docs/functions/runtimes"]
maxBriefChars: 850
---
## Investigation Brief
Cold-start candidates need a code-path check, not only a project-setting check. First prove whether cold requests are paying for imports, module-scope setup, runtime choice, or dependency weight.
## Evidence To Check
Use `startTypeSplit`, `coldVsWarmLatencyP95`, and `coldByDeployment`. In source, inspect module-scope SDK setup, database/client construction, top-level network calls, heavy dependencies, runtime exports, and deployment-local changes.
## Do Not Recommend When
Do not blame cold starts when warm requests are similarly slow. Do not recommend keep-warm traffic or more memory before proving initialization or runtime pressure.
## Verification
Name the cold-start share, cold-vs-warm gap, and exact initialization, dependency, or runtime line that explains it.
references/support-topics/core-web-vitals-client-bottlenecks.md
---
id: core-web-vitals-client-bottlenecks
title: Core Web Vitals client bottlenecks
status: active
candidateKinds: ["cwv_poor"]
frameworks: ["*"]
priority: 90
citations: ["https://vercel.com/docs/speed-insights", "https://web.dev/articles/vitals", "https://web.dev/articles/optimize-lcp", "https://web.dev/articles/optimize-inp", "https://web.dev/articles/optimize-cls"]
maxBriefChars: 850
---
## Investigation Brief
Core Web Vitals candidates need metric-specific investigation. LCP, INP, and CLS usually have different causes and fixes.
## Evidence To Check
Use the poor metric in the deep dive first. For LCP, inspect server response and critical media. For INP, inspect heavy client JavaScript and interaction handlers. For CLS, inspect dimensions, fonts, and injected content.
## Do Not Recommend When
Do not emit a generic “improve Web Vitals” recommendation. Do not optimize a metric that is not poor for this route.
## Verification
Name the poor p75 metric, its value, and the exact source mechanism behind that metric.
references/support-topics/database-egress-pooling-region.md
---
id: database-egress-pooling-region
title: Database region and connection pressure
status: active
candidateKinds: ["slow_route"]
frameworks: ["*"]
priority: 60
citations: ["https://vercel.com/docs/regions", "https://vercel.com/docs/functions", "https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package", "https://vercel.com/docs/functions/limitations"]
maxBriefChars: 800
---
## Investigation Brief
Only recommend database or region changes when source and metrics both point to downstream I/O rather than in-process compute.
## Evidence To Check
Compare `cpu.p95` with `latency.p95`, then inspect database awaits, query fan-out, connection creation, pool lifecycle handling, and configured regions in project files.
## Do Not Recommend When
Do not name a database provider, pooling product, or region change unless the repo and project config prove it applies.
## Verification
Tie the finding to the observed wall-clock gap and the exact query, pool, or region configuration line.
references/support-topics/dynamic-rendering-traps.md
---
id: dynamic-rendering-traps
title: Dynamic rendering traps
status: active
candidateKinds: ["rendering_candidate"]
frameworks: ["next@>=13.0.0"]
priority: 90
citations: ["https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config", "https://nextjs.org/docs/app/api-reference/functions/generate-static-params", "https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering"]
maxBriefChars: 850
---
## Investigation Brief
Rendering candidates are only actionable when the dynamic behavior is accidental. First prove that the route can be static, ISR, or partially static.
## Evidence To Check
Inspect `dynamic`, `revalidate`, `generateStaticParams`, route params, and dynamic APIs such as request headers or cookies. Check whether the dynamic call is in a layout, because that can affect a larger route tree.
## Do Not Recommend When
Do not remove dynamic rendering for auth, personalization, draft mode, per-request redirects, or request-specific data.
## Verification
The recommendation must cite the dynamic trigger and explain why the target route can tolerate static or ISR behavior.
references/support-topics/external-api-critical-path-platform.md
---
id: external-api-critical-path-platform
title: Cross-framework external API critical path
status: active
candidateKinds: ["external_api_slow"]
frameworks: ["*"]
priority: 86
citations: ["https://vercel.com/docs/functions/debug-slow-functions", "https://vercel.com/docs/caching/runtime-cache", "https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package"]
maxBriefChars: 850
---
## Investigation Brief
External API candidates are actionable only when the slow hostname is on a customer route's critical path. Prove the route waits on it before suggesting a cache, payload, or post-response change.
## Evidence To Check
Use hostname latency, caller routes, transfer bytes, and source awaits. Check sequential calls, overbroad payloads, repeated shared data, and side effects that can move after the response.
## Do Not Recommend When
Do not cache mutations, secrets, per-user responses, or unknown freshness contracts. Do not blame Vercel runtime when the upstream owns the latency.
## Verification
Name the hostname, caller route, observed p75/p95 or bytes, and exact await or fetch line that blocks the response.
references/support-topics/external-api-critical-path.md
---
id: external-api-critical-path
title: External API critical path
status: active
candidateKinds: ["external_api_slow"]
frameworks: ["next@>=13.0.0"]
priority: 90
citations: ["vercel-react-best-practices:async-parallel", "vercel-react-best-practices:server-parallel-fetching", "vercel-react-best-practices:server-cache-react"]
maxBriefChars: 850
---
## Investigation Brief
For external API candidates, identify the customer route that waits on the slow hostname and whether the call is on the critical path.
## Evidence To Check
Use callers-by-route evidence, transfer size, and source awaits. Check whether the upstream call can run in parallel, be cached safely, be reduced in payload size, or move after response.
## Do Not Recommend When
Do not cache mutations, secrets, per-user responses, or upstream calls whose freshness requirement is unknown.
## Verification
Name the hostname, caller route, p75 or p95 latency, and the exact source line that waits on the call.
references/support-topics/fast-data-transfer-payloads.md
---
id: fast-data-transfer-payloads
title: Fast Data Transfer payloads
status: active
candidateKinds: ["uncached_route"]
frameworks: ["*"]
priority: 65
citations: ["https://vercel.com/docs/manage-cdn-usage", "https://vercel.com/docs/caching/cdn-cache"]
maxBriefChars: 900
---
## Investigation Brief
When uncached routes carry high bandwidth, check payload shape before recommending only cache headers. Fast Data Transfer includes the bytes transferred by requests and responses; compare compressed response sizes to the signal, not raw JSON.
## Evidence To Check
Use `bandwidthByCache`, response size, and source serialization. Look for unbounded JSON, large embedded objects, static files through functions, missing pagination.
## Do Not Recommend When
Do not shrink payloads without identifying fields or assets that are unnecessary for the route’s response.
## Verification
Tie the finding to observed bytes, cache result mix, and the exact response line. A "large payload" claim must reflect post-compression bytes — the unit FDT meters.
references/support-topics/fluid-compute-caveats.md
---
id: fluid-compute-caveats
title: Fluid compute caveats
status: active
candidateKinds: ["platform_fluid_compute", "cold_start"]
frameworks: ["*"]
priority: 80
citations: ["https://vercel.com/docs/fluid-compute"]
maxBriefChars: 900
---
## Investigation Brief
Fluid compute is a project-level lever. Use it when the setting is off and metrics show cold-start or warm-instance reuse pressure. Fluid can handle multiple invocations in one function instance; avoid per-request state in module scope.
## Evidence To Check
Check project facts, `startTypeSplit`, cold-vs-warm latency, and routes carrying the cold-start share. When enabling Fluid, audit module-state hazards Fluid surfaces (not creates): module-scoped mutable state, lazy singletons holding per-user data, globals keyed on per-request inputs.
## Do Not Recommend When
Do not recommend enabling fluid compute when project facts say it is already on. Do not frame as a file-level code fix.
## Verification
State project setting, cold-start rate or fallback slow-route signal, affected route concentration. If enabling, call out module-state audit as follow-up.
references/support-topics/function-duration-io-and-after.md
---
id: function-duration-io-and-after
title: Function duration, I/O, and post-response work
status: active
candidateKinds: ["slow_route"]
frameworks: ["next@>=15.0.0"]
priority: 75
citations: ["https://nextjs.org/docs/app/api-reference/functions/after", "vercel-react-best-practices:async-parallel", "vercel-react-best-practices:server-after-nonblocking"]
maxBriefChars: 850
---
## Investigation Brief
When wall-clock latency is much higher than CPU time, check critical-path awaits before blaming rendering or compute.
## Evidence To Check
Compare `cpu.p95`, `ttfb.p95`, and `latency.p95`. In source, separate dependent awaits from independent awaits, and identify analytics, logging, or notification work that can run after the response.
## Do Not Recommend When
Do not wrap dependent operations in `Promise.all`. Do not replace `Promise.allSettled` when partial failure handling is intentional.
## Verification
Name the awaits that can move, the work that can run post-response, and the observed CPU-vs-wall-clock gap.
references/support-topics/function-invocation-reduction.md
---
id: function-invocation-reduction
title: Function invocation reduction
status: active
candidateKinds: ["slow_route"]
frameworks: ["next@>=13.0.0"]
priority: 70
citations: ["https://react.dev/reference/react/cache", "vercel-react-best-practices:server-parallel-fetching", "vercel-react-best-practices:server-cache-react"]
maxBriefChars: 850
---
## Investigation Brief
For slow routes, prove duplicated in-request work in the listed files before recommending consolidation or memoization.
## Evidence To Check
Look for repeated awaits, duplicate fetches, same-app route handler calls, and helpers that run more than once per request.
## Do Not Recommend When
Do not collapse endpoints called independently by different clients. Do not persistently cache user-specific data. Do not recommend `Promise.all` for CPU-bound or compile-bound work unless trace/span evidence shows wait time to overlap. High `cpu.p95` near `latency.p95` is a warning sign, not proof of a latency win.
## Verification
Quote duplicated call sites with `latency.p95`, `cpu.p95`, or request-count evidence. If the fix overlaps awaits, cite measured helper/span timing or state the impact is unmeasured.
references/support-topics/function-region-misconfiguration-ttfb.md
---
id: function-region-misconfiguration-ttfb
title: Function region misconfiguration (TTFB)
status: active
candidateKinds: ["region_misconfig"]
frameworks: ["*"]
scannerPatterns: ["region-pin-in-config"]
priority: 85
citations: ["https://vercel.com/docs/functions/configuring-functions/region", "https://vercel.com/docs/regions"]
maxBriefChars: 950
---
## Investigation Brief
A single function region is pinned. Per-region TTFB data is unavailable today (`evidence.dataGap`); treat as an audit prompt — validate the pinned region against user geo and data-source location before recommending changes.
## Evidence To Check
Scanner subtype (`vercel-json-single`, `segment-preferred`) and pinned regions. Cross-check Speed Insights TTFB and country analytics for traffic geo. Locate the data source — proximity to it often wins on cache-miss paths.
## Do Not Recommend When
Skip if TTFB is healthy across countries. Skip if pinned intentionally for data proximity. Skip on small projects (<20 routes). Do not propose multi-region without confirming the data layer is reachable without cross-region egress.
## Verification
Name pinned region(s), traffic geo, data-source location, and a specific call: relocate, expand, or keep with a TTFB monitor.
references/support-topics/image-optimization-cost-control.md
---
id: image-optimization-cost-control
title: Image optimization cost control
status: active
candidateKinds: ["image_optimization"]
frameworks: ["*"]
priority: 90
citations: ["https://vercel.com/docs/image-optimization", "https://vercel.com/docs/image-optimization/managing-image-optimization-costs", "https://vercel.com/docs/image-optimization/limits-and-pricing"]
maxBriefChars: 850
---
## Investigation Brief
Image recommendations should distinguish real user-facing image work from wasteful transformations.
## Evidence To Check
Inspect the sampled files for raw image tags, dimensions, remote sources, repeated transforms, source image limits, icons, SVGs, GIFs, and existing framework image components.
## Do Not Recommend When
Do not route tiny icons, SVG UI assets, or animated GIFs through image optimization just because they are images. Do not change remote-source policy without checking the existing config.
## Verification
Name the image files or components, current rendering path, and the metric or scanner evidence that makes optimization material.
references/support-topics/isr-revalidation-static-generation.md
---
id: isr-revalidation-static-generation
title: ISR revalidation and static generation
status: active
candidateKinds: ["isr_overrevalidation"]
frameworks: ["next@>=13.4.0"]
priority: 95
citations: ["https://vercel.com/docs/incremental-static-regeneration", "https://nextjs.org/docs/app/api-reference/functions/revalidateTag", "https://nextjs.org/docs/app/api-reference/functions/revalidatePath"]
maxBriefChars: 1000
---
## Investigation Brief
For ISR over-revalidation, the goal is to reduce unnecessary regeneration work without making content stale beyond the product’s tolerance.
## Evidence To Check
Compare ISR writes to reads, then inspect the route’s `revalidate`, `cacheLife()`, tag invalidation, and content update path. Look for very short timer revalidation on routes where updates are event-driven. If recommending `cacheLife()` or `cacheTag()` for tagged content, prove the exact tags are invalidated by `revalidateTag()` or `updateTag()`; near-matches do not count.
## Do Not Recommend When
Do not lengthen revalidation for inventory, pricing, auth, or other user-critical freshness without source evidence that stale content is acceptable. Do not claim existing CMS or webhook invalidation unless the matching invalidation call or config is in the allowed files.
## Verification
Tie the fix to the observed ISR writes per read and the line that controls revalidation or on-demand invalidation.
references/support-topics/middleware-proxy-edge-cost.md
---
id: middleware-proxy-edge-cost
title: Middleware edge cost
status: active
candidateKinds: ["middleware_heavy"]
frameworks: ["next@>=12.0.0"]
priority: 90
citations: ["https://nextjs.org/docs/app/building-your-application/routing/middleware", "https://vercel.com/docs/routing-middleware"]
maxBriefChars: 850
---
## Investigation Brief
Middleware recommendations should reduce unnecessary interception, not remove required request handling.
## Evidence To Check
Use `topMiddlewarePaths` and the matcher config. Confirm which paths need auth, rewrites, headers, or locale handling. Check whether static assets, images, or routes with no middleware need are being matched.
## Do Not Recommend When
Do not narrow the matcher in a way that bypasses required auth or routing behavior. Do not move middleware work into every route if the current matcher is already scoped.
## Verification
State the current middleware share, the dominant matched paths, and the exact matcher line to change.
references/support-topics/next-fetch-revalidate-floor.md
---
id: next-fetch-revalidate-floor
title: Next.js fetch revalidation floor
status: active
candidateKinds: ["uncached_route", "isr_overrevalidation"]
frameworks: ["next@>=13.0.0"]
priority: 88
citations: ["https://nextjs.org/docs/app/api-reference/functions/fetch", "https://nextjs.org/docs/app/building-your-application/caching"]
maxBriefChars: 850
---
## Investigation Brief
Next.js `fetch` options can set the route's effective cache floor. Low `revalidate`, `revalidate: 0`, or `cache: 'no-store'` can explain uncached traffic and excessive ISR work.
## Evidence To Check
Inspect route-tree `fetch` calls. Compare route revalidation with per-fetch `cache`, `next.revalidate`, tags, dynamic APIs, and duplicated URLs with conflicting options.
## Do Not Recommend When
Do not raise freshness windows for pricing, inventory, auth, draft, or user-specific data unless the source proves stale reads are acceptable.
## Verification
Name the observed cache or ISR signal, the lowest cache setting that controls the route, and the exact fetch line to change.
references/support-topics/next-font-cls-self-hosting.md
---
id: next-font-cls-self-hosting
title: Next.js font CLS guardrail
status: active
candidateKinds: ["cwv_poor"]
frameworks: ["next@>=13.2.0"]
metrics: ["CLS"]
priority: 86
citations: ["https://nextjs.org/docs/app/api-reference/components/font", "https://web.dev/articles/optimize-cls"]
maxBriefChars: 800
---
## Investigation Brief
For poor CLS, check fonts only when the route actually loads external font CSS or swaps text after render.
## Evidence To Check
Inspect layouts and global styles for external font links, CSS imports, custom font-face rules, late-loading font classes, and whether `next/font` is already used.
## Do Not Recommend When
Do not migrate fonts when CLS is caused by images, ads, embeds, or injected UI. Do not suggest `next/font` for unsupported Next.js versions.
## Verification
Name the CLS value, font-loading mechanism, and the exact layout or stylesheet line to change.
references/support-topics/next-heavy-ui-lazy-load-boundaries.md
---
id: next-heavy-ui-lazy-load-boundaries
title: Next.js heavy UI lazy-load boundaries
status: active
candidateKinds: ["cwv_poor"]
frameworks: ["next@*"]
metrics: ["LCP", "INP"]
priority: 82
citations: ["https://nextjs.org/docs/app/guides/lazy-loading", "https://web.dev/articles/optimize-inp"]
maxBriefChars: 850
---
## Investigation Brief
Heavy above-the-fold or rarely used UI can hurt LCP and INP when it ships too much JavaScript on first load. Look for concrete route-local UI, not generic bundle advice.
## Evidence To Check
Inspect client components, menus, search overlays, personalization widgets, maps, editors, and large imported libraries. Check whether they can load on interaction, viewport, or route entry with `next/dynamic` or dynamic import.
## Do Not Recommend When
Do not lazy-load essential above-the-fold content needed for initial meaning or accessibility. Do not use `ssr: false` from a Server Component.
## Verification
Name the poor metric, heavy UI boundary, imported library or component, and exact line to split.
references/support-topics/next-image-lcp-preload-sizes.md
---
id: next-image-lcp-preload-sizes
title: Next.js image LCP preload and sizes
status: active
candidateKinds: ["cwv_poor"]
frameworks: ["next@*"]
metrics: ["LCP"]
priority: 86
citations: ["https://nextjs.org/docs/app/api-reference/components/image", "https://web.dev/articles/optimize-lcp"]
maxBriefChars: 850
---
## Investigation Brief
For poor LCP, identify whether the LCP element is an image before touching unrelated JavaScript. Hero images need correct sizing, priority behavior, and source-cache hygiene.
## Evidence To Check
Inspect above-the-fold media for `next/image`, `fill` without `sizes`, deprecated `priority` on Next.js 16, missing `preload` or `fetchPriority`, oversized dimensions, and remote-image TTL/source behavior.
## Do Not Recommend When
Do not preload multiple possible LCP images or route tiny icons/SVG UI assets through image optimization. Do not change quality or TTL without checking source-update semantics.
## Verification
Name the LCP value, image element or component, current props/config, and the exact line to change.
references/support-topics/next-route-handler-get-cache-defaults.md
---
id: next-route-handler-get-cache-defaults
title: Next.js Route Handler GET cache defaults
status: active
candidateKinds: ["uncached_route", "cache_header_gap"]
frameworks: ["next@>=15.0.0"]
priority: 91
citations: ["https://nextjs.org/docs/app/api-reference/file-conventions/route", "https://vercel.com/docs/caching/cdn-cache"]
maxBriefChars: 850
---
## Investigation Brief
On Next.js 15+, GET Route Handlers are dynamic by default. For hot public GET handlers, verify whether uncached behavior is intentional before recommending cache headers or route config.
## Evidence To Check
Use method share, cache result, and source. Check `GET`, `revalidate`, `dynamic`, request headers, cookies, auth, query params, and response `Cache-Control`.
## Do Not Recommend When
Do not cache POST-style handlers, webhooks, per-user APIs, streaming responses, search requests with user-specific params, or handlers that read auth/cookies.
## Verification
Name the Next.js version, GET share, cache result mix, and the exact handler or header line that makes public caching safe.
references/support-topics/next-script-third-party-strategy.md
---
id: next-script-third-party-strategy
title: Next.js third-party script strategy
status: active
candidateKinds: ["cwv_poor"]
frameworks: ["next@*"]
metrics: ["LCP", "INP"]
priority: 85
citations: ["https://nextjs.org/docs/app/api-reference/components/script", "https://web.dev/articles/optimize-inp"]
maxBriefChars: 850
---
## Investigation Brief
Third-party scripts are only actionable when they line up with the poor metric and route. For LCP or INP, prove a specific script blocks critical rendering, hydration, or interaction.
## Evidence To Check
Inspect `next/script`, raw `<script>`, tag managers, chat widgets, analytics, and consent code. Check `beforeInteractive`, `afterInteractive`, `lazyOnload`, and whether the script is route-local or global.
## Do Not Recommend When
Do not move required bot detection, consent, auth, or payment scripts later without product evidence. Do not recommend `worker` for App Router.
## Verification
Name the poor metric, script source, current strategy, and the exact route or layout line to change.
references/support-topics/nextjs-version-cache-semantics.md
---
id: nextjs-version-cache-semantics
title: Next.js cache semantics by version
status: active
candidateKinds: ["uncached_route"]
frameworks: ["next@>=15.0.0"]
priority: 85
citations: ["https://nextjs.org/docs/app/api-reference/directives/use-cache", "https://nextjs.org/docs/app/api-reference/functions/cacheLife", "https://nextjs.org/docs/app/building-your-application/caching"]
maxBriefChars: 800
---
## Investigation Brief
On Next.js 15+, match the fix to the cache primitive already in use.
## Evidence To Check
Check `'use cache'`, `cacheLife`, `cacheTag`, `fetch` cache options, route handlers, and dynamic APIs.
## Do Not Recommend When
Do not suggest APIs outside the detected Next.js version. Do not claim `cacheLife()` emits CDN `Cache-Control` headers or that missing `cacheLife()` alone makes a `'use cache'` route run per request. Omitted `cacheLife()` calls use the default profile.
## Verification
Name the detected Next.js version and exact cache primitive or route header.
references/support-topics/not-found-catchall-request-waste.md
---
id: not-found-catchall-request-waste
title: Not-found and catch-all request waste
status: active
candidateKinds: ["uncached_route"]
frameworks: ["*"]
routePatterns: ["(^|/)404$", "not-found", "\\[\\.\\.\\."]
priority: 92
citations: ["https://vercel.com/docs/routing/", "https://vercel.com/docs/redirects/bulk-redirects/", "https://vercel.com/docs/vercel-firewall/vercel-waf/custom-rules", "https://vercel.com/docs/vercel-firewall/vercel-waf/managed-rulesets"]
maxBriefChars: 850
---
## Investigation Brief
High-volume 404 or catch-all traffic is often request waste. First determine whether the traffic is legacy URLs, bots, broken links, or a real product route.
## Evidence To Check
Use route volume, method share, cache result, bot share, and top request paths. Inspect redirects, rewrites, catch-all routes, sitemap/robots output, and any WAF rules already logging or blocking the pattern.
## Do Not Recommend When
Do not block or redirect legitimate product routes, search crawlers, or unknown traffic without a log-mode validation path. Do not replace a useful 404 page with a blanket rewrite.
## Verification
Name the dominant bad path pattern, observed request or bot volume, and the redirect, routing, or WAF rule that would stop the wasted function path.
references/support-topics/nuxt-route-rules-cache-isr.md
---
id: nuxt-route-rules-cache-isr
title: Nuxt routeRules cache and ISR
status: active
candidateKinds: ["uncached_route", "isr_overrevalidation", "rendering_candidate"]
frameworks: ["nuxt@>=3.0.0"]
priority: 90
citations: ["https://vercel.com/docs/frameworks/full-stack/nuxt", "https://nuxt.com/docs/4.x/api/utils/define-route-rules", "https://nuxt.com/docs/4.x/guide/concepts/rendering"]
maxBriefChars: 850
---
## Investigation Brief
For Nuxt on Vercel, route-level caching usually belongs in `routeRules`. Match the lever to the route: prerender for static pages, ISR for shared content, and SSR for request-specific views.
## Evidence To Check
Inspect `nuxt.config`, inline route rules, server routes, pages, auth/session reads, and observed cache or ISR read/write patterns. Verify whether the route should be Vercel cache-backed ISR rather than generic SWR.
## Do Not Recommend When
Do not cache authenticated, cart, checkout, preview, or per-user routes. Do not add routeRules without proving the route is public and the freshness window is acceptable.
## Verification
Name the observed route signal, current routeRule or missing rule, chosen cache mode, and exact config line.
references/support-topics/observability-events-cost-attribution.md
---
id: observability-events-cost-attribution
title: Observability Events cost attribution
status: active
candidateKinds: ["observability_events_attribution"]
frameworks: ["*"]
priority: 92
citations: ["https://vercel.com/docs/observability/observability-plus", "https://vercel.com/docs/alerts"]
maxBriefChars: 900
---
## Investigation Brief
Observability Events is the metered SKU under Observability Plus. When the current bill shows a large Observability Events share, event volume is the lever. Reduce upstream: lift cache hit rate, narrow middleware matchers, and reduce unnecessary custom-span cardinality.
## Evidence To Check
Verify the share from `usage.services`. Cross-reference `requestsByRouteCache`, `middlewareCount`, external API span counts, and third-party tracing (`tracesSampleRate=1`).
## Do Not Recommend When
Skip below 15% share. Skip when cache hit rate is already >90% across hot routes — the lever is elsewhere. Do not propose sampling unless the specific metered signal has a documented sampling control.
## Verification
Name the share, upstream drivers, and concrete remediation per driver, not generic "reduce events".
references/support-topics/post-response-work-waituntil.md
---
id: post-response-work-waituntil
title: Post-response work with waitUntil
status: active
candidateKinds: ["slow_route", "external_api_slow"]
frameworks: ["next@<15.0.0", "sveltekit@*", "astro@*", "nuxt@*", "unknown@*"]
priority: 78
citations: ["https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package"]
maxBriefChars: 800
---
## Investigation Brief
For stacks without Next.js `after()`, check whether non-critical work can run after the response instead of extending user-visible latency.
## Evidence To Check
Inspect the listed route for analytics, logging, notifications, cache warming, metrics, or webhook side effects that happen after the response data is ready.
## Do Not Recommend When
Do not move work that decides the response, must fail the request, changes visible state synchronously, or needs a durable retry guarantee.
## Verification
Name the blocking side effect, the observed latency or upstream signal, and the exact line that can move behind `waitUntil`.
references/support-topics/README.md
# Support Topics
Support topics are small, candidate-scoped investigation guardrails injected into sub-agent briefs.
They are not recommendations, gates, scanners, or broad documentation. A topic tells the investigator what evidence to check, what false positives to avoid, and when to abstain for one class of candidate.
## Add A Topic
Add one file: `references/support-topics/<id>.md`.
The filename must match the `id`. Frontmatter uses a strict subset of YAML: one `key: value` per line, arrays as JSON arrays.
```md
---
id: cdn-cache-auth-safety
title: CDN cache auth safety
status: active
candidateKinds: ["uncached_route", "cache_header_gap"]
frameworks: ["*"]
priority: 90
citations: ["https://vercel.com/docs/caching/cdn-cache"]
maxBriefChars: 900
---
## Investigation Brief
...
## Evidence To Check
...
## Do Not Recommend When
...
## Verification
...
```
## Rules
- Every active topic must cite only URLs or skill-rule refs already present in `references/docs-library.json`.
- Use `candidateKinds` to keep the topic narrow. Use `"*"` only for workflow/protocol topics that truly apply to every candidate.
- Use optional `metrics` only when a topic applies to a specific candidate metric, such as `["LCP"]`, `["INP"]`, or `["CLS"]` for Core Web Vitals.
- Use optional `routePatterns` as JavaScript regex source strings when a topic should appear only for specific candidate routes, such as `["(^|/)404$"]`.
- Keep the body below `maxBriefChars`; the brief renderer caps selected topics before they reach the sub-agent.
- Put URLs in frontmatter only. Topic bodies should describe checks and guardrails, not cite new sources.
- Do not include internal repository paths, service names, pricing tables, exact savings claims, or framework APIs without version gating.
references/support-topics/route-error-durable-offload.md
---
id: route-error-durable-offload
title: Durable offload for timeout-heavy routes
status: active
candidateKinds: ["route_errors"]
frameworks: ["*"]
priority: 84
citations: ["https://vercel.com/docs/workflow", "https://workflow-sdk.dev/docs/foundations/starting-workflows", "https://workflow-sdk.dev/docs/foundations/workflows-and-steps", "https://vercel.com/docs/queues", "https://vercel.com/docs/functions/limitations"]
maxBriefChars: 850
---
## Investigation Brief
Timeout-heavy routes often need a job boundary, not a higher limit. Workflow fits durable multi-step work that can continue after the response; return a run ID instead of waiting on `returnValue`.
## Evidence To Check
Use `errorStatusPattern`, `errorCodes`, and source flow. Look for fan-out, polling, batch work, AI jobs, uploads, sleeps, approval, multi-step side effects. If Workflow is already used, check whether the route waits or streams progress.
## Do Not Recommend When
Do not offload work that must finish before responding. Do not claim savings from offload alone: Workflow Steps/Storage bill separately, and invoked functions still use compute billing.
## Verification
Name the timeout/error class, long-running operation, post-enqueue response contract, and queue or workflow boundary that preserves semantics.
references/support-topics/route-error-runtime-limits.md
---
id: route-error-runtime-limits
title: Route errors and runtime limits
status: active
candidateKinds: ["route_errors"]
frameworks: ["*"]
priority: 90
citations: ["https://vercel.com/docs/functions", "https://vercel.com/docs/functions/limitations", "https://vercel.com/docs/cli/inspect"]
maxBriefChars: 850
---
## Investigation Brief
Route error candidates are reliability findings with cost impact. Determine whether the failures are app exceptions, timeouts, payload limits, or deployment-specific regressions.
## Evidence To Check
Use `errorStatusPattern`, `errorCodes`, and `errorsByDeployment`. In source, inspect the path most likely to throw, time out, or exceed a platform limit.
## Do Not Recommend When
Do not frame high 5xx volume as a performance tuning issue. Do not suggest increasing limits before proving the route needs more headroom.
## Verification
Name the error class, deployment concentration if present, and the file line that triggers or fails to handle it.
references/support-topics/runtime-cache-reusable-data.md
---
id: runtime-cache-reusable-data
title: Runtime Cache for reusable server data
status: active
candidateKinds: ["slow_route", "external_api_slow"]
frameworks: ["*"]
priority: 84
citations: ["https://vercel.com/docs/caching/runtime-cache", "https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package"]
maxBriefChars: 850
---
## Investigation Brief
Runtime Cache is only useful when the same server-side result is reused across requests. Treat it as a measured alternative when CDN response caching is unsafe or incomplete.
## Evidence To Check
Use p75/p95 latency, call count, caller routes, and transfer bytes. In source, identify database queries, external API calls, or expensive computations that return the same result for many viewers.
## Do Not Recommend When
Skip per-user data, mutations, secrets, one-off requests, or unknown freshness. Skip Runtime Cache when CDN caching solves the route. For Next with Cache Components, check `use cache: remote` first; use Runtime Cache only as a justified fallback.
## Verification
Name the reusable data, observed route or hostname pressure, required freshness window, and the exact call site to wrap.
references/support-topics/sveltekit-isr-prerender-safety.md
---
id: sveltekit-isr-prerender-safety
title: SvelteKit ISR and prerender safety
status: active
candidateKinds: ["uncached_route", "isr_overrevalidation"]
frameworks: ["sveltekit@*"]
priority: 90
citations: ["https://vercel.com/docs/frameworks/full-stack/sveltekit", "https://svelte.dev/docs/kit/adapter-vercel", "https://svelte.dev/docs/kit/page-options"]
maxBriefChars: 850
---
## Investigation Brief
For SvelteKit, the right lever is often `prerender` or adapter ISR on public consumer pages. First prove every visitor can safely see the same response for the configured interval.
## Evidence To Check
Inspect `+page`, `+page.server`, `+server`, layouts, `prerender`, `ssr`, and adapter `isr` config. Compare route cache results, ISR writes, and whether the route reads cookies, auth, or per-user locals.
## Do Not Recommend When
Do not use ISR for dashboards, carts, checkout, account data, drafts, or any route whose output varies per visitor. Do not add ISR when `prerender = true` already makes it irrelevant.
## Verification
Name the route, current SvelteKit page option or adapter config, observed cache or ISR signal, and the exact file line to change.
references/support-topics/sveltekit-split-cold-start-tradeoff.md
---
id: sveltekit-split-cold-start-tradeoff
title: SvelteKit split function cold-start tradeoff
status: active
candidateKinds: ["cold_start", "slow_route"]
frameworks: ["sveltekit@*"]
priority: 82
citations: ["https://vercel.com/docs/frameworks/full-stack/sveltekit", "https://svelte.dev/docs/kit/adapter-vercel"]
maxBriefChars: 800
---
## Investigation Brief
SvelteKit bundles routes together by default to avoid excessive cold starts. Treat `split: true` as a targeted tradeoff, not a blanket optimization.
## Evidence To Check
Use cold-start share, cold-vs-warm latency, deployment concentration, and source bundle pressure. Check adapter options and whether a large dependency belongs to one route or the whole app.
## Do Not Recommend When
Do not split every route without evidence of function size pressure or route-local initialization cost. Do not split if cold starts are already the dominant problem.
## Verification
Name the cold-start signal, route or dependency that motivates the split, and the exact adapter config line.
references/support-topics/usage-spike-triage.md
---
id: usage-spike-triage
title: Usage spike triage
status: active
candidateKinds: ["usage_spike_triage"]
frameworks: ["*"]
priority: 95
citations: ["https://vercel.com/docs/alerts", "https://vercel.com/docs/spend-management", "https://vercel.com/docs/bot-management"]
maxBriefChars: 950
---
## Investigation Brief
A single-day or single-SKU spike needs cause before fix. Branches: bot or AI crawler on a cacheable route, viral moment, pricing-model migration, or code regression.
## Evidence To Check
Confirm SKU and day from `usage.breakdown.data`. Cross-check firewall/bot data, traffic curve, SKU rename timing, and deploy log around the spike day. Spend Management and Alerts are monitoring tools; they do not replace finding the traffic or deploy cause.
## Do Not Recommend When
Do not propose a code fix until the branch is identified. Do not rate-limit a viral moment or revert a deploy for third-party crawler traffic.
## Verification
Name SKU, day, value, window mean, branch, and one supporting datum.
references/support-topics/use-cache-date-stamp-isr-write-amplifier.md
---
id: use-cache-date-stamp-isr-write-amplifier
title: "'use cache' date-stamp ISR write amplifier"
status: active
candidateKinds: ["use_cache_date_stamp"]
frameworks: ["next@>=15.0.0"]
scannerPatterns: ["use-cache-date-stamp"]
priority: 88
citations: ["https://nextjs.org/docs/app/api-reference/directives/use-cache", "https://nextjs.org/docs/app/api-reference/functions/cacheLife"]
maxBriefChars: 900
---
## Investigation Brief
`'use cache'` keys on argument identity and prerender output. A `new Date()`, `Date.now()`, or `Math.random()` baked into the cached output forces a fresh ISR write on every regeneration even when the data is unchanged.
## Evidence To Check
Check the scanner finding's `subtype`: `module-scope` (module-level date) or `in-cache-fn` (inside the cached body). Cross-reference `isrWritesByRoute` — a stable write rate against low reads is the symptom.
## Do Not Recommend When
Skip if the date is inside `useEffect`/`useCallback`/`useMemo`. Skip if `'use cache'` is only a comment. Skip if the date is the intended cache key.
## Verification
Name the file, the specific primitive call, and the replacement: build-time constant or client-side `useEffect`.
references/support-topics/use-cache-remote-shared-origin-data.md
---
id: use-cache-remote-shared-origin-data
title: Remote cache for shared origin data
status: active
candidateKinds: ["external_api_slow", "slow_route", "uncached_route"]
frameworks: ["next@>=16.0.0"]
priority: 87
citations: ["https://vercel.com/docs/caching/runtime-cache", "https://nextjs.org/docs/app/api-reference/directives/use-cache-remote"]
maxBriefChars: 950
---
## Investigation Brief
For Next 16 candidates, check whether shared origin data or reusable route-handler work belongs in remote cache. Default `'use cache'` is not cross-request on Vercel. Use `'use cache: remote'` or `generateStaticParams`.
## Evidence To Check
Hostname p75, caller routes, call count, bytes. Verify data is shared and tolerates the freshness window. Confirm `'use cache: remote'`.
## Do Not Recommend When
Skip per-user, mutation, secret, or freshness-critical data. Skip when upstream is fast or rarely called. Avoid sub-ms reads (Edge Config) — overhead exceeds source latency.
## Verification
Name hostname, shared data, freshness window, and exact boundary. State `'use cache: remote'`.
references/support-topics/workflow-resumable-stream-routes.md
---
id: workflow-resumable-stream-routes
title: Workflow resumable stream routes
status: active
candidateKinds: ["slow_route"]
frameworks: ["*"]
routePatterns: ["(^|/)api/.*/stream/?$", "(^|/)chat/.*/stream/?$", "\\[id\\].*/stream"]
priority: 98
citations: ["https://workflow-sdk.dev/docs/ai/resumable-streams", "https://workflow-sdk.dev/docs/foundations/streaming", "https://vercel.com/docs/workflow"]
maxBriefChars: 850
---
## Investigation Brief
Stream-shaped routes may be Workflow SDK reconnection endpoints. Long wall-clock duration can be the live client connection.
## Evidence To Check
Look for `WorkflowChatTransport`, `getRun`, `run.getReadable`, `startIndex`, `x-workflow-run-id`, `x-workflow-stream-tail-index`, `getWritable`, or `createUIMessageStreamResponse`. Compare CPU, TTFB, wall-clock. Check full replay, missing tail-index, unreleased locks, or unclosed streams.
## Do Not Recommend When
Do not cache stream endpoints or remove resumability. Do not call high duration a bug when CPU is low, TTFB is healthy, and the route only holds a client connection.
## Verification
Name whether the route starts or reconnects a run, then cite the exact waste: replay, missing tail-index, lock leak, unclosed stream, high CPU, or avoidable pre-first-byte work.
references/verification.md
# Verification
How claims in recommendations are mechanically verified, and when the recommender re-runs after a low pass rate.
## Table of contents
- [Why mechanical verification](#why-mechanical-verification)
- [Claim types](#claim-types)
- [Dispositions](#dispositions)
- [Re-gen trigger and accept criteria](#re-gen-trigger-and-accept-criteria)
- [Verifier implementation](#verifier-implementation)
## Why mechanical verification
The recommender is an LLM. LLMs hallucinate counts, miscount file occurrences, and confuse code snippets between similar-looking files. Mechanical verification — grep + filesystem reads + JSON checks against `signals.json` and `references/docs-library.json` — catches these failures before the customer sees them.
The contract: every numeric claim, file reference, code snippet, citation URL, and contradiction-with-other-claims is verified. The LLM is not asked to judge whether its own output is correct.
## Claim types
The verifier extracts claims from `why`, `fix`, `currentBehavior`, `desiredBehavior`, and `verify` fields. Each matched claim runs through one of these handlers:
| # | Claim type | Pattern in rec | Verification |
|---|---|---|---|
| 1 | `pattern_count` | "N fetch() calls in file X" | grep/ast-grep in X, exact count match |
| 2 | `pattern_exists` | "uses JSON.parse(JSON.stringify())" | grep, boolean |
| 3 | `pattern_absent` | "no Cache-Control header" | grep, verify absence (with guards — see below) |
| 4 | `file_exists` | "app/not-found.tsx exists" | fs.access |
| 5 | `finding_count` | "2 unoptimized images" | finding count vs `verifiedFindings.json` |
| 6 | `contradiction` | Claim A vs Claim B | Substring conflict check |
| 7 | `code_snippet` | Code fence labeled "Before:" | substring search in cited file |
| 8 | `arithmetic` | "20% of 100K = 20K" | math check |
| 9 | `repo_count` | "11 unstable_cache usages across 8 files" | grep repo, count distinct files |
| 10 | `cited_count_literal` | "60+ icons in packages/ui/src/icons" | glob directory, count by extension |
| 11 | `citation_in_library` | Any URL in `citations[]` | URL ∈ `references/docs-library.json` |
| 12 | `citation_applies_to_version` | Any URL in `citations[]` | URL's `applicableFrameworks` matches `signals.json.stack.framework@frameworkVersion` |
| 13 | `cache_vary_matches_dynamic_inputs` | CDN cache rec touches route files that read Vercel geolocation | Fails unless the rec varies by a coarse Vercel geolocation header such as `X-Vercel-IP-Country`, `X-Vercel-IP-Country-Region`, or `X-Vercel-IP-City` |
| 13a | `cache_vary_cardinality_safe` | CDN cache rec sets `Vary` on request-specific geography | Fails on high-cardinality `X-Vercel-IP-Latitude` / `X-Vercel-IP-Longitude` / `X-Vercel-IP-Postal-Code` |
| 14 | `next_cached_not_found_causal_support` | Rec claims `notFound()` inside `'use cache'` caused 5xx | Fails unless backed by Next-specific docs or runtime stack evidence |
| 15 | `next_stable_cache_api_for_version` | Next.js 16 cache rec includes code examples | Fails on `unstable_cacheLife` / `unstable_cacheTag` or one-arg `revalidateTag()` |
| 16 | `next_cache_components_runtime_cache_preference` | Next.js rec uses Runtime Cache APIs while `cacheComponents=true` | Fails unless `use cache: remote` is used or Runtime Cache is framed as a fallback |
| 17 | `next_cache_components_route_segment_config` | Next.js 16 rec suggests removed route segment config while `cacheComponents=true` | Fails on `dynamicParams`, `dynamic`, `revalidate`, or `fetchCache` recommendations |
| 17a | `next_route_revalidate_static_prereq` | Rec suggests route-level `export const revalidate` for a Next.js page/layout route | Fails when the route chain contains request-time APIs or common auth helpers that can force dynamic rendering |
| 18 | `next_cache_lifetime_freshness_supported` | Rec lengthens a tagged Cache Components lifetime with `cacheLife()` | Fails unless every affected `cacheTag()` has matching `revalidateTag()` / `updateTag()` evidence |
| 19 | `next_cache_life_cdn_header_semantics` | Rec claims `cacheLife()` emits CDN/Cache-Control headers or that missing `cacheLife()` alone makes a route run per request | Fails unless rewritten to the documented Cache Components lifetime behavior or backed by production header evidence |
| 20 | `next_cache_tag_invalidation_supported` | Cache-lifetime rec claims existing tag invalidation | Fails unless every claimed `cacheTag()` has matching `revalidateTag()` / `updateTag()` evidence |
| 21 | `cache_rec_not_error_dominated_or_acknowledged` | CDN cache rec targets a route with function 5xx metrics | Fails unless the rec excludes or acknowledges error traffic |
| 22 | `cache_control_header_syntax` | CDN cache rec includes `Cache-Control`, `CDN-Cache-Control`, or `Vercel-CDN-Cache-Control` values | Fails on empty directives such as a trailing comma |
| 23 | `cache_policy_positive_or_no_ready_rec` | Cache candidate emits a ready recommendation | Fails unless it names a positive cache policy; no-store-only belongs in no-change/observation output |
| 24 | `cache_404_long_ttl_safety` | CDN cache rec mentions a 404 or not-found branch | Fails unless the rec keeps the 404/not-found branch uncached, short-lived, or explicitly separate |
| 25 | `immutable_dynamic_route_safety` | Dynamic route rec uses browser `immutable` caching | Fails unless the URL is byte-versioned or the directive is scoped to Vercel's CDN |
| 26 | `auth_guard_parallelization_safety` | Parallelization rec touches private/auth/ownership data | Fails if private data can be fetched before the auth or ownership guard |
| 27 | `parallelization_impact_not_overclaimed` | Parallelization rec promises a helper-sized latency drop | Fails unless helper/span timing was measured |
| 28 | `parallelization_not_cpu_bound_work` | Parallelization rec targets CPU or compile work | Fails unless measured wait/I/O time proves there is independent work to overlap |
| 29 | `runtime_error_cause_supported` | Route-error rec names a runtime exception/root cause | Fails unless runtime logs or stack evidence support the cause |
| 30 | `turbo_build_cache_safety` | Rec enables Turbo build caching | Fails when the package build script has migration side effects or Turbo outputs omit framework build output |
Verifier guards:
- **`snippet_in_wrong_file`**: code snippet found, but in a different file from the cited path → disposition `unsupported` (don't fail the rec; the LLM was close, but the source file claim is wrong).
- **`line-number-as-count`**: "filename:42" matched against a `pattern_count` claim → skip; this is a line-number, not a count.
- **`prose-of-absence`**: "no cache headers" without an explicit grep confirmation → `unsupported`; absence claims require evidence.
- **`pattern_count` for abstracted DB calls**: `db.method()` in a file with DB imports + await helpers but literal count 0 → `unsupported` (import-chain resolution is out of scope).
## Dispositions
Each verified claim resolves to one of four states:
| Disposition | Meaning | Counted toward `passRate`? |
|---|---|---|
| `verified` | Claim matches reality | yes (counts as pass) |
| `failed` | Claim contradicts reality | yes (counts as fail) |
| `unsupported` | Claim can't be checked mechanically (see guards above) | no |
| `unverifiable` | Out of scope (e.g., external API behavior, runtime-only) | no |
`passRate = verified / (verified + failed)`. Unsupported and unverifiable don't count either way.
## Re-gen trigger and accept criteria
After verification:
| Condition | Action |
|---|---|
| `passRate < 0.8 AND verifiableClaimCount >= 2` | Re-run Step 3.3 (the recommender) with `topFailures` injected as feedback |
| Project-config contradiction, cache-safety failure, or framework-semantic failure | Hard re-run. The customer report holds back the original rec until re-gen fixes it or abstains |
| `passRate >= 0.8` OR `verifiableClaimCount < 2` | Accept the run, proceed to Step 4 |
_(Floor lowered 5 → 2 in May 2026 audit: a rec with 1/1 failed claim is just as broken as 1/5, and the old floor let many small recs escape re-gen entirely.)_
Re-gen accept criteria:
- `regenPassRate >= originalPassRate` AND
- Rec count not gutted (regen doesn't drop more than 50% of recs) AND
- Findings still cited (no rec orphaning)
If re-gen makes things worse, keep the original output unless the trigger was hard safety (`project_config_contradiction`, `cache_vary_safety`, or `semantic_safety`). Hard-safety failures must not ship to the customer report.
## Verifier implementation
`scripts/verify-and-regen.mjs` invokes `lib/extract-claims.mjs` and `lib/verify-claim.mjs` in-process for each verifiable claim. Pure functions, no network, no LLM — deterministic.
For `citation_in_library` and `citation_applies_to_version`, the script uses `lib/citations.mjs`'s `isKnownUrl()` and `sanitizeCitations()` helpers (already tested). For everything else, it shells out to grep + ast-grep via execFile.
references/voice.md
# Voice
Use Vercel's customer-facing voice: sharp teammate, clear, competent, no fluff.
Write for a user deciding what to fix next. Lead with the observed signal, the specific change, and how to verify it. Do not explain the skill's internals unless the user asked for debug details.
## Rules
- Use plain words. Prefer "use" over "leverage," "reduce" over "optimize" when the action is specific.
- Be direct. No apologetic preambles, no marketing language, no "For context" wrap-up paragraphs.
- Keep every recommendation tied to a route, file, metric, or project setting.
- Use observed numbers only. Never invent savings, traffic, latency, or percentages.
- Use cost magnitude language, not precise savings: "hundreds of dollars per month at current traffic," not "$340/mo."
- Use precise performance language when measured: "95th percentile duration is 1,240ms."
- Frame prerequisites as engineering constraints, not upsells. Explain the decision impact: what the missing data prevents, what the limited fallback can still do, and what the user should choose next.
- Use short bullets and tables. Avoid long paragraphs in reports and final chat messages.
- Write full sentences with punctuation in reports.
## Avoid
- `seamlessly`, `effortlessly`, `powerful`, `robust`, `leverage`, `unleash`, `blazing`, `lightning-fast`, `turnkey`, `holistic`, `best-in-class`, `next-generation`, `cutting-edge`, `world-class`, `streamline`, `elevate`, `harness`, `crafted`, `myriad`, `plethora`, `empower`, `utilize`
- Filler adverbs: `just`, `simply`, `actually`
- Hedge starts: `Consider`, `You may want to`, `It is important to note`
- Rhetorical reframes: `It's not X, it's Y`
- Unicode arrows in prose: `->`, `→`, `⇒`
- Internal process terms in customer output: `sub-agent`, `abstention`, `abstained`, `passRate`, `quality score`, `sanitizer`
Use customer-facing replacements:
| Internal | Customer-facing |
|---|---|
| `sub-agent` | `investigation` |
| `abstained` | `found no supported change` |
| `abstention` | `investigated, no change recommended` |
| `passRate` | `verification result` |
| `quality score` | `review result` |
| `inv` | `function invocations` or `requests`, based on the metric |
| `p95` | `95th percentile` |
| `perf` | `performance` |
| `CWV` | `Core Web Vitals` |
## Product names
Use these spellings:
| Right | Wrong |
|---|---|
| `Observability Plus` | `OPlus`, `Oplus`, `O11y Plus`, `o11y+`, `obs+` |
| `Vercel Functions` | `serverless functions` when referring to Vercel's product |
| `fluid compute` mid-sentence | `Fluid Compute` mid-sentence |
| `BotID` | `Bot ID`, `botID` |
| `AI Gateway` | `Vercel AI Gateway`, `ai gateway` |
| `AI SDK` | `Vercel AI SDK` |
| `Edge Config` | `EdgeConfig` |
| `Routing Middleware` | `Edge Middleware` |
| `Web Analytics` | `Vercel Analytics` |
| `Hobby`, `Pro`, `Enterprise` | `hobby`, `pro`, `enterprise` as plan names |
Mirror billing names from the user's dashboard. If a dashboard still says `Edge Requests`, use `Edge Requests`; do not rename it.
## Recommendation shape
| Field | Pattern |
|---|---|
| `what` | Verb + change + scope. Example: `Add shared caching to /api/products`. |
| `why` | State the metric and code evidence. Example: `The route handled 1,200,000 requests with a 0% cache hit rate; src/app/api/products/route.ts returns no Cache-Control header.` |
| `fix` | Numbered steps. Start each step with a verb. |
| `verify` | Tell the user exactly which metric or command to re-check. |
Good:
> Add `Cache-Control: s-maxage=300, stale-while-revalidate=86400` to `/api/products`. The route handled 1,200,000 GET requests with a 0% cache hit rate.
Bad:
> Consider leveraging a robust caching strategy to unlock better performance.
scripts/budget-summary.mjs
#!/usr/bin/env node
// Mid-flow checkpoint: should the orchestrator ask the user to raise the budget?
// JSON output is the contract the orchestrator parses; markdown is human-only.
import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { buildBudgetSummary, renderBudgetSummaryMarkdown } from '../lib/budget-summary.mjs';
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.gatePath) {
console.error('usage: node scripts/budget-summary.mjs <gate.json> [--format json|markdown] [--no-prompt]');
process.exit(1);
}
const gate = JSON.parse(await readFile(args.gatePath, 'utf-8'));
const summary = buildBudgetSummary(gate);
// --no-prompt: CI / non-interactive hosts collapse the checkpoint to a logging hop.
if (args.noPrompt) {
summary.shouldAsk = false;
summary.reason = 'forced false via --no-prompt (non-interactive host)';
summary.printContract = null;
summary.questionText = '';
summary.questionPayload = null;
summary.options = [];
summary.chatPreview = `Audit scope: no question needed — ${summary.reason}.`;
summary.exactChatMessage = {
body: summary.chatPreview,
lineCount: summary.chatPreview.split('\n').length,
sha256: createHash('sha256').update(summary.chatPreview).digest('hex'),
};
summary.printCheck = null;
}
if (args.format === 'markdown') {
process.stdout.write(renderBudgetSummaryMarkdown(summary) + '\n');
} else {
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
}
}
function parseArgs(argv) {
const out = { positional: [], format: 'json' };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--format') out.format = argv[++i];
else if (a.startsWith('--format=')) out.format = a.slice('--format='.length);
else if (a === '--no-prompt') out.noPrompt = true;
else out.positional.push(a);
}
out.gatePath = out.positional[0];
return out;
}
main().catch((err) => {
console.error('[budget-summary] FAILED:', err.message);
process.exit(1);
});
scripts/build-docs.mjs
#!/usr/bin/env node
// Regenerates references/{scanner-patterns,candidates}.md from lib/{scanners,gates}/*
// metadata. The .mjs files are the source of truth; check-docs-fresh.mjs blocks
// PRs where the regenerated output diverges from what's checked in.
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { scanners } from '../lib/scanners/index.mjs';
import { gates, MAX_CODE_CANDIDATES, GATE_VERSION } from '../lib/gates/index.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const REFS = join(HERE, '..', 'references');
const GENERATED_BANNER =
'<!-- THIS FILE IS GENERATED by scripts/build-docs.mjs. Do not edit by hand. -->\n' +
'<!-- To change scanner descriptions, edit lib/scanners/*.mjs metadata exports. -->\n' +
'<!-- To change gate thresholds, edit lib/gates/*.mjs metadata exports. -->\n\n';
async function main() {
await writeFile(join(REFS, 'scanner-patterns.md'), renderScanners());
await writeFile(join(REFS, 'candidates.md'), renderCandidates());
console.error('[build-docs] wrote scanner-patterns.md + candidates.md');
}
function renderScanners() {
const sorted = scanners.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
let out = GENERATED_BANNER + '# Scanner patterns\n\n';
out += 'AST/grep-style scanners run in parallel with metric-driven investigation. They find known anti-patterns. Findings on cold-path or unmappable files are dropped unless the scanner declares `trafficIndependent: true`.\n\n';
out += `Total scanners: ${sorted.length}.\n\n`;
out += '## Patterns\n\n';
for (const s of sorted) {
const m = s.metadata;
out += `### \`${m.id}\` — ${m.title}\n\n`;
out += `- **Severity**: ${m.severity}\n`;
out += `- **Billing dimension**: ${m.billingDimension}\n`;
out += `- **Traffic-independent**: ${m.trafficIndependent ? 'yes (cold-path findings survive the doctrine drop)' : 'no (cold-path findings get dropped)'}\n\n`;
out += `**Description.** ${m.description}\n\n`;
out += `**Fix.** ${m.fix}\n\n`;
if (m.citations?.length) {
out += `**Citations:**\n${m.citations.map((c) => `- \`${c}\``).join('\n')}\n\n`;
}
out += '---\n\n';
}
return trimTrailingBlankLine(out);
}
function renderCandidates() {
const sorted = gates.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
let out = GENERATED_BANNER + '# Candidate gates\n\n';
out += 'The deterministic threshold expressions that turn observability signals into investigation candidates. Pure JS, no LLM. Thresholds live in `lib/gates/*.mjs`.\n\n';
out += `Total gates: ${sorted.length}. Budget cap: \`MAX_CODE_CANDIDATES = ${MAX_CODE_CANDIDATES}\`. Gate version: \`${GATE_VERSION}\`.\n\n`;
out += '## Gates\n\n';
for (const g of sorted) {
const m = g.metadata;
out += `### \`${m.id}\`\n\n`;
out += `- **Threshold**: \`${m.threshold}\`\n`;
out += `- **Billing dimension**: ${m.billingDimension}\n`;
out += `- **Scope**: ${m.scope}\n`;
out += `- **Source citation**: \`${m.sourceCitation}\`\n\n`;
out += `${m.description}\n\n`;
out += '---\n\n';
}
return trimTrailingBlankLine(out);
}
function trimTrailingBlankLine(value) {
return value.replace(/\n{2,}$/, '\n');
}
main().catch((err) => {
console.error('[build-docs] FAILED:', err.message);
process.exit(1);
});
scripts/check-citations.mjs
#!/usr/bin/env node
// Offline citation-library consistency checks. This intentionally does not
// fetch URLs; it validates the local allow-list contract used by sanitizers.
import { loadLibrary, matchesFrameworkVersion } from '../lib/citations.mjs';
const URL_RE = /^https:\/\/[A-Za-z0-9.-]+\/\S*$/;
const SKILL_REF_RE = /^[\w-]+:[\w-]+$/;
const BANNED_STALE_URLS = new Set([
'https://nextjs.org/docs/app/api-reference/functions/cache-life',
'https://nextjs.org/docs/app/api-reference/functions/cache-tag',
'https://nextjs.org/docs/app/api-reference/functions/revalidate-tag',
'https://nextjs.org/docs/app/api-reference/functions/revalidate-path',
'https://nextjs.org/docs/app/api-reference/functions/cache',
]);
async function main() {
const lib = await loadLibrary();
const errors = [];
if (!Array.isArray(lib.urls)) errors.push('docs-library.urls must be an array');
if (!Array.isArray(lib.ruleSkillRefs)) errors.push('docs-library.ruleSkillRefs must be an array');
for (const [i, entry] of (lib.urls ?? []).entries()) {
const label = `urls[${i}]`;
if (!URL_RE.test(entry?.url ?? '')) errors.push(`${label}.url must be an https URL`);
if (BANNED_STALE_URLS.has(entry?.url)) {
errors.push(`${label}.url uses a stale Next.js docs path: ${entry.url}`);
}
if (typeof entry.topic !== 'string' || entry.topic.trim() === '') errors.push(`${label}.topic is required`);
if (!Array.isArray(entry.appliesTo)) errors.push(`${label}.appliesTo must be an array`);
validateFrameworks(entry.applicableFrameworks, `${label}.applicableFrameworks`, errors);
}
const seenRules = new Set();
for (const [i, entry] of (lib.ruleSkillRefs ?? []).entries()) {
const label = `ruleSkillRefs[${i}]`;
const ref = `${entry?.skill ?? ''}:${entry?.rule ?? ''}`;
if (!SKILL_REF_RE.test(ref)) errors.push(`${label} must contain skill + rule identifiers`);
if (seenRules.has(ref)) errors.push(`${label} duplicate: ${ref}`);
seenRules.add(ref);
if (typeof (entry.description ?? entry.topic) !== 'string' || (entry.description ?? entry.topic).trim() === '') {
errors.push(`${label}.topic or .description is required`);
}
validateFrameworks(entry.applicableFrameworks, `${label}.applicableFrameworks`, errors);
}
if (errors.length > 0) {
for (const error of errors) console.error(`[check-citations] ${error}`);
process.exit(1);
}
console.error(`[check-citations] OK — ${lib.urls.length} URLs, ${lib.ruleSkillRefs.length} skill-rule refs`);
}
function validateFrameworks(patterns, label, errors) {
if (!Array.isArray(patterns) || patterns.length === 0) {
errors.push(`${label} must be a non-empty array`);
return;
}
for (const pattern of patterns) {
if (typeof pattern !== 'string' || pattern.trim() === '') {
errors.push(`${label} contains an empty pattern`);
continue;
}
if (pattern === '*') continue;
// Smoke-check parser coverage with a modern Next version. Unknown framework
// patterns are still valid as long as the syntax is recognizable.
if (!/^[\w-]+@(?:\*|\d+(?:\.\d+){0,2}|[<>]=?\s*\d+(?:\.\d+){0,2})(?:\s*\|\|\s*[\w-]+@(?:\*|\d+(?:\.\d+){0,2}|[<>]=?\s*\d+(?:\.\d+){0,2}))*$/.test(pattern)) {
errors.push(`${label} has unsupported pattern: ${pattern}`);
continue;
}
matchesFrameworkVersion(pattern, 'next', '16.0.0');
}
}
main().catch((err) => {
console.error('[check-citations] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
scripts/check-docs-fresh.mjs
#!/usr/bin/env node
// CI gate: regenerate the reference docs in memory and diff against what's
// checked in. Non-zero exit forces contributors to run build-docs.mjs.
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { scanners } from '../lib/scanners/index.mjs';
import { gates, MAX_CODE_CANDIDATES, GATE_VERSION } from '../lib/gates/index.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const REFS = join(HERE, '..', 'references');
const GENERATED_BANNER =
'<!-- THIS FILE IS GENERATED by scripts/build-docs.mjs. Do not edit by hand. -->\n' +
'<!-- To change scanner descriptions, edit lib/scanners/*.mjs metadata exports. -->\n' +
'<!-- To change gate thresholds, edit lib/gates/*.mjs metadata exports. -->\n\n';
async function main() {
const expected = {
'scanner-patterns.md': renderScanners(),
'candidates.md': renderCandidates(),
};
let stale = false;
for (const [name, content] of Object.entries(expected)) {
let actual;
try { actual = await readFile(join(REFS, name), 'utf-8'); }
catch {
console.error(`[check-docs-fresh] ${name} does not exist. Run \`node scripts/build-docs.mjs\`.`);
stale = true;
continue;
}
if (actual !== content) {
console.error(`[check-docs-fresh] ${name} is stale. Run \`node scripts/build-docs.mjs\` and commit.`);
stale = true;
}
}
if (stale) process.exit(1);
console.error('[check-docs-fresh] OK — generated docs match source');
}
// MUST stay byte-identical to build-docs.mjs renderers — duplication is the contract.
function renderScanners() {
const sorted = scanners.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
let out = GENERATED_BANNER + '# Scanner patterns\n\n';
out += 'AST/grep-style scanners run in parallel with metric-driven investigation. They find known anti-patterns. Findings on cold-path or unmappable files are dropped unless the scanner declares `trafficIndependent: true`.\n\n';
out += `Total scanners: ${sorted.length}.\n\n`;
out += '## Patterns\n\n';
for (const s of sorted) {
const m = s.metadata;
out += `### \`${m.id}\` — ${m.title}\n\n`;
out += `- **Severity**: ${m.severity}\n`;
out += `- **Billing dimension**: ${m.billingDimension}\n`;
out += `- **Traffic-independent**: ${m.trafficIndependent ? 'yes (cold-path findings survive the doctrine drop)' : 'no (cold-path findings get dropped)'}\n\n`;
out += `**Description.** ${m.description}\n\n`;
out += `**Fix.** ${m.fix}\n\n`;
if (m.citations?.length) {
out += `**Citations:**\n${m.citations.map((c) => `- \`${c}\``).join('\n')}\n\n`;
}
out += '---\n\n';
}
return trimTrailingBlankLine(out);
}
function renderCandidates() {
const sorted = gates.slice().sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
let out = GENERATED_BANNER + '# Candidate gates\n\n';
out += 'The deterministic threshold expressions that turn observability signals into investigation candidates. Pure JS, no LLM. Thresholds live in `lib/gates/*.mjs`.\n\n';
out += `Total gates: ${sorted.length}. Budget cap: \`MAX_CODE_CANDIDATES = ${MAX_CODE_CANDIDATES}\`. Gate version: \`${GATE_VERSION}\`.\n\n`;
out += '## Gates\n\n';
for (const g of sorted) {
const m = g.metadata;
out += `### \`${m.id}\`\n\n`;
out += `- **Threshold**: \`${m.threshold}\`\n`;
out += `- **Billing dimension**: ${m.billingDimension}\n`;
out += `- **Scope**: ${m.scope}\n`;
out += `- **Source citation**: \`${m.sourceCitation}\`\n\n`;
out += `${m.description}\n\n`;
out += '---\n\n';
}
return trimTrailingBlankLine(out);
}
function trimTrailingBlankLine(value) {
return value.replace(/\n{2,}$/, '\n');
}
main().catch((err) => {
console.error('[check-docs-fresh] FAILED:', err.message);
process.exit(1);
});
scripts/collect-signals.mjs
#!/usr/bin/env node
// Emits signals.json: Vercel CLI capability probe + project config + plan +
// usage + codebase stack + metric queries. Status → stderr, JSON → stdout.
// Degrades gracefully when capabilities are missing.
import {
checkCliVersion,
checkAuth,
resolveProjectId,
resolveCommandScope,
hasObservabilityPlus,
checkObservabilityPlusConfiguration,
getMetricsSchema,
getProjectConfig,
getAccountPlan,
getContract,
getUsage,
filterUsageByProject,
inferPlan,
queryMetric,
detectStack,
redactSensitiveText,
} from '../lib/vercel.mjs';
import { classifyFrameworkSupport } from '../lib/framework-support.mjs';
import { QUERIES, TIME_WINDOW, normalizerFor } from '../lib/queries.mjs';
const SCHEMA_VERSION = '1.2';
const log = (...args) => console.error('[collect-signals]', ...args);
function parseArgs(argv) {
let explicitProjectId = null;
let continueWithoutObservability = process.env.VERCEL_OPTIMIZE_CONTINUE_WITHOUT_OBSERVABILITY === '1';
let continueUnsupportedFramework = process.env.VERCEL_OPTIMIZE_CONTINUE_UNSUPPORTED_FRAMEWORK === '1';
for (const arg of argv) {
if (arg === '--continue-without-observability') {
continueWithoutObservability = true;
continue;
}
if (arg === '--continue-unsupported-framework') {
continueUnsupportedFramework = true;
continue;
}
if (arg.startsWith('--')) {
throw new Error(`UNKNOWN_ARG: ${arg}`);
}
if (!explicitProjectId) {
explicitProjectId = arg;
continue;
}
throw new Error(`UNKNOWN_ARG: ${arg}`);
}
return { explicitProjectId, continueWithoutObservability, continueUnsupportedFramework };
}
async function main() {
const { explicitProjectId, continueWithoutObservability, continueUnsupportedFramework } = parseArgs(process.argv.slice(2));
log('checking Vercel CLI version…');
const cli = await checkCliVersion();
log(`vercel CLI v${cli.join('.')} OK`);
log('checking auth…');
await checkAuth();
log('auth OK');
log('resolving project id…');
const project = await resolveProjectId(explicitProjectId);
if (!project) {
throw new Error(
'NO_PROJECT_ID: pass one as argv, set VERCEL_PROJECT_ID, or run `vercel link` in this directory.'
);
}
log(`project link resolved (source=${project.source}; teamScope=${project.orgId ? 'yes' : 'no'})`);
if (!project.orgId) {
throw new Error('PROJECT_SCOPE_UNRESOLVED: the project was resolved without an owner account. Ask the user which Vercel team or personal scope owns the project, then rerun from a linked app directory or set VERCEL_PROJECT_ID with VERCEL_ORG_ID for that scope.');
}
log('checking framework support…');
const stack = await detectStack();
const frameworkSupport = classifyFrameworkSupport(stack);
log(`framework=${stack.framework}@${stack.frameworkVersion ?? '?'} support=${frameworkSupport.status}`);
if (!frameworkSupport.ok && !continueUnsupportedFramework) {
writeOutput({
schemaVersion: SCHEMA_VERSION,
collectedAt: new Date().toISOString(),
timeWindow: TIME_WINDOW,
projectId: project.projectId,
orgId: project.orgId,
projectIdSource: project.source,
commandScope: null,
frameworkSupport,
frameworkSupportBlocker: frameworkSupport.blocker,
frameworkSupportDetail: frameworkSupport.detail,
observabilityPlus: null,
observabilityPlusPreflight: null,
observabilityPlusUsable: null,
observabilityPlusBlocker: null,
observabilityPlusBlockerDetail: null,
plan: {
plan: 'uncertain',
reason: 'not collected before unsupported-framework confirmation',
},
project: null,
contract: null,
usage: null,
usageScope: null,
usageTeamTotal: null,
usageError: 'NOT_COLLECTED_UNSUPPORTED_FRAMEWORK',
stack,
metrics: {},
metricsSchema: null,
}, { usable: true, blocker: null, detail: 'Observability Plus was not checked.' }, frameworkSupport);
return;
}
if (!frameworkSupport.ok && continueUnsupportedFramework) {
log('continuing after unsupported framework blocker because --continue-unsupported-framework was set');
}
log('resolving Vercel CLI command scope…');
const commandScope = await resolveCommandScope(project);
if (!commandScope.ok) {
throw new Error(`SCOPE_UNRESOLVED: ${commandScope.detail} Run \`vercel switch <team>\` or re-link with \`vercel link --yes --project <project-name-or-id> --team <team-slug>\`.`);
}
const scope = commandScope.cliScope || undefined;
log(`command scope resolved (source=${commandScope.source}; scoped=${scope ? 'yes' : 'no'})`);
log('validating linked project belongs to the resolved scope…');
const projectCfg = await getProjectConfig(project.projectId, project.orgId);
const projectScope = validateProjectScope(projectCfg, project);
if (!projectScope.ok) {
throw new Error(`PROJECT_SCOPE_MISMATCH: ${projectScope.detail} Ask the user to confirm the exact Vercel project and team/personal scope, then rerun after \`vercel link --yes --project <project-name-or-id> --team <team-slug>\` or after setting both VERCEL_PROJECT_ID and VERCEL_ORG_ID for the intended scope.`);
}
log(`project scope verified (source=${projectScope.source})`);
log('checking Observability Plus configuration…');
const observabilityPlusConfig = await checkObservabilityPlusConfiguration({
orgId: project.orgId,
projectId: project.projectId,
});
log(`observabilityPlusPreflight=${observabilityPlusConfig.access === true ? 'enabled' : observabilityPlusConfig.blocker ?? 'unknown'} (${observabilityPlusConfig.source})`);
let oplus = observabilityPlusConfig.access === true;
if (observabilityPlusConfig.access == null) {
log('Observability Plus configuration preflight inconclusive; falling back to metrics schema probe…');
oplus = await hasObservabilityPlus(scope);
}
log(`observabilityPlus=${oplus}`);
const schema = oplus ? await getMetricsSchema(scope) : null;
if (oplus && schema) {
const count = Array.isArray(schema) ? schema.length : (schema.metrics?.length ?? 0);
log(`metric catalog: ${count} metrics available`);
}
// Check one cheap metric before pulling slower project context. If this fails,
// the orchestrator can ask the user immediately instead of waiting on billing.
let metrics = {};
let metricsCanaryOk = false;
if (oplus) {
log(`checking Observability Plus metrics access (window=${TIME_WINDOW})…`);
const t0 = Date.now();
const canary = await queryMetric('vercel.request.count', {
aggregation: 'sum',
since: TIME_WINDOW,
limit: 1,
scope,
});
metricsCanaryOk = !!canary?.ok;
if (!metricsCanaryOk) {
metrics = {
observabilityPlusCanary: {
...canary,
metricId: 'vercel.request.count',
aggregation: 'sum',
},
};
log(`metrics access check failed: ${canary?.code ?? 'unknown'} — skipping full metrics fan-out`);
} else {
log(`metrics access check passed in ${Date.now() - t0}ms`);
}
} else {
log('skipping metric queries (Observability Plus preflight did not confirm access)');
}
let oplusDiag = observabilityPlusConfig.access === false
? {
usable: false,
blocker: observabilityPlusConfig.blocker,
detail: observabilityPlusConfig.detail,
}
: (metricsCanaryOk
? { usable: true, blocker: null, detail: 'Observability Plus metrics access check passed.' }
: diagnoseObservabilityPlus(metrics, oplus));
if (!oplusDiag.usable && !continueWithoutObservability) {
writeOutput({
schemaVersion: SCHEMA_VERSION,
collectedAt: new Date().toISOString(),
timeWindow: TIME_WINDOW,
projectId: project.projectId,
orgId: project.orgId,
projectIdSource: project.source,
commandScope,
observabilityPlus: oplus,
observabilityPlusPreflight: observabilityPlusConfig,
observabilityPlusUsable: oplusDiag.usable,
observabilityPlusBlocker: oplusDiag.blocker,
observabilityPlusBlockerDetail: oplusDiag.detail,
frameworkSupport,
frameworkSupportBlocker: frameworkSupport.blocker,
frameworkSupportDetail: frameworkSupport.detail,
plan: {
plan: 'uncertain',
reason: 'not collected before Observability Plus blocker confirmation',
},
project: projectCfg,
contract: null,
usage: null,
usageScope: null,
usageTeamTotal: null,
usageError: 'NOT_COLLECTED_OBSERVABILITY_BLOCKED',
stack: null,
metrics,
metricsSchema: schema,
}, oplusDiag);
return;
}
if (!oplusDiag.usable && continueWithoutObservability) {
log('continuing after Observability Plus blocker because --continue-without-observability was set');
}
log('pulling account plan + contract + usage in parallel…');
const [accountPlan, contract, usageResult] = await Promise.all([
getAccountPlan(project.orgId || scope),
getContract(scope),
getUsage({ days: 14, scope }),
]);
let usage = null;
let usageContextMismatch = false;
let usageTotalCost = null;
let usageScope = 'team';
let usageTeamTotal = null;
if (usageResult?.ok) {
usage = usageResult.data;
const contractContext = contract?.context;
if (usage?.context && contractContext && usage.context !== contractContext) {
usageContextMismatch = true;
log(`usage: WARNING context mismatch — returned context=${usage.context} but project team=${contractContext}; treating usage as unavailable for this project`);
usage = null;
} else {
// Capture team total pre-filter so the report can label "this project vs team-wide" honestly.
usageTeamTotal = sumUsageCosts(usage);
const filterResult = filterUsageByProject(usage, project.projectId, projectCfg?.name);
if (filterResult.matched) {
usage = filterResult.filtered;
usageScope = 'project';
usageTotalCost = sumUsageCosts(usage);
log(`usage: filtered to project — ~$${usageTotalCost.toFixed(2)} (team-wide ~$${usageTeamTotal.toFixed(2)}; unattributed ~$${filterResult.unattributedTotal.toFixed(2)})`);
} else {
usageTotalCost = usageTeamTotal;
log(`usage: ~$${usageTotalCost.toFixed(2)} billed across services (team-wide — no per-project usage rows matched the linked project; report will label this team-wide)`);
}
}
} else {
log(`usage: unavailable (${usageResult?.code ?? 'unknown'}) — degrading to scanner+metrics-only mode`);
}
const planInfo = inferPlan(contract, { accountPlan, usageTotalCost });
log(`plan=${planInfo.plan} (${planInfo.reason})`);
if (projectCfg?.error) {
log(`project config: failed (${projectCfg.error}) — gates that need it will skip`);
}
log(`stack: ${stack.framework}@${stack.frameworkVersion ?? '?'} ${stack.hasAppRouter ? 'app-router' : ''}${stack.hasPagesRouter ? ' pages-router' : ''}${stack.orm !== 'none' ? ` orm=${stack.orm}` : ''}`);
// Each query is wrapped; one failure degrades only that metric.
if (oplus && metricsCanaryOk) {
log(`querying observability metrics (${QUERIES.length} metrics in parallel)…`);
const t0 = Date.now();
metrics = await collectMetrics(scope);
const wallMs = Date.now() - t0;
const counts = Object.fromEntries(
Object.entries(metrics).map(([k, v]) => {
if (!v) return [k, 'null'];
if (!v.ok) return [k, `err:${v.code}`];
const rows = Array.isArray(v.rows) ? v.rows.length : 0;
return [k, `${rows} rows`];
})
);
log(`metrics collected in ${wallMs}ms: ${JSON.stringify(counts)}`);
}
// The `vercel metrics schema` probe alone is NOT a reliable usability signal:
// it can return OK while per-route queries fail with payment_required (metrics
// unavailable for the team) or FORBIDDEN (auth-scope mismatch). Diagnose AFTER
// running queries by counting failure codes so the orchestrator can PAUSE and
// surface the choice before falling back to scanner-only mode.
oplusDiag = observabilityPlusConfig.access === false
? {
usable: false,
blocker: observabilityPlusConfig.blocker,
detail: observabilityPlusConfig.detail,
}
: diagnoseObservabilityPlus(metrics, oplus);
const output = {
schemaVersion: SCHEMA_VERSION,
collectedAt: new Date().toISOString(),
timeWindow: TIME_WINDOW,
projectId: project.projectId,
orgId: project.orgId,
projectIdSource: project.source,
commandScope,
observabilityPlus: oplus,
observabilityPlusPreflight: observabilityPlusConfig,
observabilityPlusUsable: oplusDiag.usable,
observabilityPlusBlocker: oplusDiag.blocker,
observabilityPlusBlockerDetail: oplusDiag.detail,
frameworkSupport,
frameworkSupportBlocker: frameworkSupport.blocker,
frameworkSupportDetail: frameworkSupport.detail,
plan: planInfo,
project: projectCfg,
contract,
usage,
usageScope,
usageTeamTotal,
usageError: usageResult?.ok
? (usageContextMismatch ? 'USAGE_CONTEXT_MISMATCH' : null)
: (usageResult?.code ?? 'UNKNOWN'),
stack,
metrics,
metricsSchema: schema,
};
writeOutput(output, oplusDiag);
}
function writeOutput(output, oplusDiag, frameworkSupport = output.frameworkSupport) {
if (frameworkSupport?.blocker) {
log(`⚠ Framework is not supported for metric-backed route-to-file optimization: ${frameworkSupport.detail}`);
log(' The orchestrator should PAUSE and ask whether to continue with a limited platform/scanner audit.');
}
if (!oplusDiag.usable) {
log(`⚠ Observability Plus is NOT usable on this project: blocker=${oplusDiag.blocker} (${oplusDiag.detail})`);
log(' The orchestrator should PAUSE and follow the blocker-specific remediation before proceeding.');
}
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
log('done');
}
function validateProjectScope(projectCfg, project) {
if (!projectCfg || projectCfg.error) {
return {
ok: false,
source: 'project-api',
detail: `The resolved account could not read the resolved project (project API error=${projectCfg?.error ?? 'unknown'}).`,
};
}
if (projectCfg.id && String(projectCfg.id) !== String(project.projectId)) {
return {
ok: false,
source: 'project-api',
detail: 'The project API returned a different project than the collector resolved from the link or environment.',
};
}
const ownerId = firstString(
projectCfg.accountId,
projectCfg.orgId,
projectCfg.ownerId,
projectCfg.teamId,
projectCfg.team?.id,
projectCfg.account?.id,
projectCfg.owner?.id,
);
if (ownerId && project.orgId && String(ownerId) !== String(project.orgId)) {
return {
ok: false,
source: 'project-api',
detail: 'The project API returned an owner account that differs from the collector-resolved account.',
};
}
return {
ok: true,
source: ownerId ? 'project-api-owner' : 'project-api-readable',
};
}
function firstString(...values) {
return values.find((value) => typeof value === 'string' && value.trim() !== '') ?? null;
}
async function collectMetrics(scope) {
const results = await Promise.all(
QUERIES.map(async (entry) => {
const r = await queryMetric(entry.metricId, {
aggregation: entry.aggregation,
groupBy: entry.groupBy,
filter: entry.filter,
since: TIME_WINDOW,
limit: entry.limit,
scope,
});
return [entry, r];
})
);
const out = {};
for (const [entry, result] of results) {
out[entry.id] = enrichEntry(entry, result);
}
return out;
}
function enrichEntry(entry, result) {
if (!result?.ok) {
return {
...result,
metricId: entry.metricId,
aggregation: entry.aggregation,
groupBy: entry.groupBy,
};
}
const normalize = normalizerFor(entry);
const { rows } = normalize(result.data);
return {
...result,
rows,
metricId: entry.metricId,
aggregation: entry.aggregation,
groupBy: entry.groupBy,
};
}
// `vercel usage --format json` shape is documented but not stable across CLI
// versions; try several roots, return null if none match.
function sumUsageCosts(usage) {
if (!usage) return null;
if (typeof usage.totalCost === 'number') return usage.totalCost;
if (typeof usage.totals?.billedCost === 'number') return usage.totals.billedCost;
if (Array.isArray(usage.services)) {
return usage.services.reduce((s, x) => s + (x.billedCost ?? x.cost ?? 0), 0);
}
if (Array.isArray(usage.breakdown?.data)) {
return usage.breakdown.data.reduce((s, d) => {
if (Array.isArray(d.services)) {
return s + d.services.reduce((ss, x) => ss + (x.billedCost ?? x.cost ?? 0), 0);
}
return s + (d.billedCost ?? d.cost ?? 0);
}, 0);
}
return null;
}
// Returns { usable, blocker, detail }. `blocker` enum:
// null | 'no_oplus_probe' | 'project_disabled' | 'payment_required' |
// 'forbidden' | 'daily_quota_exceeded' | 'project_not_found' |
// 'not_linked' | 'all_failed_other' | 'no_traffic'
export function diagnoseObservabilityPlus(metrics, oplusProbe) {
if (!oplusProbe) {
return {
usable: false,
blocker: 'no_oplus_probe',
detail: 'vercel metrics schema returned non-OK; the team does not have Observability Plus enabled.',
};
}
const entries = Object.values(metrics);
if (entries.length === 0) {
return { usable: false, blocker: 'no_oplus_probe', detail: 'No metrics were attempted.' };
}
const failures = entries.filter((m) => m && m.ok === false);
const successes = entries.filter((m) => m && m.ok !== false);
if (successes.length === 0) {
const codeCounts = new Map();
for (const f of failures) {
const code = String(f.code ?? 'unknown').toLowerCase();
codeCounts.set(code, (codeCounts.get(code) ?? 0) + 1);
}
const top = [...codeCounts.entries()].sort((a, b) => b[1] - a[1])[0];
const topCode = top?.[0] ?? 'unknown';
if (/daily_quota_exceeded/.test(topCode)) {
return {
usable: false,
blocker: 'daily_quota_exceeded',
detail: `${top[1]}/${entries.length} metric queries hit the daily Observability query limit. Retry after the next UTC midnight reset.`,
};
}
if (/payment_required/.test(topCode)) {
const text = failures
.map((f) => `${f.message ?? ''}\n${f.stderr ?? ''}`)
.join('\n')
.toLowerCase();
if (
/subscription to observability plus[\s\S]{0,160}required/.test(text) ||
/observability plus[\s\S]{0,160}not enabled/.test(text)
) {
return {
usable: false,
blocker: 'no_oplus_probe',
detail: `${top[1]}/${entries.length} metric queries need route-level Observability Plus data. Enable Observability Plus, then re-run the metric-backed audit.`,
};
}
return {
usable: false,
blocker: 'payment_required',
detail: `${top[1]}/${entries.length} metric queries returned payment_required. Route-level metrics were recognized for this team, but these queries are not usable. Check the team's Observability Plus subscription or event quota.`,
};
}
if (/forbidden|not_authorized|403/.test(topCode)) {
return {
usable: false,
blocker: 'forbidden',
detail: `${top[1]}/${entries.length} metric queries returned FORBIDDEN. Auth-scope mismatch — likely logged in to the wrong team (run \`vercel switch\`).`,
};
}
if (/project_not_found/.test(topCode)) {
return {
usable: false,
blocker: 'project_not_found',
detail: `Project ID not visible to the auth'd team. Run \`vercel switch\` or verify the project ID.`,
};
}
if (/not_linked/.test(topCode)) {
return {
usable: false,
blocker: 'not_linked',
detail: `${top[1]}/${entries.length} metric queries returned NOT_LINKED. Link the app directory first: \`vercel link --yes --project <project-name-or-id> --cwd <project-dir>\`; add \`--team <team-id-or-slug>\` when the team is known.`,
};
}
return {
usable: false,
blocker: 'all_failed_other',
detail: `Every metric query failed; top error code was \`${topCode}\` (${top?.[1]}/${entries.length}).`,
};
}
// Some queries succeeded; zero rows across the board = "no traffic in window",
// NOT an Observability Plus billing issue.
const totalRows = successes.reduce((s, m) => s + (Array.isArray(m.rows) ? m.rows.length : 0), 0);
if (totalRows === 0) {
return {
usable: true,
blocker: 'no_traffic',
detail: 'Observability Plus queries succeeded but every metric returned 0 rows. Either the project has no traffic in the 14-day window, or Observability Plus retention is limited (free tier = 1 day on Pro).',
};
}
return { usable: true, blocker: null, detail: 'Observability Plus is usable; queries returned data.' };
}
// Run main() only as a CLI; the test suite imports diagnoseObservabilityPlus directly.
import { fileURLToPath } from 'node:url';
import { realpathSync } from 'node:fs';
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
main().catch((err) => {
console.error('[collect-signals] FAILED:', redactSensitiveText(err.message));
process.exit(1);
});
}
scripts/collect-sub-agent-outputs.mjs
#!/usr/bin/env node
// Collect raw sub-agent outputs into the recommendations.json array consumed by
// verify-and-regen. Sub-agent hosts often wrap JSON in prose or markdown fences,
// so extraction is permissive while candidateRef coverage stays strict.
import { readFile, readdir, stat, writeFile, mkdir } from 'node:fs/promises';
import { dirname, resolve, basename } from 'node:path';
const log = (...a) => console.error('[collect-sub-agent-outputs]', ...a);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.inputs.length === 0 && !args.manifestPath) {
console.error('usage: node scripts/collect-sub-agent-outputs.mjs [--manifest briefs/manifest.json] <output-file-or-dir...> [--out recommendations.json] [--strict]');
process.exit(1);
}
const manifest = args.manifestPath
? JSON.parse(await readFile(args.manifestPath, 'utf-8'))
: null;
const expected = manifest ? readExpectedBriefs(manifest) : [];
const preResolvedRecords = manifest ? readPreResolvedRecords(manifest) : [];
const files = args.inputs.length > 0 ? await collectInputFiles(args.inputs) : [];
const collected = [];
const summary = {
files: files.length,
kept: 0,
abstained: 0,
parseFailed: 0,
nonObject: 0,
missingCandidateRef: 0,
};
const errors = [];
for (const file of files) {
const raw = await readFile(file, 'utf-8');
const extracted = extractJsonValue(raw);
if (!extracted.ok) {
summary.parseFailed++;
const msg = `${file}: ${extracted.reason}`;
if (args.strict) errors.push(msg);
else log(`warn: ${msg}`);
continue;
}
const records = normalizeOutput(extracted.value);
if (records.length === 0) {
summary.nonObject++;
const msg = `${file}: JSON did not contain a recommendation or abstention object`;
if (args.strict) errors.push(msg);
else log(`warn: ${msg}`);
continue;
}
for (const record of records) {
const candidateRef = record.candidateRef ?? inferCandidateRefFromFile(file, expected, records.length);
if (!candidateRef) {
summary.missingCandidateRef++;
errors.push(`${file}: output is missing candidateRef`);
continue;
}
collected.push({
sourcePath: file,
record: record.candidateRef ? record : { ...record, candidateRef },
});
}
}
let ordered = collected;
if (expected.length > 0) {
const byRef = new Map();
for (const item of collected) {
const ref = item.record.candidateRef;
if (!expected.some((b) => b.candidateRef === ref)) {
errors.push(`${item.sourcePath}: unknown candidateRef ${ref}`);
continue;
}
if (byRef.has(ref)) {
errors.push(`${item.sourcePath}: duplicate output for candidateRef ${ref}`);
continue;
}
byRef.set(ref, item);
}
const missing = expected.filter((b) => !byRef.has(b.candidateRef));
for (const b of missing) errors.push(`missing output for candidateRef ${b.candidateRef}`);
ordered = expected.map((b) => byRef.get(b.candidateRef)).filter(Boolean);
} else {
ordered = collected.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
}
const records = [...preResolvedRecords, ...ordered.map((item) => item.record)];
summary.kept = records.filter((r) => r?.abstain !== true).length;
summary.abstained = records.filter((r) => r?.abstain === true).length;
if (errors.length > 0) {
for (const e of errors) log(`error: ${e}`);
process.exit(2);
}
if (records.length === 0) {
log('error: no recommendation or abstention records collected');
process.exit(2);
}
const serialized = JSON.stringify(records, null, 2) + '\n';
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeFile(args.outPath, serialized, 'utf-8');
log(`wrote ${serialized.length}B → ${args.outPath}`);
} else {
process.stdout.write(serialized);
}
log(`done: ${summary.files} files, ${summary.kept} recommendation draft(s), ${summary.abstained} found no supported change, ${summary.parseFailed} parse failed, ${summary.nonObject} invalid output(s)`);
}
function parseArgs(argv) {
const out = { inputs: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--manifest') out.manifestPath = resolve(argv[++i]);
else if (a.startsWith('--manifest=')) out.manifestPath = resolve(a.slice('--manifest='.length));
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else if (a === '--strict') out.strict = true;
else out.inputs.push(resolve(a));
}
return out;
}
async function collectInputFiles(paths) {
const out = [];
for (const p of paths) {
const s = await stat(p);
if (s.isDirectory()) out.push(...await walkDir(p));
else if (s.isFile()) out.push(p);
}
return out.sort((a, b) => a.localeCompare(b));
}
async function walkDir(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const out = [];
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
if (e.name.startsWith('.')) continue;
const p = resolve(dir, e.name);
if (e.isDirectory()) out.push(...await walkDir(p));
else if (e.isFile()) out.push(p);
}
return out;
}
function readExpectedBriefs(manifest) {
if (!manifest || typeof manifest !== 'object' || !Array.isArray(manifest.briefs)) {
throw new TypeError('manifest must contain a briefs array');
}
return manifest.briefs.map((b, i) => {
if (!b?.candidateRef) throw new TypeError(`manifest.briefs[${i}].candidateRef is required`);
return {
group: b.group ?? null,
index: b.index ?? i,
candidateRef: b.candidateRef,
};
});
}
function readPreResolvedRecords(manifest) {
if (!manifest || !Array.isArray(manifest.preResolvedRecords)) return [];
return manifest.preResolvedRecords.map((r, i) => {
if (!isRecordObject(r)) {
throw new TypeError(`manifest.preResolvedRecords[${i}] must be a recommendation or no-recommendation record`);
}
if (!r.candidateRef) {
throw new TypeError(`manifest.preResolvedRecords[${i}].candidateRef is required`);
}
return r;
});
}
function extractJsonValue(raw) {
for (const block of extractFenceBlocks(raw)) {
const parsed = tryParseJson(block);
if (parsed.ok) return parsed;
}
const full = tryParseJson(raw);
if (full.ok) return full;
for (const span of findBalancedJsonSpans(raw)) {
const parsed = tryParseJson(span);
if (parsed.ok) return parsed;
}
return { ok: false, reason: 'no valid JSON object or array found' };
}
function extractFenceBlocks(raw) {
const out = [];
const re = /```(?:json|JSON)?\s*\n([\s\S]*?)```/g;
let m;
while ((m = re.exec(raw)) !== null) out.push(m[1].trim());
return out;
}
function tryParseJson(raw) {
try {
return { ok: true, value: JSON.parse(raw.trim()) };
} catch (err) {
return { ok: false, reason: err.message };
}
}
function findBalancedJsonSpans(raw) {
const spans = [];
for (let i = 0; i < raw.length; i++) {
const ch = raw[i];
if (ch !== '{' && ch !== '[') continue;
const closeFor = ch === '{' ? '}' : ']';
const stack = [closeFor];
let inString = false;
let escape = false;
for (let j = i + 1; j < raw.length; j++) {
const c = raw[j];
if (inString) {
if (escape) escape = false;
else if (c === '\\') escape = true;
else if (c === '"') inString = false;
continue;
}
if (c === '"') {
inString = true;
continue;
}
if (c === '{') stack.push('}');
else if (c === '[') stack.push(']');
else if (c === '}' || c === ']') {
if (stack.at(-1) !== c) break;
stack.pop();
if (stack.length === 0) {
spans.push(raw.slice(i, j + 1));
i = j;
break;
}
}
}
}
return spans;
}
function normalizeOutput(value) {
const unwrapped = unwrapEnvelope(value);
if (Array.isArray(unwrapped)) return unwrapped.filter(isRecordObject);
if (isRecordObject(unwrapped)) return [unwrapped];
if (unwrapped && typeof unwrapped === 'object') {
if (isRecordObject(unwrapped.recommendation)) return [unwrapped.recommendation];
if (Array.isArray(unwrapped.recommendations)) return unwrapped.recommendations.filter(isRecordObject);
}
return [];
}
function unwrapEnvelope(value) {
let current = value;
for (let depth = 0; depth < 2; depth++) {
if (!current || typeof current !== 'object' || Array.isArray(current)) return current;
if (Array.isArray(current.recommendations) || current.recommendation) return current;
const keys = Object.keys(current);
const envelopeKey = ['data', 'result', 'insights'].find((k) => keys.length === 1 && k in current);
if (!envelopeKey) return current;
current = current[envelopeKey];
}
return current;
}
function isRecordObject(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
if (value.abstain === true) return true;
return ['what', 'why', 'fix', 'bucket', 'affectedFiles', 'citations'].some((k) => k in value);
}
function inferCandidateRefFromFile(file, expected, recordCount) {
if (recordCount !== 1 || expected.length === 0) return null;
if (expected.length === 1) return expected[0].candidateRef;
const name = basename(file);
const matches = expected.filter((b) => {
if (!b.group && b.index == null) return false;
const group = escapeRegExp(String(b.group ?? ''));
const index = escapeRegExp(String(b.index));
return new RegExp(`(?:^|[^A-Za-z0-9])${group}[-_.]?${index}(?:[^A-Za-z0-9]|$)`).test(name);
});
return matches.length === 1 ? matches[0].candidateRef : null;
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
main().catch((err) => {
console.error('[collect-sub-agent-outputs] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
scripts/deep-dive.mjs
#!/usr/bin/env node
// Runs AFTER gate-investigations.mjs and BEFORE any sub-agent reads source.
// Attaches per-candidate evidence.deepDive to gate.toLaunch + gate.platform.
// Byte-stable apart from totalWallMs; each CLI query is isolated.
import { readFile } from 'node:fs/promises';
import { queryMetric, readProjectJson, resolveCommandScope } from '../lib/vercel.mjs';
import { specsForCandidate, mergeIntoEvidence, SCANNER_KINDS, TIME_WINDOW } from '../lib/deep-dive.mjs';
const SCHEMA_VERSION = '1.0';
const log = (...a) => console.error('[deep-dive]', ...a);
async function main() {
// --cwd is load-bearing: the Vercel CLI resolves project/team from cwd's
// .vercel/project.json. Outside the project, metric queries silently hit the
// wrong team and look like "no traffic". We hard-fail on mismatch below.
const positional = [];
let explicitCwd = null;
for (let i = 2; i < process.argv.length; i++) {
const a = process.argv[i];
if (a === '--cwd' && i + 1 < process.argv.length) {
explicitCwd = process.argv[++i];
} else if (a.startsWith('--cwd=')) {
explicitCwd = a.slice('--cwd='.length);
} else {
positional.push(a);
}
}
const mergedPath = positional[0];
const gatePath = positional[1];
if (!mergedPath || !gatePath) {
console.error('usage: node scripts/deep-dive.mjs <merged.json> <gate.json> [--cwd <project-dir>]');
process.exit(1);
}
const [merged, gate] = await Promise.all([
readFile(mergedPath, 'utf-8').then(JSON.parse),
readFile(gatePath, 'utf-8').then(JSON.parse),
]);
if (explicitCwd) {
process.chdir(explicitCwd);
log(`cwd: ${process.cwd()} (via --cwd)`);
}
const link = await readProjectJson(process.cwd());
if (!link) {
console.error(`[deep-dive] FATAL: cwd ${process.cwd()} has no .vercel/project.json or .vercel/repo.json.`);
console.error(' Re-run with --cwd <project-dir> pointing at the linked project, or cd into it first.');
console.error(' (The Vercel CLI resolves team/project from cwd; without a .vercel/ linkage every query returns empty rows for the wrong team.)');
process.exit(2);
}
if (merged.projectId && link.projectId !== merged.projectId) {
console.error('[deep-dive] FATAL: cwd .vercel/ links a different project than merged.json.');
console.error(' Re-run with --cwd <dir-linked-to-the-collected-project>.');
process.exit(2);
}
if (merged.orgId && link.orgId && link.orgId !== merged.orgId) {
console.error('[deep-dive] FATAL: cwd .vercel/ links the project to a different Vercel scope than signals.json.');
console.error(' Re-run with --cwd <dir-linked-to-the-collected-project>, or rerun collect-signals.mjs from the intended app directory.');
process.exit(2);
}
log(`cwd link OK (source ${link.source})`);
const commandScope = await resolveDeepDiveCommandScope(merged, link);
if (!commandScope.ok) {
console.error(`[deep-dive] FATAL: could not resolve a CLI-safe Vercel scope (${commandScope.detail ?? commandScope.error ?? 'unknown'}).`);
console.error(' Re-run collect-signals.mjs with the current skill, run `vercel switch <team>`, or re-link with `vercel link --yes --project <project> --team <team-slug>`.');
process.exit(2);
}
if (typeof commandScope.cliScope === 'string' && /^(team|usr)_/.test(commandScope.cliScope)) {
console.error('[deep-dive] FATAL: commandScope.cliScope is a raw account ID, not a CLI-safe scope.');
console.error(' Re-run collect-signals.mjs with the current skill so deep-dive queries use the same team as the broad pass.');
process.exit(2);
}
const commandAccountId = commandScope.teamId ?? commandScope.userId ?? null;
if (commandAccountId && link.orgId && link.orgId !== commandAccountId) {
console.error('[deep-dive] FATAL: cwd .vercel/ links the project to a different Vercel scope than commandScope.');
console.error(' Re-run with --cwd <dir-linked-to-the-collected-project>, or rerun collect-signals.mjs from the intended app directory.');
process.exit(2);
}
const scope = commandScope.cliScope || undefined;
log(`command scope resolved (source=${commandScope.source}; scoped=${scope ? 'yes' : 'no'})`);
const toLaunch = Array.isArray(gate.toLaunch) ? gate.toLaunch : [];
const platform = Array.isArray(gate.platform) ? gate.platform : [];
log(`enriching ${toLaunch.length} toLaunch + ${platform.length} platform candidate(s) (window=${TIME_WINDOW})`);
const t0 = Date.now();
const errors = [];
// Flatten {candidate, spec}, fire all CLI calls in one Promise.all, re-group.
// Avoids per-candidate sequentiality.
const allCandidates = [...toLaunch.map((c, i) => ({ c, group: 'toLaunch', i })),
...platform.map((c, i) => ({ c, group: 'platform', i }))];
const flatJobs = [];
const skipNotes = new Map();
for (const entry of allCandidates) {
const specs = specsForCandidate(entry.c);
if (specs.length === 0) {
if (SCANNER_KINDS.has(entry.c.kind)) {
skipNotes.set(`${entry.group}:${entry.i}`, 'scanner-driven (no deep-dive needed)');
} else if (entry.c.kind === 'platform_fluid_compute') {
skipNotes.set(`${entry.group}:${entry.i}`, 'reused from broad pass (fnStartTypeByRoute)');
} else {
skipNotes.set(`${entry.group}:${entry.i}`, `no deep-dive spec for kind=${entry.c.kind}`);
}
continue;
}
for (const spec of specs) {
flatJobs.push({ entry, spec });
}
}
// Cut CLI calls two ways: (1) extract per-route slices already collected in
// the broad pass; (2) dedupe identical queries across candidates (same route
// can fire multiple gates wanting the same metric).
let extractedFromBroadPass = 0;
let dedupedQueryHits = 0;
const broadPassResults = [];
const remainingJobs = [];
for (const job of flatJobs) {
const extracted = tryExtractFromBroadPass(job.spec, merged);
if (extracted) {
broadPassResults.push({ entry: job.entry, spec: job.spec, ok: true, ...extracted });
extractedFromBroadPass++;
} else {
remainingJobs.push(job);
}
}
// One CLI call per unique dedup key; jobs sharing a key share the result.
const queryGroups = new Map();
for (const job of remainingJobs) {
const key = queryKey(job.spec, scope);
if (!queryGroups.has(key)) {
queryGroups.set(key, { spec: job.spec, jobs: [] });
}
queryGroups.get(key).jobs.push(job);
}
dedupedQueryHits = remainingJobs.length - queryGroups.size;
const totalCliQueries = queryGroups.size;
log(`${flatJobs.length} specs total: ${extractedFromBroadPass} extracted from broad-pass, ${dedupedQueryHits} deduped, ${totalCliQueries} CLI queries to run`);
const groupResults = await Promise.all([...queryGroups.values()].map(async ({ spec, jobs }) => {
const r = await queryMetric(spec.metricId, {
aggregation: spec.aggregation,
groupBy: spec.groupBy,
filter: spec.filter,
since: spec.since,
limit: spec.limit,
scope,
});
return { spec, jobs, response: r };
}));
const cliResults = [];
for (const { spec, jobs, response: r } of groupResults) {
if (!r.ok) {
for (const job of jobs) {
errors.push({
candidateGroup: job.entry.group,
candidateIndex: job.entry.i,
kind: job.entry.c.kind,
route: job.entry.c.route ?? job.entry.c.hostname ?? null,
specId: spec.id,
code: r.code,
});
cliResults.push({ entry: job.entry, spec, ok: false, error: r.code });
}
continue;
}
const norm = normalizeResponse(r.data, spec);
for (const job of jobs) {
cliResults.push({ entry: job.entry, spec, ok: true, ...norm });
}
}
const results = [...broadPassResults, ...cliResults];
const wallMs = Date.now() - t0;
log(`done in ${wallMs}ms (${totalCliQueries} CLI queries, ${extractedFromBroadPass} extracted from broad-pass, ${dedupedQueryHits} deduped, ${errors.length} errors)`);
const byCandidate = new Map();
for (const res of results) {
const k = `${res.entry.group}:${res.entry.i}`;
if (!byCandidate.has(k)) byCandidate.set(k, []);
byCandidate.get(k).push(res);
}
function enrich(c, group, i) {
const k = `${group}:${i}`;
const note = skipNotes.get(k);
if (note) {
return {
...c,
evidence: {
...(c.evidence ?? {}),
deepDive: { note },
},
};
}
const list = byCandidate.get(k) ?? [];
const merged = mergeIntoEvidence(list);
return {
...c,
evidence: {
...(c.evidence ?? {}),
deepDive: merged,
},
};
}
const enrichedToLaunch = toLaunch.map((c, i) => enrich(c, 'toLaunch', i));
const enrichedPlatform = platform.map((c, i) => enrich(c, 'platform', i));
const out = {
schemaVersion: SCHEMA_VERSION,
appliedAt: new Date().toISOString(),
candidatesEnriched: toLaunch.length + platform.length,
specsTotal: flatJobs.length,
queriesRun: totalCliQueries,
extractedFromBroadPass,
dedupedQueryHits,
totalWallMs: wallMs,
errors,
toLaunch: enrichedToLaunch,
platform: enrichedPlatform,
};
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
}
async function resolveDeepDiveCommandScope(merged, link) {
const linkedOrgId = merged.orgId ?? link.orgId ?? null;
if (merged.commandScope?.ok && (merged.commandScope.cliScope || !linkedOrgId)) {
return merged.commandScope;
}
if (merged.commandScope && merged.commandScope.ok === false) return merged.commandScope;
return await resolveCommandScope({
projectId: merged.projectId ?? link.projectId ?? null,
orgId: merged.orgId ?? link.orgId ?? null,
});
}
// Reduce CLI response to {value} or {rows:[{value,...dims}]}. The per-metric
// underscore field (e.g. vercel_function_invocation_count_sum) gets renamed
// to `value` for compactness.
function normalizeResponse(data, spec) {
if (!data || !Array.isArray(data.summary)) return { value: null };
const field = `${spec.metricId.replace(/\./g, '_')}_${spec.aggregation}`;
if (spec.groupBy.length === 0) {
const first = data.summary[0];
if (!first) return { value: null };
const v = first[field];
return { value: typeof v === 'number' ? round4(v) : null };
}
const rows = data.summary.map((row) => {
const out = { value: typeof row[field] === 'number' ? round4(row[field]) : null };
for (const dim of spec.groupBy) {
if (row[dim] !== undefined) out[dim] = row[dim];
}
return out;
});
return { rows };
}
function round4(n) {
if (!Number.isFinite(n)) return n;
return Math.round(n * 10000) / 10000;
}
// Skip the CLI call when broad-pass already collected the same metric grouped
// by [route, dim]. Returns {rows} on hit, null on miss. Cuts rate-limit pressure
// for per-route slice specs (startTypeSplit, cacheBreakdown, methodDistribution).
function tryExtractFromBroadPass(spec, merged) {
const eq = spec.broadPassEquivalent;
if (!eq) return null;
const broadRows = merged?.metrics?.[eq.key]?.rows;
if (!Array.isArray(broadRows)) return null;
const rows = [];
for (const row of broadRows) {
if (row.route !== eq.routeFilter) continue;
const out = { value: typeof row.value === 'number' ? row.value : null };
for (const dim of (eq.projectDims ?? [])) {
if (row[dim] !== undefined) out[dim] = row[dim];
}
rows.push(out);
}
// Zero rows ≠ "no data" — broad-pass row limit may have truncated the route.
// Fall through to CLI so the caller gets a definitive answer.
if (rows.length === 0) return null;
return { rows };
}
// Two specs sharing this key answer the same question — one CLI call serves both.
// Must include everything that affects the CLI's arg list.
function queryKey(spec, scope) {
const groupBy = [...(spec.groupBy ?? [])].sort();
return JSON.stringify({
metricId: spec.metricId,
aggregation: spec.aggregation,
groupBy,
filter: spec.filter ?? null,
since: spec.since ?? null,
limit: spec.limit ?? null,
scope: scope ?? null,
});
}
main().catch((err) => {
console.error('[deep-dive] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
scripts/gate-investigations.mjs
#!/usr/bin/env node
// Pure-JS deterministic gate. Reads merged signals.json, emits
// {toLaunch, platform, gated}. Same input → byte-identical output (modulo
// appliedAt). Sort keys are stable and explicit — never change without
// a co-located golden-output test update.
import { readFile } from 'node:fs/promises';
import { gates, DEFAULT_MAX_CODE_CANDIDATES, GATE_VERSION } from '../lib/gates/index.mjs';
import { applyAuthDisqualifier } from '../lib/auth-route.mjs';
import { dedupeCandidates } from '../lib/route-normalize.mjs';
import { validateCandidates } from '../lib/gates/contract.mjs';
import { applyHardGates } from '../lib/gates/hard-gates.mjs';
import { selectLaunchCandidates } from '../lib/gates/select-candidates.mjs';
import { routePathMatchScore } from '../lib/investigation-brief.mjs';
const SCHEMA_VERSION = '1.1';
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.signalsPath) {
console.error('usage: node scripts/gate-investigations.mjs <signals.json> [--max-candidates N|all]');
console.error(' VERCEL_OPTIMIZE_MAX_CANDIDATES env var supported (same values)');
process.exit(1);
}
const budget = resolveBudget(args);
const signals = JSON.parse(await readFile(args.signalsPath, 'utf-8'));
const allSeeds = gates.flatMap((g) => {
try {
return g.gate(signals) ?? [];
} catch (err) {
console.error(`[gate-investigations] gate ${g.metadata?.id} threw: ${err.message}`);
return [];
}
});
const validSeeds = validateCandidates(allSeeds, { source: 'gate-output' });
const annotated = validSeeds.map(applyAuthDisqualifier);
const sorted = annotated.slice().sort(stableCompare);
// Next.js 16 segment-tree metric paths surface the same source file under
// many encoded labels (city variants, _tree/_index siblings, base64 flag
// prefixes). Without dedup the budget gets shredded ~4-10x per page.
const { deduped, dropped } = dedupeCandidates(sorted);
const displayAnnotated = deduped.map((candidate) => attachDisplayRoute(candidate, signals));
const hardGateResult = applyHardGates(displayAnnotated, signals);
const gateable = hardGateResult.allowed;
// Account-scope candidates don't compete with code-scope for the budget.
const codeScoped = gateable.filter((c) => !c.disqualified && c.scope !== 'account');
const platformScoped = gateable.filter((c) => !c.disqualified && c.scope === 'account');
const selection = selectLaunchCandidates(codeScoped, budget, {
diversify: args.budgetSource === 'default',
});
const toLaunch = selection.selected;
const skippedByBudget = selection.skipped;
const budgetLabel = budget === Infinity ? 'unlimited (all)' : String(budget);
const gated = [
...gateable
.filter((c) => c.disqualified)
.map((c) => ({ ...c, gatedReason: c.disqualifyReason ?? 'disqualified' })),
...hardGateResult.gated,
...skippedByBudget.map((c) => ({
...c,
gatedReason: `skippedByBudget (max-candidates=${budgetLabel}; raise with --max-candidates N or =all)`,
})),
...dropped.map((d) => ({
...d.candidate,
gatedReason: `coveredBy (${d.mergedInto}) — ${d.reason}`,
})),
];
process.stdout.write(JSON.stringify({
schemaVersion: SCHEMA_VERSION,
gateVersion: GATE_VERSION,
appliedAt: new Date().toISOString(),
budget: {
maxCandidates: budget === Infinity ? 'all' : budget,
source: args.budgetSource,
selection: selection.selectionMode,
},
toLaunch,
platform: platformScoped,
gated,
gateMetadata: gates.map((g) => ({
id: g.metadata?.id,
threshold: g.metadata?.threshold,
billingDimension: g.metadata?.billingDimension,
sourceCitation: g.metadata?.sourceCitation,
})),
}, null, 2) + '\n');
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--max-candidates') out.maxCandidatesArg = argv[++i];
else if (a.startsWith('--max-candidates=')) out.maxCandidatesArg = a.slice('--max-candidates='.length);
else out.positional.push(a);
}
out.signalsPath = out.positional[0];
return out;
}
function resolveBudget(args) {
const raw = args.maxCandidatesArg ?? process.env.VERCEL_OPTIMIZE_MAX_CANDIDATES;
if (raw == null || raw === '') {
args.budgetSource = 'default';
return DEFAULT_MAX_CODE_CANDIDATES;
}
const trimmed = String(raw).trim().toLowerCase();
if (trimmed === 'all' || trimmed === 'unlimited' || trimmed === '-1') {
args.budgetSource = args.maxCandidatesArg != null ? 'flag' : 'env';
return Infinity;
}
const n = Number(trimmed);
if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
console.error(`[gate-investigations] bad budget value '${raw}'; expected positive integer or 'all'`);
process.exit(2);
}
args.budgetSource = args.maxCandidatesArg != null ? 'flag' : 'env';
return n;
}
// Total ordering: priority desc, kind asc, route asc. Underpins byte-identical output.
function stableCompare(a, b) {
const pa = a.priority ?? 0;
const pb = b.priority ?? 0;
if (pa !== pb) return pb - pa;
const ka = String(a.kind ?? '');
const kb = String(b.kind ?? '');
if (ka !== kb) return ka.localeCompare(kb);
const ra = String(a.route ?? a.hostname ?? '');
const rb = String(b.route ?? b.hostname ?? '');
return ra.localeCompare(rb);
}
function attachDisplayRoute(candidate, signals) {
if (!candidate || candidate.scope !== 'route' || typeof candidate.route !== 'string') return candidate;
if (!candidate.route.includes('[*]')) return candidate;
const routes = (signals.codebase?.routes ?? [])
.map((route) => route?.routePath)
.filter((routePath) => typeof routePath === 'string' && routePath.length > 0);
if (routes.length === 0) return candidate;
let bestRoute = null;
let bestScore = 0;
for (const routePath of routes) {
const score = routePathMatchScore(routePath, candidate.route);
if (score > bestScore) {
bestRoute = routePath;
bestScore = score;
}
}
if (!bestRoute || bestScore <= 0 || bestRoute === candidate.route) return candidate;
return { ...candidate, displayRoute: bestRoute };
}
main().catch((err) => {
console.error('[gate-investigations] FAILED:', err.message);
process.exit(1);
});
scripts/merge-signals.mjs
#!/usr/bin/env node
// Deterministically combines Vercel metric collection with the local codebase
// scan. Keeps the merged artifact shape stable: collect-signals output at the
// top level, scan-codebase output under `codebase`.
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
import { realpathSync } from 'node:fs';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { routePathMatchScore } from '../lib/investigation-brief.mjs';
import { canonicalizeRoute } from '../lib/route-normalize.mjs';
const log = (...args) => console.error('[merge-signals]', ...args);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.signalsPath || !args.codebasePath) {
console.error('usage: node scripts/merge-signals.mjs <signals.json> <codebase.json> [--out merged.json] [--force]');
process.exit(1);
}
const [signals, codebase] = await Promise.all([
readJson(args.signalsPath, 'signals'),
readJson(args.codebasePath, 'codebase scan'),
]);
const merged = mergeSignals(signals, codebase);
const body = JSON.stringify(merged, null, 2) + '\n';
if (args.outPath) {
await writeOutput(args.outPath, body, { force: args.force });
log(`wrote ${args.outPath}`);
} else {
process.stdout.write(body);
}
}
export function mergeSignals(signals, codebase) {
assertObject(signals, 'signals');
assertObject(codebase, 'codebase scan');
if (!signals.schemaVersion) {
throw new Error('signals.json is missing schemaVersion; pass collect-signals output as the first file.');
}
if (!Array.isArray(codebase.routes) || !Array.isArray(codebase.findings) || !codebase.stack) {
throw new Error('codebase.json must be scan-codebase output with stack, routes[], and findings[].');
}
return {
...signals,
codebase: annotateCodebaseScan(signals, codebase),
};
}
export function annotateCodebaseScan(signals, codebase) {
const index = buildRouteMetricIndex(signals);
return {
...codebase,
findings: (codebase.findings ?? []).map((finding) => annotateFinding(finding, index)),
};
}
function annotateFinding(finding, index) {
if (!finding || typeof finding !== 'object') return finding;
if (finding.trafficIndependent) return finding;
if (!finding.route) return { ...finding, o11ySignal: 'NO-ROUTE-MAPPING' };
const summary = bestRouteSummary(finding.route, index);
if (!summary || !hasTraffic(summary)) return { ...finding, o11ySignal: 'COLD-PATH' };
return { ...finding, o11ySignal: formatRouteSignal(summary) };
}
function buildRouteMetricIndex(signals) {
const out = new Map();
const ensure = (route) => {
const canonical = canonicalizeRoute(route);
const existing = out.get(canonical) ?? { route: canonical };
out.set(canonical, existing);
return existing;
};
for (const row of rows(signals, 'fnStatusByRoute')) {
if (!row.route) continue;
const summary = ensure(row.route);
summary.functionRuns = (summary.functionRuns ?? 0) + numeric(row.value);
}
for (const row of rows(signals, 'fnDurationP95ByRoute')) {
if (!row.route) continue;
ensure(row.route).p95Ms = numeric(row.value);
}
for (const row of rows(signals, 'requestsByRouteCache')) {
if (!row.route) continue;
const summary = ensure(row.route);
const count = numeric(row.value);
summary.requests = (summary.requests ?? 0) + count;
if (String(row.cache_result).toUpperCase() === 'HIT') {
summary.cacheHits = (summary.cacheHits ?? 0) + count;
}
}
return out;
}
function rows(signals, metricId) {
const rows = signals?.metrics?.[metricId]?.rows;
return Array.isArray(rows) ? rows : [];
}
function numeric(value) {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
}
function bestRouteSummary(route, index) {
const canonical = canonicalizeRoute(route);
const exact = index.get(canonical);
if (exact) return exact;
let best = null;
for (const summary of index.values()) {
const score = routePathMatchScore(canonical, summary.route);
if (score <= 0) continue;
if (!best || score > best.score) best = { score, summary };
}
return best?.summary ?? null;
}
function hasTraffic(summary) {
return (summary.functionRuns ?? 0) > 0 || (summary.requests ?? 0) > 0;
}
function formatRouteSignal(summary) {
const parts = [];
if ((summary.functionRuns ?? 0) > 0) parts.push(`inv=${Math.round(summary.functionRuns)}`);
else if ((summary.requests ?? 0) > 0) parts.push(`requests=${Math.round(summary.requests)}`);
if ((summary.p95Ms ?? 0) > 0) parts.push(`p95=${Math.round(summary.p95Ms)}ms`);
if ((summary.requests ?? 0) > 0 && summary.cacheHits != null) {
const hitRate = Math.round((summary.cacheHits / summary.requests) * 100);
parts.push(`cache=${hitRate}%`);
}
return parts.join(',') || 'COLD-PATH';
}
function parseArgs(argv) {
const out = { positional: [], force: false };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--out') out.outPath = argv[++i];
else if (a.startsWith('--out=')) out.outPath = a.slice('--out='.length);
else if (a === '--force') out.force = true;
else out.positional.push(a);
}
out.signalsPath = out.positional[0];
out.codebasePath = out.positional[1];
return out;
}
async function readJson(path, label) {
try {
return JSON.parse(await readFile(path, 'utf-8'));
} catch (err) {
throw new Error(`Could not read ${label} JSON at ${path}: ${err.message}`);
}
}
function assertObject(value, label) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} must be a JSON object.`);
}
}
async function writeOutput(path, body, { force }) {
if (!force && await exists(path)) {
throw new Error(`output file already exists: ${path}. Use a fresh run directory or pass --force to overwrite.`);
}
await mkdir(dirname(path), { recursive: true });
await writeFile(path, body);
}
async function exists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
main().catch((err) => {
console.error('[merge-signals] FAILED:', err.message);
process.exit(1);
});
}
scripts/prepare-investigation-brief.mjs
#!/usr/bin/env node
// Emits the ENTIRE prompt a sub-agent sees for one candidate (candidate +
// deep-dive evidence + filtered citations + playbook + protocol + output
// schema). --list emits a manifest the orchestrator uses to decide fan-out
// vs serial. Brief → stdout, status → stderr.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import {
buildBrief,
inferPlaybook,
inferFrameworkPlaybook,
resolveFiles,
citationSubset,
} from '../lib/investigation-brief.mjs';
import { supportTopicSubset } from '../lib/support-topics.mjs';
import { candidateRefFor } from '../lib/reconcile-candidates.mjs';
import { formatCandidateLabel } from '../lib/display-labels.mjs';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const PLAYBOOKS_DIR = join(HERE, '..', 'references', 'playbooks');
const log = (...a) => console.error('[prepare-brief]', ...a);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.mergedPath || !args.investigationPath) {
console.error('usage: node scripts/prepare-investigation-brief.mjs <merged.json> <investigation.json> [--index N] [--group toLaunch|platform] [--out FILE]');
console.error(' or: node scripts/prepare-investigation-brief.mjs <merged.json> <investigation.json> --list');
process.exit(1);
}
const [merged, investigation] = await Promise.all([
readFile(args.mergedPath, 'utf-8').then(JSON.parse),
readFile(args.investigationPath, 'utf-8').then(JSON.parse),
]);
if (args.list) {
const manifest = buildManifest(merged, investigation);
process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
return;
}
const group = args.group ?? 'toLaunch';
const index = args.index ?? 0;
const pool = Array.isArray(investigation[group]) ? investigation[group] : [];
if (index < 0 || index >= pool.length) {
console.error(`[prepare-brief] FATAL: ${group}[${index}] out of range (${group} has ${pool.length} entries)`);
process.exit(2);
}
let candidate = pool[index];
// Scan output may live at merged.codebase (older shape) or merged.signals.codebase
// (current shape, after the jq merge nests it under signals). Resolve either.
const codebase = pickCodebase(merged);
const signals = {
...merged,
codebase,
};
const files = resolveFiles(candidate, signals);
candidate = {
...candidate,
candidateRef: candidate.candidateRef ?? candidateRefFor(candidate, files),
};
const playbookId = inferPlaybook(signals);
const playbookBody = playbookId ? await tryReadPlaybook(playbookId) : null;
const frameworkPlaybookId = inferFrameworkPlaybook(signals);
const frameworkPlaybookBody = frameworkPlaybookId ? await tryReadPlaybook(frameworkPlaybookId) : null;
const stack = signals.stack ?? signals.codebase?.stack ?? {};
const framework = stack.framework ?? 'unknown';
const version = stack.frameworkVersion ?? 'unknown';
const citations = await citationSubset(candidate.kind, framework, version);
const supportTopics = await supportTopicSubset({
candidate,
signals,
framework,
version,
profile: playbookId,
frameworkPlaybookId,
});
const brief = buildBrief({
candidate,
candidateIndex: index,
candidateGroup: group,
files,
signals,
citations,
playbookId,
playbookBody,
frameworkPlaybookId,
frameworkPlaybookBody,
supportTopics,
generatedAt: args.deterministic ? null : new Date().toISOString(),
});
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeBriefFile(args.outPath, brief, { force: args.force });
log(`wrote ${brief.length}B → ${args.outPath}`);
} else {
process.stdout.write(brief + '\n');
}
}
function buildManifest(merged, investigation) {
const out = [];
const groups = ['toLaunch', 'platform'];
for (const group of groups) {
const pool = Array.isArray(investigation[group]) ? investigation[group] : [];
pool.forEach((c, i) => {
const files = resolveFiles(c, { ...merged, codebase: pickCodebase(merged) });
const candidateRef = c.candidateRef ?? candidateRefFor(c, files);
out.push({
group,
index: i,
kind: c.kind,
route: c.route ?? c.hostname ?? null,
scope: c.scope ?? null,
priority: c.priority ?? null,
confidence: c.confidence ?? null,
o11ySignal: c.o11ySignal ?? null,
files,
candidateRef,
label: formatCandidateLabel({ ...c, files }),
});
});
}
return {
schemaVersion: '1.0',
totalBriefs: out.length,
toLaunchCount: out.filter((b) => b.group === 'toLaunch').length,
platformCount: out.filter((b) => b.group === 'platform').length,
preResolvedRecords: Array.isArray(investigation.preResolvedRecords)
? investigation.preResolvedRecords
: [],
fanoutPlan: buildFanoutPlan(out),
briefs: out,
};
}
function buildFanoutPlan(briefs) {
const groups = new Map();
for (const brief of briefs) {
const key = candidateFamilyKey(brief);
const existing = groups.get(key) ?? {
familyKey: key,
label: brief.label,
kind: brief.kind,
primaryBrief: { group: brief.group, index: brief.index, candidateRef: brief.candidateRef },
relatedBriefs: [],
};
if (existing.primaryBrief.candidateRef !== brief.candidateRef) {
existing.relatedBriefs.push({ group: brief.group, index: brief.index, candidateRef: brief.candidateRef });
}
groups.set(key, existing);
}
return {
totalFamilies: groups.size,
families: [...groups.values()].map((g) => ({
...g,
totalBriefs: 1 + g.relatedBriefs.length,
})),
};
}
function candidateFamilyKey(brief) {
const file = Array.isArray(brief.files) && brief.files.length > 0 ? brief.files[0] : null;
const target = file ?? brief.route ?? brief.scope ?? '<account>';
return `${brief.kind ?? 'unknown'}:${target}`;
}
// Prefer merged.codebase, fall back to merged.signals.codebase, then empty.
// Also accepts a fully-shaped scan doc directly (used in tests).
function pickCodebase(merged) {
if (!merged || typeof merged !== 'object') return {};
if (merged.codebase && typeof merged.codebase === 'object' && (merged.codebase.routes || merged.codebase.findings)) {
return merged.codebase;
}
if (merged.signals?.codebase && typeof merged.signals.codebase === 'object') {
return merged.signals.codebase;
}
return {};
}
async function tryReadPlaybook(id) {
try {
return await readFile(join(PLAYBOOKS_DIR, `${id}.md`), 'utf-8');
} catch {
return null;
}
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--index') out.index = Number(argv[++i]);
else if (a.startsWith('--index=')) out.index = Number(a.slice('--index='.length));
else if (a === '--group') out.group = argv[++i];
else if (a.startsWith('--group=')) out.group = a.slice('--group='.length);
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else if (a === '--list') out.list = true;
else if (a === '--deterministic') out.deterministic = true;
else if (a === '--force') out.force = true;
else out.positional.push(a);
}
out.mergedPath = out.positional[0];
out.investigationPath = out.positional[1];
return out;
}
async function writeBriefFile(outPath, brief, { force = false } = {}) {
try {
await writeFile(outPath, brief + '\n', { encoding: 'utf-8', flag: force ? 'w' : 'wx' });
} catch (err) {
if (err?.code === 'EEXIST') {
throw new Error(`output file already exists: ${outPath}. Use a fresh run directory or pass --force to overwrite.`);
}
throw err;
}
}
main().catch((err) => {
console.error('[prepare-brief] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
scripts/reconcile-candidates.mjs
#!/usr/bin/env node
// Deterministic reconciliation between deep-dive and investigator fan-out.
// Reads investigation-evidence.json, removes candidates whose follow-up metric
// evidence already disproves/reframes the gate hypothesis, and emits the same
// shape with preResolvedRecords for the final report.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { reconcileInvestigation } from '../lib/reconcile-candidates.mjs';
const log = (...a) => console.error('[reconcile-candidates]', ...a);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.investigationPath) {
console.error('usage: node scripts/reconcile-candidates.mjs <investigation-evidence.json> [--gate gate.json] [--out reconciled-investigation.json]');
process.exit(1);
}
const [investigation, gate] = await Promise.all([
readFile(args.investigationPath, 'utf-8').then(JSON.parse),
args.gatePath ? readFile(args.gatePath, 'utf-8').then(JSON.parse) : null,
]);
const reconciled = reconcileInvestigation(investigation, { gate });
const serialized = JSON.stringify({
...reconciled,
reconciledAt: args.noTimestamp ? null : new Date().toISOString(),
}, null, 2) + '\n';
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeFile(args.outPath, serialized, 'utf-8');
log(`wrote ${serialized.length}B -> ${args.outPath}`);
} else {
process.stdout.write(serialized);
}
const dropped = reconciled.reconciliation?.droppedBeforeInvestigation ?? 0;
if (dropped > 0) log(`dropped ${dropped} candidate(s) before investigation`);
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--gate') out.gatePath = resolve(argv[++i]);
else if (a.startsWith('--gate=')) out.gatePath = resolve(a.slice('--gate='.length));
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else if (a === '--no-timestamp') out.noTimestamp = true;
else out.positional.push(a);
}
out.investigationPath = out.positional[0] ? resolve(out.positional[0]) : null;
return out;
}
main().catch((err) => {
console.error('[reconcile-candidates] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
scripts/render-report.mjs
#!/usr/bin/env node
// Final pipeline step. Emits customer-facing markdown from
// recommendations.json + gate.json + signals.json.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { buildFinalReportMessage, renderReport } from '../lib/render-report.mjs';
import { dedupeRecommendations } from '../lib/dedup-recs.mjs';
import { canonicalizeRoute } from '../lib/route-normalize.mjs';
import { hasUnsupportedCacheLifeCdnText, splitCustomerSafeObservations } from '../lib/observation-safety.mjs';
const log = (...a) => console.error('[render-report]', ...a);
const HARD_REGEN_TRIGGERS = new Set([
'project_config_contradiction',
'cache_vary_safety',
'semantic_safety',
]);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.recsPath || !args.gatePath || !args.signalsPath) {
console.error('usage: node scripts/render-report.mjs <recommendations.json> <gate.json> <signals.json> [--project NAME] [--out FILE] [--message-out FILE] [--no-timestamp] [--debug-out FILE]');
process.exit(1);
}
const [recsRaw, gateRaw, signalsRaw] = await Promise.all([
readFile(args.recsPath, 'utf-8').then(JSON.parse),
readFile(args.gatePath, 'utf-8').then(JSON.parse),
readFile(args.signalsPath, 'utf-8').then(JSON.parse),
]);
// Accept either a raw rec array OR the verify-and-regen wrapper
// {recsGraded, qualityDropped, ...}. Stale-rec defense: when verify-and-regen
// flagged a hard-safety issue but the orchestrator skipped re-spawn, the
// original rec is still in recsGraded. Filter it here so it can't ship; it
// surfaces in "Investigated, no change recommended" instead.
const hardRegenRefs = new Set(
Array.isArray(recsRaw.regenPlan)
? recsRaw.regenPlan
.filter((p) => HARD_REGEN_TRIGGERS.has(p.regenTrigger))
.map((p) => p.candidateRef)
.filter(Boolean)
: []
);
const activeCandidates = [
...(Array.isArray(gateRaw.toLaunch) ? gateRaw.toLaunch : []),
...(Array.isArray(gateRaw.platform) ? gateRaw.platform : []),
];
const enforceCurrentGate = !Array.isArray(recsRaw) && activeCandidates.length > 0;
const staleRecommendationDrops = [];
const wrapperRecommendations = Array.isArray(recsRaw.renderableRecommendations)
? recsRaw.renderableRecommendations
: (recsRaw.recsGraded ?? []);
const needsReviewDrops = [];
const candidateRecommendations = Array.isArray(recsRaw)
? recsRaw.filter((r) => r?.abstain !== true)
: wrapperRecommendations
.filter((r, i) => (r.quality?.overall ?? 0) >= 0.55)
.filter((r) => !hardRegenRefs.has(r.candidateRef));
const recommendationsRaw = candidateRecommendations
.filter((r) => {
if (r?.abstain === true || r?.needsReview !== true) return true;
needsReviewDrops.push({
candidateRef: r.candidateRef ?? null,
reason: 'This recommendation needs a manual safety review before it is ready to apply.',
});
return false;
})
.filter((r) => {
if (!enforceCurrentGate) return true;
if (recommendationMatchesActiveCandidate(r, activeCandidates)) return true;
staleRecommendationDrops.push({
candidateRef: r.candidateRef ?? null,
reason: 'This recommendation came from a candidate that is not in the current run output. Re-run from a clean run directory before applying it.',
});
return false;
});
const recommendations = dedupeRecommendations(recommendationsRaw);
const readyTargets = new Set(
recommendations
.map((r) => candidateTarget(r?.candidateRef))
.filter(Boolean)
);
const droppedContradictions = !Array.isArray(recsRaw)
? (recsRaw.recsGraded ?? [])
.map((r, i) => ({ r, i }))
.filter(({ r }) => hardRegenRefs.has(r.candidateRef))
.map(({ r, i }) => ({
candidateRef: r.candidateRef ?? null,
reason: publicHardRegenReason(recsRaw.regenPlan?.find((p) => p.index === i || p.candidateRef === r.candidateRef)),
}))
: [];
const gated = Array.isArray(gateRaw.gated) ? gateRaw.gated : [];
// No-change findings are first-class investigation outputs ("the hypothesis didn't hold").
// Contradiction-dropped recs ride alongside them so customers see WHY a rec
// was held back instead of it silently disappearing.
const baseAbstentions = Array.isArray(recsRaw)
? recsRaw.filter((r) => r?.abstain === true).map((r) => ({
candidateRef: r.candidateRef ?? null,
reason: publicNoChangeReason(r.reason ?? '(no reason recorded)'),
}))
: (recsRaw.abstentions ?? []).map((r) => ({
...r,
reason: publicNoChangeReason(r.reason ?? '(no reason recorded)'),
}));
const publicBaseAbstentions = baseAbstentions.filter((r) => !readyTargets.has(candidateTarget(r?.candidateRef)));
// Observations: no-change findings carrying a structured non-perf finding
// (deployment regression, error storm, etc.).
const flattenedObservations = Array.isArray(recsRaw)
? flattenObservations(recsRaw.filter((r) => r?.abstain === true))
: flattenObservations([
...(Array.isArray(recsRaw.observations) ? recsRaw.observations : []),
...(Array.isArray(recsRaw.abstentions) ? recsRaw.abstentions : []),
]);
const { observations: safeObservations, heldBackObservations } = splitCustomerSafeObservations(flattenedObservations, baseAbstentions, signalsRaw);
const observations = suppressReadyCoveredObservations(safeObservations, recommendations);
const abstentions = [
...publicBaseAbstentions,
...droppedContradictions,
...staleRecommendationDrops,
...needsReviewDrops,
...(Array.isArray(recsRaw.withheldRecommendations) ? recsRaw.withheldRecommendations.map((d) => ({
candidateRef: d.candidateRef ?? null,
reason: publicWithheldReason(d),
needsEvidence: true,
})) : []),
...(Array.isArray(recsRaw.sanitizerDropped) ? recsRaw.sanitizerDropped.map((d) => ({
candidateRef: d.candidateRef ?? null,
reason: `This needs a closer review before it is safe to apply: ${d.reason ?? 'review required'}.`,
needsEvidence: true,
})) : []),
...(Array.isArray(recsRaw.heldBackObservations) ? recsRaw.heldBackObservations.map((d) => ({
...d,
needsEvidence: true,
})) : []),
...heldBackObservations,
];
// Full catalog lets the renderer recover o11ySignal + aliasRoutes that recs
// didn't propagate, and canonicalize segment-tree candidateRefs.
const allCandidates = [
...activeCandidates,
...gated,
];
const md = renderReport({
recommendations,
gated,
abstentions,
observations,
signals: signalsRaw,
candidates: allCandidates,
opts: {
projectName: args.projectName,
generatedAt: args.noTimestamp ? null : new Date().toISOString(),
heldBackCount: (Number.isInteger(recsRaw.summary?.withheldRecommendations)
? recsRaw.summary.withheldRecommendations
: (Array.isArray(recsRaw.regenPlan) ? recsRaw.regenPlan.length : 0) +
(Array.isArray(recsRaw.qualityDropped) ? recsRaw.qualityDropped.length : 0)) +
(Array.isArray(recsRaw.sanitizerDropped) ? recsRaw.sanitizerDropped.length : 0) +
(Array.isArray(recsRaw.heldBackObservations) ? recsRaw.heldBackObservations.length : 0) +
heldBackObservations.length,
noChangeCount: Number.isInteger(recsRaw.summary?.abstentions)
? Math.min(recsRaw.summary.abstentions, publicBaseAbstentions.length)
: publicBaseAbstentions.length,
},
});
if (args.debugOutPath) {
const debugArtifact = buildDebugArtifact({
recsRaw,
recommendationsRaw,
recommendations,
gateRaw,
abstentions,
observations,
heldBackObservations,
staleRecommendationDrops,
droppedContradictions,
});
const serializedDebug = JSON.stringify(debugArtifact, null, 2) + '\n';
await mkdir(dirname(args.debugOutPath), { recursive: true });
await writeFile(args.debugOutPath, serializedDebug, 'utf-8');
log(`wrote debug ${serializedDebug.length}B → ${args.debugOutPath}`);
}
if (args.messageOutPath) {
const messageArtifact = buildFinalReportMessage({
reportPath: args.outPath ?? '(stdout)',
markdown: md,
recommendations,
signals: signalsRaw,
});
const serializedMessage = JSON.stringify(messageArtifact, null, 2) + '\n';
await mkdir(dirname(args.messageOutPath), { recursive: true });
await writeFile(args.messageOutPath, serializedMessage, 'utf-8');
log(`wrote final message ${serializedMessage.length}B → ${args.messageOutPath}`);
}
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeFile(args.outPath, md + '\n', 'utf-8');
log(`wrote ${md.length}B → ${args.outPath}`);
} else {
process.stdout.write(md + '\n');
}
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--project') out.projectName = argv[++i];
else if (a.startsWith('--project=')) out.projectName = a.slice('--project='.length);
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else if (a === '--message-out') out.messageOutPath = resolve(argv[++i]);
else if (a.startsWith('--message-out=')) out.messageOutPath = resolve(a.slice('--message-out='.length));
else if (a === '--no-timestamp') out.noTimestamp = true;
else if (a === '--debug-out') out.debugOutPath = resolve(argv[++i]);
else if (a.startsWith('--debug-out=')) out.debugOutPath = resolve(a.slice('--debug-out='.length));
else if (a === '--debug') {
console.error('[render-report] --debug no longer writes internal details into customer markdown; use --debug-out FILE');
}
else out.positional.push(a);
}
out.recsPath = out.positional[0];
out.gatePath = out.positional[1];
out.signalsPath = out.positional[2];
return out;
}
function publicWithheldReason(record) {
switch (record?.reason) {
case 'needs_review':
return 'Automated checks added a safety caveat, so this run kept the recommendation out of the ready-to-apply list.';
case 'quality_floor':
return 'The recommendation did not meet the evidence bar for this report.';
case 'project_config_contradiction':
case 'cache_vary_safety':
case 'semantic_safety':
return publicHardRegenReason({ regenTrigger: record.reason });
default:
return 'This recommendation needs stronger evidence before it is safe to apply.';
}
}
function publicHardRegenReason(plan) {
switch (plan?.regenTrigger) {
case 'project_config_contradiction':
return 'The recommendation tried to turn on a project setting that is already enabled. Re-run the investigation with refreshed project-config evidence.';
case 'cache_vary_safety':
return 'The recommendation added shared CDN caching to output that varies by request geography without the required Vary header. Re-run the investigation with the cache-safety failure in scope.';
case 'semantic_safety':
return 'This recommendation needs stronger framework evidence before it is safe to apply. Re-run the investigation with that evidence in scope.';
default:
return 'This recommendation needs stronger evidence before it is safe to apply. Re-run the investigation with those checks in scope.';
}
}
function recommendationMatchesActiveCandidate(rec, candidates) {
const ref = parseCandidateRef(rec?.candidateRef);
if (!ref) return true;
return candidates.some((candidate) => candidateMatchesRef(candidate, ref));
}
function parseCandidateRef(ref) {
if (typeof ref !== 'string' || ref.length === 0) return null;
const [kind, ...targetParts] = ref.split(':');
if (!kind) return null;
return { kind, target: targetParts.join(':') };
}
function candidateMatchesRef(candidate, ref) {
if (!candidate || candidate.kind !== ref.kind) return false;
if (candidate.scope === 'account' || ref.target === '<account>') return true;
const candidateTarget = candidate.route ?? candidate.hostname ?? candidate.file ?? candidate.target ?? null;
if (!candidateTarget || !ref.target) return false;
const a = String(candidateTarget);
const b = String(ref.target);
return a === b || canonicalizeRoute(a) === canonicalizeRoute(b);
}
function suppressReadyCoveredObservations(observations, recommendations = []) {
if (!Array.isArray(observations) || observations.length === 0) return [];
const readyFamiliesByTarget = new Map();
for (const rec of recommendations) {
const parsed = parseCandidateRef(rec?.candidateRef);
const target = candidateTarget(rec?.candidateRef);
const family = candidateFamily(parsed?.kind);
if (!target || !family) continue;
const set = readyFamiliesByTarget.get(target) ?? new Set();
set.add(family);
readyFamiliesByTarget.set(target, set);
}
return observations.filter((observation) => {
const parsed = parseCandidateRef(observation?.candidateRef);
const target = candidateTarget(observation?.candidateRef);
const family = candidateFamily(parsed?.kind);
if (!target || !family) return true;
return !readyFamiliesByTarget.get(target)?.has(family);
});
}
function candidateFamily(kind) {
switch (kind) {
case 'uncached_route':
case 'cache_header_gap':
case 'missing_cache_headers':
case 'max_age_without_s_maxage':
return 'cache';
case 'slow_route':
case 'cold_start':
case 'external_api_slow':
case 'cwv_poor':
return 'performance';
case 'route_errors':
return 'reliability';
case 'isr_overrevalidation':
return 'isr';
case 'middleware_heavy':
return 'middleware';
case 'build_minutes_fanout':
return 'build';
default:
return kind || null;
}
}
function candidateTarget(ref) {
if (typeof ref !== 'string') return null;
const idx = ref.indexOf(':');
if (idx === -1) return null;
return ref.slice(idx + 1);
}
function publicNoChangeReason(reason) {
if (hasUnsupportedCacheLifeCdnText(reason)) {
return 'This candidate overlapped a cache-lifetime draft that did not meet the framework evidence bar. No supported change shipped from this run.';
}
return reason;
}
function buildDebugArtifact({
recsRaw,
recommendationsRaw,
recommendations,
gateRaw,
abstentions = [],
observations = [],
heldBackObservations = [],
staleRecommendationDrops = [],
droppedContradictions = [],
}) {
const wrapper = Array.isArray(recsRaw) ? null : recsRaw;
const sourceRecords = Array.isArray(recsRaw)
? recsRaw
: (recsRaw.recsGraded ?? []);
const summary = wrapper?.summary
? {
...wrapper.summary,
rawRecommendationCount: recommendationsRaw.length,
renderedRecommendationCount: recommendations.length,
}
: null;
return {
schemaVersion: '1.0',
summary,
regenPlan: wrapper?.regenPlan ?? [],
qualityDropped: wrapper?.qualityDropped ?? [],
withheldRecommendations: wrapper?.withheldRecommendations ?? [],
abstentions,
observations,
heldBackObservations,
staleRecommendationDrops,
droppedContradictions,
sanitizerDropped: wrapper?.sanitizerDropped ?? [],
renderedRecommendationCount: recommendations.length,
rawRecommendationCount: recommendationsRaw.length,
gateBudget: gateRaw?.budget ?? null,
recommendations: sourceRecords
.filter((record) => record && record.abstain !== true)
.map((record) => ({
candidateRef: record.candidateRef ?? null,
what: record.what ?? null,
verification: record.verification ?? null,
quality: record.quality ?? null,
passRate: record.passRate ?? record.verification?.passRate ?? null,
avgQuality: record.avgQuality ?? null,
needsReview: record.needsReview === true,
sanitizerTrail: Array.isArray(record.sanitizerTrail) ? record.sanitizerTrail : [],
})),
};
}
function flattenObservations(records) {
const out = [];
for (const record of records) {
if (!record || typeof record !== 'object') continue;
if (record.observation && typeof record.observation === 'object') {
out.push({
candidateRef: record.candidateRef ?? null,
summary: coerceOptionalString(record.observation.summary),
evidence: record.observation.evidence ?? null,
suggestedAction: record.observation.suggestedAction ?? null,
kind: record.observation.kind ?? 'other',
});
continue;
}
if ('summary' in record || 'evidence' in record || 'suggestedAction' in record || 'kind' in record) {
out.push({
candidateRef: record.candidateRef ?? null,
summary: coerceOptionalString(record.summary),
evidence: record.evidence ?? null,
suggestedAction: record.suggestedAction ?? null,
kind: record.kind ?? 'other',
});
}
}
return out;
}
function coerceOptionalString(value) {
return value == null ? value : String(value);
}
main().catch((err) => {
console.error('[render-report] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
scripts/scan-codebase.mjs
#!/usr/bin/env node
// Walks the repo, runs every scanner in lib/scanners/, emits findings + routes
// + stack as JSON. Output is merged into signals.codebase.*. New scanners drop
// into lib/scanners/ + the barrel; this file is closed for modification.
import { readdir, readFile } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { scanners } from '../lib/scanners/index.mjs';
import { detectStack } from '../lib/vercel.mjs';
import {
detectMonorepoRoot,
listWorkspacePackages,
buildResolver,
resolveWorkspaceImports,
} from '../lib/workspace-resolver.mjs';
const SCHEMA_VERSION = '1.0';
const SKIP_DIRS = new Set(['node_modules', '.next', '.vercel', 'dist', 'build', '.git', 'coverage', '.turbo', '__tests__', 'cypress']);
const SKIP_FILE_PATTERNS = [/\.test\./, /\.spec\./, /\.d\.ts$/];
async function main() {
const rootDir = process.argv[2] || process.cwd();
process.stderr.write(`[scan-codebase] scanning ${rootDir}\n`);
const [stack, files, routes] = await Promise.all([
detectStack(rootDir),
collectFiles(rootDir),
enumerateRoutes(rootDir),
]);
// In a monorepo, route files often re-export from workspace packages. Without
// resolving those, sub-agents abstain because the workspace path is outside
// their read scope.
const monorepoRoot = await detectMonorepoRoot(rootDir);
let workspacePackages = [];
let resolver = () => null;
if (monorepoRoot) {
workspacePackages = await listWorkspacePackages(monorepoRoot);
resolver = buildResolver(workspacePackages);
process.stderr.write(`[scan-codebase] monorepo root: ${monorepoRoot} (${workspacePackages.length} workspace packages)\n`);
}
await enrichRoutesWithWorkspaceImports(routes, rootDir, resolver, monorepoRoot);
process.stderr.write(`[scan-codebase] ${files.length} files, ${routes.length} routes, ${scanners.length} scanners\n`);
const findings = [];
for (const scanner of scanners) {
try {
const applicable = filterApplicable(files, scanner.metadata);
// Scanners may be sync or async (large-static-asset does fs.stat walks).
const found = await scanner.scan({ files: applicable, rootDir, routes, stack });
for (const f of (found ?? [])) {
findings.push({
...f,
route: mapFileToRoute(f.file, routes),
});
}
} catch (err) {
process.stderr.write(`[scan-codebase] scanner ${scanner.metadata?.id} threw: ${err.message}\n`);
}
}
findings.sort((a, b) =>
a.file.localeCompare(b.file)
|| (a.line ?? 0) - (b.line ?? 0)
|| a.pattern.localeCompare(b.pattern)
);
process.stdout.write(JSON.stringify({
schemaVersion: SCHEMA_VERSION,
scannedAt: new Date().toISOString(),
rootDir,
monorepoRoot: monorepoRoot ?? null,
workspacePackages: workspacePackages.map((p) => ({ name: p.name, dir: relative(monorepoRoot ?? rootDir, p.dir) })),
stack,
routes,
findings,
scannerMetadata: scanners.map((s) => ({
id: s.metadata.id,
title: s.metadata.title,
severity: s.metadata.severity,
billingDimension: s.metadata.billingDimension,
trafficIndependent: s.metadata.trafficIndependent,
})),
}, null, 2) + '\n');
process.stderr.write(`[scan-codebase] ${findings.length} finding(s)\n`);
}
// Record workspace-package imports per route so the brief allowlists them and
// sub-agents can investigate the real source rather than abstaining on a thin
// re-export shell. Capped to keep the brief focused (source order ≈ import order,
// so the primary view component usually leads).
const WORKSPACE_IMPORT_LIMIT_PER_ROUTE = 12;
async function enrichRoutesWithWorkspaceImports(routes, scanRootDir, resolver, monorepoRoot) {
if (!monorepoRoot) return;
for (const r of routes) {
if (!r?.file) continue;
const abs = join(scanRootDir, r.file);
const resolved = await resolveWorkspaceImports(abs, resolver, {
pureBarrelDepth: 3,
suffixFanoutDepth: 2,
perSpecifierCap: 3,
});
if (resolved.length === 0) continue;
// Paths must be relative to the monorepo root so they align between signals + verifier.
r.workspaceImports = resolved
.slice(0, WORKSPACE_IMPORT_LIMIT_PER_ROUTE)
.map((abs) => relative(monorepoRoot, abs));
}
}
async function collectFiles(root) {
const entries = await readdir(root, { recursive: true, withFileTypes: true });
const out = [];
for (const e of entries) {
if (!e.isFile()) continue;
const segments = (e.parentPath ?? e.path ?? root).split('/');
if (segments.some((s) => SKIP_DIRS.has(s))) continue;
if (SKIP_FILE_PATTERNS.some((re) => re.test(e.name))) continue;
if (!/\.(tsx?|jsx?|mjs|cjs|html|svelte|astro|vue|json)$/.test(e.name)) continue;
const full = join(e.parentPath ?? e.path ?? root, e.name);
try {
const content = await readFile(full, 'utf-8');
if (content.length > 500_000) continue;
out.push({ path: relative(root, full), content });
} catch {}
}
return out;
}
function filterApplicable(files, meta) {
const incl = meta.includeGlobs ?? ['**/*'];
return files.filter((f) => incl.some((g) => globMatch(g, f.path)));
}
// Tiny glob → regex. Supports **, *, and {a,b} alternation.
function globMatch(pattern, path) {
const re = new RegExp(
'^' +
pattern
.replace(/[.+^$()|[\]\\]/g, '\\$&')
.replace(/\{([^}]+)\}/g, (_, inner) => '(' + inner.split(',').join('|') + ')')
.replace(/\*\*/g, '__GLOBSTAR__')
.replace(/\*/g, '[^/]*')
.replace(/__GLOBSTAR__/g, '.*')
+ '$'
);
return re.test(path);
}
async function enumerateRoutes(root) {
const entries = await readdir(root, { recursive: true, withFileTypes: true });
const routes = [];
for (const e of entries) {
if (!e.isFile()) continue;
const segments = (e.parentPath ?? e.path ?? root).split('/');
if (segments.some((s) => SKIP_DIRS.has(s))) continue;
const full = join(e.parentPath ?? e.path ?? root, e.name);
const rel = relative(root, full);
// App Router: route groups ((name)), parallel routes (@slot), private folders
// (_name), and the top-level page.tsx (no path segment) all need explicit handling.
let m = rel.match(/^(?:src\/)?app\/(.*)\/(page|route|layout)\.(tsx?|jsx?)$/);
if (!m) {
const top = rel.match(/^(?:src\/)?app\/(page|route|layout)\.(tsx?|jsx?)$/);
if (top) {
routes.push({
routePath: '/',
file: rel,
type: routeEntryType(top[1]),
});
continue;
}
}
if (m) {
const stripped = m[1]
.split('/')
.filter((seg) => !/^\([^)]+\)$/.test(seg) && !/^@/.test(seg) && !/^_/.test(seg))
.join('/')
.replace(/^\/+|\/+$/g, '');
const routePath = stripped === '' ? '/' : `/${stripped}`;
routes.push({
routePath,
file: rel,
type: routeEntryType(m[2]),
});
continue;
}
// Astro endpoint filenames commonly include the response extension
// (`feed.xml.ts`, `robots.txt.ts`). Handle these before the generic
// `src/pages` rule, which otherwise treats them as page components.
m = rel.match(/^src\/pages\/(.*\.(?:xml|json|txt|rss|atom|svg|png|jpg|jpeg|webp))\.(tsx?|jsx?|mjs|cjs)$/);
if (m) {
const name = normalizeRouteFileStem(m[1]);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: 'route',
});
continue;
}
m = rel.match(/^(?:src\/)?pages\/(.*)\.(tsx?|jsx?)$/);
if (m) {
const name = m[1].replace(/\/index$/, '').replace(/^index$/, '');
const isApi = /^api\//.test(name);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: isApi ? 'route' : 'page',
});
continue;
}
// Nuxt 3/4 pages. Dynamic segments use the same bracket shape as metrics
// (`[id]`, `[...slug]`), so keep them intact for route matching.
m = rel.match(/^(?:app\/)?pages\/(.*)\.vue$/);
if (m) {
const name = normalizeRouteFileStem(m[1]);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: 'page',
});
continue;
}
// Nuxt server routes: server/api/foo.get.ts -> /api/foo,
// server/routes/rss.xml.ts -> /rss.xml.
m = rel.match(/^server\/(api|routes)\/(.*)\.(tsx?|jsx?|mjs|cjs)$/);
if (m) {
const base = m[1] === 'api' ? 'api/' : '';
const name = normalizeRouteFileStem(`${base}${m[2]}`);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: 'route',
});
continue;
}
// Astro pages and endpoints. This is limited framework support, but route
// mapping still improves reports when Vercel metrics use user-facing paths.
m = rel.match(/^src\/pages\/(.*)\.(astro|tsx?|jsx?|mjs|cjs)$/);
if (m) {
const name = normalizeRouteFileStem(m[1]);
routes.push({
routePath: name === '' ? '/' : '/' + name,
file: rel,
type: m[2] === 'astro' ? 'page' : 'route',
});
continue;
}
// SvelteKit: +page.svelte = page, +page.server.{ts,js} pairs with it (treat
// as page), +server.{ts,js} = API route, +layout.* = ancestor layout context.
// Route groups (auth) stripped like Next; dynamic segments [slug]/[...rest]/[[opt]] preserved.
m = rel.match(/^src\/routes\/(.*)\/\+(page\.svelte|page\.server\.(?:ts|js)|server\.(?:ts|js)|layout\.svelte|layout\.server\.(?:ts|js))$/);
if (m || /^src\/routes\/\+(page\.svelte|page\.server\.(?:ts|js)|server\.(?:ts|js)|layout\.svelte|layout\.server\.(?:ts|js))$/.test(rel)) {
const fileTypeMatch = rel.match(/\+(page\.svelte|page\.server\.(?:ts|js)|server\.(?:ts|js)|layout\.svelte|layout\.server\.(?:ts|js))$/);
const fileType = fileTypeMatch?.[1] ?? '';
const segs = (m?.[1] ?? '').split('/').filter(Boolean)
.filter((seg) => !/^\([^)]+\)$/.test(seg));
const routePath = segs.length === 0 ? '/' : '/' + segs.join('/');
const type = fileType.startsWith('server') ? 'route' : fileType.startsWith('layout') ? 'layout' : 'page';
// When +page.svelte AND +page.server.ts both exist, +page.svelte wins ownership.
const existing = type === 'layout' ? null : routes.find((r) => r.routePath === routePath && r.type !== 'layout');
if (existing) {
if (fileType === 'page.svelte' && existing.type === 'page') {
existing.file = rel;
}
continue;
}
routes.push({ routePath, file: rel, type });
continue;
}
}
return routes.sort((a, b) =>
a.routePath.localeCompare(b.routePath)
|| routeTypeOrder(a.type) - routeTypeOrder(b.type)
|| a.file.localeCompare(b.file)
);
}
function routeEntryType(name) {
return name === 'route' ? 'route' : name === 'layout' ? 'layout' : 'page';
}
function normalizeRouteFileStem(stem) {
return String(stem ?? '')
.replace(/\/index$/, '')
.replace(/^index$/, '')
.replace(/\.(?:get|post|put|patch|delete|options|head)$/, '')
.replace(/^\/+|\/+$/g, '');
}
function routeTypeOrder(type) {
return type === 'page' ? 0 : type === 'route' ? 1 : type === 'layout' ? 2 : 3;
}
function mapFileToRoute(filePath, routes) {
const r = routes.find((rt) => rt.file === filePath);
return r?.routePath ?? null;
}
main().catch((err) => {
process.stderr.write(`[scan-codebase] FAILED: ${err.message}\n`);
process.exit(1);
});
scripts/verify-and-regen.mjs
#!/usr/bin/env node
// Verify → grade → emit regenPlan. Does NOT spawn sub-agents — the
// orchestrator reads regenPlan, re-spawns one sub-agent per targeted candidate
// with topFailures injected, then re-runs this script. Thresholds are tuned
// below (REGEN_*, QUALITY_FLOOR) — read those constants for the live values.
import { readFile, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { mkdir } from 'node:fs/promises';
import { verifyClaim } from '../lib/verify-claim.mjs';
import { extractClaims, summarizeClaimResults } from '../lib/extract-claims.mjs';
import { gradeRecommendation, applyQualityFloor } from '../lib/grade-recommendation.mjs';
import { deriveProjectFacts } from '../lib/project-facts.mjs';
import { resolveRepoRoot } from '../lib/repo-root.mjs';
import { applySanitizers } from '../lib/sanitizers/index.mjs';
const SCHEMA_VERSION = '1.0';
const REGEN_PASS_RATE_THRESHOLD = 0.8;
// 1/1 failed is as broken as 1/5; below 2 claims is below the noise floor.
const REGEN_MIN_CLAIMS = 2;
// The Poor/Fair grade boundary — Poor recs erode trust faster than recall helps.
const QUALITY_FLOOR = 0.55;
const log = (...a) => console.error('[verify-and-regen]', ...a);
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.recsPath) {
console.error('usage: node scripts/verify-and-regen.mjs <recommendations.json> [--signals merged.json] [--repo-root DIR] [--out FILE]');
process.exit(1);
}
const recs = JSON.parse(await readFile(args.recsPath, 'utf-8'));
if (!Array.isArray(recs)) {
console.error('[verify-and-regen] FATAL: recommendations.json must be an array of rec objects');
process.exit(2);
}
let framework, version, cacheComponents, knownFindings = [], projectFacts = [], signals = null;
if (args.signalsPath) {
signals = JSON.parse(await readFile(args.signalsPath, 'utf-8'));
const stack = signals.stack ?? signals.codebase?.stack ?? {};
framework = stack.framework;
version = stack.frameworkVersion;
cacheComponents = stack.cacheComponents;
knownFindings = (signals.codebase?.findings ?? signals.findings ?? [])
.filter((f) => f.file && (f.line != null))
.map((f) => ({ file: f.file, line: f.line }));
projectFacts = deriveProjectFacts(signals);
if (projectFacts.length > 0) {
log(`project facts in play: ${projectFacts.map((f) => f.id).join(', ')}`);
}
}
// Repo-root priority: (1) signals.project.rootDirectory from Vercel API
// (authoritative — returns "apps/<name>" so cwd can be back-mapped without
// filesystem probing), (2) supplied --repo-root, (3) walk-up from cwd.
const rootResult = await resolveRepoRoot(recs, args.repoRoot, process.cwd(), signals);
const repoRoot = rootResult.root;
if (rootResult.source === 'api') {
log(`repo-root from Vercel API: '${repoRoot}' (rootDirectory='${rootResult.apiOffset}')`);
} else if (rootResult.source === 'auto-detected') {
log(`repo-root auto-detected: '${repoRoot}' (probe: ${rootResult.probe})`);
} else if (rootResult.source === 'corrected') {
log(`repo-root auto-corrected: '${args.repoRoot}' → '${repoRoot}' (sub-agent paths resolve there)`);
}
log(`verifying ${recs.length} rec(s) — framework=${framework ?? '?'}@${version ?? '?'} repoRoot=${repoRoot}`);
// knownFindings MUST combine scanner findings + sub-agent's verified
// findingRefs — scanner-only grounding would miss every metric-gate rec.
// Abstentions are first-class outputs ({abstain:true, candidateRef, reason})
// and MUST NOT be graded; the abstention IS the answer.
const recsGraded = [];
const abstentions = [];
const observations = [];
const sanitizerDropped = [];
for (let i = 0; i < recs.length; i++) {
const rec = recs[i];
if (rec?.abstain === true) {
abstentions.push({
index: i,
candidateRef: rec.candidateRef ?? null,
reason: rec.reason ?? '(no reason recorded)',
});
// Observation: real non-perf signal worth surfacing (regression, error storm).
if (rec.observation && typeof rec.observation === 'object' && rec.observation.summary) {
observations.push({
index: i,
candidateRef: rec.candidateRef ?? null,
summary: String(rec.observation.summary),
evidence: rec.observation.evidence ?? null,
suggestedAction: rec.observation.suggestedAction ?? null,
kind: rec.observation.kind ?? 'other',
});
}
continue;
}
const baseClaimCtx = {
framework,
version,
repoRoot,
projectFacts,
projectRootDirectory: signals?.project?.rootDirectory ?? null,
cacheComponents,
signals,
};
const initialClaims = extractClaims(rec, baseClaimCtx);
const initialVerifyResults = await Promise.all(initialClaims.map((c) => verifyClaim(c)));
const initialClaimsWithResults = initialVerifyResults.map((r, j) => ({
...r,
type: initialClaims[j]?.type,
claimType: initialClaims[j]?.type,
claim: initialClaims[j],
}));
const sanitizerResult = await applySanitizers(rec, {
framework,
version,
signals,
verifyResults: initialClaimsWithResults,
});
if (!sanitizerResult.kept) {
sanitizerDropped.push({
index: i,
candidateRef: rec.candidateRef ?? null,
what: rec.what ?? null,
reason: sanitizerResult.dropReason ?? 'automated-check',
});
continue;
}
const sanitizedRec = sanitizerResult.rec;
const claims = extractClaims(sanitizedRec, baseClaimCtx);
const verifyResults = await Promise.all(claims.map((c) => verifyClaim(c)));
const verification = summarizeClaimResults(verifyResults);
// A findingRef whose file_exists claim verified counts as grounding evidence.
const verifiedRefs = [];
for (let j = 0; j < claims.length; j++) {
const c = claims[j];
const r = verifyResults[j];
if (r?.disposition !== 'verified') continue;
if (c.sourceField === 'findingRefs' && c.type === 'file_exists') {
const ref = (rec.findingRefs ?? []).find((x) => String(x).startsWith(c.file + ':'));
if (ref) {
const m = String(ref).match(/^(.+?):(\d+)$/);
if (m) verifiedRefs.push({ file: m[1], line: Number(m[2]) });
}
}
}
const recKnownFindings = [...knownFindings, ...verifiedRefs];
const quality = gradeRecommendation(sanitizedRec, { knownFindings: recKnownFindings });
recsGraded.push({
index: i,
rec: { ...sanitizedRec, verification, verifyResults, quality },
claims,
verifyResults,
verification,
quality,
});
}
// Project-config contradictions are a HARD trigger: a "turn on Fluid" rec on
// a project where Fluid is already on passes 8/9 claims but is the wrong rec.
// passRate alone won't catch this.
const regenPlan = [];
for (const g of recsGraded) {
const { passRate, verifiable } = g.verification;
const claimsWithResults = g.verifyResults.map((r, j) => ({ ...r, claim: g.claims[j] }));
const contradictions = claimsWithResults.filter(
(r) => r.disposition === 'failed' && r.claim?.type === 'does_not_contradict_project_config'
);
const triggeredByPassRate = verifiable >= REGEN_MIN_CLAIMS && passRate < REGEN_PASS_RATE_THRESHOLD;
const cacheSafetyFailures = claimsWithResults.filter(
(r) => r.disposition === 'failed' && (
r.claim?.type === 'cache_vary_matches_dynamic_inputs' ||
r.claim?.type === 'cache_vary_cardinality_safe'
)
);
const semanticSafetyFailures = claimsWithResults.filter(
(r) => r.disposition === 'failed' && (
r.claim?.type === 'next_cached_not_found_causal_support' ||
r.claim?.type === 'next_stable_cache_api_for_version' ||
r.claim?.type === 'next_runtime_cache_api_for_version' ||
r.claim?.type === 'next_cache_life_single_execution' ||
r.claim?.type === 'next_cache_lifetime_freshness_supported' ||
r.claim?.type === 'next_cache_components_route_chain_file' ||
r.claim?.type === 'next_cache_life_cdn_header_semantics' ||
r.claim?.type === 'image_response_headers_citation' ||
r.claim?.type === 'next_image_priority_api_for_version' ||
r.claim?.type === 'next_cache_components_route_segment_config' ||
r.claim?.type === 'next_route_revalidate_static_prereq' ||
r.claim?.type === 'next_cache_tag_invalidation_supported' ||
r.claim?.type === 'cache_rec_not_error_dominated_or_acknowledged' ||
r.claim?.type === 'cache_control_header_syntax' ||
r.claim?.type === 'cache_control_headers_citation' ||
r.claim?.type === 'cache_404_long_ttl_safety' ||
r.claim?.type === 'route_error_not_found_status_and_scope' ||
r.claim?.type === 'immutable_dynamic_route_safety' ||
r.claim?.type === 'auth_guard_parallelization_safety' ||
r.claim?.type === 'parallelization_impact_not_overclaimed' ||
r.claim?.type === 'parallelization_not_cpu_bound_work' ||
r.claim?.type === 'runtime_error_cause_supported' ||
r.claim?.type === 'vercel_ignore_command_project_state'
)
);
const triggeredByContradiction = contradictions.length > 0;
const triggeredByCacheSafety = cacheSafetyFailures.length > 0;
const triggeredBySemanticSafety = semanticSafetyFailures.length > 0;
if (!triggeredByPassRate && !triggeredByContradiction && !triggeredByCacheSafety && !triggeredBySemanticSafety) continue;
const failures = claimsWithResults
.filter((r) => r.disposition === 'failed')
.slice(0, 5);
regenPlan.push({
index: g.index,
candidateRef: g.rec.candidateRef ?? null,
what: g.rec.what ?? null,
verifiableClaimCount: verifiable,
passRate,
regenTrigger: triggeredByContradiction
? 'project_config_contradiction'
: triggeredByCacheSafety
? 'cache_vary_safety'
: triggeredBySemanticSafety
? 'semantic_safety'
: 'pass_rate_below_threshold',
topFailures: failures.map((f) => ({
claimType: f.claim?.type,
field: f.claim?.sourceField,
url: f.claim?.url,
file: f.claim?.file,
pattern: f.claim?.pattern,
reason: f.reason,
})),
regenBriefHint: triggeredByContradiction
? 'Sub-agent recommended toggling on a project setting that is already enabled. Re-spawn with the project-config Strengths block highlighted; the rec must drop the contradictory step and keep only the actionable parts.'
: triggeredByCacheSafety
? 'Sub-agent recommended CDN caching with unsafe or missing Vary behavior. Re-spawn with the cache safety failure highlighted; the rec must use a low-cardinality Vary header that matches the dynamic inputs, or abstain.'
: triggeredBySemanticSafety
? 'Sub-agent made a framework-semantic claim that failed deterministic checks. Re-spawn with the failure highlighted; the rec must either add version-correct code/citations/runtime evidence or abstain.'
: 'Re-spawn the sub-agent with this rec\'s topFailures injected as feedback. Re-emit the rec only if regenPassRate >= originalPassRate AND citation count not gutted.',
});
}
const qualityCheck = applyQualityFloor(recsGraded.map((g) => g.rec), QUALITY_FLOOR);
const hardRegenIndexes = new Set(regenPlan.map((p) => p.index));
const qualityDroppedIndexes = new Set(
qualityCheck.dropped
.map((d) => recsGraded.findIndex((g) => g.rec === d.rec))
.filter((i) => i >= 0)
);
const needsReviewIndexes = new Set(
recsGraded
.filter((g) => g.rec.needsReview === true)
.map((g) => g.index)
);
const verifiedRecommendations = recsGraded
.filter((g) => !hardRegenIndexes.has(g.index) && !qualityDroppedIndexes.has(g.index) && !needsReviewIndexes.has(g.index))
.map((g) => g.rec);
const withheldRecommendations = recsGraded
.filter((g) => hardRegenIndexes.has(g.index) || qualityDroppedIndexes.has(g.index) || needsReviewIndexes.has(g.index))
.map((g) => ({
index: g.index,
candidateRef: g.rec.candidateRef ?? null,
what: g.rec.what ?? null,
reason: hardRegenIndexes.has(g.index)
? (regenPlan.find((p) => p.index === g.index)?.regenTrigger ?? 'verification')
: qualityDroppedIndexes.has(g.index)
? 'quality_floor'
: 'needs_review',
}));
const summary = {
totalRecs: recs.length,
abstentions: abstentions.length,
observations: observations.length,
sanitizerDropped: sanitizerDropped.length,
needsRegen: regenPlan.length,
qualityDropped: qualityCheck.dropped.length,
needsReview: needsReviewIndexes.size,
verifiedRecommendations: verifiedRecommendations.length,
withheldRecommendations: withheldRecommendations.length,
averagePassRate: recsGraded.length > 0
? round4(recsGraded.reduce((s, g) => s + g.verification.passRate, 0) / recsGraded.length)
: null,
averageQuality: recsGraded.length > 0
? round4(recsGraded.reduce((s, g) => s + g.quality.overall, 0) / recsGraded.length)
: null,
};
const output = {
schemaVersion: SCHEMA_VERSION,
summary,
recsGraded: recsGraded.map((g) => g.rec),
verifiedRecommendations,
renderableRecommendations: verifiedRecommendations,
withheldRecommendations,
abstentions,
observations,
sanitizerDropped,
regenPlan,
qualityDropped: qualityCheck.dropped.map((d) => ({
index: recsGraded.findIndex((g) => g.rec === d.rec),
candidateRef: d.rec.candidateRef ?? null,
quality: d.rec.quality,
reason: d.reason,
})),
};
const serialized = JSON.stringify(output, null, 2) + '\n';
if (args.outPath) {
await mkdir(dirname(args.outPath), { recursive: true });
await writeFile(args.outPath, serialized, 'utf-8');
log(`wrote ${serialized.length}B → ${args.outPath}`);
} else {
process.stdout.write(serialized);
}
log(`done: ${summary.totalRecs} records checked; ${summary.verifiedRecommendations} ready, ${summary.withheldRecommendations} held back, ${summary.abstentions} found no supported change, ${summary.sanitizerDropped} dropped by safety checks`);
}
function parseArgs(argv) {
const out = { positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--signals') out.signalsPath = argv[++i];
else if (a.startsWith('--signals=')) out.signalsPath = a.slice('--signals='.length);
else if (a === '--repo-root') out.repoRoot = argv[++i];
else if (a.startsWith('--repo-root=')) out.repoRoot = a.slice('--repo-root='.length);
else if (a === '--out') out.outPath = resolve(argv[++i]);
else if (a.startsWith('--out=')) out.outPath = resolve(a.slice('--out='.length));
else out.positional.push(a);
}
out.recsPath = out.positional[0];
return out;
}
function round4(n) { return Math.round(n * 10000) / 10000; }
main().catch((err) => {
console.error('[verify-and-regen] FAILED:', err.message);
console.error(err.stack);
process.exit(1);
});
scripts/verify-finding.mjs
#!/usr/bin/env node
// CLI shell around lib/verify-claim.mjs. argv[2] = JSON claim, stdout = result.
// Claim `type` enum: pattern_count | pattern_exists | pattern_absent | file_exists |
// code_snippet | repo_count | citation_in_library | citation_applies_to_version.
import { verifyClaim } from '../lib/verify-claim.mjs';
const SCHEMA_VERSION = '1.0';
async function main() {
const claim = JSON.parse(process.argv[2] || '{}');
const result = await verifyClaim(claim);
process.stdout.write(JSON.stringify({ schemaVersion: SCHEMA_VERSION, ...result }, null, 2) + '\n');
}
main().catch((err) => {
process.stderr.write(`[verify-finding] FAILED: ${err.message}\n`);
process.exit(1);
});
SKILL.md
---
name: vercel-optimize
description: "Use for Vercel cost and performance optimization on deployed projects, especially Next.js, SvelteKit, Nuxt, and limited Astro apps. Collect Vercel metrics, usage, project config, and code scan results first; investigate only metric-backed candidates; produce ranked recommendations grounded in verified files and version-aware Vercel/framework docs. Trigger for Vercel bill reduction, slow or expensive routes, caching opportunities, Function Invocations, Build Minutes, Fast Data Transfer, Core Web Vitals, Bot Management, Fluid compute, or cost breakdown requests."
metadata:
version: "1.2.0"
---
# Vercel Optimize
Run an observability-first Vercel optimization audit. Do not inspect source files until `signals.json` exists and a deterministic gate points to a route, file, or project setting.
Core doctrine: read [references/doctrine.md](references/doctrine.md) if any rule is unclear.
- Metrics first. Recommendations start from Vercel production signals, not repo-wide grep.
- Deterministic gates. `scripts/gate-investigations.mjs` decides what deserves investigation.
- Candidate-bound scope. Read only files named by a candidate or a route-local import chain.
- Version-aware citations. Use only `references/docs-library.json`; invalid or version-mismatched citations are stripped.
- Customer copy. Read [references/voice.md](references/voice.md) before writing report text or chat output.
## Prerequisites
- Vercel CLI v53+ with `vercel metrics`, `vercel usage`, `vercel contract`, and `vercel api`.
- Authenticated CLI session: `vercel login`.
- Linked app directory: `vercel link`. `VERCEL_PROJECT_ID` can help resolve project config, but `vercel metrics` still requires directory linkage. The link or environment must include the intended project org/team/user scope so the collector can resolve a CLI-safe `--scope` and keep `vercel metrics`, `vercel usage`, and `vercel contract` on the same account.
- Node.js 20+.
- Observability Plus for route-level metric-backed recommendations.
Never put auth tokens in shell commands. Do not type `VERCEL_TOKEN=...`, `--token ...`, or `Authorization: Bearer ...` into commands that may be echoed in chat.
## Framework Support
The preflight reads `package.json` and sets expectations before metric fan-out.
| Framework | Status | Notes |
|---|---|---|
| Next.js App Router | supported | strongest route mapping, scanners, playbooks, citations |
| Next.js Pages Router | supported | scoped to Pages Router idioms when detected |
| SvelteKit | supported | route mapping for `src/routes` files and SvelteKit scanner |
| Nuxt | supported | route mapping plus generic/platform checks; fewer framework-specific recs |
| Astro | limited | route mapping plus generic checks; fewer framework-specific recs |
| Hono / Remix / unknown | blocked by default | continue only if the user accepts a limited platform/code-only audit |
If unsupported, stop and ask before scanning or gating:
```text
This project uses <framework>. Vercel Optimize supports metric-backed code recommendations for Next.js, SvelteKit, and Nuxt. Astro support is limited. For <framework>, I can still run a limited platform/scanner audit, but route-level Vercel metrics may not map back to source files.
Do you want me to continue with the limited audit, or stop here?
```
If the user continues, rerun collection with `--continue-unsupported-framework`.
## Run Directory
Use a fresh run directory for every audit. Do not reuse briefs, sub-agent outputs, or reports across runs.
```bash
RUN_DIR="$(mktemp -d -t vercel-optimize-XXXXXX)"
```
## Pipeline
### 1. Collect, scan, and merge signals
Run from the linked app directory or pass `--cwd` where a script supports it. Keep stdout JSON separate from stderr logs. Do not combine streams.
```bash
node scripts/collect-signals.mjs [projectId] > "$RUN_DIR/vercel-signals.json" 2> "$RUN_DIR/collect.stderr"
node -e 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))' "$RUN_DIR/vercel-signals.json"
node scripts/scan-codebase.mjs <repo-root> > "$RUN_DIR/codebase.json"
node scripts/merge-signals.mjs "$RUN_DIR/vercel-signals.json" "$RUN_DIR/codebase.json" --out "$RUN_DIR/signals.json"
```
Collection details, schemas, metric IDs, and degradation behavior live in [references/data-collection.md](references/data-collection.md). The metric registry is [lib/queries.mjs](lib/queries.mjs); keep all queries on the shared 14-day window.
`collect-signals.mjs` resolves the linked project owner to `commandScope.cliScope` and verifies that the resolved account can read the resolved project before it checks Observability Plus. Downstream scripts reuse that scope for every Vercel CLI command that accepts `--scope`. Do not run `vercel usage`, `vercel metrics`, or `vercel contract` manually without the same scope; unscoped usage can report the user's personal organization while route metrics come from the team project.
If project or scope resolution is ambiguous, stop and ask the user which Vercel project and team/personal scope they want audited. Do not infer the intended scope from the current `vercel whoami` team, and do not proceed with metrics, usage, or contract collection until the link, an exact project match in `.vercel/repo.json`, or `VERCEL_PROJECT_ID` + `VERCEL_ORG_ID` identifies the intended account.
Use this prompt for `PROJECT_SCOPE_UNRESOLVED`, `SCOPE_UNRESOLVED`, or `PROJECT_SCOPE_MISMATCH`:
```text
I can't safely identify the Vercel project and account for this audit yet.
Please confirm the Vercel project name or ID and the team slug/name, or tell me it's under your personal account. Once confirmed, I'll relink or rerun collection against that exact scope before checking metrics.
```
### 1.1 Stop on blockers
Check blockers before gating:
```bash
jq '{frameworkSupportBlocker, observabilityPlus, observabilityPlusUsable, observabilityPlusBlocker, observabilityPlusBlockerDetail}' "$RUN_DIR/signals.json"
```
Required actions:
- `frameworkSupportBlocker === "unsupported_framework"`: use the unsupported-framework prompt above.
- `PROJECT_SCOPE_UNRESOLVED`, `SCOPE_UNRESOLVED`, or `PROJECT_SCOPE_MISMATCH`: stop and ask which Vercel project and team/personal scope the user wants audited. For team projects, rerun after `vercel link --yes --project <project-name-or-id> --team <team-slug>`; for personal projects, rerun after linking under the intended user account or after setting both `VERCEL_PROJECT_ID` and `VERCEL_ORG_ID`.
- `observabilityPlusBlocker === null`: continue.
- `no_traffic`: tell the user route metrics are sparse; continue only if they accept limited output.
- `payment_required` or `no_oplus_probe`: render [references/observability-plus.md](references/observability-plus.md) verbatim and ask.
- `project_disabled`: tell the user to enable Observability Plus for the project or accept a limited audit.
- `daily_quota_exceeded`: stop and tell the user the Observability query quota is exhausted; retry after the next UTC midnight reset, or ask whether to continue with a limited code-only audit.
- `not_linked`: link the app directory, then rerun Step 1. If app path and project are known:
```bash
vercel link --yes --project <project-name-or-id> --cwd <app-dir>
# add --team <team-id-or-slug> when known
```
- `forbidden` or `project_not_found`: fix auth/team scope. Do not pitch Observability Plus.
- `all_failed_other`: show the raw error code and ask whether to continue in limited code-only mode.
Do not silently fall back to code-only mode. If the user accepts a limited audit, rerun collection with:
```bash
node scripts/collect-signals.mjs [projectId] --continue-without-observability > "$RUN_DIR/vercel-signals.json" 2> "$RUN_DIR/collect.stderr"
```
Then scan and merge again.
### 2. Gate candidates
```bash
node scripts/gate-investigations.mjs "$RUN_DIR/signals.json" > "$RUN_DIR/gate.json"
```
Output shape:
- `toLaunch`: code-scope candidates to investigate.
- `platform`: project/account-scope recommendations.
- `gated`: skipped, covered, or disqualified candidates that must still appear in the report.
- `budget`: candidate budget and selection mode.
Default budget is 6 code-scope candidates with a diversity guardrail. To expand:
```bash
node scripts/gate-investigations.mjs "$RUN_DIR/signals.json" --max-candidates 12 > "$RUN_DIR/gate.json"
node scripts/gate-investigations.mjs "$RUN_DIR/signals.json" --max-candidates all > "$RUN_DIR/gate.json"
```
Generated candidate docs: [references/candidates.md](references/candidates.md).
### 2.1 Ask about audit scope when needed
Before deep-dive, run:
```bash
node scripts/budget-summary.mjs "$RUN_DIR/gate.json" --format json > "$RUN_DIR/budget-summary.json"
```
If `shouldAsk` is false, continue.
If `shouldAsk` is true:
1. Print `exactChatMessage.body` exactly as returned. Do not summarize, truncate, reorder, or rewrite it.
2. Then ask `questionText` using `questionPayload` when the host supports structured questions.
3. If the user chooses a different number, rerun the gate with `--max-candidates <choice>`.
Never put the long preview inside the question field. The preview and the question are separate surfaces.
### 2.2 Deep-dive and reconcile
```bash
node scripts/deep-dive.mjs "$RUN_DIR/signals.json" "$RUN_DIR/gate.json" --cwd <project-dir> > "$RUN_DIR/investigation-evidence.json"
node scripts/reconcile-candidates.mjs "$RUN_DIR/investigation-evidence.json" \
--gate "$RUN_DIR/gate.json" \
--out "$RUN_DIR/reconciled-investigation.json"
```
`--cwd` must be the linked project directory so `deep-dive.mjs` can verify the same project link and reuse `signals.json.commandScope.cliScope` for any follow-up `vercel metrics` calls.
Reconciliation deterministically converts disproven candidates into observations before any source investigation:
- `metric_mismatch`
- `error_storm`
- `deployment_regression`
- `scanner_only_no_metric`
### 2.3 Generate briefs and investigate
List the work:
```bash
node scripts/prepare-investigation-brief.mjs "$RUN_DIR/signals.json" "$RUN_DIR/reconciled-investigation.json" --list > "$RUN_DIR/briefs-manifest.json"
```
Generate one brief for every entry in `briefs-manifest.json.briefs`. The `group` can be `toLaunch` or `platform`; do not generate only `toLaunch` briefs.
```bash
mkdir -p "$RUN_DIR/briefs" "$RUN_DIR/sub-agent-outputs"
node scripts/prepare-investigation-brief.mjs "$RUN_DIR/signals.json" "$RUN_DIR/reconciled-investigation.json" \
--group <brief.group> --index <brief.index> --out "$RUN_DIR/briefs/<brief.group>-<brief.index>.md"
```
Use `briefs-manifest.json.briefs[].label` for visible worker names, for example `Low cache-hit route on /docs/llm-digest/[...slug]`, not `toLaunch-7`.
Fan-out rule:
- 1-2 briefs: investigate inline.
- 3+ briefs: spawn one sub-agent per brief when the host supports it.
- Hosts without sub-agents: run inline serially.
Sub-agent contract:
- The brief is the whole prompt.
- Read only files listed in the brief, plus route-local imports when needed.
- Emit one JSON recommendation or one JSON no-change finding using [references/recommendations.md](references/recommendations.md).
- Do not cite URLs outside the provided citation subset.
- Do not recommend framework features unavailable in the detected version.
If a sub-agent reaches for repo-wide grep, the candidate is malformed; drop or abstain rather than widening scope.
### 2.4 Collect outputs
Save each raw investigation result in `$RUN_DIR/sub-agent-outputs/`, then collect:
```bash
node scripts/collect-sub-agent-outputs.mjs \
--manifest "$RUN_DIR/briefs-manifest.json" \
--out "$RUN_DIR/recommendations.json" \
"$RUN_DIR/sub-agent-outputs/"
```
The collector extracts JSON, prepends pre-resolved records, enforces manifest order, and fails on missing, duplicate, unknown, or mismatched `candidateRef` values.
### 3. Verify recommendations
```bash
node scripts/verify-and-regen.mjs "$RUN_DIR/recommendations.json" \
--signals "$RUN_DIR/signals.json" \
--repo-root <project-dir> \
--out "$RUN_DIR/verify.json"
```
This script extracts claims, verifies files/citations/version fit, grades quality, applies sanitizers, emits `verifiedRecommendations`, `withheldRecommendations`, `renderableRecommendations`, and creates `regenPlan` for failed or unsafe recommendations.
Recommendation schema, writing rules, sanitizer order, and grading rules: [references/recommendations.md](references/recommendations.md). Verification rules: [references/verification.md](references/verification.md).
For each `regenPlan` entry, rerun the same brief with a `Previous attempt failed these checks` section listing `topFailures`. Keep the regenerated output only if verification improves without gutting citations.
### 4. Render report and final message
```bash
node scripts/render-report.mjs "$RUN_DIR/verify.json" "$RUN_DIR/gate.json" "$RUN_DIR/signals.json" \
--project <name> \
--out "$RUN_DIR/report.md" \
--message-out "$RUN_DIR/final-message.json"
```
Use `--debug-out "$RUN_DIR/debug.json"` only when developing the skill. Customer Markdown and chat output must not expose `passRate`, `quality`, sanitizer trails, raw sub-agent names, or other implementation fields.
After rendering, print `final-message.json.body` verbatim and stop. Do not add highlights, debug notes, raw counts, sub-agent summaries, or extra explanation. Render-time dedupe, platform caps, and hard-safety drops can change the customer-visible count, so never summarize from raw `verify.json`.
Report structure and impact framing: [references/scoring.md](references/scoring.md).
## Recommendation Rules
Every recommendation must:
- Trace to a launched candidate, platform candidate, pre-resolved observation, or verified traffic-independent scanner finding.
- Include observed metric evidence from `signals.json` or `evidence.deepDive`.
- Cite verified files with line numbers when code is involved.
- Include at least one allowed citation that applies to the detected framework/version.
- Use precise observed performance numbers.
- Use cost magnitude phrases only; never customer-facing `$N` savings.
- Do not recommend duration reductions for Vercel Workflow runtime endpoints (`/.well-known/workflow/v1/*`). These are generated orchestration routes for durable step/flow execution and should be hard-gated before investigation.
- Workflow recommendations must name the boundary being changed. Valid examples: enqueue durable work and return a run ID instead of awaiting completion, fix stream replay/closure/locks, or reduce verified excess Workflow Steps/Storage. Do not infer cost savings from Workflow endpoint wall-clock duration.
- For streaming, SSE, resumable chat, or other intentionally long-lived routes, do not frame wall-clock function duration as a problem by itself. Require evidence of avoidable pre-first-byte work, high active CPU, duplicate invocations, or post-response work that can move out of the user-visible path.
- Name a specific cache policy when recommending caching.
- Keep unsafe responses dynamic unless evidence proves they are safe to cache: auth-sensitive paths, errors, fallback responses, missing content, invalid requests, geolocation/device-varying output, and unversioned dynamic URLs.
Never recommend "verify X is on" for facts already present in `signals.project`, including Fluid compute status, memory tier, regions, in-function concurrency, and timeout.
## Scanner Rules
Scanner findings are supplementary. Drop findings annotated `COLD-PATH` or `NO-ROUTE-MAPPING` unless the scanner declares `metadata.trafficIndependent === true`.
Traffic-independent examples: middleware matcher, source maps, React Compiler config, build settings. Route-local cache or data-fetch patterns need route-level traffic evidence.
Scanner docs: [references/scanner-patterns.md](references/scanner-patterns.md).
## Final Customer Terms
Use:
- `recommendations ready`
- `observations from investigation`
- `investigated, no change recommended`
- `not investigated in this run`
Avoid:
- `sub-agent`
- `abstention`
- `passRate`
- `quality score`
- `gate`
- `LLM`
## Failure Copy
Use these messages without adding sales copy or process detail.
**No traffic in the last 14 days:**
> This project has no meaningful traffic in the last 14 days, so route-level metrics are sparse. I can still check traffic-independent scanner findings and project settings, but I cannot rank route fixes until traffic accumulates.
**Route-level metrics unavailable:**
> Use the verbatim choice template in [references/observability-plus.md](references/observability-plus.md). Do not silently fall back to code-only mode; present the two-path choice: enable Observability Plus and rerun the metric-backed audit, or accept a limited code-only run.
**Project is not linked:**
> This worktree is not linked to a Vercel project. Run `vercel link --yes --project <project-name-or-id> --cwd <app-dir>` and rerun the audit. If the team is known, add `--team <team-id-or-slug>`.
**Most route-to-file mappings failed:**
> The route inventory matched fewer than half of the routes we saw in observability. This is common in monorepos with custom routing. I've surfaced what I can match; the rest appear in the "Not investigated in this run" section.