fix(docs): validate translated links to canonical product facts
This commit is contained in:
1 parent
67252b23b9
commit
34f7c4fd66
2 files changed
+68
-45
No files matched your search
+24
-29
@@ -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");
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user