Files
playbook/antigravity-awesome-skills/tools/local-skill-reviewer/validation.js
T
2026-07-18 00:02:59 +00:00

146 lines
9.0 KiB
JavaScript

"use strict";
const YAML = require("yaml");
const { MAX_FRONTMATTER_DEPTH, MAX_FRONTMATTER_NODES } = require("./constants");
const VALID_RISKS = new Set(["none", "safe", "critical", "offensive", "unknown"]);
function splitFrontmatter(content) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
if (!match) return { metadata: null, body: content, errors: ["missing_or_malformed_frontmatter"] };
if (Buffer.byteLength(match[1], "utf8") > 32 * 1024) {
return { metadata: null, body: content.slice(match[0].length), errors: ["frontmatter_too_large"] };
}
if (/(^|[\s\[{,:])[*&][A-Za-z0-9_-]+/m.test(match[1]) || /(^|\s)![^\s]+/m.test(match[1])) {
return { metadata: null, body: content.slice(match[0].length), errors: ["yaml_alias_anchor_or_tag_forbidden"] };
}
try {
const document = YAML.parseDocument(match[1], {
maxAliasCount: 20,
prettyErrors: false,
strict: true,
uniqueKeys: true,
});
if (document.errors.length) return { metadata: null, body: content.slice(match[0].length), errors: ["invalid_yaml"] };
let nodes = 0;
function visit(node, depth) {
if (!node) return;
nodes += 1;
if (depth > MAX_FRONTMATTER_DEPTH || nodes > MAX_FRONTMATTER_NODES) throw new Error("yaml_structure_limit");
if (Array.isArray(node.items)) {
for (const item of node.items) {
if (item?.key !== undefined || item?.value !== undefined) { visit(item.key, depth + 1); visit(item.value, depth + 1); }
else visit(item, depth + 1);
}
}
}
visit(document.contents, 1);
const metadata = document.toJS({ maxAliasCount: 20 });
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
return { metadata: null, body: content.slice(match[0].length), errors: ["frontmatter_not_mapping"] };
}
return { metadata, body: content.slice(match[0].length), errors: [] };
} catch {
return { metadata: null, body: content.slice(match[0].length), errors: ["invalid_yaml"] };
}
}
function deterministicValidation(content, skillId) {
const { metadata, body, errors } = splitFrontmatter(content);
const checks = [];
const check = (id, passed, detail) => checks.push({ id, passed: Boolean(passed), detail });
check("frontmatter", errors.length === 0, errors.join(", ") || "valid");
check("name", typeof metadata?.name === "string" && metadata.name.trim() === skillId.split("/").at(-1), "name matches canonical leaf id");
check("description", typeof metadata?.description === "string" && metadata.description.trim().length >= 20, "description has at least 20 characters");
check("risk", VALID_RISKS.has(metadata?.risk), "risk uses an AAS value");
check("source", typeof metadata?.source === "string" && metadata.source.trim().length > 0, "source is present");
check("source_fidelity", typeof metadata?.source === "string" && !/\b(?:unknown|todo|tbd)\b/i.test(metadata.source), "source is attributable");
check("when_to_use", /^##\s+When to Use\b/im.test(body), "When to Use section is present");
check("limitations", /^##\s+Limitations\b/im.test(body), "Limitations section is present");
const risky = ["critical", "offensive"].includes(metadata?.risk);
check("safety_boundary", !risky || /\b(authorized|approval|permission|safety|do not|never|before)\b/i.test(body), "high-risk skills declare a safety boundary");
check("repo_conventions", !/^#\s*$/m.test(body) && !/\bTODO\b/i.test(body), "body avoids empty headings and TODO markers");
check("substantive_body", body.trim().length >= 100, "body has at least 100 characters");
const passed = checks.filter((item) => item.passed).length;
return {
implementation: "aas-compatible-validation-v1",
score: passed / checks.length,
checks,
};
}
function tesslAlignedValidation(bundle, skillId) {
const primary = bundle.files[0];
const { metadata, body, errors: parseErrors } = splitFrontmatter(primary.text);
const checks = [];
const add = (name, status, detail) => checks.push({ name, status, detail });
const lineCount = primary.text.split(/\r?\n/).length;
add("skill_md_line_count", lineCount > 500 ? "warning" : "passed", { lineCount, limit: 500 });
add("frontmatter_valid", parseErrors.length ? "error" : "passed", { errors: parseErrors });
const name = metadata?.name;
const validName = typeof name === "string" && name.length >= 1 && name.length <= 64
&& /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) && name === skillId.split("/").at(-1);
add("name_field", validName ? "passed" : "error", { length: typeof name === "string" ? name.length : 0 });
const description = metadata?.description;
const validDescription = typeof description === "string" && description.length >= 1 && description.length <= 1024;
add("description_field", validDescription ? "passed" : "error", { length: typeof description === "string" ? description.length : 0 });
const compatibility = metadata?.compatibility;
add("compatibility_field", compatibility === undefined || (typeof compatibility === "string" && compatibility.length >= 1 && compatibility.length <= 500) ? "passed" : "error", { present: compatibility !== undefined });
const allowedTools = metadata?.["allowed-tools"];
add("allowed_tools_field", allowedTools === undefined || typeof allowedTools === "string" ? "passed" : "error", { present: allowedTools !== undefined });
const metadataField = metadata?.metadata;
const metadataMap = metadataField && typeof metadataField === "object" && !Array.isArray(metadataField);
const version = metadataMap ? metadataField.version : undefined;
const validVersion = typeof version === "string" && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version);
add("metadata_version", metadataField === undefined || validVersion ? "passed" : "warning", { present: version !== undefined });
const validMetadata = metadataField === undefined || (metadataMap && Object.entries(metadataField).every(([key, value]) => typeof key === "string" && typeof value === "string"));
add("metadata_field", validMetadata ? "passed" : "warning", { present: metadataField !== undefined });
const license = metadata?.license;
add("license_field", license === undefined || (typeof license === "string" && license.trim()) ? "passed" : "error", { present: license !== undefined });
const allowed = new Set(["name", "description", "compatibility", "allowed-tools", "metadata", "license"]);
const unknown = Object.keys(metadata || {}).filter((key) => !allowed.has(key)).sort();
add("frontmatter_unknown_keys", unknown.length ? "warning" : "passed", { count: unknown.length });
add("body_present", body.trim() ? "passed" : "error", { bytes: Buffer.byteLength(body, "utf8") });
for (const root of ["scripts", "references", "assets"]) {
add(`${root}_directory`, "passed", { files: bundle.files.filter((file) => file.path.includes(`/${root}/`)).length });
}
const base = primary.path.slice(0, -"SKILL.md".length);
const available = new Set(bundle.files.map((file) => file.path.slice(base.length)));
const exists = (target) => available.has(target) || [...available].some((item) => item.startsWith(`${target.replace(/\/$/, "")}/`));
const classify = (targets) => {
const issues = { missing: [], too_deep: [], suspicious: [] };
for (const raw of [...new Set(targets)]) {
let target = raw.replace(/^<|>$/g, "").split("#", 1)[0].split("?", 1)[0];
try { target = decodeURIComponent(target); } catch { issues.suspicious.push(raw); continue; }
if (!target || /^(?:[a-z][a-z0-9+.-]*:|#|\/)/i.test(target)) continue;
if (target.includes("\\") || target.split("/").includes("..") || target.split("/").includes(".")) { issues.suspicious.push(raw); continue; }
const parts = target.replace(/\/$/, "").split("/").filter(Boolean);
if (parts.length > 2) { issues.too_deep.push(raw); continue; }
if (!exists(target)) issues.missing.push(raw);
}
return Object.fromEntries(Object.entries(issues).filter(([, values]) => values.length));
};
const linkTargets = [];
for (const match of body.matchAll(/!?\[[^\]]*\]\(\s*(?:<([^>]+)>|([^\s)]+))/g)) linkTargets.push(match[1] || match[2]);
const linkIssues = classify(linkTargets);
add("relative_links", Object.keys(linkIssues).length ? "warning" : "passed", linkIssues);
const mentioned = [];
for (const match of body.matchAll(/\b(?:scripts|references|assets)\/[A-Za-z0-9._/-]+/g)) mentioned.push(match[0].replace(/[.,;:]+$/, ""));
const pathIssues = classify(mentioned);
add("referenced_paths_exist", Object.keys(pathIssues).length ? "warning" : "passed", pathIssues);
const errors = checks.filter((item) => item.status === "error").length;
const warnings = checks.filter((item) => item.status === "warning").length;
const normalized = Math.max(0, (checks.length - errors - 0.5 * warnings) / checks.length);
return {
implementation: "tessl-aligned-validation-v2",
overallPassed: errors === 0,
errorCount: errors,
warningCount: warnings,
normalized,
score: normalized,
checks,
};
}
module.exports = { deterministicValidation, splitFrontmatter, tesslAlignedValidation };