playbook/antigravity-awesome-skills/plugins/antigravity-awesome-skills-.../skills/vercel-optimize/scripts/check-citations.mjs

82 lines
3.4 KiB
JavaScript

#!/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);
});