playbook/antigravity-awesome-skills/skills/vercel-optimize/lib/verify-claim.mjs

1292 lines
59 KiB
JavaScript

// 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';
import { escapeRegex } from './util.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, '\\$&');
}
function compilePattern(pattern, flags) {
return new RegExp(escapeRegex(String(pattern ?? '')), 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;
}