Use the website language inventory when comparing README links. Accept the matching localized page while retaining checks for wrong locales, changed destinations, fragments and query parameters.
138 lines
4.7 KiB
JavaScript
138 lines
4.7 KiB
JavaScript
import { readFile, readdir, stat } from "node:fs/promises";
|
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
|
const readmeLinkTargets = (content, language = "en") =>
|
|
[...content.matchAll(/\]\(([^)\s]+)\)/gu)]
|
|
.map((match) => match[1] ?? "")
|
|
.filter(
|
|
(target) =>
|
|
!target.startsWith("#") &&
|
|
!/^README(?:_[A-Za-z0-9-]+)?\.md$/u.test(target),
|
|
)
|
|
.map((target) => {
|
|
if (language === "en" || !URL.canParse(target)) return target;
|
|
const url = new URL(target);
|
|
const prefix = `/${language}/`;
|
|
if (
|
|
url.origin !== "https://rea.tools" ||
|
|
!url.pathname.startsWith(prefix)
|
|
)
|
|
return target;
|
|
url.pathname = url.pathname.slice(prefix.length - 1);
|
|
return url.href;
|
|
})
|
|
.sort();
|
|
|
|
const requireText = (issues, path, content, expected) => {
|
|
if (!content.includes(expected)) issues.push(`${path}: missing ${expected}`);
|
|
};
|
|
|
|
/** Check that local Markdown references travel with an installed skill bundle. */
|
|
export const skillReferenceIssues = async (skillRoot) => {
|
|
const issues = [];
|
|
const paths = (await readdir(skillRoot, { recursive: true }))
|
|
.filter((path) => path.endsWith(".md"))
|
|
.sort();
|
|
for (const path of paths) {
|
|
const content = await readFile(join(skillRoot, path), "utf8");
|
|
for (const match of content.matchAll(/\[[^\]\n]*\]\(([^)\s]+)\)/gu)) {
|
|
const destination = match[1];
|
|
if (/^(?:https?:\/\/|mailto:|#)/u.test(destination)) continue;
|
|
const target = resolve(
|
|
dirname(join(skillRoot, path)),
|
|
destination.split("#")[0],
|
|
);
|
|
const withinBundle = relative(resolve(skillRoot), target);
|
|
if (
|
|
withinBundle === ".." ||
|
|
withinBundle.startsWith(`..${sep}`) ||
|
|
isAbsolute(withinBundle)
|
|
) {
|
|
issues.push(
|
|
`${path}: reference escapes installed skill bundle: ${destination}`,
|
|
);
|
|
continue;
|
|
}
|
|
try {
|
|
if (!(await stat(target)).isFile())
|
|
issues.push(`${path}: reference is not a file: ${destination}`);
|
|
} catch (cause) {
|
|
if (cause?.code !== "ENOENT") throw cause;
|
|
issues.push(`${path}: missing skill reference: ${destination}`);
|
|
}
|
|
}
|
|
}
|
|
return issues;
|
|
};
|
|
|
|
/** Return every caller-visible documentation mismatch against canonical facts. */
|
|
export const documentationFactIssues = async (root, catalog) => {
|
|
const issues = await skillReferenceIssues(
|
|
join(root, "skills/reverse-engineer-anything"),
|
|
);
|
|
const readmePaths = (await readdir(root))
|
|
.filter((path) => /^README(?:_[A-Za-z0-9-]+)?\.md$/u.test(path))
|
|
.sort();
|
|
const readmes = new Map();
|
|
for (const path of readmePaths) {
|
|
const content = await readFile(join(root, path), "utf8");
|
|
readmes.set(path, content);
|
|
requireText(issues, path, content, "MCP-tool_catalog");
|
|
if (path === "README.md") {
|
|
requireText(
|
|
issues,
|
|
path,
|
|
content,
|
|
"(docs/installation.md#supported-agents)",
|
|
);
|
|
requireText(issues, path, content, "(docs/installation.md#mcp-registry)");
|
|
requireText(
|
|
issues,
|
|
path,
|
|
content,
|
|
"(docs/mcp-contracts.md#generated-catalog)",
|
|
);
|
|
}
|
|
}
|
|
const canonicalLinks = readmeLinkTargets(readmes.get("README.md") ?? "");
|
|
const languageInventory = JSON.parse(
|
|
await readFile(join(root, "website/locales/languages.json"), "utf8"),
|
|
);
|
|
const readmeLanguages = new Map(
|
|
languageInventory.map(({ readme, code }) => [readme, code]),
|
|
);
|
|
for (const path of readmePaths.filter((path) => path !== "README.md"))
|
|
if (
|
|
JSON.stringify(
|
|
readmeLinkTargets(readmes.get(path) ?? "", readmeLanguages.get(path)),
|
|
) !== JSON.stringify(canonicalLinks)
|
|
)
|
|
issues.push(`${path}: documentation links differ from README.md`);
|
|
|
|
const installationPath = "docs/installation.md";
|
|
const installation = await readFile(join(root, installationPath), "utf8");
|
|
for (const client of catalog.setup_clients)
|
|
requireText(issues, installationPath, installation, client.display_name);
|
|
|
|
const agents = await readFile(join(root, "AGENTS.md"), "utf8");
|
|
requireText(issues, "AGENTS.md", agents, "docs/public/product-catalog.json");
|
|
|
|
const templatePath = ".github/pull_request_template.md";
|
|
const template = await readFile(join(root, templatePath), "utf8");
|
|
requireText(
|
|
issues,
|
|
templatePath,
|
|
template,
|
|
"docs/public/product-catalog.json",
|
|
);
|
|
|
|
return issues;
|
|
};
|
|
|
|
/** Fail once with all documentation fact mismatches. */
|
|
export const assertDocumentationFacts = async (root, catalog) => {
|
|
const issues = await documentationFactIssues(root, catalog);
|
|
if (issues.length > 0)
|
|
throw new Error(`Documentation facts drifted:\n- ${issues.join("\n- ")}`);
|
|
};
|