📦 deps(thirdparty): update snapshots
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
// 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 } : {};
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// 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 } : {};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// 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 } : {};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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 };
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// 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 };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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 {};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// 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 };
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// 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}`) };
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 } : {};
|
||||
}
|
||||
Reference in New Issue
Block a user