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:
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user