- {/* biome-ignore lint/a11y/noStaticElementInteractions: prevent close when clicking modal content */}
- {/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: prevent close when clicking modal content */}
-
e.stopPropagation()}
- >
-
-
- Player Characters
-
-
+ )}
+
);
}
diff --git a/package.json b/package.json
index 733e502..d3a3a77 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"knip": "knip",
"jscpd": "jscpd",
"oxlint": "oxlint --tsconfig apps/web/tsconfig.json --type-aware",
- "check": "pnpm audit --audit-level=high && knip && biome check . && oxlint --tsconfig apps/web/tsconfig.json --type-aware && tsc --build && vitest run && jscpd"
+ "check:ignores": "node scripts/check-lint-ignores.mjs",
+ "check": "pnpm audit --audit-level=high && knip && biome check . && oxlint --tsconfig apps/web/tsconfig.json --type-aware && node scripts/check-lint-ignores.mjs && tsc --build && vitest run && jscpd"
}
}
diff --git a/scripts/check-lint-ignores.mjs b/scripts/check-lint-ignores.mjs
new file mode 100644
index 0000000..8e3edb9
--- /dev/null
+++ b/scripts/check-lint-ignores.mjs
@@ -0,0 +1,108 @@
+/**
+ * Backpressure check for biome-ignore comments.
+ *
+ * 1. Ratcheting cap — source and test files have separate max counts.
+ * Lower these numbers as you fix ignores; they can never go up silently.
+ * 2. Banned rules — ignoring certain rule categories is never allowed.
+ * 3. Justification — every ignore must have a non-empty explanation after
+ * the rule name.
+ */
+
+import { execSync } from "node:child_process";
+import { readFileSync } from "node:fs";
+
+// ── Configuration ──────────────────────────────────────────────────────
+const MAX_SOURCE_IGNORES = 12;
+const MAX_TEST_IGNORES = 3;
+
+/** Rule prefixes that must never be suppressed. */
+const BANNED_PREFIXES = [
+ "lint/security/",
+ "lint/correctness/noGlobalObjectCalls",
+ "lint/correctness/noUnsafeFinally",
+];
+// ───────────────────────────────────────────────────────────────────────
+
+const IGNORE_PATTERN = /biome-ignore\s+([\w/]+)(?::\s*(.*))?/;
+
+function findFiles() {
+ return execSync("git ls-files -- '*.ts' '*.tsx'", { encoding: "utf-8" })
+ .trim()
+ .split("\n")
+ .filter(Boolean);
+}
+
+function isTestFile(path) {
+ return (
+ path.includes("__tests__/") ||
+ path.endsWith(".test.ts") ||
+ path.endsWith(".test.tsx")
+ );
+}
+
+let errors = 0;
+let sourceCount = 0;
+let testCount = 0;
+
+for (const file of findFiles()) {
+ const lines = readFileSync(file, "utf-8").split("\n");
+
+ for (let i = 0; i < lines.length; i++) {
+ const match = lines[i].match(IGNORE_PATTERN);
+ if (!match) continue;
+
+ const rule = match[1];
+ const justification = (match[2] ?? "").trim();
+ const loc = `${file}:${i + 1}`;
+
+ // Count by category
+ if (isTestFile(file)) {
+ testCount++;
+ } else {
+ sourceCount++;
+ }
+
+ // Banned rules
+ for (const prefix of BANNED_PREFIXES) {
+ if (rule.startsWith(prefix)) {
+ console.error(`BANNED: ${loc} — ${rule} must not be suppressed`);
+ errors++;
+ }
+ }
+
+ // Justification required
+ if (!justification) {
+ console.error(
+ `MISSING JUSTIFICATION: ${loc} — biome-ignore ${rule} needs an explanation after the colon`,
+ );
+ errors++;
+ }
+ }
+}
+
+// Ratcheting caps
+if (sourceCount > MAX_SOURCE_IGNORES) {
+ console.error(
+ `SOURCE CAP EXCEEDED: ${sourceCount} biome-ignore comments in source (max ${MAX_SOURCE_IGNORES}). Fix issues and lower the cap.`,
+ );
+ errors++;
+}
+
+if (testCount > MAX_TEST_IGNORES) {
+ console.error(
+ `TEST CAP EXCEEDED: ${testCount} biome-ignore comments in tests (max ${MAX_TEST_IGNORES}). Fix issues and lower the cap.`,
+ );
+ errors++;
+}
+
+// Summary
+console.log(
+ `biome-ignore: ${sourceCount} source (max ${MAX_SOURCE_IGNORES}), ${testCount} test (max ${MAX_TEST_IGNORES})`,
+);
+
+if (errors > 0) {
+ console.error(`\n${errors} problem(s) found.`);
+ process.exit(1);
+} else {
+ console.log("All checks passed.");
+}