Files
2026-07-13 13:39:12 +08:00

131 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
// Detects hardcoded old versions / stale dates in docs that should follow the current release.
// Uses hardcoded regexes to avoid dynamic RegExp() (ReDoS concern flagged by semgrep).
// Exits 0 if clean, 1 (in --strict mode) if drift detected.
//
// Run: node scripts/check/check-deprecated-versions.mjs
// Strict: node scripts/check/check-deprecated-versions.mjs --strict
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..", "..");
const DOCS_DIR = path.join(ROOT, "docs");
const PKG_JSON = path.join(ROOT, "package.json");
const STRICT = process.argv.includes("--strict");
function currentVersion() {
try {
return JSON.parse(fs.readFileSync(PKG_JSON, "utf8")).version || "0.0.0";
} catch {
return "0.0.0";
}
}
// Hardcoded set of obviously-stale version patterns. Update when bumping major/minor.
// These match any version <= v3.6.x or any pre-3 major.
const STALE_VERSION_PATTERNS = [
/\bv?[12]\.\d+\.\d+\b/, // 1.x.x / 2.x.x
/\bv?3\.[0-6]\.\d+\b/, // 3.0.x..3.6.x
];
const SAFE_CONTEXTS =
/(historical|archive|legacy|previously|deprecated|since|introduced|was|originally|fix|fixed)/i;
// Dates older than 60 days are considered stale for "Last updated" / "Last consolidated".
const STALE_DATE_DAYS = 60;
const today = new Date();
const LAST_UPDATED_RE = /Last (?:updated|consolidated|generated)[^\d]{0,30}(\d{4}-\d{2}-\d{2})/i;
function isStaleDate(yyyy_mm_dd) {
const d = new Date(yyyy_mm_dd);
if (Number.isNaN(d.getTime())) return false;
const ageDays = (today - d) / (1000 * 60 * 60 * 24);
return ageDays > STALE_DATE_DAYS;
}
const IGNORED_DIRS = new Set(["archive", "i18n", "superpowers"]);
const IGNORED_BASENAMES = new Set(["CHANGELOG.md", "RFC-AUTO-ASSESSMENT-DRAFT.md"]);
function walkDocs(dir) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (IGNORED_DIRS.has(entry.name)) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...walkDocs(full));
} else if (entry.isFile() && entry.name.endsWith(".md")) {
if (IGNORED_BASENAMES.has(entry.name)) continue;
out.push(full);
}
}
return out;
}
function main() {
const cur = currentVersion();
console.log(`Version drift report (current: v${cur})`);
console.log("=".repeat(40));
const files = walkDocs(DOCS_DIR);
let drift = 0;
for (const file of files) {
const rel = path.relative(ROOT, file);
const txt = fs.readFileSync(file, "utf8");
const lines = txt.split("\n");
const issues = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.length > 500) continue; // skip very long lines (likely code blocks / data)
// 1. Stale version refs
for (const re of STALE_VERSION_PATTERNS) {
const m = re.exec(line);
if (m && !SAFE_CONTEXTS.test(line)) {
issues.push({
line: i + 1,
type: "stale-version",
match: m[0],
text: line.trim().slice(0, 100),
});
break;
}
}
// 2. Stale "Last updated/consolidated/generated" dates
const dateMatch = LAST_UPDATED_RE.exec(line);
if (dateMatch && isStaleDate(dateMatch[1])) {
issues.push({
line: i + 1,
type: "stale-date",
match: dateMatch[1],
text: line.trim().slice(0, 100),
});
}
}
if (issues.length > 0) {
console.log(`\n ${rel}`);
for (const iss of issues.slice(0, 5)) {
console.log(` L${iss.line} [${iss.type}] ${iss.match}: ${iss.text}`);
}
if (issues.length > 5) console.log(` ... and ${issues.length - 5} more`);
drift += issues.length;
}
}
console.log();
if (drift > 0) {
console.warn(`⚠ ${drift} potential drift(s) detected across ${files.length} doc files.`);
if (STRICT) process.exit(1);
} else {
console.log(`✓ No drift detected across ${files.length} doc files.`);
}
}
main();