playbook/antigravity-awesome-skills/skills/vercel-optimize/lib/support-topics.mjs

356 lines
12 KiB
JavaScript

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;
}