#!/usr/bin/env node // scripts/check/check-rtl-ratchet.mjs // RTL layout ratchet. Counts physical directional Tailwind classes in TSX. // // tests/unit/ui/rtl-logical-classes.test.tsx pins four high-impact components // and says so: "#3541 (partial, core layout)". This measures the rest, so the // remaining backlog cannot grow while it is worked through. // // Physical classes (ml/mr/pl/pr/left/right/text-left/border-l/rounded-l ...) do // not mirror under dir=rtl. Tailwind v4 logical utilities (ms/me/ps/pe/start/ // end/text-start/border-s/rounded-s) do. // // Output: rtlPhysicalClasses=N // // Advisory by default (exit 0). With --ratchet, reads // metrics.rtlPhysicalClasses.value from config/quality/quality-baseline.json and // exits 1 only when the measured count is HIGHER (direction: down). // // node scripts/check/check-rtl-ratchet.mjs // node scripts/check/check-rtl-ratchet.mjs --list # show the worst files // node scripts/check/check-rtl-ratchet.mjs --ratchet # blocking import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; const ROOT = process.cwd(); const QUIET = process.argv.includes("--quiet"); const LIST = process.argv.includes("--list"); const RATCHET = process.argv.includes("--ratchet"); const BASELINE_PATH = path.join(ROOT, "config/quality/quality-baseline.json"); const SCAN_DIRS = ["src", "electron"]; const SKIP = new Set(["node_modules", ".next", "dist", "build", "out", "coverage", ".git"]); // Physical utilities that govern placement and do not mirror under dir=rtl. const PHYSICAL = /(? 0) { perFile.push({ file: path.relative(ROOT, file), count: n }); total += n; } } } perFile.sort((a, b) => b.count - a.count); return { total, perFile }; } function main() { const { total, perFile } = measure(); console.log(`rtlPhysicalClasses=${total}`); if (LIST) { for (const { file, count } of perFile.slice(0, 25)) { console.log(` ${String(count).padStart(4)} ${file}`); } console.log(` ${perFile.length} file(s) affected`); } if (!RATCHET) return 0; let baseline; try { const json = JSON.parse(fs.readFileSync(BASELINE_PATH, "utf-8")); baseline = json?.metrics?.rtlPhysicalClasses?.value; } catch (err) { // A measurement failure must not block, only a measured regression. if (!QUIET) console.log(`rtlPhysicalClasses=SKIP reason=baseline-unreadable (${err.message})`); return 0; } if (typeof baseline !== "number") { if (!QUIET) console.log("rtlPhysicalClasses=SKIP reason=baseline-absent"); return 0; } if (total > baseline) { console.error( `RTL ratchet: ${total} physical directional classes, baseline ${baseline}. ` + `Use logical utilities (ms/me/ps/pe/start/end/text-start) so the layout ` + `mirrors under dir=rtl, or re-baseline with justification.`, ); return 1; } if (!QUIET) console.log(`rtlPhysicalClasses OK (${total} <= ${baseline})`); return 0; } // Only run when invoked directly, so countViolations can be unit tested. if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { process.exit(main()); }