125 lines
4.0 KiB
JavaScript
125 lines
4.0 KiB
JavaScript
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
const README_PATHS = [
|
|
"README.md",
|
|
"README_zh.md",
|
|
"README_ja.md",
|
|
"README_ko.md",
|
|
"README_ar.md",
|
|
];
|
|
const familyLabel = (id) => (id === "electron" ? "Electron" : id);
|
|
|
|
const tableCounts = (content, path, total) => {
|
|
const lines = content.split(/\r?\n/u);
|
|
const headingIndex = lines.findIndex(
|
|
(line) => line.startsWith("## ") && line.includes(String(total)),
|
|
);
|
|
if (headingIndex === -1) {
|
|
throw new Error(`Missing ${total}-tool heading in ${path}`);
|
|
}
|
|
const counts = [];
|
|
for (const line of lines.slice(headingIndex + 1)) {
|
|
if (line.startsWith("## ")) break;
|
|
if (!line.trim().startsWith("|") || /^\|\s*-/u.test(line)) continue;
|
|
const count = Number(line.split("|")[2]?.trim());
|
|
if (!Number.isInteger(count)) {
|
|
if (counts.length === 0) continue;
|
|
throw new Error(`Non-numeric tool count in ${path}`);
|
|
}
|
|
counts.push(count);
|
|
}
|
|
return counts;
|
|
};
|
|
|
|
const requireText = (issues, path, content, expected) => {
|
|
if (!content.includes(expected)) issues.push(`${path}: missing ${expected}`);
|
|
};
|
|
|
|
/** Return every caller-visible documentation mismatch against canonical facts. */
|
|
export const documentationFactIssues = async (root, catalog) => {
|
|
const issues = [];
|
|
const expectedCounts = catalog.tools.families.map(({ count }) => count);
|
|
for (const path of README_PATHS) {
|
|
const content = await readFile(join(root, path), "utf8");
|
|
requireText(
|
|
issues,
|
|
path,
|
|
content,
|
|
`MCP_tools-${String(catalog.tools.total)}`,
|
|
);
|
|
for (const client of catalog.setup_clients)
|
|
requireText(issues, path, content, client.display_name);
|
|
try {
|
|
const actualCounts = tableCounts(content, path, catalog.tools.total);
|
|
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 inventory = catalog.tools.families
|
|
.map(({ id, count }) => `${String(count)} ${familyLabel(id)}`)
|
|
.join(", ")
|
|
.replace(/, ([^,]+)$/u, ", and $1");
|
|
const plusInventory = catalog.tools.families
|
|
.map(({ id, count }) => `${String(count)} ${familyLabel(id)}`)
|
|
.join(" + ");
|
|
const agents = await readFile(join(root, "AGENTS.md"), "utf8");
|
|
requireText(issues, "AGENTS.md", agents, inventory);
|
|
requireText(
|
|
issues,
|
|
"AGENTS.md",
|
|
agents,
|
|
`(${String(catalog.tools.total)} total)`,
|
|
);
|
|
requireText(
|
|
issues,
|
|
"AGENTS.md",
|
|
agents,
|
|
`${String(catalog.tools.total)}-tool target-free MCP server`,
|
|
);
|
|
requireText(issues, "AGENTS.md", agents, "docs/product-catalog.json");
|
|
|
|
const templatePath = ".github/pull_request_template.md";
|
|
const template = await readFile(join(root, templatePath), "utf8");
|
|
requireText(
|
|
issues,
|
|
templatePath,
|
|
template,
|
|
`current inventory is ${String(catalog.tools.total)} tools (${plusInventory})`,
|
|
);
|
|
requireText(issues, templatePath, template, "docs/product-catalog.json");
|
|
|
|
const english = await readFile(join(root, "README.md"), "utf8");
|
|
const schemaVersion = (id) => {
|
|
const schema = catalog.schemas.find((candidate) => candidate.id === id);
|
|
if (schema === undefined) throw new Error(`Missing ${id} catalog schema`);
|
|
return String(schema.version);
|
|
};
|
|
requireText(
|
|
issues,
|
|
"README.md",
|
|
english,
|
|
`Evidence v${schemaVersion("evidence")}`,
|
|
);
|
|
requireText(
|
|
issues,
|
|
"README.md",
|
|
english,
|
|
`Process Capture v${schemaVersion("process_capture")}`,
|
|
);
|
|
requireText(issues, "README.md", english, "docs/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- ")}`);
|
|
};
|