docs: the gate register, held to the effective config by a test

docs/gates.md lists every place a gate deliberately checks less than it
could, each with its reason and the count measured when the row was
written:
- an ESLint rule off, at warn, or not applied, by scope;
- a weak compiler flag, a ruff ignore, the basedpyright mode and baseline;
- a knip exclusion, a lane that does not run everywhere;
- an accepted CVE.
gates.contract.test.ts reads the EFFECTIVE configuration and fails when a
narrowing has no row:
- ESLint's computed config per scope;
- the three tsconfigs, ruff.toml, pyrightconfig, pytest.ini and knip.jsonc;
- .trivyignore.yaml, where an entry also needs a statement and an expiry;
- CI's pip-audit ignores.
It also pins `--max-warnings 0`, which is what makes a warn-level rule fail.
Seen red on a missing row, a new `off` rule and an allowlist entry without
an expiry.

CLAUDE.md gains the rule E32 exists for: a gate with any real hit is never
switched off or narrowed, and an "enforced by" comment is checked by running
the check. The gate descriptions there, in docs/development.md and in
backend/app/README.md name the lanes as they now are, and the "directly"
recipe points at the full list.
This commit is contained in:
2026-10-03 17:26:42 +02:00
parent 751e1ae5ed
commit 783f6408f9
5 changed files with 370 additions and 19 deletions
+223
View File
@@ -0,0 +1,223 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { ESLint, type Linter } from "eslint";
import prettierConfig from "eslint-config-prettier";
import ts from "typescript";
import tseslint from "typescript-eslint";
import { describe, expect, it } from "vitest";
// The gate register (docs/gates.md) is held to the EFFECTIVE configuration (E32). A narrowing that
// is not on the register fails here — a rule switched off for a scope, a weak compiler flag, a ruff
// ignore, a knip exclusion, an accepted CVE. So "the gate checks X" is a claim this test verifies,
// not one a comment makes. (The comment that claimed the React Compiler's rules "are also linted"
// stood for months while the rule that would have said so was off.)
//
// Each narrowing is a (scope, id) pair; the register must hold a table row whose first cell is the
// scope and which names the id in backticks.
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const FRONTEND = `${REPO}frontend/`;
const read = (path: string) => readFileSync(REPO + path, "utf8");
/** register scope → every backticked id on its rows. */
function register(): Map<string, Set<string>> {
const rows = new Map<string, Set<string>>();
for (const line of read("docs/gates.md").split("\n")) {
if (!line.startsWith("|") || /^\|(\s*-+\s*\|)+\s*$/.test(line)) continue; // not the |---| rule
const scope = line.split("|")[1]!.trim();
const ids = rows.get(scope) ?? new Set<string>();
for (const m of line.matchAll(/`([^`]+)`/g)) ids.add(m[1]!);
rows.set(scope, ids);
}
return rows;
}
function missingFrom(found: [scope: string, id: string][]): string[] {
const reg = register();
return found.filter(([scope, id]) => !reg.get(scope)?.has(id)).map(([s, id]) => `${s}: ${id}`);
}
const jsonc = (path: string) => {
const { config, error } = ts.parseConfigFileTextToJson(path, read(path));
if (error)
throw new Error(`${path}: ${ts.flattenDiagnosticMessageText(error.messageText, "\n")}`);
return config as Record<string, unknown>;
};
// --- ESLint ---------------------------------------------------------------------------------------
type Rules = Partial<Linter.RulesRecord>;
const severity = (v: Linter.RuleEntry | undefined): number => {
if (v === undefined) return -1; // not applied at all
const s = Array.isArray(v) ? v[0] : v;
return s === 2 || s === "error" ? 2 : s === 1 || s === "warn" ? 1 : 0;
};
// Formatting is Prettier's (its own lane): the rules eslint-config-prettier switches off are not a
// narrowing of anything.
const PRETTIER_OFF = new Set(
Object.entries(prettierConfig.rules ?? {})
.filter(([, v]) => severity(v as Linter.RuleEntry) === 0)
.map(([r]) => r),
);
// …and the core rules typescript-eslint's compatibility layer switches off for TS files because the
// type checker reports them itself (no-undef, no-redeclare, no-dupe-keys, …).
const TSC_OWNED = new Set(
Object.entries(tseslint.configs.eslintRecommended.rules ?? {})
.filter(([, v]) => severity(v as Linter.RuleEntry) === 0)
.map(([r]) => r),
);
const family = (rule: string) => (rule.includes("/") ? `${rule.split("/")[0]}/*` : rule);
const SCOPES: Record<string, string> = {
src: "src/components/Feed.tsx",
"src tests": "src/components/Feed.ytsort.dom.test.tsx",
"FocusSafeButton.tsx": "src/components/ui/FocusSafeButton.tsx",
"AnchoredPopover.tsx": "src/components/ui/AnchoredPopover.tsx",
"design-lab": "src/design-lab/batchA.tsx",
e2e: "e2e/nav.spec.ts",
"ts configs": "vite.config.ts",
"js configs": "eslint.config.js",
};
async function eslintNarrowings(): Promise<[string, string][]> {
const eslint = new ESLint({ cwd: FRONTEND });
const rules: Record<string, Rules> = {};
for (const [scope, file] of Object.entries(SCOPES))
rules[scope] = ((await eslint.calculateConfigForFile(file)) as Linter.Config).rules ?? {};
const src = rules.src!;
const ignorable = (rule: string, all: Rules) =>
PRETTIER_OFF.has(rule) ||
TSC_OWNED.has(rule) ||
// a core rule off because its typescript-eslint twin replaces it is reported as the twin
(!rule.includes("/") && `@typescript-eslint/${rule}` in all);
const found: [string, string][] = [];
// src: everything at off or warn
for (const [rule, v] of Object.entries(src))
if (severity(v) < 2 && !ignorable(rule, src)) found.push(["src", rule]);
// every other scope: what is WEAKER there than in src (a rule family not applied at all is one id)
for (const scope of Object.keys(SCOPES).filter((s) => s !== "src")) {
const ids = new Set<string>();
for (const [rule, v] of Object.entries(src)) {
if (ignorable(rule, src) || severity(v) < 2) continue;
const there = severity(rules[scope]![rule]);
if (there < 2 && !(there === 1 && severity(v) === 1))
ids.add(there < 0 ? family(rule) : rule);
}
for (const id of ids) found.push([scope, id]);
}
for (const ignored of eslintIgnores()) found.push(["everywhere", ignored]);
return found;
}
/** The global `ignores` of eslint.config.js (the first, file-less config object). */
function eslintIgnores(): string[] {
const m = /\{\s*ignores:\s*\[([^\]]*)\]\s*\}/.exec(read("frontend/eslint.config.js"));
return m ? [...m[1]!.matchAll(/"([^"]+)"/g)].map((x) => x[1]!) : [];
}
describe("the gate register (docs/gates.md)", () => {
it("lists every ESLint rule narrowed, by scope", async () => {
expect(missingFrom(await eslintNarrowings())).toEqual([]);
}, 60_000);
it("keeps --max-warnings 0, which is what makes a warn-level rule fail", () => {
const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> };
expect(pkg.scripts.lint).toMatch(/--max-warnings 0\b/);
});
// --- TypeScript ---------------------------------------------------------------------------------
const PROJECTS = [
"frontend/tsconfig.json",
"frontend/tsconfig.node.json",
"frontend/e2e/tsconfig.json",
];
const REQUIRED_ON = [
"strict",
"noUncheckedIndexedAccess",
"noUnusedLocals",
"noUnusedParameters",
"noFallthroughCasesInSwitch",
];
it.each(PROJECTS)("%s keeps the strict flags on", (path) => {
const opts = jsonc(path).compilerOptions as Record<string, unknown>;
expect(REQUIRED_ON.filter((flag) => opts[flag] !== true)).toEqual([]);
});
it("lists every weakening compiler flag", () => {
const found: [string, string][] = [];
for (const path of PROJECTS) {
const opts = jsonc(path).compilerOptions as Record<string, unknown>;
if (opts.skipLibCheck === true) found.push(["all three projects", "skipLibCheck"]);
}
expect(missingFrom(found)).toEqual([]);
});
// --- Ruff -----------------------------------------------------------------------------------------
it("lists every ruff ignore and immutable call", () => {
const toml = read("backend/ruff.toml");
const list = (key: string) => {
const m = new RegExp(`^${key}\\s*=\\s*\\[([^\\]]*)\\]`, "m").exec(toml);
return m ? [...m[1]!.matchAll(/"([^"]+)"/g)].map((x) => x[1]!) : [];
};
const found: [string, string][] = [
...list("ignore").map((code): [string, string] => ["backend + scripts", code]),
...list("extend-immutable-calls").map((c): [string, string] => ["backend + scripts", c]),
];
expect(toml).not.toMatch(/per-file-ignores/); // none today — a new one needs a row and this line
expect(missingFrom(found)).toEqual([]);
});
// --- basedpyright ---------------------------------------------------------------------------------
it("lists the type-check mode, the baseline and every rule override", () => {
const cfg = jsonc("backend/pyrightconfig.json");
const found: [string, string][] = [];
if (cfg.typeCheckingMode !== "all" && cfg.typeCheckingMode !== "strict")
found.push(["backend/app + alembic", "typeCheckingMode"]);
found.push(["backend/app + alembic", "baseline.json"]);
for (const [key, value] of Object.entries(cfg))
if (key.startsWith("report") && (value === false || value === "none" || value === "warning"))
found.push(["backend/app + alembic", key]);
expect(cfg.include).toEqual(["app", "alembic"]); // tests/ out — its row says why
expect(missingFrom(found)).toEqual([]);
});
// --- pytest ---------------------------------------------------------------------------------------
it("keeps pytest strict: no deselection, no warning filters, --strict-markers", () => {
const ini = read("backend/pytest.ini");
expect(ini).toMatch(/--strict-markers/);
expect(ini).not.toMatch(/--deselect|--ignore|-p no:|filterwarnings|\s-k\s/);
});
// --- knip -----------------------------------------------------------------------------------------
it("lists every knip production exclusion and ignore", () => {
const cfg = jsonc("frontend/knip.jsonc") as { project?: string[] } & Record<string, unknown>;
const found: [string, string][] = (cfg.project ?? [])
.filter((p) => p.startsWith("!"))
.map((p): [string, string] => ["--production", p]);
for (const key of Object.keys(cfg).filter((k) => k.startsWith("ignore")))
found.push(["knip", key]);
expect(missingFrom(found)).toEqual([]);
});
// --- accepted findings ----------------------------------------------------------------------------
it("accepts a CVE only with a statement, an expiry, and a row", () => {
const yaml = read(".trivyignore.yaml");
const entries = yaml
.split(/\n\s*- id:\s*/)
.slice(1)
.map((chunk) => ({
id: chunk.split("\n")[0]!.trim(),
statement: /\n\s*statement:\s*\S/.test(chunk),
expires: /\n\s*expired_at:\s*\d{4}-\d{2}-\d{2}/.test(chunk),
}));
expect(entries.filter((e) => !e.statement || !e.expires).map((e) => e.id)).toEqual([]);
const pipIgnores = read(".gitea/workflows/check.yml")
.split("\n")
.filter((l) => !l.trim().startsWith("#")) // the step's comment explains the flag; it is not one
.flatMap((l) => [...l.matchAll(/--ignore-vuln[ =](\S+)/g)].map((m) => m[1]!));
const found = [...entries.map((e) => e.id), ...pipIgnores].map((id): [string, string] => [
"accepted",
id,
]);
expect(missingFrom(found)).toEqual([]);
});
});