test(gates): hold the lint and lane configs to a committed snapshot

The register test detected narrowings by modelling config shapes, and each
review round found one more shape it did not model (a basePath global ignore,
a rule src never declares, a widened glob filed under a narrow row, a substring
check on a CI line, knip.json beside knip.jsonc). It now compares instead:
gates.snapshot.json holds ESLint's effective config for every code file and the
lines that run each lane, and any difference fails until it is regenerated.
The register is read off the snapshot's configs, which surfaced the ui/ and
lib/ test scopes the sampling never reached.

The complexity ratchet also names a wrapped function by its binding
(const onSave = useCallback(() => ...)), not as "useCallback callback".
This commit is contained in:
2026-10-04 04:47:10 +02:00
parent 12a39f5291
commit 182ec31120
6 changed files with 2725 additions and 164 deletions
+284 -144
View File
@@ -1,7 +1,10 @@
import { readdirSync, readFileSync } from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createHash } from "node:crypto";
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { relative } from "node:path";
import { fileURLToPath } from "node:url";
import { ESLint, type Linter } from "eslint";
import prettierConfig from "eslint-config-prettier";
import { format, resolveConfig } from "prettier";
import { parse as parseToml } from "smol-toml";
import ts from "typescript";
import tseslint from "typescript-eslint";
@@ -25,10 +28,15 @@ 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);
// a row may cover several scopes the same way: `src tests · ui tests · lib tests`
for (const scope of line
.split("|")[1]!
.split(" · ")
.map((s) => s.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;
}
@@ -47,7 +55,6 @@ const jsonc = (path: string) => {
// --- 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;
@@ -69,57 +76,6 @@ const TSC_OWNED = new Set(
);
const family = (rule: string) => (rule.includes("/") ? `${rule.split("/")[0]}/*` : rule);
// Each scope is checked through its sample files' EFFECTIVE config (what a rule family not applied
// there means), and every config block that targets files is checked on its own (below): each of its
// globs must reach a sample, so a glob no sample reaches fails rather than passing unseen (E32 review:
// a narrowing scoped to any other file changed none of the sampled configs and needed no row; round
// 2: a block was sampled through ONE of its globs, and `reactPlugin.ts` rode along unseen).
const SCOPES: Record<string, string[]> = {
src: ["src/components/Feed.tsx"],
"src/lib": ["src/lib/format.ts"],
"lib/http.ts": ["src/lib/http.ts"],
"lib/api/core.ts": ["src/lib/api/core.ts"],
"lib/messagesSocket.ts": ["src/lib/messagesSocket.ts"],
"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", "reactPlugin.ts", "devOnlyGuard.ts"],
"js configs": ["eslint.config.js"],
};
const SAMPLES = Object.entries(SCOPES).flatMap(([scope, files]) =>
files.map((f): [scope: string, file: string] => [scope, f]),
);
// The block every src file gets: the reference a scope's re-declared `no-restricted-*` list is held to.
const SRC_GLOB = "src/**/*.{ts,tsx}";
/** A flat-config `files` glob as a regex — the shapes this config uses: `**`, `*`, `{a,b}`. */
function globRegex(glob: string): RegExp {
let out = "";
for (let i = 0; i < glob.length; i++) {
const c = glob[i]!;
if (glob.startsWith("**/", i)) {
out += "(?:.*/)?";
i += 2;
} else if (c === "*") out += "[^/]*";
else if (c === "{") {
const end = glob.indexOf("}", i);
out += `(?:${glob
.slice(i + 1, end)
.split(",")
.join("|")})`;
i = end;
} else out += /[.+?^$()|[\]\\]/.test(c) ? `\\${c}` : c;
}
return new RegExp(`^${out}$`);
}
type Block = Linter.Config & { files?: (string | string[])[]; ignores?: string[] };
const blocks = async (): Promise<Block[]> =>
((await import(pathToFileURL(`${FRONTEND}eslint.config.js`).href)) as { default: Block[] })
.default;
// A rule option whose key says it lets something through (`allowShortCircuit`,
// `allowForKnownSafeCalls`, `ignoreRestSiblings`, `exceptAfterSingleLine`) narrows the rule, and a
// `no-restricted-*` list re-declared for a scope narrows it by every entry it leaves out.
@@ -143,101 +99,275 @@ function restrictedEntries(entry: Linter.RuleEntry | undefined): Set<string> {
return new Set(flat.map((e) => JSON.stringify(e)));
}
async function eslintNarrowings(): Promise<[string, string][]> {
// --- the snapshot ---------------------------------------------------------------------------------
// The configs that can narrow a gate in more shapes than a check can enumerate are held to a
// COMMITTED SNAPSHOT (E32 review): ESLint's effective config for every file `eslint .` reaches, and
// the lines that run the lanes. Any change fails until the snapshot is regenerated, so a narrowing
// arrives as a diff of `gates.snapshot.json` in review instead of slipping past a detector. (Three
// review rounds kept finding shapes the detector this replaced did not model — a `basePath` global
// ignore, a widened glob filed under a narrow row, a substring check on a CI line. A comparison
// against everything has no shape to miss.)
//
// Regenerate: GATE_SNAPSHOT=update npx vitest run src/lib/gates.contract.test.ts
// then read the diff like any gate change. An ESLint config that changed keeps its scope name when
// most of its files had it; one no name fits is written as `new config N` — rename it to the
// register scope it is (the register test below then asks for its rows).
const SNAPSHOT = "frontend/src/lib/gates.snapshot.json";
const NOT_LINTED = "(not linted)";
type EslintConfig = {
rules: Record<string, unknown[]>;
linterOptions: Record<string, unknown>;
languageOptions: Record<string, unknown>;
plugins: string[];
settings: unknown;
};
type Snapshot = {
eslint: { configs: Record<string, EslintConfig>; files: Record<string, string[]> };
lanes: Record<string, unknown>;
};
const sortKeys = (v: unknown): unknown =>
Array.isArray(v)
? v.map(sortKeys)
: v !== null && typeof v === "object"
? Object.fromEntries(
Object.keys(v)
.sort()
.map((k) => [k, sortKeys((v as Record<string, unknown>)[k])]),
)
: v;
// through JSON, so a value compares as what the file can hold (a RegExp option is `{}` on both sides)
const canonical = <T>(v: T): T => JSON.parse(JSON.stringify(sortKeys(v) ?? null)) as T;
function normalize(cfg: Linter.Config): EslintConfig {
const lang = { ...(cfg.languageOptions ?? {}) } as Record<string, unknown>;
const parser = lang.parser as { meta?: { name?: string } } | undefined;
const parserOptions = { ...(lang.parserOptions as Record<string, unknown> | undefined) };
if (typeof parserOptions.tsconfigRootDir === "string")
// absolute: differs per machine
parserOptions.tsconfigRootDir = relative(FRONTEND, parserOptions.tsconfigRootDir) || ".";
// ~1200 browser globals per config: held by count and digest, not listed
const globals = Object.entries((lang.globals ?? {}) as Record<string, unknown>)
.map(([name, v]) => `${name}:${String(v)}`)
.sort();
const digest = createHash("sha1").update(globals.join("\n")).digest("hex").slice(0, 12);
return canonical({
languageOptions: {
...lang,
parser: parser ? (parser.meta?.name ?? "(unnamed parser)") : null,
parserOptions,
globals: `${globals.length} names, sha1 ${digest}`,
},
linterOptions: (cfg.linterOptions ?? {}) as Record<string, unknown>,
plugins: Object.keys(cfg.plugins ?? {}).sort(),
settings: cfg.settings ?? null,
rules: Object.fromEntries(
Object.entries(cfg.rules ?? {}).map(([rule, v]) => [
rule,
Array.isArray(v) ? [severity(v), ...v.slice(1)] : [severity(v)],
]),
),
});
}
// Every code file `eslint .` walks into. The skipped directories are the global ignores' generated
// output (the register's `everywhere` row) — untracked, and present on one machine but not another.
// Every other file is asked for its config, so a file a new ignore drops lands in `(not linted)`.
const WALK_SKIP = new Set([
"node_modules",
"dist",
"playwright-report",
"test-results",
"e2e/.auth",
]);
const CODE = /[.](?:[mc]?[jt]s|[jt]sx)$/;
function codeFiles(dir = ""): string[] {
return readdirSync(FRONTEND + dir, { withFileTypes: true }).flatMap((d) => {
const path = dir + d.name;
if (d.isDirectory()) return WALK_SKIP.has(path) ? [] : codeFiles(`${path}/`);
return CODE.test(d.name) ? [path] : [];
});
}
// On disk every config but src is written as its difference from src — the rules it sets otherwise
// and the src rules it does not apply — so a scope reads like its register rows, and one rule set
// is not repeated fourteen times. In memory it is always whole.
type Stored = Omit<EslintConfig, "rules"> & {
rules: Record<string, unknown[]>;
notApplied?: string[];
};
function toStored(configs: Record<string, EslintConfig>): Record<string, Stored> {
const src = configs.src?.rules;
if (!src) return configs;
return Object.fromEntries(
Object.entries(configs).map(([name, cfg]) => {
if (name === "src") return [name, cfg];
const rules = Object.fromEntries(
Object.entries(cfg.rules).filter(([r, v]) => JSON.stringify(v) !== JSON.stringify(src[r])),
);
return [
name,
{ ...cfg, rules, notApplied: Object.keys(src).filter((r) => !(r in cfg.rules)) },
];
}),
);
}
function fromStored(stored: Record<string, Stored>): Record<string, EslintConfig> {
const src = stored.src?.rules ?? {};
return Object.fromEntries(
Object.entries(stored).map(([name, { notApplied, ...cfg }]) => {
if (!notApplied) return [name, cfg];
const rules = { ...src, ...cfg.rules };
for (const r of notApplied) delete rules[r];
return [name, canonical({ ...cfg, rules })];
}),
);
}
const committed = (): Snapshot => {
const s = JSON.parse(read(SNAPSHOT)) as Snapshot & {
eslint: { configs: Record<string, Stored> };
};
return canonical({ ...s, eslint: { ...s.eslint, configs: fromStored(s.eslint.configs) } });
};
async function eslintActual(names: Snapshot["eslint"]): Promise<Snapshot["eslint"]> {
const eslint = new ESLint({ cwd: FRONTEND });
const found: [string, string][] = [];
const rules: [scope: string, rules: Rules][] = [];
for (const [scope, file] of SAMPLES) {
const cfg = (await eslint.calculateConfigForFile(file)) as Linter.Config | undefined;
if (!cfg) {
found.push([scope, `${file} is ignored`]); // ignored files get no config at all
const groups = new Map<string, { config: EslintConfig; files: string[] }>();
const notLinted: string[] = [];
for (const file of codeFiles().sort()) {
const raw = (await eslint.calculateConfigForFile(file)) as Linter.Config | undefined;
if (!raw) {
notLinted.push(file);
continue;
}
rules.push([scope, cfg.rules ?? {}]);
// an unused `eslint-disable` is an error on every file, or a stale suppression stays armed
if (severity(cfg.linterOptions?.reportUnusedDisableDirectives as Linter.RuleEntry) !== 2)
found.push([scope, "reportUnusedDisableDirectives"]);
const config = normalize(raw);
const key = JSON.stringify(config);
const group = groups.get(key) ?? { config, files: [] };
group.files.push(file);
groups.set(key, group);
}
const src = rules.find(([scope]) => scope === "src")![1];
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);
// src: everything at off or warn, and every rule whose options let something through
for (const [rule, v] of Object.entries(src)) {
if (ignorable(rule, src)) continue;
if (severity(v) < 2 || (Array.isArray(v) && relaxes(v.slice(1)))) found.push(["src", rule]);
// a config is named by the snapshot entry it equals; a changed one by the old name most of its
// files had (if still free), so the diff shows the rule that moved, not a renamed scope
const byContent = new Map(Object.entries(names.configs).map(([n, c]) => [JSON.stringify(c), n]));
const oldName = new Map(
Object.entries(names.files).flatMap(([n, files]) => files.map((f) => [f, n] as const)),
);
const configs: Record<string, EslintConfig> = {};
const files: Record<string, string[]> = { [NOT_LINTED]: notLinted };
const unnamed: { config: EslintConfig; files: string[] }[] = [];
for (const [key, group] of groups) {
const name = byContent.get(key);
if (name) [configs[name], files[name]] = [group.config, group.files];
else unnamed.push(group);
}
// every other sample: what is WEAKER there than in src (a rule family not applied at all is one
// id). A src rule at warn counts too: switched off for a scope, it is narrowed there.
for (const [scope, theirs] of rules.filter(([s]) => s !== "src")) {
const ids = new Set<string>();
for (const [rule, v] of Object.entries(src)) {
if (ignorable(rule, src) || severity(v) === 0) continue;
const there = severity(theirs[rule]);
if (there < severity(v)) ids.add(there < 0 ? family(rule) : rule);
unnamed.forEach((group, i) => {
const votes = new Map<string, number>();
for (const f of group.files) {
const n = oldName.get(f);
if (n && n !== NOT_LINTED) votes.set(n, (votes.get(n) ?? 0) + 1);
}
const best = [...votes].sort((a, b) => b[1] - a[1])[0]?.[0];
const name = best && !(best in configs) ? best : `new config ${i + 1}`;
[configs[name], files[name]] = [group.config, group.files];
});
return canonical({ configs, files });
}
// the lines that run each lane, comments and blank lines aside
function meaningful(path: string): string[] {
return read(path)
.split("\n")
.map((l) => l.trimEnd())
.filter((l) => {
const t = l.trim();
return t !== "" && !/^(#|\/\/|\/\*|\*)/.test(t);
});
}
const lanes = () => ({
".gitea/workflows/check.yml": meaningful(".gitea/workflows/check.yml"),
"backend/docker-compose.test.yml": meaningful("backend/docker-compose.test.yml"),
"backend/Dockerfile.test": meaningful("backend/Dockerfile.test"),
"frontend/package.json scripts": (
JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> }
).scripts,
"frontend/vitest.config.ts": meaningful("frontend/vitest.config.ts"),
"frontend/.prettierignore": meaningful("frontend/.prettierignore"),
});
let actual: Promise<Snapshot> | undefined;
const current = () =>
(actual ??= (async () =>
canonical({ eslint: await eslintActual(committed().eslint), lanes: lanes() }))());
// --- the register, read off the snapshot ------------------------------------------------------------
const ignorable = (rule: string, all: Record<string, unknown>) =>
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 entry = (v: unknown[] | undefined) => v as Linter.RuleEntry | undefined;
/** Every narrowing in the effective ESLint configs, as (scope, id). */
function eslintNarrowings({ configs, files }: Snapshot["eslint"]): [string, string][] {
const found: [string, string][] = files[NOT_LINTED]!.map((f) => [
"everywhere",
`${f} — not linted`,
]);
const src = configs.src?.rules;
if (!src) return [...found, ["src", "(no config named src)"]];
for (const [scope, { rules, linterOptions }] of Object.entries(configs)) {
// an unused `eslint-disable` is an error on every file, or a stale suppression stays armed
if (severity(linterOptions.reportUnusedDisableDirectives as Linter.RuleEntry) !== 2)
found.push([scope, "reportUnusedDisableDirectives"]);
const ids = new Set<string>();
for (const [rule, v] of Object.entries(rules)) {
if (ignorable(rule, rules)) continue;
const mine = entry(v)!;
const theirs = scope === "src" ? undefined : entry(src[rule]);
const relaxed = relaxes(v.slice(1)) && JSON.stringify(v) !== JSON.stringify(src[rule]);
if (theirs === undefined) {
// src, or a rule src does not have (the test and e2e plugins): at off or warn, or relaxed
if (severity(mine) < 2 || (scope === "src" ? relaxes(v.slice(1)) : relaxed)) ids.add(rule);
} else if (severity(mine) < severity(theirs)) ids.add(rule);
else if (rule.includes("no-restricted-")) {
const own = restrictedEntries(mine);
if ([...restrictedEntries(theirs)].some((e) => !own.has(e))) ids.add(rule);
} else if (relaxed) ids.add(rule);
}
// a src rule not applied at all here: one id for its whole family
if (scope !== "src")
for (const [rule, v] of Object.entries(src))
if (!(rule in rules) && !ignorable(rule, src) && severity(entry(v)) > 0)
ids.add(family(rule));
for (const id of ids) found.push([scope, id]);
}
// every config block: a global `ignores` is a narrowing everywhere; one that targets files must
// reach a sample through EACH glob, and what it declares is held to src on its own — a later block
// that switches a rule back on for the sample would otherwise hide where the first one is off
const all = await blocks();
const isSrcBlock = (b: Block) => b.files?.flat().join() === SRC_GLOB;
const reference = new Map<string, Set<string>>(); // the FIRST src-block declaration of each list
for (const block of all.filter(isSrcBlock))
for (const [rule, v] of Object.entries(block.rules ?? {}))
if (rule.includes("no-restricted-") && !reference.has(rule))
reference.set(rule, restrictedEntries(v));
let globalIgnores = 0;
for (const block of all) {
// ESLint reads a block as GLOBAL ignores when `ignores` is its only key besides `name`
if (
!block.files &&
Object.keys(block)
.filter((k) => k !== "name")
.join() === "ignores"
) {
globalIgnores++;
for (const ignored of block.ignores ?? []) found.push(["everywhere", ignored]);
continue;
}
for (const [key, v] of Object.entries(block.linterOptions ?? {}))
if (key !== "reportUnusedDisableDirectives" || severity(v as Linter.RuleEntry) !== 2)
found.push(["linterOptions", key]);
if (!block.files) continue;
let scope: string | undefined;
for (const glob of block.files.flat()) {
const hit = SAMPLES.find(([, file]) => globRegex(glob).test(file));
if (!hit) found.push(["unsampled block", glob]);
scope ??= hit?.[0];
}
if (!scope) continue;
if (block.ignores?.length) found.push([scope, `ignores: ${block.ignores.join(", ")}`]);
for (const [rule, v] of Object.entries(block.rules ?? {})) {
if (ignorable(rule, src)) continue;
if (rule.includes("no-restricted-")) {
const ref = reference.get(rule);
const own = restrictedEntries(v);
if (ref && [...ref].some((e) => !own.has(e))) found.push([scope, rule]);
} else if (severity(v) < severity(src[rule]) || (Array.isArray(v) && relaxes(v.slice(1))))
found.push([scope, rule]);
}
}
// the detection above must still see the block every config has, or `everywhere` is never checked
if (globalIgnores === 0) found.push(["everywhere", "(no global ignores block found)"]);
return found;
}
describe("the gate snapshot (gates.snapshot.json)", () => {
it("holds the effective ESLint config of every file and the lines that run each lane", async () => {
const now = await current();
if (process.env.GATE_SNAPSHOT === "update") {
const path = REPO + SNAPSHOT;
const options = { ...(await resolveConfig(path)), filepath: path };
const stored = { ...now, eslint: { ...now.eslint, configs: toStored(now.eslint.configs) } };
writeFileSync(path, await format(JSON.stringify(sortKeys(stored)), options));
return;
}
expect(now).toEqual(committed());
}, 120_000);
});
describe("the gate register (docs/gates.md)", () => {
it("lists every ESLint rule narrowed, by scope", async () => {
expect(missingFrom(await eslintNarrowings())).toEqual([]);
}, 60_000);
expect(missingFrom(eslintNarrowings((await current()).eslint))).toEqual([]);
}, 120_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/);
expect(pkg.scripts.lint).toBe("eslint . --max-warnings 0");
});
// --- TypeScript ---------------------------------------------------------------------------------
@@ -355,15 +485,20 @@ describe("the gate register (docs/gates.md)", () => {
});
// Every ruff lane passes `--config backend/ruff.toml`, which makes ruff ignore any other config
// file it would discover (measured: a nested `ruff.toml` selecting the `D` rules changed nothing).
// So that flag is what makes this file the whole configuration, and it is held here.
it("runs every CI ruff step on backend/ruff.toml", () => {
// So that flag is what makes this file the whole configuration. The snapshot holds every CI line;
// these two are spelled out here because the reason is (round 3: a substring check let an extra
// `--config "lint.extend-ignore=…"` override through).
it("runs every CI ruff step on backend/ruff.toml and nothing else", () => {
const steps = read(".gitea/workflows/check.yml")
.split("\n")
.filter((l) => /python -m ruff /.test(l));
expect(steps.length).toBe(2);
expect(steps.filter((l) => !l.includes("--config backend/ruff.toml backend scripts"))).toEqual(
[],
);
.filter((l) => /\bruff\b/.test(l) && !l.trim().startsWith("#"))
.map((l) => l.trim());
expect(steps).toEqual([
"- name: ruff",
"run: python -m ruff check --config backend/ruff.toml backend scripts",
"- name: ruff format --check",
"run: python -m ruff format --check --config backend/ruff.toml backend scripts",
]);
});
// --- basedpyright ---------------------------------------------------------------------------------
@@ -423,6 +558,11 @@ describe("the gate register (docs/gates.md)", () => {
expect(pkg.scripts.knip).toBe("knip && knip --production");
// the positive pattern is the project itself: narrowing it drops files from both modes
expect((cfg.project ?? []).filter((p) => !p.startsWith("!"))).toEqual(["**/*.{ts,tsx,js}!"]);
// and knip.jsonc is the config knip reads: a knip.json (read first) or a package.json `knip` key
// (merged in) would narrow it unseen (E32 review round 3)
const KNIP_FILES = /^\.?knip(\.config)?\.(json|jsonc|js|ts|mjs|cjs|mts|cts|toml|ya?ml)$/;
expect(readdirSync(FRONTEND).filter((f) => KNIP_FILES.test(f))).toEqual(["knip.jsonc"]);
expect("knip" in pkg).toBe(false);
});
// --- accepted findings ----------------------------------------------------------------------------