fix(docs): validate translated links to canonical product facts

This commit is contained in:
morluto committed 2026-10-09 05:15:23 +08:00
1 parent 67252b23b9
commit 34f7c4fd66
2 files changed
+68 -45

No files matched your search

+24 -29
View File
@@ -8,21 +8,14 @@ const README_PATHS = [
"README_ko.md",
"README_ar.md",
];
const tableCounts = (content, path, expectedCounts) => {
const lines = content.split(/\r?\n/u);
for (const [index, line] of lines.entries()) {
if (!/^\|\s*-/u.test(line)) continue;
const counts = [];
for (const row of lines.slice(index + 1)) {
if (!row.trim().startsWith("|")) break;
const count = Number(row.split("|")[2]?.trim());
if (!Number.isInteger(count)) break;
counts.push(count);
}
if (counts.length === expectedCounts.length) return counts;
}
throw new Error(`Missing tool-family inventory table in ${path}`);
};
const readmeLinkTargets = (content) =>
[...content.matchAll(/\]\(([^)\s]+)\)/gu)]
.map((match) => match[1] ?? "")
.filter(
(target) =>
!target.startsWith("#") && !/^README(?:_[a-z]+)?\.md$/u.test(target),
)
.sort();
const requireText = (issues, path, content, expected) => {
if (!content.includes(expected)) issues.push(`${path}: missing ${expected}`);
@@ -71,32 +64,34 @@ export const documentationFactIssues = async (root, catalog) => {
const issues = await skillReferenceIssues(
join(root, "skills/reverse-engineer-anything"),
);
const expectedCounts = catalog.tools.families.map(({ count }) => count);
const readmes = new Map();
for (const path of README_PATHS) {
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");
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)",
);
continue;
}
for (const client of catalog.setup_clients)
requireText(issues, path, content, client.display_name);
try {
const actualCounts = tableCounts(content, path, expectedCounts);
if (JSON.stringify(actualCounts) !== JSON.stringify(expectedCounts))
issues.push(
`${path}: tool family counts ${JSON.stringify(actualCounts)} do not match ${JSON.stringify(expectedCounts)}`,
);
} catch (cause) {
issues.push(cause instanceof Error ? cause.message : String(cause));
}
}
const canonicalLinks = readmeLinkTargets(readmes.get("README.md") ?? "");
for (const path of README_PATHS.slice(1))
if (
JSON.stringify(readmeLinkTargets(readmes.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");
+44 -16
View File
@@ -1,7 +1,7 @@
import { EVMOLE_PROVIDER_IDENTITY } from "../../../src/evm/EvmoleRelease.js";
import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
@@ -273,19 +273,10 @@ describe("canonical product catalog drift", () => {
]);
});
it("reports tool-family and setup-client fact drift", async () => {
it("requires setup-client facts in the canonical installation guide", async () => {
const catalog = await createProductCatalog(root);
const firstFamily = catalog.tools.families[0];
if (firstFamily === undefined) throw new TypeError("Missing tool family");
const drifted = {
...catalog,
tools: {
...catalog.tools,
families: [
{ ...firstFamily, count: firstFamily.count + 1 },
...catalog.tools.families.slice(1),
],
},
setup_clients: [
...catalog.setup_clients,
{
@@ -297,14 +288,51 @@ describe("canonical product catalog drift", () => {
],
};
const issues = await documentationFactIssues(root, drifted);
expect(issues.some((issue) => issue.includes("tool family counts"))).toBe(
true,
);
expect(issues.some((issue) => issue.includes("Future Client"))).toBe(true);
expect(issues).toContain("docs/installation.md: missing Future Client");
expect(issues).not.toContain("README.md: missing Future Client");
});
it("keeps translated readmes linked to canonical setup and tool details", async () => {
const directory = await createTestTempDirectory("rea-readme-facts-");
temporaryRoots.push(directory);
const paths = [
"README.md",
"README_zh.md",
"README_ja.md",
"README_ko.md",
"README_ar.md",
"docs/installation.md",
"AGENTS.md",
".github/pull_request_template.md",
];
for (const path of paths) {
const destination = join(directory, path);
await mkdir(dirname(destination), { recursive: true });
await copyFile(join(root, path), destination);
}
await mkdir(join(directory, "skills/reverse-engineer-anything"), {
recursive: true,
});
const translatedPath = join(directory, "README_zh.md");
const translated = await readFile(translatedPath, "utf8");
await writeFile(
translatedPath,
translated.replace(
"(docs/installation.md#supported-agents)",
"(docs/installation.md#missing-agents)",
),
"utf8",
);
const issues = await documentationFactIssues(
directory,
await createProductCatalog(root),
);
expect(issues).toContain(
"README_zh.md: documentation links differ from README.md",
);
});
it("changes the provider projection digest when provider facts drift", async () => {
const catalog = await createProductCatalog(root);
const firstProvider = catalog.providers[0];