File size: 4,166 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/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();