scripts/docs-check.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
/**
* docs staleness check — same idea as i18n-check, for repo docs.
*
* docs/TRANSLATIONS.md records, per translation, the `git hash-object README.md`
* value the translation was written against. When README.md changes, the
* recorded hash no longer matches and this script fails with the file(s) that
* need a re-sync.
*
* Usage:
* node scripts/docs-check.js # check
* node scripts/docs-check.js --update # rewrite hashes in TRANSLATIONS.md
*/
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import fs from "node:fs";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(__dirname, "..");
const table = path.join(root, "docs", "TRANSLATIONS.md");
const update = process.argv.includes("--update");
const current = execSync("git hash-object README.md", { cwd: root })
.toString()
.trim();
let content = fs.readFileSync(table, "utf8");
const lines = content.split("\n");
const stale = [];
const out = lines.map((line) => {
const m = line.match(/^(\|[^|]+\|[^|]+\|\s*)(`[0-9a-f]{40}`)(\s*\|[^|]+\|\s*)$/);
if (!m) return line;
const recorded = m[2].slice(1, -1);
if (recorded === current) return line;
const lang = line.split("|")[1].trim();
if (update) {
stale.push(lang);
return line.replace(recorded, current);
}
stale.push(`${lang} (recorded ${recorded.slice(0, 8)}, current ${current.slice(0, 8)})`);
return line;
});
if (update) {
fs.writeFileSync(table, out.join("\n"));
console.log(stale.length ? `Updated hashes for: ${stale.join(", ")}` : "Nothing to update.");
process.exit(0);
}
// E.78 content rule: README endpoint tables use the `:id` placeholder
// convention, never the OpenAPI `{id}` template syntax (that lives only in
// the spec). A translator drifting into {id} fails here.
const readmes = fs.readdirSync(root).filter((f) => /^README(\.[^.]+)?\.md$/.test(f));
const badPlaceholders = readmes.filter((f) =>
/\{id\}/.test(fs.readFileSync(path.join(root, f), "utf8")),
);
if (badPlaceholders.length) {
console.error("OAS-style {id} placeholder leaked into README(s) — table convention is :id:");
for (const f of badPlaceholders) console.error(` - ${f}`);
process.exit(1);
}
// E.93 content rule: every translation must carry the SAME number of ```bash
// fence lines as the authoritative README.md (cookbook blocks translate their
// comments only — a dropped/added block is a re-sync bug, headings or not).
const fenceCount = (txt, lang) =>
txt.split("\n").filter((line) => line === `\`\`\`${lang}`).length;
const enFences = fenceCount(fs.readFileSync(path.join(root, "README.md"), "utf8"), "bash");
const fenceDrift = readmes
.filter((f) => f !== "README.md")
.filter((f) => fenceCount(fs.readFileSync(path.join(root, f), "utf8"), "bash") !== enFences);
if (fenceDrift.length) {
console.error(`\`\`\`bash fence count drift — README.md has ${enFences}, mismatches:`);
for (const f of fenceDrift) console.error(` - ${f}`);
process.exit(1);
}
// E.116 content rule: every jq filter in every README must address the
// success envelope roots ({ data } / { meta }) — a path like `.rows` would
// mean the documented contract drifted from apiOk's actual shape.
const jqOutsideEnvelope = [];
for (const f of readmes) {
const txt = fs.readFileSync(path.join(root, f), "utf8");
for (const m of txt.matchAll(/jq '([^']+)'/g)) {
if (!/^(\.data\b|\.meta\b)/.test(m[1].trim())) jqOutsideEnvelope.push(`${f}: ${m[1]}`);
}
}
if (jqOutsideEnvelope.length) {
console.error("jq filter outside the { data, meta } envelope root:");
for (const j of jqOutsideEnvelope) console.error(` - ${j}`);
process.exit(1);
}
if (stale.length) {
console.error("Stale translation(s) — README.md changed since they were synced:");
for (const s of stale) console.error(` - ${s}`);
console.error("\nRe-sync the translation(s), then run: npm run docs:check -- --update");
process.exit(1);
}
console.log("✓ all translations reference the current README.md");